Merge "Document ambiguous camera state errors" into androidx-main
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
index 79ad758..2b2530f 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
@@ -33,7 +33,6 @@
 import androidx.test.rule.GrantPermissionRule
 import androidx.test.uiautomator.UiDevice
 import androidx.testutils.withActivity
-import kotlinx.coroutines.delay
 import kotlinx.coroutines.runBlocking
 import org.junit.After
 import org.junit.AfterClass
@@ -158,11 +157,6 @@
                 Espresso.onView(ViewMatchers.withId(R.id.direction_toggle))
                     .perform(ViewActions.click())
 
-                // TODO(b/159257773): Currently have no reliable way of checking that camera has
-                //  switched. Delay to ensure previous camera has stopped streaming and the
-                //  idling resource actually is becoming idle due to frames from front camera.
-                delay(500)
-
                 // Check front camera is now idle
                 withActivity { resetViewIdlingResource() }
                 waitForViewfinderIdle()
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index 0b6db82..6c2f515 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -81,6 +81,7 @@
 import androidx.camera.lifecycle.ProcessCameraProvider;
 import androidx.core.content.ContextCompat;
 import androidx.core.math.MathUtils;
+import androidx.core.util.Consumer;
 import androidx.lifecycle.MutableLiveData;
 import androidx.lifecycle.ViewModelProvider;
 import androidx.test.espresso.IdlingResource;
@@ -214,6 +215,20 @@
     private CompoundButton.OnCheckedChangeListener mOnCheckedChangeListener =
             (compoundButton, isChecked) -> tryBindUseCases();
 
+    private Consumer<Long> mFrameUpdateListener = timestamp -> {
+        if (mPreviewFrameCount.getAndIncrement() >= FRAMES_UNTIL_VIEW_IS_READY) {
+            try {
+                if (!this.mViewIdlingResource.isIdleNow()) {
+                    Log.d(TAG, FRAMES_UNTIL_VIEW_IS_READY + " or more counted on preview."
+                            + " Make IdlingResource idle.");
+                    this.mViewIdlingResource.decrement();
+                }
+            } catch (IllegalStateException e) {
+                Log.e(TAG, "Unexpected decrement. Continuing");
+            }
+        }
+    };
+
     // Espresso testing variables
     private final CountingIdlingResource mViewIdlingResource = new CountingIdlingResource("view");
     private static final int FRAMES_UNTIL_VIEW_IS_READY = 5;
@@ -605,21 +620,6 @@
                 Objects.requireNonNull((DisplayManager) getSystemService(Context.DISPLAY_SERVICE));
         dpyMgr.registerDisplayListener(mDisplayListener, new Handler(Looper.getMainLooper()));
 
-        previewRenderer.setFrameUpdateListener(ContextCompat.getMainExecutor(this), timestamp -> {
-            // Wait until surface texture receives enough updates. This is for testing.
-            if (mPreviewFrameCount.getAndIncrement() >= FRAMES_UNTIL_VIEW_IS_READY) {
-                try {
-                    if (!mViewIdlingResource.isIdleNow()) {
-                        Log.d(TAG, FRAMES_UNTIL_VIEW_IS_READY + " or more counted on preview."
-                                + " Make IdlingResource idle.");
-                        mViewIdlingResource.decrement();
-                    }
-                } catch (IllegalStateException e) {
-                    Log.e(TAG, "Unexpected decrement. Continuing");
-                }
-            }
-        });
-
         StrictMode.VmPolicy vmPolicy =
                 new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build();
         StrictMode.setVmPolicy(vmPolicy);
@@ -691,6 +691,9 @@
             // next thing being ready.
             return;
         }
+        // Clear listening frame update before unbind all.
+        mPreviewRenderer.clearFrameUpdateListener();
+
         mCameraProvider.unbindAll();
         try {
             List<UseCase> useCases = buildUseCases();
@@ -725,7 +728,14 @@
                     .setTargetName("Preview")
                     .build();
             resetViewIdlingResource();
-            mPreviewRenderer.attachInputPreview(preview);
+            // Use the listener of the future to make sure the Preview setup the new surface.
+            mPreviewRenderer.attachInputPreview(preview).addListener(() -> {
+                Log.d(TAG, "OpenGLRenderer get the new surface for the Preview");
+                mPreviewRenderer.setFrameUpdateListener(
+                        ContextCompat.getMainExecutor(this), mFrameUpdateListener
+                );
+            }, ContextCompat.getMainExecutor(this));
+
             useCases.add(preview);
         }
 
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLActivity.java
index 6e5feda..fe77d0b 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLActivity.java
@@ -236,8 +236,9 @@
         // with ConstraintLayout).
         Preview preview = new Preview.Builder().setTargetAspectRatio(AspectRatio.RATIO_4_3).build();
 
-        mRenderer.attachInputPreview(preview);
-
+        mRenderer.attachInputPreview(preview).addListener(() -> {
+            Log.d(TAG, "OpenGLRenderer get the new surface for the Preview");
+        }, ContextCompat.getMainExecutor(this));
         CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
 
         mCameraProvider.bindToLifecycle(this, cameraSelector, preview);
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLRenderer.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLRenderer.java
index 50646a9..6793c95 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLRenderer.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLRenderer.java
@@ -101,47 +101,63 @@
         mExecutor.execute(() -> mNativeContext = initContext());
     }
 
+    /**
+     * Attach the Preview to the renderer.
+     *
+     * @param preview Preview use-case used in the renderer.
+     * @return A {@link ListenableFuture} that signals the new surface is ready to be used in the
+     * renderer for the input Preview use-case.
+     */
     @OptIn(markerClass = ExperimentalUseCaseGroup.class)
     @MainThread
-    void attachInputPreview(@NonNull Preview preview) {
-        preview.setSurfaceProvider(
-                mExecutor,
-                surfaceRequest -> {
-                    if (mIsShutdown) {
-                        surfaceRequest.willNotProvideSurface();
-                        return;
-                    }
-                    SurfaceTexture surfaceTexture = resetPreviewTexture(
-                            surfaceRequest.getResolution());
-                    Surface inputSurface = new Surface(surfaceTexture);
-                    mNumOutstandingSurfaces++;
-                    surfaceRequest.setTransformationInfoListener(
-                            mExecutor,
-                            transformationInfo -> {
-                                mMvpDirty = true;
-                                // TODO(b/159127941): add the rotation to MVP transformation.
-                                if (!isCropRectFullTexture(transformationInfo.getCropRect())) {
-                                    // Crop rect is pre-calculated. Use it directly.
-                                    mPreviewCropRect = new RectF(
-                                            transformationInfo.getCropRect());
-                                } else {
-                                    // Crop rect needs to be calculated before drawing.
-                                    mPreviewCropRect = null;
-                                }
-                            });
-                    surfaceRequest.provideSurface(
-                            inputSurface,
-                            mExecutor,
-                            result -> {
-                                inputSurface.release();
-                                surfaceTexture.release();
-                                if (surfaceTexture == mPreviewTexture) {
-                                    mPreviewTexture = null;
-                                }
-                                mNumOutstandingSurfaces--;
-                                doShutdownExecutorIfNeeded();
-                            });
-                });
+    @SuppressWarnings("ObjectToString")
+    @NonNull
+    ListenableFuture<Void> attachInputPreview(@NonNull Preview preview) {
+        return CallbackToFutureAdapter.getFuture(completer -> {
+            preview.setSurfaceProvider(
+                    mExecutor,
+                    surfaceRequest -> {
+                        if (mIsShutdown) {
+                            surfaceRequest.willNotProvideSurface();
+                            return;
+                        }
+                        SurfaceTexture surfaceTexture = resetPreviewTexture(
+                                surfaceRequest.getResolution());
+                        Surface inputSurface = new Surface(surfaceTexture);
+                        mNumOutstandingSurfaces++;
+
+                        surfaceRequest.setTransformationInfoListener(
+                                mExecutor,
+                                transformationInfo -> {
+                                    mMvpDirty = true;
+                                    if (!isCropRectFullTexture(transformationInfo.getCropRect())) {
+                                        // Crop rect is pre-calculated. Use it directly.
+                                        mPreviewCropRect = new RectF(
+                                                transformationInfo.getCropRect());
+                                    } else {
+                                        // Crop rect needs to be calculated before drawing.
+                                        mPreviewCropRect = null;
+                                    }
+                                });
+
+                        surfaceRequest.provideSurface(
+                                inputSurface,
+                                mExecutor,
+                                result -> {
+                                    inputSurface.release();
+                                    surfaceTexture.release();
+                                    if (surfaceTexture == mPreviewTexture) {
+                                        mPreviewTexture = null;
+                                    }
+                                    mNumOutstandingSurfaces--;
+                                    doShutdownExecutorIfNeeded();
+                                });
+                        // Make sure the renderer use the new surface for the input Preview.
+                        completer.set(null);
+
+                    });
+            return "attachInputPreview [" + this + "]";
+        });
     }
 
     void attachOutputSurface(
@@ -187,6 +203,14 @@
         }
     }
 
