| /* |
| * Copyright 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.wear.watchface.client.test |
| |
| 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 |
| import android.graphics.Bitmap |
| 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 |
| 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.BoundingArc |
| import androidx.wear.watchface.CanvasComplication |
| import androidx.wear.watchface.complications.ComplicationSlotBounds |
| import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy |
| import androidx.wear.watchface.complications.SystemDataSources |
| import androidx.wear.watchface.complications.data.ComplicationText |
| import androidx.wear.watchface.complications.data.ComplicationType |
| import androidx.wear.watchface.complications.data.LongTextComplicationData |
| import androidx.wear.watchface.complications.data.PlainComplicationText |
| import androidx.wear.watchface.complications.data.RangedValueComplicationData |
| import androidx.wear.watchface.complications.data.ShortTextComplicationData |
| import androidx.wear.watchface.CanvasType |
| import androidx.wear.watchface.ComplicationSlot |
| import androidx.wear.watchface.ComplicationSlotBoundsType |
| 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.WatchFaceFlavorsExperimental |
| 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.HeadlessWatchFaceClient |
| import androidx.wear.watchface.client.InteractiveWatchFaceClient |
| import androidx.wear.watchface.client.WatchFaceControlClient |
| import androidx.wear.watchface.client.WatchUiState |
| import androidx.wear.watchface.complications.data.ComplicationData |
| import androidx.wear.watchface.complications.data.ComplicationExperimental |
| import androidx.wear.watchface.complications.data.NoDataComplicationData |
| import androidx.wear.watchface.control.IInteractiveWatchFace |
| import androidx.wear.watchface.control.WatchFaceControlService |
| import androidx.wear.watchface.samples.BLUE_STYLE |
| import androidx.wear.watchface.samples.COLOR_STYLE_SETTING |
| import androidx.wear.watchface.samples.COMPLICATIONS_STYLE_SETTING |
| import androidx.wear.watchface.samples.DRAW_HOUR_PIPS_STYLE_SETTING |
| import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID |
| import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID |
| import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService |
| import androidx.wear.watchface.samples.ExampleOpenGLBackgroundInitWatchFaceService |
| import androidx.wear.watchface.samples.GREEN_STYLE |
| import androidx.wear.watchface.samples.NO_COMPLICATIONS |
| import androidx.wear.watchface.samples.WATCH_HAND_LENGTH_STYLE_SETTING |
| import androidx.wear.watchface.style.CurrentUserStyleRepository |
| import androidx.wear.watchface.style.UserStyleData |
| 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 kotlinx.coroutines.CompletableDeferred |
| import kotlinx.coroutines.CoroutineScope |
| import kotlinx.coroutines.Deferred |
| import kotlinx.coroutines.android.asCoroutineDispatcher |
| import kotlinx.coroutines.async |
| import kotlinx.coroutines.launch |
| import kotlinx.coroutines.runBlocking |
| import org.junit.After |
| import org.junit.Assert.assertFalse |
| import org.junit.Assert.assertNull |
| import org.junit.Assert.assertTrue |
| import org.junit.Before |
| import org.junit.Ignore |
| import org.junit.Rule |
| import org.junit.Test |
| import org.junit.runner.RunWith |
| import org.mockito.Mock |
| import org.mockito.Mockito |
| import org.mockito.MockitoAnnotations |
| import java.time.Instant |
| import java.time.ZoneId |
| import java.time.ZonedDateTime |
| import java.util.concurrent.CountDownLatch |
| import java.util.concurrent.TimeUnit |
| import java.util.concurrent.TimeoutException |
| |
| private const val CONNECT_TIMEOUT_MILLIS = 500L |
| private const val DESTROY_TIMEOUT_MILLIS = 500L |
| private const val UPDATE_TIMEOUT_MILLIS = 500L |
| |
| @RunWith(AndroidJUnit4::class) |
| @MediumTest |
| class WatchFaceControlClientTest { |
| private val context = ApplicationProvider.getApplicationContext<Context>() |
| private val service = runBlocking { |
| WatchFaceControlClient.createWatchFaceControlClientImpl( |
| context, |
| Intent(context, WatchFaceControlTestService::class.java).apply { |
| action = WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE |
| } |
| ) |
| } |
| |
| @Mock |
| private lateinit var mockBinder: IBinder |
| |
| @Mock |
| private lateinit var iInteractiveWatchFace: IInteractiveWatchFace |
| |
| @Mock |
| private lateinit var surfaceHolder: SurfaceHolder |
| |
| @Mock |
| private 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 = |
| CoroutineScope(Handler(handler.looper).asCoroutineDispatcher()) |
| private lateinit var wallpaperService: WatchFaceService |
| |
| @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) |
| Mockito.`when`(surface.isValid).thenReturn(false) |
| } |
| |
| @After |
| fun tearDown() { |
| // Interactive instances are not currently shut down when all instances go away. E.g. WCS |
| // crashing does not cause the watch face to stop. So we need to shut down explicitly. |
| if (this::engine.isInitialized) { |
| val latch = CountDownLatch(1) |
| handler.post { |
| engine.onDestroy() |
| latch.countDown() |
| } |
| latch.await(DESTROY_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) |
| } |
| 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) |
| ) |
| .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) |
| ) |
| .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 |
| } |
| } |
| |
| private fun <X> awaitWithTimeout( |
| thing: Deferred<X>, |
| timeoutMillis: Long = CONNECT_TIMEOUT_MILLIS |
| ): X { |
| var value: X? = null |
| val latch = CountDownLatch(1) |
| handlerCoroutineScope.launch { |
| value = thing.await() |
| latch.countDown() |
| } |
| if (!latch.await(timeoutMillis, TimeUnit.MILLISECONDS)) { |
| throw TimeoutException("Timeout waiting for thing!") |
| } |
| return value!! |
| } |
| |
| @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") // 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.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.LONG_TEXT, |
| ComplicationType.SHORT_TEXT, |
| ComplicationType.MONOCHROMATIC_IMAGE, |
| ComplicationType.SMALL_IMAGE |
| ) |
| assertTrue(rightComplicationDetails.isEnabled) |
| |
| headlessInstance.close() |
| } |
| |
| @Test |
| fun complicationProviderDefaults() { |
| val wallpaperService = TestComplicationProviderDefaultsWatchFaceService( |
| context, |
| surfaceHolder |
| ) |
| val deferredInteractiveInstance = handlerCoroutineScope.async { |
| 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) |
| |
| try { |
| assertThat(interactiveInstance.complicationSlotsState.keys).containsExactly(123) |
| |
| val slot = interactiveInstance.complicationSlotsState[123]!! |
| assertThat(slot.defaultDataSourcePolicy.primaryDataSource) |
| .isEqualTo(ComponentName("com.package1", "com.app1")) |
| assertThat(slot.defaultDataSourcePolicy.primaryDataSourceDefaultType) |
| .isEqualTo(ComplicationType.PHOTO_IMAGE) |
| |
| assertThat(slot.defaultDataSourcePolicy.secondaryDataSource) |
| .isEqualTo(ComponentName("com.package2", "com.app2")) |
| assertThat(slot.defaultDataSourcePolicy.secondaryDataSourceDefaultType) |
| .isEqualTo(ComplicationType.LONG_TEXT) |
| |
| assertThat(slot.defaultDataSourcePolicy.systemDataSourceFallback) |
| .isEqualTo(SystemDataSources.DATA_SOURCE_STEP_COUNT) |
| assertThat(slot.defaultDataSourcePolicy.systemDataSourceFallbackDefaultType) |
| .isEqualTo(ComplicationType.SHORT_TEXT) |
| } finally { |
| interactiveInstance.close() |
| } |
| } |
| |
| @Test |
| fun unspecifiedComplicationSlotNames() { |
| val wallpaperService = TestComplicationProviderDefaultsWatchFaceService( |
| context, |
| surfaceHolder |
| ) |
| val deferredInteractiveInstance = handlerCoroutineScope.async { |
| 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) |
| |
| try { |
| assertThat(interactiveInstance.complicationSlotsState.keys).containsExactly(123) |
| |
| val slot = interactiveInstance.complicationSlotsState[123]!! |
| assertThat(slot.nameResourceId).isNull() |
| assertThat(slot.screenReaderNameResourceId).isNull() |
| } finally { |
| interactiveInstance.close() |
| } |
| } |
| |
| @Test |
| 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() |
| } |
| |
| @OptIn(WatchFaceFlavorsExperimental::class) |
| @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 |
| 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 { |
| 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 { |
| 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") // defaultDataSourceType |
| @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) |
| |
| assertThat(interactiveInstance.complicationSlotsState.size).isEqualTo(2) |
| |
| val leftComplicationDetails = interactiveInstance.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.LONG_TEXT, |
| ComplicationType.SHORT_TEXT, |
| ComplicationType.MONOCHROMATIC_IMAGE, |
| ComplicationType.SMALL_IMAGE |
| ) |
| assertTrue(leftComplicationDetails.isEnabled) |
| assertThat(leftComplicationDetails.currentType).isEqualTo( |
| ComplicationType.SHORT_TEXT |
| ) |
| assertThat(leftComplicationDetails.nameResourceId) |
| .isEqualTo(androidx.wear.watchface.samples.R.string.left_complication_screen_name) |
| assertThat(leftComplicationDetails.screenReaderNameResourceId).isEqualTo( |
| androidx.wear.watchface.samples.R.string.left_complication_screen_reader_name |
| ) |
| |
| val rightComplicationDetails = interactiveInstance.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.LONG_TEXT, |
| ComplicationType.SHORT_TEXT, |
| ComplicationType.MONOCHROMATIC_IMAGE, |
| ComplicationType.SMALL_IMAGE |
| ) |
| assertTrue(rightComplicationDetails.isEnabled) |
| assertThat(rightComplicationDetails.currentType).isEqualTo( |
| ComplicationType.SHORT_TEXT |
| ) |
| assertThat(rightComplicationDetails.nameResourceId) |
| .isEqualTo(androidx.wear.watchface.samples.R.string.right_complication_screen_name) |
| assertThat(rightComplicationDetails.screenReaderNameResourceId).isEqualTo( |
| androidx.wear.watchface.samples.R.string.right_complication_screen_reader_name |
| ) |
| |
| interactiveInstance.close() |
| } |
| |
| @Test |
| public fun updateComplicationData() { |
| val deferredInteractiveInstance = handlerCoroutineScope.async { |
| service.getOrCreateInteractiveWatchFaceClient( |
| "testId", |
| deviceConfig, |
| systemState, |
| null, |
| complications |
| ) |
| } |
| |
| // Create the engine which triggers creation of InteractiveWatchFaceClient. |
| createEngine() |
| |
| val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) |
| |
| // 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 |
| // inspecting complicationSlotsState otherwise we risk test flakes. |
| val updateCountDownLatch = CountDownLatch(1) |
| var leftComplicationSlot: ComplicationSlot |
| |
| runBlocking { |
| leftComplicationSlot = engine.deferredWatchFaceImpl.await() |
| .complicationSlotsManager.complicationSlots[ |
| EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID |
| ]!! |
| } |
| |
| handlerCoroutineScope.launch { |
| leftComplicationSlot.complicationData.collect { |
| updateCountDownLatch.countDown() |
| } |
| } |
| |
| interactiveInstance.updateComplicationData( |
| mapOf( |
| EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to |
| RangedValueComplicationData.Builder( |
| 50.0f, |
| 10.0f, |
| 100.0f, |
| ComplicationText.EMPTY |
| ).build(), |
| EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID to |
| LongTextComplicationData.Builder( |
| PlainComplicationText.Builder("Test").build(), |
| ComplicationText.EMPTY |
| ).build() |
| ) |
| ) |
| assertTrue(updateCountDownLatch.await(UPDATE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) |
| |
| assertThat(interactiveInstance.complicationSlotsState.size).isEqualTo(2) |
| |
| val leftComplicationDetails = interactiveInstance.complicationSlotsState[ |
| EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID |
| ]!! |
| val rightComplicationDetails = interactiveInstance.complicationSlotsState[ |
| EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID |
| ]!! |
| |
| assertThat(leftComplicationDetails.currentType).isEqualTo( |
| ComplicationType.RANGED_VALUE |
| ) |
| assertThat(rightComplicationDetails.currentType).isEqualTo( |
| ComplicationType.LONG_TEXT |
| ) |
| } |
| |
| @Test |
| fun getOrCreateInteractiveWatchFaceClient_existingOpenInstance() { |
| val deferredInteractiveInstance = handlerCoroutineScope.async { |
| service.getOrCreateInteractiveWatchFaceClient( |
| "testId", |
| deviceConfig, |
| systemState, |
| null, |
| complications |
| ) |
| } |
| |
| // Create the engine which triggers creation of InteractiveWatchFaceClient. |
| createEngine() |
| |
| awaitWithTimeout(deferredInteractiveInstance) |
| |
| val deferredInteractiveInstance2 = handlerCoroutineScope.async { |
| service.getOrCreateInteractiveWatchFaceClient( |
| "testId", |
| deviceConfig, |
| systemState, |
| null, |
| complications |
| ) |
| } |
| |
| assertThat(awaitWithTimeout(deferredInteractiveInstance2).instanceId).isEqualTo("testId") |
| } |
| |
| @SuppressLint("NewApi") // renderWatchFaceToBitmap |
| @Test |
| fun getOrCreateInteractiveWatchFaceClient_existingOpenInstance_styleChange() { |
| val deferredInteractiveInstance = handlerCoroutineScope.async { |
| service.getOrCreateInteractiveWatchFaceClient( |
| "testId", |
| deviceConfig, |
| systemState, |
| null, |
| complications |
| ) |
| } |
| |
| // Create the engine which triggers creation of InteractiveWatchFaceClient. |
| createEngine() |
| |
| awaitWithTimeout(deferredInteractiveInstance) |
| |
| val deferredInteractiveInstance2 = handlerCoroutineScope.async { |
| 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() |
| } |
| } |
| |
| @Test |
| fun getOrCreateInteractiveWatchFaceClient_existingClosedInstance() { |
| val deferredInteractiveInstance = handlerCoroutineScope.async { |
| 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) |
| |
| // Closing this interface means the subsequent |
| // getOrCreateInteractiveWatchFaceClient won't immediately return |
| // a resolved future. |
| interactiveInstance.close() |
| |
| val deferredExistingInstance = handlerCoroutineScope.async { |
| service.getOrCreateInteractiveWatchFaceClient( |
| "testId", |
| deviceConfig, |
| systemState, |
| null, |
| complications |
| ) |
| } |
| |
| assertFalse(deferredExistingInstance.isCompleted) |
| |
| // We don't want to leave a pending request or it'll mess up subsequent tests. |
| handler.post { |
| wallpaperService.onCreateEngine() as WatchFaceService.EngineWrapper |
| } |
| |
| awaitWithTimeout(deferredExistingInstance) |
| } |
| |
| @Test |
| fun getInteractiveWatchFaceInstance() { |
| val deferredInteractiveInstance = handlerCoroutineScope.async { |
| service.getOrCreateInteractiveWatchFaceClient( |
| "testId", |
| deviceConfig, |
| systemState, |
| null, |
| complications |
| ) |
| } |
| |
| // Create the engine which triggers creation of InteractiveWatchFaceClient. |
| createEngine() |
| |
| // Wait for the instance to be created. |
| awaitWithTimeout(deferredInteractiveInstance) |
| |
| val sysUiInterface = |
| service.getInteractiveWatchFaceClientInstance("testId")!! |
| |
| val contentDescriptionLabels = sysUiInterface.contentDescriptionLabels |
| assertThat(contentDescriptionLabels.size).isEqualTo(3) |
| // Central clock element. Note we don't know the timezone this test will be running in |
| // so we can't assert the contents of the clock's test. |
| assertThat(contentDescriptionLabels[0].bounds).isEqualTo(Rect(100, 100, 300, 300)) |
| assertThat( |
| contentDescriptionLabels[0].getTextAt(context.resources, Instant.EPOCH) |
| ).isNotEqualTo("") |
| |
| // Left complication. |
| assertThat(contentDescriptionLabels[1].bounds).isEqualTo(Rect(80, 160, 160, 240)) |
| assertThat( |
| contentDescriptionLabels[1].getTextAt(context.resources, Instant.EPOCH) |
| ).isEqualTo("ID Left") |
| |
| // Right complication. |
| assertThat(contentDescriptionLabels[2].bounds).isEqualTo(Rect(240, 160, 320, 240)) |
| assertThat( |
| contentDescriptionLabels[2].getTextAt(context.resources, Instant.EPOCH) |
| ).isEqualTo("ID Right") |
| |
| assertThat(sysUiInterface.overlayStyle.backgroundColor).isNull() |
| assertThat(sysUiInterface.overlayStyle.foregroundColor).isNull() |
| |
| sysUiInterface.close() |
| } |
| |
| @Test |
| fun additionalContentDescriptionLabels() { |
| val deferredInteractiveInstance = handlerCoroutineScope.async { |
| 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) |
| |
| // 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 |
| // call which by necessity awaits full initialization. |
| interactiveInstance.complicationSlotsState |
| |
| // Add some additional ContentDescriptionLabels |
| val pendingIntent1 = PendingIntent.getActivity( |
| context, 0, Intent("One"), |
| PendingIntent.FLAG_IMMUTABLE |
| ) |
| val pendingIntent2 = PendingIntent.getActivity( |
| context, 0, Intent("Two"), |
| PendingIntent.FLAG_IMMUTABLE |
| ) |
| (wallpaperService as TestExampleCanvasAnalogWatchFaceService) |
| .watchFace.renderer.additionalContentDescriptionLabels = listOf( |
| Pair( |
| 0, |
| ContentDescriptionLabel( |
| PlainComplicationText.Builder("Before").build(), |
| Rect(10, 10, 20, 20), |
| pendingIntent1 |
| ) |
| ), |
| Pair( |
| 20000, |
| ContentDescriptionLabel( |
| PlainComplicationText.Builder("After").build(), |
| Rect(30, 30, 40, 40), |
| pendingIntent2 |
| ) |
| ) |
| ) |
| |
| val sysUiInterface = |
| service.getInteractiveWatchFaceClientInstance("testId")!! |
| |
| val contentDescriptionLabels = sysUiInterface.contentDescriptionLabels |
| assertThat(contentDescriptionLabels.size).isEqualTo(5) |
| |
| // Central clock element. Note we don't know the timezone this test will be running in |
| // so we can't assert the contents of the clock's test. |
| assertThat(contentDescriptionLabels[0].bounds).isEqualTo(Rect(100, 100, 300, 300)) |
| assertThat( |
| contentDescriptionLabels[0].getTextAt(context.resources, Instant.EPOCH) |
| ).isNotEqualTo("") |
| |
| // First additional ContentDescriptionLabel. |
| assertThat(contentDescriptionLabels[1].bounds).isEqualTo(Rect(10, 10, 20, 20)) |
| assertThat( |
| contentDescriptionLabels[1].getTextAt(context.resources, Instant.EPOCH) |
| ).isEqualTo("Before") |
| assertThat(contentDescriptionLabels[1].tapAction).isEqualTo(pendingIntent1) |
| |
| // Left complication. |
| assertThat(contentDescriptionLabels[2].bounds).isEqualTo(Rect(80, 160, 160, 240)) |
| assertThat( |
| contentDescriptionLabels[2].getTextAt(context.resources, Instant.EPOCH) |
| ).isEqualTo("ID Left") |
| |
| // Right complication. |
| assertThat(contentDescriptionLabels[3].bounds).isEqualTo(Rect(240, 160, 320, 240)) |
| assertThat( |
| contentDescriptionLabels[3].getTextAt(context.resources, Instant.EPOCH) |
| ).isEqualTo("ID Right") |
| |
| // Second additional ContentDescriptionLabel. |
| assertThat(contentDescriptionLabels[4].bounds).isEqualTo(Rect(30, 30, 40, 40)) |
| assertThat( |
| contentDescriptionLabels[4].getTextAt(context.resources, Instant.EPOCH) |
| ).isEqualTo("After") |
| assertThat(contentDescriptionLabels[4].tapAction).isEqualTo(pendingIntent2) |
| } |
| |
| @Test |
| fun contentDescriptionLabels_after_close() { |
| val deferredInteractiveInstance = handlerCoroutineScope.async { |
| 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) |
| |
| assertThat(interactiveInstance.contentDescriptionLabels).isNotEmpty() |
| interactiveInstance.close() |
| assertThat(interactiveInstance.contentDescriptionLabels).isEmpty() |
| } |
| |
| @SuppressLint("NewApi") // renderWatchFaceToBitmap |
| @Test |
| fun updateInstance() { |
| val deferredInteractiveInstance = handlerCoroutineScope.async { |
| 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 { |
| service.getOrCreateInteractiveWatchFaceClient( |
| "testId", |
| deviceConfig, |
| systemState, |
| null, |
| complications |
| ) |
| } |
| |
| // Create the engine which triggers creation of InteractiveWatchFaceClient. |
| createEngine() |
| |
| val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) |
| |
| assertNull(interactiveInstance.getComplicationIdAt(0, 0)) |
| assertThat(interactiveInstance.getComplicationIdAt(85, 165)).isEqualTo( |
| EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID |
| ) |
| assertThat(interactiveInstance.getComplicationIdAt(255, 165)).isEqualTo( |
| EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID |
| ) |
| interactiveInstance.close() |
| } |
| |
| @Suppress("DEPRECATION") // DefaultComplicationDataSourcePolicyAndType |
| @Test |
| fun getDefaultProviderPolicies() { |
| assertThat( |
| service.getDefaultComplicationDataSourcePoliciesAndType( |
| exampleCanvasAnalogWatchFaceComponentName |
| ) |
| ).containsExactlyEntriesIn( |
| mapOf( |
| EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to |
| androidx.wear.watchface.client.DefaultComplicationDataSourcePolicyAndType( |
| DefaultComplicationDataSourcePolicy( |
| ComponentName( |
| androidx.wear.watchface.samples.CONFIGURABLE_DATA_SOURCE_PKG, |
| androidx.wear.watchface.samples.CONFIGURABLE_DATA_SOURCE |
| ), |
| ComplicationType.SHORT_TEXT, |
| SystemDataSources.DATA_SOURCE_DAY_OF_WEEK, |
| ComplicationType.SHORT_TEXT |
| ), |
| ComplicationType.SHORT_TEXT |
| ), |
| EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID to |
| androidx.wear.watchface.client.DefaultComplicationDataSourcePolicyAndType( |
| DefaultComplicationDataSourcePolicy( |
| SystemDataSources.DATA_SOURCE_STEP_COUNT, |
| ComplicationType.SHORT_TEXT |
| ), |
| ComplicationType.SHORT_TEXT |
| ) |
| ) |
| ) |
| } |
| |
| @Suppress("DEPRECATION") // DefaultComplicationDataSourcePolicyAndType |
| @Test |
| fun getDefaultProviderPoliciesOldApi() { |
| WatchFaceControlTestService.apiVersionOverride = 1 |
| assertThat( |
| service.getDefaultComplicationDataSourcePoliciesAndType( |
| exampleCanvasAnalogWatchFaceComponentName |
| ) |
| ).containsExactlyEntriesIn( |
| mapOf( |
| EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to |
| androidx.wear.watchface.client.DefaultComplicationDataSourcePolicyAndType( |
| DefaultComplicationDataSourcePolicy( |
| ComponentName( |
| androidx.wear.watchface.samples.CONFIGURABLE_DATA_SOURCE_PKG, |
| androidx.wear.watchface.samples.CONFIGURABLE_DATA_SOURCE |
| ), |
| ComplicationType.SHORT_TEXT, |
| SystemDataSources.DATA_SOURCE_DAY_OF_WEEK, |
| ComplicationType.SHORT_TEXT |
| ), |
| ComplicationType.SHORT_TEXT |
| ), |
| EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID to |
| androidx.wear.watchface.client.DefaultComplicationDataSourcePolicyAndType( |
| DefaultComplicationDataSourcePolicy( |
| SystemDataSources.DATA_SOURCE_STEP_COUNT, |
| ComplicationType.SHORT_TEXT |
| ), |
| ComplicationType.SHORT_TEXT |
| ) |
| ) |
| ) |
| } |
| |
| @Suppress("DEPRECATION") // DefaultComplicationDataSourcePolicyAndType |
| @Test |
| fun getDefaultProviderPolicies_with_TestCrashingWatchFaceService() { |
| // Tests that we can retrieve the DefaultComplicationDataSourcePolicy without invoking any |
| // parts of TestCrashingWatchFaceService that deliberately crash. |
| assertThat( |
| service.getDefaultComplicationDataSourcePoliciesAndType( |
| ComponentName( |
| "androidx.wear.watchface.client.test", |
| "androidx.wear.watchface.client.test.TestCrashingWatchFaceService" |
| |
| ) |
| ) |
| ).containsExactlyEntriesIn( |
| mapOf( |
| TestCrashingWatchFaceService.COMPLICATION_ID to |
| androidx.wear.watchface.client.DefaultComplicationDataSourcePolicyAndType( |
| DefaultComplicationDataSourcePolicy( |
| SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET, |
| ComplicationType.LONG_TEXT |
| ), |
| ComplicationType.LONG_TEXT |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun addWatchFaceReadyListener_canvasRender() { |
| val initCompletableDeferred = CompletableDeferred<Unit>() |
| val wallpaperService = TestAsyncCanvasRenderInitWatchFaceService( |
| context, |
| surfaceHolder, |
| initCompletableDeferred |
| ) |
| val deferredInteractiveInstance = handlerCoroutineScope.async { |
| service.getOrCreateInteractiveWatchFaceClient( |
| "testId", |
| deviceConfig, |
| systemState, |
| null, |
| complications |
| ) |
| } |
| |
| val bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) |
| val canvas = Canvas(bitmap) |
| Mockito.`when`(surfaceHolder.lockHardwareCanvas()).thenReturn(canvas) |
| |
| // 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) |
| |
| try { |
| val wfReady = CompletableDeferred<Unit>() |
| interactiveInstance.addOnWatchFaceReadyListener( |
| { runnable -> runnable.run() }, |
| { wfReady.complete(Unit) } |
| ) |
| assertThat(wfReady.isCompleted).isFalse() |
| |
| initCompletableDeferred.complete(Unit) |
| |
| // This should not timeout. |
| awaitWithTimeout(wfReady) |
| } finally { |
| interactiveInstance.close() |
| } |
| } |
| |
| @Test |
| fun removeWatchFaceReadyListener_canvasRender() { |
| val initCompletableDeferred = CompletableDeferred<Unit>() |
| val wallpaperService = TestAsyncCanvasRenderInitWatchFaceService( |
| context, |
| surfaceHolder, |
| initCompletableDeferred |
| ) |
| val deferredInteractiveInstance = handlerCoroutineScope.async { |
| service.getOrCreateInteractiveWatchFaceClient( |
| "testId", |
| deviceConfig, |
| systemState, |
| null, |
| complications |
| ) |
| } |
| |
| val bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) |
| val canvas = Canvas(bitmap) |
| Mockito.`when`(surfaceHolder.lockHardwareCanvas()).thenReturn(canvas) |
| |
| val renderLatch = CountDownLatch(1) |
| Mockito.`when`(surfaceHolder.unlockCanvasAndPost(canvas)).then { |
| renderLatch.countDown() |
| } |
| |
| // 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) |
| |
| try { |
| var listenerCalled = false |
| val listener = |
| InteractiveWatchFaceClient.OnWatchFaceReadyListener { listenerCalled = true } |
| interactiveInstance.addOnWatchFaceReadyListener( |
| { runnable -> runnable.run() }, |
| listener |
| ) |
| interactiveInstance.removeOnWatchFaceReadyListener(listener) |
| assertThat(listenerCalled).isFalse() |
| |
| initCompletableDeferred.complete(Unit) |
| |
| assertTrue(renderLatch.await(DESTROY_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) |
| |
| assertThat(listenerCalled).isFalse() |
| } finally { |
| interactiveInstance.close() |
| } |
| } |
| |
| @Test |
| fun addWatchFaceReadyListener_glesRender() { |
| val surfaceTexture = SurfaceTexture(false) |
| surfaceTexture.setDefaultBufferSize(10, 10) |
| Mockito.`when`(surfaceHolder2.surface).thenReturn(Surface(surfaceTexture)) |
| Mockito.`when`(surfaceHolder2.surfaceFrame) |
| .thenReturn(Rect(0, 0, 10, 10)) |
| |
| val onUiThreadGlSurfaceCreatedCompletableDeferred = CompletableDeferred<Unit>() |
| val onBackgroundThreadGlContextCreatedCompletableDeferred = CompletableDeferred<Unit>() |
| val wallpaperService = TestAsyncGlesRenderInitWatchFaceService( |
| context, |
| surfaceHolder2, |
| onUiThreadGlSurfaceCreatedCompletableDeferred, |
| onBackgroundThreadGlContextCreatedCompletableDeferred |
| ) |
| val deferredInteractiveInstance = handlerCoroutineScope.async { |
| 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) |
| |
| try { |
| val wfReady = CompletableDeferred<Unit>() |
| interactiveInstance.addOnWatchFaceReadyListener( |
| { runnable -> runnable.run() }, |
| { wfReady.complete(Unit) } |
| ) |
| assertThat(wfReady.isCompleted).isFalse() |
| |
| onUiThreadGlSurfaceCreatedCompletableDeferred.complete(Unit) |
| onBackgroundThreadGlContextCreatedCompletableDeferred.complete(Unit) |
| |
| // This can be a bit slow. |
| awaitWithTimeout(wfReady, 2000) |
| } finally { |
| interactiveInstance.close() |
| } |
| } |
| |
| @Test |
| fun isConnectionAlive_false_after_close() { |
| val deferredInteractiveInstance = handlerCoroutineScope.async { |
| service.getOrCreateInteractiveWatchFaceClient( |
| "testId", |
| deviceConfig, |
| systemState, |
| null, |
| complications |
| ) |
| } |
| |
| // Create the engine which triggers creation of InteractiveWatchFaceClient. |
| createEngine() |
| |
| val interactiveInstance = awaitWithTimeout(deferredInteractiveInstance) |
| assertThat(interactiveInstance.isConnectionAlive()).isTrue() |
| |
| interactiveInstance.close() |
| assertThat(interactiveInstance.isConnectionAlive()).isFalse() |
| } |
| |
| @Test |
| fun hasComplicationCache_oldApi() { |
| WatchFaceControlTestService.apiVersionOverride = 3 |
| assertFalse(service.hasComplicationDataCache()) |
| } |
| |
| @Test |
| fun hasComplicationCache_currentApi() { |
| assertTrue(service.hasComplicationDataCache()) |
| } |
| |
| @Ignore // b/225230182 |
| @RequiresApi(Build.VERSION_CODES.O_MR1) |
| @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 { |
| 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( |
| context, |
| surfaceHolder, |
| WatchFace.OverlayStyle(Color.valueOf(Color.RED), Color.valueOf(Color.BLACK)) |
| ) |
| val deferredInteractiveInstance = handlerCoroutineScope.async { |
| 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) |
| |
| assertThat(interactiveInstance.overlayStyle.backgroundColor) |
| .isEqualTo(Color.valueOf(Color.RED)) |
| assertThat(interactiveInstance.overlayStyle.foregroundColor) |
| .isEqualTo(Color.valueOf(Color.BLACK)) |
| |
| interactiveInstance.close() |
| } |
| |
| @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 { |
| 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) |
| |
| try { |
| assertThat(interactiveInstance.complicationSlotsState.keys).containsExactly(123) |
| |
| val slot = interactiveInstance.complicationSlotsState[123]!! |
| assertThat(slot.boundsType).isEqualTo(ComplicationSlotBoundsType.EDGE) |
| assertThat(slot.getBoundingArc()).isEqualTo(BoundingArc(45f, 90f, 0.1f)) |
| assertThat(slot.bounds).isEqualTo(Rect(0, 0, 400, 400)) |
| } finally { |
| interactiveInstance.close() |
| } |
| } |
| } |
| |
| 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") |
| } |
| } |
| ).setSystemTimeProvider(object : WatchFace.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 |
| ) { |
| } |
| } |
| ) |
| } |