Merge "feat: add left/right animation and minor prop renaming in Carousel" into androidx-main
diff --git a/camera/camera-camera2-pipe-integration/build.gradle b/camera/camera-camera2-pipe-integration/build.gradle
index 0161db3..5ba8cb5 100644
--- a/camera/camera-camera2-pipe-integration/build.gradle
+++ b/camera/camera-camera2-pipe-integration/build.gradle
@@ -91,6 +91,7 @@
     androidTestImplementation(project(":annotation:annotation-experimental"))
     androidTestImplementation(project(":camera:camera-lifecycle"))
     androidTestImplementation(project(":camera:camera-testing"))
+    androidTestImplementation(project(":camera:camera-video"))
     androidTestImplementation(project(":concurrent:concurrent-futures-ktx"))
     androidTestImplementation(project(":internal-testutils-truth"))
     androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt
index e5bccd9..a9e2aeb 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CameraControlAdapterDeviceTest.kt
@@ -17,16 +17,25 @@
 package androidx.camera.camera2.pipe.integration
 
 import android.content.Context
+import android.graphics.SurfaceTexture
 import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES
+import android.hardware.camera2.CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES
 import android.hardware.camera2.CameraCharacteristics.CONTROL_MAX_REGIONS_AE
 import android.hardware.camera2.CameraCharacteristics.CONTROL_MAX_REGIONS_AF
 import android.hardware.camera2.CameraCharacteristics.CONTROL_MAX_REGIONS_AWB
+import android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_OFF
 import android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON
 import android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH
 import android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH
+import android.hardware.camera2.CameraMetadata.CONTROL_AF_MODE_AUTO
+import android.hardware.camera2.CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE
+import android.hardware.camera2.CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_VIDEO
+import android.hardware.camera2.CameraMetadata.CONTROL_AF_MODE_OFF
 import android.hardware.camera2.CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION
 import android.hardware.camera2.CaptureRequest.CONTROL_AE_MODE
 import android.hardware.camera2.CaptureRequest.CONTROL_AE_REGIONS
+import android.hardware.camera2.CaptureRequest.CONTROL_AF_MODE
 import android.hardware.camera2.CaptureRequest.CONTROL_AF_REGIONS
 import android.hardware.camera2.CaptureRequest.CONTROL_AWB_REGIONS
 import android.hardware.camera2.CaptureRequest.CONTROL_CAPTURE_INTENT
@@ -36,6 +45,7 @@
 import android.hardware.camera2.CaptureRequest.FLASH_MODE_TORCH
 import android.hardware.camera2.CaptureRequest.SCALER_CROP_REGION
 import android.os.Build
+import android.util.Size
 import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.FrameInfo
 import androidx.camera.camera2.pipe.RequestMetadata
@@ -49,11 +59,15 @@
 import androidx.camera.core.FocusMeteringAction
 import androidx.camera.core.ImageAnalysis
 import androidx.camera.core.ImageCapture
+import androidx.camera.core.Preview
 import androidx.camera.core.SurfaceOrientedMeteringPointFactory
 import androidx.camera.core.UseCase
 import androidx.camera.core.internal.CameraUseCaseAdapter
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraXUtil
+import androidx.camera.testing.SurfaceTextureProvider
+import androidx.camera.video.Recorder
+import androidx.camera.video.VideoCapture
 import androidx.concurrent.futures.await
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -188,7 +202,7 @@
 
         waitForResult(captureCount = 60).verify(
             { requestMeta: RequestMetadata, _ ->
-                requestMeta.request[CONTROL_AE_MODE] == CONTROL_AE_MODE_ON_AUTO_FLASH
+                requestMeta.isAeMode(CONTROL_AE_MODE_ON_AUTO_FLASH)
             },
             TIMEOUT
         )
@@ -203,7 +217,7 @@
 
         waitForResult(captureCount = 60).verify(
             { requestMeta: RequestMetadata, _ ->
-                requestMeta[CONTROL_AE_MODE] == CONTROL_AE_MODE_ON
+                requestMeta.isAeMode(CONTROL_AE_MODE_ON)
             },
             TIMEOUT
         )
@@ -218,7 +232,7 @@
 
         waitForResult(captureCount = 60).verify(
             { requestMeta: RequestMetadata, _ ->
-                requestMeta[CONTROL_AE_MODE] == CONTROL_AE_MODE_ON_ALWAYS_FLASH
+                requestMeta.isAeMode(CONTROL_AE_MODE_ON_ALWAYS_FLASH)
             },
             TIMEOUT
         )
@@ -234,7 +248,7 @@
         waitForResult(captureCount = 30).verify(
             { requestMeta: RequestMetadata, frameInfo: FrameInfo ->
                 frameInfo.requestMetadata[FLASH_MODE] == FLASH_MODE_TORCH &&
-                    requestMeta[CONTROL_AE_MODE] == CONTROL_AE_MODE_ON
+                    requestMeta.isAeMode(CONTROL_AE_MODE_ON)
             },
             TIMEOUT
         )
@@ -250,7 +264,7 @@
         waitForResult(captureCount = 30).verify(
             { requestMeta: RequestMetadata, frameInfo: FrameInfo ->
                 frameInfo.requestMetadata[FLASH_MODE] != FLASH_MODE_TORCH &&
-                    requestMeta[CONTROL_AE_MODE] == CONTROL_AE_MODE_ON_AUTO_FLASH
+                    requestMeta.isAeMode(CONTROL_AE_MODE_ON_AUTO_FLASH)
             },
             TIMEOUT
         )
@@ -341,6 +355,32 @@
     }
 
     @Test
+    fun setTemplatePreview_afModeToContinuousPicture() = runBlocking {
+        bindUseCase(createPreview())
+
+        // Assert. Verify the afMode.
+        waitForResult(captureCount = 60).verify(
+            { requestMeta: RequestMetadata, _ ->
+                requestMeta.isAfMode(CONTROL_AF_MODE_CONTINUOUS_PICTURE)
+            },
+            TIMEOUT
+        )
+    }
+
+    @Test
+    fun setTemplateRecord_afModeToContinuousVideo() = runBlocking {
+        bindUseCase(createVideoCapture())
+
+        // Assert. Verify the afMode.
+        waitForResult(captureCount = 60).verify(
+            { requestMeta: RequestMetadata, _ ->
+                requestMeta.isAfMode(CONTROL_AF_MODE_CONTINUOUS_VIDEO)
+            },
+            TIMEOUT
+        )
+    }
+
+    @Test
     fun setZoomRatio_operationCanceledExceptionIfNoUseCase() {
         assertFutureFailedWithOperationCancellation(cameraControl.setZoomRatio(1.5f))
     }
@@ -436,4 +476,70 @@
         )
         cameraControl = camera.cameraControl as CameraControlAdapter
     }
+
+    private fun createVideoCapture(): VideoCapture<Recorder> {
+        return VideoCapture.withOutput(Recorder.Builder().build())
+    }
+
+    private suspend fun createPreview(): Preview =
+        Preview.Builder().build().also { preview ->
+            withContext(Dispatchers.Main) {
+                preview.setSurfaceProvider(getSurfaceProvider())
+            }
+        }
+
+    private fun getSurfaceProvider(): Preview.SurfaceProvider {
+        return SurfaceTextureProvider.createSurfaceTextureProvider(
+            object : SurfaceTextureProvider.SurfaceTextureCallback {
+                override fun onSurfaceTextureReady(
+                    surfaceTexture: SurfaceTexture,
+                    resolution: Size
+                ) {
+                    // No-op
+                }
+
+                override fun onSafeToRelease(surfaceTexture: SurfaceTexture) {
+                    surfaceTexture.release()
+                }
+            }
+        )
+    }
+
+    private fun RequestMetadata.isAfMode(afMode: Int): Boolean {
+        return if (characteristics.isAfModeSupported(afMode)) {
+            getOrDefault(CONTROL_AF_MODE, null) == afMode
+        } else {
+            val fallbackMode =
+                if (characteristics.isAfModeSupported(CONTROL_AF_MODE_CONTINUOUS_PICTURE)) {
+                    CONTROL_AF_MODE_CONTINUOUS_PICTURE
+                } else if (characteristics.isAfModeSupported(CONTROL_AF_MODE_AUTO)) {
+                    CONTROL_AF_MODE_AUTO
+                } else {
+                    CONTROL_AF_MODE_OFF
+                }
+            getOrDefault(CONTROL_AF_MODE, null) == fallbackMode
+        }
+    }
+
+    private fun RequestMetadata.isAeMode(aeMode: Int): Boolean {
+        return if (characteristics.isAeModeSupported(aeMode)) {
+            getOrDefault(CONTROL_AE_MODE, null) == aeMode
+        } else {
+            val fallbackMode =
+                if (characteristics.isAeModeSupported(CONTROL_AE_MODE_ON)) {
+                    CONTROL_AE_MODE_ON
+                } else {
+                    CONTROL_AE_MODE_OFF
+                }
+            getOrDefault(CONTROL_AE_MODE, null) == fallbackMode
+        }
+    }
+
+    private fun CameraCharacteristics.isAfModeSupported(
+        afMode: Int
+    ) = (get(CONTROL_AF_AVAILABLE_MODES) ?: intArrayOf(-1)).contains(afMode)
+
+    private fun CameraCharacteristics.isAeModeSupported(
+        aeMode: Int
+    ) = (get(CONTROL_AE_AVAILABLE_MODES) ?: intArrayOf(-1)).contains(aeMode)
 }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
index 4c1ef19..de3e23f 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
@@ -35,6 +35,7 @@
 import androidx.camera.camera2.pipe.integration.impl.EvCompControl
 import androidx.camera.camera2.pipe.integration.impl.FlashControl
 import androidx.camera.camera2.pipe.integration.impl.FocusMeteringControl
+import androidx.camera.camera2.pipe.integration.impl.State3AControl
 import androidx.camera.camera2.pipe.integration.impl.TorchControl
 import androidx.camera.camera2.pipe.integration.impl.UseCaseThreads
 import androidx.camera.camera2.pipe.integration.impl.ZoomControl
@@ -67,6 +68,7 @@
         EvCompControl.Bindings::class,
         FlashControl.Bindings::class,
         FocusMeteringControl.Bindings::class,
+        State3AControl.Bindings::class,
         TorchControl.Bindings::class,
         Camera2CameraControlCompat.Bindings::class,
     ],
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FlashControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FlashControl.kt
index 0a00d07..21fe187 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FlashControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FlashControl.kt
@@ -16,7 +16,6 @@
 
 package androidx.camera.camera2.pipe.integration.impl
 
-import android.hardware.camera2.CaptureRequest
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.integration.config.CameraScope
 import androidx.camera.core.CameraControl
@@ -25,12 +24,12 @@
 import dagger.Binds
 import dagger.Module
 import dagger.multibindings.IntoSet
+import javax.inject.Inject
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.launch
-import javax.inject.Inject
 