+    void clearFrameUpdateListener() {
+        try {
+            mExecutor.execute(() -> mFrameUpdateListener = null);
+        } catch (RejectedExecutionException e) {
+            // Renderer is shutting down. Ignore.
+        }
+    }
+
     void invalidateSurface(int surfaceRotationDegrees) {
         try {
             mExecutor.execute(
diff --git a/car/app/app-samples/navigation/automotive/github_build.gradle b/car/app/app-samples/navigation/automotive/github_build.gradle
index c4a6f6a..7f7f843 100644
--- a/car/app/app-samples/navigation/automotive/github_build.gradle
+++ b/car/app/app-samples/navigation/automotive/github_build.gradle
@@ -21,7 +21,7 @@
 
     defaultConfig {
         applicationId "androidx.car.app.sample.navigation"
-        minSdkVersion 23
+        minSdkVersion 29
         targetSdkVersion 29
         versionCode 1
         versionName "1.0"
diff --git a/car/app/app-samples/navigation/common/github_build.gradle b/car/app/app-samples/navigation/common/github_build.gradle
index 24cd50d..97e6f97 100644
--- a/car/app/app-samples/navigation/common/github_build.gradle
+++ b/car/app/app-samples/navigation/common/github_build.gradle
@@ -25,6 +25,10 @@
         versionCode 1
         versionName "1.0"
     }
+    compileOptions {
+        targetCompatibility = JavaVersion.VERSION_1_8
+        sourceCompatibility = JavaVersion.VERSION_1_8
+    }
 }
 
 dependencies {
@@ -32,4 +36,5 @@
     implementation "androidx.core:core:1.5.0-alpha01"
 
     implementation "androidx.car.app:app:1.0.0-rc01"
+    implementation "androidx.annotation:annotation-experimental:1.0.0"
 }
diff --git a/car/app/app-samples/places/common/github_build.gradle b/car/app/app-samples/places/common/github_build.gradle
index 093e8ef..8fc79fb 100644
--- a/car/app/app-samples/places/common/github_build.gradle
+++ b/car/app/app-samples/places/common/github_build.gradle
@@ -24,6 +24,10 @@
         versionCode 1
         versionName "1.0"
     }
+    compileOptions {
+        targetCompatibility = JavaVersion.VERSION_1_8
+        sourceCompatibility = JavaVersion.VERSION_1_8
+    }
 }
 
 dependencies {
diff --git a/car/app/app-samples/showcase/common/github_build.gradle b/car/app/app-samples/showcase/common/github_build.gradle
index 6ba37a2..e9be321 100644
--- a/car/app/app-samples/showcase/common/github_build.gradle
+++ b/car/app/app-samples/showcase/common/github_build.gradle
@@ -17,19 +17,24 @@
 apply plugin: 'com.android.library'
 
 android {
-    compileSdkVersion 30
+    compileSdkVersion 29
 
     defaultConfig {
-        minSdkVersion 29
-        targetSdkVersion 30
+        minSdkVersion 23
+        targetSdkVersion 29
         versionCode 1
         versionName "1.0"
     }
+
+    compileOptions {
+        targetCompatibility = JavaVersion.VERSION_1_8
+        sourceCompatibility = JavaVersion.VERSION_1_8
+    }
 }
 
 dependencies {
     implementation "androidx.core:core:1.6.0-alpha01"
     implementation "androidx.activity:activity:1.2.2"
     implementation "androidx.car.app:app:1.0.0-rc01"
-    implementation(project(":annotation:annotation-experimental"))
+    implementation "androidx.annotation:annotation-experimental:1.0.0"
 }
diff --git a/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml b/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml
index df5857e..8b58ce6 100644
--- a/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml
@@ -58,20 +58,6 @@
           android:value="androidx.car.app.samples.showcase.Showcase" />
     </service>
 
-    <activity-alias
-        android:enabled="true"
-        android:exported="true"
-        android:label="Showcase"
-        android:name=".Showcase"
-        android:targetActivity="androidx.car.app.activity.CarAppActivity">
-      <intent-filter>
-        <action android:name="android.intent.action.MAIN" />
-        <category android:name="android.intent.category.LAUNCHER" />
-      </intent-filter>
-      <meta-data android:name="androidx.car.app.CAR_APP_SERVICE"
-          android:value="androidx.car.app.samples.showcase.ShowcaseService" />
-    </activity-alias>
-
     <service
         android:name=".common.navigation.NavigationNotificationService"
         android:exported="true">
diff --git a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt
index 2f4a022..dfed0118 100644
--- a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt
+++ b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt
@@ -115,14 +115,6 @@
             // with 0 parameters, but one that has a receiver scope (SomeScope.() -> Unit).
             if (functionType != argumentType) return
 
-            // Unfortunately if the types come from a separate module, we don't have access to
-            // the type information in the function / argument, so instead we just get an error
-            // type. If both compare equally, and they are reporting an error type, we cannot do
-            // anything about this so just skip warning. This will only happen if there _are_
-            // types, i.e a scoped / parameterized function type, so it's rare enough that it
-            // shouldn't matter that much in practice.
-            if (functionType.reference.canonicalText.contains(NonExistentClass)) return
-
             val expectedComposable = node.isComposable
 
             // Hack to get the psi of the lambda declaration / source. The !!s here probably
@@ -148,9 +140,7 @@
     }
 
     companion object {
-        private const val NonExistentClass = "error.NonExistentClass"
-
-        private const val explanation =
+        private const val Explanation =
             "Creating this extra lambda instead of just passing the already captured lambda means" +
                 " that during code generation the Compose compiler will insert code around " +
                 "this lambda to track invalidations. This adds some extra runtime cost so you" +
@@ -159,7 +149,7 @@
         val ISSUE = Issue.create(
             "UnnecessaryLambdaCreation",
             "Creating an unnecessary lambda to emit a captured lambda",
-            explanation,
+            Explanation,
             Category.PERFORMANCE, 5, Severity.ERROR,
             Implementation(
                 UnnecessaryLambdaCreationDetector::class.java,
diff --git a/compose/material/material-lint/src/main/java/androidx/compose/material/lint/ColorsDetector.kt b/compose/material/material-lint/src/main/java/androidx/compose/material/lint/ColorsDetector.kt
index 27efac9..d76a86f 100644
--- a/compose/material/material-lint/src/main/java/androidx/compose/material/lint/ColorsDetector.kt
+++ b/compose/material/material-lint/src/main/java/androidx/compose/material/lint/ColorsDetector.kt
@@ -30,11 +30,16 @@
 import com.android.tools.lint.detector.api.Scope
 import com.android.tools.lint.detector.api.Severity
 import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.android.tools.lint.detector.api.UastLintUtils
 import com.intellij.psi.PsiParameter
+import com.intellij.psi.PsiVariable
 import org.jetbrains.kotlin.asJava.elements.KtLightElement
 import org.jetbrains.kotlin.psi.KtParameter
 import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
 import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.toUElement
+import org.jetbrains.uast.tryResolve
 import org.jetbrains.uast.util.isConstructorCall
 import java.util.EnumSet
 
@@ -56,10 +61,10 @@
             if (node.isConstructorCall()) {
                 if (method.containingClass?.name != Colors.shortName) return
             } else {
-                // Functions with inline class parameters have their names mangled, so we use
-                // startsWith instead of comparing the full name.
-                if (!method.name.startsWith(LightColors.shortName) &&
-                    !method.name.startsWith(DarkColors.shortName)
+                if (
+                    node.methodName != null &&
+                    node.methodName != LightColors.shortName &&
+                    node.methodName != DarkColors.shortName
                 ) return
             }
 
@@ -179,21 +184,43 @@
      * file - so we can't resolve what it is.
      */
     val sourceText: String? by lazy {
-        val argumentText = argument?.sourcePsi?.text
-        when {
+        val sourceExpression: UElement? = when {
             // An argument was provided
-            argumentText != null -> argumentText
+            argument != null -> argument
             // A default value exists (so !! is safe), and we are browsing Kotlin source
             // Note: this should be is KtLightParameter, but this was changed from an interface
             // to a class, so we get an IncompatibleClassChangeError.
             // TODO: change to KtParameter when we upgrade the min lint version we compile against
             //  b/182832722
             parameter is KtLightElement<*, *> -> {
-                (parameter.kotlinOrigin!! as KtParameter).defaultValue!!.text
+                (parameter.kotlinOrigin!! as KtParameter).defaultValue.toUElement()
             }
             // A default value exists, but it is in a class file so we can't access it anymore
             else -> null
         }
+
+        sourceExpression?.resolveToDeclarationText()
+    }
+
+    /**
+     * Returns a string that matches the original declaration for this UElement. If this is a
+     * literal or a reference to something out of scope (such as parameter), this will just be
+     * that text. If this is a reference to a variable, this will try and find the text of the
+     * variable, the last time it was assigned.
+     */
+    private fun UElement.resolveToDeclarationText(): String? {
+        // Get the source psi and go back to a UElement since if the declaration is a property, it
+        // will actually be represented as a method (since in Kotlin properties are just getter
+        // methods by default). Going to the source then back again gives us the actual UField.
+        // This might be fixed in later versions of UAST, but not on the current version we run
+        // tests against
+        val resolved = tryResolve()?.toUElement()?.sourcePsi.toUElement()
+        return if (resolved is PsiVariable) {
+            val declaration = UastLintUtils.findLastAssignment(resolved, this)
+            declaration?.resolveToDeclarationText() ?: sourcePsi?.text
+        } else {
+            sourcePsi?.text
+        }
     }
 }
 
diff --git a/compose/material/material-lint/src/test/java/androidx/compose/material/lint/ColorsDetectorTest.kt b/compose/material/material-lint/src/test/java/androidx/compose/material/lint/ColorsDetectorTest.kt
index 27e545d..6e5dada 100644
--- a/compose/material/material-lint/src/test/java/androidx/compose/material/lint/ColorsDetectorTest.kt
+++ b/compose/material/material-lint/src/test/java/androidx/compose/material/lint/ColorsDetectorTest.kt
@@ -334,6 +334,54 @@
     }
 
     @Test
+    fun trackVariableAssignment() {
+        lint().files(
+            kotlin(
+                """
+                package androidx.compose.material.foo
+
+                import androidx.compose.material.*
+                import androidx.compose.ui.graphics.*
+
+                val testColor1 = Color.Black
+
+                fun test() {
+                    val colors = lightColors(
+                        primary = Color.Green,
+                        background = Color.Green,
+                        onPrimary = testColor1,
+                        onBackground = Color.Black,
+                    )
+
+                    val testColor2 = Color.Black
+
+                    val colors2 = lightColors(
+                        primary = Color.Green,
+                        background = Color.Green,
+                        onPrimary = testColor2,
+                        onBackground = Color.Black,
+                    )
+
+                    var testColor3 = Color.Green
+                    testColor3 = Color.Black
+
+                    val colors2 = lightColors(
+                        primary = Color.Green,
+                        background = Color.Green,
+                        onPrimary = testColor3,
+                        onBackground = Color.Black,
+                    )
+                }
+            """
+            ),
+            ColorStub,
+            ColorsStub
+        )
+            .run()
+            .expectClean()
+    }
+
+    @Test
     fun noErrors() {
         lint().files(
             kotlin(
diff --git a/wear/wear-complications-data/api/current.txt b/wear/wear-complications-data/api/current.txt
index 81e1229..2031367 100644
--- a/wear/wear-complications-data/api/current.txt
+++ b/wear/wear-complications-data/api/current.txt
@@ -164,7 +164,7 @@
   public final class ProviderInfoRetriever implements java.lang.AutoCloseable {
     ctor public ProviderInfoRetriever(android.content.Context context);
     method public void close();
-    method @RequiresApi(android.os.Build.VERSION_CODES.R) @kotlin.jvm.Throws(exceptionClasses=ServiceDisconnectedException::class) public suspend Object? requestPreviewComplicationData(android.content.ComponentName providerComponent, androidx.wear.complications.data.ComplicationType complicationType, kotlin.coroutines.Continuation<? super androidx.wear.complications.data.ComplicationData> p) throws androidx.wear.complications.ProviderInfoRetriever.ServiceDisconnectedException;
+    method @RequiresApi(android.os.Build.VERSION_CODES.R) @kotlin.jvm.Throws(exceptionClasses=ServiceDisconnectedException::class) public suspend Object? retrievePreviewComplicationData(android.content.ComponentName providerComponent, androidx.wear.complications.data.ComplicationType complicationType, kotlin.coroutines.Continuation<? super androidx.wear.complications.data.ComplicationData> p) throws androidx.wear.complications.ProviderInfoRetriever.ServiceDisconnectedException;
     method @kotlin.jvm.Throws(exceptionClasses=ServiceDisconnectedException::class) public suspend Object? retrieveProviderInfo(android.content.ComponentName watchFaceComponent, int[] watchFaceComplicationIds, kotlin.coroutines.Continuation<? super androidx.wear.complications.ProviderInfoRetriever.ProviderInfo[]> p) throws androidx.wear.complications.ProviderInfoRetriever.ServiceDisconnectedException;
   }
 
diff --git a/wear/wear-complications-data/api/public_plus_experimental_current.txt b/wear/wear-complications-data/api/public_plus_experimental_current.txt
index 8829abf..8f1e849 100644
--- a/wear/wear-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/wear-complications-data/api/public_plus_experimental_current.txt
@@ -164,7 +164,7 @@
   public final class ProviderInfoRetriever implements java.lang.AutoCloseable {
     ctor public ProviderInfoRetriever(android.content.Context context);
     method public void close();
-    method @RequiresApi(android.os.Build.VERSION_CODES.R) @kotlin.jvm.Throws(exceptionClasses=ServiceDisconnectedException::class) public suspend Object? requestPreviewComplicationData(android.content.ComponentName providerComponent, androidx.wear.complications.data.ComplicationType complicationType, kotlin.coroutines.Continuation<? super androidx.wear.complications.data.ComplicationData> p) throws androidx.wear.complications.ProviderInfoRetriever.ServiceDisconnectedException;
+    method @RequiresApi(android.os.Build.VERSION_CODES.R) @kotlin.jvm.Throws(exceptionClasses=ServiceDisconnectedException::class) public suspend Object? retrievePreviewComplicationData(android.content.ComponentName providerComponent, androidx.wear.complications.data.ComplicationType complicationType, kotlin.coroutines.Continuation<? super androidx.wear.complications.data.ComplicationData> p) throws androidx.wear.complications.ProviderInfoRetriever.ServiceDisconnectedException;
     method @kotlin.jvm.Throws(exceptionClasses=ServiceDisconnectedException::class) public suspend Object? retrieveProviderInfo(android.content.ComponentName watchFaceComponent, int[] watchFaceComplicationIds, kotlin.coroutines.Continuation<? super androidx.wear.complications.ProviderInfoRetriever.ProviderInfo[]> p) throws androidx.wear.complications.ProviderInfoRetriever.ServiceDisconnectedException;
   }
 
diff --git a/wear/wear-complications-data/api/restricted_current.txt b/wear/wear-complications-data/api/restricted_current.txt
index 2c16c7d..da29a6f 100644
--- a/wear/wear-complications-data/api/restricted_current.txt
+++ b/wear/wear-complications-data/api/restricted_current.txt
@@ -205,7 +205,7 @@
   public final class ProviderInfoRetriever implements java.lang.AutoCloseable {
     ctor public ProviderInfoRetriever(android.content.Context context);
     method public void close();
-    method @RequiresApi(android.os.Build.VERSION_CODES.R) @kotlin.jvm.Throws(exceptionClasses=ServiceDisconnectedException::class) public suspend Object? requestPreviewComplicationData(android.content.ComponentName providerComponent, androidx.wear.complications.data.ComplicationType complicationType, kotlin.coroutines.Continuation<? super androidx.wear.complications.data.ComplicationData> p) throws androidx.wear.complications.ProviderInfoRetriever.ServiceDisconnectedException;
+    method @RequiresApi(android.os.Build.VERSION_CODES.R) @kotlin.jvm.Throws(exceptionClasses=ServiceDisconnectedException::class) public suspend Object? retrievePreviewComplicationData(android.content.ComponentName providerComponent, androidx.wear.complications.data.ComplicationType complicationType, kotlin.coroutines.Continuation<? super androidx.wear.complications.data.ComplicationData> p) throws androidx.wear.complications.ProviderInfoRetriever.ServiceDisconnectedException;
     method @kotlin.jvm.Throws(exceptionClasses=ServiceDisconnectedException::class) public suspend Object? retrieveProviderInfo(android.content.ComponentName watchFaceComponent, int[] watchFaceComplicationIds, kotlin.coroutines.Continuation<? super androidx.wear.complications.ProviderInfoRetriever.ProviderInfo[]> p) throws androidx.wear.complications.ProviderInfoRetriever.ServiceDisconnectedException;
   }
 
diff --git a/wear/wear-complications-data/src/main/java/androidx/wear/complications/ProviderInfoRetriever.kt b/wear/wear-complications-data/src/main/java/androidx/wear/complications/ProviderInfoRetriever.kt
index 70dea0a..c714b42 100644
--- a/wear/wear-complications-data/src/main/java/androidx/wear/complications/ProviderInfoRetriever.kt
+++ b/wear/wear-complications-data/src/main/java/androidx/wear/complications/ProviderInfoRetriever.kt
@@ -135,7 +135,7 @@
      */
     @Throws(ServiceDisconnectedException::class)
     @RequiresApi(Build.VERSION_CODES.R)
-    public suspend fun requestPreviewComplicationData(
+    public suspend fun retrievePreviewComplicationData(
         providerComponent: ComponentName,
         complicationType: ComplicationType
     ): ComplicationData? = TraceEvent(
diff --git a/wear/wear-complications-data/src/test/java/androidx/wear/complications/ProviderInfoRetrieverTest.kt b/wear/wear-complications-data/src/test/java/androidx/wear/complications/ProviderInfoRetrieverTest.kt
index 6c7fac6..94803bb 100644
--- a/wear/wear-complications-data/src/test/java/androidx/wear/complications/ProviderInfoRetrieverTest.kt
+++ b/wear/wear-complications-data/src/test/java/androidx/wear/complications/ProviderInfoRetrieverTest.kt
@@ -80,7 +80,7 @@
             )
 
             val previewData =
-                providerInfoRetriever.requestPreviewComplicationData(component, type)!!
+                providerInfoRetriever.retrievePreviewComplicationData(component, type)!!
             assertThat(previewData.type).isEqualTo(type)
             assertThat(
                 (previewData as LongTextComplicationData).text.getTextAt(
@@ -108,7 +108,7 @@
                 any()
             )
 
-            assertThat(providerInfoRetriever.requestPreviewComplicationData(component, type))
+            assertThat(providerInfoRetriever.retrievePreviewComplicationData(component, type))
                 .isNull()
         }
     }
@@ -121,7 +121,7 @@
             Mockito.`when`(mockService.apiVersion).thenReturn(0)
             Mockito.`when`(mockService.asBinder()).thenReturn(mockBinder)
 
-            assertThat(providerInfoRetriever.requestPreviewComplicationData(component, type))
+            assertThat(providerInfoRetriever.retrievePreviewComplicationData(component, type))
                 .isNull()
         }
     }
@@ -141,7 +141,7 @@
                 any()
             )
 
-            assertThat(providerInfoRetriever.requestPreviewComplicationData(component, type))
+            assertThat(providerInfoRetriever.retrievePreviewComplicationData(component, type))
                 .isNull()
         }
     }
diff --git a/wear/wear-watchface-client/build.gradle b/wear/wear-watchface-client/build.gradle
index dc89440..3c29c46 100644
--- a/wear/wear-watchface-client/build.gradle
+++ b/wear/wear-watchface-client/build.gradle
@@ -46,6 +46,12 @@
     androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
     androidTestImplementation(TRUTH)
 
+    testImplementation(ANDROIDX_TEST_EXT_JUNIT)
+    testImplementation(ANDROIDX_TEST_CORE)
+    testImplementation(ANDROIDX_TEST_RULES)
+    testImplementation(ROBOLECTRIC)
+    testImplementation(TRUTH)
+
     implementation("androidx.core:core:1.1.0")
 }
 
