Refactor WatchFaceControlClientTest Split WatchFaceControlClientTest in 4 classes of tests: in headless client and control client tests and in screenshots and non screenshots. This will allow us to add non-screenshot tests more easily. Bug: 251454220 Test: Refactor - tests passing Change-Id: I340f176d19ecc547cd1b1211dfa7707c4d3ffb83
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/HeadlessWatchFaceClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/HeadlessWatchFaceClientTest.kt new file mode 100644 index 0000000..7bdb9f6 --- /dev/null +++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/HeadlessWatchFaceClientTest.kt
@@ -0,0 +1,331 @@ +/* + * 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.client.test + +import android.annotation.SuppressLint +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.graphics.Rect +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import androidx.test.screenshot.AndroidXScreenshotTestRule +import androidx.test.screenshot.assertAgainstGolden +import androidx.wear.watchface.ComplicationSlotBoundsType +import androidx.wear.watchface.DrawMode +import androidx.wear.watchface.RenderParameters +import androidx.wear.watchface.client.DeviceConfig +import androidx.wear.watchface.client.HeadlessWatchFaceClient +import androidx.wear.watchface.client.WatchFaceControlClient +import androidx.wear.watchface.client.test.TestServicesHelpers.componentOf +import androidx.wear.watchface.client.test.TestServicesHelpers.createTestComplications +import androidx.wear.watchface.complications.SystemDataSources +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.control.WatchFaceControlService +import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService +import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID +import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID +import androidx.wear.watchface.samples.ExampleOpenGLBackgroundInitWatchFaceService +import androidx.wear.watchface.style.WatchFaceLayer +import com.google.common.truth.Truth +import java.time.Instant +import kotlinx.coroutines.runBlocking +import org.junit.Assert +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RequiresApi(Build.VERSION_CODES.O_MR1) +abstract class HeadlessWatchFaceClientTestBase { + protected val context: Context = ApplicationProvider.getApplicationContext() + protected val service = runBlocking { + WatchFaceControlClient.createWatchFaceControlClientImpl( + context, + Intent(context, WatchFaceControlTestService::class.java).apply { + action = WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE + } + ) + } + + protected fun createHeadlessWatchFaceClient( + componentName: ComponentName = exampleCanvasAnalogWatchFaceComponentName + ): HeadlessWatchFaceClient { + return service.createHeadlessWatchFaceClient( + "id", + componentName, + deviceConfig, + 400, + 400 + )!! + } + + protected val exampleCanvasAnalogWatchFaceComponentName = + componentOf<ExampleCanvasAnalogWatchFaceService>() + + protected val exampleOpenGLWatchFaceComponentName = + componentOf<ExampleOpenGLBackgroundInitWatchFaceService>() + + protected val deviceConfig = DeviceConfig( + hasLowBitAmbient = false, + hasBurnInProtection = false, + analogPreviewReferenceTimeMillis = 0, + digitalPreviewReferenceTimeMillis = 0 + ) +} + +@RunWith(AndroidJUnit4::class) +@MediumTest +@RequiresApi(Build.VERSION_CODES.O_MR1) +class HeadlessWatchFaceClientTest : HeadlessWatchFaceClientTestBase() { + @Suppress("DEPRECATION", "NewApi") // defaultDataSourceType + @Test + fun headlessComplicationDetails() { + val headlessInstance = createHeadlessWatchFaceClient() + + Truth.assertThat(headlessInstance.complicationSlotsState.size).isEqualTo(2) + + val leftComplicationDetails = headlessInstance.complicationSlotsState[ + EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID + ]!! + Truth.assertThat(leftComplicationDetails.bounds).isEqualTo(Rect(80, 160, 160, 240)) + Truth.assertThat(leftComplicationDetails.boundsType) + .isEqualTo(ComplicationSlotBoundsType.ROUND_RECT) + Truth.assertThat( + leftComplicationDetails.defaultDataSourcePolicy.systemDataSourceFallback + ).isEqualTo( + SystemDataSources.DATA_SOURCE_DAY_OF_WEEK + ) + Truth.assertThat(leftComplicationDetails.defaultDataSourceType).isEqualTo( + ComplicationType.SHORT_TEXT + ) + Truth.assertThat(leftComplicationDetails.supportedTypes).containsExactly( + ComplicationType.RANGED_VALUE, + ComplicationType.GOAL_PROGRESS, + ComplicationType.WEIGHTED_ELEMENTS, + ComplicationType.LONG_TEXT, + ComplicationType.SHORT_TEXT, + ComplicationType.MONOCHROMATIC_IMAGE, + ComplicationType.SMALL_IMAGE + ) + Assert.assertTrue(leftComplicationDetails.isEnabled) + + val rightComplicationDetails = headlessInstance.complicationSlotsState[ + EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID + ]!! + Truth.assertThat(rightComplicationDetails.bounds).isEqualTo(Rect(240, 160, 320, 240)) + Truth.assertThat(rightComplicationDetails.boundsType) + .isEqualTo(ComplicationSlotBoundsType.ROUND_RECT) + Truth.assertThat( + rightComplicationDetails.defaultDataSourcePolicy.systemDataSourceFallback + ).isEqualTo( + SystemDataSources.DATA_SOURCE_STEP_COUNT + ) + Truth.assertThat(rightComplicationDetails.defaultDataSourceType).isEqualTo( + ComplicationType.SHORT_TEXT + ) + Truth.assertThat(rightComplicationDetails.supportedTypes).containsExactly( + ComplicationType.RANGED_VALUE, + ComplicationType.GOAL_PROGRESS, + ComplicationType.WEIGHTED_ELEMENTS, + ComplicationType.LONG_TEXT, + ComplicationType.SHORT_TEXT, + ComplicationType.MONOCHROMATIC_IMAGE, + ComplicationType.SMALL_IMAGE + ) + + Truth.assertThat(rightComplicationDetails.isEnabled).isTrue() + + headlessInstance.close() + } + + @Test + @Suppress("Deprecation") // userStyleSettings + fun headlessUserStyleSchema() { + val headlessInstance = createHeadlessWatchFaceClient() + + Truth.assertThat(headlessInstance.userStyleSchema.userStyleSettings.size).isEqualTo(5) + Truth.assertThat(headlessInstance.userStyleSchema.userStyleSettings[0].id.value).isEqualTo( + "color_style_setting" + ) + Truth.assertThat(headlessInstance.userStyleSchema.userStyleSettings[1].id.value).isEqualTo( + "draw_hour_pips_style_setting" + ) + Truth.assertThat(headlessInstance.userStyleSchema.userStyleSettings[2].id.value).isEqualTo( + "watch_hand_length_style_setting" + ) + Truth.assertThat(headlessInstance.userStyleSchema.userStyleSettings[3].id.value).isEqualTo( + "complications_style_setting" + ) + Truth.assertThat(headlessInstance.userStyleSchema.userStyleSettings[4].id.value).isEqualTo( + "hours_draw_freq_style_setting" + ) + + headlessInstance.close() + } + + @Test + fun headlessUserStyleFlavors() { + val headlessInstance = createHeadlessWatchFaceClient() + + Truth.assertThat(headlessInstance.getUserStyleFlavors().flavors.size).isEqualTo(1) + val flavorA = headlessInstance.getUserStyleFlavors().flavors[0] + Truth.assertThat(flavorA.id).isEqualTo("exampleFlavor") + Truth.assertThat(flavorA.style.userStyleMap.containsKey("color_style_setting")) + Truth.assertThat(flavorA.style.userStyleMap.containsKey("watch_hand_length_style_setting")) + Truth.assertThat(flavorA.complications + .containsKey(EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID)) + Truth.assertThat(flavorA.complications + .containsKey(EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID)) + + headlessInstance.close() + } + + @Test + @Suppress("Deprecation") // userStyleSettings + fun headlessToBundleAndCreateFromBundle() { + val headlessInstance = HeadlessWatchFaceClient.createFromBundle( + service.createHeadlessWatchFaceClient( + "id", + exampleCanvasAnalogWatchFaceComponentName, + deviceConfig, + 400, + 400 + )!!.toBundle() + ) + + Truth.assertThat(headlessInstance.userStyleSchema.userStyleSettings.size).isEqualTo(5) + } + + @Test + fun computeUserStyleSchemaDigestHash() { + val headlessInstance1 = createHeadlessWatchFaceClient( + exampleCanvasAnalogWatchFaceComponentName + ) + + val headlessInstance2 = createHeadlessWatchFaceClient( + exampleOpenGLWatchFaceComponentName + ) + + Truth.assertThat(headlessInstance1.getUserStyleSchemaDigestHash()).isNotEqualTo( + headlessInstance2.getUserStyleSchemaDigestHash() + ) + } + + @Test + fun headlessLifeCycle() { + val headlessInstance = createHeadlessWatchFaceClient( + componentOf<TestLifeCycleWatchFaceService>() + ) + + // Blocks until the headless instance has been fully constructed. + headlessInstance.previewReferenceInstant + headlessInstance.close() + + Truth.assertThat(TestLifeCycleWatchFaceService.lifeCycleEvents).containsExactly( + "WatchFaceService.onCreate", + "Renderer.constructed", + "Renderer.onDestroy", + "WatchFaceService.onDestroy" + ) + } +} + +@RunWith(AndroidJUnit4::class) +@MediumTest +@RequiresApi(Build.VERSION_CODES.O_MR1) +class HeadlessWatchFaceClientScreenshotTest : HeadlessWatchFaceClientTestBase() { + @get:Rule + val screenshotRule: AndroidXScreenshotTestRule = + AndroidXScreenshotTestRule("wear/wear-watchface-client") + + private val complications = createTestComplications(context) + + @SuppressLint("NewApi") // renderWatchFaceToBitmap + @Test + fun headlessScreenshot() { + val headlessInstance = createHeadlessWatchFaceClient() + + val bitmap = headlessInstance.renderWatchFaceToBitmap( + RenderParameters( + DrawMode.INTERACTIVE, + WatchFaceLayer.ALL_WATCH_FACE_LAYERS, + null + ), + Instant.ofEpochMilli(1234567), + null, + complications + ) + + bitmap.assertAgainstGolden(screenshotRule, "headlessScreenshot") + + headlessInstance.close() + } + + @SuppressLint("NewApi") // renderWatchFaceToBitmap + @Test + fun yellowComplicationHighlights() { + val headlessInstance = createHeadlessWatchFaceClient() + + val bitmap = headlessInstance.renderWatchFaceToBitmap( + RenderParameters( + DrawMode.INTERACTIVE, + WatchFaceLayer.ALL_WATCH_FACE_LAYERS, + RenderParameters.HighlightLayer( + RenderParameters.HighlightedElement.AllComplicationSlots, + Color.YELLOW, + Color.argb(128, 0, 0, 0) // Darken everything else. + ) + ), + Instant.ofEpochMilli(1234567), + null, + complications + ) + + bitmap.assertAgainstGolden(screenshotRule, "yellowComplicationHighlights") + + headlessInstance.close() + } + + @SuppressLint("NewApi") // renderWatchFaceToBitmap + @Test + fun highlightOnlyLayer() { + val headlessInstance = createHeadlessWatchFaceClient() + + val bitmap = headlessInstance.renderWatchFaceToBitmap( + RenderParameters( + DrawMode.INTERACTIVE, + emptySet(), + RenderParameters.HighlightLayer( + RenderParameters.HighlightedElement.AllComplicationSlots, + Color.YELLOW, + Color.argb(128, 0, 0, 0) // Darken everything else. + ) + ), + Instant.ofEpochMilli(1234567), + null, + complications + ) + + bitmap.assertAgainstGolden(screenshotRule, "highlightOnlyLayer") + + headlessInstance.close() + } +}
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/TestWatchFaceServices.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/TestWatchFaceServices.kt new file mode 100644 index 0000000..0d06e96 --- /dev/null +++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/TestWatchFaceServices.kt
@@ -0,0 +1,758 @@ +/* + * 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.client.test + +import android.app.PendingIntent +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.RectF +import android.view.SurfaceHolder +import androidx.wear.watchface.BoundingArc +import androidx.wear.watchface.CanvasComplication +import androidx.wear.watchface.CanvasType +import androidx.wear.watchface.ComplicationSlot +import androidx.wear.watchface.ComplicationSlotsManager +import androidx.wear.watchface.RenderParameters +import androidx.wear.watchface.Renderer +import androidx.wear.watchface.WatchFace +import androidx.wear.watchface.WatchFaceService +import androidx.wear.watchface.WatchFaceType +import androidx.wear.watchface.WatchState +import androidx.wear.watchface.complications.ComplicationSlotBounds +import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy +import androidx.wear.watchface.complications.SystemDataSources +import androidx.wear.watchface.complications.data.ComplicationData +import androidx.wear.watchface.complications.data.ComplicationExperimental +import androidx.wear.watchface.complications.data.ComplicationText +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.complications.data.NoDataComplicationData +import androidx.wear.watchface.complications.data.PlainComplicationText +import androidx.wear.watchface.complications.data.ShortTextComplicationData +import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService +import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.COMPLICATIONS_STYLE_SETTING +import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.LEFT_COMPLICATION +import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.NO_COMPLICATIONS +import androidx.wear.watchface.samples.ExampleOpenGLBackgroundInitWatchFaceService +import androidx.wear.watchface.samples.R +import androidx.wear.watchface.style.CurrentUserStyleRepository +import androidx.wear.watchface.style.UserStyleSchema +import androidx.wear.watchface.style.UserStyleSetting +import androidx.wear.watchface.style.WatchFaceLayer +import java.time.ZoneId +import java.time.ZonedDateTime +import java.util.concurrent.CountDownLatch +import kotlinx.coroutines.CompletableDeferred + +internal class TestLifeCycleWatchFaceService : WatchFaceService() { + companion object { + val lifeCycleEvents = ArrayList<String>() + } + + override fun onCreate() { + super.onCreate() + lifeCycleEvents.add("WatchFaceService.onCreate") + } + + override fun onDestroy() { + super.onDestroy() + lifeCycleEvents.add("WatchFaceService.onDestroy") + } + + override suspend fun createWatchFace( + surfaceHolder: SurfaceHolder, + watchState: WatchState, + complicationSlotsManager: ComplicationSlotsManager, + currentUserStyleRepository: CurrentUserStyleRepository + ) = WatchFace( + WatchFaceType.DIGITAL, + @Suppress("deprecation") + object : Renderer.GlesRenderer( + surfaceHolder, + currentUserStyleRepository, + watchState, + 16 + ) { + init { + lifeCycleEvents.add("Renderer.constructed") + } + + override fun onDestroy() { + super.onDestroy() + lifeCycleEvents.add("Renderer.onDestroy") + } + + override fun render(zonedDateTime: ZonedDateTime) {} + + override fun renderHighlightLayer(zonedDateTime: ZonedDateTime) {} + } + ) +} + +internal class TestExampleCanvasAnalogWatchFaceService( + testContext: Context, + private var surfaceHolderOverride: SurfaceHolder +) : ExampleCanvasAnalogWatchFaceService() { + internal lateinit var watchFace: WatchFace + + init { + attachBaseContext(testContext) + } + + override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride + + override suspend fun createWatchFace( + surfaceHolder: SurfaceHolder, + watchState: WatchState, + complicationSlotsManager: ComplicationSlotsManager, + currentUserStyleRepository: CurrentUserStyleRepository + ): WatchFace { + watchFace = super.createWatchFace( + surfaceHolder, + watchState, + complicationSlotsManager, + currentUserStyleRepository + ) + return watchFace + } +} + +internal class TestExampleOpenGLBackgroundInitWatchFaceService( + testContext: Context, + private var surfaceHolderOverride: SurfaceHolder +) : ExampleOpenGLBackgroundInitWatchFaceService() { + internal lateinit var watchFace: WatchFace + + init { + attachBaseContext(testContext) + } + + override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride + + override suspend fun createWatchFace( + surfaceHolder: SurfaceHolder, + watchState: WatchState, + complicationSlotsManager: ComplicationSlotsManager, + currentUserStyleRepository: CurrentUserStyleRepository + ): WatchFace { + watchFace = super.createWatchFace( + surfaceHolder, + watchState, + complicationSlotsManager, + currentUserStyleRepository + ) + return watchFace + } +} + +internal open class TestCrashingWatchFaceService : WatchFaceService() { + + companion object { + const val COMPLICATION_ID = 123 + } + + override fun createComplicationSlotsManager( + currentUserStyleRepository: CurrentUserStyleRepository + ): ComplicationSlotsManager { + return ComplicationSlotsManager( + listOf( + ComplicationSlot.createRoundRectComplicationSlotBuilder( + COMPLICATION_ID, + { _, _ -> throw Exception("Deliberately crashing") }, + listOf(ComplicationType.LONG_TEXT), + DefaultComplicationDataSourcePolicy( + SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET, + ComplicationType.LONG_TEXT + ), + ComplicationSlotBounds(RectF(0.1f, 0.1f, 0.4f, 0.4f)) + ).build() + ), + currentUserStyleRepository + ) + } + + override suspend fun createWatchFace( + surfaceHolder: SurfaceHolder, + watchState: WatchState, + complicationSlotsManager: ComplicationSlotsManager, + currentUserStyleRepository: CurrentUserStyleRepository + ): WatchFace { + throw Exception("Deliberately crashing") + } +} + +internal class TestWatchfaceOverlayStyleWatchFaceService( + testContext: Context, + private var surfaceHolderOverride: SurfaceHolder, + private var watchFaceOverlayStyle: WatchFace.OverlayStyle +) : WatchFaceService() { + + init { + attachBaseContext(testContext) + } + + override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride + + override suspend fun createWatchFace( + surfaceHolder: SurfaceHolder, + watchState: WatchState, + complicationSlotsManager: ComplicationSlotsManager, + currentUserStyleRepository: CurrentUserStyleRepository + ) = WatchFace( + WatchFaceType.DIGITAL, + @Suppress("deprecation") + object : Renderer.CanvasRenderer( + surfaceHolder, + currentUserStyleRepository, + watchState, + CanvasType.HARDWARE, + 16 + ) { + override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) { + // Actually rendering something isn't required. + } + + override fun renderHighlightLayer( + canvas: Canvas, + bounds: Rect, + zonedDateTime: ZonedDateTime + ) { + // Actually rendering something isn't required. + } + } + ).setOverlayStyle(watchFaceOverlayStyle) +} + +internal class TestAsyncCanvasRenderInitWatchFaceService( + testContext: Context, + private var surfaceHolderOverride: SurfaceHolder, + private var initCompletableDeferred: CompletableDeferred<Unit> +) : WatchFaceService() { + + init { + attachBaseContext(testContext) + } + + override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride + + override suspend fun createWatchFace( + surfaceHolder: SurfaceHolder, + watchState: WatchState, + complicationSlotsManager: ComplicationSlotsManager, + currentUserStyleRepository: CurrentUserStyleRepository + ) = WatchFace( + WatchFaceType.DIGITAL, + @Suppress("deprecation") + object : Renderer.CanvasRenderer( + surfaceHolder, + currentUserStyleRepository, + watchState, + CanvasType.HARDWARE, + 16 + ) { + override suspend fun init() { + initCompletableDeferred.await() + } + + override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) { + // Actually rendering something isn't required. + } + + override fun renderHighlightLayer( + canvas: Canvas, + bounds: Rect, + zonedDateTime: ZonedDateTime + ) { + TODO("Not yet implemented") + } + } + ) + + override fun getSystemTimeProvider() = object : SystemTimeProvider { + override fun getSystemTimeMillis() = 123456789L + + override fun getSystemTimeZoneId() = ZoneId.of("UTC") + } +} + +internal class TestAsyncGlesRenderInitWatchFaceService( + testContext: Context, + private var surfaceHolderOverride: SurfaceHolder, + private var onUiThreadGlSurfaceCreatedCompletableDeferred: CompletableDeferred<Unit>, + private var onBackgroundThreadGlContextCreatedCompletableDeferred: CompletableDeferred<Unit> +) : WatchFaceService() { + internal lateinit var watchFace: WatchFace + + init { + attachBaseContext(testContext) + } + + override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride + + override suspend fun createWatchFace( + surfaceHolder: SurfaceHolder, + watchState: WatchState, + complicationSlotsManager: ComplicationSlotsManager, + currentUserStyleRepository: CurrentUserStyleRepository + ) = WatchFace( + WatchFaceType.DIGITAL, + @Suppress("deprecation") + object : Renderer.GlesRenderer( + surfaceHolder, + currentUserStyleRepository, + watchState, + 16 + ) { + override suspend fun onUiThreadGlSurfaceCreated(width: Int, height: Int) { + onUiThreadGlSurfaceCreatedCompletableDeferred.await() + } + + override suspend fun onBackgroundThreadGlContextCreated() { + onBackgroundThreadGlContextCreatedCompletableDeferred.await() + } + + override fun render(zonedDateTime: ZonedDateTime) { + // GLES rendering is complicated and not strictly necessary for our test. + } + + override fun renderHighlightLayer(zonedDateTime: ZonedDateTime) { + TODO("Not yet implemented") + } + } + ) +} + +internal class TestComplicationProviderDefaultsWatchFaceService( + testContext: Context, + private var surfaceHolderOverride: SurfaceHolder +) : WatchFaceService() { + + init { + attachBaseContext(testContext) + } + + override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride + + override fun createComplicationSlotsManager( + currentUserStyleRepository: CurrentUserStyleRepository + ): ComplicationSlotsManager { + return ComplicationSlotsManager( + listOf( + ComplicationSlot.createRoundRectComplicationSlotBuilder( + 123, + { _, _ -> + object : CanvasComplication { + override fun render( + canvas: Canvas, + bounds: Rect, + zonedDateTime: ZonedDateTime, + renderParameters: RenderParameters, + slotId: Int + ) { + } + + override fun drawHighlight( + canvas: Canvas, + bounds: Rect, + boundsType: Int, + zonedDateTime: ZonedDateTime, + color: Int + ) { + } + + override fun getData() = NoDataComplicationData() + + override fun loadData( + complicationData: ComplicationData, + loadDrawablesAsynchronous: Boolean + ) { + } + } + }, + listOf( + ComplicationType.PHOTO_IMAGE, + ComplicationType.LONG_TEXT, + ComplicationType.SHORT_TEXT + ), + DefaultComplicationDataSourcePolicy( + ComponentName("com.package1", "com.app1"), + ComplicationType.PHOTO_IMAGE, + ComponentName("com.package2", "com.app2"), + ComplicationType.LONG_TEXT, + SystemDataSources.DATA_SOURCE_STEP_COUNT, + ComplicationType.SHORT_TEXT + ), + ComplicationSlotBounds( + RectF(0.1f, 0.2f, 0.3f, 0.4f) + ) + ) + .build() + ), + currentUserStyleRepository + ) + } + + override suspend fun createWatchFace( + surfaceHolder: SurfaceHolder, + watchState: WatchState, + complicationSlotsManager: ComplicationSlotsManager, + currentUserStyleRepository: CurrentUserStyleRepository + ) = WatchFace( + WatchFaceType.DIGITAL, + @Suppress("deprecation") + object : Renderer.CanvasRenderer( + surfaceHolder, + currentUserStyleRepository, + watchState, + CanvasType.HARDWARE, + 16 + ) { + override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {} + + override fun renderHighlightLayer( + canvas: Canvas, + bounds: Rect, + zonedDateTime: ZonedDateTime + ) { + } + } + ) +} + +internal class TestEdgeComplicationWatchFaceService( + testContext: Context, + private var surfaceHolderOverride: SurfaceHolder +) : WatchFaceService() { + + init { + attachBaseContext(testContext) + } + + override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride + + @OptIn(ComplicationExperimental::class) + override fun createComplicationSlotsManager( + currentUserStyleRepository: CurrentUserStyleRepository + ): ComplicationSlotsManager { + return ComplicationSlotsManager( + listOf( + ComplicationSlot.createEdgeComplicationSlotBuilder( + 123, + { _, _ -> + object : CanvasComplication { + override fun render( + canvas: Canvas, + bounds: Rect, + zonedDateTime: ZonedDateTime, + renderParameters: RenderParameters, + slotId: Int + ) { + } + + override fun drawHighlight( + canvas: Canvas, + bounds: Rect, + boundsType: Int, + zonedDateTime: ZonedDateTime, + color: Int + ) { + } + + override fun getData() = NoDataComplicationData() + + override fun loadData( + complicationData: ComplicationData, + loadDrawablesAsynchronous: Boolean + ) { + } + } + }, + listOf( + ComplicationType.PHOTO_IMAGE, + ComplicationType.LONG_TEXT, + ComplicationType.SHORT_TEXT + ), + DefaultComplicationDataSourcePolicy( + ComponentName("com.package1", "com.app1"), + ComplicationType.PHOTO_IMAGE, + ComponentName("com.package2", "com.app2"), + ComplicationType.LONG_TEXT, + SystemDataSources.DATA_SOURCE_STEP_COUNT, + ComplicationType.SHORT_TEXT + ), + ComplicationSlotBounds( + RectF(0f, 0f, 1f, 1f) + ), + BoundingArc(45f, 90f, 0.1f) + ) + .build() + ), + currentUserStyleRepository + ) + } + + override suspend fun createWatchFace( + surfaceHolder: SurfaceHolder, + watchState: WatchState, + complicationSlotsManager: ComplicationSlotsManager, + currentUserStyleRepository: CurrentUserStyleRepository + ) = WatchFace( + WatchFaceType.DIGITAL, + @Suppress("deprecation") + object : Renderer.CanvasRenderer( + surfaceHolder, + currentUserStyleRepository, + watchState, + CanvasType.HARDWARE, + 16 + ) { + override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {} + + override fun renderHighlightLayer( + canvas: Canvas, + bounds: Rect, + zonedDateTime: ZonedDateTime + ) { + } + } + ) +} + +internal class TestWatchFaceServiceWithPreviewImageUpdateRequest( + testContext: Context, + private var surfaceHolderOverride: SurfaceHolder, +) : WatchFaceService() { + val rendererInitializedLatch = CountDownLatch(1) + + init { + attachBaseContext(testContext) + } + + override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride + + @Suppress("deprecation") + private lateinit var renderer: Renderer.CanvasRenderer + + fun triggerPreviewImageUpdateRequest() { + renderer.sendPreviewImageNeedsUpdateRequest() + } + + override suspend fun createWatchFace( + surfaceHolder: SurfaceHolder, + watchState: WatchState, + complicationSlotsManager: ComplicationSlotsManager, + currentUserStyleRepository: CurrentUserStyleRepository + ): WatchFace { + @Suppress("deprecation") + renderer = object : Renderer.CanvasRenderer( + surfaceHolder, + currentUserStyleRepository, + watchState, + CanvasType.HARDWARE, + 16 + ) { + override suspend fun init() { + rendererInitializedLatch.countDown() + } + + override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {} + + override fun renderHighlightLayer( + canvas: Canvas, + bounds: Rect, + zonedDateTime: ZonedDateTime + ) { + } + } + return WatchFace(WatchFaceType.DIGITAL, renderer) + } +} + +internal class TestComplicationStyleUpdateWatchFaceService( + testContext: Context, + private var surfaceHolderOverride: SurfaceHolder +) : WatchFaceService() { + + init { + attachBaseContext(testContext) + } + + @Suppress("deprecation") + private val complicationsStyleSetting = + UserStyleSetting.ComplicationSlotsUserStyleSetting( + UserStyleSetting.Id(COMPLICATIONS_STYLE_SETTING), + resources, + R.string.watchface_complications_setting, + R.string.watchface_complications_setting_description, + icon = null, + complicationConfig = listOf( + UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption( + UserStyleSetting.Option.Id(NO_COMPLICATIONS), + resources, + R.string.watchface_complications_setting_none, + null, + listOf( + UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + 123, + enabled = false + ) + ) + ), + UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption( + UserStyleSetting.Option.Id(LEFT_COMPLICATION), + resources, + R.string.watchface_complications_setting_left, + null, + listOf( + UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + 123, + enabled = true, + nameResourceId = R.string.left_complication_screen_name, + screenReaderNameResourceId = + R.string.left_complication_screen_reader_name + ) + ) + ) + ), + listOf(WatchFaceLayer.COMPLICATIONS) + ) + + override fun createUserStyleSchema(): UserStyleSchema = + UserStyleSchema(listOf(complicationsStyleSetting)) + + override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride + + override fun createComplicationSlotsManager( + currentUserStyleRepository: CurrentUserStyleRepository + ): ComplicationSlotsManager { + return ComplicationSlotsManager( + listOf( + ComplicationSlot.createRoundRectComplicationSlotBuilder( + 123, + { _, _ -> + object : CanvasComplication { + override fun render( + canvas: Canvas, + bounds: Rect, + zonedDateTime: ZonedDateTime, + renderParameters: RenderParameters, + slotId: Int + ) { + } + + override fun drawHighlight( + canvas: Canvas, + bounds: Rect, + boundsType: Int, + zonedDateTime: ZonedDateTime, + color: Int + ) { + } + + override fun getData() = NoDataComplicationData() + + override fun loadData( + complicationData: ComplicationData, + loadDrawablesAsynchronous: Boolean + ) { + } + } + }, + listOf( + ComplicationType.PHOTO_IMAGE, + ComplicationType.LONG_TEXT, + ComplicationType.SHORT_TEXT + ), + DefaultComplicationDataSourcePolicy( + ComponentName("com.package1", "com.app1"), + ComplicationType.PHOTO_IMAGE, + ComponentName("com.package2", "com.app2"), + ComplicationType.LONG_TEXT, + SystemDataSources.DATA_SOURCE_STEP_COUNT, + ComplicationType.SHORT_TEXT + ), + ComplicationSlotBounds( + RectF(0.1f, 0.2f, 0.3f, 0.4f) + ) + ).build() + ), + currentUserStyleRepository + ) + } + + override suspend fun createWatchFace( + surfaceHolder: SurfaceHolder, + watchState: WatchState, + complicationSlotsManager: ComplicationSlotsManager, + currentUserStyleRepository: CurrentUserStyleRepository + ) = WatchFace( + WatchFaceType.ANALOG, + @Suppress("deprecation") + object : Renderer.CanvasRenderer( + surfaceHolder, + currentUserStyleRepository, + watchState, + CanvasType.HARDWARE, + 16 + ) { + override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {} + + override fun renderHighlightLayer( + canvas: Canvas, + bounds: Rect, + zonedDateTime: ZonedDateTime + ) { + } + } + ) +} + +internal object TestServicesHelpers { + fun createTestComplications(context: Context) = mapOf( + ExampleCanvasAnalogWatchFaceService.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to + ShortTextComplicationData.Builder( + PlainComplicationText.Builder("ID").build(), + ComplicationText.EMPTY + ).setTitle(PlainComplicationText.Builder("Left").build()) + .setTapAction( + PendingIntent.getActivity(context, 0, Intent("left"), + PendingIntent.FLAG_IMMUTABLE + ) + ) + .build(), + ExampleCanvasAnalogWatchFaceService.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID to + ShortTextComplicationData.Builder( + PlainComplicationText.Builder("ID").build(), + ComplicationText.EMPTY + ).setTitle(PlainComplicationText.Builder("Right").build()) + .setTapAction( + PendingIntent.getActivity(context, 0, Intent("right"), + PendingIntent.FLAG_IMMUTABLE + ) + ) + .build() + ) + + inline fun <reified T>componentOf(): ComponentName { + return ComponentName( + T::class.java.`package`?.name!!, + T::class.java.name + ) + } +}
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt index 99b7e4e..a10788a 100644 --- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt +++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -18,7 +18,6 @@ import android.annotation.SuppressLint import android.app.PendingIntent -import android.app.PendingIntent.FLAG_IMMUTABLE import android.content.ComponentName import android.content.Context import android.content.Intent @@ -26,11 +25,9 @@ import android.graphics.Canvas import android.graphics.Color import android.graphics.Rect -import android.graphics.RectF import android.graphics.SurfaceTexture import android.os.Build import android.os.Handler -import android.os.IBinder import android.os.Looper import android.view.Surface import android.view.SurfaceHolder @@ -41,21 +38,15 @@ import androidx.test.screenshot.AndroidXScreenshotTestRule import androidx.test.screenshot.assertAgainstGolden import androidx.wear.watchface.BoundingArc -import androidx.wear.watchface.CanvasComplication -import androidx.wear.watchface.CanvasType import androidx.wear.watchface.ComplicationSlot import androidx.wear.watchface.ComplicationSlotBoundsType -import androidx.wear.watchface.ComplicationSlotsManager import androidx.wear.watchface.ContentDescriptionLabel import androidx.wear.watchface.DrawMode import androidx.wear.watchface.RenderParameters -import androidx.wear.watchface.Renderer import androidx.wear.watchface.WatchFace import androidx.wear.watchface.WatchFaceColors import androidx.wear.watchface.WatchFaceExperimental import androidx.wear.watchface.WatchFaceService -import androidx.wear.watchface.WatchFaceType -import androidx.wear.watchface.WatchState import androidx.wear.watchface.client.DeviceConfig import androidx.wear.watchface.client.DisconnectReason import androidx.wear.watchface.client.DisconnectReasons @@ -64,7 +55,8 @@ import androidx.wear.watchface.client.WatchFaceClientExperimental import androidx.wear.watchface.client.WatchFaceControlClient import androidx.wear.watchface.client.WatchUiState -import androidx.wear.watchface.complications.ComplicationSlotBounds +import androidx.wear.watchface.client.test.TestServicesHelpers.componentOf +import androidx.wear.watchface.client.test.TestServicesHelpers.createTestComplications import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy import androidx.wear.watchface.complications.SystemDataSources import androidx.wear.watchface.complications.data.ComplicationData @@ -72,11 +64,8 @@ import androidx.wear.watchface.complications.data.ComplicationText import androidx.wear.watchface.complications.data.ComplicationType import androidx.wear.watchface.complications.data.LongTextComplicationData -import androidx.wear.watchface.complications.data.NoDataComplicationData import androidx.wear.watchface.complications.data.PlainComplicationText import androidx.wear.watchface.complications.data.RangedValueComplicationData -import androidx.wear.watchface.complications.data.ShortTextComplicationData -import androidx.wear.watchface.control.IInteractiveWatchFace import androidx.wear.watchface.control.WatchFaceControlService import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.BLUE_STYLE @@ -88,26 +77,22 @@ import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.GREEN_STYLE -import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.LEFT_COMPLICATION import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.NO_COMPLICATIONS import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.WATCH_HAND_LENGTH_STYLE_SETTING import androidx.wear.watchface.samples.ExampleOpenGLBackgroundInitWatchFaceService import androidx.wear.watchface.samples.R -import androidx.wear.watchface.style.CurrentUserStyleRepository import androidx.wear.watchface.style.UserStyle import androidx.wear.watchface.style.UserStyleData -import androidx.wear.watchface.style.UserStyleSchema -import androidx.wear.watchface.style.UserStyleSetting import androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting.BooleanOption import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption import androidx.wear.watchface.style.WatchFaceLayer import com.google.common.truth.Truth.assertThat import java.time.Instant -import java.time.ZoneId -import java.time.ZonedDateTime import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executor import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException +import java.util.function.Consumer import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred @@ -132,12 +117,10 @@ private const val DESTROY_TIMEOUT_MILLIS = 500L private const val UPDATE_TIMEOUT_MILLIS = 500L -@RunWith(AndroidJUnit4::class) -@MediumTest @RequiresApi(Build.VERSION_CODES.O_MR1) -class WatchFaceControlClientTest { - private val context = ApplicationProvider.getApplicationContext<Context>() - private val service = runBlocking { +abstract class WatchFaceControlClientTestBase { + protected val context: Context = ApplicationProvider.getApplicationContext() + protected val service = runBlocking { WatchFaceControlClient.createWatchFaceControlClientImpl( context, Intent(context, WatchFaceControlTestService::class.java).apply { @@ -147,31 +130,35 @@ } @Mock - private lateinit var mockBinder: IBinder + protected lateinit var surfaceHolder: SurfaceHolder @Mock - private lateinit var iInteractiveWatchFace: IInteractiveWatchFace - - @Mock - private lateinit var surfaceHolder: SurfaceHolder - - @Mock - private lateinit var surfaceHolder2: SurfaceHolder + protected lateinit var surfaceHolder2: SurfaceHolder @Mock private lateinit var surface: Surface - private lateinit var engine: WatchFaceService.EngineWrapper - private val handler = Handler(Looper.getMainLooper()) - private val handlerCoroutineScope = + + protected val handler = Handler(Looper.getMainLooper()) + protected val handlerCoroutineScope = CoroutineScope(Handler(handler.looper).asCoroutineDispatcher()) - private lateinit var wallpaperService: WatchFaceService + + protected lateinit var engine: WatchFaceService.EngineWrapper + + protected val deviceConfig = DeviceConfig( + hasLowBitAmbient = false, + hasBurnInProtection = false, + analogPreviewReferenceTimeMillis = 0, + digitalPreviewReferenceTimeMillis = 0 + ) + + protected val systemState = WatchUiState(false, 0) + + protected val complications = createTestComplications(context) @Before fun setUp() { MockitoAnnotations.initMocks(this) WatchFaceControlTestService.apiVersionOverride = null - wallpaperService = TestExampleCanvasAnalogWatchFaceService(context, surfaceHolder) - Mockito.`when`(surfaceHolder.surfaceFrame) .thenReturn(Rect(0, 0, 400, 400)) Mockito.`when`(surfaceHolder.surface).thenReturn(surface) @@ -193,59 +180,48 @@ service.close() } - @get:Rule - val screenshotRule: AndroidXScreenshotTestRule = - AndroidXScreenshotTestRule("wear/wear-watchface-client") - - private val exampleCanvasAnalogWatchFaceComponentName = ComponentName( - "androidx.wear.watchface.samples.test", - "androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService" - ) - - private val exampleOpenGLWatchFaceComponentName = ComponentName( - "androidx.wear.watchface.samples.test", - "androidx.wear.watchface.samples.ExampleOpenGLBackgroundInitWatchFaceService" - ) - - private val deviceConfig = DeviceConfig( - false, - false, - 0, - 0 - ) - - private val systemState = WatchUiState(false, 0) - - private val complications = mapOf( - EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to - ShortTextComplicationData.Builder( - PlainComplicationText.Builder("ID").build(), - ComplicationText.EMPTY - ).setTitle(PlainComplicationText.Builder("Left").build()) - .setTapAction( - PendingIntent.getActivity(context, 0, Intent("left"), FLAG_IMMUTABLE) + protected fun getOrCreateTestSubject( + watchFaceService: WatchFaceService = + TestExampleCanvasAnalogWatchFaceService(context, surfaceHolder), + instanceId: String = "testId", + userStyle: UserStyleData? = null, + complications: Map<Int, ComplicationData>? = this.complications, + previewExecutor: Executor? = null, + previewListener: Consumer<String>? = null + ): InteractiveWatchFaceClient { + val deferredInteractiveInstance = handlerCoroutineScope.async { + if (previewExecutor != null && previewListener != null) { + service.getOrCreateInteractiveWatchFaceClient( + instanceId, + deviceConfig, + systemState, + userStyle, + complications, + previewExecutor, + previewListener ) - .build(), - EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID to - ShortTextComplicationData.Builder( - PlainComplicationText.Builder("ID").build(), - ComplicationText.EMPTY - ).setTitle(PlainComplicationText.Builder("Right").build()) - .setTapAction( - PendingIntent.getActivity(context, 0, Intent("right"), FLAG_IMMUTABLE) + } else { + @Suppress("deprecation") + service.getOrCreateInteractiveWatchFaceClient( + instanceId, + deviceConfig, + systemState, + userStyle, + complications ) - .build() - ) - - private fun createEngine() { - // onCreateEngine must run after getOrCreateInteractiveWatchFaceClient. To ensure the - // ordering relationship both calls should run on the same handler. - handler.post { - engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper + } } + + // Create the engine which triggers construction of the interactive instance. + handler.post { + engine = watchFaceService.onCreateEngine() as WatchFaceService.EngineWrapper + } + + // Wait for the instance to be created. + return awaitWithTimeout(deferredInteractiveInstance) } - private fun <X> awaitWithTimeout( + protected fun <X> awaitWithTimeout( thing: Deferred<X>, timeoutMillis: Long = CONNECT_TIMEOUT_MILLIS ): X { @@ -260,173 +236,14 @@ } return value!! } +} +@RunWith(AndroidJUnit4::class) +@MediumTest +@RequiresApi(Build.VERSION_CODES.O_MR1) +class WatchFaceControlClientTest : WatchFaceControlClientTestBase() { - @SuppressLint("NewApi") // renderWatchFaceToBitmap - @Test - fun headlessScreenshot() { - val headlessInstance = service.createHeadlessWatchFaceClient( - "id", - exampleCanvasAnalogWatchFaceComponentName, - DeviceConfig( - false, - false, - 0, - 0 - ), - 400, - 400 - )!! - val bitmap = headlessInstance.renderWatchFaceToBitmap( - RenderParameters( - DrawMode.INTERACTIVE, - WatchFaceLayer.ALL_WATCH_FACE_LAYERS, - null - ), - Instant.ofEpochMilli(1234567), - null, - complications - ) - - bitmap.assertAgainstGolden(screenshotRule, "headlessScreenshot") - - headlessInstance.close() - } - - @SuppressLint("NewApi") // renderWatchFaceToBitmap - @Test - fun yellowComplicationHighlights() { - val headlessInstance = service.createHeadlessWatchFaceClient( - "id", - exampleCanvasAnalogWatchFaceComponentName, - DeviceConfig( - false, - false, - 0, - 0 - ), - 400, - 400 - )!! - val bitmap = headlessInstance.renderWatchFaceToBitmap( - RenderParameters( - DrawMode.INTERACTIVE, - WatchFaceLayer.ALL_WATCH_FACE_LAYERS, - RenderParameters.HighlightLayer( - RenderParameters.HighlightedElement.AllComplicationSlots, - Color.YELLOW, - Color.argb(128, 0, 0, 0) // Darken everything else. - ) - ), - Instant.ofEpochMilli(1234567), - null, - complications - ) - - bitmap.assertAgainstGolden(screenshotRule, "yellowComplicationHighlights") - - headlessInstance.close() - } - - @SuppressLint("NewApi") // renderWatchFaceToBitmap - @Test - fun highlightOnlyLayer() { - val headlessInstance = service.createHeadlessWatchFaceClient( - "id", - exampleCanvasAnalogWatchFaceComponentName, - DeviceConfig( - false, - false, - 0, - 0 - ), - 400, - 400 - )!! - val bitmap = headlessInstance.renderWatchFaceToBitmap( - RenderParameters( - DrawMode.INTERACTIVE, - emptySet(), - RenderParameters.HighlightLayer( - RenderParameters.HighlightedElement.AllComplicationSlots, - Color.YELLOW, - Color.argb(128, 0, 0, 0) // Darken everything else. - ) - ), - Instant.ofEpochMilli(1234567), - null, - complications - ) - - bitmap.assertAgainstGolden(screenshotRule, "highlightOnlyLayer") - - headlessInstance.close() - } - - @Suppress("DEPRECATION", "NewApi") // defaultDataSourceType - @Test - fun headlessComplicationDetails() { - val headlessInstance = service.createHeadlessWatchFaceClient( - "id", - exampleCanvasAnalogWatchFaceComponentName, - deviceConfig, - 400, - 400 - )!! - - assertThat(headlessInstance.complicationSlotsState.size).isEqualTo(2) - - val leftComplicationDetails = headlessInstance.complicationSlotsState[ - EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID - ]!! - assertThat(leftComplicationDetails.bounds).isEqualTo(Rect(80, 160, 160, 240)) - assertThat(leftComplicationDetails.boundsType) - .isEqualTo(ComplicationSlotBoundsType.ROUND_RECT) - assertThat( - leftComplicationDetails.defaultDataSourcePolicy.systemDataSourceFallback - ).isEqualTo( - SystemDataSources.DATA_SOURCE_DAY_OF_WEEK - ) - assertThat(leftComplicationDetails.defaultDataSourceType).isEqualTo( - ComplicationType.SHORT_TEXT - ) - assertThat(leftComplicationDetails.supportedTypes).containsExactly( - ComplicationType.RANGED_VALUE, - ComplicationType.GOAL_PROGRESS, - ComplicationType.WEIGHTED_ELEMENTS, - ComplicationType.LONG_TEXT, - ComplicationType.SHORT_TEXT, - ComplicationType.MONOCHROMATIC_IMAGE, - ComplicationType.SMALL_IMAGE - ) - assertTrue(leftComplicationDetails.isEnabled) - - val rightComplicationDetails = headlessInstance.complicationSlotsState[ - EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID - ]!! - assertThat(rightComplicationDetails.bounds).isEqualTo(Rect(240, 160, 320, 240)) - assertThat(rightComplicationDetails.boundsType) - .isEqualTo(ComplicationSlotBoundsType.ROUND_RECT) - assertThat( - rightComplicationDetails.defaultDataSourcePolicy.systemDataSourceFallback - ).isEqualTo( - SystemDataSources.DATA_SOURCE_STEP_COUNT - ) - assertThat(rightComplicationDetails.defaultDataSourceType).isEqualTo( - ComplicationType.SHORT_TEXT - ) - assertThat(rightComplicationDetails.supportedTypes).containsExactly( - ComplicationType.RANGED_VALUE, - ComplicationType.GOAL_PROGRESS, - ComplicationType.WEIGHTED_ELEMENTS, - ComplicationType.LONG_TEXT, - ComplicationType.SHORT_TEXT, - ComplicationType.MONOCHROMATIC_IMAGE, - ComplicationType.SMALL_IMAGE - ) - assertTrue(rightComplicationDetails.isEnabled) - - headlessInstance.close() - } + private val exampleCanvasAnalogWatchFaceComponentName = + componentOf<ExampleCanvasAnalogWatchFaceService>() @Test fun complicationProviderDefaults() { @@ -434,23 +251,9 @@ context, surfaceHolder ) - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } - // Create the engine which triggers construction of the interactive instance. - handler.post { - engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper - } - - // Wait for the instance to be created. - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) + val interactiveInstance = getOrCreateTestSubject( + wallpaperService + ) try { assertThat(interactiveInstance.complicationSlotsState.keys).containsExactly(123) @@ -480,23 +283,9 @@ context, surfaceHolder ) - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } - // Create the engine which triggers construction of the interactive instance. - handler.post { - engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper - } - - // Wait for the instance to be created. - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) + val interactiveInstance = getOrCreateTestSubject( + wallpaperService + ) try { assertThat(interactiveInstance.complicationSlotsState.keys).containsExactly(123) @@ -516,23 +305,8 @@ context, surfaceHolder ) - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } - // Create the engine which triggers construction of the interactive instance. - handler.post { - engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper - } - // Wait for the instance to be created. - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) + val interactiveInstance = getOrCreateTestSubject(wallpaperService) // User style settings to be updated val userStyleSettings = interactiveInstance.userStyleSchema.userStyleSettings @@ -561,176 +335,10 @@ } } - @Test - @Suppress("Deprecation") // userStyleSettings - fun headlessUserStyleSchema() { - val headlessInstance = service.createHeadlessWatchFaceClient( - "id", - exampleCanvasAnalogWatchFaceComponentName, - deviceConfig, - 400, - 400 - )!! - - assertThat(headlessInstance.userStyleSchema.userStyleSettings.size).isEqualTo(5) - assertThat(headlessInstance.userStyleSchema.userStyleSettings[0].id.value).isEqualTo( - "color_style_setting" - ) - assertThat(headlessInstance.userStyleSchema.userStyleSettings[1].id.value).isEqualTo( - "draw_hour_pips_style_setting" - ) - assertThat(headlessInstance.userStyleSchema.userStyleSettings[2].id.value).isEqualTo( - "watch_hand_length_style_setting" - ) - assertThat(headlessInstance.userStyleSchema.userStyleSettings[3].id.value).isEqualTo( - "complications_style_setting" - ) - assertThat(headlessInstance.userStyleSchema.userStyleSettings[4].id.value).isEqualTo( - "hours_draw_freq_style_setting" - ) - - headlessInstance.close() - } - - @Test - fun headlessUserStyleFlavors() { - val headlessInstance = service.createHeadlessWatchFaceClient( - "id", - exampleCanvasAnalogWatchFaceComponentName, - deviceConfig, - 400, - 400 - )!! - - assertThat(headlessInstance.getUserStyleFlavors().flavors.size).isEqualTo(1) - val flavorA = headlessInstance.getUserStyleFlavors().flavors[0] - assertThat(flavorA.id).isEqualTo("exampleFlavor") - assertThat(flavorA.style.userStyleMap.containsKey("color_style_setting")) - assertThat(flavorA.style.userStyleMap.containsKey("watch_hand_length_style_setting")) - assertThat(flavorA.complications.containsKey(EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID)) - assertThat( - flavorA.complications.containsKey( - EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID - ) - ) - - headlessInstance.close() - } - - @Test - @Suppress("Deprecation") // userStyleSettings - fun headlessToBundleAndCreateFromBundle() { - val headlessInstance = HeadlessWatchFaceClient.createFromBundle( - service.createHeadlessWatchFaceClient( - "id", - exampleCanvasAnalogWatchFaceComponentName, - deviceConfig, - 400, - 400 - )!!.toBundle() - ) - - assertThat(headlessInstance.userStyleSchema.userStyleSettings.size).isEqualTo(5) - } - - @SuppressLint("NewApi") // renderWatchFaceToBitmap - @Test - fun getOrCreateInteractiveWatchFaceClient() { - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } - - // Create the engine which triggers creation of InteractiveWatchFaceClient. - createEngine() - - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) - - val bitmap = interactiveInstance.renderWatchFaceToBitmap( - RenderParameters( - DrawMode.INTERACTIVE, - WatchFaceLayer.ALL_WATCH_FACE_LAYERS, - null - ), - Instant.ofEpochMilli(1234567), - null, - complications - ) - - try { - bitmap.assertAgainstGolden(screenshotRule, "interactiveScreenshot") - } finally { - interactiveInstance.close() - } - } - - @SuppressLint("NewApi") // renderWatchFaceToBitmap - @Test - fun getOrCreateInteractiveWatchFaceClient_initialStyle() { - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - // An incomplete map which is OK. - UserStyleData( - mapOf( - "color_style_setting" to "green_style".encodeToByteArray(), - "draw_hour_pips_style_setting" to BooleanOption.FALSE.id.value, - "watch_hand_length_style_setting" to DoubleRangeOption(0.8).id.value - ) - ), - complications - ) - } - - // Create the engine which triggers creation of InteractiveWatchFaceClient. - createEngine() - - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) - - val bitmap = interactiveInstance.renderWatchFaceToBitmap( - RenderParameters( - DrawMode.INTERACTIVE, - WatchFaceLayer.ALL_WATCH_FACE_LAYERS, - null - ), - Instant.ofEpochMilli(1234567), - null, - complications - ) - - try { - bitmap.assertAgainstGolden(screenshotRule, "initialStyle") - } finally { - interactiveInstance.close() - } - } - @Suppress("DEPRECATION", "newApi") // defaultDataSourceType & ComplicationType @Test fun interactiveWatchFaceClient_ComplicationDetails() { - val deferredInteractiveInstance = handlerCoroutineScope.async { - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } - - // Create the engine which triggers creation of InteractiveWatchFaceClient. - createEngine() - - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) + val interactiveInstance = getOrCreateTestSubject() assertThat(interactiveInstance.complicationSlotsState.size).isEqualTo(2) @@ -762,9 +370,9 @@ ComplicationType.SHORT_TEXT ) assertThat(leftComplicationDetails.nameResourceId) - .isEqualTo(androidx.wear.watchface.samples.R.string.left_complication_screen_name) + .isEqualTo(R.string.left_complication_screen_name) assertThat(leftComplicationDetails.screenReaderNameResourceId).isEqualTo( - androidx.wear.watchface.samples.R.string.left_complication_screen_reader_name + R.string.left_complication_screen_reader_name ) val rightComplicationDetails = interactiveInstance.complicationSlotsState[ @@ -793,31 +401,17 @@ ComplicationType.SHORT_TEXT ) assertThat(rightComplicationDetails.nameResourceId) - .isEqualTo(androidx.wear.watchface.samples.R.string.right_complication_screen_name) + .isEqualTo(R.string.right_complication_screen_name) assertThat(rightComplicationDetails.screenReaderNameResourceId).isEqualTo( - androidx.wear.watchface.samples.R.string.right_complication_screen_reader_name + R.string.right_complication_screen_reader_name ) interactiveInstance.close() } @Test - public fun updateComplicationData() { - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } - - // Create the engine which triggers creation of InteractiveWatchFaceClient. - createEngine() - - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) + fun updateComplicationData() { + val interactiveInstance = getOrCreateTestSubject() // Under the hood updateComplicationData is a oneway aidl method so we need to perform some // additional synchronization to ensure it's side effects have been applied before @@ -877,21 +471,9 @@ @Test fun getOrCreateInteractiveWatchFaceClient_existingOpenInstance() { - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } + val watchFaceService = TestExampleCanvasAnalogWatchFaceService(context, surfaceHolder) - // Create the engine which triggers creation of InteractiveWatchFaceClient. - createEngine() - - awaitWithTimeout(deferredInteractiveInstance) + getOrCreateTestSubject(watchFaceService) val deferredInteractiveInstance2 = handlerCoroutineScope.async { @Suppress("deprecation") @@ -904,91 +486,22 @@ ) } - assertThat(awaitWithTimeout(deferredInteractiveInstance2).instanceId).isEqualTo("testId") - } - - @SuppressLint("NewApi") // renderWatchFaceToBitmap - @Test - fun getOrCreateInteractiveWatchFaceClient_existingOpenInstance_styleChange() { - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } - - // Create the engine which triggers creation of InteractiveWatchFaceClient. - createEngine() - - awaitWithTimeout(deferredInteractiveInstance) - - val deferredInteractiveInstance2 = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - UserStyleData( - mapOf( - "color_style_setting" to "blue_style".encodeToByteArray(), - "draw_hour_pips_style_setting" to BooleanOption.FALSE.id.value, - "watch_hand_length_style_setting" to DoubleRangeOption(0.25).id.value - ) - ), - complications - ) - } - - val interactiveInstance2 = awaitWithTimeout(deferredInteractiveInstance2) - assertThat(interactiveInstance2.instanceId).isEqualTo("testId") - - val bitmap = interactiveInstance2.renderWatchFaceToBitmap( - RenderParameters( - DrawMode.INTERACTIVE, - WatchFaceLayer.ALL_WATCH_FACE_LAYERS, - null - ), - Instant.ofEpochMilli(1234567), - null, - complications - ) - - try { - // Note the hour hand pips and both complicationSlots should be visible in this image. - bitmap.assertAgainstGolden(screenshotRule, "existingOpenInstance_styleChange") - } finally { - interactiveInstance2.close() - } + assertThat(awaitWithTimeout(deferredInteractiveInstance2).instanceId) + .isEqualTo("testId") } @Test fun getOrCreateInteractiveWatchFaceClient_existingClosedInstance() { - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } + val wallpaperService = TestExampleCanvasAnalogWatchFaceService(context, surfaceHolder) - // Create the engine which triggers creation of InteractiveWatchFaceClient. - createEngine() - - // Wait for the instance to be created. - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) + val interactiveInstance = getOrCreateTestSubject(wallpaperService) // Closing this interface means the subsequent // getOrCreateInteractiveWatchFaceClient won't immediately return // a resolved future. interactiveInstance.close() + // Connect again to the same wallpaperService instance val deferredExistingInstance = handlerCoroutineScope.async { @Suppress("deprecation") service.getOrCreateInteractiveWatchFaceClient( @@ -1012,25 +525,13 @@ @Test fun getInteractiveWatchFaceInstance() { - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } + val testId = "testId" + // Create and wait for an interactive instance without capturing a reference to it + getOrCreateTestSubject(instanceId = testId) - // Create the engine which triggers creation of InteractiveWatchFaceClient. - createEngine() - - // Wait for the instance to be created. - awaitWithTimeout(deferredInteractiveInstance) - + // Get the instance created above val sysUiInterface = - service.getInteractiveWatchFaceClientInstance("testId")!! + service.getInteractiveWatchFaceClientInstance(testId)!! val contentDescriptionLabels = sysUiInterface.contentDescriptionLabels assertThat(contentDescriptionLabels.size).isEqualTo(3) @@ -1061,22 +562,9 @@ @Test fun additionalContentDescriptionLabels() { - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } + val wallpaperService = TestExampleCanvasAnalogWatchFaceService(context, surfaceHolder) - // Create the engine which triggers creation of InteractiveWatchFaceClient. - createEngine() - - // Wait for the instance to be created. - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) + val interactiveInstance = getOrCreateTestSubject(wallpaperService) // We need to wait for watch face init to have completed before lateinit // wallpaperService.watchFace will be assigned. To do this we issue an arbitrary API @@ -1092,7 +580,7 @@ context, 0, Intent("Two"), PendingIntent.FLAG_IMMUTABLE ) - (wallpaperService as TestExampleCanvasAnalogWatchFaceService) + (wallpaperService) .watchFace.renderer.additionalContentDescriptionLabels = listOf( Pair( 0, @@ -1154,115 +642,16 @@ @Test fun contentDescriptionLabels_after_close() { - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } - - // Create the engine which triggers creation of InteractiveWatchFaceClient. - createEngine() - - // Wait for the instance to be created. - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) + val interactiveInstance = getOrCreateTestSubject() assertThat(interactiveInstance.contentDescriptionLabels).isNotEmpty() interactiveInstance.close() assertThat(interactiveInstance.contentDescriptionLabels).isEmpty() } - @SuppressLint("NewApi") // renderWatchFaceToBitmap - @Test - fun updateInstance() { - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - UserStyleData( - mapOf( - COLOR_STYLE_SETTING to GREEN_STYLE.encodeToByteArray(), - WATCH_HAND_LENGTH_STYLE_SETTING to DoubleRangeOption(0.25).id.value, - DRAW_HOUR_PIPS_STYLE_SETTING to BooleanOption.FALSE.id.value, - COMPLICATIONS_STYLE_SETTING to NO_COMPLICATIONS.encodeToByteArray() - ) - ), - complications - ) - } - - // Create the engine which triggers creation of InteractiveWatchFaceClient. - createEngine() - - // Wait for the instance to be created. - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) - - assertThat(interactiveInstance.instanceId).isEqualTo("testId") - - // Note this map doesn't include all the categories, which is fine the others will be set - // to their defaults. - interactiveInstance.updateWatchFaceInstance( - "testId2", - UserStyleData( - mapOf( - COLOR_STYLE_SETTING to BLUE_STYLE.encodeToByteArray(), - WATCH_HAND_LENGTH_STYLE_SETTING to DoubleRangeOption(0.9).id.value, - ) - ) - ) - - assertThat(interactiveInstance.instanceId).isEqualTo("testId2") - - // It should be possible to create an instance with the updated id. - val instance = - service.getInteractiveWatchFaceClientInstance("testId2") - assertThat(instance).isNotNull() - instance?.close() - - // The previous instance should still be usable despite the new instance being closed. - interactiveInstance.updateComplicationData(complications) - val bitmap = interactiveInstance.renderWatchFaceToBitmap( - RenderParameters( - DrawMode.INTERACTIVE, - WatchFaceLayer.ALL_WATCH_FACE_LAYERS, - null - ), - Instant.ofEpochMilli(1234567), - null, - complications - ) - - try { - // Note the hour hand pips and both complicationSlots should be visible in this image. - bitmap.assertAgainstGolden(screenshotRule, "setUserStyle") - } finally { - interactiveInstance.close() - } - } - @Test fun getComplicationIdAt() { - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } - - // Create the engine which triggers creation of InteractiveWatchFaceClient. - createEngine() - - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) + val interactiveInstance = getOrCreateTestSubject() assertNull(interactiveInstance.getComplicationIdAt(0, 0)) assertThat(interactiveInstance.getComplicationIdAt(85, 165)).isEqualTo( @@ -1491,23 +880,8 @@ onUiThreadGlSurfaceCreatedCompletableDeferred, onBackgroundThreadGlContextCreatedCompletableDeferred ) - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } - // Create the engine which triggers creation of the interactive instance. - handler.post { - engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper - } - // Wait for the instance to be created. - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) + val interactiveInstance = getOrCreateTestSubject(wallpaperService) try { val wfReady = CompletableDeferred<Unit>() @@ -1529,21 +903,8 @@ @Test fun isConnectionAlive_false_after_close() { - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } + val interactiveInstance = getOrCreateTestSubject() - // Create the engine which triggers creation of InteractiveWatchFaceClient. - createEngine() - - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) assertThat(interactiveInstance.isConnectionAlive()).isTrue() interactiveInstance.close() @@ -1561,74 +922,6 @@ assertTrue(service.hasComplicationDataCache()) } - @Ignore // b/225230182 - @Test - fun interactiveAndHeadlessOpenGlWatchFaceInstances() { - val surfaceTexture = SurfaceTexture(false) - surfaceTexture.setDefaultBufferSize(400, 400) - Mockito.`when`(surfaceHolder2.surface).thenReturn(Surface(surfaceTexture)) - Mockito.`when`(surfaceHolder2.surfaceFrame) - .thenReturn(Rect(0, 0, 400, 400)) - - wallpaperService = TestExampleOpenGLBackgroundInitWatchFaceService(context, surfaceHolder2) - - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - emptyMap() - ) - } - - // Create the engine which triggers creation of InteractiveWatchFaceClient. - createEngine() - - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) - val headlessInstance = HeadlessWatchFaceClient.createFromBundle( - service.createHeadlessWatchFaceClient( - "id", - exampleOpenGLWatchFaceComponentName, - deviceConfig, - 200, - 200 - )!!.toBundle() - ) - - // Take screenshots from both instances to confirm rendering works as expected despite the - // watch face using shared SharedAssets. - val interactiveBitmap = interactiveInstance.renderWatchFaceToBitmap( - RenderParameters( - DrawMode.INTERACTIVE, - WatchFaceLayer.ALL_WATCH_FACE_LAYERS, - null - ), - Instant.ofEpochMilli(1234567), - null, - null - ) - - interactiveBitmap.assertAgainstGolden(screenshotRule, "opengl_interactive") - - val headlessBitmap = headlessInstance.renderWatchFaceToBitmap( - RenderParameters( - DrawMode.INTERACTIVE, - WatchFaceLayer.ALL_WATCH_FACE_LAYERS, - null - ), - Instant.ofEpochMilli(1234567), - null, - null - ) - - headlessBitmap.assertAgainstGolden(screenshotRule, "opengl_headless") - - headlessInstance.close() - interactiveInstance.close() - } - @Test fun watchfaceOverlayStyle() { val wallpaperService = TestWatchfaceOverlayStyleWatchFaceService( @@ -1636,24 +929,8 @@ surfaceHolder, WatchFace.OverlayStyle(Color.valueOf(Color.RED), Color.valueOf(Color.BLACK)) ) - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } - // Create the engine which triggers creation of the interactive instance. - handler.post { - engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper - } - - // Wait for the instance to be created. - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) + val interactiveInstance = getOrCreateTestSubject(wallpaperService) assertThat(interactiveInstance.overlayStyle.backgroundColor) .isEqualTo(Color.valueOf(Color.RED)) @@ -1670,24 +947,8 @@ surfaceHolder, WatchFace.OverlayStyle(Color.valueOf(Color.RED), Color.valueOf(Color.BLACK)) ) - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } - // Create the engine which triggers creation of the interactive instance. - handler.post { - engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper - } - - // Wait for the instance to be created. - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) + val interactiveInstance = getOrCreateTestSubject(wallpaperService) interactiveInstance.close() @@ -1696,57 +957,14 @@ } @Test - fun computeUserStyleSchemaDigestHash() { - val headlessInstance1 = service.createHeadlessWatchFaceClient( - "id", - exampleCanvasAnalogWatchFaceComponentName, - DeviceConfig( - false, - false, - 0, - 0 - ), - 400, - 400 - )!! - - val headlessInstance2 = service.createHeadlessWatchFaceClient( - "id", - exampleOpenGLWatchFaceComponentName, - deviceConfig, - 400, - 400 - )!! - - assertThat(headlessInstance1.getUserStyleSchemaDigestHash()).isNotEqualTo( - headlessInstance2.getUserStyleSchemaDigestHash() - ) - } - - @Test @OptIn(ComplicationExperimental::class) fun edgeComplication_boundingArc() { val wallpaperService = TestEdgeComplicationWatchFaceService( context, surfaceHolder ) - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } - // Create the engine which triggers construction of the interactive instance. - handler.post { - engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper - } - // Wait for the instance to be created. - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) + val interactiveInstance = getOrCreateTestSubject(wallpaperService) try { assertThat(interactiveInstance.complicationSlotsState.keys).containsExactly(123) @@ -1760,49 +978,10 @@ } } - @Test - fun headlessLifeCycle() { - val headlessInstance = service.createHeadlessWatchFaceClient( - "id", - ComponentName( - "androidx.wear.watchface.client.test", - "androidx.wear.watchface.client.test.TestLifeCycleWatchFaceService" - ), - deviceConfig, - 400, - 400 - )!! - - // Blocks until the headless instance has been fully constructed. - headlessInstance.previewReferenceInstant - headlessInstance.close() - - assertThat(TestLifeCycleWatchFaceService.lifeCycleEvents).containsExactly( - "WatchFaceService.onCreate", - "Renderer.constructed", - "Renderer.onDestroy", - "WatchFaceService.onDestroy" - ) - } - @OptIn(WatchFaceClientExperimental::class, WatchFaceExperimental::class) @Test fun watchFaceColors() { - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } - - // Create the engine which triggers creation of InteractiveWatchFaceClient. - createEngine() - - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) + val interactiveInstance = getOrCreateTestSubject() try { val watchFaceColorsLatch = CountDownLatch(1) @@ -1869,25 +1048,12 @@ TestWatchFaceServiceWithPreviewImageUpdateRequest(context, surfaceHolder) var lastPreviewImageUpdateRequestedId = "" - val deferredInteractiveInstance = handlerCoroutineScope.async { - service.getOrCreateInteractiveWatchFaceClient( - "wfId-1", - deviceConfig, - systemState, - null, - complications, - { runnable -> runnable.run() }, - { lastPreviewImageUpdateRequestedId = it } - ) - } - - // Create the engine which triggers creation of the interactive instance. - handler.post { - engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper - } - - // Wait for the instance to be created. - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) + val interactiveInstance = getOrCreateTestSubject( + watchFaceService = wallpaperService, + instanceId = "wfId-1", + previewExecutor = { runnable -> runnable.run() }, + previewListener = { lastPreviewImageUpdateRequestedId = it } + ) assertTrue( wallpaperService.rendererInitializedLatch.await( @@ -1909,23 +1075,8 @@ context, surfaceHolder ) - val deferredInteractiveInstance = handlerCoroutineScope.async { - @Suppress("deprecation") - service.getOrCreateInteractiveWatchFaceClient( - "testId", - deviceConfig, - systemState, - null, - complications - ) - } - // Create the engine which triggers construction of the interactive instance. - handler.post { - engine = wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper - } - // Wait for the instance to be created. - val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) + val interactiveInstance = getOrCreateTestSubject(wallpaperService) var lastDisconnectReason = 0 interactiveInstance.addClientDisconnectListener( @@ -1933,9 +1084,8 @@ override fun onClientDisconnected(@DisconnectReason disconnectReason: Int) { lastDisconnectReason = disconnectReason } - }, - { it.run() } - ) + } + ) { it.run() } // Simulate detach. engine.onDestroy() @@ -1944,665 +1094,232 @@ } } -internal class TestExampleCanvasAnalogWatchFaceService( - testContext: Context, - private var surfaceHolderOverride: SurfaceHolder -) : ExampleCanvasAnalogWatchFaceService() { - internal lateinit var watchFace: WatchFace +@RunWith(AndroidJUnit4::class) +@MediumTest +@RequiresApi(Build.VERSION_CODES.O_MR1) +class WatchFaceControlClientScreenshotTest : WatchFaceControlClientTestBase() { + @get:Rule + val screenshotRule: AndroidXScreenshotTestRule = + AndroidXScreenshotTestRule("wear/wear-watchface-client") - init { - attachBaseContext(testContext) - } + private val exampleOpenGLWatchFaceComponentName = + componentOf<ExampleOpenGLBackgroundInitWatchFaceService>() - override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride + @SuppressLint("NewApi") // renderWatchFaceToBitmap + @Test + fun getOrCreateInteractiveWatchFaceClient() { + val interactiveInstance = getOrCreateTestSubject() - override suspend fun createWatchFace( - surfaceHolder: SurfaceHolder, - watchState: WatchState, - complicationSlotsManager: ComplicationSlotsManager, - currentUserStyleRepository: CurrentUserStyleRepository - ): WatchFace { - watchFace = super.createWatchFace( - surfaceHolder, - watchState, - complicationSlotsManager, - currentUserStyleRepository - ) - return watchFace - } -} - -internal class TestExampleOpenGLBackgroundInitWatchFaceService( - testContext: Context, - private var surfaceHolderOverride: SurfaceHolder -) : ExampleOpenGLBackgroundInitWatchFaceService() { - internal lateinit var watchFace: WatchFace - - init { - attachBaseContext(testContext) - } - - override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride - - override suspend fun createWatchFace( - surfaceHolder: SurfaceHolder, - watchState: WatchState, - complicationSlotsManager: ComplicationSlotsManager, - currentUserStyleRepository: CurrentUserStyleRepository - ): WatchFace { - watchFace = super.createWatchFace( - surfaceHolder, - watchState, - complicationSlotsManager, - currentUserStyleRepository - ) - return watchFace - } -} - -internal open class TestCrashingWatchFaceService : WatchFaceService() { - - companion object { - const val COMPLICATION_ID = 123 - } - - override fun createComplicationSlotsManager( - currentUserStyleRepository: CurrentUserStyleRepository - ): ComplicationSlotsManager { - return ComplicationSlotsManager( - listOf( - ComplicationSlot.createRoundRectComplicationSlotBuilder( - COMPLICATION_ID, - { _, _ -> throw Exception("Deliberately crashing") }, - listOf(ComplicationType.LONG_TEXT), - DefaultComplicationDataSourcePolicy( - SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET, - ComplicationType.LONG_TEXT - ), - ComplicationSlotBounds(RectF(0.1f, 0.1f, 0.4f, 0.4f)) - ).build() + val bitmap = interactiveInstance.renderWatchFaceToBitmap( + RenderParameters( + DrawMode.INTERACTIVE, + WatchFaceLayer.ALL_WATCH_FACE_LAYERS, + null ), - currentUserStyleRepository + Instant.ofEpochMilli(1234567), + null, + complications ) - } - override suspend fun createWatchFace( - surfaceHolder: SurfaceHolder, - watchState: WatchState, - complicationSlotsManager: ComplicationSlotsManager, - currentUserStyleRepository: CurrentUserStyleRepository - ): WatchFace { - throw Exception("Deliberately crashing") - } -} - -internal class TestWatchfaceOverlayStyleWatchFaceService( - testContext: Context, - private var surfaceHolderOverride: SurfaceHolder, - private var watchFaceOverlayStyle: WatchFace.OverlayStyle -) : WatchFaceService() { - - init { - attachBaseContext(testContext) - } - - override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride - - override suspend fun createWatchFace( - surfaceHolder: SurfaceHolder, - watchState: WatchState, - complicationSlotsManager: ComplicationSlotsManager, - currentUserStyleRepository: CurrentUserStyleRepository - ) = WatchFace( - WatchFaceType.DIGITAL, - @Suppress("deprecation") - object : Renderer.CanvasRenderer( - surfaceHolder, - currentUserStyleRepository, - watchState, - CanvasType.HARDWARE, - 16 - ) { - override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) { - // Actually rendering something isn't required. - } - - override fun renderHighlightLayer( - canvas: Canvas, - bounds: Rect, - zonedDateTime: ZonedDateTime - ) { - // Actually rendering something isn't required. - } + try { + bitmap.assertAgainstGolden(screenshotRule, "interactiveScreenshot") + } finally { + interactiveInstance.close() } - ).setOverlayStyle(watchFaceOverlayStyle) -} - -internal class TestAsyncCanvasRenderInitWatchFaceService( - testContext: Context, - private var surfaceHolderOverride: SurfaceHolder, - private var initCompletableDeferred: CompletableDeferred<Unit> -) : WatchFaceService() { - - init { - attachBaseContext(testContext) } - override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride - - override suspend fun createWatchFace( - surfaceHolder: SurfaceHolder, - watchState: WatchState, - complicationSlotsManager: ComplicationSlotsManager, - currentUserStyleRepository: CurrentUserStyleRepository - ) = WatchFace( - WatchFaceType.DIGITAL, - @Suppress("deprecation") - object : Renderer.CanvasRenderer( - surfaceHolder, - currentUserStyleRepository, - watchState, - CanvasType.HARDWARE, - 16 - ) { - override suspend fun init() { - initCompletableDeferred.await() - } - - override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) { - // Actually rendering something isn't required. - } - - override fun renderHighlightLayer( - canvas: Canvas, - bounds: Rect, - zonedDateTime: ZonedDateTime - ) { - TODO("Not yet implemented") - } - } - ) - - override fun getSystemTimeProvider() = object : SystemTimeProvider { - override fun getSystemTimeMillis() = 123456789L - - override fun getSystemTimeZoneId() = ZoneId.of("UTC") - } -} - -internal class TestAsyncGlesRenderInitWatchFaceService( - testContext: Context, - private var surfaceHolderOverride: SurfaceHolder, - private var onUiThreadGlSurfaceCreatedCompletableDeferred: CompletableDeferred<Unit>, - private var onBackgroundThreadGlContextCreatedCompletableDeferred: CompletableDeferred<Unit> -) : WatchFaceService() { - internal lateinit var watchFace: WatchFace - - init { - attachBaseContext(testContext) - } - - override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride - - override suspend fun createWatchFace( - surfaceHolder: SurfaceHolder, - watchState: WatchState, - complicationSlotsManager: ComplicationSlotsManager, - currentUserStyleRepository: CurrentUserStyleRepository - ) = WatchFace( - WatchFaceType.DIGITAL, - @Suppress("deprecation") - object : Renderer.GlesRenderer( - surfaceHolder, - currentUserStyleRepository, - watchState, - 16 - ) { - override suspend fun onUiThreadGlSurfaceCreated(width: Int, height: Int) { - onUiThreadGlSurfaceCreatedCompletableDeferred.await() - } - - override suspend fun onBackgroundThreadGlContextCreated() { - onBackgroundThreadGlContextCreatedCompletableDeferred.await() - } - - override fun render(zonedDateTime: ZonedDateTime) { - // GLES rendering is complicated and not strictly necessary for our test. - } - - override fun renderHighlightLayer(zonedDateTime: ZonedDateTime) { - TODO("Not yet implemented") - } - } - ) -} - -internal class TestComplicationProviderDefaultsWatchFaceService( - testContext: Context, - private var surfaceHolderOverride: SurfaceHolder -) : WatchFaceService() { - - init { - attachBaseContext(testContext) - } - - override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride - - override fun createComplicationSlotsManager( - currentUserStyleRepository: CurrentUserStyleRepository - ): ComplicationSlotsManager { - return ComplicationSlotsManager( - listOf( - ComplicationSlot.createRoundRectComplicationSlotBuilder( - 123, - { _, _ -> - object : CanvasComplication { - override fun render( - canvas: Canvas, - bounds: Rect, - zonedDateTime: ZonedDateTime, - renderParameters: RenderParameters, - slotId: Int - ) { - } - - override fun drawHighlight( - canvas: Canvas, - bounds: Rect, - boundsType: Int, - zonedDateTime: ZonedDateTime, - color: Int - ) { - } - - override fun getData() = NoDataComplicationData() - - override fun loadData( - complicationData: ComplicationData, - loadDrawablesAsynchronous: Boolean - ) { - } - } - }, - listOf( - ComplicationType.PHOTO_IMAGE, - ComplicationType.LONG_TEXT, - ComplicationType.SHORT_TEXT - ), - DefaultComplicationDataSourcePolicy( - ComponentName("com.package1", "com.app1"), - ComplicationType.PHOTO_IMAGE, - ComponentName("com.package2", "com.app2"), - ComplicationType.LONG_TEXT, - SystemDataSources.DATA_SOURCE_STEP_COUNT, - ComplicationType.SHORT_TEXT - ), - ComplicationSlotBounds( - RectF(0.1f, 0.2f, 0.3f, 0.4f) - ) + @SuppressLint("NewApi") // renderWatchFaceToBitmap + @Test + fun getOrCreateInteractiveWatchFaceClient_initialStyle() { + val interactiveInstance = getOrCreateTestSubject( + // An incomplete map which is OK. + userStyle = UserStyleData( + mapOf( + "color_style_setting" to "green_style".encodeToByteArray(), + "draw_hour_pips_style_setting" to BooleanOption.FALSE.id.value, + "watch_hand_length_style_setting" to DoubleRangeOption(0.8).id.value ) - .build() - ), - currentUserStyleRepository + ) ) - } - override suspend fun createWatchFace( - surfaceHolder: SurfaceHolder, - watchState: WatchState, - complicationSlotsManager: ComplicationSlotsManager, - currentUserStyleRepository: CurrentUserStyleRepository - ) = WatchFace( - WatchFaceType.DIGITAL, - @Suppress("deprecation") - object : Renderer.CanvasRenderer( - surfaceHolder, - currentUserStyleRepository, - watchState, - CanvasType.HARDWARE, - 16 - ) { - override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {} - - override fun renderHighlightLayer( - canvas: Canvas, - bounds: Rect, - zonedDateTime: ZonedDateTime - ) { - } - } - ) -} - -internal class TestEdgeComplicationWatchFaceService( - testContext: Context, - private var surfaceHolderOverride: SurfaceHolder -) : WatchFaceService() { - - init { - attachBaseContext(testContext) - } - - override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride - - @OptIn(ComplicationExperimental::class) - override fun createComplicationSlotsManager( - currentUserStyleRepository: CurrentUserStyleRepository - ): ComplicationSlotsManager { - return ComplicationSlotsManager( - listOf( - ComplicationSlot.createEdgeComplicationSlotBuilder( - 123, - { _, _ -> - object : CanvasComplication { - override fun render( - canvas: Canvas, - bounds: Rect, - zonedDateTime: ZonedDateTime, - renderParameters: RenderParameters, - slotId: Int - ) { - } - - override fun drawHighlight( - canvas: Canvas, - bounds: Rect, - boundsType: Int, - zonedDateTime: ZonedDateTime, - color: Int - ) { - } - - override fun getData() = NoDataComplicationData() - - override fun loadData( - complicationData: ComplicationData, - loadDrawablesAsynchronous: Boolean - ) { - } - } - }, - listOf( - ComplicationType.PHOTO_IMAGE, - ComplicationType.LONG_TEXT, - ComplicationType.SHORT_TEXT - ), - DefaultComplicationDataSourcePolicy( - ComponentName("com.package1", "com.app1"), - ComplicationType.PHOTO_IMAGE, - ComponentName("com.package2", "com.app2"), - ComplicationType.LONG_TEXT, - SystemDataSources.DATA_SOURCE_STEP_COUNT, - ComplicationType.SHORT_TEXT - ), - ComplicationSlotBounds( - RectF(0f, 0f, 1f, 1f) - ), - BoundingArc(45f, 90f, 0.1f) - ) - .build() + val bitmap = interactiveInstance.renderWatchFaceToBitmap( + RenderParameters( + DrawMode.INTERACTIVE, + WatchFaceLayer.ALL_WATCH_FACE_LAYERS, + null ), - currentUserStyleRepository + Instant.ofEpochMilli(1234567), + null, + complications ) - } - override suspend fun createWatchFace( - surfaceHolder: SurfaceHolder, - watchState: WatchState, - complicationSlotsManager: ComplicationSlotsManager, - currentUserStyleRepository: CurrentUserStyleRepository - ) = WatchFace( - WatchFaceType.DIGITAL, - @Suppress("deprecation") - object : Renderer.CanvasRenderer( - surfaceHolder, - currentUserStyleRepository, - watchState, - CanvasType.HARDWARE, - 16 - ) { - override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {} - - override fun renderHighlightLayer( - canvas: Canvas, - bounds: Rect, - zonedDateTime: ZonedDateTime - ) { - } + try { + bitmap.assertAgainstGolden(screenshotRule, "initialStyle") + } finally { + interactiveInstance.close() } - ) -} - -internal class TestLifeCycleWatchFaceService : WatchFaceService() { - companion object { - val lifeCycleEvents = ArrayList<String>() } - override fun onCreate() { - super.onCreate() - lifeCycleEvents.add("WatchFaceService.onCreate") - } + @SuppressLint("NewApi") // renderWatchFaceToBitmap + @Test + fun getOrCreateInteractiveWatchFaceClient_existingOpenInstance_styleChange() { + val watchFaceService = TestExampleCanvasAnalogWatchFaceService(context, surfaceHolder) - override fun onDestroy() { - super.onDestroy() - lifeCycleEvents.add("WatchFaceService.onDestroy") - } + val testId = "testId" - override suspend fun createWatchFace( - surfaceHolder: SurfaceHolder, - watchState: WatchState, - complicationSlotsManager: ComplicationSlotsManager, - currentUserStyleRepository: CurrentUserStyleRepository - ) = WatchFace( - WatchFaceType.DIGITAL, - @Suppress("deprecation") - object : Renderer.GlesRenderer( - surfaceHolder, - currentUserStyleRepository, - watchState, - 16 - ) { - init { - lifeCycleEvents.add("Renderer.constructed") - } + getOrCreateTestSubject(watchFaceService, instanceId = testId) - override fun onDestroy() { - super.onDestroy() - lifeCycleEvents.add("Renderer.onDestroy") - } - - override fun render(zonedDateTime: ZonedDateTime) {} - - override fun renderHighlightLayer(zonedDateTime: ZonedDateTime) {} - } - ) -} - -internal class TestWatchFaceServiceWithPreviewImageUpdateRequest( - testContext: Context, - private var surfaceHolderOverride: SurfaceHolder, -) : WatchFaceService() { - val rendererInitializedLatch = CountDownLatch(1) - - init { - attachBaseContext(testContext) - } - - override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride - - @Suppress("deprecation") - private lateinit var renderer: Renderer.CanvasRenderer - - fun triggerPreviewImageUpdateRequest() { - renderer.sendPreviewImageNeedsUpdateRequest() - } - - override suspend fun createWatchFace( - surfaceHolder: SurfaceHolder, - watchState: WatchState, - complicationSlotsManager: ComplicationSlotsManager, - currentUserStyleRepository: CurrentUserStyleRepository - ): WatchFace { - @Suppress("deprecation") - renderer = object : Renderer.CanvasRenderer( - surfaceHolder, - currentUserStyleRepository, - watchState, - CanvasType.HARDWARE, - 16 - ) { - override suspend fun init() { - rendererInitializedLatch.countDown() - } - - override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {} - - override fun renderHighlightLayer( - canvas: Canvas, - bounds: Rect, - zonedDateTime: ZonedDateTime - ) { - } - } - return WatchFace(WatchFaceType.DIGITAL, renderer) - } -} - -internal class TestComplicationStyleUpdateWatchFaceService( - testContext: Context, - private var surfaceHolderOverride: SurfaceHolder -) : WatchFaceService() { - - init { - attachBaseContext(testContext) - } - - @Suppress("deprecation") - private val complicationsStyleSetting = - UserStyleSetting.ComplicationSlotsUserStyleSetting( - UserStyleSetting.Id(COMPLICATIONS_STYLE_SETTING), - resources, - R.string.watchface_complications_setting, - R.string.watchface_complications_setting_description, - icon = null, - complicationConfig = listOf( - UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption( - UserStyleSetting.Option.Id(NO_COMPLICATIONS), - resources, - R.string.watchface_complications_setting_none, - null, - listOf( - UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( - 123, - enabled = false - ) + val deferredInteractiveInstance2 = handlerCoroutineScope.async { + @Suppress("deprecation") + service.getOrCreateInteractiveWatchFaceClient( + testId, + deviceConfig, + systemState, + UserStyleData( + mapOf( + "color_style_setting" to "blue_style".encodeToByteArray(), + "draw_hour_pips_style_setting" to BooleanOption.FALSE.id.value, + "watch_hand_length_style_setting" to DoubleRangeOption(0.25).id.value ) ), - UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption( - UserStyleSetting.Option.Id(LEFT_COMPLICATION), - resources, - R.string.watchface_complications_setting_left, - null, - listOf( - UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( - 123, - enabled = true, - nameResourceId = R.string.left_complication_screen_name, - screenReaderNameResourceId = - R.string.left_complication_screen_reader_name - ) - ) - ) + complications + ) + } + + val interactiveInstance2 = awaitWithTimeout(deferredInteractiveInstance2) + assertThat(interactiveInstance2.instanceId).isEqualTo("testId") + + val bitmap = interactiveInstance2.renderWatchFaceToBitmap( + RenderParameters( + DrawMode.INTERACTIVE, + WatchFaceLayer.ALL_WATCH_FACE_LAYERS, + null ), - listOf(WatchFaceLayer.COMPLICATIONS) + Instant.ofEpochMilli(1234567), + null, + complications ) - override fun createUserStyleSchema(): UserStyleSchema = - UserStyleSchema(listOf(complicationsStyleSetting)) - - override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride - - override fun createComplicationSlotsManager( - currentUserStyleRepository: CurrentUserStyleRepository - ): ComplicationSlotsManager { - return ComplicationSlotsManager( - listOf( - ComplicationSlot.createRoundRectComplicationSlotBuilder( - 123, - { _, _ -> - object : CanvasComplication { - override fun render( - canvas: Canvas, - bounds: Rect, - zonedDateTime: ZonedDateTime, - renderParameters: RenderParameters, - slotId: Int - ) { - } - - override fun drawHighlight( - canvas: Canvas, - bounds: Rect, - boundsType: Int, - zonedDateTime: ZonedDateTime, - color: Int - ) { - } - - override fun getData() = NoDataComplicationData() - - override fun loadData( - complicationData: ComplicationData, - loadDrawablesAsynchronous: Boolean - ) { - } - } - }, - listOf( - ComplicationType.PHOTO_IMAGE, - ComplicationType.LONG_TEXT, - ComplicationType.SHORT_TEXT - ), - DefaultComplicationDataSourcePolicy( - ComponentName("com.package1", "com.app1"), - ComplicationType.PHOTO_IMAGE, - ComponentName("com.package2", "com.app2"), - ComplicationType.LONG_TEXT, - SystemDataSources.DATA_SOURCE_STEP_COUNT, - ComplicationType.SHORT_TEXT - ), - ComplicationSlotBounds( - RectF(0.1f, 0.2f, 0.3f, 0.4f) - ) - ).build() - ), - currentUserStyleRepository - ) + try { + // Note the hour hand pips and both complicationSlots should be visible in this image. + bitmap.assertAgainstGolden(screenshotRule, "existingOpenInstance_styleChange") + } finally { + interactiveInstance2.close() + } } - override suspend fun createWatchFace( - surfaceHolder: SurfaceHolder, - watchState: WatchState, - complicationSlotsManager: ComplicationSlotsManager, - currentUserStyleRepository: CurrentUserStyleRepository - ) = WatchFace( - WatchFaceType.ANALOG, - @Suppress("deprecation") - object : Renderer.CanvasRenderer( - surfaceHolder, - currentUserStyleRepository, - watchState, - CanvasType.HARDWARE, - 16 - ) { - override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {} + @SuppressLint("NewApi") // renderWatchFaceToBitmap + @Test + fun updateInstance() { + val interactiveInstance = getOrCreateTestSubject( + userStyle = UserStyleData( + mapOf( + COLOR_STYLE_SETTING to GREEN_STYLE.encodeToByteArray(), + WATCH_HAND_LENGTH_STYLE_SETTING to DoubleRangeOption(0.25).id.value, + DRAW_HOUR_PIPS_STYLE_SETTING to BooleanOption.FALSE.id.value, + COMPLICATIONS_STYLE_SETTING to NO_COMPLICATIONS.encodeToByteArray() + ) + ) + ) - override fun renderHighlightLayer( - canvas: Canvas, - bounds: Rect, - zonedDateTime: ZonedDateTime - ) { - } + assertThat(interactiveInstance.instanceId).isEqualTo("testId") + + // Note this map doesn't include all the categories, which is fine the others will be set + // to their defaults. + interactiveInstance.updateWatchFaceInstance( + "testId2", + UserStyleData( + mapOf( + COLOR_STYLE_SETTING to BLUE_STYLE.encodeToByteArray(), + WATCH_HAND_LENGTH_STYLE_SETTING to DoubleRangeOption(0.9).id.value, + ) + ) + ) + + assertThat(interactiveInstance.instanceId).isEqualTo("testId2") + + // It should be possible to create an instance with the updated id. + val instance = + service.getInteractiveWatchFaceClientInstance("testId2") + assertThat(instance).isNotNull() + instance?.close() + + // The previous instance should still be usable despite the new instance being closed. + interactiveInstance.updateComplicationData(complications) + val bitmap = interactiveInstance.renderWatchFaceToBitmap( + RenderParameters( + DrawMode.INTERACTIVE, + WatchFaceLayer.ALL_WATCH_FACE_LAYERS, + null + ), + Instant.ofEpochMilli(1234567), + null, + complications + ) + + try { + // Note the hour hand pips and both complicationSlots should be visible in this image. + bitmap.assertAgainstGolden(screenshotRule, "setUserStyle") + } finally { + interactiveInstance.close() } - ) + } + + @Ignore // b/225230182 + @Test + fun interactiveAndHeadlessOpenGlWatchFaceInstances() { + val surfaceTexture = SurfaceTexture(false) + surfaceTexture.setDefaultBufferSize(400, 400) + Mockito.`when`(surfaceHolder2.surface).thenReturn(Surface(surfaceTexture)) + Mockito.`when`(surfaceHolder2.surfaceFrame) + .thenReturn(Rect(0, 0, 400, 400)) + + val wallpaperService = + TestExampleOpenGLBackgroundInitWatchFaceService(context, surfaceHolder2) + + val interactiveInstance = getOrCreateTestSubject(wallpaperService, + complications = emptyMap() + ) + + val headlessInstance = HeadlessWatchFaceClient.createFromBundle( + service.createHeadlessWatchFaceClient( + "id", + exampleOpenGLWatchFaceComponentName, + deviceConfig, + 200, + 200 + )!!.toBundle() + ) + + // Take screenshots from both instances to confirm rendering works as expected despite the + // watch face using shared SharedAssets. + val interactiveBitmap = interactiveInstance.renderWatchFaceToBitmap( + RenderParameters( + DrawMode.INTERACTIVE, + WatchFaceLayer.ALL_WATCH_FACE_LAYERS, + null + ), + Instant.ofEpochMilli(1234567), + null, + null + ) + + interactiveBitmap.assertAgainstGolden(screenshotRule, "opengl_interactive") + + val headlessBitmap = headlessInstance.renderWatchFaceToBitmap( + RenderParameters( + DrawMode.INTERACTIVE, + WatchFaceLayer.ALL_WATCH_FACE_LAYERS, + null + ), + Instant.ofEpochMilli(1234567), + null, + null + ) + + headlessBitmap.assertAgainstGolden(screenshotRule, "opengl_headless") + + headlessInstance.close() + interactiveInstance.close() + } }