blob: f8a1c6d728f7609ad979cca8f18bb3e7f4baa156 [file] [log] [blame]
/*
* 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
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Insets
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Icon
import android.os.BatteryManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Parcel
import android.support.wearable.complications.ComplicationData
import android.support.wearable.complications.ComplicationText
import android.support.wearable.watchface.Constants
import android.support.wearable.watchface.IWatchFaceService
import android.support.wearable.watchface.WatchFaceStyle
import android.support.wearable.watchface.accessibility.ContentDescriptionLabel
import android.view.Choreographer
import android.view.SurfaceHolder
import android.view.WindowInsets
import androidx.annotation.Px
import androidx.annotation.RequiresApi
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SdkSuppress
import androidx.wear.watchface.complications.ComplicationSlotBounds
import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
import androidx.wear.watchface.complications.SystemDataSources
import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.complications.data.EmptyComplicationData
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.complications.rendering.CanvasComplicationDrawable
import androidx.wear.watchface.complications.rendering.ComplicationDrawable
import androidx.wear.watchface.control.IInteractiveWatchFace
import androidx.wear.watchface.control.IPendingInteractiveWatchFace
import androidx.wear.watchface.control.InteractiveInstanceManager
import androidx.wear.watchface.control.data.CrashInfoParcel
import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams
import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
import androidx.wear.watchface.data.ComplicationSlotMetadataWireFormat
import androidx.wear.watchface.data.DeviceConfig
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
import androidx.wear.watchface.data.WatchUiState
import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyle
import androidx.wear.watchface.style.UserStyleSchema
import androidx.wear.watchface.style.UserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay
import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption
import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.Option
import androidx.wear.watchface.style.WatchFaceLayer
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.mock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Assume
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.Mockito.`when`
import org.mockito.Mockito.atLeastOnce
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.reset
import org.mockito.Mockito.times
import org.mockito.Mockito.validateMockitoUsage
import org.mockito.Mockito.verify
import org.robolectric.annotation.Config
import java.time.Instant
import java.util.ArrayDeque
import java.util.PriorityQueue
import kotlin.test.assertFailsWith
private const val INTERACTIVE_UPDATE_RATE_MS = 16L
private const val LEFT_COMPLICATION_ID = 1000
private const val RIGHT_COMPLICATION_ID = 1001
private const val EDGE_COMPLICATION_ID = 1002
private const val BACKGROUND_COMPLICATION_ID = 1111
private const val NO_COMPLICATIONS = "NO_COMPLICATIONS"
private const val LEFT_COMPLICATION = "LEFT_COMPLICATION"
private const val RIGHT_COMPLICATION = "RIGHT_COMPLICATION"
private const val LEFT_AND_RIGHT_COMPLICATIONS = "LEFT_AND_RIGHT_COMPLICATIONS"
private const val RIGHT_AND_LEFT_COMPLICATIONS = "RIGHT_AND_LEFT_COMPLICATIONS"
@Config(manifest = Config.NONE)
@RequiresApi(Build.VERSION_CODES.O)
@RunWith(WatchFaceTestRunner::class)
public class WatchFaceServiceTest {
private val handler = mock<Handler>()
private val iWatchFaceService = mock<IWatchFaceService>()
private val surfaceHolder = mock<SurfaceHolder>()
private val tapListener = mock<WatchFace.TapListener>()
private val choreographer = mock<Choreographer>()
private val watchState = MutableWatchState()
init {
`when`(surfaceHolder.surfaceFrame).thenReturn(ONE_HUNDRED_BY_ONE_HUNDRED_RECT)
`when`(surfaceHolder.lockHardwareCanvas()).thenReturn(Canvas())
}
private companion object {
val ONE_HUNDRED_BY_ONE_HUNDRED_RECT = Rect(0, 0, 100, 100)
}
private val context: Context = ApplicationProvider.getApplicationContext()
private val complicationDrawableLeft = ComplicationDrawable(context)
private val complicationDrawableRight = ComplicationDrawable(context)
private val complicationDrawableEdge = ComplicationDrawable(context)
private val complicationDrawableBackground = ComplicationDrawable(context)
private val redStyleOption =
ListUserStyleSetting.ListOption(Option.Id("red_style"), "Red", icon = null)
private val greenStyleOption =
ListUserStyleSetting.ListOption(Option.Id("green_style"), "Green", icon = null)
private val blueStyleOption =
ListUserStyleSetting.ListOption(Option.Id("blue_style"), "Blue", icon = null)
private val colorStyleList = listOf(redStyleOption, greenStyleOption, blueStyleOption)
private val colorStyleSetting = ListUserStyleSetting(
UserStyleSetting.Id("color_style_setting"),
"Colors",
"Watchface colorization", /* icon = */
null,
colorStyleList,
listOf(WatchFaceLayer.BASE)
)
private val classicStyleOption =
ListUserStyleSetting.ListOption(Option.Id("classic_style"), "Classic", icon = null)
private val modernStyleOption =
ListUserStyleSetting.ListOption(Option.Id("modern_style"), "Modern", icon = null)
private val gothicStyleOption =
ListUserStyleSetting.ListOption(Option.Id("gothic_style"), "Gothic", icon = null)
private val watchHandStyleList =
listOf(classicStyleOption, modernStyleOption, gothicStyleOption)
private val watchHandStyleSetting = ListUserStyleSetting(
UserStyleSetting.Id("hand_style_setting"),
"Hand Style",
"Hand visual look", /* icon = */
null,
watchHandStyleList,
listOf(WatchFaceLayer.COMPLICATIONS_OVERLAY)
)
private val badStyleOption =
ListUserStyleSetting.ListOption(Option.Id("bad_option"), "Bad", icon = null)
private val leftComplication =
ComplicationSlot.createRoundRectComplicationSlotBuilder(
LEFT_COMPLICATION_ID,
{ watchState, listener ->
CanvasComplicationDrawable(
complicationDrawableLeft,
watchState,
listener
)
},
listOf(
ComplicationType.RANGED_VALUE,
ComplicationType.LONG_TEXT,
ComplicationType.SHORT_TEXT,
ComplicationType.MONOCHROMATIC_IMAGE,
ComplicationType.SMALL_IMAGE
),
DefaultComplicationDataSourcePolicy(SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET),
ComplicationSlotBounds(RectF(0.2f, 0.4f, 0.4f, 0.6f))
).setDefaultDataSourceType(ComplicationType.SHORT_TEXT)
.build()
private val rightComplication =
ComplicationSlot.createRoundRectComplicationSlotBuilder(
RIGHT_COMPLICATION_ID,
{ watchState, listener ->
CanvasComplicationDrawable(
complicationDrawableRight,
watchState,
listener
)
},
listOf(
ComplicationType.RANGED_VALUE,
ComplicationType.LONG_TEXT,
ComplicationType.SHORT_TEXT,
ComplicationType.MONOCHROMATIC_IMAGE,
ComplicationType.SMALL_IMAGE
),
DefaultComplicationDataSourcePolicy(SystemDataSources.DATA_SOURCE_DAY_OF_WEEK),
ComplicationSlotBounds(RectF(0.6f, 0.4f, 0.8f, 0.6f))
).setDefaultDataSourceType(ComplicationType.SHORT_TEXT)
.build()
private val edgeComplicationHitTester = mock<ComplicationTapFilter>()
private val edgeComplication =
ComplicationSlot.createEdgeComplicationSlotBuilder(
EDGE_COMPLICATION_ID,
{ watchState, listener ->
CanvasComplicationDrawable(
complicationDrawableEdge,
watchState,
listener
)
},
listOf(
ComplicationType.RANGED_VALUE,
ComplicationType.LONG_TEXT,
ComplicationType.SHORT_TEXT,
ComplicationType.MONOCHROMATIC_IMAGE,
ComplicationType.SMALL_IMAGE
),
DefaultComplicationDataSourcePolicy(SystemDataSources.DATA_SOURCE_DAY_OF_WEEK),
ComplicationSlotBounds(RectF(0.0f, 0.4f, 0.4f, 0.6f)),
edgeComplicationHitTester
).setDefaultDataSourceType(ComplicationType.SHORT_TEXT)
.build()
private val backgroundComplication =
ComplicationSlot.createBackgroundComplicationSlotBuilder(
BACKGROUND_COMPLICATION_ID,
{ watchState, listener ->
CanvasComplicationDrawable(
complicationDrawableBackground,
watchState,
listener
)
},
listOf(
ComplicationType.PHOTO_IMAGE
),
DefaultComplicationDataSourcePolicy()
).setDefaultDataSourceType(ComplicationType.PHOTO_IMAGE)
.build()
private val leftAndRightComplicationsOption = ComplicationSlotsOption(
Option.Id(LEFT_AND_RIGHT_COMPLICATIONS),
"Left and Right",
null,
listOf(
ComplicationSlotOverlay.Builder(LEFT_COMPLICATION_ID)
.setEnabled(true).build(),
ComplicationSlotOverlay.Builder(RIGHT_COMPLICATION_ID)
.setEnabled(true).build()
)
)
private val noComplicationsOption = ComplicationSlotsOption(
Option.Id(NO_COMPLICATIONS),
"None",
null,
listOf(
ComplicationSlotOverlay.Builder(LEFT_COMPLICATION_ID)
.setEnabled(false).build(),
ComplicationSlotOverlay.Builder(RIGHT_COMPLICATION_ID)
.setEnabled(false).build()
)
)
private val leftComplicationsOption = ComplicationSlotsOption(
Option.Id(LEFT_COMPLICATION),
"Left",
null,
listOf(
ComplicationSlotOverlay.Builder(LEFT_COMPLICATION_ID)
.setEnabled(true).build(),
ComplicationSlotOverlay.Builder(RIGHT_COMPLICATION_ID)
.setEnabled(false).build()
)
)
private val rightComplicationsOption = ComplicationSlotsOption(
Option.Id(RIGHT_COMPLICATION),
"Right",
null,
listOf(
ComplicationSlotOverlay.Builder(LEFT_COMPLICATION_ID)
.setEnabled(false).build(),
ComplicationSlotOverlay.Builder(RIGHT_COMPLICATION_ID)
.setEnabled(true).build()
)
)
private val complicationsStyleSetting = ComplicationSlotsUserStyleSetting(
UserStyleSetting.Id("complications_style_setting"),
"AllComplicationSlots",
"Number and position",
icon = null,
complicationConfig = listOf(
leftAndRightComplicationsOption,
noComplicationsOption,
leftComplicationsOption,
rightComplicationsOption
),
affectsWatchFaceLayers = listOf(WatchFaceLayer.COMPLICATIONS)
)
private lateinit var renderer: TestRenderer
private lateinit var complicationSlotsManager: ComplicationSlotsManager
private lateinit var currentUserStyleRepository: CurrentUserStyleRepository
private lateinit var watchFaceImpl: WatchFaceImpl
private lateinit var testWatchFaceService: TestWatchFaceService
private lateinit var engineWrapper: WatchFaceService.EngineWrapper
private lateinit var interactiveWatchFaceInstance: IInteractiveWatchFace
private class Task(val runTimeMillis: Long, val runnable: Runnable) : Comparable<Task> {
override fun compareTo(other: Task) = runTimeMillis.compareTo(other.runTimeMillis)
}
private var looperTimeMillis = 0L
private val pendingTasks = PriorityQueue<Task>()
private fun runPostedTasksFor(durationMillis: Long) {
val stopTime = looperTimeMillis + durationMillis
while (pendingTasks.isNotEmpty() &&
pendingTasks.peek()!!.runTimeMillis <= stopTime
) {
val task = pendingTasks.remove()
testWatchFaceService.mockSystemTimeMillis = task.runTimeMillis
looperTimeMillis = task.runTimeMillis
task.runnable.run()
}
looperTimeMillis = stopTime
testWatchFaceService.mockSystemTimeMillis = stopTime
}
private fun initEngine(
@WatchFaceType watchFaceType: Int,
complicationSlots: List<ComplicationSlot>,
userStyleSchema: UserStyleSchema,
apiVersion: Int = 2,
hasLowBitAmbient: Boolean = false,
hasBurnInProtection: Boolean = false,
tapListener: WatchFace.TapListener? = null,
setInitialComplicationData: Boolean = true
) {
initEngineBeforeGetWatchFaceImpl(
watchFaceType,
complicationSlots,
userStyleSchema,
apiVersion,
hasLowBitAmbient,
hasBurnInProtection,
tapListener,
setInitialComplicationData
)
// [WatchFaceService.createWatchFace] Will have run by now because we're using an immediate
// coroutine dispatcher.
watchFaceImpl = engineWrapper.getWatchFaceImplOrNull()!!
currentUserStyleRepository = watchFaceImpl.currentUserStyleRepository
complicationSlotsManager = watchFaceImpl.complicationSlotsManager
testWatchFaceService.setIsVisible(true)
}
private fun initEngineBeforeGetWatchFaceImpl(
watchFaceType: Int,
complicationSlots: List<ComplicationSlot>,
userStyleSchema: UserStyleSchema,
apiVersion: Int = 2,
hasLowBitAmbient: Boolean = false,
hasBurnInProtection: Boolean = false,
tapListener: WatchFace.TapListener? = null,
setInitialComplicationData: Boolean = true
) {
testWatchFaceService = TestWatchFaceService(
watchFaceType,
complicationSlots,
{ _, currentUserStyleRepository, watchState ->
renderer = TestRenderer(
surfaceHolder,
currentUserStyleRepository,
watchState,
INTERACTIVE_UPDATE_RATE_MS
)
renderer
},
userStyleSchema,
watchState,
handler,
tapListener,
true,
null,
choreographer
)
engineWrapper = testWatchFaceService.onCreateEngine() as WatchFaceService.EngineWrapper
engineWrapper.onCreate(surfaceHolder)
// Set some initial complication data.
if (setInitialComplicationData) {
for (complication in complicationSlots) {
setComplicationViaWallpaperCommand(
complication.id,
when (complication.defaultDataSourceType) {
ComplicationType.SHORT_TEXT ->
ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText("Initial Short"))
.build()
ComplicationType.PHOTO_IMAGE ->
ComplicationData.Builder(ComplicationData.TYPE_LARGE_IMAGE)
.setLargeImage(Icon.createWithContentUri("someuri"))
.build()
else -> throw UnsupportedOperationException()
}
)
}
}
// Trigger watch face creation by setting the binder and the immutable properties.
sendBinder(engineWrapper, apiVersion)
sendImmutableProperties(engineWrapper, hasLowBitAmbient, hasBurnInProtection)
engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
}
private fun initWallpaperInteractiveWatchFaceInstance(
@WatchFaceType watchFaceType: Int,
complicationSlots: List<ComplicationSlot>,
userStyleSchema: UserStyleSchema,
wallpaperInteractiveWatchFaceInstanceParams: WallpaperInteractiveWatchFaceInstanceParams
) {
testWatchFaceService = TestWatchFaceService(
watchFaceType,
complicationSlots,
{ _, currentUserStyleRepository, watchState ->
renderer = TestRenderer(
surfaceHolder,
currentUserStyleRepository,
watchState,
INTERACTIVE_UPDATE_RATE_MS
)
renderer
},
userStyleSchema,
watchState,
handler,
null,
false,
null,
choreographer
)
InteractiveInstanceManager
.getExistingInstanceOrSetPendingWallpaperInteractiveWatchFaceInstance(
InteractiveInstanceManager.PendingWallpaperInteractiveWatchFaceInstance(
wallpaperInteractiveWatchFaceInstanceParams,
object : IPendingInteractiveWatchFace.Stub() {
override fun getApiVersion() =
IPendingInteractiveWatchFace.API_VERSION
override fun onInteractiveWatchFaceCreated(
iInteractiveWatchFace: IInteractiveWatchFace
) {
interactiveWatchFaceInstance = iInteractiveWatchFace
}
override fun onInteractiveWatchFaceCrashed(exception: CrashInfoParcel?) {
fail("WatchFace crashed: $exception")
}
}
)
)
engineWrapper = testWatchFaceService.onCreateEngine() as WatchFaceService.EngineWrapper
engineWrapper.onCreate(surfaceHolder)
engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
// [WatchFaceService.createWatchFace] Will have run by now because we're using an immediate
// coroutine dispatcher.
runBlocking {
watchFaceImpl = engineWrapper.deferredWatchFaceImpl.await()
}
currentUserStyleRepository = watchFaceImpl.currentUserStyleRepository
complicationSlotsManager = watchFaceImpl.complicationSlotsManager
testWatchFaceService.setIsVisible(true)
}
private fun sendBinder(engine: WatchFaceService.EngineWrapper, apiVersion: Int) {
`when`(iWatchFaceService.apiVersion).thenReturn(apiVersion)
engine.onCommand(
Constants.COMMAND_SET_BINDER,
0,
0,
0,
Bundle().apply {
putBinder(
Constants.EXTRA_BINDER,
WatchFaceServiceStub(iWatchFaceService).asBinder()
)
},
false
)
}
private fun sendImmutableProperties(
engine: WatchFaceService.EngineWrapper,
hasLowBitAmbient: Boolean,
hasBurnInProtection: Boolean
) {
engine.wslFlow.onPropertiesChanged(
Bundle().apply {
putBoolean(Constants.PROPERTY_LOW_BIT_AMBIENT, hasLowBitAmbient)
putBoolean(Constants.PROPERTY_BURN_IN_PROTECTION, hasBurnInProtection)
}
)
}
private fun sendRequestStyle() {
engineWrapper.onCommand(Constants.COMMAND_REQUEST_STYLE, 0, 0, 0, null, false)
}
private fun setComplicationViaWallpaperCommand(
complicationSlotId: Int,
complicationData: ComplicationData
) {
engineWrapper.onCommand(
Constants.COMMAND_COMPLICATION_DATA,
0,
0,
0,
Bundle().apply {
putInt(Constants.EXTRA_COMPLICATION_ID, complicationSlotId)
putParcelable(Constants.EXTRA_COMPLICATION_DATA, complicationData)
},
false
)
}
@Before
public fun setUp() {
Assume.assumeTrue("This test suite assumes API 26", Build.VERSION.SDK_INT >= 26)
`when`(handler.getLooper()).thenReturn(Looper.myLooper())
// Capture tasks posted to mHandler and insert in mPendingTasks which is under our control.
doAnswer {
pendingTasks.add(Task(looperTimeMillis, it.arguments[0] as Runnable))
}.`when`(handler).post(any())
doAnswer {
pendingTasks.add(
Task(looperTimeMillis + it.arguments[1] as Long, it.arguments[0] as Runnable)
)
}.`when`(handler).postDelayed(any(), anyLong())
doAnswer {
// Simulate waiting for the next frame.
val nextFrameTimeMillis = looperTimeMillis + (16 - looperTimeMillis % 16)
val callback = it.arguments[0] as Choreographer.FrameCallback
pendingTasks.add(Task(nextFrameTimeMillis, { callback.doFrame(0) }))
}.`when`(choreographer).postFrameCallback(any())
doAnswer {
// Remove task from the priority queue. There's no good way of doing this quickly.
val queue = ArrayDeque<Task>()
while (pendingTasks.isNotEmpty()) {
val task = pendingTasks.remove()
if (task.runnable != it.arguments[0]) {
queue.add(task)
}
}
// Push filtered tasks back on the queue.
while (queue.isNotEmpty()) {
pendingTasks.add(queue.remove())
}
}.`when`(handler).removeCallbacks(any())
}
@After
public fun validate() {
if (this::interactiveWatchFaceInstance.isInitialized) {
interactiveWatchFaceInstance.release()
}
if (this::engineWrapper.isInitialized && !engineWrapper.destroyed) {
engineWrapper.onDestroy()
}
validateMockitoUsage()
}
@Test
public fun maybeUpdateDrawMode_setsCorrectDrawMode() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList())
)
watchState.isAmbient.value = false
assertThat(renderer.renderParameters.drawMode).isEqualTo(DrawMode.INTERACTIVE)
watchState.isBatteryLowAndNotCharging.value = true
watchFaceImpl.maybeUpdateDrawMode()
assertThat(renderer.renderParameters.drawMode).isEqualTo(DrawMode.LOW_BATTERY_INTERACTIVE)
watchState.isBatteryLowAndNotCharging.value = false
watchState.isAmbient.value = true
watchFaceImpl.maybeUpdateDrawMode()
assertThat(renderer.renderParameters.drawMode).isEqualTo(DrawMode.AMBIENT)
watchState.isAmbient.value = false
watchFaceImpl.maybeUpdateDrawMode()
assertThat(renderer.renderParameters.drawMode).isEqualTo(DrawMode.INTERACTIVE)
watchState.interruptionFilter.value = NotificationManager.INTERRUPTION_FILTER_NONE
watchFaceImpl.maybeUpdateDrawMode()
assertThat(renderer.renderParameters.drawMode).isEqualTo(DrawMode.MUTE)
watchState.interruptionFilter.value = NotificationManager.INTERRUPTION_FILTER_ALL
watchFaceImpl.maybeUpdateDrawMode()
assertThat(renderer.renderParameters.drawMode).isEqualTo(DrawMode.INTERACTIVE)
watchState.interruptionFilter.value = NotificationManager.INTERRUPTION_FILTER_PRIORITY
watchFaceImpl.maybeUpdateDrawMode()
assertThat(renderer.renderParameters.drawMode).isEqualTo(DrawMode.MUTE)
watchState.interruptionFilter.value = NotificationManager.INTERRUPTION_FILTER_UNKNOWN
watchFaceImpl.maybeUpdateDrawMode()
assertThat(renderer.renderParameters.drawMode).isEqualTo(DrawMode.INTERACTIVE)
watchState.interruptionFilter.value = NotificationManager.INTERRUPTION_FILTER_ALARMS
watchFaceImpl.maybeUpdateDrawMode()
assertThat(renderer.renderParameters.drawMode).isEqualTo(DrawMode.MUTE)
// Ambient takes precidence over interruption filter.
watchState.isAmbient.value = true
watchFaceImpl.maybeUpdateDrawMode()
assertThat(renderer.renderParameters.drawMode).isEqualTo(DrawMode.AMBIENT)
watchState.isAmbient.value = false
watchState.interruptionFilter.value = 0
watchFaceImpl.maybeUpdateDrawMode()
assertThat(renderer.renderParameters.drawMode).isEqualTo(DrawMode.INTERACTIVE)
}
@Test
public fun onDraw_zonedDateTime_setFromSystemTime() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList())
)
watchState.isAmbient.value = false
testWatchFaceService.mockSystemTimeMillis = 1000L
watchFaceImpl.onDraw()
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(1000L)
}
@Test
public fun onDraw_zonedDateTime_affectedCorrectly_with2xMockTime() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList())
)
watchState.isAmbient.value = false
testWatchFaceService.mockSystemTimeMillis = 1000L
watchFaceImpl.broadcastsReceiver!!.mockTimeReceiver.onReceive(
context,
Intent(WatchFaceImpl.MOCK_TIME_INTENT).apply {
putExtra(WatchFaceImpl.EXTRA_MOCK_TIME_SPEED_MULTIPLIER, 2.0f)
putExtra(WatchFaceImpl.EXTRA_MOCK_TIME_WRAPPING_MIN_TIME, -1L)
}
)
// Time should not diverge initially.
watchFaceImpl.onDraw()
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(1000L)
// However 1000ms of real time should result in 2000ms observed by onDraw.
testWatchFaceService.mockSystemTimeMillis = 2000L
watchFaceImpl.onDraw()
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(3000L)
}
@Test
public fun onDraw_zonedDateTime_affectedCorrectly_withMockTimeWrapping() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList())
)
watchState.isAmbient.value = false
testWatchFaceService.mockSystemTimeMillis = 1000L
watchFaceImpl.broadcastsReceiver!!.mockTimeReceiver.onReceive(
context,
Intent(WatchFaceImpl.MOCK_TIME_INTENT).apply {
putExtra(WatchFaceImpl.EXTRA_MOCK_TIME_SPEED_MULTIPLIER, 2.0f)
putExtra(WatchFaceImpl.EXTRA_MOCK_TIME_WRAPPING_MIN_TIME, 1000L)
putExtra(WatchFaceImpl.EXTRA_MOCK_TIME_WRAPPING_MAX_TIME, 2000L)
}
)
// Time in millis observed by onDraw should wrap betwween 1000 and 2000.
watchFaceImpl.onDraw()
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(1000L)
testWatchFaceService.mockSystemTimeMillis = 1250L
watchFaceImpl.onDraw()
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(1500L)
testWatchFaceService.mockSystemTimeMillis = 1499L
watchFaceImpl.onDraw()
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(1998L)
testWatchFaceService.mockSystemTimeMillis = 1500L
watchFaceImpl.onDraw()
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(1000L)
testWatchFaceService.mockSystemTimeMillis = 1750L
watchFaceImpl.onDraw()
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(1500L)
testWatchFaceService.mockSystemTimeMillis = 1999L
watchFaceImpl.onDraw()
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(1998L)
testWatchFaceService.mockSystemTimeMillis = 2000L
watchFaceImpl.onDraw()
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(1000L)
}
private fun tapAt(x: Int, y: Int) {
// The eventTime is ignored.
watchFaceImpl.onTapCommand(
TapType.DOWN,
TapEvent(x, y, Instant.ofEpochMilli(looperTimeMillis))
)
watchFaceImpl.onTapCommand(
TapType.UP,
TapEvent(x, y, Instant.ofEpochMilli(looperTimeMillis))
)
}
private fun tapCancelAt(x: Int, y: Int) {
watchFaceImpl.onTapCommand(
TapType.DOWN,
TapEvent(x, y, Instant.ofEpochMilli(looperTimeMillis))
)
watchFaceImpl.onTapCommand(
TapType.CANCEL,
TapEvent(x, y, Instant.ofEpochMilli(looperTimeMillis))
)
}
@Test
public fun singleTaps_correctlyDetected() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList())
)
assertThat(complicationSlotsManager.lastComplicationTapDownEvents[LEFT_COMPLICATION_ID])
.isNull()
assertThat(complicationSlotsManager.lastComplicationTapDownEvents[RIGHT_COMPLICATION_ID])
.isNull()
// Tap left complication.
tapAt(30, 50)
assertThat(complicationSlotsManager.lastComplicationTapDownEvents[LEFT_COMPLICATION_ID])
.isEqualTo(TapEvent(30, 50, Instant.EPOCH))
assertThat(complicationSlotsManager.lastComplicationTapDownEvents[RIGHT_COMPLICATION_ID])
.isNull()
assertThat(testWatchFaceService.tappedComplicationSlotIds)
.isEqualTo(listOf(LEFT_COMPLICATION_ID))
runPostedTasksFor(100)
// Tap right complication.
testWatchFaceService.reset()
tapAt(70, 50)
assertThat(complicationSlotsManager.lastComplicationTapDownEvents[LEFT_COMPLICATION_ID])
.isEqualTo(TapEvent(30, 50, Instant.EPOCH))
assertThat(complicationSlotsManager.lastComplicationTapDownEvents[RIGHT_COMPLICATION_ID])
.isEqualTo(TapEvent(70, 50, Instant.ofEpochMilli(100)))
assertThat(testWatchFaceService.tappedComplicationSlotIds)
.isEqualTo(listOf(RIGHT_COMPLICATION_ID))
runPostedTasksFor(100)
// Tap on blank space.
testWatchFaceService.reset()
tapAt(1, 1)
// No change in lastComplicationTapDownEvents
assertThat(complicationSlotsManager.lastComplicationTapDownEvents[LEFT_COMPLICATION_ID])
.isEqualTo(TapEvent(30, 50, Instant.EPOCH))
assertThat(complicationSlotsManager.lastComplicationTapDownEvents[RIGHT_COMPLICATION_ID])
.isEqualTo(TapEvent(70, 50, Instant.ofEpochMilli(100)))
assertThat(testWatchFaceService.tappedComplicationSlotIds).isEmpty()
}
@Test
public fun singleTaps_onDifferentComplications() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList())
)
assertThat(complicationSlotsManager.lastComplicationTapDownEvents[LEFT_COMPLICATION_ID])
.isNull()
assertThat(complicationSlotsManager.lastComplicationTapDownEvents[RIGHT_COMPLICATION_ID])
.isNull()
// Rapidly tap left then right complication.
tapAt(30, 50)
tapAt(70, 50)
// Taps are registered on both complicationSlots.
assertThat(complicationSlotsManager.lastComplicationTapDownEvents[LEFT_COMPLICATION_ID])
.isEqualTo(TapEvent(30, 50, Instant.EPOCH))
assertThat(complicationSlotsManager.lastComplicationTapDownEvents[RIGHT_COMPLICATION_ID])
.isEqualTo(TapEvent(70, 50, Instant.EPOCH))
assertThat(testWatchFaceService.tappedComplicationSlotIds)
.isEqualTo(listOf(LEFT_COMPLICATION_ID, RIGHT_COMPLICATION_ID))
}
@Test
public fun tapCancel_after_tapDown_CancelsTap() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList())
)
testWatchFaceService.reset()
// Tap/Cancel left complication
tapCancelAt(30, 50)
assertThat(testWatchFaceService.tappedComplicationSlotIds).isEmpty()
}
@Test
public fun edgeComplication_tap() {
initEngine(
WatchFaceType.ANALOG,
listOf(edgeComplication),
UserStyleSchema(emptyList()),
tapListener = tapListener
)
assertThat(complicationSlotsManager.lastComplicationTapDownEvents[EDGE_COMPLICATION_ID])
.isNull()
`when`(
edgeComplicationHitTester.hitTest(
edgeComplication,
ONE_HUNDRED_BY_ONE_HUNDRED_RECT,
0,
50
)
).thenReturn(true)
// Tap the edge complication.
tapAt(0, 50)
assertThat(complicationSlotsManager.lastComplicationTapDownEvents[EDGE_COMPLICATION_ID])
.isEqualTo(TapEvent(0, 50, Instant.EPOCH))
assertThat(testWatchFaceService.tappedComplicationSlotIds)
.isEqualTo(listOf(EDGE_COMPLICATION_ID))
}
@Test
public fun tapListener_tap() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList()),
tapListener = tapListener
)
// Tap on nothing.
tapAt(1, 1)
verify(tapListener).onTapEvent(
TapType.DOWN,
TapEvent(1, 1, Instant.ofEpochMilli(looperTimeMillis)),
null
)
verify(tapListener).onTapEvent(
TapType.UP,
TapEvent(1, 1, Instant.ofEpochMilli(looperTimeMillis)),
null
)
}
@Test
public fun tapListener_tap_viaWallpaperCommand() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList()),
tapListener = tapListener
)
// Tap on nothing.
engineWrapper.onCommand(
Constants.COMMAND_TOUCH,
10,
20,
0,
Bundle().apply {
putBinder(
Constants.EXTRA_BINDER,
WatchFaceServiceStub(iWatchFaceService).asBinder()
)
},
false
)
engineWrapper.onCommand(
Constants.COMMAND_TAP,
10,
20,
0,
Bundle().apply {
putBinder(
Constants.EXTRA_BINDER,
WatchFaceServiceStub(iWatchFaceService).asBinder()
)
},
false
)
verify(tapListener).onTapEvent(
TapType.DOWN,
TapEvent(10, 20, Instant.ofEpochMilli(looperTimeMillis)),
null
)
verify(tapListener).onTapEvent(
TapType.UP,
TapEvent(10, 20, Instant.ofEpochMilli(looperTimeMillis)),
null
)
}
@Test
public fun tapListener_tapComplication() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList()),
tapListener = tapListener
)
// Tap right complication.
tapAt(70, 50)
verify(tapListener).onTapEvent(
TapType.DOWN,
TapEvent(70, 50, Instant.ofEpochMilli(looperTimeMillis)),
rightComplication
)
verify(tapListener).onTapEvent(
TapType.UP,
TapEvent(70, 50, Instant.ofEpochMilli(looperTimeMillis)),
rightComplication
)
}
@Test
public fun interactiveFrameRate_reducedWhenBatteryLow() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList())
)
assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0)).isEqualTo(
INTERACTIVE_UPDATE_RATE_MS
)
// The delay should change when battery is low.
watchFaceImpl.broadcastsReceiver!!.actionBatteryLowReceiver.onReceive(
context,
Intent(Intent.ACTION_BATTERY_LOW)
)
assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0)).isEqualTo(
WatchFaceImpl.MAX_LOW_POWER_INTERACTIVE_UPDATE_RATE_MS
)
// And go back to normal when battery is OK.
watchFaceImpl.broadcastsReceiver!!.actionBatteryOkayReceiver.onReceive(
context,
Intent(Intent.ACTION_BATTERY_OKAY)
)
assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0)).isEqualTo(
INTERACTIVE_UPDATE_RATE_MS
)
}
@Test
public fun interactiveFrameRate_restoreWhenPowerConnectedAfterBatteryLow() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList())
)
assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0)).isEqualTo(
INTERACTIVE_UPDATE_RATE_MS
)
// The delay should change when battery is low.
watchFaceImpl.broadcastsReceiver!!.actionBatteryLowReceiver.onReceive(
context,
Intent(Intent.ACTION_BATTERY_LOW)
)
assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0)).isEqualTo(
WatchFaceImpl.MAX_LOW_POWER_INTERACTIVE_UPDATE_RATE_MS
)
// And go back to normal when power is connected.
watchFaceImpl.broadcastsReceiver!!.actionPowerConnectedReceiver.onReceive(
context,
Intent(Intent.ACTION_POWER_CONNECTED)
)
assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0)).isEqualTo(
INTERACTIVE_UPDATE_RATE_MS
)
}
@Test
public fun computeDelayTillNextFrame_accountsForSlowDraw() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList())
)
assertThat(
watchFaceImpl.computeDelayTillNextFrame(
startTimeMillis = 0,
currentTimeMillis = 2
)
)
.isEqualTo(INTERACTIVE_UPDATE_RATE_MS - 2)
}
@Test
public fun computeDelayTillNextFrame_verySlowDraw() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList())
)
// If the frame is very slow we'll want to post a choreographer frame immediately.
assertThat(
watchFaceImpl.computeDelayTillNextFrame(
startTimeMillis = 2,
currentTimeMillis = INTERACTIVE_UPDATE_RATE_MS + 3
)
).isEqualTo(- 1)
}
@Test
public fun computeDelayTillNextFrame_beginFrameTimeInTheFuture() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList())
)
watchFaceImpl.nextDrawTimeMillis = 1000
// Simulate time going backwards between renders.
assertThat(
watchFaceImpl.computeDelayTillNextFrame(
startTimeMillis = 20,
currentTimeMillis = 24
)
).isEqualTo(INTERACTIVE_UPDATE_RATE_MS - 4)
}
@Test
public fun computeDelayTillNextFrame_1000ms_update_atTopOfSecond() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList())
)
renderer.interactiveDrawModeUpdateDelayMillis = 1000
// Simulate rendering 0.74s into a second, after which we expect a short delay.
watchFaceImpl.nextDrawTimeMillis = 100740
assertThat(
watchFaceImpl.computeDelayTillNextFrame(
startTimeMillis = 100740,
currentTimeMillis = 100750
)
).isEqualTo(250)
}
@Test
public fun getComplicationSlotIdAt_returnsCorrectComplications() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList())
)
assertThat(complicationSlotsManager.getComplicationSlotAt(30, 50)!!.id)
.isEqualTo(LEFT_COMPLICATION_ID)
leftComplication.enabled = false
assertThat(complicationSlotsManager.getComplicationSlotAt(30, 50)).isNull()
assertThat(complicationSlotsManager.getComplicationSlotAt(70, 50)!!.id)
.isEqualTo(RIGHT_COMPLICATION_ID)
assertThat(complicationSlotsManager.getComplicationSlotAt(1, 1)).isNull()
}
@Test
public fun getBackgroundComplicationSlotId_returnsNull() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList())
)
// Flush pending tasks posted as a result of initEngine.
runPostedTasksFor(0)
assertThat(complicationSlotsManager.getBackgroundComplicationSlot()).isNull()
engineWrapper.onDestroy()
}
@Test
public fun getBackgroundComplicationSlotId_returnsCorrectId() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, backgroundComplication),
UserStyleSchema(emptyList())
)
assertThat(complicationSlotsManager.getBackgroundComplicationSlot()!!.id).isEqualTo(
BACKGROUND_COMPLICATION_ID
)
}
@Test
public fun getStoredUserStyleNotSupported_userStyle_isPersisted() {
// The style should get persisted in a file because this test is set up using the legacy
// Wear 2.0 APIs.
initEngine(
WatchFaceType.ANALOG,
emptyList(),
UserStyleSchema(listOf(colorStyleSetting, watchHandStyleSetting)),
2
)
// This should get persisted.
currentUserStyleRepository.updateUserStyle(
UserStyle(
hashMapOf(
colorStyleSetting to blueStyleOption,
watchHandStyleSetting to gothicStyleOption
)
)
)
engineWrapper.onDestroy()
// Flush pending tasks posted as a result of initEngine.
runPostedTasksFor(0)
val service2 = TestWatchFaceService(
WatchFaceType.ANALOG,
emptyList(),
{ _, currentUserStyleRepository, watchState ->
TestRenderer(
surfaceHolder,
currentUserStyleRepository,
watchState,
INTERACTIVE_UPDATE_RATE_MS
)
},
UserStyleSchema(listOf(colorStyleSetting, watchHandStyleSetting)),
watchState,
handler,
null,
true,
null,
choreographer
)
// Trigger watch face creation.
val engine2 = service2.onCreateEngine() as WatchFaceService.EngineWrapper
sendBinder(engine2, apiVersion = 2)
sendImmutableProperties(engine2, false, false)
engine2.onSurfaceChanged(surfaceHolder, 0, 100, 100)
val watchFaceImpl2 = engine2.getWatchFaceImplOrNull()!!
val userStyleRepository2 = watchFaceImpl2.currentUserStyleRepository
assertThat(userStyleRepository2.userStyle.value[colorStyleSetting]!!.id)
.isEqualTo(
blueStyleOption.id
)
assertThat(userStyleRepository2.userStyle.value[watchHandStyleSetting]!!.id)
.isEqualTo(
gothicStyleOption.id
)
}
@SdkSuppress(maxSdkVersion = 29)
@Test
public fun onApplyWindowInsetsBeforeR_setsChinHeight() {
initEngine(
WatchFaceType.ANALOG,
emptyList(),
UserStyleSchema(emptyList())
)
// Initially the chin size is set to zero.
assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(0)
// When window insets are delivered to the watch face.
engineWrapper.onApplyWindowInsets(getChinWindowInsetsApi25(chinHeight = 12))
// Then the chin size is updated.
assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(12)
}
@SdkSuppress(minSdkVersion = 30)
@Test
public fun onApplyWindowInsetsRAndAbove_setsChinHeight() {
initEngine(
WatchFaceType.ANALOG,
emptyList(),
UserStyleSchema(emptyList())
)
// Initially the chin size is set to zero.
assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(0)
// When window insets are delivered to the watch face.
engineWrapper.onApplyWindowInsets(getChinWindowInsetsApi30(chinHeight = 12))
// Then the chin size is updated.
assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(12)
}
@Test
public fun onApplyWindowInsetsBeforeR_multipleCallsIgnored() {
initEngine(
WatchFaceType.ANALOG,
emptyList(),
UserStyleSchema(emptyList())
)
// Initially the chin size is set to zero.
assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(0)
// When window insets are delivered to the watch face.
engineWrapper.onApplyWindowInsets(getChinWindowInsetsApi25(chinHeight = 12))
// Then the chin size is updated.
assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(12)
// When the same window insets are delivered to the watch face again.
engineWrapper.onApplyWindowInsets(getChinWindowInsetsApi25(chinHeight = 12))
// Nothing happens.
assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(12)
// When different window insets are delivered to the watch face again.
engineWrapper.onApplyWindowInsets(getChinWindowInsetsApi25(chinHeight = 24))
// Nothing happens and the size is unchanged.
assertThat(engineWrapper.mutableWatchState.chinHeight).isEqualTo(12)
}
@Test
public fun initWallpaperInteractiveWatchFaceInstanceWithUserStyle() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
emptyList(),
UserStyleSchema(listOf(colorStyleSetting, watchHandStyleSetting)),
WallpaperInteractiveWatchFaceInstanceParams(
"interactiveInstanceId",
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(
hashMapOf(
colorStyleSetting to blueStyleOption,
watchHandStyleSetting to gothicStyleOption
)
).toWireFormat(),
null
)
)
// The style option above should get applied during watch face creation.
assertThat(currentUserStyleRepository.userStyle.value[colorStyleSetting]!!.id)
.isEqualTo(
blueStyleOption.id
)
assertThat(currentUserStyleRepository.userStyle.value[watchHandStyleSetting]!!.id)
.isEqualTo(
gothicStyleOption.id
)
}
@Test
public fun initWallpaperInteractiveWatchFaceInstanceWithUserStyleThatDoesntMatchSchema() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
emptyList(),
UserStyleSchema(listOf(colorStyleSetting, watchHandStyleSetting)),
WallpaperInteractiveWatchFaceInstanceParams(
"interactiveInstanceId",
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(mapOf(watchHandStyleSetting to badStyleOption)).toWireFormat(),
null
)
)
assertThat(currentUserStyleRepository.userStyle.value[watchHandStyleSetting])
.isEqualTo(watchHandStyleList.first())
}
@Test
public fun wear2ImmutablePropertiesSetCorrectly() {
initEngine(
WatchFaceType.ANALOG,
emptyList(),
UserStyleSchema(emptyList()),
2,
true,
false
)
assertTrue(watchState.hasLowBitAmbient)
assertFalse(watchState.hasBurnInProtection)
}
@Test
public fun wear2ImmutablePropertiesSetCorrectly2() {
initEngine(
WatchFaceType.ANALOG,
emptyList(),
UserStyleSchema(emptyList()),
2,
false,
true
)
assertFalse(watchState.hasLowBitAmbient)
assertTrue(watchState.hasBurnInProtection)
}
@Test
public fun wallpaperInteractiveWatchFaceImmutablePropertiesSetCorrectly() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
emptyList(),
UserStyleSchema(listOf(colorStyleSetting, watchHandStyleSetting)),
WallpaperInteractiveWatchFaceInstanceParams(
"interactiveInstanceId",
DeviceConfig(
true,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(mapOf(watchHandStyleSetting to badStyleOption)).toWireFormat(),
null
)
)
assertTrue(watchState.hasLowBitAmbient)
assertFalse(watchState.hasBurnInProtection)
}
@Test
public fun onCreate_calls_setActiveComplications_withCorrectIDs() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication, backgroundComplication),
UserStyleSchema(emptyList())
)
verify(iWatchFaceService).setActiveComplications(
intArrayOf(LEFT_COMPLICATION_ID, RIGHT_COMPLICATION_ID, BACKGROUND_COMPLICATION_ID),
true
)
}
@Test
public fun onCreate_calls_setContentDescriptionLabels_withCorrectArgs() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication, backgroundComplication),
UserStyleSchema(emptyList())
)
runPostedTasksFor(0)
// setContentDescriptionLabels gets called twice in the legacy WSL flow, once initially and
// once in response to the complication data wallpaper commands.
val arguments = ArgumentCaptor.forClass(Array<ContentDescriptionLabel>::class.java)
verify(iWatchFaceService, times(2)).setContentDescriptionLabels(arguments.capture())
val argument = arguments.allValues[1]
assertThat(argument.size).isEqualTo(3)
assertThat(argument[0].bounds).isEqualTo(Rect(25, 25, 75, 75)) // Clock element.
assertThat(argument[1].bounds).isEqualTo(Rect(20, 40, 40, 60)) // Left complication.
assertThat(argument[2].bounds).isEqualTo(Rect(60, 40, 80, 60)) // Right complication.
}
@Test
public fun moveComplications() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList()),
4
)
leftComplication.complicationSlotBounds =
ComplicationSlotBounds(RectF(0.3f, 0.3f, 0.5f, 0.5f))
rightComplication.complicationSlotBounds =
ComplicationSlotBounds(RectF(0.7f, 0.75f, 0.9f, 0.95f))
val complicationDetails = watchFaceImpl.getComplicationState()
assertThat(complicationDetails[0].id).isEqualTo(LEFT_COMPLICATION_ID)
assertThat(complicationDetails[0].complicationState.boundsType).isEqualTo(
ComplicationSlotBoundsType.ROUND_RECT
)
assertThat(complicationDetails[0].complicationState.bounds).isEqualTo(
Rect(30, 30, 50, 50)
)
assertThat(complicationDetails[1].id).isEqualTo(RIGHT_COMPLICATION_ID)
assertThat(complicationDetails[1].complicationState.boundsType).isEqualTo(
ComplicationSlotBoundsType.ROUND_RECT
)
assertThat(complicationDetails[1].complicationState.bounds).isEqualTo(
Rect(70, 75, 90, 95)
)
// Despite disabling the background complication we should still get a
// ContentDescriptionLabel for the main clock element.
engineWrapper.updateContentDescriptionLabels()
val contentDescriptionLabels = engineWrapper.contentDescriptionLabels
assertThat(contentDescriptionLabels.size).isEqualTo(3)
assertThat(contentDescriptionLabels[0].bounds).isEqualTo(
Rect(
25,
25,
75,
75
)
) // Clock element.
assertThat(contentDescriptionLabels[1].bounds).isEqualTo(
Rect(
30,
30,
50,
50
)
) // Left complication.
assertThat(contentDescriptionLabels[2].bounds).isEqualTo(
Rect(
70,
75,
90,
95
)
) // Right complication.
}
@Test
public fun styleChangesAccessibilityTraversalIndex() {
val rightAndSelectComplicationsOption = ComplicationSlotsOption(
Option.Id(RIGHT_AND_LEFT_COMPLICATIONS),
"Right and Left",
null,
listOf(
ComplicationSlotOverlay.Builder(LEFT_COMPLICATION_ID)
.setEnabled(true).setAccessibilityTraversalIndex(RIGHT_COMPLICATION_ID).build(),
ComplicationSlotOverlay.Builder(RIGHT_COMPLICATION_ID)
.setEnabled(true).setAccessibilityTraversalIndex(LEFT_COMPLICATION_ID).build()
)
)
val complicationsStyleSetting = ComplicationSlotsUserStyleSetting(
UserStyleSetting.Id("complications_style_setting"),
"AllComplicationSlots",
"Number and position",
icon = null,
complicationConfig = listOf(
leftAndRightComplicationsOption,
rightAndSelectComplicationsOption
),
affectsWatchFaceLayers = listOf(WatchFaceLayer.COMPLICATIONS)
)
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(listOf(complicationsStyleSetting)),
4
)
// Despite disabling the background complication we should still get a
// ContentDescriptionLabel for the main clock element.
engineWrapper.updateContentDescriptionLabels()
val contentDescriptionLabels = engineWrapper.contentDescriptionLabels
assertThat(contentDescriptionLabels.size).isEqualTo(3)
assertThat(contentDescriptionLabels[0].bounds).isEqualTo(
Rect(
25,
25,
75,
75
)
) // Clock element.
assertThat(contentDescriptionLabels[1].bounds).isEqualTo(
Rect(
20,
40,
40,
60
)
) // Left complication.
assertThat(contentDescriptionLabels[2].bounds).isEqualTo(
Rect(
60,
40,
80,
60
)
) // Right complication.
// Change the style
watchFaceImpl.currentUserStyleRepository.updateUserStyle(
watchFaceImpl.currentUserStyleRepository.userStyle.value.toMutableUserStyle().apply {
this[complicationsStyleSetting] = rightAndSelectComplicationsOption
}.toUserStyle()
)
runPostedTasksFor(0)
val contentDescriptionLabels2 = engineWrapper.contentDescriptionLabels
assertThat(contentDescriptionLabels2.size).isEqualTo(3)
assertThat(contentDescriptionLabels2[0].bounds).isEqualTo(
Rect(
25,
25,
75,
75
)
) // Clock element.
assertThat(contentDescriptionLabels2[1].bounds).isEqualTo(
Rect(
60,
40,
80,
60
)
) // Right complication.
assertThat(contentDescriptionLabels2[2].bounds).isEqualTo(
Rect(
20,
40,
40,
60
)
) // Left complication.
}
@Test
public fun getOptionForIdentifier_ListViewStyleSetting() {
// Check the correct Options are returned for known option names.
assertThat(colorStyleSetting.getOptionForId(redStyleOption.id)).isEqualTo(redStyleOption)
assertThat(colorStyleSetting.getOptionForId(greenStyleOption.id)).isEqualTo(
greenStyleOption
)
assertThat(colorStyleSetting.getOptionForId(blueStyleOption.id)).isEqualTo(blueStyleOption)
// For unknown option names the first element in the list should be returned.
assertThat(colorStyleSetting.getOptionForId(Option.Id("unknown".encodeToByteArray())))
.isEqualTo(colorStyleList.first())
}
@Test
public fun centerX_and_centerY_containUpToDateValues() {
initEngine(WatchFaceType.ANALOG, emptyList(), UserStyleSchema(emptyList()))
assertThat(renderer.centerX).isEqualTo(50f)
assertThat(renderer.centerY).isEqualTo(50f)
val argument = ArgumentCaptor.forClass(SurfaceHolder.Callback::class.java)
verify(surfaceHolder, atLeastOnce()).addCallback(argument.capture())
// Trigger an update to a larger surface which should move the center.
reset(surfaceHolder)
`when`(surfaceHolder.surfaceFrame).thenReturn(Rect(0, 0, 200, 300))
for (value in argument.allValues) {
value.surfaceChanged(surfaceHolder, 0, 200, 300)
}
assertThat(renderer.centerX).isEqualTo(100f)
assertThat(renderer.centerY).isEqualTo(150f)
}
@Test
public fun requestStyleBeforeSetBinder() {
testWatchFaceService = TestWatchFaceService(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication, backgroundComplication),
{ _, currentUserStyleRepository, watchState ->
TestRenderer(
surfaceHolder,
currentUserStyleRepository,
watchState,
INTERACTIVE_UPDATE_RATE_MS
)
},
UserStyleSchema(emptyList()),
watchState,
handler,
null,
true,
null,
choreographer
)
engineWrapper = testWatchFaceService.onCreateEngine() as WatchFaceService.EngineWrapper
engineWrapper.onCreate(surfaceHolder)
`when`(surfaceHolder.surfaceFrame).thenReturn(ONE_HUNDRED_BY_ONE_HUNDRED_RECT)
sendRequestStyle()
// Trigger watch face creation.
sendBinder(engineWrapper, apiVersion = 2)
sendImmutableProperties(engineWrapper, false, false)
engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
watchFaceImpl = engineWrapper.getWatchFaceImplOrNull()!!
val argument = ArgumentCaptor.forClass(WatchFaceStyle::class.java)
verify(iWatchFaceService).setStyle(argument.capture())
assertThat(argument.value.acceptsTapEvents).isEqualTo(true)
}
@Test
public fun defaultComplicationDataSourcesWithFallbacks_newApi() {
val dataSource1 = ComponentName("com.app1", "com.app1.App1")
val dataSource2 = ComponentName("com.app2", "com.app2.App2")
val complication = ComplicationSlot.createRoundRectComplicationSlotBuilder(
LEFT_COMPLICATION_ID,
{ watchState, listener ->
CanvasComplicationDrawable(complicationDrawableLeft, watchState, listener)
},
emptyList(),
DefaultComplicationDataSourcePolicy(
dataSource1,
dataSource2,
SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET
),
ComplicationSlotBounds(RectF(0.2f, 0.4f, 0.4f, 0.6f))
).setDefaultDataSourceType(ComplicationType.SHORT_TEXT)
.build()
initEngine(WatchFaceType.ANALOG, listOf(complication), UserStyleSchema(emptyList()))
runPostedTasksFor(0)
verify(iWatchFaceService).setDefaultComplicationProviderWithFallbacks(
LEFT_COMPLICATION_ID,
listOf(dataSource1, dataSource2),
SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET,
ComplicationData.TYPE_SHORT_TEXT
)
}
@Test
public fun defaultComplicationDataSourcesWithFallbacks_oldApi() {
val dataSource1 = ComponentName("com.app1", "com.app1.App1")
val dataSource2 = ComponentName("com.app2", "com.app2.App2")
val complication = ComplicationSlot.createRoundRectComplicationSlotBuilder(
LEFT_COMPLICATION_ID,
{ watchState, listener ->
CanvasComplicationDrawable(complicationDrawableLeft, watchState, listener)
},
emptyList(),
DefaultComplicationDataSourcePolicy(
dataSource1,
dataSource2,
SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET
),
ComplicationSlotBounds(RectF(0.2f, 0.4f, 0.4f, 0.6f))
).setDefaultDataSourceType(ComplicationType.SHORT_TEXT)
.build()
initEngine(
WatchFaceType.ANALOG,
listOf(complication),
UserStyleSchema(emptyList()),
apiVersion = 0
)
runPostedTasksFor(0)
verify(iWatchFaceService).setDefaultComplicationProvider(
LEFT_COMPLICATION_ID, dataSource2, ComplicationData.TYPE_SHORT_TEXT
)
verify(iWatchFaceService).setDefaultComplicationProvider(
LEFT_COMPLICATION_ID, dataSource1, ComplicationData.TYPE_SHORT_TEXT
)
verify(iWatchFaceService).setDefaultSystemComplicationProvider(
LEFT_COMPLICATION_ID,
SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET,
ComplicationData.TYPE_SHORT_TEXT
)
}
@Test
public fun previewReferenceTimeMillisAnalog() {
val instanceParams = WallpaperInteractiveWatchFaceInstanceParams(
"interactiveInstanceId",
DeviceConfig(
false,
false,
1000,
2000,
),
WatchUiState(false, 0),
UserStyle(
hashMapOf(
colorStyleSetting to blueStyleOption,
watchHandStyleSetting to gothicStyleOption
)
).toWireFormat(),
null
)
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
emptyList(),
UserStyleSchema(listOf(colorStyleSetting, watchHandStyleSetting)),
instanceParams
)
assertThat(watchFaceImpl.previewReferenceInstant.toEpochMilli()).isEqualTo(1000)
}
@Test
public fun previewReferenceTimeMillisDigital() {
val instanceParams = WallpaperInteractiveWatchFaceInstanceParams(
"interactiveInstanceId",
DeviceConfig(
false,
false,
1000,
2000,
),
WatchUiState(false, 0),
UserStyle(
hashMapOf(
colorStyleSetting to blueStyleOption,
watchHandStyleSetting to gothicStyleOption
)
).toWireFormat(),
null
)
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.DIGITAL,
emptyList(),
UserStyleSchema(listOf(colorStyleSetting, watchHandStyleSetting)),
instanceParams
)
assertThat(watchFaceImpl.previewReferenceInstant.toEpochMilli()).isEqualTo(2000)
}
@Test
public fun getComplicationDetails() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication, backgroundComplication),
UserStyleSchema(emptyList()),
apiVersion = 4
)
val complicationDetails = watchFaceImpl.getComplicationState()
assertThat(complicationDetails[0].id).isEqualTo(LEFT_COMPLICATION_ID)
assertThat(complicationDetails[0].complicationState.boundsType).isEqualTo(
ComplicationSlotBoundsType.ROUND_RECT
)
assertThat(complicationDetails[0].complicationState.bounds).isEqualTo(
Rect(20, 40, 40, 60)
)
assertThat(complicationDetails[0].complicationState.supportedTypes).isEqualTo(
intArrayOf(
ComplicationData.TYPE_RANGED_VALUE,
ComplicationData.TYPE_LONG_TEXT,
ComplicationData.TYPE_SHORT_TEXT,
ComplicationData.TYPE_ICON,
ComplicationData.TYPE_SMALL_IMAGE
)
)
assertThat(complicationDetails[1].id).isEqualTo(RIGHT_COMPLICATION_ID)
assertThat(complicationDetails[1].complicationState.boundsType).isEqualTo(
ComplicationSlotBoundsType.ROUND_RECT
)
assertThat(complicationDetails[1].complicationState.bounds).isEqualTo(
Rect(60, 40, 80, 60)
)
assertThat(complicationDetails[1].complicationState.supportedTypes).isEqualTo(
intArrayOf(
ComplicationData.TYPE_RANGED_VALUE,
ComplicationData.TYPE_LONG_TEXT,
ComplicationData.TYPE_SHORT_TEXT,
ComplicationData.TYPE_ICON,
ComplicationData.TYPE_SMALL_IMAGE
)
)
assertThat(complicationDetails[2].id).isEqualTo(BACKGROUND_COMPLICATION_ID)
assertThat(complicationDetails[2].complicationState.boundsType).isEqualTo(
ComplicationSlotBoundsType.BACKGROUND
)
assertThat(complicationDetails[2].complicationState.bounds).isEqualTo(
Rect(0, 0, 100, 100)
)
assertThat(complicationDetails[2].complicationState.supportedTypes).isEqualTo(
intArrayOf(ComplicationData.TYPE_LARGE_IMAGE)
)
}
@Test
public fun shouldAnimateOverrideControlsEnteringAmbientMode() {
lateinit var testRenderer: TestRendererWithShouldAnimate
testWatchFaceService = TestWatchFaceService(
WatchFaceType.ANALOG,
emptyList(),
{ _, currentUserStyleRepository, watchState ->
testRenderer = TestRendererWithShouldAnimate(
surfaceHolder,
currentUserStyleRepository,
watchState,
INTERACTIVE_UPDATE_RATE_MS
)
testRenderer
},
UserStyleSchema(emptyList()),
watchState,
handler,
null,
true,
null,
choreographer
)
engineWrapper = testWatchFaceService.onCreateEngine() as WatchFaceService.EngineWrapper
engineWrapper.onCreate(surfaceHolder)
`when`(surfaceHolder.surfaceFrame).thenReturn(ONE_HUNDRED_BY_ONE_HUNDRED_RECT)
// Trigger watch face creation.
sendBinder(engineWrapper, apiVersion = 2)
sendImmutableProperties(engineWrapper, false, false)
engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
watchFaceImpl = engineWrapper.getWatchFaceImplOrNull()!!
// Enter ambient mode.
watchState.isAmbient.value = true
watchFaceImpl.maybeUpdateDrawMode()
assertThat(testRenderer.renderParameters.drawMode).isEqualTo(DrawMode.INTERACTIVE)
// Simulate enter ambient animation finishing.
testRenderer.animate = false
watchFaceImpl.maybeUpdateDrawMode()
assertThat(testRenderer.renderParameters.drawMode).isEqualTo(DrawMode.AMBIENT)
}
@Test
public fun complicationsUserStyleSettingSelectionAppliesChanges() {
initEngine(
WatchFaceType.DIGITAL,
listOf(leftComplication, rightComplication),
UserStyleSchema(listOf(complicationsStyleSetting)),
apiVersion = 4
)
reset(iWatchFaceService)
// Select a new style which turns off both complicationSlots.
val mutableUserStyleA = currentUserStyleRepository.userStyle.value.toMutableUserStyle()
mutableUserStyleA[complicationsStyleSetting] = noComplicationsOption
currentUserStyleRepository.updateUserStyle(mutableUserStyleA.toUserStyle())
assertFalse(leftComplication.enabled)
assertFalse(rightComplication.enabled)
verify(iWatchFaceService).setActiveComplications(intArrayOf(), false)
val argumentA = ArgumentCaptor.forClass(Array<ContentDescriptionLabel>::class.java)
verify(iWatchFaceService).setContentDescriptionLabels(argumentA.capture())
assertThat(argumentA.value.size).isEqualTo(1)
assertThat(argumentA.value[0].bounds).isEqualTo(Rect(25, 25, 75, 75)) // Clock element.
reset(iWatchFaceService)
// Select a new style which turns on only the left complication.
val mutableUserStyleB = currentUserStyleRepository.userStyle.value.toMutableUserStyle()
mutableUserStyleB[complicationsStyleSetting] = leftComplicationsOption
currentUserStyleRepository.updateUserStyle(mutableUserStyleB.toUserStyle())
assertTrue(leftComplication.enabled)
assertFalse(rightComplication.enabled)
verify(iWatchFaceService).setActiveComplications(intArrayOf(LEFT_COMPLICATION_ID), false)
val argumentB = ArgumentCaptor.forClass(Array<ContentDescriptionLabel>::class.java)
verify(iWatchFaceService).setContentDescriptionLabels(argumentB.capture())
assertThat(argumentB.value.size).isEqualTo(2)
assertThat(argumentB.value[0].bounds).isEqualTo(Rect(25, 25, 75, 75)) // Clock element.
assertThat(argumentB.value[1].bounds).isEqualTo(Rect(20, 40, 40, 60)) // Left complication.
}
@Test
public fun partialComplicationOverrides() {
val bothComplicationsOption = ComplicationSlotsOption(
Option.Id(LEFT_AND_RIGHT_COMPLICATIONS),
"Left And Right",
null,
// An empty list means use the initial config.
emptyList()
)
val leftOnlyComplicationsOption = ComplicationSlotsOption(
Option.Id(LEFT_COMPLICATION),
"Left",
null,
listOf(ComplicationSlotOverlay.Builder(RIGHT_COMPLICATION_ID).setEnabled(false).build())
)
val rightOnlyComplicationsOption = ComplicationSlotsOption(
Option.Id(RIGHT_COMPLICATION),
"Right",
null,
listOf(ComplicationSlotOverlay.Builder(LEFT_COMPLICATION_ID).setEnabled(false).build())
)
val complicationsStyleSetting = ComplicationSlotsUserStyleSetting(
UserStyleSetting.Id("complications_style_setting"),
"AllComplicationSlots",
"Number and position",
icon = null,
complicationConfig = listOf(
bothComplicationsOption,
leftOnlyComplicationsOption,
rightOnlyComplicationsOption
),
affectsWatchFaceLayers = listOf(WatchFaceLayer.COMPLICATIONS)
)
initEngine(
WatchFaceType.DIGITAL,
listOf(leftComplication, rightComplication),
UserStyleSchema(listOf(complicationsStyleSetting)),
apiVersion = 4
)
assertTrue(leftComplication.enabled)
assertTrue(rightComplication.enabled)
// Select left complication only.
val mutableUserStyleA = currentUserStyleRepository.userStyle.value.toMutableUserStyle()
mutableUserStyleA[complicationsStyleSetting] = leftOnlyComplicationsOption
currentUserStyleRepository.updateUserStyle(mutableUserStyleA.toUserStyle())
runPostedTasksFor(0)
assertTrue(leftComplication.enabled)
assertFalse(rightComplication.enabled)
// Select right complication only.
val mutableUserStyleB = currentUserStyleRepository.userStyle.value.toMutableUserStyle()
mutableUserStyleB[complicationsStyleSetting] = rightOnlyComplicationsOption
currentUserStyleRepository.updateUserStyle(mutableUserStyleB.toUserStyle())
runPostedTasksFor(0)
assertFalse(leftComplication.enabled)
assertTrue(rightComplication.enabled)
// Select both complicationSlots.
val mutableUserStyleC = currentUserStyleRepository.userStyle.value.toMutableUserStyle()
mutableUserStyleC[complicationsStyleSetting] = bothComplicationsOption
currentUserStyleRepository.updateUserStyle(mutableUserStyleC.toUserStyle())
runPostedTasksFor(0)
assertTrue(leftComplication.enabled)
assertTrue(rightComplication.enabled)
}
@Test
public fun partialComplicationOverrideAppliedToInitialStyle() {
val bothComplicationsOption = ComplicationSlotsOption(
Option.Id(LEFT_AND_RIGHT_COMPLICATIONS),
"Left And Right",
null,
// An empty list means use the initial config.
emptyList()
)
val leftOnlyComplicationsOption = ComplicationSlotsOption(
Option.Id(LEFT_COMPLICATION),
"Left",
null,
listOf(ComplicationSlotOverlay.Builder(RIGHT_COMPLICATION_ID).setEnabled(false).build())
)
val complicationsStyleSetting = ComplicationSlotsUserStyleSetting(
UserStyleSetting.Id("complications_style_setting"),
"AllComplicationSlots",
"Number and position",
icon = null,
complicationConfig = listOf(
leftOnlyComplicationsOption, // The default value which should be applied.
bothComplicationsOption,
),
affectsWatchFaceLayers = listOf(WatchFaceLayer.COMPLICATIONS)
)
initEngine(
WatchFaceType.DIGITAL,
listOf(leftComplication, rightComplication),
UserStyleSchema(listOf(complicationsStyleSetting)),
apiVersion = 4
)
assertTrue(leftComplication.enabled)
assertFalse(rightComplication.enabled)
}
public fun UserStyleManager_init_applies_ComplicationsUserStyleSetting() {
val complicationSlotId1 = 101
val complicationSlotId2 = 102
val complicationsStyleSetting = ComplicationSlotsUserStyleSetting(
UserStyleSetting.Id("ID"),
"",
"",
icon = null,
complicationConfig = listOf(
ComplicationSlotsOption(
Option.Id("one"),
"one",
null,
listOf(
ComplicationSlotOverlay(
complicationSlotId1,
enabled = true
),
)
),
ComplicationSlotsOption(
Option.Id("two"),
"teo",
null,
listOf(
ComplicationSlotOverlay(
complicationSlotId2,
enabled = true
),
)
)
),
listOf(WatchFaceLayer.COMPLICATIONS)
)
val currentUserStyleRepository =
CurrentUserStyleRepository(UserStyleSchema(listOf(complicationsStyleSetting)))
val manager = ComplicationSlotsManager(
listOf(
ComplicationSlot.createRoundRectComplicationSlotBuilder(
complicationSlotId1,
{ watchState, listener ->
CanvasComplicationDrawable(complicationDrawableLeft, watchState, listener)
},
listOf(
ComplicationType.RANGED_VALUE,
),
DefaultComplicationDataSourcePolicy(SystemDataSources.DATA_SOURCE_DAY_OF_WEEK),
ComplicationSlotBounds(RectF(0.2f, 0.7f, 0.4f, 0.9f))
).setDefaultDataSourceType(ComplicationType.RANGED_VALUE)
.setEnabled(false)
.build(),
ComplicationSlot.createRoundRectComplicationSlotBuilder(
complicationSlotId2,
{ watchState, listener ->
CanvasComplicationDrawable(complicationDrawableRight, watchState, listener)
},
listOf(
ComplicationType.LONG_TEXT,
),
DefaultComplicationDataSourcePolicy(SystemDataSources.DATA_SOURCE_DAY_OF_WEEK),
ComplicationSlotBounds(RectF(0.2f, 0.7f, 0.4f, 0.9f))
).setDefaultDataSourceType(ComplicationType.LONG_TEXT)
.setEnabled(false)
.build()
),
currentUserStyleRepository
)
// The init function of ComplicationSlotsManager should enable complicationSlotId1.
assertThat(manager[complicationSlotId1]!!.enabled).isTrue()
}
@Test
public fun observeComplicationData() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList()),
WallpaperInteractiveWatchFaceInstanceParams(
"interactiveInstanceId",
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null
)
)
lateinit var leftComplicationData: ComplicationData
lateinit var rightComplicationData: ComplicationData
val scope = CoroutineScope(Dispatchers.Main.immediate)
scope.launch {
leftComplication.complicationData.collect {
leftComplicationData = it.asWireComplicationData()
}
}
scope.launch {
rightComplication.complicationData.collect {
rightComplicationData = it.asWireComplicationData()
}
}
interactiveWatchFaceInstance.updateComplicationData(
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT)
.setLongText(ComplicationText.plainText("TYPE_LONG_TEXT")).build()
),
IdAndComplicationDataWireFormat(
RIGHT_COMPLICATION_ID,
ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT")).build()
)
)
)
assertThat(leftComplicationData.type).isEqualTo(ComplicationData.TYPE_LONG_TEXT)
assertThat(leftComplicationData.longText?.getTextAt(context.resources, 0))
.isEqualTo("TYPE_LONG_TEXT")
assertThat(rightComplicationData.type).isEqualTo(ComplicationData.TYPE_SHORT_TEXT)
assertThat(rightComplicationData.shortText?.getTextAt(context.resources, 0))
.isEqualTo("TYPE_SHORT_TEXT")
}
@Test
public fun complicationsInitialized_with_NoComplicationComplicationData() {
initEngine(
WatchFaceType.DIGITAL,
listOf(leftComplication, rightComplication),
UserStyleSchema(listOf(complicationsStyleSetting)),
setInitialComplicationData = false
)
assertThat(
watchFaceImpl.complicationSlotsManager[LEFT_COMPLICATION_ID]!!.complicationData.value
).isInstanceOf(NoDataComplicationData::class.java)
assertThat(
watchFaceImpl.complicationSlotsManager[RIGHT_COMPLICATION_ID]!!.complicationData.value
).isInstanceOf(NoDataComplicationData::class.java)
}
@RequiresApi(Build.VERSION_CODES.O_MR1)
@Test
public fun headless_complicationsInitialized_with_EmptyComplicationData() {
testWatchFaceService = TestWatchFaceService(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
{ _, currentUserStyleRepository, watchState ->
TestRenderer(
surfaceHolder,
currentUserStyleRepository,
watchState,
INTERACTIVE_UPDATE_RATE_MS
)
},
UserStyleSchema(emptyList()),
watchState,
handler,
null,
false, // Allows DirectBoot
WallpaperInteractiveWatchFaceInstanceParams(
"Headless",
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(
hashMapOf(
colorStyleSetting to blueStyleOption,
watchHandStyleSetting to gothicStyleOption
)
).toWireFormat(),
null
),
choreographer
)
engineWrapper =
testWatchFaceService.createHeadlessEngine() as WatchFaceService.EngineWrapper
engineWrapper.createHeadlessInstance(
HeadlessWatchFaceInstanceParams(
ComponentName("test.watchface.app", "test.watchface.class"),
DeviceConfig(false, false, 100, 200),
100,
100
)
)
// [WatchFaceService.createWatchFace] Will have run by now because we're using an immediate
// coroutine dispatcher.
runBlocking {
watchFaceImpl = engineWrapper.deferredWatchFaceImpl.await()
}
assertThat(
watchFaceImpl.complicationSlotsManager[LEFT_COMPLICATION_ID]!!.complicationData.value
).isInstanceOf(EmptyComplicationData::class.java)
assertThat(
watchFaceImpl.complicationSlotsManager[RIGHT_COMPLICATION_ID]!!.complicationData.value
).isInstanceOf(EmptyComplicationData::class.java)
}
@Test
public fun complication_isActiveAt() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
listOf(leftComplication),
UserStyleSchema(emptyList()),
WallpaperInteractiveWatchFaceInstanceParams(
"interactiveInstanceId",
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null
)
)
interactiveWatchFaceInstance.updateComplicationData(
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT"))
.build()
)
)
)
// Initially the complication should be active.
assertThat(leftComplication.isActiveAt(Instant.EPOCH)).isTrue()
// Send empty data.
interactiveWatchFaceInstance.updateComplicationData(
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
ComplicationData.Builder(ComplicationData.TYPE_EMPTY).build()
)
)
)
assertThat(leftComplication.isActiveAt(Instant.EPOCH)).isFalse()
// Send a complication that is active for a time range.
interactiveWatchFaceInstance.updateComplicationData(
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT"))
.setStartDateTimeMillis(1000000)
.setEndDateTimeMillis(2000000)
.build()
)
)
)
assertThat(leftComplication.isActiveAt(Instant.ofEpochMilli(999999))).isFalse()
assertThat(leftComplication.isActiveAt(Instant.ofEpochMilli(1000000))).isTrue()
assertThat(leftComplication.isActiveAt(Instant.ofEpochMilli(2000000))).isTrue()
assertThat(leftComplication.isActiveAt(Instant.ofEpochMilli(2000001))).isFalse()
}
@Test
public fun updateInvalidCOmpliationIdDoesNotCrash() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
listOf(leftComplication),
UserStyleSchema(emptyList()),
WallpaperInteractiveWatchFaceInstanceParams(
"interactiveInstanceId",
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null
)
)
// Send a complication with an invalid id - this should get ignored.
interactiveWatchFaceInstance.updateComplicationData(
listOf(
IdAndComplicationDataWireFormat(
RIGHT_COMPLICATION_ID,
ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText("TYPE_SHORT_TEXT"))
.build()
)
)
)
}
@Test
public fun invalidateRendererBeforeFullInit() {
renderer = TestRenderer(
surfaceHolder,
CurrentUserStyleRepository(UserStyleSchema(emptyList())),
watchState.asWatchState(),
INTERACTIVE_UPDATE_RATE_MS
)
// This should not throw an exception.
renderer.invalidate()
}
@Test
public fun watchStateStateFlowDataMembersHaveValues() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
emptyList(),
UserStyleSchema(emptyList()),
WallpaperInteractiveWatchFaceInstanceParams(
"interactiveInstanceId",
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null
)
)
assertTrue(watchState.interruptionFilter.hasValue())
assertTrue(watchState.isAmbient.hasValue())
assertTrue(watchState.isBatteryLowAndNotCharging.hasValue())
assertTrue(watchState.isVisible.hasValue())
}
@Test
public fun isBatteryLowAndNotCharging_modified_by_broadcasts() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
emptyList(),
UserStyleSchema(emptyList()),
WallpaperInteractiveWatchFaceInstanceParams(
"interactiveInstanceId",
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null
)
)
watchFaceImpl.broadcastsObserver.onActionPowerConnected()
watchFaceImpl.broadcastsObserver.onActionBatteryOkay()
assertFalse(watchState.isBatteryLowAndNotCharging.value!!)
watchFaceImpl.broadcastsObserver.onActionBatteryLow()
assertFalse(watchState.isBatteryLowAndNotCharging.value!!)
watchFaceImpl.broadcastsObserver.onActionPowerDisconnected()
assertTrue(watchState.isBatteryLowAndNotCharging.value!!)
watchFaceImpl.broadcastsObserver.onActionBatteryOkay()
assertFalse(watchState.isBatteryLowAndNotCharging.value!!)
}
@Test
public fun processBatteryStatus() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
emptyList(),
UserStyleSchema(emptyList()),
WallpaperInteractiveWatchFaceInstanceParams(
"interactiveInstanceId",
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null
)
)
watchFaceImpl.broadcastsReceiver!!.processBatteryStatus(
Intent().apply {
putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_DISCHARGING)
putExtra(BatteryManager.EXTRA_LEVEL, 0)
putExtra(BatteryManager.EXTRA_SCALE, 100)
}
)
assertTrue(watchState.isBatteryLowAndNotCharging.value!!)
watchFaceImpl.broadcastsReceiver!!.processBatteryStatus(
Intent().apply {
putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_CHARGING)
putExtra(BatteryManager.EXTRA_LEVEL, 0)
putExtra(BatteryManager.EXTRA_SCALE, 100)
}
)
assertFalse(watchState.isBatteryLowAndNotCharging.value!!)
watchFaceImpl.broadcastsReceiver!!.processBatteryStatus(
Intent().apply {
putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_DISCHARGING)
putExtra(BatteryManager.EXTRA_LEVEL, 80)
putExtra(BatteryManager.EXTRA_SCALE, 100)
}
)
assertFalse(watchState.isBatteryLowAndNotCharging.value!!)
watchFaceImpl.broadcastsReceiver!!.processBatteryStatus(Intent())
assertFalse(watchState.isBatteryLowAndNotCharging.value!!)
watchFaceImpl.broadcastsReceiver!!.processBatteryStatus(null)
assertFalse(watchState.isBatteryLowAndNotCharging.value!!)
}
@Test
public fun isAmbientInitalisedEvenWithoutPropertiesSent() {
testWatchFaceService = TestWatchFaceService(
WatchFaceType.ANALOG,
emptyList(),
{ _, currentUserStyleRepository, watchState ->
TestRenderer(
surfaceHolder,
currentUserStyleRepository,
watchState,
INTERACTIVE_UPDATE_RATE_MS
)
},
UserStyleSchema(emptyList()),
watchState,
handler,
null,
true,
null,
choreographer
)
engineWrapper = testWatchFaceService.onCreateEngine() as WatchFaceService.EngineWrapper
engineWrapper.onCreate(surfaceHolder)
sendBinder(engineWrapper, 1)
// At this stage we haven't sent properties such as isAmbient, we expect it to be
// initialized to false (as opposed to null).
assertThat(watchState.isAmbient.value).isFalse()
}
@Test
public fun ambientToInteractiveTransition() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
emptyList(),
UserStyleSchema(emptyList()),
WallpaperInteractiveWatchFaceInstanceParams(
"interactiveInstanceId",
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(true, 0),
UserStyle(emptyMap()).toWireFormat(),
null
)
)
// We get an initial renderer when watch face init completes.
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(0L)
runPostedTasksFor(1000L)
// But no subsequent renders are scheduled.
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(0L)
// An ambientTickUpdate should trigger a render immediately.
engineWrapper.ambientTickUpdate()
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(1000L)
// But not trigger any subsequent rendering.
runPostedTasksFor(1000L)
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(1000L)
// When going interactive a frame should be rendered immediately.
engineWrapper.setWatchUiState(WatchUiState(false, 0))
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(2000L)
// And we should be producing frames.
runPostedTasksFor(100L)
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(2096L)
}
@Test
public fun interactiveToAmbientTransition() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
emptyList(),
UserStyleSchema(emptyList()),
WallpaperInteractiveWatchFaceInstanceParams(
"interactiveInstanceId",
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null
)
)
// We get an initial renderer when watch face init completes.
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(0L)
runPostedTasksFor(1000L)
// There's a number of subsequent renders every 16ms.
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(992L)
// After going ambient we should render immediately and then stop.
engineWrapper.setWatchUiState(WatchUiState(true, 0))
runPostedTasksFor(5000L)
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(1000L)
// An ambientTickUpdate should trigger a render immediately.
engineWrapper.ambientTickUpdate()
assertThat(renderer.lastOnDrawZonedDateTime!!.toInstant().toEpochMilli()).isEqualTo(6000L)
}
@Test
public fun onDestroy_clearsInstanceRecord() {
val instanceId = "interactiveInstanceId"
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
emptyList(),
UserStyleSchema(listOf(colorStyleSetting, watchHandStyleSetting)),
WallpaperInteractiveWatchFaceInstanceParams(
instanceId,
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(hashMapOf(colorStyleSetting to blueStyleOption)).toWireFormat(),
null
)
)
engineWrapper.onDestroy()
assertNull(InteractiveInstanceManager.getAndRetainInstance(instanceId))
}
@Test
public fun sendComplicationWallpaperCommandPreRFlow() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList())
)
setComplicationViaWallpaperCommand(
LEFT_COMPLICATION_ID,
ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText("Override"))
.build()
)
val complication =
watchFaceImpl.complicationSlotsManager[LEFT_COMPLICATION_ID]!!.complicationData.value as
ShortTextComplicationData
assertThat(
complication.text.getTextAt(
ApplicationProvider.getApplicationContext<Context>().resources,
Instant.EPOCH
)
).isEqualTo("Override")
}
@Test
public fun sendComplicationWallpaperCommandIgnoredPostRFlow() {
val instanceId = "interactiveInstanceId"
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList()),
WallpaperInteractiveWatchFaceInstanceParams(
instanceId,
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText("INITIAL_VALUE"))
.build()
)
)
)
)
// This should be ignored because we're on the R flow.
setComplicationViaWallpaperCommand(
LEFT_COMPLICATION_ID,
ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText("Override"))
.build()
)
val complication =
watchFaceImpl.complicationSlotsManager[LEFT_COMPLICATION_ID]!!.complicationData.value as
ShortTextComplicationData
assertThat(
complication.text.getTextAt(
ApplicationProvider.getApplicationContext<Context>().resources,
Instant.EPOCH
)
).isEqualTo("INITIAL_VALUE")
}
@Test
public fun directBoot() {
val instanceId = "DirectBootInstance"
testWatchFaceService = TestWatchFaceService(
WatchFaceType.ANALOG,
emptyList(),
{ _, currentUserStyleRepository, watchState ->
TestRenderer(
surfaceHolder,
currentUserStyleRepository,
watchState,
INTERACTIVE_UPDATE_RATE_MS
)
},
UserStyleSchema(listOf(colorStyleSetting, watchHandStyleSetting)),
watchState,
handler,
null,
false, // Allows DirectBoot
WallpaperInteractiveWatchFaceInstanceParams(
instanceId,
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(
hashMapOf(
colorStyleSetting to blueStyleOption,
watchHandStyleSetting to gothicStyleOption
)
).toWireFormat(),
null
),
choreographer
)
engineWrapper = testWatchFaceService.onCreateEngine() as WatchFaceService.EngineWrapper
engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
val instance = InteractiveInstanceManager.getAndRetainInstance(instanceId)
assertThat(instance).isNotNull()
watchFaceImpl = engineWrapper.getWatchFaceImplOrNull()!!
val userStyle = watchFaceImpl.currentUserStyleRepository.userStyle.value
assertThat(userStyle[colorStyleSetting]).isEqualTo(blueStyleOption)
assertThat(userStyle[watchHandStyleSetting]).isEqualTo(gothicStyleOption)
InteractiveInstanceManager.deleteInstance(instanceId)
}
@Test
public fun headlessFlagPreventsDirectBoot() {
val instanceId = "DirectBootInstance"
testWatchFaceService = TestWatchFaceService(
WatchFaceType.ANALOG,
emptyList(),
{ _, currentUserStyleRepository, watchState ->
TestRenderer(
surfaceHolder,
currentUserStyleRepository,
watchState,
INTERACTIVE_UPDATE_RATE_MS
)
},
UserStyleSchema(emptyList()),
watchState,
handler,
null,
false, // Allows DirectBoot
WallpaperInteractiveWatchFaceInstanceParams(
instanceId,
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(
hashMapOf(
colorStyleSetting to blueStyleOption,
watchHandStyleSetting to gothicStyleOption
)
).toWireFormat(),
null
),
choreographer
)
testWatchFaceService.createHeadlessEngine()
runPostedTasksFor(0)
val instance = InteractiveInstanceManager.getAndRetainInstance(instanceId)
assertThat(instance).isNull()
}
@Test
public fun firstOnVisibilityChangedIgnoredPostRFlow() {
val instanceId = "interactiveInstanceId"
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList()),
WallpaperInteractiveWatchFaceInstanceParams(
instanceId,
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText("INITIAL_VALUE"))
.build()
)
)
)
)
var numOfCalls = 0
CoroutineScope(handler.asCoroutineDispatcher().immediate).launch {
watchState.isVisible.collect {
numOfCalls++
}
}
// The collect call will be triggered immediately to report the current value, so make
// sure that numOfCalls has increased before proceeding next.
runPostedTasksFor(0)
assertEquals(1, numOfCalls)
// This should be ignored and not trigger collect.
engineWrapper.onVisibilityChanged(true)
runPostedTasksFor(0)
assertEquals(1, numOfCalls)
// This should trigger the observer.
engineWrapper.onVisibilityChanged(false)
runPostedTasksFor(0)
assertEquals(2, numOfCalls)
}
@Test
public fun complicationsUserStyleSetting_with_setComplicationBounds() {
val rightComplicationBoundsOption = ComplicationSlotsOption(
Option.Id(RIGHT_COMPLICATION),
"Right",
null,
listOf(
ComplicationSlotOverlay.Builder(RIGHT_COMPLICATION_ID)
.setComplicationSlotBounds(
ComplicationSlotBounds(RectF(0.1f, 0.1f, 0.2f, 0.2f))
).build()
)
)
val complicationsStyleSetting = ComplicationSlotsUserStyleSetting(
UserStyleSetting.Id("complications_style_setting"),
"AllComplicationSlots",
"Number and position",
icon = null,
complicationConfig = listOf(
ComplicationSlotsOption(
Option.Id("Default"),
"Default",
null,
emptyList()
),
rightComplicationBoundsOption
),
affectsWatchFaceLayers = listOf(WatchFaceLayer.COMPLICATIONS)
)
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(listOf(complicationsStyleSetting)),
WallpaperInteractiveWatchFaceInstanceParams(
"interactiveInstanceId",
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText("INITIAL_VALUE"))
.build()
)
)
)
)
var complicationDetails = watchFaceImpl.getComplicationState()
assertThat(complicationDetails[1].id).isEqualTo(RIGHT_COMPLICATION_ID)
assertThat(complicationDetails[1].complicationState.bounds).isEqualTo(
Rect(60, 40, 80, 60)
)
// Select a style which changes the bounds of the right complication.
val mutableUserStyle = currentUserStyleRepository.userStyle.value.toMutableUserStyle()
mutableUserStyle[complicationsStyleSetting] = rightComplicationBoundsOption
currentUserStyleRepository.updateUserStyle(mutableUserStyle.toUserStyle())
complicationDetails = watchFaceImpl.getComplicationState()
assertThat(complicationDetails[1].id).isEqualTo(RIGHT_COMPLICATION_ID)
assertThat(complicationDetails[1].complicationState.bounds).isEqualTo(
Rect(10, 10, 20, 20)
)
}
@Test
public fun canvasComplication_onRendererCreated() {
val leftCanvasComplication = mock<CanvasComplication>()
val leftComplication =
ComplicationSlot.createRoundRectComplicationSlotBuilder(
LEFT_COMPLICATION_ID,
{ _, _ -> leftCanvasComplication },
listOf(
ComplicationType.SHORT_TEXT,
),
DefaultComplicationDataSourcePolicy(SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET),
ComplicationSlotBounds(RectF(0.2f, 0.4f, 0.4f, 0.6f))
).setDefaultDataSourceType(ComplicationType.SHORT_TEXT)
.build()
val rightCanvasComplication = mock<CanvasComplication>()
val rightComplication =
ComplicationSlot.createRoundRectComplicationSlotBuilder(
RIGHT_COMPLICATION_ID,
{ _, _ -> rightCanvasComplication },
listOf(
ComplicationType.SHORT_TEXT,
),
DefaultComplicationDataSourcePolicy(SystemDataSources.DATA_SOURCE_DATE),
ComplicationSlotBounds(RectF(0.6f, 0.4f, 0.8f, 0.6f))
).setDefaultDataSourceType(ComplicationType.SHORT_TEXT)
.build()
initEngine(
WatchFaceType.DIGITAL,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList())
)
verify(leftCanvasComplication).onRendererCreated(renderer)
verify(rightCanvasComplication).onRendererCreated(renderer)
}
@Test
public fun complicationSlotsWithTheSameRenderer() {
val sameCanvasComplication = mock<CanvasComplication>()
val leftComplication =
ComplicationSlot.createRoundRectComplicationSlotBuilder(
LEFT_COMPLICATION_ID,
{ _, _ -> sameCanvasComplication },
listOf(ComplicationType.SHORT_TEXT),
DefaultComplicationDataSourcePolicy(SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET),
ComplicationSlotBounds(RectF(0.2f, 0.4f, 0.4f, 0.6f))
).setDefaultDataSourceType(ComplicationType.SHORT_TEXT)
.build()
val rightComplication =
ComplicationSlot.createRoundRectComplicationSlotBuilder(
RIGHT_COMPLICATION_ID,
{ _, _ -> sameCanvasComplication },
listOf(ComplicationType.SHORT_TEXT),
DefaultComplicationDataSourcePolicy(SystemDataSources.DATA_SOURCE_DATE),
ComplicationSlotBounds(RectF(0.6f, 0.4f, 0.8f, 0.6f))
).setDefaultDataSourceType(ComplicationType.SHORT_TEXT)
.build()
// We don't want full init as in other tests with initEngine(), since
// engineWrapper.getWatchFaceImplOrNull() will return null and test will brake with NPE.
initEngineBeforeGetWatchFaceImpl(
WatchFaceType.DIGITAL,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList())
)
assertTrue(engineWrapper.deferredWatchFaceImpl.isCancelled)
runBlocking {
assertFailsWith<IllegalArgumentException> {
engineWrapper.deferredWatchFaceImpl.await()
}
}
}
@Test
public fun additionalContentDescriptionLabelsSetBeforeWatchFaceInitComplete() {
testWatchFaceService = TestWatchFaceService(
WatchFaceType.ANALOG,
emptyList(),
{ _, currentUserStyleRepository, watchState ->
renderer = TestRenderer(
surfaceHolder,
currentUserStyleRepository,
watchState,
INTERACTIVE_UPDATE_RATE_MS
)
// Set additionalContentDescriptionLabels before renderer.watchFaceHostApi has been
// set.
renderer.additionalContentDescriptionLabels = listOf(
Pair(
0,
ContentDescriptionLabel(
PlainComplicationText.Builder("Example").build(),
Rect(10, 10, 20, 20),
null
)
)
)
renderer
},
UserStyleSchema(emptyList()),
watchState,
handler,
null,
false,
null,
choreographer
)
InteractiveInstanceManager
.getExistingInstanceOrSetPendingWallpaperInteractiveWatchFaceInstance(
InteractiveInstanceManager.PendingWallpaperInteractiveWatchFaceInstance(
WallpaperInteractiveWatchFaceInstanceParams(
"TestID",
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList()
),
object : IPendingInteractiveWatchFace.Stub() {
override fun getApiVersion() =
IPendingInteractiveWatchFace.API_VERSION
override fun onInteractiveWatchFaceCreated(
iInteractiveWatchFace: IInteractiveWatchFace
) {
interactiveWatchFaceInstance = iInteractiveWatchFace
}
override fun onInteractiveWatchFaceCrashed(exception: CrashInfoParcel?) {
fail("WatchFace crashed: $exception")
}
}
)
)
engineWrapper = testWatchFaceService.onCreateEngine() as WatchFaceService.EngineWrapper
engineWrapper.onCreate(surfaceHolder)
engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
// Check the additional ContentDescriptionLabel was applied.
assertThat(engineWrapper.contentDescriptionLabels.size).isEqualTo(2)
assertThat(
engineWrapper.contentDescriptionLabels[1].text.getTextAt(
ApplicationProvider.getApplicationContext<Context>().resources,
0
)
).isEqualTo("Example")
}
@Test
public fun setComplicationDataList() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList()),
WallpaperInteractiveWatchFaceInstanceParams(
"TestID",
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList()
)
)
val interactiveInstance = InteractiveInstanceManager.getAndRetainInstance("TestID")
interactiveInstance!!.updateComplicationData(
mutableListOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText("LEFT!"))
.build()
),
IdAndComplicationDataWireFormat(
RIGHT_COMPLICATION_ID,
ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText("RIGHT!"))
.build()
)
)
)
assertThat(engineWrapper.contentDescriptionLabels.size).isEqualTo(3)
assertThat(
engineWrapper.contentDescriptionLabels[1].text.getTextAt(
ApplicationProvider.getApplicationContext<Context>().resources,
0
)
).isEqualTo("LEFT!")
assertThat(
engineWrapper.contentDescriptionLabels[2].text.getTextAt(
ApplicationProvider.getApplicationContext<Context>().resources,
0
)
).isEqualTo("RIGHT!")
}
@Test
public fun schemaWithTooLargeIcon() {
val tooLargeIcon = Icon.createWithBitmap(
Bitmap.createBitmap(
WatchFaceService.MAX_REASONABLE_SCHEMA_ICON_WIDTH + 1,
WatchFaceService.MAX_REASONABLE_SCHEMA_ICON_HEIGHT + 1,
Bitmap.Config.ARGB_8888
)
)
val settingWithTooLargeIcon = ListUserStyleSetting(
UserStyleSetting.Id("color_style_setting"),
"Colors",
"Watchface colorization", /* icon = */
tooLargeIcon,
colorStyleList,
listOf(WatchFaceLayer.BASE)
)
try {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
emptyList(),
UserStyleSchema(listOf(settingWithTooLargeIcon, watchHandStyleSetting)),
WallpaperInteractiveWatchFaceInstanceParams(
"interactiveInstanceId",
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(
hashMapOf(
colorStyleSetting to blueStyleOption,
watchHandStyleSetting to gothicStyleOption
)
).toWireFormat(),
null
)
)
fail("Should have thrown an exception due to an Icon that's too large")
} catch (e: Exception) {
assertThat(e.message).contains(
"UserStyleSetting id color_style_setting has a 401 x 401 icon. This is too big, " +
"the maximum size is 400 x 400."
)
}
}
@Test
public fun schemaWithTooLargeWireFormat() {
val longOptionsList = ArrayList<ListUserStyleSetting.ListOption>()
for (i in 0..10000) {
longOptionsList.add(
ListUserStyleSetting.ListOption(Option.Id("id$i"), "Name", icon = null)
)
}
val tooLargeList = ListUserStyleSetting(
UserStyleSetting.Id("too_large"),
"Too large!",
"Description", /* icon = */
null,
longOptionsList,
listOf(WatchFaceLayer.BASE)
)
try {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
emptyList(),
UserStyleSchema(listOf(tooLargeList, watchHandStyleSetting)),
WallpaperInteractiveWatchFaceInstanceParams(
"interactiveInstanceId",
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(hashMapOf(watchHandStyleSetting to gothicStyleOption)).toWireFormat(),
null
)
)
fail("Should have thrown an exception due to an Icon that's too large")
} catch (e: Exception) {
assertThat(e.message).contains(
"The estimated wire size of the supplied UserStyleSchemas for watch face " +
"androidx.wear.watchface.test is too big"
)
}
}
@Test
public fun getComplicationSlotMetadataWireFormats_parcelTest() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
UserStyleSchema(emptyList()),
WallpaperInteractiveWatchFaceInstanceParams(
"TestID",
DeviceConfig(
false,
false,
0,
0
),
WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
emptyList()
)
)
val metadata = engineWrapper.getComplicationSlotMetadataWireFormats()
assertThat(metadata.size).isEqualTo(2)
assertThat(metadata[0].id).isEqualTo(leftComplication.id)
assertThat(metadata[1].id).isEqualTo(rightComplication.id)
val parcel = Parcel.obtain()
metadata[0].writeToParcel(parcel, 0)
parcel.setDataPosition(0)
// This shouldn't throw an exception.
val unparceled = ComplicationSlotMetadataWireFormat.CREATOR.createFromParcel(parcel)
parcel.recycle()
// A quick check, we don't need to test everything here.
assertThat(unparceled.id).isEqualTo(leftComplication.id)
assertThat(unparceled.complicationBounds.size)
.isEqualTo(metadata[0].complicationBounds.size)
assertThat(unparceled.complicationBoundsType.size)
.isEqualTo(metadata[0].complicationBounds.size)
assertThat(unparceled.isInitiallyEnabled).isTrue()
}
@SuppressLint("NewApi")
@Suppress("DEPRECATION")
private fun getChinWindowInsetsApi25(@Px chinHeight: Int): WindowInsets =
WindowInsets.Builder().setSystemWindowInsets(
Insets.of(0, 0, 0, chinHeight)
).build()
@SuppressLint("NewApi")
private fun getChinWindowInsetsApi30(@Px chinHeight: Int): WindowInsets =
WindowInsets.Builder().setInsets(
WindowInsets.Type.systemBars(),
Insets.of(Rect().apply { bottom = chinHeight })
).build()
}