diff --git a/wear/wear-watchface-client/src/test/java/androidx/wear/watchface/client/WatchFaceIdTest.kt b/wear/wear-watchface-client/src/test/java/androidx/wear/watchface/client/WatchFaceIdTest.kt
new file mode 100644
index 0000000..25cd2ec
--- /dev/null
+++ b/wear/wear-watchface-client/src/test/java/androidx/wear/watchface/client/WatchFaceIdTest.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.client
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.internal.bytecode.InstrumentationConfiguration
+import org.junit.runner.RunWith
+import org.junit.runners.model.FrameworkMethod
+
+// Without this we get test failures with an error:
+// "failed to access class kotlin.jvm.internal.DefaultConstructorMarker".
+public class EditorTestRunner(testClass: Class<*>) : RobolectricTestRunner(testClass) {
+    override fun createClassLoaderConfig(method: FrameworkMethod): InstrumentationConfiguration =
+        InstrumentationConfiguration.Builder(
+            super.createClassLoaderConfig(method)
+        )
+            .doNotInstrumentPackage("androidx.wear.watchface.client")
+            .build()
+}
+
+@RunWith(EditorTestRunner::class)
+public class WatchFaceIdTest {
+    @Test
+    public fun watchFaceId_equals() {
+        val a1 = WatchFaceId("A")
+        val a2 = WatchFaceId("A")
+        val b1 = WatchFaceId("B")
+
+        assertThat(a1).isEqualTo(a1)
+        assertThat(a1).isEqualTo(a2)
+        assertThat(a1).isNotEqualTo(b1)
+        assertThat(a1).isNotEqualTo(false)
+    }
+
+    @Test
+    public fun watchFaceId_hashCode() {
+        val a1 = WatchFaceId("A")
+        val a2 = WatchFaceId("A")
+        val b1 = WatchFaceId("B")
+
+        assertThat(a1.hashCode()).isEqualTo(a2.hashCode())
+        assertThat(a1.hashCode()).isNotEqualTo(b1.hashCode())
+    }
+}
\ No newline at end of file
diff --git a/wear/wear-watchface-client/src/test/resources/robolectric.properties b/wear/wear-watchface-client/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..ce87047
--- /dev/null
+++ b/wear/wear-watchface-client/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# Robolectric currently doesn't support API 30, so we have to explicitly specify 29 as the target
+# sdk for now. Remove when no longer necessary.
+sdk=29
diff --git a/wear/wear-watchface-complications-rendering/api/current.txt b/wear/wear-watchface-complications-rendering/api/current.txt
index 1e136b6..bd8e23b 100644
--- a/wear/wear-watchface-complications-rendering/api/current.txt
+++ b/wear/wear-watchface-complications-rendering/api/current.txt
@@ -13,6 +13,7 @@
     method public long getCurrentTimeMillis();
     method public static androidx.wear.watchface.complications.rendering.ComplicationDrawable? getDrawable(android.content.Context, int);
     method public long getHighlightDuration();