-private const val DEFAULT_FLASH_MODE = ImageCapture.FLASH_MODE_OFF
+internal const val DEFAULT_FLASH_MODE = ImageCapture.FLASH_MODE_OFF
 
 /**
  * Implementation of Flash control exposed by [CameraControlInternal].
@@ -38,6 +37,7 @@
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 @CameraScope
 class FlashControl @Inject constructor(
+    private val state3AControl: State3AControl,
     private val threads: UseCaseThreads,
 ) : UseCaseCameraControl {
     private var _useCaseCamera: UseCaseCamera? = null
@@ -74,7 +74,7 @@
     fun setFlashAsync(flashMode: Int): Deferred<Unit> {
         val signal = CompletableDeferred<Unit>()
 
-        useCaseCamera?.let { useCaseCamera ->
+        useCaseCamera?.let {
 
             // Update _flashMode immediately so that CameraControlInternal#getFlashMode()
             // returns correct value.
@@ -84,24 +84,8 @@
                 stopRunningTask()
 
                 _updateSignal = signal
-                when (flashMode) {
-                    ImageCapture.FLASH_MODE_OFF -> CaptureRequest.CONTROL_AE_MODE_ON
-                    ImageCapture.FLASH_MODE_ON -> CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH
-                    ImageCapture.FLASH_MODE_AUTO -> CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
-                    // TODO(b/209383160): porting the Quirk for AEModeDisabler
-                    //      mAutoFlashAEModeDisabler.getCorrectedAeMode(
-                    //      CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
-                    //    )
-                    else -> CaptureRequest.CONTROL_AE_MODE_ON
-                }.let { aeMode ->
-                    // TODO: check the AE mode is supported before set it.
-                    useCaseCamera.requestControl.addParametersAsync(
-                        type = UseCaseCameraRequestControl.Type.FLASH,
-                        values = mapOf(
-                            CaptureRequest.CONTROL_AE_MODE to aeMode,
-                        )
-                    )
-                }.join()
+                state3AControl.flashMode = flashMode
+                state3AControl.updateSignal?.join()
 
                 signal.complete(Unit)
             }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
index 43b6750..08fd80a 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
@@ -19,6 +19,7 @@
 import android.graphics.PointF
 import android.graphics.Rect
 import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.CaptureResult
 import android.hardware.camera2.params.MeteringRectangle
 import android.util.Rational
@@ -54,8 +55,9 @@
 @CameraScope
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 class FocusMeteringControl @Inject constructor(
-    val cameraProperties: CameraProperties,
-    val threads: UseCaseThreads,
+    private val cameraProperties: CameraProperties,
+    private val state3AControl: State3AControl,
+    private val threads: UseCaseThreads,
 ) : UseCaseCameraControl {
     private var _useCaseCamera: UseCaseCamera? = null
 
@@ -155,6 +157,9 @@
                     )
                     return@launch
                 }
+                if (afRectangles.isNotEmpty()) {
+                    state3AControl.preferredFocusMode = CaptureRequest.CONTROL_AF_MODE_AUTO
+                }
                 val (isCancelEnabled, timeout) = if (action.isAutoCancelEnabled &&
                     action.autoCancelDurationInMillis < autoFocusTimeoutMs
                 ) {
@@ -259,6 +264,7 @@
         signalToCancel: CompletableDeferred<FocusMeteringResult>?,
     ): Result3A {
         signalToCancel?.setCancelException("Cancelled by cancelFocusAndMetering()")
+        state3AControl.preferredFocusMode = null
         return useCaseCamera.requestControl.cancelFocusAndMeteringAsync().await()
     }
 
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt
new file mode 100644
index 0000000..740438c
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.integration.impl
+
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CaptureRequest
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.adapter.SessionConfigAdapter
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CaptureConfig
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.lifecycle.Observer
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+import javax.inject.Inject
+import kotlin.properties.ObservableProperty
+import kotlin.reflect.KProperty
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+
+@CameraScope
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class State3AControl @Inject constructor(
+    val cameraProperties: CameraProperties,
+) : UseCaseCameraControl {
+    private var _useCaseCamera: UseCaseCamera? = null
+    override var useCaseCamera: UseCaseCamera?
+        get() = _useCaseCamera
+        set(value) {
+            val previousUseCaseCamera = _useCaseCamera
+            _useCaseCamera = value
+            CameraXExecutors.mainThreadExecutor().execute {
+                previousUseCaseCamera?.runningUseCasesLiveData?.removeObserver(
+                    useCaseChangeObserver
+                )
+                value?.let {
+                    it.runningUseCasesLiveData.observeForever(useCaseChangeObserver)
+                    invalidate() // Always apply the settings to the camera.
+                }
+            }
+        }
+
+    private val useCaseChangeObserver =
+        Observer<Set<UseCase>> { useCases -> useCases.updateTemplate() }
+    private val afModes = cameraProperties.metadata.getOrDefault(
+        CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES,
+        intArrayOf(CaptureRequest.CONTROL_AF_MODE_OFF)
+    ).asList()
+    private val aeModes = cameraProperties.metadata.getOrDefault(
+        CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES,
+        intArrayOf(CaptureRequest.CONTROL_AE_MODE_OFF)
+    ).asList()
+    private val awbModes = cameraProperties.metadata.getOrDefault(
+        CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES,
+        intArrayOf(CaptureRequest.CONTROL_AWB_MODE_OFF)
+    ).asList()
+
+    var updateSignal: Deferred<Unit>? = null
+        private set
+    var flashMode by updateOnPropertyChange(DEFAULT_FLASH_MODE)
+    var template by updateOnPropertyChange(DEFAULT_REQUEST_TEMPLATE)
+    var preferredAeMode: Int? by updateOnPropertyChange(null)
+    var preferredFocusMode: Int? by updateOnPropertyChange(null)
+
+    override fun reset() {
+        preferredAeMode = null
+        preferredFocusMode = null
+        flashMode = DEFAULT_FLASH_MODE
+        template = DEFAULT_REQUEST_TEMPLATE
+    }
+
+    private fun <T> updateOnPropertyChange(
+        initialValue: T
+    ) = object : ObservableProperty<T>(initialValue) {
+        override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) {
+            if (newValue != oldValue) {
+                invalidate()
+            }
+        }
+    }
+
+    fun invalidate() {
+        val preferAeMode = preferredAeMode ?: when (flashMode) {
+            ImageCapture.FLASH_MODE_OFF -> CaptureRequest.CONTROL_AE_MODE_ON
+            ImageCapture.FLASH_MODE_ON -> CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH
+            ImageCapture.FLASH_MODE_AUTO -> CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
+            // TODO(b/209383160): porting the Quirk for AEModeDisabler
+            //      mAutoFlashAEModeDisabler.getCorrectedAeMode(
+            //      CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
+            //    )
+            else -> CaptureRequest.CONTROL_AE_MODE_ON
+        }
+
+        val preferAfMode = preferredFocusMode ?: getDefaultAfMode()
+
+        updateSignal = useCaseCamera?.requestControl?.addParametersAsync(
+            values = mapOf(
+                CaptureRequest.CONTROL_AE_MODE to getSupportedAeMode(preferAeMode),
+                CaptureRequest.CONTROL_AF_MODE to getSupportedAfMode(preferAfMode),
+                CaptureRequest.CONTROL_AWB_MODE to getSupportedAwbMode(
+                    CaptureRequest.CONTROL_AWB_MODE_AUTO
+                ),
+            )
+        ) ?: CompletableDeferred(null)
+    }
+
+    private fun getDefaultAfMode(): Int = when (template) {
+        CameraDevice.TEMPLATE_RECORD -> CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO
+        CameraDevice.TEMPLATE_PREVIEW -> CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
+        else -> CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
+    }
+
+    /**
+     * If preferredMode not available, priority is CONTINUOUS_PICTURE > AUTO > OFF
+     */
+    private fun getSupportedAfMode(preferredMode: Int) = when {
+        afModes.contains(preferredMode) -> {
+            preferredMode
+        }
+
+        afModes.contains(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) -> {
+            CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
+        }
+
+        afModes.contains(CaptureRequest.CONTROL_AF_MODE_AUTO) -> {
+            CaptureRequest.CONTROL_AF_MODE_AUTO
+        }
+
+        else -> {
+            CaptureRequest.CONTROL_AF_MODE_OFF
+        }
+    }
+
+    /**
+     * If preferredMode not available, priority is AE_ON > AE_OFF
+     */
+    private fun getSupportedAeMode(preferredMode: Int) = when {
+        aeModes.contains(preferredMode) -> {
+            preferredMode
+        }
+
+        aeModes.contains(CaptureRequest.CONTROL_AE_MODE_ON) -> {
+            CaptureRequest.CONTROL_AE_MODE_ON
+        }
+
+        else -> {
+            CaptureRequest.CONTROL_AE_MODE_OFF
+        }
+    }
+
+    /**
+     * If preferredMode not available, priority is AWB_AUTO > AWB_OFF
+     */
+    private fun getSupportedAwbMode(preferredMode: Int) = when {
+        awbModes.contains(preferredMode) -> {
+            preferredMode
+        }
+
+        awbModes.contains(CaptureRequest.CONTROL_AWB_MODE_AUTO) -> {
+            CaptureRequest.CONTROL_AWB_MODE_AUTO
+        }
+
+        else -> {
+            CaptureRequest.CONTROL_AWB_MODE_OFF
+        }
+    }
+
+    private fun Collection<UseCase>.updateTemplate() {
+        SessionConfigAdapter(this).getValidSessionConfigOrNull()?.let {
+            val templateType = it.repeatingCaptureConfig.templateType
+            template = if (templateType != CaptureConfig.TEMPLATE_TYPE_NONE) {
+                templateType
+            } else {
+                DEFAULT_REQUEST_TEMPLATE
+            }
+        }
+    }
+
+    @Module
+    abstract class Bindings {
+        @Binds
+        @IntoSet
+        abstract fun provideControls(state3AControl: State3AControl): UseCaseCameraControl
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt
index ffce1ecb..7981dc9 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/TorchControl.kt
@@ -29,10 +29,10 @@
 import dagger.Binds
 import dagger.Module
 import dagger.multibindings.IntoSet
+import javax.inject.Inject
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.launch
-import javax.inject.Inject
 
 /**
  * Implementation of Torch control exposed by [CameraControlInternal].
@@ -41,6 +41,7 @@
 @CameraScope
 class TorchControl @Inject constructor(
     cameraProperties: CameraProperties,
+    private val state3AControl: State3AControl,
     private val threads: UseCaseThreads,
 ) : UseCaseCameraControl {
 
@@ -94,20 +95,10 @@
                 // TODO(b/209757083), handle the failed result of the setTorchAsync().
                 useCaseCamera.requestControl.setTorchAsync(torch).join()
 
-                if (torch) {
-                    // Hold the internal AE mode to ON while the torch is turned ON.
-                    useCaseCamera.requestControl.addParametersAsync(
-                        type = UseCaseCameraRequestControl.Type.TORCH,
-                        values = mapOf(
-                            CaptureRequest.CONTROL_AE_MODE to CaptureRequest.CONTROL_AE_MODE_ON,
-                        )
-                    )
-                } else {
-                    // Restore the AE mode after the torch control has been used.
-                    useCaseCamera.requestControl.setConfigAsync(
-                        type = UseCaseCameraRequestControl.Type.TORCH,
-                    )
-                }.join()
+                // Hold the internal AE mode to ON while the torch is turned ON.
+                state3AControl.preferredAeMode =
+                    if (torch) CaptureRequest.CONTROL_AE_MODE_ON else null
+                state3AControl.updateSignal?.join()
 
                 signal.complete(Unit)
             }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
index 3d9377d..852b4f4 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
@@ -48,7 +48,7 @@
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.async
 
-private const val DEFAULT_REQUEST_TEMPLATE = CameraDevice.TEMPLATE_PREVIEW
+internal const val DEFAULT_REQUEST_TEMPLATE = CameraDevice.TEMPLATE_PREVIEW
 
 /**
  * The RequestControl provides a couple of APIs to update the config of the camera, it also stores
@@ -66,8 +66,6 @@
     enum class Type {
         SESSION_CONFIG,
         DEFAULT,
-        FLASH,
-        TORCH,
         CAMERA2_CAMERA_CONTROL,
     }
 
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
index 8ca5d39..5c5af8b 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
@@ -31,6 +31,7 @@
 import androidx.camera.camera2.pipe.Result3A
 import androidx.camera.camera2.pipe.integration.impl.CameraProperties
 import androidx.camera.camera2.pipe.integration.impl.FocusMeteringControl
+import androidx.camera.camera2.pipe.integration.impl.State3AControl
 import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
 import androidx.camera.camera2.pipe.integration.impl.UseCaseCameraRequestControl
 import androidx.camera.camera2.pipe.integration.impl.UseCaseThreads
@@ -1028,19 +1029,84 @@
         assertFutureFocusCompleted(future, false)
     }
 
+    @Test
+    fun startFocusMetering_afAutoModeIsSet() {
+        // Arrange.
+        val action = FocusMeteringAction.Builder(point1, FocusMeteringAction.FLAG_AF).build()
+        val state3AControl = createState3AControl(CAMERA_ID_0)
+        focusMeteringControl = initFocusMeteringControl(
+            CAMERA_ID_0,
+            setOf(createPreview(Size(1920, 1080))),
+            fakeUseCaseThreads,
+            state3AControl,
+        )
+
+        // Act.
+        focusMeteringControl.startFocusAndMetering(
+            action
+        )[5, TimeUnit.SECONDS]
+
+        // Assert.
+        assertThat(
+            state3AControl.preferredFocusMode
+        ).isEqualTo(CaptureRequest.CONTROL_AF_MODE_AUTO)
+    }
+
+    @Test
+    fun startFocusMetering_AfNotInvolved_afAutoModeNotSet() {
+        // Arrange.
+        val action = FocusMeteringAction.Builder(
+            point1,
+            FocusMeteringAction.FLAG_AE or FocusMeteringAction.FLAG_AWB
+        ).build()
+        val state3AControl = createState3AControl(CAMERA_ID_0)
+        focusMeteringControl = initFocusMeteringControl(
+            CAMERA_ID_0,
+            setOf(createPreview(Size(1920, 1080))),
+            fakeUseCaseThreads,
+            state3AControl,
+        )
+
+        // Act.
+        focusMeteringControl.startFocusAndMetering(
+            action
+        )[5, TimeUnit.SECONDS]
+
+        // Assert.
+        assertThat(
+            state3AControl.preferredFocusMode
+        ).isEqualTo(null)
+    }
+
+    @Test
+    fun startAndThenCancel_afAutoModeNotSet(): Unit = runBlocking {
+        // Arrange.
+        val action = FocusMeteringAction.Builder(point1, FocusMeteringAction.FLAG_AF).build()
+        val state3AControl = createState3AControl(CAMERA_ID_0)
+        focusMeteringControl = initFocusMeteringControl(
+            CAMERA_ID_0,
+            setOf(createPreview(Size(1920, 1080))),
+            fakeUseCaseThreads,
+            state3AControl,
+        )
+
+        // Act.
+        focusMeteringControl.startFocusAndMetering(
+            action
+        )[5, TimeUnit.SECONDS]
+        focusMeteringControl.cancelFocusAndMeteringAsync().join()
+
+        // Assert.
+        assertThat(
+            state3AControl.preferredFocusMode
+        ).isEqualTo(null)
+    }
+
     // TODO: Port the following tests once their corresponding logics have been implemented.
     //  - [b/255679866] triggerAfWithTemplate, triggerAePrecaptureWithTemplate,
     //          cancelAfAeTriggerWithTemplate
     //  - startFocusAndMetering_AfRegionCorrectedByQuirk
     //  - [b/262225455] cropRegionIsSet_resultBasedOnCropRegion
-    //  The following ones will depend on how exactly they will be implemented.
-    //  - [b/264018162] addFocusMeteringOptions_hasCorrectAfMode,
-    //                  startFocusMetering_isAfAutoModeIsTrue,
-    //                  startFocusMetering_AfNotInvolved_isAfAutoModeIsSet,
-    //                  startAndThenCancel_isAfAutoModeIsFalse
-    //      (an alternative way can be checking the AF mode
-    //      at the frame with AF_TRIGGER_START request in capture callback, but this requires
-    //      invoking actual camera operations, ref: TapToFocusDeviceTest)
 
     private fun assertFutureFocusCompleted(
         future: ListenableFuture<FocusMeteringResult>,
@@ -1135,8 +1201,11 @@
         cameraId: String,
         useCases: Set<UseCase> = emptySet(),
         useCaseThreads: UseCaseThreads = fakeUseCaseThreads,
+        state3AControl: State3AControl = createState3AControl(cameraId),
     ) = FocusMeteringControl(
-            cameraPropertiesMap[cameraId]!!, useCaseThreads
+            cameraPropertiesMap[cameraId]!!,
+            state3AControl,
+            useCaseThreads
         ).apply {
             fakeUseCaseCamera.runningUseCasesLiveData.value = useCases
             useCaseCamera = fakeUseCaseCamera
@@ -1260,4 +1329,12 @@
                     StreamSpec.builder(suggestedStreamSpecResolution).build()
                 )
             }
+
+    private fun createState3AControl(
+        cameraId: String = CAMERA_ID_0,
+        properties: CameraProperties = cameraPropertiesMap[cameraId]!!,
+        useCaseCamera: UseCaseCamera = fakeUseCaseCamera,
+    ) = State3AControl(properties).apply {
+        this.useCaseCamera = useCaseCamera
+    }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
index 6337c98..f6a563e 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
@@ -177,15 +177,21 @@
 
     @Before
     fun setUp() {
+        val fakeUseCaseCamera = FakeUseCaseCamera(requestControl = fakeRequestControl)
+        val fakeCameraProperties = FakeCameraProperties(
+            FakeCameraMetadata(
+                mapOf(CameraCharacteristics.FLASH_INFO_AVAILABLE to true),
+            )
+        )
+
         torchControl = TorchControl(
-            FakeCameraProperties(
-                FakeCameraMetadata(
-                    mapOf(CameraCharacteristics.FLASH_INFO_AVAILABLE to true),
-                )
-            ),
+            fakeCameraProperties,
+            State3AControl(fakeCameraProperties).apply {
+                useCaseCamera = fakeUseCaseCamera
+            },
             fakeUseCaseThreads,
         ).also {
-            it.useCaseCamera = FakeUseCaseCamera(requestControl = fakeRequestControl)
+            it.useCaseCamera = fakeUseCaseCamera
 
             // Ensure the control is updated after the UseCaseCamera been set.
             assertThat(
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/TorchControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/TorchControlTest.kt
index be46710..6b24bc2 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/TorchControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/TorchControlTest.kt
@@ -147,33 +147,50 @@
 
     @Before
     fun setUp() {
+        val fakeUseCaseCamera = FakeUseCaseCamera()
+        val fakeCameraProperties = FakeCameraProperties(metadata)
         torchControl = TorchControl(
-            FakeCameraProperties(metadata),
+            fakeCameraProperties,
+            State3AControl(fakeCameraProperties).apply {
+                useCaseCamera = fakeUseCaseCamera
+            },
             fakeUseCaseThreads,
         )
-        torchControl.useCaseCamera = FakeUseCaseCamera()
+        torchControl.useCaseCamera = fakeUseCaseCamera
     }
 
     @Test
     fun enableTorch_whenNoFlashUnit(): Unit = runBlocking {
         assertThrows<IllegalStateException> {
+            val fakeUseCaseCamera = FakeUseCaseCamera()
+            val fakeCameraProperties = FakeCameraProperties()
+
             // Without a flash unit, this Job will complete immediately with a IllegalStateException
             TorchControl(
-                FakeCameraProperties(),
+                fakeCameraProperties,
+                State3AControl(fakeCameraProperties).apply {
+                    useCaseCamera = fakeUseCaseCamera
+                },
                 fakeUseCaseThreads,
             ).also {
-                it.useCaseCamera = FakeUseCaseCamera()
+                it.useCaseCamera = fakeUseCaseCamera
             }.setTorchAsync(true).await()
         }
     }
 
     @Test
     fun getTorchState_whenNoFlashUnit() {
+        val fakeUseCaseCamera = FakeUseCaseCamera()
+        val fakeCameraProperties = FakeCameraProperties()
+
         val torchState = TorchControl(
-            FakeCameraProperties(),
+            fakeCameraProperties,
+            State3AControl(fakeCameraProperties).apply {
+                useCaseCamera = fakeUseCaseCamera
+            },
             fakeUseCaseThreads,
         ).also {
-            it.useCaseCamera = FakeUseCaseCamera()
+            it.useCaseCamera = fakeUseCaseCamera
         }.torchStateLiveData.value
 
         Truth.assertThat(torchState).isEqualTo(TorchState.OFF)
@@ -182,8 +199,14 @@
     @Test
     fun enableTorch_whenInactive(): Unit = runBlocking {
         assertThrows<CameraControl.OperationCanceledException> {
+            val fakeUseCaseCamera = FakeUseCaseCamera()
+            val fakeCameraProperties = FakeCameraProperties(metadata)
+
             TorchControl(
-                FakeCameraProperties(metadata),
+                fakeCameraProperties,
+                State3AControl(fakeCameraProperties).apply {
+                    useCaseCamera = fakeUseCaseCamera
+                },
                 fakeUseCaseThreads,
             ).setTorchAsync(true).await()
         }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
index 5c6e224..e98ffc8 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
@@ -29,6 +29,7 @@
 import androidx.camera.camera2.pipe.integration.impl.CameraProperties
 import androidx.camera.camera2.pipe.integration.impl.EvCompControl
 import androidx.camera.camera2.pipe.integration.impl.FocusMeteringControl
+import androidx.camera.camera2.pipe.integration.impl.State3AControl
 import androidx.camera.camera2.pipe.integration.impl.TorchControl
 import androidx.camera.camera2.pipe.integration.impl.UseCaseThreads
 import androidx.camera.camera2.pipe.integration.impl.ZoomControl
@@ -79,21 +80,28 @@
             cameraId
         ),
         zoomControl: ZoomControl = this.zoomControl,
-    ) = CameraInfoAdapter(
-        cameraProperties,
-        CameraConfig(cameraId),
-        CameraStateAdapter(),
-        CameraControlStateAdapter(
-            zoomControl,
-            EvCompControl(FakeEvCompCompat()),
-            TorchControl(cameraProperties, useCaseThreads),
-        ),
-        CameraCallbackMap(),
-        FocusMeteringControl(
-            cameraProperties,
-            useCaseThreads
-        ).apply {
-            useCaseCamera = FakeUseCaseCamera()
+    ): CameraInfoAdapter {
+        val fakeUseCaseCamera = FakeUseCaseCamera()
+        val state3AControl = State3AControl(cameraProperties).apply {
+            useCaseCamera = fakeUseCaseCamera
         }
-    )
+        return CameraInfoAdapter(
+            cameraProperties,
+            CameraConfig(cameraId),
+            CameraStateAdapter(),
+            CameraControlStateAdapter(
+                zoomControl,
+                EvCompControl(FakeEvCompCompat()),
+                TorchControl(cameraProperties, state3AControl, useCaseThreads),
+            ),
+            CameraCallbackMap(),
+            FocusMeteringControl(
+                cameraProperties,
+                state3AControl,
+                useCaseThreads
+            ).apply {
+                useCaseCamera = fakeUseCaseCamera
+            }
+        )
+    }
 }
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathMeasureTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathMeasureTest.kt
index afacfe3..64a36aa 100644
--- a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathMeasureTest.kt
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathMeasureTest.kt
@@ -41,9 +41,9 @@
 
         val tangent = pathMeasure.getTangent(distance * 0.5f)
 
-        Assert.assertEquals(50f, position.x)
-        Assert.assertEquals(50f, position.y)
-        Assert.assertEquals(0.707106f, tangent.x, 0.00001f)
-        Assert.assertEquals(0.707106f, tangent.y, 0.00001f)
+        Assert.assertEquals(50f, position.x, 0.0001f)
+        Assert.assertEquals(50f, position.y, 0.0001f)
+        Assert.assertEquals(0.707106f, tangent.x, 0.0001f)
+        Assert.assertEquals(0.707106f, tangent.y, 0.0001f)
     }
 }
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceServiceTest.java b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceServiceTest.java
index bc848ab..4742770 100644
--- a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceServiceTest.java
+++ b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceServiceTest.java
@@ -32,13 +32,14 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.wear.protolayout.expression.DynamicBuilders;
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString;
 import androidx.wear.watchface.complications.data.ComplicationData;
 import androidx.wear.watchface.complications.data.ComplicationText;
 import androidx.wear.watchface.complications.data.ComplicationType;
 import androidx.wear.watchface.complications.data.LongTextComplicationData;
 import androidx.wear.watchface.complications.data.PlainComplicationText;
-import androidx.wear.watchface.complications.data.StringExpression;
-import androidx.wear.watchface.complications.data.StringExpressionComplicationText;
+import androidx.wear.watchface.complications.data.ComplicationTextExpression;
 
 import org.junit.After;
 import org.junit.Before;
@@ -195,8 +196,9 @@
             throws Exception {
         mService.responseData =
                 new LongTextComplicationData.Builder(
-                        new StringExpressionComplicationText(
-                                new StringExpression(new byte[]{1, 2})),
+                        new ComplicationTextExpression(
+                                DynamicString.constant("hello").concat(
+                                        DynamicString.constant(" world"))),
                         ComplicationText.EMPTY)
                         .build();
 
@@ -209,8 +211,9 @@
         verify(mRemoteManager).updateComplicationData(
                 eq(123),
                 eq(new LongTextComplicationData.Builder(
-                        new StringExpressionComplicationText(
-                                new StringExpression(new byte[]{1, 2})),
+                        new ComplicationTextExpression(
+                                DynamicString.constant("hello").concat(
+                                        DynamicString.constant(" world"))),
                         ComplicationText.EMPTY)
                         .build()
                         .asWireComplicationData()));
@@ -222,8 +225,9 @@
             throws Exception {
         mService.responseData =
                 new LongTextComplicationData.Builder(
-                        new StringExpressionComplicationText(
-                                new StringExpression(new byte[]{1, 2})),
+                        new ComplicationTextExpression(
+                                DynamicString.constant("hello").concat(
+                                        DynamicString.constant(" world"))),
                         ComplicationText.EMPTY)
                         .build();
 
@@ -236,9 +240,10 @@
         verify(mRemoteManager).updateComplicationData(
                 eq(123),
                 eq(new LongTextComplicationData.Builder(
-                        // TODO(b/260065006): Verify that it is actually evaluated.
-                        new StringExpressionComplicationText(
-                                new StringExpression(new byte[]{1, 2})),
+                        // TODO(b/260065006): new PlainComplicationText.Builder("hello world")
+                        new ComplicationTextExpression(
+                                DynamicString.constant("hello").concat(
+                                        DynamicString.constant(" world"))),
                         ComplicationText.EMPTY)
                         .build()
                         .asWireComplicationData()));
diff --git a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java
index 4dccf73..8ea484e 100644
--- a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java
+++ b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java
@@ -132,9 +132,9 @@
         assertThat(TIMELINE_A.toString()).isEqualTo(
                 "ComplicationDataTimeline(defaultComplicationData=ShortTextComplicationData("
                         + "text=ComplicationText{mSurroundingText=Hello, mTimeDependentText=null, "
-                        + "mStringExpression=null}, title=null, monochromaticImage=null, "
+                        + "mExpression=null}, title=null, monochromaticImage=null, "
                         + "smallImage=null, contentDescription=ComplicationText{"
-                        + "mSurroundingText=, mTimeDependentText=null, mStringExpression=null}, "
+                        + "mSurroundingText=, mTimeDependentText=null, mExpression=null}, "
                         + "tapActionLostDueToSerialization=false, tapAction=null, "
                         + "validTimeRange=TimeRange(startDateTimeMillis="
                         + "-1000000000-01-01T00:00:00Z, endDateTimeMillis="
@@ -143,10 +143,10 @@
                         + "TimelineEntry(validity=TimeInterval(start=1970-01-02T03:46:40Z, "
                         + "end=1970-01-03T07:33:20Z), complicationData=ShortTextComplicationData("
                         + "text=ComplicationText{mSurroundingText=Updated, "
-                        + "mTimeDependentText=null, mStringExpression=null}, title=null, "
+                        + "mTimeDependentText=null, mExpression=null}, title=null, "
                         + "monochromaticImage=null, smallImage=null, "
                         + "contentDescription=ComplicationText{mSurroundingText=, "
-                        + "mTimeDependentText=null, mStringExpression=null}, "
+                        + "mTimeDependentText=null, mExpression=null}, "
                         + "tapActionLostDueToSerialization=false, tapAction=null, "
                         + "validTimeRange=TimeRange(startDateTimeMillis="
                         + "-1000000000-01-01T00:00:00Z, endDateTimeMillis="
diff --git a/wear/watchface/watchface-complications-data/api/current.txt b/wear/watchface/watchface-complications-data/api/current.txt
index c657cfb..40675d8 100644
--- a/wear/watchface/watchface-complications-data/api/current.txt
+++ b/wear/watchface/watchface-complications-data/api/current.txt
@@ -94,9 +94,6 @@
     field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
   }
 
-  public final class FloatExpressionKt {
-  }
-
   @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class GoalProgressComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
     method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
     method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
diff --git a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
index 0bb17ae..d49c322 100644
--- a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
@@ -97,9 +97,6 @@
     field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
   }
 
-  public final class FloatExpressionKt {
-  }
-
   @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class GoalProgressComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
     method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
     method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
diff --git a/wear/watchface/watchface-complications-data/api/restricted_current.txt b/wear/watchface/watchface-complications-data/api/restricted_current.txt
index f0e643f..58155a5 100644
--- a/wear/watchface/watchface-complications-data/api/restricted_current.txt
+++ b/wear/watchface/watchface-complications-data/api/restricted_current.txt
@@ -94,9 +94,6 @@
     field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
   }
 
-  public final class FloatExpressionKt {
-  }
-
   @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class GoalProgressComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
     method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
     method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
diff --git a/wear/watchface/watchface-complications-data/build.gradle b/wear/watchface/watchface-complications-data/build.gradle
index 953db5b..3b9d23c 100644
--- a/wear/watchface/watchface-complications-data/build.gradle
+++ b/wear/watchface/watchface-complications-data/build.gradle
@@ -30,6 +30,11 @@
     api("androidx.versionedparcelable:versionedparcelable:1.1.0")
     api(libs.kotlinStdlib)
     api(libs.kotlinCoroutinesAndroid)
+
+    // TODO(b/267170061): Use released protolayout-expression, remove protolayout-proto.
+    api(project(":wear:protolayout:protolayout-expression"))
+    implementation(project(":wear:protolayout:protolayout-proto"))
+
     implementation("androidx.core:core:1.1.0")
     implementation("androidx.preference:preference:1.1.0")
     implementation("androidx.annotation:annotation:1.2.0")
@@ -44,6 +49,7 @@
     testImplementation(libs.junit)
     testImplementation(libs.kotlinTest)
     testImplementation(libs.testParameterInjector)
+    testImplementation(libs.guavaTestlib)
 
     annotationProcessor(project(":versionedparcelable:versionedparcelable-compiler"))
 }
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
index 41954c9..813758c 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
@@ -30,12 +30,11 @@
 import androidx.annotation.IntDef
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
 import androidx.wear.watchface.complications.data.ComplicationDisplayPolicies
 import androidx.wear.watchface.complications.data.ComplicationDisplayPolicy
 import androidx.wear.watchface.complications.data.ComplicationPersistencePolicies
 import androidx.wear.watchface.complications.data.ComplicationPersistencePolicy
-import androidx.wear.watchface.complications.data.FloatExpression
-import androidx.wear.watchface.complications.data.toFloatExpression
 import androidx.wear.watchface.utility.iconEquals
 import androidx.wear.watchface.utility.iconHashCode
 import java.io.IOException
@@ -43,6 +42,7 @@
 import java.io.ObjectInputStream
 import java.io.ObjectOutputStream
 import java.io.Serializable
+import java.util.Arrays
 import java.util.Objects
 
 /**
@@ -179,7 +179,7 @@
             }
             if (isFieldValidForType(FIELD_VALUE_EXPRESSION, type)) {
                 oos.writeNullable(complicationData.rangedValueExpression) {
-                    oos.writeByteArray(it.asByteArray())
+                    oos.writeByteArray(it.toDynamicFloatByteArray())
                 }
             }
             if (isFieldValidForType(FIELD_VALUE_TYPE, type)) {
@@ -607,10 +607,11 @@
      * Valid only if the type of this complication data is [TYPE_RANGED_VALUE] and
      * [TYPE_GOAL_PROGRESS].
      */
-    val rangedValueExpression: FloatExpression?
+    val rangedValueExpression: DynamicFloat?
         get() {
             checkFieldValidForTypeWithoutThrowingException(FIELD_VALUE_EXPRESSION, type)
-            return fields.getByteArray(FIELD_VALUE_EXPRESSION)?.toFloatExpression()
+            return fields.getByteArray(FIELD_VALUE_EXPRESSION)
+                ?.let { DynamicFloat.fromByteArray(it) }
         }
 
     /**
@@ -1185,7 +1186,8 @@
             equalsWithoutExpressions(other) &&
             (!isFieldValidForType(FIELD_VALUE, type) || rangedValue == other.rangedValue) &&
             (!isFieldValidForType(FIELD_VALUE_EXPRESSION, type) ||
-                rangedValueExpression == other.rangedValueExpression) &&
+                rangedValueExpression?.toDynamicFloatByteArray() contentEquals
+                other.rangedValueExpression?.toDynamicFloatByteArray()) &&
             (!isFieldValidForType(FIELD_SHORT_TITLE, type) || shortTitle == other.shortTitle) &&
             (!isFieldValidForType(FIELD_SHORT_TEXT, type) || shortText == other.shortText) &&
             (!isFieldValidForType(FIELD_LONG_TITLE, type) || longTitle == other.longTitle) &&
@@ -1202,7 +1204,8 @@
             ) {
                 !isFieldValidForType(FIELD_VALUE, type) || rangedValue == other.rangedValue
             } else {
-                rangedValueExpression == other.rangedValueExpression
+                rangedValueExpression?.toDynamicFloatByteArray() contentEquals
+                    other.rangedValueExpression?.toDynamicFloatByteArray()
             } &&
             (!isFieldValidForType(FIELD_SHORT_TITLE, type) ||
                 shortTitle equalsUnevaluated other.shortTitle) &&
@@ -1219,11 +1222,14 @@
                     ((placeholder != null && other.placeholder != null) &&
                         placeholder!! equalsUnevaluated other.placeholder!!)))
 
-    private infix fun ComplicationText?.equalsUnevaluated(other: ComplicationText?): Boolean =
-        (this == null && other == null) ||
-            ((this != null && other != null) &&
-                if (stringExpression == null) equals(other)
-                else stringExpression == other.stringExpression)
+    private infix fun ComplicationText?.equalsUnevaluated(other: ComplicationText?): Boolean {
+        if (this == null && other == null) return true
+        if (this == null || other == null) return false
+        // Both are non-null.
+        if (stringExpression == null) return equals(other)
+        return stringExpression?.toDynamicStringByteArray() contentEquals
+            other.stringExpression?.toDynamicStringByteArray()
+    }
 
     private fun equalsWithoutExpressions(other: ComplicationData): Boolean =
         this === other || (
@@ -1305,7 +1311,11 @@
         if (isFieldValidForType(EXP_FIELD_LIST_ENTRIES, type)) listEntries else null,
         if (isFieldValidForType(FIELD_DATA_SOURCE, type)) dataSource else null,
         if (isFieldValidForType(FIELD_VALUE, type)) rangedValue else null,
-        if (isFieldValidForType(FIELD_VALUE_EXPRESSION, type)) rangedValueExpression else null,
+        if (isFieldValidForType(FIELD_VALUE_EXPRESSION, type)) {
+            Arrays.hashCode(rangedValueExpression?.toDynamicFloatByteArray())
+        } else {
+            null
+        },
         if (isFieldValidForType(FIELD_VALUE_TYPE, type)) rangedValueType else null,
         if (isFieldValidForType(FIELD_MIN_VALUE, type)) rangedMinValue else null,
         if (isFieldValidForType(FIELD_MAX_VALUE, type)) rangedMaxValue else null,
@@ -1467,8 +1477,8 @@
          *
          * @throws IllegalStateException if this field is not valid for the complication type
          */
-        fun setRangedValueExpression(value: FloatExpression?) =
-            apply { putOrRemoveField(FIELD_VALUE_EXPRESSION, value?.asByteArray()) }
+        fun setRangedValueExpression(value: DynamicFloat?) =
+            apply { putOrRemoveField(FIELD_VALUE_EXPRESSION, value?.toDynamicFloatByteArray()) }
 
         /**
          * Sets the *value type* field which provides meta data about the value. This is
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java
index 58f43c9..cefbd65 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java
@@ -36,8 +36,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
-import androidx.annotation.VisibleForTesting;
-import androidx.wear.watchface.complications.data.StringExpression;
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString;
 
 import java.io.IOException;
 import java.io.InvalidObjectException;
@@ -46,6 +45,7 @@
 import java.io.Serializable;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
 import java.util.Objects;
 import java.util.TimeZone;
 import java.util.concurrent.TimeUnit;
@@ -76,17 +76,32 @@
             return false;
         }
         ComplicationText that = (ComplicationText) o;
-        if (!Objects.equals(mStringExpression, that.mStringExpression)) {
-            return false;
+        if (mExpression == null) {
+            if (that.mExpression != null) {
+                return false;
+            }
+        } else {
+            if (that.mExpression == null) {
+                return false;
+            } else if (
+                    !Arrays.equals(mExpression.toDynamicStringByteArray(),
+                            that.mExpression.toDynamicStringByteArray())
+            ) {
+                return false;
+            }
         }
         if (!Objects.equals(mTimeDependentText, that.mTimeDependentText)) {
             return false;
         }
         if (mSurroundingText == null) {
-            return that.mSurroundingText == null;
-        } else {
             if (that.mSurroundingText != null) {
-                return mSurroundingText.toString().contentEquals(that.mSurroundingText);
+                return false;
+            }
+        } else {
+            if (that.mSurroundingText == null) {
+                return false;
+            } else if (!mSurroundingText.toString().contentEquals(that.mSurroundingText)) {
+                return false;
             }
         }
         return true;
@@ -94,16 +109,20 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(mSurroundingText, mTimeDependentText, mStringExpression);
+        return Objects.hash(
+                mSurroundingText,
+                mTimeDependentText,
+                mExpression == null ?
+                        null : Arrays.hashCode(mExpression.toDynamicStringByteArray()));
     }
 
     @NonNull
     @Override
     public String toString() {
-        return "ComplicationText{" + "mSurroundingText="
-                + ComplicationData.maybeRedact(mSurroundingText)
-                + ", mTimeDependentText=" + mTimeDependentText + ", mStringExpression="
-                + mStringExpression + "}";
+        return "ComplicationText{"
+                + "mSurroundingText=" + ComplicationData.maybeRedact(mSurroundingText)
+                + ", mTimeDependentText=" + mTimeDependentText
+                + ", mExpression=" + mExpression + "}";
     }
 
     /** @hide */
@@ -274,9 +293,9 @@
     @Nullable
     private final TimeDependentText mTimeDependentText;
 
-    /** A [StringExpression] which will be evaluated by the system on the WatchFace's behalf. */
+    /** A {@link DynamicString} which will be evaluated by the system on the WatchFace's behalf. */
     @Nullable
-    private final StringExpression mStringExpression;
+    private final DynamicString mExpression;
 
     /** Used to replace occurrences of ^1 with time dependent text and ignore ^[2-9]. */
     private final CharSequence[] mTemplateValues =
@@ -290,29 +309,29 @@
     private ComplicationText(
             @Nullable CharSequence surroundingText,
             @Nullable TimeDependentText timeDependentText,
-            @Nullable StringExpression stringExpression) {
+            @Nullable DynamicString expression) {
         mSurroundingText = surroundingText;
         mTimeDependentText = timeDependentText;
-        mStringExpression = stringExpression;
+        mExpression = expression;
         checkFields();
     }
 
     public ComplicationText(@NonNull CharSequence surroundingText) {
-        this(surroundingText, /* timeDependentText = */ null, /* stringExpression = */ null);
+        this(surroundingText, /* timeDependentText = */ null, /* expression = */ null);
     }
 
     public ComplicationText(
             @NonNull CharSequence surroundingText, @NonNull TimeDependentText timeDependentText) {
-        this(surroundingText, timeDependentText, /* stringExpression = */ null);
+        this(surroundingText, timeDependentText, /* expression = */ null);
     }
 
     public ComplicationText(
-            @NonNull CharSequence surroundingText, @NonNull StringExpression stringExpression) {
-        this(surroundingText, /* timeDependentText = */ null, stringExpression);
+            @NonNull CharSequence surroundingText, @NonNull DynamicString expression) {
+        this(surroundingText, /* timeDependentText = */ null, expression);
     }
 
-    public ComplicationText(@NonNull StringExpression stringExpression) {
-        this(/* surroundingText = */ null, /* timeDependentText = */ null, stringExpression);
+    public ComplicationText(@NonNull DynamicString expression) {
+        this(/* surroundingText = */ null, /* timeDependentText = */ null, expression);
     }
 
     private ComplicationText(@NonNull Parcel in) {
@@ -320,9 +339,9 @@
         mSurroundingText = bundle.getCharSequence(KEY_SURROUNDING_STRING);
 
         if (bundle.containsKey(KEY_STRING_EXPRESSION)) {
-            mStringExpression = new StringExpression(bundle.getByteArray(KEY_STRING_EXPRESSION));
+            mExpression = DynamicString.fromByteArray(bundle.getByteArray(KEY_STRING_EXPRESSION));
         } else {
-            mStringExpression = null;
+            mExpression = null;
         }
 
         if (bundle.containsKey(KEY_DIFFERENCE_STYLE)
@@ -355,23 +374,23 @@
     private static class SerializedForm implements Serializable {
         CharSequence mSurroundingText;
         TimeDependentText mTimeDependentText;
-        StringExpression mStringExpression;
+        DynamicString mExpression;
 
         SerializedForm(@Nullable CharSequence surroundingText,
                 @Nullable TimeDependentText timeDependentText,
-                @Nullable StringExpression stringExpression) {
+                @Nullable DynamicString expression) {
             mSurroundingText = surroundingText;
             mTimeDependentText = timeDependentText;
-            mStringExpression = stringExpression;
+            mExpression = expression;
         }
 
         private void writeObject(ObjectOutputStream oos) throws IOException {
             CharSequenceSerializableHelper.writeToStream(mSurroundingText, oos);
             oos.writeObject(mTimeDependentText);
-            if (mStringExpression == null) {
+            if (mExpression == null) {
                 oos.writeInt(0);
             } else {
-                byte[] bytes = mStringExpression.asByteArray();
+                byte[] bytes = mExpression.toDynamicStringByteArray();
                 oos.writeInt(bytes.length);
                 oos.write(bytes);
             }
@@ -382,22 +401,22 @@
             mTimeDependentText = (TimeDependentText) ois.readObject();
             int length = ois.readInt();
             if (length == 0) {
-                mStringExpression = null;
+                mExpression = null;
             } else {
                 byte[] bytes = new byte[length];
                 ois.readFully(bytes);
-                mStringExpression = new StringExpression(bytes);
+                mExpression = DynamicString.fromByteArray(bytes);
             }
         }
 
         @SuppressLint("SyntheticAccessor")
         Object readResolve() {
-            return new ComplicationText(mSurroundingText, mTimeDependentText, mStringExpression);
+            return new ComplicationText(mSurroundingText, mTimeDependentText, mExpression);
         }
     }
 
     Object writeReplace() {
-        return new SerializedForm(mSurroundingText, mTimeDependentText, mStringExpression);
+        return new SerializedForm(mSurroundingText, mTimeDependentText, mExpression);
     }
 
     private void readObject(ObjectInputStream stream) throws InvalidObjectException {
@@ -421,10 +440,9 @@
     }
 
     private void checkFields() {
-        if (mSurroundingText == null && mTimeDependentText == null && mStringExpression == null) {
+        if (mSurroundingText == null && mTimeDependentText == null && mExpression == null) {
             throw new IllegalStateException(
-                    "One of mSurroundingText, mTimeDependentText and mStringExpression must be"
-                            + " non-null");
+                    "One of mSurroundingText, mTimeDependentText and mExpression must be non-null");
         }
     }
 
@@ -439,8 +457,8 @@
         Bundle bundle = new Bundle();
         bundle.putCharSequence(KEY_SURROUNDING_STRING, mSurroundingText);
 
-        if (mStringExpression != null) {
-            bundle.putByteArray(KEY_STRING_EXPRESSION, mStringExpression.asByteArray());
+        if (mExpression != null) {
+            bundle.putByteArray(KEY_STRING_EXPRESSION, mExpression.toDynamicStringByteArray());
         }
 
         if (mTimeDependentText != null) {
@@ -476,7 +494,7 @@
     @NonNull
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     public TimeDependentText getTimeDependentText() {
-        if (mStringExpression != null) {
+        if (mExpression != null) {
             throw new UnsupportedOperationException("getTimeDependentText not supported for "
                     + "StringExpressions");
         }
@@ -500,7 +518,7 @@
     @NonNull
     @Override
     public CharSequence getTextAt(@NonNull Resources resources, long dateTimeMillis) {
-        if (mStringExpression != null && mTimeDependentText == null && mSurroundingText == null) {
+        if (mExpression != null && mTimeDependentText == null && mSurroundingText == null) {
             throw new UnsupportedOperationException("getTextAt not supported for "
                     + "StringExpressions");
         }
@@ -533,10 +551,10 @@
         return mSurroundingText;
     }
 
-    /** Returns the {@link StringExpression} to be evaluated to display this text. */
+    /** Returns the {@link DynamicString} to be evaluated to display this text. */
     @Nullable
-    public StringExpression getStringExpression() {
-        return mStringExpression;
+    public DynamicString getStringExpression() {
+        return mExpression;
     }
 
     /** Whether or not this is a placeholder. */
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
index 90dc690..444a7d1 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
@@ -31,6 +31,7 @@
 import androidx.annotation.IntDef
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
 import androidx.wear.watchface.complications.data.GoalProgressComplicationData.Companion.PLACEHOLDER
 import androidx.wear.watchface.complications.data.RangedValueComplicationData.Companion.PLACEHOLDER
 import androidx.wear.watchface.complications.data.RangedValueComplicationData.Companion.TYPE_RATING
@@ -847,7 +848,7 @@
 public class RangedValueComplicationData internal constructor(
     public val value: Float,
     @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
-    valueExpression: FloatExpression?,
+    valueExpression: DynamicFloat?,
     public val min: Float,
     public val max: Float,
     public val monochromaticImage: MonochromaticImage?,
@@ -873,13 +874,13 @@
     displayPolicy = displayPolicy
 ) {
     /**
-     * The [FloatExpression] optionally set by the data source. If present the system will
+     * The [DynamicFloat] optionally set by the data source. If present the system will
      * dynamically evaluate this and store the result in [value]. Watch faces can typically ignore
      * this field.
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public val valueExpression: FloatExpression? = valueExpression
+    public val valueExpression: DynamicFloat? = valueExpression
 
     /** @hide */
     @IntDef(value = [TYPE_UNDEFINED, TYPE_RATING, TYPE_PERCENTAGE])
@@ -896,7 +897,7 @@
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public constructor(
         private val value: Float,
-        private val valueExpression: FloatExpression?,
+        private val valueExpression: DynamicFloat?,
         private val min: Float,
         private val max: Float,
         private var contentDescription: ComplicationText
@@ -920,9 +921,9 @@
         ) : this(value, valueExpression = null, min, max, contentDescription)
 
         /**
-         * Creates a [Builder] for a [RangedValueComplicationData] with a [FloatExpression] value.
+         * Creates a [Builder] for a [RangedValueComplicationData] with a [DynamicFloat] value.
          *
-         * @param valueExpression The [FloatExpression] of the ranged complication which will be
+         * @param valueExpression The [DynamicFloat] of the ranged complication which will be
          * evaluated into a value dynamically, and should be in the range [[min]] .. [[max]]. The
          * semantic meaning of value can be specified via [setValueType].
          * @param min The minimum value. For [TYPE_PERCENTAGE] this must be 0f.
@@ -934,7 +935,7 @@
          */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         public constructor(
-            valueExpression: FloatExpression,
+            valueExpression: DynamicFloat,
             min: Float,
             max: Float,
             contentDescription: ComplicationText
@@ -1203,7 +1204,7 @@
 internal constructor(
     public val value: Float,
     @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
-    valueExpression: FloatExpression?,
+    valueExpression: DynamicFloat?,
     public val targetValue: Float,
     public val monochromaticImage: MonochromaticImage?,
     public val smallImage: SmallImage?,
@@ -1227,13 +1228,13 @@
     displayPolicy = displayPolicy
 ) {
     /**
-     * The [FloatExpression] optionally set by the data source. If present the system will
+     * The [DynamicFloat] optionally set by the data source. If present the system will
      * dynamically evaluate this and store the result in [value]. Watch faces can typically ignore
      * this field.
      * @hide
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public val valueExpression: FloatExpression? = valueExpression
+    public val valueExpression: DynamicFloat? = valueExpression
 
     /**
      * Builder for [GoalProgressComplicationData].
@@ -1247,7 +1248,7 @@
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public constructor(
         private val value: Float,
-        private val valueExpression: FloatExpression?,
+        private val valueExpression: DynamicFloat?,
         private val targetValue: Float,
         private var contentDescription: ComplicationText
     ) : BaseBuilder<Builder, GoalProgressComplicationData>() {
@@ -1266,9 +1267,9 @@
         ) : this(value, valueExpression = null, targetValue, contentDescription)
 
         /**
-         * Creates a [Builder] for a [GoalProgressComplicationData] with a [FloatExpression] value.
+         * Creates a [Builder] for a [GoalProgressComplicationData] with a [DynamicFloat] value.
          *
-         * @param valueExpression The [FloatExpression] of the goal complication which will be
+         * @param valueExpression The [DynamicFloat] of the goal complication which will be
          * evaluated into a value dynamically, and should be >= 0.
          * @param targetValue The target value. This must be less than [Float.MAX_VALUE].
          * @param contentDescription Localized description for use by screen readers. Please do not
@@ -1277,7 +1278,7 @@
          */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         public constructor(
-            valueExpression: FloatExpression,
+            valueExpression: DynamicFloat,
             targetValue: Float,
             contentDescription: ComplicationText
         ) : this(
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/FloatExpression.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/FloatExpression.kt
deleted file mode 100644
index 5d2215e..0000000
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/FloatExpression.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.watchface.complications.data
-
-import androidx.annotation.RestrictTo
-
-/**
- * Placeholder for FloatExpression implementation by tiles.
- * @hide
- */
-// TODO(b/260065006): Replace this with the real implementation.
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-abstract class FloatExpression {
-    abstract fun asByteArray(): ByteArray
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        // Not checking for exact same class because it's not implemented yet.
-        if (other !is FloatExpression) return false
-        return asByteArray().contentEquals(other.asByteArray())
-    }
-
-    override fun hashCode() = asByteArray().contentHashCode()
-
-    override fun toString() = "FloatExpressionPlaceholder${asByteArray().contentToString()}"
-}
-
-/**
- * Placeholder parser for [FloatExpression] from [ByteArray].
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-fun ByteArray.toFloatExpression() = object : FloatExpression() {
-    override fun asByteArray() = this@toFloatExpression
-}
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
index 5887dd6..22f7fc7 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
@@ -34,6 +34,7 @@
 import android.text.style.TypefaceSpan
 import android.text.style.UnderlineSpan
 import androidx.annotation.RestrictTo
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
 import java.time.Instant
 import java.util.concurrent.TimeUnit
 
@@ -628,43 +629,15 @@
     DelegatingTimeDependentText(this)
 
 /**
- * This is a placeholder for the tiles StringExpression which isn't currently available. We'll
- * remove this later in favor of the real thing.
- * @hide
- */
-// TODO(b/260065006): Remove this in favor of the real thing when available.
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class StringExpression(private val expression: ByteArray) {
-    fun asByteArray() = expression
-
-    override fun toString(): String {
-        return "StringExpression(expression=${expression.contentToString()})"
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-
-        other as StringExpression
-
-        if (!expression.contentEquals(other.expression)) return false
-
-        return true
-    }
-
-    override fun hashCode() = expression.contentHashCode()
-}
-
-/**
- * A [ComplicationText] where the system evaluates a [StringExpression] on behalf of the watch face.
+ * A [ComplicationText] where the system evaluates a [DynamicString] on behalf of the watch face.
  * By the time this reaches the watch face's Renderer, it'll have been converted to a plain
  * ComplicationText.
  *
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class StringExpressionComplicationText(
-    public val expression: StringExpression
+public class ComplicationTextExpression(
+    public val expression: DynamicString
 ) : ComplicationText {
     private val delegate = DelegatingComplicationText(WireComplicationText(expression))
 
@@ -695,7 +668,7 @@
         if (this === other) return true
         if (javaClass != other?.javaClass) return false
 
-        other as StringExpressionComplicationText
+        other as ComplicationTextExpression
 
         if (delegate != other.delegate) return false
 
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataEqualityTest.kt b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataEqualityTest.kt
index bde19c8..9dd28c8 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataEqualityTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataEqualityTest.kt
@@ -24,11 +24,11 @@
 import android.support.wearable.complications.ComplicationText.plainText
 import android.util.Log
 import androidx.test.core.app.ApplicationProvider
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
 import androidx.wear.watchface.complications.data.ComplicationDisplayPolicies
 import androidx.wear.watchface.complications.data.ComplicationPersistencePolicies
-import androidx.wear.watchface.complications.data.FloatExpression
 import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
-import androidx.wear.watchface.complications.data.StringExpression
 import com.google.common.truth.Expect
 import kotlin.random.Random
 import org.junit.Before
@@ -88,20 +88,8 @@
             { setRangedValue(2f) },
         ),
         RANGED_VALUE_EXPRESSION(
-            {
-                setRangedValueExpression(
-                    object : FloatExpression() {
-                        override fun asByteArray() = byteArrayOf(1, 2)
-                    }
-                )
-            },
-            {
-                setRangedValueExpression(
-                    object : FloatExpression() {
-                        override fun asByteArray() = byteArrayOf(3, 4)
-                    }
-                )
-            },
+            { setRangedValueExpression(DynamicFloat.constant(1.2f)) },
+            { setRangedValueExpression(DynamicFloat.constant(3.4f)) },
         ),
         RANGED_VALUE_TYPE(
             { setRangedValueType(1) },
@@ -352,19 +340,11 @@
         RANGED_VALUE_EXPRESSION(
             {
                 setRangedValue(Random.nextFloat()) // Ignored when there's an expression.
-                    .setRangedValueExpression(
-                        object : FloatExpression() {
-                            override fun asByteArray() = byteArrayOf(1, 2)
-                        }
-                    )
+                    .setRangedValueExpression(DynamicFloat.constant(1.2f))
             },
             {
                 setRangedValue(Random.nextFloat()) // Ignored when there's an expression.
-                    .setRangedValueExpression(
-                        object : FloatExpression() {
-                            override fun asByteArray() = byteArrayOf(3, 4)
-                        }
-                    )
+                    .setRangedValueExpression(DynamicFloat.constant(3.4f))
             },
         ),
 
@@ -378,7 +358,7 @@
                 setShortTitle(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        StringExpression(byteArrayOf(1, 2))
+                        DynamicString.constant("1")
                     )
                 )
             },
@@ -386,7 +366,7 @@
                 setShortTitle(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        StringExpression(byteArrayOf(3, 4))
+                        DynamicString.constant("2")
                     )
                 )
             },
@@ -402,7 +382,7 @@
                 setShortText(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        StringExpression(byteArrayOf(1, 2))
+                        DynamicString.constant("1")
                     )
                 )
             },
@@ -410,7 +390,7 @@
                 setShortText(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        StringExpression(byteArrayOf(3, 4))
+                        DynamicString.constant("2")
                     )
                 )
             },
@@ -426,7 +406,7 @@
                 setLongTitle(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        StringExpression(byteArrayOf(1, 2))
+                        DynamicString.constant("1")
                     )
                 )
             },
@@ -434,7 +414,7 @@
                 setLongTitle(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        StringExpression(byteArrayOf(3, 4))
+                        DynamicString.constant("2")
                     )
                 )
             },
@@ -450,7 +430,7 @@
                 setLongText(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        StringExpression(byteArrayOf(1, 2))
+                        DynamicString.constant("1")
                     )
                 )
             },
@@ -458,7 +438,7 @@
                 setLongText(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        StringExpression(byteArrayOf(3, 4))
+                        DynamicString.constant("2")
                     )
                 )
             },
@@ -474,7 +454,7 @@
                 setContentDescription(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        StringExpression(byteArrayOf(1, 2))
+                        DynamicString.constant("1")
                     )
                 )
             },
@@ -482,7 +462,7 @@
                 setContentDescription(
                     ComplicationText(
                         Random.nextInt().toString(), // Ignored when there's an expression.
-                        StringExpression(byteArrayOf(3, 4))
+                        DynamicString.constant("2")
                     )
                 )
             },
@@ -500,7 +480,7 @@
                         .setShortText(
                             ComplicationText(
                                 Random.nextInt().toString(), // Ignored when there's an expression.
-                                StringExpression(byteArrayOf(1, 2))
+                                DynamicString.constant("1")
                             )
                         )
                         .build()
@@ -512,7 +492,7 @@
                         .setShortText(
                             ComplicationText(
                                 Random.nextInt().toString(), // Ignored when there's an expression.
-                                StringExpression(byteArrayOf(3, 4))
+                                DynamicString.constant("2")
                             )
                         )
                         .build()
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
index a090595..617d51a 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
@@ -24,8 +24,8 @@
 import android.support.wearable.complications.ComplicationText.TimeFormatBuilder
 import android.support.wearable.complications.ComplicationText.plainText
 import androidx.test.core.app.ApplicationProvider
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
 import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
-import androidx.wear.watchface.complications.data.toFloatExpression
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert
 import org.junit.Assert.assertThrows
@@ -100,7 +100,7 @@
         // GIVEN complication data of the RANGED_VALUE type created by the Builder...
         val data =
             ComplicationData.Builder(ComplicationData.TYPE_RANGED_VALUE)
-                .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
+                .setRangedValueExpression(DynamicFloat.constant(20f))
                 .setRangedMinValue(5f)
                 .setRangedMaxValue(150f)
                 .setShortTitle(plainText("title"))
@@ -109,7 +109,8 @@
 
         // WHEN the relevant getters are called on the resulting data
         // THEN the correct values are returned.
-        assertThat(data.rangedValueExpression!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
+        assertThat(data.rangedValueExpression?.toDynamicFloatByteArray())
+            .isEqualTo(DynamicFloat.constant(20f).toDynamicFloatByteArray())
         Assert.assertEquals(data.rangedMinValue, 5f, 0f)
         Assert.assertEquals(data.rangedMaxValue, 150f, 0f)
         assertThat(data.shortTitle!!.getTextAt(mResources, 0)).isEqualTo("title")
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
index 3b3e6b9..331b996 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
@@ -18,11 +18,13 @@
 
 import android.content.Context
 import android.os.Parcel
+import android.support.wearable.complications.ComplicationText.FORMAT_STYLE_DEFAULT
 import android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder
 import android.support.wearable.complications.ComplicationText.TimeFormatBuilder
 import androidx.test.core.app.ApplicationProvider
-import androidx.wear.watchface.complications.data.StringExpression
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
 import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
+import com.google.common.testing.EqualsTester
 import com.google.common.truth.Truth
 import org.junit.Assert
 import org.junit.Test
@@ -36,6 +38,33 @@
     private val mResources = ApplicationProvider.getApplicationContext<Context>().resources
 
     @Test
+    public fun testEquality() {
+        fun dup(builder: () -> ComplicationText) = arrayOf(builder(), builder())
+        fun timeFormat(value: String) = TimeFormatText(value, FORMAT_STYLE_DEFAULT, null)
+
+        // Verifying all possible constructors, with duplicate values and different values for each
+        // constructor argument.
+        EqualsTester()
+            .addEqualityGroup(dup { ComplicationText("surrounding") })
+            .addEqualityGroup(dup { ComplicationText("surrounding 2") })
+            .addEqualityGroup(dup { ComplicationText("surrounding", timeFormat("%h")) })
+            .addEqualityGroup(dup { ComplicationText("surrounding 2", timeFormat("%h")) })
+            .addEqualityGroup(dup { ComplicationText("surrounding", timeFormat("%m")) })
+            .addEqualityGroup(dup { ComplicationText(DynamicString.constant("expression")) })
+            .addEqualityGroup(dup { ComplicationText(DynamicString.constant("expression 2")) })
+            .addEqualityGroup(
+                dup { ComplicationText("surrounding", DynamicString.constant("expression")) }
+            )
+            .addEqualityGroup(
+                dup { ComplicationText("surrounding 2", DynamicString.constant("expression")) }
+            )
+            .addEqualityGroup(
+                dup { ComplicationText("surrounding", DynamicString.constant("expression 2")) }
+            )
+            .testEquals()
+    }
+
+    @Test
     public fun testPlainText() {
         // GIVEN ComplicationText of the plain string type
         val complicationText =
@@ -803,7 +832,7 @@
 
     @Test
     public fun stringExpressionToParcelRoundTrip() {
-        val text = ComplicationText(StringExpression(byteArrayOf(1, 2, 3)))
+        val text = ComplicationText(DynamicString.constant("hello"))
 
         Truth.assertThat(text.toParcelRoundTrip()).isEqualTo(text)
     }
@@ -812,7 +841,7 @@
     public fun getTextAt_ignoresStringExpressionIfSurroundingStringPresent() {
         val text = ComplicationText(
             "hello" as CharSequence,
-            StringExpression(byteArrayOf(1, 2, 3))
+            DynamicString.constant("world")
         )
 
         Truth.assertThat(text.getTextAt(mResources, 132456789).toString())
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
index 5ca6603..0ed46d2 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
@@ -32,6 +32,8 @@
 import android.util.Log
 import androidx.annotation.RequiresApi
 import androidx.test.core.app.ApplicationProvider
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
 import com.google.common.truth.Truth.assertThat
 import com.google.testing.junit.testparameterinjector.TestParameter
 import com.google.testing.junit.testparameterinjector.TestParameterInjector
@@ -151,11 +153,11 @@
 
         assertThat(data.toString()).isEqualTo(
             "ShortTextComplicationData(text=ComplicationText{mSurroundingText=text, " +
-                "mTimeDependentText=null, mStringExpression=null}, title=ComplicationText{" +
-                "mSurroundingText=title, mTimeDependentText=null, mStringExpression=null}, " +
+                "mTimeDependentText=null, mExpression=null}, title=ComplicationText{" +
+                "mSurroundingText=title, mTimeDependentText=null, mExpression=null}, " +
                 "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText{" +
                 "mSurroundingText=content description, mTimeDependentText=null, " +
-                "mStringExpression=null}, tapActionLostDueToSerialization=false, tapAction=null, " +
+                "mExpression=null}, tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=1, displayPolicy=1)"
@@ -204,13 +206,13 @@
 
         assertThat(data.toString()).isEqualTo(
             "ShortTextComplicationData(text=ComplicationText{mSurroundingText=text, " +
-                "mTimeDependentText=null, mStringExpression=null}, title=ComplicationText{" +
-                "mSurroundingText=title, mTimeDependentText=null, mStringExpression=null}, " +
+                "mTimeDependentText=null, mExpression=null}, title=ComplicationText{" +
+                "mSurroundingText=title, mTimeDependentText=null, mExpression=null}, " +
                 "monochromaticImage=MonochromaticImage(image=Icon(typ=URI uri=someuri), " +
                 "ambientImage=null), smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), " +
                 "type=PHOTO, ambientImage=null), contentDescription=ComplicationText{" +
                 "mSurroundingText=content description, mTimeDependentText=null, " +
-                "mStringExpression=null}, tapActionLostDueToSerialization=false, tapAction=null, " +
+                "mExpression=null}, tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
@@ -248,11 +250,11 @@
 
         assertThat(data.toString()).isEqualTo(
             "LongTextComplicationData(text=ComplicationText{mSurroundingText=text, " +
-                "mTimeDependentText=null, mStringExpression=null}, title=ComplicationText{" +
-                "mSurroundingText=title, mTimeDependentText=null, mStringExpression=null}, " +
+                "mTimeDependentText=null, mExpression=null}, title=ComplicationText{" +
+                "mSurroundingText=title, mTimeDependentText=null, mExpression=null}, " +
                 "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText{" +
                 "mSurroundingText=content description, mTimeDependentText=null, " +
-                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                "mExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
                 " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
@@ -301,13 +303,13 @@
 
         assertThat(data.toString()).isEqualTo(
             "LongTextComplicationData(text=ComplicationText{mSurroundingText=text, " +
-                "mTimeDependentText=null, mStringExpression=null}, " +
+                "mTimeDependentText=null, mExpression=null}, " +
                 "title=ComplicationText{mSurroundingText=title, mTimeDependentText=null, " +
-                "mStringExpression=null}, monochromaticImage=MonochromaticImage(" +
+                "mExpression=null}, monochromaticImage=MonochromaticImage(" +
                 "image=Icon(typ=URI uri=someuri), ambientImage=null), smallImage=SmallImage(" +
                 "image=Icon(typ=URI uri=someuri2), type=PHOTO, ambientImage=null), " +
                 "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null, mStringExpression=null}), " +
+                "mTimeDependentText=null, mExpression=null}), " +
                 "tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
@@ -353,9 +355,9 @@
             "RangedValueComplicationData(value=95.0, valueExpression=null, valueType=0, " +
                 "min=0.0, max=100.0, monochromaticImage=null, smallImage=null, " +
                 "title=ComplicationText{mSurroundingText=battery, mTimeDependentText=null, " +
-                "mStringExpression=null}, text=null, contentDescription=ComplicationText{" +
+                "mExpression=null}, text=null, contentDescription=ComplicationText{" +
                 "mSurroundingText=content description, mTimeDependentText=null, " +
-                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                "mExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
                 " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
@@ -366,7 +368,7 @@
     @Test
     public fun rangedValueComplicationData_withValueExpression() {
         val data = RangedValueComplicationData.Builder(
-            valueExpression = byteArrayOf(42, 107).toFloatExpression(),
+            valueExpression = DynamicFloat.constant(20f),
             min = 5f,
             max = 100f,
             contentDescription = "content description".complicationText
@@ -377,7 +379,7 @@
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
                 WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
-                    .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
+                    .setRangedValueExpression(DynamicFloat.constant(20f))
                     .setRangedValue(5f) // min as a sensible default
                     .setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
                     .setRangedMinValue(5f)
@@ -393,7 +395,8 @@
         val deserialized = serializeAndDeserialize(data) as RangedValueComplicationData
         assertThat(deserialized.max).isEqualTo(100f)
         assertThat(deserialized.min).isEqualTo(5f)
-        assertThat(deserialized.valueExpression!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
+        assertThat(deserialized.valueExpression?.toDynamicFloatByteArray())
+            .isEqualTo(DynamicFloat.constant(20f).toDynamicFloatByteArray())
         assertThat(deserialized.value).isEqualTo(5f) // min as a sensible default
         assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
             .isEqualTo("content description")
@@ -402,12 +405,12 @@
 
         assertThat(data.toString()).isEqualTo(
             "RangedValueComplicationData(value=5.0, " +
-                "valueExpression=FloatExpressionPlaceholder[42, 107], valueType=0, min=5.0, " +
+                "valueExpression=FixedFloat{value=20.0}, valueType=0, min=5.0, " +
                 "max=100.0, monochromaticImage=null, smallImage=null, title=ComplicationText{" +
-                "mSurroundingText=battery, mTimeDependentText=null, mStringExpression=null}, " +
+                "mSurroundingText=battery, mTimeDependentText=null, mExpression=null}, " +
                 "text=null, contentDescription=ComplicationText{" +
                 "mSurroundingText=content description, mTimeDependentText=null, " +
-                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                "mExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
                 " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
                 "ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
@@ -421,9 +424,7 @@
             value = 95f, min = 0f, max = 100f,
             contentDescription = "content description".complicationText
         )
-            .setTitle(
-                StringExpressionComplicationText(StringExpression(byteArrayOf(1, 2, 3, 4, 5)))
-            )
+            .setTitle(ComplicationTextExpression(DynamicString.constant("title")))
             .setDataSource(dataSource)
             .build()
         ParcelableSubject.assertThat(data.asWireComplicationData())
@@ -433,9 +434,7 @@
                     .setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
                     .setRangedMinValue(0f)
                     .setRangedMaxValue(100f)
-                    .setShortTitle(
-                        WireComplicationText(StringExpression(byteArrayOf(1, 2, 3, 4, 5)))
-                    )
+                    .setShortTitle(WireComplicationText(DynamicString.constant("title")))
                     .setContentDescription(WireComplicationText.plainText("content description"))
                     .setDataSource(dataSource)
                     .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
@@ -454,9 +453,9 @@
             "RangedValueComplicationData(value=95.0, valueExpression=null, valueType=0, " +
                 "min=0.0, max=100.0, monochromaticImage=null, smallImage=null, " +
                 "title=ComplicationText{mSurroundingText=(null), mTimeDependentText=null, " +
-                "mStringExpression=StringExpression(expression=[1, 2, 3, 4, 5])}, text=null, " +
+                "mExpression=FixedString{value=title}}, text=null, " +
                 "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null, mStringExpression=null}), " +
+                "mTimeDependentText=null, mExpression=null}), " +
                 "tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
@@ -516,9 +515,9 @@
                 "monochromaticImage=MonochromaticImage(image=Icon(typ=URI uri=someuri), " +
                 "ambientImage=null), smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), " +
                 "type=PHOTO, ambientImage=null), title=ComplicationText{mSurroundingText=battery," +
-                " mTimeDependentText=null, mStringExpression=null}, text=null, " +
+                " mTimeDependentText=null, mExpression=null}, text=null, " +
                 "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null, mStringExpression=null}), " +
+                "mTimeDependentText=null, mExpression=null}), " +
                 "tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
@@ -562,9 +561,9 @@
             "GoalProgressComplicationData(value=1200.0, valueExpression=null, " +
                 "targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
                 "title=ComplicationText{mSurroundingText=steps, mTimeDependentText=null, " +
-                "mStringExpression=null}, text=null, contentDescription=ComplicationText{" +
+                "mExpression=null}, text=null, contentDescription=ComplicationText{" +
                 "mSurroundingText=content description, mTimeDependentText=null, " +
-                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                "mExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
                 " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
@@ -575,7 +574,7 @@
     @Test
     public fun goalProgressComplicationData_withValueExpression() {
         val data = GoalProgressComplicationData.Builder(
-            valueExpression = byteArrayOf(42, 107).toFloatExpression(),
+            valueExpression = DynamicFloat.constant(10f),
             targetValue = 10000f,
             contentDescription = "content description".complicationText
         )
@@ -585,7 +584,7 @@
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
                 WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
-                    .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
+                    .setRangedValueExpression(DynamicFloat.constant(10f))
                     .setRangedValue(0f) // sensible default
                     .setTargetValue(10000f)
                     .setShortTitle(WireComplicationText.plainText("steps"))
@@ -597,7 +596,8 @@
             )
         testRoundTripConversions(data)
         val deserialized = serializeAndDeserialize(data) as GoalProgressComplicationData
-        assertThat(deserialized.valueExpression!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
+        assertThat(deserialized.valueExpression?.toDynamicFloatByteArray())
+            .isEqualTo(DynamicFloat.constant(10f).toDynamicFloatByteArray())
         assertThat(deserialized.value).isEqualTo(0f) // sensible default
         assertThat(deserialized.targetValue).isEqualTo(10000f)
         assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
@@ -607,11 +607,11 @@
 
         assertThat(data.toString()).isEqualTo(
             "GoalProgressComplicationData(value=0.0, valueExpression=" +
-                "FloatExpressionPlaceholder[42, 107], targetValue=10000.0, " +
+                "FixedFloat{value=10.0}, targetValue=10000.0, " +
                 "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
-                "mSurroundingText=steps, mTimeDependentText=null, mStringExpression=null}, " +
+                "mSurroundingText=steps, mTimeDependentText=null, mExpression=null}, " +
                 "text=null, contentDescription=ComplicationText{mSurroundingText=content " +
-                "description, mTimeDependentText=null, mStringExpression=null}), " +
+                "description, mTimeDependentText=null, mExpression=null}), " +
                 "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
                 "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
@@ -657,9 +657,9 @@
             "GoalProgressComplicationData(value=1200.0, valueExpression=null, " +
                 "targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
                 "title=ComplicationText{mSurroundingText=steps, mTimeDependentText=null, " +
-                "mStringExpression=null}, text=null, contentDescription=ComplicationText{" +
+                "mExpression=null}, text=null, contentDescription=ComplicationText{" +
                 "mSurroundingText=content description, mTimeDependentText=null, " +
-                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                "mExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
                 " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=ColorRamp(colors=[-65536, " +
@@ -717,9 +717,9 @@
                 "monochromaticImage=MonochromaticImage(image=Icon(typ=URI uri=someuri), " +
                 "ambientImage=null), smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), " +
                 "type=PHOTO, ambientImage=null), title=ComplicationText{mSurroundingText=steps, " +
-                "mTimeDependentText=null, mStringExpression=null}, text=null, " +
+                "mTimeDependentText=null, mExpression=null}, text=null, " +
                 "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null, mStringExpression=null}), " +
+                "mTimeDependentText=null, mExpression=null}), " +
                 "tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
@@ -768,9 +768,9 @@
             "RangedValueComplicationData(value=95.0, valueExpression=null, " +
                 "valueType=0, min=0.0, max=100.0, " +
                 "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
-                "mSurroundingText=battery, mTimeDependentText=null, mStringExpression=null}, " +
+                "mSurroundingText=battery, mTimeDependentText=null, mExpression=null}, " +
                 "text=null, contentDescription=ComplicationText{mSurroundingText=content " +
-                "description, mTimeDependentText=null, mStringExpression=null}), " +
+                "description, mTimeDependentText=null, mExpression=null}), " +
                 "tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
@@ -839,9 +839,9 @@
                 " Element(color=-16711936, weight=1.0), Element(color=-16776961, weight=2.0), " +
                 "elementBackgroundColor=-7829368, monochromaticImage=null, smallImage=null, " +
                 "title=ComplicationText{mSurroundingText=calories, mTimeDependentText=null, " +
-                "mStringExpression=null}, text=null, contentDescription=ComplicationText{" +
+                "mExpression=null}, text=null, contentDescription=ComplicationText{" +
                 "mSurroundingText=content description, mTimeDependentText=null, " +
-                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                "mExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
                 " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
@@ -906,9 +906,9 @@
                 "image=Icon(typ=URI uri=someuri), ambientImage=null), " +
                 "smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), type=PHOTO, " +
                 "ambientImage=null), title=ComplicationText{mSurroundingText=calories, " +
-                "mTimeDependentText=null, mStringExpression=null}, text=null, " +
+                "mTimeDependentText=null, mExpression=null}, text=null, " +
                 "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null, mStringExpression=null}), " +
+                "mTimeDependentText=null, mExpression=null}), " +
                 "tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
@@ -943,7 +943,7 @@
             "MonochromaticImageComplicationData(monochromaticImage=MonochromaticImage(" +
                 "image=Icon(typ=URI uri=someuri), ambientImage=null), contentDescription=" +
                 "ComplicationText{mSurroundingText=content description, mTimeDependentText=null, " +
-                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                "mExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
                 " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
@@ -980,7 +980,7 @@
             "SmallImageComplicationData(smallImage=SmallImage(image=Icon(" +
                 "typ=URI uri=someuri), type=PHOTO, ambientImage=null), " +
                 "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null, mStringExpression=null}), " +
+                "mTimeDependentText=null, mExpression=null}), " +
                 "tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
@@ -1048,7 +1048,7 @@
         assertThat(data.toString()).isEqualTo(
             "PhotoImageComplicationData(photoImage=Icon(typ=URI uri=someuri), " +
                 "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null, mStringExpression=null}), " +
+                "mTimeDependentText=null, mExpression=null}), " +
                 "tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
@@ -1078,7 +1078,7 @@
 
         assertThat(data.toString()).isEqualTo(
             "NoPermissionComplicationData(text=ComplicationText{mSurroundingText=needs location, " +
-                "mTimeDependentText=null, mStringExpression=null}, title=null, " +
+                "mTimeDependentText=null, mExpression=null}, title=null, " +
                 "monochromaticImage=null, smallImage=null, tapActionLostDueToSerialization=false," +
                 " tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
                 "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
@@ -1115,7 +1115,7 @@
         assertThat(data.toString()).isEqualTo(
             "NoPermissionComplicationData(text=ComplicationText{" +
                 "mSurroundingText=needs location, mTimeDependentText=null, " +
-                "mStringExpression=null}, title=null, monochromaticImage=MonochromaticImage(" +
+                "mExpression=null}, title=null, monochromaticImage=MonochromaticImage(" +
                 "image=Icon(typ=URI uri=someuri), ambientImage=null), smallImage=SmallImage(" +
                 "image=Icon(typ=URI uri=someuri2), type=PHOTO, ambientImage=null), " +
                 "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
@@ -1165,13 +1165,13 @@
         assertThat(data.toString()).isEqualTo(
             "NoDataComplicationData(placeholder=ShortTextComplicationData(text=" +
                 "ComplicationText{mSurroundingText=__placeholder__, mTimeDependentText=null, " +
-                "mStringExpression=null}, title=ComplicationText{" +
+                "mExpression=null}, title=ComplicationText{" +
                 "mSurroundingText=__placeholder__, mTimeDependentText=null, " +
-                "mStringExpression=null}, monochromaticImage=MonochromaticImage(" +
+                "mExpression=null}, monochromaticImage=MonochromaticImage(" +
                 "image=Icon(typ=RESOURCE pkg= id=0xffffffff), ambientImage=null), " +
                 "smallImage=null, contentDescription=ComplicationText{" +
                 "mSurroundingText=content description, mTimeDependentText=null, " +
-                "mStringExpression=null}, tapActionLostDueToSerialization=false, tapAction=null, " +
+                "mExpression=null}, tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, " +
@@ -1216,9 +1216,9 @@
         assertThat(data.toString()).isEqualTo(
             "NoDataComplicationData(placeholder=LongTextComplicationData(" +
                 "text=ComplicationText{mSurroundingText=text, mTimeDependentText=null, " +
-                "mStringExpression=null}, title=null, monochromaticImage=null, smallImage=null, " +
+                "mExpression=null}, title=null, monochromaticImage=null, smallImage=null, " +
                 "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null, mStringExpression=null}), " +
+                "mTimeDependentText=null, mExpression=null}), " +
                 "tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
@@ -1275,9 +1275,9 @@
                 "value=3.4028235E38, valueExpression=null, valueType=0, min=0.0, max=100.0, " +
                 "monochromaticImage=null, smallImage=null, title=null, text=ComplicationText{" +
                 "mSurroundingText=__placeholder__, mTimeDependentText=null, " +
-                "mStringExpression=null}, contentDescription=ComplicationText{" +
+                "mExpression=null}, contentDescription=ComplicationText{" +
                 "mSurroundingText=content description, mTimeDependentText=null, " +
-                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                "mExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
                 " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
@@ -1333,9 +1333,9 @@
                 "value=3.4028235E38, valueExpression=null, targetValue=10000.0, " +
                 "monochromaticImage=null, smallImage=null, title=null, text=ComplicationText{" +
                 "mSurroundingText=__placeholder__, mTimeDependentText=null, " +
-                "mStringExpression=null}, contentDescription=ComplicationText{" +
+                "mExpression=null}, contentDescription=ComplicationText{" +
                 "mSurroundingText=content description, mTimeDependentText=null, " +
-                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                "mExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
                 " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, " +
@@ -1396,9 +1396,9 @@
                 "weight=1.0), Element(color=-16776961, weight=2.0), " +
                 "elementBackgroundColor=-7829368, monochromaticImage=null, smallImage=null, " +
                 "title=ComplicationText{mSurroundingText=calories, mTimeDependentText=null, " +
-                "mStringExpression=null}, text=null, contentDescription=ComplicationText{" +
+                "mExpression=null}, text=null, contentDescription=ComplicationText{" +
                 "mSurroundingText=content description, mTimeDependentText=null, " +
-                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                "mExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
                 " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, " +
@@ -1458,9 +1458,9 @@
                 "value=3.4028235E38, valueExpression=null, valueType=1, min=0.0, max=100.0, " +
                 "monochromaticImage=null, smallImage=null, title=null, text=ComplicationText{" +
                 "mSurroundingText=__placeholder__, mTimeDependentText=null, " +
-                "mStringExpression=null}, contentDescription=ComplicationText{" +
+                "mExpression=null}, contentDescription=ComplicationText{" +
                 "mSurroundingText=content description, mTimeDependentText=null, " +
-                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                "mExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
                 " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=ColorRamp(colors=[-65536, " +
@@ -1508,7 +1508,7 @@
                 "monochromaticImage=MonochromaticImage(image=Icon(typ=RESOURCE pkg= " +
                 "id=0xffffffff), ambientImage=null), contentDescription=ComplicationText{" +
                 "mSurroundingText=content description, mTimeDependentText=null, " +
-                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                "mExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
                 " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
                 "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, " +
@@ -1555,7 +1555,7 @@
             "NoDataComplicationData(placeholder=SmallImageComplicationData(smallImage=" +
                 "SmallImage(image=Icon(typ=RESOURCE pkg= id=0xffffffff), type=ICON, " +
                 "ambientImage=null), contentDescription=ComplicationText{mSurroundingText=" +
-                "content description, mTimeDependentText=null, mStringExpression=null}), " +
+                "content description, mTimeDependentText=null, mExpression=null}), " +
                 "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
                 "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
@@ -1602,7 +1602,7 @@
             "NoDataComplicationData(placeholder=PhotoImageComplicationData(" +
                 "photoImage=Icon(typ=RESOURCE pkg= id=0xffffffff), " +
                 "contentDescription=ComplicationText{mSurroundingText=content description, " +
-                "mTimeDependentText=null, mStringExpression=null}), " +
+                "mTimeDependentText=null, mExpression=null}), " +
                 "tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
                 "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
@@ -1714,7 +1714,7 @@
     public fun rangedValueComplicationData_withValueExpression() {
         assertRoundtrip(
             WireComplicationData.Builder(WireComplicationData.TYPE_RANGED_VALUE)
-                .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
+                .setRangedValueExpression(DynamicFloat.constant(10f))
                 .setRangedMinValue(0f)
                 .setRangedMaxValue(100f)
                 .setShortTitle(WireComplicationText.plainText("battery"))
@@ -1763,7 +1763,7 @@
     public fun goalProgressComplicationData_withValueExpression() {
         assertRoundtrip(
             WireComplicationData.Builder(WireComplicationData.TYPE_GOAL_PROGRESS)
-                .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
+                .setRangedValueExpression(DynamicFloat.constant(10f))
                 .setTargetValue(10000f)
                 .setShortTitle(WireComplicationText.plainText("steps"))
                 .setContentDescription(WireComplicationText.plainText("content description"))
@@ -2695,10 +2695,10 @@
 
         assertThat(data.toString()).isEqualTo(
             "ShortTextComplicationData(text=ComplicationText{mSurroundingText=REDACTED, " +
-                "mTimeDependentText=null, mStringExpression=null}, title=ComplicationText{" +
-                "mSurroundingText=REDACTED, mTimeDependentText=null, mStringExpression=null}, " +
+                "mTimeDependentText=null, mExpression=null}, title=ComplicationText{" +
+                "mSurroundingText=REDACTED, mTimeDependentText=null, mExpression=null}, " +
                 "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText{" +
-                "mSurroundingText=REDACTED, mTimeDependentText=null, mStringExpression=null}, " +
+                "mSurroundingText=REDACTED, mTimeDependentText=null, mExpression=null}, " +
                 "tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(REDACTED), dataSource=null, persistencePolicy=0, " +
                 "displayPolicy=0)"
@@ -2719,10 +2719,10 @@
 
         assertThat(data.toString()).isEqualTo(
             "LongTextComplicationData(text=ComplicationText{mSurroundingText=REDACTED, " +
-                "mTimeDependentText=null, mStringExpression=null}, title=ComplicationText{" +
-                "mSurroundingText=REDACTED, mTimeDependentText=null, mStringExpression=null}, " +
+                "mTimeDependentText=null, mExpression=null}, title=ComplicationText{" +
+                "mSurroundingText=REDACTED, mTimeDependentText=null, mExpression=null}, " +
                 "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText{" +
-                "mSurroundingText=REDACTED, mTimeDependentText=null, mStringExpression=null}), " +
+                "mSurroundingText=REDACTED, mTimeDependentText=null, mExpression=null}), " +
                 "tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(REDACTED), dataSource=null, persistencePolicy=0, " +
                 "displayPolicy=0)"
@@ -2748,10 +2748,10 @@
             "RangedValueComplicationData(value=REDACTED, valueExpression=REDACTED, " +
                 "valueType=0, min=0.0, max=100.0, monochromaticImage=null, smallImage=null, " +
                 "title=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null, " +
-                "mStringExpression=null}, text=ComplicationText{mSurroundingText=REDACTED, " +
-                "mTimeDependentText=null, mStringExpression=null}, contentDescription=" +
+                "mExpression=null}, text=ComplicationText{mSurroundingText=REDACTED, " +
+                "mTimeDependentText=null, mExpression=null}, contentDescription=" +
                 "ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null, " +
-                "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+                "mExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
                 " validTimeRange=TimeRange(REDACTED), dataSource=null, colorRamp=null, " +
                 "persistencePolicy=0, displayPolicy=0)"
         )
@@ -2775,8 +2775,8 @@
             "GoalProgressComplicationData(value=REDACTED, valueExpression=REDACTED, " +
                 "targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
                 "title=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null, " +
-                "mStringExpression=null}, text=null, contentDescription=ComplicationText{" +
-                "mSurroundingText=REDACTED, mTimeDependentText=null, mStringExpression=null}), " +
+                "mExpression=null}, text=null, contentDescription=ComplicationText{" +
+                "mSurroundingText=REDACTED, mTimeDependentText=null, mExpression=null}), " +
                 "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
                 "TimeRange(REDACTED), dataSource=null, colorRamp=ColorRamp(colors=[-65536, " +
                 "-16711936, -16776961], interpolated=true), persistencePolicy=0, displayPolicy=0)"
@@ -2798,9 +2798,9 @@
         assertThat(data.toString()).isEqualTo(
             "NoDataComplicationData(placeholder=LongTextComplicationData(" +
                 "text=ComplicationText{mSurroundingText=__placeholder__, mTimeDependentText=null," +
-                " mStringExpression=null}, title=null, monochromaticImage=null, smallImage=null, " +
+                " mExpression=null}, title=null, monochromaticImage=null, smallImage=null, " +
                 "contentDescription=ComplicationText{mSurroundingText=REDACTED, " +
-                "mTimeDependentText=null, mStringExpression=null}), " +
+                "mTimeDependentText=null, mExpression=null}), " +
                 "tapActionLostDueToSerialization=false, tapAction=null, " +
                 "validTimeRange=TimeRange(REDACTED), dataSource=null, persistencePolicy=0, " +
                 "displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 84970d4..a90f3c4 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -6737,4 +6737,4 @@
 
         override fun getSystemTimeZoneId() = ZoneId.of("UTC")
     }
-}
\ No newline at end of file
+}