Get rid of pendingInitialComplications
Now we update complications regardless of the initialization state.
setComplicationDataList yields new value to the stream which is monitored by internal coroutine.
InteractiveWatchFaceClient.updateComplicationData doesn't wait UI thread anymore.
Bug: 258210649
Change-Id: I1f25c390a26df375d49c18942b23ca53e27e10fc
Test: existing tests + manually (complications work)
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 5e8f737..0e94efe 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -47,6 +47,7 @@
import android.view.SurfaceHolder
import android.view.WindowInsets
import android.view.accessibility.AccessibilityManager
+import androidx.annotation.AnyThread
import androidx.annotation.Px
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
@@ -102,6 +103,8 @@
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@@ -1264,9 +1267,9 @@
private var asyncWatchFaceConstructionPending = false
- // Stores the initial ComplicationSlots which could get updated before they're applied.
- internal var pendingInitialComplications: List<IdAndComplicationDataWireFormat>? = null
- private set
+ @VisibleForTesting
+ internal val complicationsFlow =
+ MutableStateFlow<List<IdAndComplicationDataWireFormat>>(emptyList())
private var initialUserStyle: UserStyleWireFormat? = null
internal lateinit var interactiveInstanceId: String
@@ -1524,61 +1527,46 @@
}
}
- @UiThread
+ @AnyThread
internal fun setComplicationDataList(
complicationDataWireFormats: List<IdAndComplicationDataWireFormat>
): Unit = TraceEvent("EngineWrapper.setComplicationDataList").use {
- val earlyInitDetails = getEarlyInitDetailsOrNull()
- if (earlyInitDetails != null) {
- applyComplications(
- earlyInitDetails.complicationSlotsManager,
- complicationDataWireFormats
- )
- } else {
- setPendingInitialComplications(complicationDataWireFormats)
- }
- }
-
- @UiThread
- internal fun applyComplications(
- complicationSlotsManager: ComplicationSlotsManager,
- complicationDataWireFormats: List<IdAndComplicationDataWireFormat>
- ) {
- val now = Instant.ofEpochMilli(systemTimeProvider.getSystemTimeMillis())
- for (idAndComplicationData in complicationDataWireFormats) {
- complicationSlotsManager.onComplicationDataUpdate(
- idAndComplicationData.id,
- idAndComplicationData.complicationData.toApiComplicationData(),
- now
- )
- }
- complicationSlotsManager.onComplicationsUpdated()
- invalidate()
- scheduleWriteComplicationDataCache()
- }
-
- @UiThread
- internal fun setPendingInitialComplications(
- complicationDataWireFormats: List<IdAndComplicationDataWireFormat>
- ) {
- // If the watchface hasn't been created yet, update pendingInitialComplications so
- // it can be applied later.
- if (pendingInitialComplications == null) {
- pendingInitialComplications = complicationDataWireFormats
- } else {
+ complicationsFlow.update { base ->
// We need to merge the updates.
- val complicationUpdateMap = pendingInitialComplications!!.associate {
+ val complicationUpdateMap = base.associate {
Pair(it.id, it.complicationData)
}.toMutableMap()
for (data in complicationDataWireFormats) {
complicationUpdateMap[data.id] = data.complicationData
}
- pendingInitialComplications = complicationUpdateMap.map {
+ complicationUpdateMap.map {
IdAndComplicationDataWireFormat(it.key, it.value)
}
}
}
+ @WorkerThread
+ private fun listenForComplicationChanges(
+ complicationSlotsManager: ComplicationSlotsManager
+ ) {
+ // Add a listener so we can track changes and automatically apply them on the UIThread
+ uiThreadCoroutineScope.launch {
+ complicationsFlow.collect { complicationDataWireFormats ->
+ val now = Instant.ofEpochMilli(systemTimeProvider.getSystemTimeMillis())
+ for (idAndComplicationData in complicationDataWireFormats) {
+ complicationSlotsManager.onComplicationDataUpdate(
+ idAndComplicationData.id,
+ idAndComplicationData.complicationData.toApiComplicationData(),
+ now
+ )
+ }
+ complicationSlotsManager.onComplicationsUpdated()
+ invalidate()
+ scheduleWriteComplicationDataCache()
+ }
+ }
+ }
+
@UiThread
internal suspend fun updateInstance(newInstanceId: String) {
val watchFaceImpl = deferredWatchFaceImpl.await()
@@ -1999,10 +1987,12 @@
// Store the initial complications, this could be modified by new data before being
// applied.
- pendingInitialComplications = params.idAndComplicationDataWireFormats
-
- if (pendingInitialComplications == null || pendingInitialComplications!!.isEmpty()) {
- pendingInitialComplications = readComplicationDataCache(_context, params.instanceId)
+ var initialComplications = params.idAndComplicationDataWireFormats
+ if (initialComplications.isNullOrEmpty()) {
+ initialComplications = readComplicationDataCache(_context, params.instanceId)
+ }
+ if (!initialComplications.isNullOrEmpty()) {
+ setComplicationDataList(initialComplications)
}
createWatchFaceInternal(
@@ -2052,6 +2042,7 @@
}
complicationSlotsManager.watchState = watchState
complicationSlotsManager.listenForStyleChanges(uiThreadCoroutineScope)
+ listenForComplicationChanges(complicationSlotsManager)
val userStyleFlavors =
TraceEvent("WatchFaceService.createUserStyleFlavors").use {
@@ -2085,16 +2076,6 @@
)
)
- withContext(uiThreadCoroutineScope.coroutineContext) {
- // Apply any pendingInitialComplications, this must be done after
- // deferredEarlyInitDetails has completed or there's a window in which complication
- // updates get lost.
- pendingInitialComplications?.let {
- applyComplications(complicationSlotsManager, it)
- }
- pendingInitialComplications = null
- }
-
val watchFace = TraceEvent("WatchFaceService.createWatchFace").use {
// Note by awaiting deferredSurfaceHolder we ensure onSurfaceChanged has
// been called and we're passing the correct updated surface holder. This is
@@ -2722,7 +2703,7 @@
writer.println("destroyed=$destroyed")
writer.println("surfaceDestroyed=$surfaceDestroyed")
writer.println(
- "pendingInitialComplications=" + pendingInitialComplications?.joinToString()
+ "lastComplications=" + complicationsFlow.value.joinToString()
)
synchronized(lock) {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
index 1eae0b4..3134a78 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
@@ -160,9 +160,7 @@
override fun updateComplicationData(
complicationDatumWireFormats: MutableList<IdAndComplicationDataWireFormat>
- ): Unit = uiThreadCoroutineScope.runBlockingWithTracing(
- "InteractiveWatchFaceImpl.updateComplicationData"
- ) {
+ ): Unit = TraceEvent("InteractiveWatchFaceImpl.updateComplicationData").use {
if ("user" != Build.TYPE) {
Log.d(TAG, "updateComplicationData " + complicationDatumWireFormats.joinToString())
}
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 97ca016..520e72f 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
@@ -5801,7 +5801,7 @@
}
@Test
- public fun setPendingInitialComplications() {
+ public fun setComplicationDataListMergesCorrectly() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -5829,15 +5829,17 @@
.build()
)
- engineWrapper.setPendingInitialComplications(listOf(left1))
- assertThat(engineWrapper.pendingInitialComplications).containsExactly(left1)
+ engineWrapper.setComplicationDataList(listOf(left1))
+ // In initEngine we fill initial complication data using
+ // setComplicationViaWallpaperCommand, that's why lastComplications initially is not empty
+ assertThat(engineWrapper.complicationsFlow.value).contains(left1)
// Check merges are working as expected.
- engineWrapper.setPendingInitialComplications(listOf(right))
- assertThat(engineWrapper.pendingInitialComplications).containsExactly(left1, right)
+ engineWrapper.setComplicationDataList(listOf(right))
+ assertThat(engineWrapper.complicationsFlow.value).containsExactly(left1, right)
- engineWrapper.setPendingInitialComplications(listOf(left2))
- assertThat(engineWrapper.pendingInitialComplications).containsExactly(left2, right)
+ engineWrapper.setComplicationDataList(listOf(left2))
+ assertThat(engineWrapper.complicationsFlow.value).containsExactly(left2, right)
}
@Test