+    method public CharSequence? getNoDataText();
     method @Deprecated public int getOpacity();
     method public boolean isBurnInProtectionOn();
     method public boolean isHighlighted();
diff --git a/wear/wear-watchface-complications-rendering/api/public_plus_experimental_current.txt b/wear/wear-watchface-complications-rendering/api/public_plus_experimental_current.txt
index 1e136b6..bd8e23b 100644
--- a/wear/wear-watchface-complications-rendering/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-complications-rendering/api/public_plus_experimental_current.txt
@@ -13,6 +13,7 @@
     method public long getCurrentTimeMillis();
     method public static androidx.wear.watchface.complications.rendering.ComplicationDrawable? getDrawable(android.content.Context, int);
     method public long getHighlightDuration();
+    method public CharSequence? getNoDataText();
     method @Deprecated public int getOpacity();
     method public boolean isBurnInProtectionOn();
     method public boolean isHighlighted();
diff --git a/wear/wear-watchface-complications-rendering/api/restricted_current.txt b/wear/wear-watchface-complications-rendering/api/restricted_current.txt
index 3fc7fc4..25d5b4f 100644
--- a/wear/wear-watchface-complications-rendering/api/restricted_current.txt
+++ b/wear/wear-watchface-complications-rendering/api/restricted_current.txt
@@ -13,6 +13,7 @@
     method public long getCurrentTimeMillis();
     method public static androidx.wear.watchface.complications.rendering.ComplicationDrawable? getDrawable(android.content.Context, int);
     method public long getHighlightDuration();
+    method public CharSequence? getNoDataText();
     method @Deprecated public int getOpacity();
     method public boolean isBurnInProtectionOn();
     method public boolean isHighlighted();
diff --git a/wear/wear-watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationDrawable.java b/wear/wear-watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationDrawable.java
index f9b8b7e..449cad4 100644
--- a/wear/wear-watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationDrawable.java
+++ b/wear/wear-watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationDrawable.java
@@ -848,8 +848,11 @@
         return mComplicationRenderer;
     }
 
+    /**
+     * Returns the text to be rendered when {@link ComplicationData} is of type {@link
+     * ComplicationData#TYPE_NO_DATA}.
+     */
     @Nullable
-    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
     public CharSequence getNoDataText() {
         return mNoDataText;
     }
diff --git a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/StyleConfigFragment.kt b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/StyleConfigFragment.kt
index e3b783f..c6140e0 100644
--- a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/StyleConfigFragment.kt
+++ b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/StyleConfigFragment.kt
@@ -34,9 +34,13 @@
 import androidx.wear.watchface.style.UserStyleSchema
 import androidx.wear.watchface.style.UserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting.BooleanOption
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption
 import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting
 import androidx.wear.watchface.style.UserStyleData
 import androidx.wear.watchface.style.data.UserStyleSchemaWireFormat
 import androidx.wear.watchface.style.data.UserStyleWireFormat
@@ -86,28 +90,17 @@
             inflater.inflate(R.layout.style_options_layout, container, false)
                 as SwipeDismissFrameLayout
 
-        val styleOptions = styleSetting.options
-        val booleanUserStyleSetting =
-            styleOptions.filterIsInstance<BooleanUserStyleSetting.BooleanOption>()
-        val listUserStyleSetting =
-            styleOptions.filterIsInstance<ListUserStyleSetting.ListOption>()
-        val complicationsUserStyleSetting =
-            styleOptions.filterIsInstance<ComplicationsUserStyleSetting.ComplicationsOption>()
-        val rangeUserStyleSetting =
-            styleOptions.filterIsInstance<DoubleRangeUserStyleSetting.DoubleRangeOption>()
-
         val booleanStyle = view.findViewById<ToggleButton>(R.id.styleToggle)
         val styleOptionsList = view.findViewById<WearableRecyclerView>(R.id.styleOptionsList)
         val rangedStyle = view.findViewById<SeekBar>(R.id.styleRange)
 
-        when {
-            booleanUserStyleSetting.isNotEmpty() -> {
-                booleanStyle.isChecked = userStyle[styleSetting]?.toBooleanOption()!!.value
+        val userStyleOption = userStyle[styleSetting]!!
+        when (styleSetting) {
+            is BooleanUserStyleSetting -> {
+                booleanStyle.isChecked = (userStyleOption as BooleanOption).value
                 booleanStyle.setOnCheckedChangeListener { _, isChecked ->
                     setUserStyleOption(
-                        styleSetting.getOptionForId(
-                            BooleanUserStyleSetting.BooleanOption(isChecked).id.value
-                        )
+                        styleSetting.getOptionForId(BooleanOption(isChecked).id.value)
                     )
                 }
                 styleOptionsList.visibility = View.GONE
@@ -116,13 +109,13 @@
                 rangedStyle.isEnabled = false
             }
 
-            listUserStyleSetting.isNotEmpty() -> {
+            is ListUserStyleSetting -> {
                 booleanStyle.isEnabled = false
                 booleanStyle.visibility = View.GONE
                 styleOptionsList.adapter =
                     ListStyleSettingViewAdapter(
                         requireContext(),
-                        listUserStyleSetting,
+                        styleSetting.options.filterIsInstance<ListUserStyleSetting.ListOption>(),
                         this@StyleConfigFragment
                     )
                 styleOptionsList.layoutManager = WearableLinearLayoutManager(context)
@@ -130,13 +123,14 @@
                 rangedStyle.visibility = View.GONE
             }
 
-            complicationsUserStyleSetting.isNotEmpty() -> {
+            is ComplicationsUserStyleSetting -> {
                 booleanStyle.isEnabled = false
                 booleanStyle.visibility = View.GONE
                 styleOptionsList.adapter =
                     ComplicationsStyleSettingViewAdapter(
                         requireContext(),
-                        complicationsUserStyleSetting,
+                        styleSetting.options
+                            .filterIsInstance<ComplicationsUserStyleSetting.ComplicationsOption>(),
                         this@StyleConfigFragment
                     )
                 styleOptionsList.layoutManager = WearableLinearLayoutManager(context)
@@ -144,20 +138,18 @@
                 rangedStyle.visibility = View.GONE
             }
 
-            rangeUserStyleSetting.isNotEmpty() -> {
+            is CustomValueUserStyleSetting -> {
+                // TODO(alexclarke): Implement.
+            }
+
+            is DoubleRangeUserStyleSetting -> {
                 val rangedStyleSetting = styleSetting as DoubleRangeUserStyleSetting
                 val minValue =
-                    (
-                        rangedStyleSetting.options.first() as
-                            DoubleRangeUserStyleSetting.DoubleRangeOption
-                        ).value
+                    (rangedStyleSetting.options.first() as DoubleRangeOption).value
                 val maxValue =
-                    (
-                        rangedStyleSetting.options.last() as
-                            DoubleRangeUserStyleSetting.DoubleRangeOption
-                        ).value
+                    (rangedStyleSetting.options.last() as DoubleRangeOption).value
                 val delta = (maxValue - minValue) / 100.0f
-                val value = userStyle[styleSetting]!!.toDoubleRangeOption()!!.value.toFloat()
+                val value = (userStyleOption as DoubleRangeOption).value.toFloat()
                 rangedStyle.progress = ((value - minValue) / delta).toInt()
                 rangedStyle.setOnSeekBarChangeListener(
                     object : SeekBar.OnSeekBarChangeListener {
@@ -168,9 +160,8 @@
                         ) {
                             setUserStyleOption(
                                 rangedStyleSetting.getOptionForId(
-                                    DoubleRangeUserStyleSetting.DoubleRangeOption(
-                                        minValue + delta * progress.toFloat()
-                                    ).id.value
+                                    DoubleRangeOption(minValue + delta * progress.toFloat())
+                                        .id.value
                                 )
                             )
                         }
@@ -185,6 +176,10 @@
                 styleOptionsList.isEnabled = false
                 styleOptionsList.visibility = View.GONE
             }
+
+            is LongRangeUserStyleSetting -> {
+                // TODO(alexclarke): Implement.
+            }
         }
 
         view.addCallback(object : SwipeDismissFrameLayout.Callback() {
@@ -196,7 +191,7 @@
         return view
     }
 
-    internal fun readOptionsFromArguments() {
+    private fun readOptionsFromArguments() {
         settingId = requireArguments().getCharSequence(SETTING_ID).toString()
 
         styleSchema = UserStyleSchema(
diff --git a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
index 65863bd..7e2dffe4 100644
--- a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
+++ b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
@@ -374,7 +374,7 @@
         // Fetch preview ComplicationData if possible.
         return providerInfo.providerComponentName?.let {
             try {
-                providerInfoRetriever.requestPreviewComplicationData(
+                providerInfoRetriever.retrievePreviewComplicationData(
                     it,
                     ComplicationType.fromWireType(providerInfo.complicationType)
                 )
diff --git a/wear/wear-watchface-style/api/current.txt b/wear/wear-watchface-style/api/current.txt
index c63feab..ff75d4a 100644
--- a/wear/wear-watchface-style/api/current.txt
+++ b/wear/wear-watchface-style/api/current.txt
@@ -55,6 +55,7 @@
     method public androidx.wear.watchface.style.UserStyleSetting.Option getOptionForId(byte[] optionId);
     method public final java.util.List<androidx.wear.watchface.style.UserStyleSetting.Option> getOptions();
     property public final java.util.Collection<androidx.wear.watchface.style.Layer> affectedLayers;
+    property public final androidx.wear.watchface.style.UserStyleSetting.Option defaultOption;
     property public final int defaultOptionIndex;
     property public final CharSequence description;
     property public final CharSequence displayName;
@@ -125,6 +126,9 @@
     method public double getDefaultValue();
     method public double getMaximumValue();
     method public double getMinimumValue();
+    property public final double defaultValue;
+    property public final double maximumValue;
+    property public final double minimumValue;
   }
 
   public static final class UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
@@ -162,6 +166,9 @@
     method public long getDefaultValue();
     method public long getMaximumValue();
     method public long getMinimumValue();
+    property public final long defaultValue;
+    property public final long maximumValue;
+    property public final long minimumValue;
   }
 
   public static final class UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
@@ -173,12 +180,6 @@
   public abstract static class UserStyleSetting.Option {
     ctor public UserStyleSetting.Option(androidx.wear.watchface.style.UserStyleSetting.Option.Id id);
     method public final androidx.wear.watchface.style.UserStyleSetting.Option.Id getId();
-    method public final androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting.BooleanOption? toBooleanOption();
-    method public final androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption? toComplicationsOption();
-    method public final androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption? toCustomValueOption();
-    method public final androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption? toDoubleRangeOption();
-    method public final androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption? toListOption();
-    method public final androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption? toLongRangeOption();
     property public final androidx.wear.watchface.style.UserStyleSetting.Option.Id id;
     field public static final androidx.wear.watchface.style.UserStyleSetting.Option.Companion Companion;
   }
diff --git a/wear/wear-watchface-style/api/public_plus_experimental_current.txt b/wear/wear-watchface-style/api/public_plus_experimental_current.txt
index bd690a3..7bc6a09 100644
--- a/wear/wear-watchface-style/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-style/api/public_plus_experimental_current.txt
@@ -55,6 +55,7 @@
     method public androidx.wear.watchface.style.UserStyleSetting.Option getOptionForId(byte[] optionId);
     method public final java.util.List<androidx.wear.watchface.style.UserStyleSetting.Option> getOptions();
     property public final java.util.Collection<androidx.wear.watchface.style.Layer> affectedLayers;
+    property public final androidx.wear.watchface.style.UserStyleSetting.Option defaultOption;
     property public final int defaultOptionIndex;
     property public final CharSequence description;
     property public final CharSequence displayName;
@@ -132,6 +133,9 @@
     method public double getMaximumValue();
     method public double getMinimumValue();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.DoubleRangeUserStyleSettingWireFormat toWireFormat();
+    property public final double defaultValue;
+    property public final double maximumValue;
+    property public final double minimumValue;
   }
 
   public static final class UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
@@ -173,6 +177,9 @@
     method public long getMaximumValue();
     method public long getMinimumValue();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.LongRangeUserStyleSettingWireFormat toWireFormat();
+    property public final long defaultValue;
+    property public final long maximumValue;
+    property public final long minimumValue;
   }
 
   public static final class UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
@@ -185,12 +192,6 @@
   public abstract static class UserStyleSetting.Option {
     ctor public UserStyleSetting.Option(androidx.wear.watchface.style.UserStyleSetting.Option.Id id);
     method public final androidx.wear.watchface.style.UserStyleSetting.Option.Id getId();
-    method public final androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting.BooleanOption? toBooleanOption();
-    method public final androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption? toComplicationsOption();
-    method public final androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption? toCustomValueOption();
-    method public final androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption? toDoubleRangeOption();
-    method public final androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption? toListOption();
-    method public final androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption? toLongRangeOption();
     property public final androidx.wear.watchface.style.UserStyleSetting.Option.Id id;
     field public static final androidx.wear.watchface.style.UserStyleSetting.Option.Companion Companion;
   }
diff --git a/wear/wear-watchface-style/api/restricted_current.txt b/wear/wear-watchface-style/api/restricted_current.txt
index 2b3bab3..707ed6a 100644
--- a/wear/wear-watchface-style/api/restricted_current.txt
+++ b/wear/wear-watchface-style/api/restricted_current.txt
@@ -62,6 +62,7 @@
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final java.util.List<androidx.wear.watchface.style.data.OptionWireFormat> getWireFormatOptionsList();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract androidx.wear.watchface.style.data.UserStyleSettingWireFormat toWireFormat();
     property public final java.util.Collection<androidx.wear.watchface.style.Layer> affectedLayers;
+    property public final androidx.wear.watchface.style.UserStyleSetting.Option defaultOption;
     property public final int defaultOptionIndex;
     property public final CharSequence description;
     property public final CharSequence displayName;
@@ -139,6 +140,9 @@
     method public double getMaximumValue();
     method public double getMinimumValue();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.DoubleRangeUserStyleSettingWireFormat toWireFormat();
+    property public final double defaultValue;
+    property public final double maximumValue;
+    property public final double minimumValue;
   }
 
   public static final class UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
@@ -180,6 +184,9 @@
     method public long getMaximumValue();
     method public long getMinimumValue();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.LongRangeUserStyleSettingWireFormat toWireFormat();
+    property public final long defaultValue;
+    property public final long maximumValue;
+    property public final long minimumValue;
   }
 
   public static final class UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
@@ -192,12 +199,6 @@
   public abstract static class UserStyleSetting.Option {
     ctor public UserStyleSetting.Option(androidx.wear.watchface.style.UserStyleSetting.Option.Id id);
     method public final androidx.wear.watchface.style.UserStyleSetting.Option.Id getId();
-    method public final androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting.BooleanOption? toBooleanOption();
-    method public final androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption? toComplicationsOption();
-    method public final androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption? toCustomValueOption();
-    method public final androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption? toDoubleRangeOption();
-    method public final androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption? toListOption();
-    method public final androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption? toLongRangeOption();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract androidx.wear.watchface.style.data.OptionWireFormat toWireFormat();
     property public final androidx.wear.watchface.style.UserStyleSetting.Option.Id id;
     field public static final androidx.wear.watchface.style.UserStyleSetting.Option.Companion Companion;
diff --git a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
index 4e8622f..8f78541 100644
--- a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
+++ b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
@@ -59,7 +59,7 @@
                 if (option != null) {
                     this[styleSetting] = styleSetting.getSettingOptionForId(option)
                 } else {
-                    this[styleSetting] = styleSetting.getDefaultOption()
+                    this[styleSetting] = styleSetting.defaultOption
                 }
             }
         }
@@ -207,7 +207,7 @@
     public var userStyle: UserStyle = UserStyle(
         HashMap<UserStyleSetting, UserStyleSetting.Option>().apply {
             for (setting in schema.userStyleSettings) {
-                this[setting] = setting.getDefaultOption()
+                this[setting] = setting.defaultOption
             }
         }
     )
diff --git a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
index 9fb5fac..edaa98f 100644
--- a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
+++ b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
@@ -151,7 +151,8 @@
         options.map { it.toWireFormat() }
 
     /** Returns the default for when the user hasn't selected an option. */
-    public fun getDefaultOption(): Option = options[defaultOptionIndex]
+    public val defaultOption: Option
+        get() = options[defaultOptionIndex]
 
     override fun toString(): String = "{${id.value} : " +
         options.joinToString(transform = { it.toString() }) + "}"
@@ -233,48 +234,6 @@
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
         public abstract fun toWireFormat(): OptionWireFormat
 
-        public fun toBooleanOption(): BooleanUserStyleSetting.BooleanOption? =
-            if (this is BooleanUserStyleSetting.BooleanOption) {
-                this
-            } else {
-                null
-            }
-
-        public fun toComplicationsOption(): ComplicationsUserStyleSetting.ComplicationsOption? =
-            if (this is ComplicationsUserStyleSetting.ComplicationsOption) {
-                this
-            } else {
-                null
-            }
-
-        public fun toCustomValueOption(): CustomValueUserStyleSetting.CustomValueOption? =
-            if (this is CustomValueUserStyleSetting.CustomValueOption) {
-                this
-            } else {
-                null
-            }
-
-        public fun toDoubleRangeOption(): DoubleRangeUserStyleSetting.DoubleRangeOption? =
-            if (this is DoubleRangeUserStyleSetting.DoubleRangeOption) {
-                this
-            } else {
-                null
-            }
-
-        public fun toListOption(): ListUserStyleSetting.ListOption? =
-            if (this is ListUserStyleSetting.ListOption) {
-                this
-            } else {
-                null
-            }
-
-        public fun toLongRangeOption(): LongRangeUserStyleSetting.LongRangeOption? =
-            if (this is LongRangeUserStyleSetting.LongRangeOption) {
-                this
-            } else {
-                null
-            }
-
         override fun toString(): String =
             try {
                 id.value.decodeToString()
@@ -676,14 +635,16 @@
         }
 
         /** Returns the minimum value. */
-        public fun getMinimumValue(): Double = (options.first() as DoubleRangeOption).value
+        public val minimumValue: Double
+            get() = (options.first() as DoubleRangeOption).value
 
         /** Returns the maximum value. */
-        public fun getMaximumValue(): Double = (options.last() as DoubleRangeOption).value
+        public val maximumValue: Double
+            get() = (options.last() as DoubleRangeOption).value
 
         /** Returns the default value. */
-        public fun getDefaultValue(): Double =
-            (options[defaultOptionIndex] as DoubleRangeOption).value
+        public val defaultValue: Double
+            get() = (options[defaultOptionIndex] as DoubleRangeOption).value
 
         /** We support all values in the range [min ... max] not just min & max. */
         override fun getOptionForId(optionId: ByteArray): Option =
@@ -692,7 +653,7 @@
         private fun checkedOptionForId(optionId: ByteArray): DoubleRangeOption {
             return try {
                 val value = ByteBuffer.wrap(optionId).double
-                if (value < getMinimumValue() || value > getMaximumValue()) {
+                if (value < minimumValue || value > maximumValue) {
                     options[defaultOptionIndex] as DoubleRangeOption
                 } else {
                     DoubleRangeOption(value)
@@ -910,20 +871,17 @@
             override fun toString(): String = value.toString()
         }
 
-        /**
-         * Returns the minimum value.
-         */
-        public fun getMinimumValue(): Long = (options.first() as LongRangeOption).value
+        /** The minimum value. */
+        public val minimumValue: Long
+            get() = (options.first() as LongRangeOption).value
 
-        /**
-         * Returns the maximum value.
-         */
-        public fun getMaximumValue(): Long = (options.last() as LongRangeOption).value
+        /** The maximum value. */
+        public val maximumValue: Long
+            get() = (options.last() as LongRangeOption).value
 
-        /**
-         * Returns the default value.
-         */
-        public fun getDefaultValue(): Long = (options[defaultOptionIndex] as LongRangeOption).value
+        /** The default value. */
+        public val defaultValue: Long
+            get() = (options[defaultOptionIndex] as LongRangeOption).value
 
         /**
          * We support all values in the range [min ... max] not just min & max.
@@ -934,7 +892,7 @@
         private fun checkedOptionForId(optionId: ByteArray): LongRangeOption {
             return try {
                 val value = ByteBuffer.wrap(optionId).long
-                if (value < getMinimumValue() || value > getMaximumValue()) {
+                if (value < minimumValue || value > maximumValue) {
                     options[defaultOptionIndex] as LongRangeOption
                 } else {
                     LongRangeOption(value)
diff --git a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
index f178383..cba47d2 100644
--- a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
+++ b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption
 import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting
@@ -313,9 +314,8 @@
         )
 
         assertThat(
-            userStyleRepository.userStyle[
-                customStyleSetting
-            ]?.toCustomValueOption()!!.customValue.decodeToString()
+            (userStyleRepository.userStyle[customStyleSetting]!! as CustomValueOption)
+                .customValue.decodeToString()
         ).isEqualTo("test")
     }
 
@@ -349,6 +349,19 @@
     }
 
     @Test
+    public fun userStyleData_toString() {
+        val userStyleData = UserStyleData(
+            mapOf(
+                "A" to "a".encodeToByteArray(),
+                "B" to "b".encodeToByteArray()
+            )
+        )
+
+        assertThat(userStyleData.toString()).contains("A=a")
+        assertThat(userStyleData.toString()).contains("B=b")
+    }
+
+    @Test
     public fun optionIdToStringTest() {
         assertThat(BooleanUserStyleSetting.BooleanOption(true).toString()).isEqualTo("true")
         assertThat(BooleanUserStyleSetting.BooleanOption(false).toString()).isEqualTo("false")
diff --git a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
index 322f0f0..3afade1 100644
--- a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
+++ b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
@@ -24,6 +24,7 @@
 import androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption
 import androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.Option
 import androidx.wear.watchface.style.data.UserStyleSchemaWireFormat
@@ -43,10 +44,10 @@
     private val icon2 = Icon.createWithContentUri("icon2")
     private val icon3 = Icon.createWithContentUri("icon3")
     private val icon4 = Icon.createWithContentUri("icon4")
-    private val option1 = ListUserStyleSetting.ListOption(Option.Id("1"), "one", icon1)
-    private val option2 = ListUserStyleSetting.ListOption(Option.Id("2"), "two", icon2)
-    private val option3 = ListUserStyleSetting.ListOption(Option.Id("3"), "three", icon3)
-    private val option4 = ListUserStyleSetting.ListOption(Option.Id("4"), "four", icon4)
+    private val option1 = ListOption(Option.Id("1"), "one", icon1)
+    private val option2 = ListOption(Option.Id("2"), "two", icon2)
+    private val option3 = ListOption(Option.Id("3"), "three", icon3)
+    private val option4 = ListOption(Option.Id("4"), "four", icon4)
 
     @Test
     public fun parcelAndUnparcelStyleSettingAndOption() {
@@ -79,9 +80,7 @@
         assertThat(unparceled.icon!!.uri.toString()).isEqualTo("settingIcon")
         assertThat(unparceled.affectedLayers.size).isEqualTo(1)
         assertThat(unparceled.affectedLayers.first()).isEqualTo(Layer.BASE)
-        val optionArray =
-            unparceled.options.filterIsInstance<ListUserStyleSetting.ListOption>()
-                .toTypedArray()
+        val optionArray = unparceled.options.filterIsInstance<ListOption>().toTypedArray()
         assertThat(optionArray.size).isEqualTo(3)
         assertThat(optionArray[0].id.value.decodeToString()).isEqualTo("1")
         assertThat(optionArray[0].displayName).isEqualTo("one")
@@ -100,12 +99,9 @@
         val wireFormat2 = option2.toWireFormat()
         val wireFormat3 = option3.toWireFormat()
 
-        val unmarshalled1 =
-            UserStyleSetting.Option.createFromWireFormat(wireFormat1).toListOption()!!
-        val unmarshalled2 =
-            UserStyleSetting.Option.createFromWireFormat(wireFormat2).toListOption()!!
-        val unmarshalled3 =
-            UserStyleSetting.Option.createFromWireFormat(wireFormat3).toListOption()!!
+        val unmarshalled1 = Option.createFromWireFormat(wireFormat1) as ListOption
+        val unmarshalled2 = Option.createFromWireFormat(wireFormat2) as ListOption
+        val unmarshalled3 = Option.createFromWireFormat(wireFormat3) as ListOption
 
         assertThat(unmarshalled1.id.value.decodeToString()).isEqualTo("1")
         assertThat(unmarshalled1.displayName).isEqualTo("one")
@@ -177,8 +173,7 @@
         assertThat(schema.userStyleSettings[0].affectedLayers.size).isEqualTo(1)
         assertThat(schema.userStyleSettings[0].affectedLayers.first()).isEqualTo(Layer.BASE)
         val optionArray1 =
-            schema.userStyleSettings[0].options.filterIsInstance<ListUserStyleSetting.ListOption>()
-                .toTypedArray()
+            schema.userStyleSettings[0].options.filterIsInstance<ListOption>().toTypedArray()
         assertThat(optionArray1.size).isEqualTo(2)
         assertThat(optionArray1[0].id.value.decodeToString()).isEqualTo("1")
         assertThat(optionArray1[0].displayName).isEqualTo("one")
@@ -197,8 +192,7 @@
             Layer.COMPLICATIONS_OVERLAY
         )
         val optionArray2 =
-            schema.userStyleSettings[1].options.filterIsInstance<ListUserStyleSetting.ListOption>()
-                .toTypedArray()
+            schema.userStyleSettings[1].options.filterIsInstance<ListOption>().toTypedArray()
         assertThat(optionArray2.size).isEqualTo(2)
         assertThat(optionArray2[0].id.value.decodeToString()).isEqualTo("3")
         assertThat(optionArray2[0].displayName).isEqualTo("three")
@@ -216,7 +210,7 @@
         assertThat(schema.userStyleSettings[2].affectedLayers.first()).isEqualTo(Layer.BASE)
 
         assert(schema.userStyleSettings[3] is CustomValueUserStyleSetting)
-        assertThat(schema.userStyleSettings[3].getDefaultOption().id.value.decodeToString())
+        assertThat(schema.userStyleSettings[3].defaultOption.id.value.decodeToString())
             .isEqualTo("default")
         assertThat(schema.userStyleSettings[3].affectedLayers.size).isEqualTo(1)
         assertThat(schema.userStyleSettings[3].affectedLayers.first()).isEqualTo(Layer.BASE)
@@ -301,7 +295,7 @@
             listOf(Layer.BASE),
             -1.0
         )
-        assertThat(doubleRangeUserStyleSettingDefaultMin.getDefaultValue()).isEqualTo(-1.0)
+        assertThat(doubleRangeUserStyleSettingDefaultMin.defaultValue).isEqualTo(-1.0)
 
         val doubleRangeUserStyleSettingDefaultMid = DoubleRangeUserStyleSetting(
             UserStyleSetting.Id("id2"),
@@ -313,7 +307,7 @@
             listOf(Layer.BASE),
             0.5
         )
-        assertThat(doubleRangeUserStyleSettingDefaultMid.getDefaultValue()).isEqualTo(0.5)
+        assertThat(doubleRangeUserStyleSettingDefaultMid.defaultValue).isEqualTo(0.5)
 
         val doubleRangeUserStyleSettingDefaultMax = DoubleRangeUserStyleSetting(
             UserStyleSetting.Id("id2"),
@@ -325,7 +319,7 @@
             listOf(Layer.BASE),
             1.0
         )
-        assertThat(doubleRangeUserStyleSettingDefaultMax.getDefaultValue()).isEqualTo(1.0)
+        assertThat(doubleRangeUserStyleSettingDefaultMax.defaultValue).isEqualTo(1.0)
     }
 
     @Test
@@ -340,7 +334,7 @@
             listOf(Layer.BASE),
             -1,
         )
-        assertThat(longRangeUserStyleSettingDefaultMin.getDefaultValue()).isEqualTo(-1)
+        assertThat(longRangeUserStyleSettingDefaultMin.defaultValue).isEqualTo(-1)
 
         val longRangeUserStyleSettingDefaultMid = LongRangeUserStyleSetting(
             UserStyleSetting.Id("id2"),
@@ -352,7 +346,7 @@
             listOf(Layer.BASE),
             5
         )
-        assertThat(longRangeUserStyleSettingDefaultMid.getDefaultValue()).isEqualTo(5)
+        assertThat(longRangeUserStyleSettingDefaultMid.defaultValue).isEqualTo(5)
 
         val longRangeUserStyleSettingDefaultMax = LongRangeUserStyleSetting(
             UserStyleSetting.Id("id2"),
@@ -364,7 +358,7 @@
             listOf(Layer.BASE),
             10
         )
-        assertThat(longRangeUserStyleSettingDefaultMax.getDefaultValue()).isEqualTo(10)
+        assertThat(longRangeUserStyleSettingDefaultMax.defaultValue).isEqualTo(10)
     }
 
     @Test
diff --git a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt
index 742850d..dbd8114 100644
--- a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt
+++ b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt
@@ -16,9 +16,10 @@
 
 package androidx.wear.watchface.style
 
-import androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting.BooleanOption
 import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
-import androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption
+import androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption
 import androidx.wear.watchface.style.UserStyleSetting.Option
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.fail
@@ -50,27 +51,29 @@
             )
 
         assertThat(
-            rangedUserStyleSetting.getOptionForId("not a number".encodeToByteArray())
-                .toDoubleRangeOption()!!.value
+            (
+                rangedUserStyleSetting.getOptionForId("not a number".encodeToByteArray()) as
+                    DoubleRangeOption
+                ).value
         ).isEqualTo(defaultValue)
 
         assertThat(
-            rangedUserStyleSetting.getOptionForId("-1".encodeToByteArray())
-                .toDoubleRangeOption()!!.value
+            (rangedUserStyleSetting.getOptionForId("-1".encodeToByteArray()) as DoubleRangeOption)
+                .value
         ).isEqualTo(defaultValue)
 
         assertThat(
-            rangedUserStyleSetting.getOptionForId("10".encodeToByteArray())
-                .toDoubleRangeOption()!!.value
+            (rangedUserStyleSetting.getOptionForId("10".encodeToByteArray()) as DoubleRangeOption)
+                .value
         ).isEqualTo(defaultValue)
     }
 
     @Test
     public fun byteArrayConversion() {
-        assertThat(BooleanUserStyleSetting.BooleanOption(true).value).isEqualTo(true)
-        assertThat(BooleanUserStyleSetting.BooleanOption(false).value).isEqualTo(false)
-        assertThat(DoubleRangeUserStyleSetting.DoubleRangeOption(123.4).value).isEqualTo(123.4)
-        assertThat(LongRangeUserStyleSetting.LongRangeOption(1234).value).isEqualTo(1234)
+        assertThat(BooleanOption(true).value).isEqualTo(true)
+        assertThat(BooleanOption(false).value).isEqualTo(false)
+        assertThat(DoubleRangeOption(123.4).value).isEqualTo(123.4)
+        assertThat(LongRangeOption(1234).value).isEqualTo(1234)
     }
 
     @Test
diff --git a/wear/wear-watchface/api/current.txt b/wear/wear-watchface/api/current.txt
index a489434..68ca68b 100644
--- a/wear/wear-watchface/api/current.txt
+++ b/wear/wear-watchface/api/current.txt
@@ -252,8 +252,9 @@
   }
 
   public final class WatchState {
-    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis, boolean isHeadless);
+    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis, @Px int chinHeight, boolean isHeadless);
     method public long getAnalogPreviewReferenceTimeMillis();
+    method @Px public int getChinHeight();
     method public long getDigitalPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Integer> getInterruptionFilter();
     method public boolean hasBurnInProtection();
@@ -262,6 +263,7 @@
     method public boolean isHeadless();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible();
     property public final long analogPreviewReferenceTimeMillis;
+    property @Px public final int chinHeight;
     property public final long digitalPreviewReferenceTimeMillis;
     property public final boolean hasBurnInProtection;
     property public final boolean hasLowBitAmbient;
diff --git a/wear/wear-watchface/api/public_plus_experimental_current.txt b/wear/wear-watchface/api/public_plus_experimental_current.txt
index a489434..68ca68b 100644
--- a/wear/wear-watchface/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface/api/public_plus_experimental_current.txt
@@ -252,8 +252,9 @@
   }
 
   public final class WatchState {
-    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis, boolean isHeadless);
+    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis, @Px int chinHeight, boolean isHeadless);
     method public long getAnalogPreviewReferenceTimeMillis();
+    method @Px public int getChinHeight();
     method public long getDigitalPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Integer> getInterruptionFilter();
     method public boolean hasBurnInProtection();
@@ -262,6 +263,7 @@
     method public boolean isHeadless();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible();
     property public final long analogPreviewReferenceTimeMillis;
+    property @Px public final int chinHeight;
     property public final long digitalPreviewReferenceTimeMillis;
     property public final boolean hasBurnInProtection;
     property public final boolean hasLowBitAmbient;
diff --git a/wear/wear-watchface/api/restricted_current.txt b/wear/wear-watchface/api/restricted_current.txt
index 33f3560..7a3ec19 100644
--- a/wear/wear-watchface/api/restricted_current.txt
+++ b/wear/wear-watchface/api/restricted_current.txt
@@ -123,6 +123,7 @@
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final class MutableWatchState {
     method public androidx.wear.watchface.WatchState asWatchState();
     method public long getAnalogPreviewReferenceTimeMillis();
+    method @Px public int getChinHeight();
     method public long getDigitalPreviewReferenceTimeMillis();
     method public boolean getHasBurnInProtection();
     method public boolean getHasLowBitAmbient();
@@ -132,12 +133,14 @@
     method public boolean isHeadless();
     method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isVisible();
     method public void setAnalogPreviewReferenceTimeMillis(long p);
+    method public void setChinHeight(@Px int value);
     method public void setDigitalPreviewReferenceTimeMillis(long p);
     method public void setHasBurnInProtection(boolean p);
     method public void setHasLowBitAmbient(boolean p);
     method public void setHeadless(boolean p);
     method public void setInterruptionFilter(androidx.wear.watchface.MutableObservableWatchData<java.lang.Integer> p);
     property public final long analogPreviewReferenceTimeMillis;
+    property @Px public final int chinHeight;
     property public final long digitalPreviewReferenceTimeMillis;
     property public final boolean hasBurnInProtection;
     property public final boolean hasLowBitAmbient;
@@ -319,8 +322,9 @@
   }
 
   public final class WatchState {
-    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis, boolean isHeadless);
+    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis, @Px int chinHeight, boolean isHeadless);
     method public long getAnalogPreviewReferenceTimeMillis();
+    method @Px public int getChinHeight();
     method public long getDigitalPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Integer> getInterruptionFilter();
     method public boolean hasBurnInProtection();
@@ -329,6 +333,7 @@
     method public boolean isHeadless();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible();
     property public final long analogPreviewReferenceTimeMillis;
+    property @Px public final int chinHeight;
     property public final long digitalPreviewReferenceTimeMillis;
     property public final boolean hasBurnInProtection;
     property public final boolean hasLowBitAmbient;
diff --git a/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.java b/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.java
index 2becac8..eb93d21a 100644
--- a/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.java
+++ b/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.java
@@ -42,6 +42,7 @@
     private static final long UPDATE_DELAY_MILLIS = TimeUnit.SECONDS.toMillis(1);
     private static final char[] DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
 
+    private final WatchState mWatchState;
     private final Paint mPaint;
     private final char[] mTimeText = new char[5];
 
@@ -51,6 +52,7 @@
             @NotNull WatchState watchState) {
         super(surfaceHolder, currentUserStyleRepository, watchState, CanvasType.HARDWARE,
                 UPDATE_DELAY_MILLIS);
+        mWatchState = watchState;
         mPaint = new Paint();
         mPaint.setTextAlign(Align.CENTER);
         mPaint.setTextSize(64f);
@@ -69,6 +71,11 @@
         mTimeText[2] = second % 2 == 0 ? ':' : ' ';
         mTimeText[3] = DIGITS[minute / 10];
         mTimeText[4] = DIGITS[minute % 10];
-        canvas.drawText(mTimeText, 0, 5, rect.centerX(), rect.centerY(), mPaint);
+        canvas.drawText(mTimeText,
+                0,
+                5,
+                rect.centerX(),
+                rect.centerY() - mWatchState.getChinHeight(),
+                mPaint);
     }
 }
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
index 7f0943b..ad5079f 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
@@ -41,15 +41,17 @@
 import androidx.wear.watchface.WatchFaceService
 import androidx.wear.watchface.WatchFaceType
 import androidx.wear.watchface.WatchState
+import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.Layer
 import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSchema
 import androidx.wear.watchface.style.UserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting.BooleanOption
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay
 import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption
 import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.Option
 import kotlin.math.cos
@@ -334,12 +336,10 @@
                         complication.renderer.drawable = watchFaceColorStyle.getDrawable(context)!!
                     }
 
-                    val drawPipsOption = userStyle[drawPipsStyleSetting]?.toBooleanOption()!!
-                    val watchHandLengthOption =
-                        userStyle[watchHandLengthStyleSettingDouble]?.toDoubleRangeOption()!!
-
-                    drawHourPips = drawPipsOption.value
-                    watchHandScale = watchHandLengthOption.value.toFloat()
+                    drawHourPips = (userStyle[drawPipsStyleSetting]!! as BooleanOption).value
+                    watchHandScale =
+                        (userStyle[watchHandLengthStyleSettingDouble]!! as DoubleRangeOption)
+                            .value.toFloat()
                 }
             }
         )
diff --git a/wear/wear-watchface/samples/src/main/res/values/dimens.xml b/wear/wear-watchface/samples/src/main/res/values/dimens.xml
index 2662e6e..5080c63c 100644
--- a/wear/wear-watchface/samples/src/main/res/values/dimens.xml
+++ b/wear/wear-watchface/samples/src/main/res/values/dimens.xml
@@ -17,6 +17,6 @@
 
 <resources>
     <!-- constants for watch face element renderer. -->
-    <dimen name="hour_mark_size">10sp</dimen>
+    <dimen name="hour_mark_size">10dp</dimen>
     <dimen name="clock_hand_stroke_width">2dp</dimen>
 </resources>
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
index f39f8e4..121eae73 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
@@ -35,6 +35,7 @@
 import androidx.wear.watchface.style.UserStyle
 import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption
 import java.lang.ref.WeakReference
 
 private fun getComponentName(context: Context) = ComponentName(
@@ -135,7 +136,7 @@
                 object : CurrentUserStyleRepository.UserStyleChangeListener {
                     override fun onUserStyleChanged(userStyle: UserStyle) {
                         val newlySelectedOption =
-                            userStyle[complicationsStyleCategory]?.toComplicationsOption()!!
+                            userStyle[complicationsStyleCategory]!! as ComplicationsOption
                         if (previousOption != newlySelectedOption) {
                             previousOption = newlySelectedOption
                             applyComplicationsStyleCategoryOption(newlySelectedOption)
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 61b0832..a2d4a1a 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -41,7 +41,9 @@
 import android.view.Choreographer
 import android.view.Surface
 import android.view.SurfaceHolder
+import android.view.WindowInsets
 import androidx.annotation.IntDef
+import androidx.annotation.Px
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
 import androidx.annotation.UiThread
@@ -66,10 +68,10 @@
 import androidx.wear.watchface.data.IdAndComplicationStateWireFormat
 import androidx.wear.watchface.data.WatchUiState
 import androidx.wear.watchface.editor.EditorService
-import androidx.wear.watchface.style.UserStyle
 import androidx.wear.watchface.style.CurrentUserStyleRepository
-import androidx.wear.watchface.style.UserStyleSetting
+import androidx.wear.watchface.style.UserStyle
 import androidx.wear.watchface.style.UserStyleData
+import androidx.wear.watchface.style.UserStyleSetting
 import androidx.wear.watchface.style.data.UserStyleWireFormat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.android.asCoroutineDispatcher
@@ -375,6 +377,7 @@
 
         internal var firstSetWatchUiState = true
         internal var immutableSystemStateDone = false
+        internal var immutableChinHeightDone = false
         private var ignoreNextOnVisibilityChanged = false
 
         internal var lastActiveComplications: IntArray? = null
@@ -776,6 +779,29 @@
             }
         }
 
+        override fun onApplyWindowInsets(insets: WindowInsets?) {
+            super.onApplyWindowInsets(insets)
+            @Px val chinHeight =
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                    ChinHeightApi30.extractFromWindowInsets(insets)
+                } else {
+                    ChinHeightApi25.extractFromWindowInsets(insets)
+                }
+            if (immutableChinHeightDone) {
+                // The chin size cannot change so this should be called only once.
+                if (mutableWatchState.chinHeight != chinHeight) {
+                    Log.w(
+                        TAG,
+                        "unexpected chin size change ignored: " +
+                            "${mutableWatchState.chinHeight} != $chinHeight"
+                    )
+                }
+                return
+            }
+            mutableWatchState.chinHeight = chinHeight
+            immutableChinHeightDone = true
+        }
+
         override fun onDestroy(): Unit = TraceEvent("EngineWrapper.onDestroy").use {
             destroyed = true
             coroutineScope.cancel()
@@ -1340,6 +1366,20 @@
         HeadlessWatchFaceImpl.dump(indentingPrintWriter)
         indentingPrintWriter.flush()
     }
+
+    private object ChinHeightApi25 {
+        @Suppress("DEPRECATION")
+        @Px
+        fun extractFromWindowInsets(insets: WindowInsets?) =
+            insets?.systemWindowInsetBottom ?: 0
+    }
+
+    @RequiresApi(30)
+    private object ChinHeightApi30 {
+        @Px
+        fun extractFromWindowInsets(insets: WindowInsets?) =
+            insets?.getInsets(WindowInsets.Type.systemBars())?.bottom ?: 0
+    }
 }
 
 /**
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
index 7caeaed..d1ee966 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
@@ -17,6 +17,7 @@
 package androidx.wear.watchface
 
 import android.app.NotificationManager
+import androidx.annotation.Px
 import androidx.annotation.RestrictTo
 import androidx.annotation.UiThread
 
@@ -46,6 +47,9 @@
  *     milliseconds since the epoch.
  * @param digitalPreviewReferenceTimeMillis UTC reference time for previews of digital watch faces
  *     in milliseconds since the epoch.
+ * @param chinHeight the size, in pixels, of the chin or zero if the device does not have a
+ *     chin. A chin is a section at the bottom of a circular display that is visible due to
+ *     hardware limitations.
  * @param isHeadless Whether or not this is a headless watchface.
  */
 public class WatchState(
@@ -60,6 +64,7 @@
     public val hasBurnInProtection: Boolean,
     public val analogPreviewReferenceTimeMillis: Long,
     public val digitalPreviewReferenceTimeMillis: Long,
+    @Px @get:Px public val chinHeight: Int,
     public val isHeadless: Boolean
 ) {
     @UiThread
@@ -74,6 +79,7 @@
         writer.println("hasBurnInProtection=$hasBurnInProtection")
         writer.println("analogPreviewReferenceTimeMillis=$analogPreviewReferenceTimeMillis")
         writer.println("digitalPreviewReferenceTimeMillis=$digitalPreviewReferenceTimeMillis")
+        writer.println("chinHeight=$chinHeight")
         writer.println("isHeadless=$isHeadless")
         writer.decreaseIndent()
     }
@@ -91,6 +97,12 @@
     public var hasBurnInProtection: Boolean = false
     public var analogPreviewReferenceTimeMillis: Long = 0
     public var digitalPreviewReferenceTimeMillis: Long = 0
+    @Px
+    public var chinHeight: Int = 0
+        @Px get
+        set(@Px value) {
+            field = value
+        }
     public var isHeadless: Boolean = false
 
     public fun asWatchState(): WatchState = WatchState(
@@ -102,6 +114,7 @@
         hasBurnInProtection = hasBurnInProtection,
         analogPreviewReferenceTimeMillis = analogPreviewReferenceTimeMillis,
         digitalPreviewReferenceTimeMillis = digitalPreviewReferenceTimeMillis,
+        chinHeight = chinHeight,
         isHeadless = isHeadless
     )
 }
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index db00764..8352418 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -21,6 +21,7 @@
 import android.content.Context
 import android.content.Intent
 import android.graphics.Color
+import android.graphics.Insets
 import android.graphics.Rect
 import android.graphics.RectF
 import android.os.BatteryManager
@@ -35,7 +36,10 @@
 import android.support.wearable.watchface.accessibility.ContentDescriptionLabel
 import android.view.SurfaceHolder
 import android.view.ViewConfiguration
+import android.view.WindowInsets
+import androidx.annotation.Px
 import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SdkSuppress
 import androidx.wear.complications.ComplicationBounds
 import androidx.wear.complications.DefaultComplicationProviderPolicy
 import androidx.wear.complications.SystemProviders
@@ -50,9 +54,9 @@
 import androidx.wear.watchface.data.DeviceConfig
 import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
 import androidx.wear.watchface.data.WatchUiState
+import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.Layer
 import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSchema
 import androidx.wear.watchface.style.UserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting
@@ -1113,6 +1117,61 @@
             )
     }
 
+    @SdkSuppress(maxSdkVersion = 29)
+    @Test
+    public fun onApplyWindowInsetsBeforeR_setsChinHeight() {
+        initEngine(
+            WatchFaceType.ANALOG,
+            emptyList(),
+            UserStyleSchema(emptyList())
+        )
+        // Initially the chin size is set to zero.
+        assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(0)
+        // When window insets are delivered to the watch face.
+        engineWrapper.onApplyWindowInsets(getChinWindowInsetsApi25(chinHeight = 12))
+        // Then the chin size is updated.
+        assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(12)
+    }
+
+    @SdkSuppress(minSdkVersion = 30)
+    @Test
+    public fun onApplyWindowInsetsRAndAbove_setsChinHeight() {
+        initEngine(
+            WatchFaceType.ANALOG,
+            emptyList(),
+            UserStyleSchema(emptyList())
+        )
+        // Initially the chin size is set to zero.
+        assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(0)
+        // When window insets are delivered to the watch face.
+        engineWrapper.onApplyWindowInsets(getChinWindowInsetsApi30(chinHeight = 12))
+        // Then the chin size is updated.
+        assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(12)
+    }
+
+    @Test
+    public fun onApplyWindowInsetsBeforeR_multipleCallsIgnored() {
+        initEngine(
+            WatchFaceType.ANALOG,
+            emptyList(),
+            UserStyleSchema(emptyList())
+        )
+        // Initially the chin size is set to zero.
+        assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(0)
+        // When window insets are delivered to the watch face.
+        engineWrapper.onApplyWindowInsets(getChinWindowInsetsApi25(chinHeight = 12))
+        // Then the chin size is updated.
+        assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(12)
+        // When the same window insets are delivered to the watch face again.
+        engineWrapper.onApplyWindowInsets(getChinWindowInsetsApi25(chinHeight = 12))
+        // Nothing happens.
+        assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(12)
+        // When different window insets are delivered to the watch face again.
+        engineWrapper.onApplyWindowInsets(getChinWindowInsetsApi25(chinHeight = 24))
+        // Nothing happens and the size is unchanged.
+        assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(12)
+    }
+
     @Test
     public fun initWallpaperInteractiveWatchFaceInstanceWithUserStyle() {
         initWallpaperInteractiveWatchFaceInstance(
@@ -2380,4 +2439,16 @@
         engineWrapper.onVisibilityChanged(false)
         verify(observer).onChanged(true)
     }
+
+    @Suppress("DEPRECATION")
+    private fun getChinWindowInsetsApi25(@Px chinHeight: Int): WindowInsets =
+        WindowInsets.Builder().setSystemWindowInsets(
+            Insets.of(0, 0, 0, chinHeight)
+        ).build()
+
+    private fun getChinWindowInsetsApi30(@Px chinHeight: Int): WindowInsets =
+        WindowInsets.Builder().setInsets(
+            WindowInsets.Type.systemBars(),
+            Insets.of(Rect().apply { bottom = chinHeight })
+        ).build()
 }
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchStateTest.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchStateTest.kt
index 221b3ad..bae221e 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchStateTest.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchStateTest.kt
@@ -125,6 +125,18 @@
     }
 
     @Test
+    public fun asWatchFace_chinHeight_isNotPropagated() {
+        val mutableWatchState = MutableWatchState()
+        var watchState = mutableWatchState.asWatchState()
+        // Defaults to 0.
+        assertThat(watchState.chinHeight).isEqualTo(0)
+        // Value updated is not propagated unless a new instance is created.
+        mutableWatchState.chinHeight = 48
+        assertThat(watchState.chinHeight).isEqualTo(0)
+        assertThat(mutableWatchState.asWatchState().chinHeight).isEqualTo(48)
+    }
+
+    @Test
     public fun asWatchFace_isHeadless_isNotPropagated() {
         val mutableWatchState = MutableWatchState()
         var watchState = mutableWatchState.asWatchState()