blob: e7caa3a3a96917ceb510f1cd0c04369cd64e89e2 [file] [log] [blame]
/*
* Copyright 2021 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.editor
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Icon
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.os.IBinder
import android.os.Looper
import android.support.wearable.complications.IPreviewComplicationDataCallback
import android.support.wearable.complications.IProviderInfoService
import android.support.wearable.watchface.Constants
import android.view.Surface
import android.view.SurfaceHolder
import androidx.activity.ComponentActivity
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import androidx.wear.watchface.complications.ComplicationDataSourceInfo
import androidx.wear.watchface.complications.ComplicationDataSourceInfoRetriever
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.EmptyComplicationData
import androidx.wear.watchface.complications.data.LongTextComplicationData
import androidx.wear.watchface.complications.data.PlainComplicationText
import androidx.wear.watchface.complications.data.ShortTextComplicationData
import androidx.wear.watchface.BroadcastsObserver
import androidx.wear.watchface.CanvasComplication
import androidx.wear.watchface.CanvasType
import androidx.wear.watchface.ComplicationDataSourceChooserIntent
import androidx.wear.watchface.ComplicationHelperActivity
import androidx.wear.watchface.ComplicationSlot
import androidx.wear.watchface.ComplicationSlotsManager
import androidx.wear.watchface.MutableWatchState
import androidx.wear.watchface.RenderParameters
import androidx.wear.watchface.Renderer
import androidx.wear.watchface.WatchFace
import androidx.wear.watchface.WatchFaceHostApi
import androidx.wear.watchface.WatchFaceImpl
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.WatchFaceId
import androidx.wear.watchface.client.asApiEditorState
import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
import androidx.wear.watchface.complications.rendering.ComplicationDrawable
import androidx.wear.watchface.ComplicationSlotBoundsType
import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.editor.EditorSession.Companion.EDITING_SESSION_TIMEOUT
import androidx.wear.watchface.editor.data.EditorStateWireFormat
import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyle
import androidx.wear.watchface.style.UserStyleData
import androidx.wear.watchface.style.UserStyleSchema
import androidx.wear.watchface.style.UserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption
import androidx.wear.watchface.style.UserStyleSetting.Option
import androidx.wear.watchface.style.WatchFaceLayer
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlin.test.assertFailsWith
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito
import org.mockito.Mockito.`when`
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.mock
import java.lang.IllegalArgumentException
import java.time.Instant
import java.time.ZonedDateTime
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.MILLISECONDS
public const val LEFT_COMPLICATION_ID: Int = 1000
public const val RIGHT_COMPLICATION_ID: Int = 1001
public const val BACKGROUND_COMPLICATION_ID: Int = 1111
public val dataSource1: ComponentName = ComponentName("dataSource.app1", "dataSource.class1")
public val dataSource2: ComponentName = ComponentName("dataSource.app2", "dataSource.class2")
public val dataSource3: ComponentName = ComponentName("dataSource.app3", "dataSource.class3")
private const val TIMEOUT_MILLIS = 500L
private const val PROVIDER_CHOOSER_EXTRA_KEY = "PROVIDER_CHOOSER_EXTRA_KEY"
private const val PROVIDER_CHOOSER_EXTRA_VALUE = "PROVIDER_CHOOSER_EXTRA_VALUE"
private const val PROVIDER_CHOOSER_RESULT_EXTRA_KEY = "PROVIDER_CHOOSER_RESULT_EXTRA_KEY"
private const val PROVIDER_CHOOSER_RESULT_EXTRA_VALUE = "PROVIDER_CHOOSER_RESULT_EXTRA_VALUE"
private typealias WireComplicationProviderInfo =
android.support.wearable.complications.ComplicationProviderInfo
internal val redStyleOption = ListOption(Option.Id("red_style"), "Red", icon = null)
internal val greenStyleOption = ListOption(Option.Id("green_style"), "Green", icon = null)
internal val blueStyleOption = ListOption(Option.Id("blue_style"), "Blue", icon = null)
internal val colorStyleList = listOf(redStyleOption, greenStyleOption, blueStyleOption)
internal val colorStyleSetting = UserStyleSetting.ListUserStyleSetting(
UserStyleSetting.Id("color_style_setting"),
"Colors",
"Watchface colorization", /* icon = */
null,
colorStyleList,
listOf(WatchFaceLayer.BASE)
)
internal val classicStyleOption = ListOption(Option.Id("classic_style"), "Classic", icon = null)
internal val modernStyleOption = ListOption(Option.Id("modern_style"), "Modern", icon = null)
internal val gothicStyleOption = ListOption(Option.Id("gothic_style"), "Gothic", icon = null)
internal val watchHandStyleList =
listOf(classicStyleOption, modernStyleOption, gothicStyleOption)
internal val watchHandStyleSetting = UserStyleSetting.ListUserStyleSetting(
UserStyleSetting.Id("hand_style_setting"),
"Hand Style",
"Hand visual look", /* icon = */
null,
watchHandStyleList,
listOf(WatchFaceLayer.COMPLICATIONS_OVERLAY)
)
private val mockInvalidateCallback =
Mockito.mock(CanvasComplication.InvalidateCallback::class.java)
private val placeholderWatchState = MutableWatchState().asWatchState()
private val mockLeftCanvasComplication = CanvasComplicationDrawable(
ComplicationDrawable(),
placeholderWatchState,
mockInvalidateCallback
)
private val mockRightCanvasComplication = CanvasComplicationDrawable(
ComplicationDrawable(),
placeholderWatchState,
mockInvalidateCallback
)
private val mockBackgroundCanvasComplication =
CanvasComplicationDrawable(
ComplicationDrawable(),
placeholderWatchState,
mockInvalidateCallback
)
private val backgroundComplication =
ComplicationSlot.createBackgroundComplicationSlotBuilder(
BACKGROUND_COMPLICATION_ID,
{ _, _ -> mockBackgroundCanvasComplication },
emptyList(),
DefaultComplicationDataSourcePolicy()
).setEnabled(false).build()
private val bothComplicationsOption =
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
"Left And Right",
null,
// An empty list means use the initial config.
emptyList()
)
private val leftOnlyComplicationsOption =
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
Option.Id("LEFT_COMPLICATION"),
"Left",
null,
listOf(
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay.Builder(
RIGHT_COMPLICATION_ID
).setEnabled(false).build()
)
)
private val rightOnlyComplicationsOption =
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
Option.Id("RIGHT_COMPLICATION"),
"Right",
null,
listOf(
UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay.Builder(
LEFT_COMPLICATION_ID
).setEnabled(false).build()
)
)
private val complicationsStyleSetting = UserStyleSetting.ComplicationSlotsUserStyleSetting(
UserStyleSetting.Id("complications_style_setting"),
"AllComplicationSlots",
"Number and position",
icon = null,
complicationConfig = listOf(
bothComplicationsOption,
leftOnlyComplicationsOption,
rightOnlyComplicationsOption
),
affectsWatchFaceLayers = listOf(WatchFaceLayer.COMPLICATIONS)
)
/** A trivial [WatchFaceService] used for testing headless editor instances. */
public class TestHeadlessWatchFaceService : WatchFaceService() {
override fun createUserStyleSchema() =
UserStyleSchema(listOf(colorStyleSetting, watchHandStyleSetting))
override fun createComplicationSlotsManager(
currentUserStyleRepository: CurrentUserStyleRepository
) = ComplicationSlotsManager(emptyList(), currentUserStyleRepository)
override suspend fun createWatchFace(
surfaceHolder: SurfaceHolder,
watchState: WatchState,
complicationSlotsManager: ComplicationSlotsManager,
currentUserStyleRepository: CurrentUserStyleRepository
) = WatchFace(
WatchFaceType.ANALOG,
object : Renderer.CanvasRenderer(
surfaceHolder,
currentUserStyleRepository,
watchState,
CanvasType.SOFTWARE,
100
) {
override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
when (currentUserStyleRepository.userStyle.value[colorStyleSetting]!!) {
redStyleOption -> canvas.drawColor(Color.RED)
greenStyleOption -> canvas.drawColor(Color.GREEN)
blueStyleOption -> canvas.drawColor(Color.BLUE)
}
}
override fun renderHighlightLayer(
canvas: Canvas,
bounds: Rect,
zonedDateTime: ZonedDateTime
) {
// NOP
}
}
)
}
/** Trivial "editor" which exposes the EditorSession for testing. */
public open class OnWatchFaceEditingTestActivity : ComponentActivity() {
public lateinit var editorSession: EditorSession
public lateinit var onCreateException: Exception
public val creationLatch: CountDownLatch = CountDownLatch(1)
public val deferredDone = CompletableDeferred<Unit>()
public val listenableEditorSession: ListenableEditorSession by lazy {
ListenableEditorSession(editorSession)
}
internal companion object {
internal var complicationDataSourceInfoRetrieverProvider:
ComplicationDataSourceInfoRetrieverProvider? = null
}
public val immediateCoroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main.immediate)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
immediateCoroutineScope.launch {
try {
editorSession = EditorSession.createOnWatchEditorSessionImpl(
this@OnWatchFaceEditingTestActivity,
intent!!,
complicationDataSourceInfoRetrieverProvider!!
)
} catch (e: Exception) {
onCreateException = e
} finally {
deferredDone.complete(Unit)
creationLatch.countDown()
}
}
}
}
public open class TestComplicationDataSourceInfoRetrieverProvider(
val getProviderInfosLatch: CountDownLatch? = null
) : ComplicationDataSourceInfoRetrieverProvider, IProviderInfoService.Stub() {
private val dataSourceIcon1: Icon =
Icon.createWithBitmap(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
private val dataSourceIcon2: Icon =
Icon.createWithBitmap(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
private val watchFaceComponent = ComponentName("test.package", "test.class")
private val dataSourceInfos = mapOf(
LEFT_COMPLICATION_ID to ComplicationDataSourceInfo(
"DataSourceApp1",
"DataSource1",
dataSourceIcon1,
ComplicationType.SHORT_TEXT,
dataSource1
),
RIGHT_COMPLICATION_ID to ComplicationDataSourceInfo(
"DataSourceApp2",
"DataSource2",
dataSourceIcon2,
ComplicationType.LONG_TEXT,
dataSource2
)
)
private val previewData = mapOf(
dataSource1 to
ShortTextComplicationData.Builder(
PlainComplicationText.Builder("Left").build(),
ComplicationText.EMPTY
).build().asWireComplicationData(),
dataSource2 to
LongTextComplicationData.Builder(
PlainComplicationText.Builder("Right").build(),
ComplicationText.EMPTY
).build().asWireComplicationData(),
dataSource3 to
LongTextComplicationData.Builder(
PlainComplicationText.Builder("DataSource3").build(),
ComplicationText.EMPTY
).build().asWireComplicationData(),
)
public lateinit var lastComplicationDataSourceInfoRetriever: ComplicationDataSourceInfoRetriever
override fun getComplicationDataSourceInfoRetriever(): ComplicationDataSourceInfoRetriever {
lastComplicationDataSourceInfoRetriever = ComplicationDataSourceInfoRetriever(this)
return lastComplicationDataSourceInfoRetriever
}
override fun getProviderInfos(
watchFaceComponent: ComponentName,
ids: IntArray
): Array<WireComplicationProviderInfo?>? {
getProviderInfosLatch?.await()
if (watchFaceComponent != this.watchFaceComponent) {
return null
}
return ArrayList<WireComplicationProviderInfo?>().apply {
for (id in ids) {
add(dataSourceInfos[id]?.toWireComplicationProviderInfo())
}
}.toTypedArray()
}
override fun getApiVersion(): Int = 1
override fun requestPreviewComplicationData(
dataSourceComponent: ComponentName,
complicationType: Int,
previewComplicationDataCallback: IPreviewComplicationDataCallback
): Boolean {
previewComplicationDataCallback.updateComplicationData(previewData[dataSourceComponent])
return true
}
}
/** Fake ComplicationHelperActivity for testing. */
public class TestComplicationHelperActivity : Activity() {
public companion object {
public var lastIntent: Intent? = null
public var resultIntent: CompletableDeferred<Intent?>? = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lastIntent = intent
CoroutineScope(Dispatchers.Main.immediate).launch {
setResult(123, resultIntent!!.await())
finish()
}
}
}
/** Fake complication data source chooser for testing. */
public class TestComplicationDataSourceChooserActivity : Activity() {
public companion object {
public var lastIntent: Intent? = null
public var resultIntent: Intent? = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lastIntent = intent
setResult(123, resultIntent)
finish()
}
}
public class TestEditorObserver : IEditorObserver.Stub() {
private lateinit var editorState: EditorStateWireFormat
private var latch = CountDownLatch(1)
override fun getApiVersion() = IEditorObserver.API_VERSION
override fun onEditorStateChange(editorState: EditorStateWireFormat) {
this.editorState = editorState
latch.countDown()
}
fun awaitEditorStateChange(timeout: Long, unit: TimeUnit): EditorStateWireFormat {
require(latch.await(timeout, unit))
return editorState
}
fun stateChangeObserved() = this::editorState.isInitialized
}
@RunWith(AndroidJUnit4::class)
@MediumTest
public class EditorSessionTest {
private val headlessWatchFaceComponentName = ComponentName(
"test.package",
TestHeadlessWatchFaceService::class.qualifiedName!!
)
private val testEditorPackageName = "test.package"
private val testInstanceId = WatchFaceId("TEST_INSTANCE_ID")
private lateinit var editorDelegate: WatchFace.EditorDelegate
private val screenBounds = Rect(0, 0, 400, 400)
private val fakeBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
private val onDestroyLatch = CountDownLatch(1)
private val dataSourceIcon =
Icon.createWithBitmap(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
private val dataSourceComponentName = ComponentName("test.package", "test.class")
internal val complicationDeniedDialogIntent = Intent("ComplicationDeniedDialog")
internal val complicationRationaleDialogIntent = Intent("ComplicationRationaleDialog")
private val leftComplication =
ComplicationSlot.createRoundRectComplicationSlotBuilder(
LEFT_COMPLICATION_ID,
{ _, _ -> mockLeftCanvasComplication },
listOf(
ComplicationType.RANGED_VALUE,
ComplicationType.LONG_TEXT,
ComplicationType.SHORT_TEXT,
ComplicationType.MONOCHROMATIC_IMAGE,
ComplicationType.SMALL_IMAGE
),
DefaultComplicationDataSourcePolicy(
ComponentName("com.primary.package", "com.primary.app"),
ComplicationType.LONG_TEXT,
SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET,
ComplicationType.SHORT_TEXT
),
ComplicationSlotBounds(
ComplicationType.values().associateWith {
if (it == ComplicationType.LONG_TEXT) {
RectF(0.1f, 0.4f, 0.4f, 0.6f)
} else {
RectF(0.3f, 0.4f, 0.4f, 0.6f)
}
}
)
)
.build()
private val rightComplication =
ComplicationSlot.createRoundRectComplicationSlotBuilder(
RIGHT_COMPLICATION_ID,
{ _, _ -> mockRightCanvasComplication },
listOf(
ComplicationType.RANGED_VALUE,
ComplicationType.LONG_TEXT,
ComplicationType.SHORT_TEXT,
ComplicationType.MONOCHROMATIC_IMAGE,
ComplicationType.SMALL_IMAGE
),
DefaultComplicationDataSourcePolicy(
ComponentName("com.primary.package", "com.primary.app"),
ComplicationType.LONG_TEXT,
ComponentName("com.secondary.package", "com.secondary.app"),
ComplicationType.PHOTO_IMAGE,
SystemDataSources.DATA_SOURCE_DAY_OF_WEEK,
ComplicationType.SHORT_TEXT
),
ComplicationSlotBounds(
ComplicationType.values().associateWith {
if (it == ComplicationType.LONG_TEXT) {
RectF(0.6f, 0.4f, 0.9f, 0.6f)
} else {
RectF(0.6f, 0.4f, 0.7f, 0.6f)
}
}
)
)
.setConfigExtras(
Bundle().apply {
putString(PROVIDER_CHOOSER_EXTRA_KEY, PROVIDER_CHOOSER_EXTRA_VALUE)
}
)
.build()
@SuppressLint("NewApi") // EditorRequest
private fun createOnWatchFaceEditingTestActivity(
userStyleSettings: List<UserStyleSetting>,
complicationSlots: List<ComplicationSlot>,
watchFaceId: WatchFaceId = testInstanceId,
previewReferenceInstant: Instant = Instant.ofEpochMilli(12345),
complicationDataSourceInfoRetrieverProvider: ComplicationDataSourceInfoRetrieverProvider =
TestComplicationDataSourceInfoRetrieverProvider(),
shouldTimeout: Boolean = false,
preRFlow: Boolean = false,
headlessDeviceConfig: DeviceConfig? = null,
initialUserStyle: UserStyleData? = null,
watchComponentName: ComponentName = ComponentName("test.package", "test.class"),
previewScreenshotParams: PreviewScreenshotParams? = null
): ActivityScenario<OnWatchFaceEditingTestActivity> {
val userStyleRepository = CurrentUserStyleRepository(UserStyleSchema(userStyleSettings))
val complicationSlotsManager =
ComplicationSlotsManager(complicationSlots, userStyleRepository)
complicationSlotsManager.watchState = placeholderWatchState
complicationSlotsManager.listenForStyleChanges(CoroutineScope(Dispatchers.Main.immediate))
// Mocking getters and setters with mockito at the same time is hard so we do this instead.
editorDelegate = object : WatchFace.EditorDelegate {
private val backgroundHandlerThread = HandlerThread("TestBackgroundThread").apply {
start()
}
private val backgroundHandler = Handler(backgroundHandlerThread.looper)
override val userStyleSchema = userStyleRepository.schema
override var userStyle: UserStyle
get() = userStyleRepository.userStyle.value
set(value) {
userStyleRepository.updateUserStyle(value)
}
override val complicationSlotsManager = complicationSlotsManager
override val screenBounds = this@EditorSessionTest.screenBounds
override val previewReferenceInstant = previewReferenceInstant
override val backgroundThreadHandler = backgroundHandler
override val complicationDeniedDialogIntent =
this@EditorSessionTest.complicationDeniedDialogIntent
override val complicationRationaleDialogIntent =
this@EditorSessionTest.complicationRationaleDialogIntent
override fun renderWatchFaceToBitmap(
renderParameters: RenderParameters,
instant: Instant,
slotIdToComplicationData:
Map<Int, ComplicationData>?
) = fakeBitmap
override fun onDestroy() {
onDestroyLatch.countDown()
backgroundHandlerThread.quitSafely()
}
override fun setComplicationSlotConfigExtrasChangeCallback(
callback: WatchFace.ComplicationSlotConfigExtrasChangeCallback?
) {
complicationSlotsManager.configExtrasChangeCallback = callback
}
}
if (!shouldTimeout) {
WatchFace.registerEditorDelegate(watchComponentName, editorDelegate)
}
OnWatchFaceEditingTestActivity.complicationDataSourceInfoRetrieverProvider =
complicationDataSourceInfoRetrieverProvider
if (preRFlow) {
return ActivityScenario.launch(
Intent().apply {
putExtra(Constants.EXTRA_WATCH_FACE_COMPONENT, watchComponentName)
component = ComponentName(
ApplicationProvider.getApplicationContext<Context>(),
OnWatchFaceEditingTestActivity::class.java
)
}
)
}
return ActivityScenario.launch(
WatchFaceEditorContract().createIntent(
ApplicationProvider.getApplicationContext<Context>(),
EditorRequest(
watchComponentName,
testEditorPackageName,
initialUserStyle,
watchFaceId,
headlessDeviceConfig,
previewScreenshotParams
)
).apply {
component = ComponentName(
ApplicationProvider.getApplicationContext<Context>(),
OnWatchFaceEditingTestActivity::class.java
)
}
)
}
private fun createOnWatchFaceEditingTestActivityThatThrowsTimeoutException():
ActivityScenario<OnWatchFaceEditingTestActivity> =
createOnWatchFaceEditingTestActivity(
emptyList(), emptyList(), /* other params are default */ shouldTimeout = true
)
@After
public fun tearDown() {
OnWatchFaceEditingTestActivity.complicationDataSourceInfoRetrieverProvider = null
ComplicationDataSourceChooserContract.useTestComplicationHelperActivity = false
ComplicationHelperActivity.useTestComplicationDataSourceChooserActivity = false
ComplicationHelperActivity.skipPermissionCheck = false
WatchFace.clearAllEditorDelegates()
EditorService.globalEditorService.clearCloseCallbacks()
}
@Test
public fun createOnWatchEditingSessionThrowsTimeoutException() {
val scenario = createOnWatchFaceEditingTestActivityThatThrowsTimeoutException()
lateinit var activity: OnWatchFaceEditingTestActivity
scenario.onActivity { activity = it }
activity.creationLatch.await(EDITING_SESSION_TIMEOUT.toMillis() + 500, MILLISECONDS)
assert(activity.onCreateException is TimeoutCancellationException)
}
@Test
public fun watchFaceComponentName() {
val scenario = createOnWatchFaceEditingTestActivity(emptyList(), emptyList())
scenario.onActivity {
assertThat(it.editorSession.watchFaceComponentName)
.isEqualTo(ComponentName("test.package", "test.class"))
}
}
@Test
public fun watchFaceComponentName_headless() {
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
emptyList(),
headlessDeviceConfig = DeviceConfig(false, false, 0, 0),
watchComponentName = headlessWatchFaceComponentName
)
lateinit var activity: OnWatchFaceEditingTestActivity
scenario.onActivity {
activity = it
}
runBlocking {
activity.deferredDone.await()
}
assertThat(activity.editorSession.watchFaceComponentName)
.isEqualTo(headlessWatchFaceComponentName)
}
@Test
public fun instanceId() {
val scenario = createOnWatchFaceEditingTestActivity(emptyList(), emptyList())
scenario.onActivity {
assertThat(it.editorSession.watchFaceId.id).isEqualTo(testInstanceId.id)
}
}
@Test
public fun instanceId_headless() {
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
emptyList(),
headlessDeviceConfig = DeviceConfig(false, false, 0, 0),
watchComponentName = headlessWatchFaceComponentName
)
lateinit var activity: OnWatchFaceEditingTestActivity
scenario.onActivity {
activity = it
}
runBlocking {
activity.deferredDone.await()
}
assertThat(activity.editorSession.watchFaceId.id).isEqualTo(testInstanceId.id)
}
@Test
public fun backgroundComplicationId_noBackgroundComplication() {
val scenario = createOnWatchFaceEditingTestActivity(emptyList(), emptyList())
scenario.onActivity {
assertThat(it.editorSession.backgroundComplicationSlotId).isEqualTo(null)
}
}
@Test
public fun previewReferenceInstant() {
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
emptyList(),
previewReferenceInstant = Instant.ofEpochMilli(54321L)
)
scenario.onActivity {
assertThat(it.editorSession.previewReferenceInstant).isEqualTo(
Instant.ofEpochMilli(54321L)
)
}
}
@Test
public fun userStyleSchema() {
val scenario = createOnWatchFaceEditingTestActivity(
listOf(colorStyleSetting, watchHandStyleSetting),
emptyList()
)
scenario.onActivity {
val userStyleSchema = it.editorSession.userStyleSchema
assertThat(userStyleSchema.userStyleSettings.size).isEqualTo(2)
assertThat(userStyleSchema.userStyleSettings[0].id.value)
.isEqualTo(colorStyleSetting.id.value)
assertThat(userStyleSchema.userStyleSettings[0].options.size)
.isEqualTo(colorStyleSetting.options.size)
assertThat(userStyleSchema.userStyleSettings[1].id.value)
.isEqualTo(watchHandStyleSetting.id.value)
assertThat(userStyleSchema.userStyleSettings[1].options.size)
.isEqualTo(watchHandStyleSetting.options.size)
// We could test more state but this should be enough.
}
}
@Test
public fun backgroundComplicationId() {
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
listOf(backgroundComplication)
)
scenario.onActivity {
assertThat(it.editorSession.backgroundComplicationSlotId).isEqualTo(
BACKGROUND_COMPLICATION_ID
)
}
}
@Test
@Suppress("DEPRECATION") // defaultDataSourceType
public fun complicationState() {
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
listOf(leftComplication, rightComplication, backgroundComplication)
)
scenario.onActivity {
val complicationSlotsState = it.editorSession.complicationSlotsState.value
assertThat(complicationSlotsState.size).isEqualTo(3)
val leftSlot = complicationSlotsState[LEFT_COMPLICATION_ID]!!
assertThat(leftSlot.bounds).isEqualTo(Rect(120, 160, 160, 240))
assertThat(leftSlot.boundsType).isEqualTo(ComplicationSlotBoundsType.ROUND_RECT)
assertFalse(leftSlot.fixedComplicationDataSource)
assertTrue(leftSlot.isInitiallyEnabled)
assertThat(leftSlot.defaultDataSourcePolicy.primaryDataSource)
.isEqualTo(ComponentName("com.primary.package", "com.primary.app"))
assertThat(leftSlot.defaultDataSourcePolicy.primaryDataSourceDefaultType)
.isEqualTo(ComplicationType.LONG_TEXT)
assertThat(leftSlot.defaultDataSourcePolicy.secondaryDataSource).isNull()
assertThat(leftSlot.defaultDataSourcePolicy.secondaryDataSourceDefaultType)
.isNull()
assertThat(leftSlot.defaultDataSourcePolicy.systemDataSourceFallback).isEqualTo(
SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET
)
assertThat(
leftSlot.defaultDataSourcePolicy.systemDataSourceFallbackDefaultType
).isEqualTo(ComplicationType.SHORT_TEXT)
assertThat(leftSlot.defaultDataSourceType).isEqualTo(ComplicationType.SHORT_TEXT)
val rightSlot = complicationSlotsState[RIGHT_COMPLICATION_ID]!!
assertThat(rightSlot.bounds).isEqualTo(Rect(240, 160, 280, 240))
assertThat(rightSlot.boundsType).isEqualTo(ComplicationSlotBoundsType.ROUND_RECT)
assertFalse(rightSlot.fixedComplicationDataSource)
assertTrue(rightSlot.isInitiallyEnabled)
assertThat(rightSlot.defaultDataSourcePolicy.primaryDataSource)
.isEqualTo(ComponentName("com.primary.package", "com.primary.app"))
assertThat(rightSlot.defaultDataSourcePolicy.primaryDataSourceDefaultType)
.isEqualTo(ComplicationType.LONG_TEXT)
assertThat(rightSlot.defaultDataSourcePolicy.secondaryDataSource)
.isEqualTo(ComponentName("com.secondary.package", "com.secondary.app"))
assertThat(rightSlot.defaultDataSourcePolicy.secondaryDataSourceDefaultType)
.isEqualTo(ComplicationType.PHOTO_IMAGE)
assertThat(
rightSlot.defaultDataSourcePolicy.systemDataSourceFallbackDefaultType
).isEqualTo(ComplicationType.SHORT_TEXT)
assertThat(rightSlot.defaultDataSourceType).isEqualTo(ComplicationType.SHORT_TEXT)
val backgroundSlot = complicationSlotsState[BACKGROUND_COMPLICATION_ID]!!
assertThat(backgroundSlot.bounds).isEqualTo(screenBounds)
assertThat(backgroundSlot.boundsType).isEqualTo(ComplicationSlotBoundsType.BACKGROUND)
assertFalse(backgroundSlot.fixedComplicationDataSource)
assertFalse(backgroundSlot.isInitiallyEnabled)
// We could test more state but this should be enough.
}
}
@Suppress("DEPRECATION") // Old DefaultComplicationDataSourcePolicy constructor
@Test
public fun fixedComplicationDataSource() {
val mockLeftCanvasComplication =
CanvasComplicationDrawable(
ComplicationDrawable(),
placeholderWatchState,
mockInvalidateCallback
)
val fixedLeftComplication =
ComplicationSlot.createRoundRectComplicationSlotBuilder(
LEFT_COMPLICATION_ID,
{ _, _ -> mockLeftCanvasComplication },
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)
.setFixedComplicationDataSource(true)
.build()
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
listOf(fixedLeftComplication)
)
scenario.onActivity {
assertTrue(
it.editorSession.complicationSlotsState.value[
LEFT_COMPLICATION_ID
]!!.fixedComplicationDataSource
)
try {
runBlocking {
it.editorSession.openComplicationDataSourceChooser(LEFT_COMPLICATION_ID)
fail(
"openComplicationDataSourceChooser should fail for a fixed complication " +
"data source"
)
}
} catch (e: Exception) {
// Expected.
}
}
}
@Test
public fun getPreviewData_null_dataSourceInfo() {
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
listOf(leftComplication, rightComplication, backgroundComplication)
)
scenario.onActivity {
runBlocking {
val editorSession = it.editorSession as OnWatchFaceEditorSessionImpl
val mockProviderInfoService = Mockito.mock(IProviderInfoService::class.java)
val dataSourceInfoRetriever =
ComplicationDataSourceInfoRetriever(mockProviderInfoService)
assertThat(
editorSession.getPreviewData(dataSourceInfoRetriever, null)
).isNull()
dataSourceInfoRetriever.close()
}
}
}
@Test
public fun getPreviewData() {
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
listOf(leftComplication, rightComplication, backgroundComplication)
)
scenario.onActivity {
runBlocking {
val editorSession = it.editorSession as OnWatchFaceEditorSessionImpl
val mockProviderInfoService = Mockito.mock(IProviderInfoService::class.java)
val complicationType = ComplicationType.SHORT_TEXT
val complicationText = "TestText"
val mockBinder = Mockito.mock(IBinder::class.java)
`when`(mockProviderInfoService.apiVersion).thenReturn(1)
`when`(mockProviderInfoService.asBinder()).thenReturn(mockBinder)
doAnswer {
val callback = it.arguments[2] as IPreviewComplicationDataCallback
callback.updateComplicationData(
ShortTextComplicationData.Builder(
PlainComplicationText.Builder(complicationText).build(),
ComplicationText.EMPTY
).build().asWireComplicationData()
)
true
}.`when`(mockProviderInfoService).requestPreviewComplicationData(
eq(dataSourceComponentName),
eq(complicationType.toWireComplicationType()),
any()
)
val complicationDataSourceInfoRetriever =
ComplicationDataSourceInfoRetriever(mockProviderInfoService)
val deferredPreviewData = async {
editorSession.getPreviewData(
complicationDataSourceInfoRetriever,
ComplicationDataSourceInfo(
"dataSource.app",
"dataSource",
dataSourceIcon,
complicationType,
dataSourceComponentName
)
)
}
val result = deferredPreviewData.await()
assertThat(result).isInstanceOf(ShortTextComplicationData::class.java)
assertThat(
(result as ShortTextComplicationData).text.getTextAt(
ApplicationProvider.getApplicationContext<Context>().resources,
Instant.EPOCH
)
).isEqualTo(complicationText)
complicationDataSourceInfoRetriever.close()
}
}
}
@Test
public fun getPreviewData_dataSourceSendsWrongType() {
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
listOf(leftComplication, rightComplication, backgroundComplication)
)
scenario.onActivity {
runBlocking {
val editorSession = it.editorSession as OnWatchFaceEditorSessionImpl
val mockProviderInfoService = Mockito.mock(IProviderInfoService::class.java)
val complicationType = ComplicationType.LONG_TEXT
val complicationText = "TestText"
val mockBinder = Mockito.mock(IBinder::class.java)
`when`(mockProviderInfoService.apiVersion).thenReturn(1)
`when`(mockProviderInfoService.asBinder()).thenReturn(mockBinder)
doAnswer {
val callback = it.arguments[2] as IPreviewComplicationDataCallback
callback.updateComplicationData(
ShortTextComplicationData.Builder(
PlainComplicationText.Builder(complicationText).build(),
ComplicationText.EMPTY
).build().asWireComplicationData()
)
true
}.`when`(mockProviderInfoService).requestPreviewComplicationData(
eq(dataSourceComponentName),
eq(complicationType.toWireComplicationType()),
any()
)
val complicationDataSourceInfoRetriever =
ComplicationDataSourceInfoRetriever(mockProviderInfoService)
val deferredPreviewData = async {
editorSession.getPreviewData(
complicationDataSourceInfoRetriever,
ComplicationDataSourceInfo(
"dataSource.app",
"dataSource",
dataSourceIcon,
complicationType,
dataSourceComponentName
)
)
}
val result = deferredPreviewData.await()
assertThat(result).isInstanceOf(LongTextComplicationData::class.java)
assertThat(
(result as LongTextComplicationData).text.getTextAt(
ApplicationProvider.getApplicationContext<Context>().resources,
Instant.EPOCH
)
).isEqualTo("dataSource") // Fallback has been used.
complicationDataSourceInfoRetriever.close()
}
}
}
@Test
@SdkSuppress(maxSdkVersion = 28)
public fun getPreviewData_preRFallback() {
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
listOf(leftComplication, rightComplication, backgroundComplication)
)
scenario.onActivity {
runBlocking {
val editorSession = it.editorSession as OnWatchFaceEditorSessionImpl
val mockProviderInfoService = Mockito.mock(IProviderInfoService::class.java)
val complicationType = ComplicationType.SHORT_TEXT
val complicationDataSourceInfoRetriever =
ComplicationDataSourceInfoRetriever(mockProviderInfoService)
val previewComplication = editorSession.getPreviewData(
complicationDataSourceInfoRetriever,
// Construct a ComplicationDataSourceInfo with null componentName.
ComplicationDataSourceInfo(
"dataSource.app",
"dataSource",
dataSourceIcon,
complicationType,
null,
)
) as ShortTextComplicationData
assertThat(
previewComplication.text.getTextAt(
ApplicationProvider.getApplicationContext<Context>().resources,
Instant.EPOCH
)
).isEqualTo("dataSou")
complicationDataSourceInfoRetriever.close()
}
}
}
@Test
@SdkSuppress(minSdkVersion = 30)
public fun getPreviewData_postRFallback() {
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
listOf(leftComplication, rightComplication, backgroundComplication)
)
scenario.onActivity {
runBlocking {
val editorSession = it.editorSession as OnWatchFaceEditorSessionImpl
val mockProviderInfoService = Mockito.mock(IProviderInfoService::class.java)
val complicationType = ComplicationType.SHORT_TEXT
`when`(mockProviderInfoService.apiVersion).thenReturn(1)
`when`(
mockProviderInfoService.requestPreviewComplicationData(
eq(dataSourceComponentName),
eq(complicationType.toWireComplicationType()),
any(IPreviewComplicationDataCallback::class.java)
)
).thenReturn(false) // Triggers the ExecutionException.
val previewComplication = editorSession.getPreviewData(
ComplicationDataSourceInfoRetriever(mockProviderInfoService),
ComplicationDataSourceInfo(
"dataSource.app",
"dataSource",
dataSourceIcon,
complicationType,
dataSourceComponentName
)
) as ShortTextComplicationData
assertThat(
previewComplication.text.getTextAt(
ApplicationProvider.getApplicationContext<Context>().resources,
Instant.EPOCH
)
).isEqualTo("dataSou") // Fallback truncates for short text.
}
}
}
@Test
public fun launchComplicationDataSourceChooser() {
ComplicationDataSourceChooserContract.useTestComplicationHelperActivity = true
val chosenComplicationDataSourceInfo = ComplicationDataSourceInfo(
"TestDataSource3App",
"TestDataSource3",
Icon.createWithBitmap(
Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
),
ComplicationType.LONG_TEXT,
dataSource3
)
TestComplicationHelperActivity.resultIntent = CompletableDeferred(
Intent().apply {
putExtra(
ComplicationDataSourceChooserIntent.EXTRA_PROVIDER_INFO,
chosenComplicationDataSourceInfo.toWireComplicationProviderInfo()
)
}
)
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
listOf(leftComplication, rightComplication)
)
lateinit var editorSession: EditorSession
scenario.onActivity { activity ->
editorSession = activity.editorSession
}
runBlocking {
assertThat(
editorSession.complicationSlotsState.value[LEFT_COMPLICATION_ID]!!.bounds
).isEqualTo(Rect(120, 160, 160, 240))
assertThat(
editorSession.complicationsDataSourceInfo.value[LEFT_COMPLICATION_ID]!!.name
).isEqualTo("DataSource1")
/**
* Invoke [TestComplicationHelperActivity] which will change the complication data
* source (and hence the preview data) for [LEFT_COMPLICATION_ID].
*/
val chosenComplicationDataSource =
editorSession.openComplicationDataSourceChooser(LEFT_COMPLICATION_ID)
assertThat(chosenComplicationDataSource).isNotNull()
checkNotNull(chosenComplicationDataSource)
assertThat(chosenComplicationDataSource.complicationSlotId)
.isEqualTo(LEFT_COMPLICATION_ID)
assertEquals(
chosenComplicationDataSourceInfo,
chosenComplicationDataSource.complicationDataSourceInfo
)
// This should update the preview data to point to the updated DataSource3 data.
val previewComplication =
editorSession.complicationsPreviewData.value[LEFT_COMPLICATION_ID]
as LongTextComplicationData
assertThat(
previewComplication.text.getTextAt(
ApplicationProvider.getApplicationContext<Context>().resources,
Instant.EPOCH
)
).isEqualTo("DataSource3")
assertThat(
editorSession.complicationsDataSourceInfo.value[LEFT_COMPLICATION_ID]!!.name
).isEqualTo("TestDataSource3")
assertThat(
TestComplicationHelperActivity.lastIntent?.extras?.getString(
ComplicationDataSourceChooserIntent.EXTRA_WATCHFACE_INSTANCE_ID
)
).isEqualTo(testInstanceId.id)
assertThat(
(TestComplicationHelperActivity.lastIntent?.getParcelableExtra(
ComplicationDataSourceChooserIntent.EXTRA_COMPLICATION_DENIED
) as Intent?)?.action
).isEqualTo(
complicationDeniedDialogIntent.action
)
assertThat(
(TestComplicationHelperActivity.lastIntent?.getParcelableExtra(
ComplicationDataSourceChooserIntent.EXTRA_COMPLICATION_RATIONALE
) as Intent?)?.action
).isEqualTo(
complicationRationaleDialogIntent.action
)
// Selecting a LONG_TEXT complication should enlarge the complication's bounds due to
// it's set up.
assertThat(
editorSession.complicationSlotsState.value[LEFT_COMPLICATION_ID]!!.bounds
).isEqualTo(Rect(40, 160, 160, 240))
}
}
@Test
public fun launchComplicationDataSourceChooserTwiceBackToBack() {
ComplicationDataSourceChooserContract.useTestComplicationHelperActivity = true
TestComplicationHelperActivity.resultIntent = CompletableDeferred(
Intent().apply {
putExtra(
ComplicationDataSourceChooserIntent.EXTRA_PROVIDER_INFO,
ComplicationDataSourceInfo(
"TestDataSource3App",
"TestDataSource3",
Icon.createWithBitmap(
Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
),
ComplicationType.LONG_TEXT,
dataSource3
).toWireComplicationProviderInfo()
)
}
)
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
listOf(leftComplication, rightComplication)
)
lateinit var editorSession: EditorSession
scenario.onActivity { activity ->
editorSession = activity.editorSession
}
runBlocking {
assertThat(editorSession.openComplicationDataSourceChooser(LEFT_COMPLICATION_ID))
.isNotNull()
// This shouldn't crash.
assertThat(editorSession.openComplicationDataSourceChooser(LEFT_COMPLICATION_ID))
.isNotNull()
}
}
@Test
public fun launchConcurrentComplicationDataSourceChoosers() {
ComplicationDataSourceChooserContract.useTestComplicationHelperActivity = true
TestComplicationHelperActivity.resultIntent = CompletableDeferred()
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
listOf(leftComplication, rightComplication)
)
lateinit var editorSession: EditorSession
scenario.onActivity { activity ->
editorSession = activity.editorSession
}
runBlocking {
// This won't complete till later.
val firstComplicationDataSourceChooserResult = async {
editorSession.openComplicationDataSourceChooser(LEFT_COMPLICATION_ID)
}
async {
try {
editorSession.openComplicationDataSourceChooser(LEFT_COMPLICATION_ID)
fail("A concurrent openComplicationDataSourceChooser should throw an exception")
} catch (e: Exception) {
assertThat(e).isInstanceOf(IllegalStateException::class.java)
}
// Allow firstComplicationDataSourceChooserResult to complete.
TestComplicationHelperActivity.resultIntent!!.complete(
Intent().apply {
putExtra(
ComplicationDataSourceChooserIntent.EXTRA_PROVIDER_INFO,
ComplicationDataSourceInfo(
"TestDataSource3App",
"TestDataSource3",
Icon.createWithBitmap(
Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
),
ComplicationType.LONG_TEXT,
dataSource3
).toWireComplicationProviderInfo()
)
}
)
}
assertThat(firstComplicationDataSourceChooserResult.await()).isNotNull()
}
}
@Test
public fun launchComplicationDataSourceChooser_chooseEmpty() {
ComplicationDataSourceChooserContract.useTestComplicationHelperActivity = true
TestComplicationHelperActivity.resultIntent = CompletableDeferred(Intent())
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
listOf(leftComplication, rightComplication)
)
lateinit var editorSession: EditorSession
scenario.onActivity { activity ->
editorSession = activity.editorSession
}
runBlocking {
/**
* Invoke [TestComplicationHelperActivity] which will change the complication data
* source (and hence the preview data) for [LEFT_COMPLICATION_ID].
*/
val chosenComplicationDataSource =
editorSession.openComplicationDataSourceChooser(LEFT_COMPLICATION_ID)
assertThat(chosenComplicationDataSource).isNotNull()
checkNotNull(chosenComplicationDataSource)
assertThat(chosenComplicationDataSource.complicationSlotId)
.isEqualTo(LEFT_COMPLICATION_ID)
assertThat(chosenComplicationDataSource.complicationDataSourceInfo).isNull()
assertThat(editorSession.complicationsPreviewData.value[LEFT_COMPLICATION_ID])
.isInstanceOf(EmptyComplicationData::class.java)
}
}
@Test
public fun launchComplicationDataSourceChooser_cancel() {
ComplicationDataSourceChooserContract.useTestComplicationHelperActivity = true
// NB CompletableDeferred(null) doesn't do what we expect...
TestComplicationHelperActivity.resultIntent = CompletableDeferred<Intent?>().apply {
complete(null)
}
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
listOf(leftComplication, rightComplication)
)
lateinit var editorSession: EditorSession
scenario.onActivity { activity ->
editorSession = activity.editorSession
}
runBlocking {
/**
* Invoke [TestComplicationHelperActivity] which will simulate the user canceling.
*/
assertThat(editorSession.openComplicationDataSourceChooser(LEFT_COMPLICATION_ID))
.isNull()
}
}
@Test
public fun launchComplicationDataSourceChooser_ComplicationConfigExtrasToHelper() {
ComplicationDataSourceChooserContract.useTestComplicationHelperActivity = true
val chosenComplicationDataSourceInfo = ComplicationDataSourceInfo(
"TestDataSource3App",
"TestDataSource3",
Icon.createWithBitmap(
Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
),
ComplicationType.LONG_TEXT,
dataSource3
)
TestComplicationHelperActivity.resultIntent = CompletableDeferred(
Intent().apply {
putExtra(
ComplicationDataSourceChooserIntent.EXTRA_PROVIDER_INFO,
chosenComplicationDataSourceInfo.toWireComplicationProviderInfo()
)
putExtra(PROVIDER_CHOOSER_RESULT_EXTRA_KEY, PROVIDER_CHOOSER_RESULT_EXTRA_VALUE)
}
)
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
listOf(leftComplication, rightComplication)
)
lateinit var editorSession: EditorSession
scenario.onActivity { activity ->
editorSession = activity.editorSession
}
runBlocking {
val chosenComplicationDataSource =
editorSession.openComplicationDataSourceChooser(RIGHT_COMPLICATION_ID)
assertThat(chosenComplicationDataSource).isNotNull()
checkNotNull(chosenComplicationDataSource)
assertThat(chosenComplicationDataSource.complicationSlotId)
.isEqualTo(RIGHT_COMPLICATION_ID)
assertEquals(
chosenComplicationDataSourceInfo,
chosenComplicationDataSource.complicationDataSourceInfo
)
assertThat(
chosenComplicationDataSource.extras[PROVIDER_CHOOSER_RESULT_EXTRA_KEY]
).isEqualTo(PROVIDER_CHOOSER_RESULT_EXTRA_VALUE)
assertThat(
TestComplicationHelperActivity.lastIntent?.extras?.getString(
PROVIDER_CHOOSER_EXTRA_KEY
)
).isEqualTo(PROVIDER_CHOOSER_EXTRA_VALUE)
}
}
@Test
public fun launchComplicationDataSourceChooser_ComplicationConfigExtrasToChooser() {
// Invoke the test data source chooser to record the result.
ComplicationHelperActivity.useTestComplicationDataSourceChooserActivity = true
// Invoke the data source chooser without checking for permissions first.
ComplicationHelperActivity.skipPermissionCheck = true
val chosenComplicationDataSourceInfo = ComplicationDataSourceInfo(
"TestDataSource3App",
"TestDataSource3",
Icon.createWithBitmap(
Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
),
ComplicationType.LONG_TEXT,
dataSource3
)
TestComplicationDataSourceChooserActivity.resultIntent = Intent().apply {
putExtra(
ComplicationDataSourceChooserIntent.EXTRA_PROVIDER_INFO,
chosenComplicationDataSourceInfo.toWireComplicationProviderInfo()
)
putExtra(PROVIDER_CHOOSER_RESULT_EXTRA_KEY, PROVIDER_CHOOSER_RESULT_EXTRA_VALUE)
}
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
listOf(leftComplication, rightComplication)
)
lateinit var editorSession: EditorSession
scenario.onActivity { activity ->
editorSession = activity.editorSession
}
runBlocking {
val chosenComplicationDataSource =
editorSession.openComplicationDataSourceChooser(RIGHT_COMPLICATION_ID)
assertThat(chosenComplicationDataSource).isNotNull()
checkNotNull(chosenComplicationDataSource)
assertThat(chosenComplicationDataSource.complicationSlotId)
.isEqualTo(RIGHT_COMPLICATION_ID)
assertEquals(
chosenComplicationDataSourceInfo,
chosenComplicationDataSource.complicationDataSourceInfo
)
assertThat(
chosenComplicationDataSource.extras[PROVIDER_CHOOSER_RESULT_EXTRA_KEY]
).isEqualTo(PROVIDER_CHOOSER_RESULT_EXTRA_VALUE)
assertThat(
TestComplicationDataSourceChooserActivity.lastIntent?.extras?.getString(
PROVIDER_CHOOSER_EXTRA_KEY
)
).isEqualTo(PROVIDER_CHOOSER_EXTRA_VALUE)
}
}
@Test
public fun mutable_configExtras() {
// Invoke the test data source chooser to record the result.
ComplicationHelperActivity.useTestComplicationDataSourceChooserActivity = true
// Invoke the data source chooser without checking for permissions first.
ComplicationHelperActivity.skipPermissionCheck = true
val chosenComplicationDataSourceInfo = ComplicationDataSourceInfo(
"TestDataSource3App",
"TestDataSource3",
Icon.createWithBitmap(
Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
),
ComplicationType.LONG_TEXT,
dataSource3
)
TestComplicationDataSourceChooserActivity.lastIntent = null
TestComplicationDataSourceChooserActivity.resultIntent = Intent().apply {
putExtra(
ComplicationDataSourceChooserIntent.EXTRA_PROVIDER_INFO,
chosenComplicationDataSourceInfo.toWireComplicationProviderInfo()
)
}
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
listOf(leftComplication, rightComplication)
)
lateinit var editorSession: EditorSession
scenario.onActivity { activity ->
editorSession = activity.editorSession
}
runBlocking {
rightComplication.configExtras = Bundle().apply {
putString(PROVIDER_CHOOSER_EXTRA_KEY, "Updated")
}
val chosenComplicationDataSource =
editorSession.openComplicationDataSourceChooser(RIGHT_COMPLICATION_ID)
assertThat(chosenComplicationDataSource).isNotNull()
assertThat(
TestComplicationDataSourceChooserActivity.lastIntent?.extras?.getString(
PROVIDER_CHOOSER_EXTRA_KEY
)
).isEqualTo("Updated")
}
}
@Test
public fun getComplicationIdAt() {
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
listOf(leftComplication, rightComplication, backgroundComplication)
)
scenario.onActivity {
assertThat(it.editorSession.getComplicationSlotIdAt(0, 0)).isEqualTo(null)
assertThat(it.editorSession.getComplicationSlotIdAt(125, 165))
.isEqualTo(LEFT_COMPLICATION_ID)
assertThat(it.editorSession.getComplicationSlotIdAt(245, 165))
.isEqualTo(RIGHT_COMPLICATION_ID)
}
}
@Test
public fun renderWatchFaceToBitmap() {
val scenario = createOnWatchFaceEditingTestActivity(emptyList(), emptyList())
scenario.onActivity {
assertThat(
it.editorSession.renderWatchFaceToBitmap(
RenderParameters.DEFAULT_INTERACTIVE,
Instant.ofEpochMilli(1234L),
null
)
).isEqualTo(fakeBitmap)
}
}
@Test
public fun initialUserStyle() {
val scenario = createOnWatchFaceEditingTestActivity(
listOf(colorStyleSetting, watchHandStyleSetting),
listOf(leftComplication, rightComplication),
initialUserStyle = UserStyleData(
mapOf(
colorStyleSetting.id.value to greenStyleOption.id.value,
watchHandStyleSetting.id.value to modernStyleOption.id.value,
)
)
)
scenario.onActivity { activity ->
assertThat(activity.editorSession.userStyle.value[colorStyleSetting])
.isEqualTo(greenStyleOption)
assertThat(activity.editorSession.userStyle.value[watchHandStyleSetting])
.isEqualTo(modernStyleOption)
}
}
@Suppress("NewApi") // result.watchFaceId
@Test
public fun userStyleAndComplicationPreviewDataInEditorObserver() {
val scenario = createOnWatchFaceEditingTestActivity(
listOf(colorStyleSetting, watchHandStyleSetting),
listOf(leftComplication, rightComplication)
)
val editorObserver = TestEditorObserver()
val observerId = EditorService.globalEditorService.registerObserver(editorObserver)
scenario.onActivity { activity ->
// Select [blueStyleOption] and [gothicStyleOption].
val mutableUserStyle = activity.editorSession.userStyle.value.toMutableUserStyle()
for (userStyleSetting in activity.editorSession.userStyleSchema.userStyleSettings) {
mutableUserStyle[userStyleSetting] = userStyleSetting.options.last()
}
activity.editorSession.userStyle.value = mutableUserStyle.toUserStyle()
activity.editorSession.close()
activity.finish()
}
val result = editorObserver.awaitEditorStateChange(
TIMEOUT_MILLIS,
TimeUnit.MILLISECONDS
).asApiEditorState()
assertThat(result.userStyle.userStyleMap[colorStyleSetting.id.value])
.isEqualTo(blueStyleOption.id.value)
assertThat(result.userStyle.userStyleMap[watchHandStyleSetting.id.value])
.isEqualTo(gothicStyleOption.id.value)
assertThat(result.watchFaceId.id).isEqualTo(testInstanceId.id)
assertTrue(result.shouldCommitChanges)
// The style change should also be applied to the watchface.
assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
.isEqualTo(blueStyleOption.id.value)
assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
.isEqualTo(gothicStyleOption.id.value)
assertThat(result.previewComplicationsData.size).isEqualTo(2)
val leftComplicationData = result.previewComplicationsData[LEFT_COMPLICATION_ID] as
ShortTextComplicationData
assertThat(
leftComplicationData.text.getTextAt(
ApplicationProvider.getApplicationContext<Context>().resources,
Instant.EPOCH
)
).isEqualTo("Left")
val rightComplicationData = result.previewComplicationsData[RIGHT_COMPLICATION_ID] as
LongTextComplicationData
assertThat(
rightComplicationData.text.getTextAt(
ApplicationProvider.getApplicationContext<Context>().resources,
Instant.EPOCH
)
).isEqualTo("Right")
EditorService.globalEditorService.unregisterObserver(observerId)
}
@Suppress("NewApi") // result.watchFaceId
@Test
public fun editorStatePreviewComplicationData_onlyContainsEnabledComplications() {
val scenario = createOnWatchFaceEditingTestActivity(
listOf(complicationsStyleSetting),
listOf(leftComplication, rightComplication)
)
val editorObserver = TestEditorObserver()
val observerId = EditorService.globalEditorService.registerObserver(editorObserver)
scenario.onActivity { activity ->
// Hide the left complication.
val mutableUserStyle = activity.editorSession.userStyle.value.toMutableUserStyle()
mutableUserStyle[complicationsStyleSetting] = rightOnlyComplicationsOption
activity.editorSession.userStyle.value = mutableUserStyle.toUserStyle()
activity.editorSession.close()
activity.finish()
}
val result = editorObserver.awaitEditorStateChange(
TIMEOUT_MILLIS,
TimeUnit.MILLISECONDS
).asApiEditorState()
assertThat(result.previewComplicationsData.size).isEqualTo(1)
assertThat(result.previewComplicationsData[RIGHT_COMPLICATION_ID]).isNotNull()
EditorService.globalEditorService.unregisterObserver(observerId)
}
@SuppressLint("NewApi") // result.watchFaceId
@Test
public fun emptyInstanceId() {
val scenario = createOnWatchFaceEditingTestActivity(
listOf(colorStyleSetting, watchHandStyleSetting),
emptyList(),
watchFaceId = WatchFaceId("")
)
val editorObserver = TestEditorObserver()
val observerId = EditorService.globalEditorService.registerObserver(editorObserver)
scenario.onActivity { activity ->
runBlocking {
assertThat(activity.editorSession.watchFaceId.id).isEmpty()
activity.editorSession.close()
activity.finish()
}
}
val result = editorObserver.awaitEditorStateChange(
TIMEOUT_MILLIS,
TimeUnit.MILLISECONDS
).asApiEditorState()
assertThat(result.watchFaceId.id).isEmpty()
EditorService.globalEditorService.unregisterObserver(observerId)
}
@Test
public fun emptyComplicationPreviewDataInEditorObserver() {
val scenario = createOnWatchFaceEditingTestActivity(emptyList(), emptyList())
val editorObserver = TestEditorObserver()
val observerId = EditorService.globalEditorService.registerObserver(editorObserver)
scenario.onActivity { activity ->
runBlocking {
activity.editorSession.close()
activity.finish()
}
}
val result = editorObserver.awaitEditorStateChange(
TIMEOUT_MILLIS,
TimeUnit.MILLISECONDS
).asApiEditorState()
assertThat(result.previewComplicationsData).isEmpty()
EditorService.globalEditorService.unregisterObserver(observerId)
}
@Test
public fun commit_headless() {
val scenario = createOnWatchFaceEditingTestActivity(
listOf(colorStyleSetting, watchHandStyleSetting),
emptyList(),
headlessDeviceConfig = DeviceConfig(false, false, 0, 0),
watchComponentName = headlessWatchFaceComponentName
)
val editorObserver = TestEditorObserver()
val observerId = EditorService.globalEditorService.registerObserver(editorObserver)
scenario.onActivity { activity ->
runBlocking {
activity.deferredDone.await()
}
assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
.isEqualTo(redStyleOption.id.value)
assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
.isEqualTo(classicStyleOption.id.value)
// Select [blueStyleOption] and [gothicStyleOption].
val mutableUserStyle = activity.editorSession.userStyle.value.toMutableUserStyle()
for (userStyleSetting in activity.editorSession.userStyleSchema.userStyleSettings) {
mutableUserStyle[userStyleSetting] = userStyleSetting.options.last()
}
activity.editorSession.userStyle.value = mutableUserStyle.toUserStyle()
// The editorDelegate should be unaffected because a separate headless instance is
// used.
assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
.isEqualTo(redStyleOption.id.value)
assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
.isEqualTo(classicStyleOption.id.value)
activity.editorSession.close()
activity.finish()
}
val result = editorObserver.awaitEditorStateChange(
TIMEOUT_MILLIS,
TimeUnit.MILLISECONDS
).asApiEditorState()
assertThat(result.userStyle.userStyleMap[colorStyleSetting.id.value])
.isEqualTo(blueStyleOption.id.value)
assertThat(result.userStyle.userStyleMap[watchHandStyleSetting.id.value])
.isEqualTo(gothicStyleOption.id.value)
assertTrue(result.shouldCommitChanges)
assertNull(result.previewImage)
EditorService.globalEditorService.unregisterObserver(observerId)
}
@SuppressLint("NewApi")
@Test
public fun commitWithPreviewImage() {
val scenario = createOnWatchFaceEditingTestActivity(
listOf(colorStyleSetting, watchHandStyleSetting),
emptyList(),
headlessDeviceConfig = DeviceConfig(false, false, 0, 0),
watchComponentName = headlessWatchFaceComponentName,
previewScreenshotParams = PreviewScreenshotParams(
RenderParameters.DEFAULT_INTERACTIVE,
Instant.EPOCH
)
)
val editorObserver = TestEditorObserver()
val observerId = EditorService.globalEditorService.registerObserver(editorObserver)
scenario.onActivity { activity ->
runBlocking {
activity.deferredDone.await()
}
// Select [blueStyleOption] and [gothicStyleOption].
val mutableUserStyle = activity.editorSession.userStyle.value.toMutableUserStyle()
for (userStyleSetting in activity.editorSession.userStyleSchema.userStyleSettings) {
mutableUserStyle[userStyleSetting] = userStyleSetting.options.last()
}
activity.editorSession.userStyle.value = mutableUserStyle.toUserStyle()
activity.editorSession.close()
activity.finish()
}
val result = editorObserver.awaitEditorStateChange(
TIMEOUT_MILLIS,
TimeUnit.MILLISECONDS
).asApiEditorState()
// previewImage is only supported from API 27 onwards.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
assertNotNull(result.previewImage)
assertThat(result.previewImage!!.getPixel(0, 0)).isEqualTo(Color.BLUE)
} else {
assertNull(result.previewImage)
}
EditorService.globalEditorService.unregisterObserver(observerId)
}
@Test
public fun doNotCommit() {
val scenario = createOnWatchFaceEditingTestActivity(
listOf(colorStyleSetting, watchHandStyleSetting),
emptyList(),
previewScreenshotParams = PreviewScreenshotParams(
RenderParameters.DEFAULT_INTERACTIVE,
Instant.EPOCH
)
)
val editorObserver = TestEditorObserver()
val observerId = EditorService.globalEditorService.registerObserver(editorObserver)
scenario.onActivity { activity ->
assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
.isEqualTo(redStyleOption.id.value)
assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
.isEqualTo(classicStyleOption.id.value)
// Select [blueStyleOption] and [gothicStyleOption].
val mutableUserStyle = activity.editorSession.userStyle.value.toMutableUserStyle()
for (userStyleSetting in activity.editorSession.userStyleSchema.userStyleSettings) {
mutableUserStyle[userStyleSetting] = userStyleSetting.options.last()
}
activity.editorSession.userStyle.value = mutableUserStyle.toUserStyle()
// This should cause the style on the to be reverted back to the initial style.
activity.editorSession.commitChangesOnClose = false
activity.editorSession.close()
activity.finish()
}
val result = editorObserver.awaitEditorStateChange(
TIMEOUT_MILLIS,
TimeUnit.MILLISECONDS
).asApiEditorState()
assertThat(result.userStyle.userStyleMap[colorStyleSetting.id.value])
.isEqualTo(blueStyleOption.id.value)
assertThat(result.userStyle.userStyleMap[watchHandStyleSetting.id.value])
.isEqualTo(gothicStyleOption.id.value)
assertFalse(result.shouldCommitChanges)
assertNull(result.previewImage)
// The original style should be applied to the watch face however because
// commitChangesOnClose is false.
assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
.isEqualTo(redStyleOption.id.value)
assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
.isEqualTo(classicStyleOption.id.value)
EditorService.globalEditorService.unregisterObserver(observerId)
}
@Test
public fun commitChanges_preRFlow() {
val scenario = createOnWatchFaceEditingTestActivity(
listOf(colorStyleSetting, watchHandStyleSetting),
emptyList(),
preRFlow = true
)
val editorObserver = TestEditorObserver()
val observerId = EditorService.globalEditorService.registerObserver(editorObserver)
scenario.onActivity { activity ->
assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
.isEqualTo(redStyleOption.id.value)
assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
.isEqualTo(classicStyleOption.id.value)
// Select [blueStyleOption] and [gothicStyleOption].
val mutableUserStyle = activity.editorSession.userStyle.value.toMutableUserStyle()
for (userStyleSetting in activity.editorSession.userStyleSchema.userStyleSettings) {
mutableUserStyle[userStyleSetting] = userStyleSetting.options.last()
}
activity.editorSession.userStyle.value = mutableUserStyle.toUserStyle()
activity.editorSession.close()
activity.finish()
}
val result = editorObserver.awaitEditorStateChange(
TIMEOUT_MILLIS,
TimeUnit.MILLISECONDS
).asApiEditorState()
assertThat(result.userStyle.userStyleMap[colorStyleSetting.id.value])
.isEqualTo(blueStyleOption.id.value)
assertThat(result.userStyle.userStyleMap[watchHandStyleSetting.id.value])
.isEqualTo(gothicStyleOption.id.value)
assertTrue(result.shouldCommitChanges)
// Changes should be applied to the delegate too.
assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
.isEqualTo(blueStyleOption.id.value)
assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
.isEqualTo(gothicStyleOption.id.value)
EditorService.globalEditorService.unregisterObserver(observerId)
}
@Suppress("NewApi") // EditorRequest
@Test
public fun watchFaceEditorContract_createIntent() {
val testComponentName = ComponentName("test.package", "test.class")
val intent = WatchFaceEditorContract().createIntent(
ApplicationProvider.getApplicationContext<Context>(),
EditorRequest(
testComponentName,
testEditorPackageName,
null,
testInstanceId,
null,
null
)
)
assertThat(intent.getPackage()).isEqualTo(testEditorPackageName)
val editorRequest = EditorRequest.createFromIntent(intent)
assertThat(editorRequest.editorPackageName).isEqualTo(testEditorPackageName)
assertThat(editorRequest.initialUserStyle).isNull()
assertThat(editorRequest.watchFaceComponentName).isEqualTo(testComponentName)
assertThat(editorRequest.watchFaceId.id).isEqualTo(testInstanceId.id)
}
@Test
@Ignore // TODO(b/200917204): This test is flaking on the bots.
public fun forceCloseEditorSessionDuring_fetchComplicationsData() {
val getProviderInfosLatch = CountDownLatch(1)
val complicationDataSourceInfoRetrieverProvider =
TestComplicationDataSourceInfoRetrieverProvider(getProviderInfosLatch)
val scenario = createOnWatchFaceEditingTestActivity(
listOf(colorStyleSetting, watchHandStyleSetting),
listOf(leftComplication, rightComplication),
complicationDataSourceInfoRetrieverProvider =
complicationDataSourceInfoRetrieverProvider
)
scenario.onActivity { activity ->
activity.immediateCoroutineScope.launch {
activity.editorSession.complicationsPreviewData.collect {}
fail("We shouldn't get here due to the editor closing")
}
}
EditorService.globalEditorService.closeEditor()
getProviderInfosLatch.countDown()
assertTrue(onDestroyLatch.await(5L, TimeUnit.SECONDS))
assertTrue(
complicationDataSourceInfoRetrieverProvider.lastComplicationDataSourceInfoRetriever
.closed
)
}
@Test
public fun forceCloseEditorSession() {
val scenario = createOnWatchFaceEditingTestActivity(
listOf(colorStyleSetting, watchHandStyleSetting),
listOf(leftComplication, rightComplication)
)
val editorObserver = TestEditorObserver()
val observerId = EditorService.globalEditorService.registerObserver(editorObserver)
scenario.onActivity { activity ->
// Select [blueStyleOption] and [gothicStyleOption].
val mutableUserStyle = activity.editorSession.userStyle.value.toMutableUserStyle()
for (userStyleSetting in activity.editorSession.userStyleSchema.userStyleSettings) {
mutableUserStyle[userStyleSetting] = userStyleSetting.options.last()
}
activity.editorSession.userStyle.value = mutableUserStyle.toUserStyle()
}
EditorService.globalEditorService.closeEditor()
assertTrue(onDestroyLatch.await(5L, TimeUnit.SECONDS))
// We don't expect the observer to have fired.
assertFalse(editorObserver.stateChangeObserved())
// The style change should not have been applied to the watchface.
assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
.isEqualTo(redStyleOption.id.value)
assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
.isEqualTo(classicStyleOption.id.value)
EditorService.globalEditorService.unregisterObserver(observerId)
}
@SuppressLint("NewApi") // EditorRequest
@Test
public fun closeEditorSessionBeforeInitCompleted() {
val testComponentName = ComponentName("test.package", "test.class")
OnWatchFaceEditingTestActivity.complicationDataSourceInfoRetrieverProvider =
TestComplicationDataSourceInfoRetrieverProvider()
val session: ActivityScenario<OnWatchFaceEditingTestActivity> = ActivityScenario.launch(
WatchFaceEditorContract().createIntent(
ApplicationProvider.getApplicationContext<Context>(),
EditorRequest(
testComponentName,
testEditorPackageName,
null,
WatchFaceId("instanceId"),
null,
null
)
).apply {
component = ComponentName(
ApplicationProvider.getApplicationContext<Context>(),
OnWatchFaceEditingTestActivity::class.java
)
}
)
session.onActivity {
// This shouldn't throw an exception.
EditorService.globalEditorService.closeEditor()
}
}
@Test
public fun cancelDuring_updatePreviewData() {
ComplicationDataSourceChooserContract.useTestComplicationHelperActivity = true
TestComplicationHelperActivity.resultIntent = CompletableDeferred(
Intent().apply {
putExtra(
ComplicationDataSourceChooserIntent.EXTRA_PROVIDER_INFO,
ComplicationDataSourceInfo(
"TestDataSource3App",
"TestDataSource3",
Icon.createWithBitmap(
Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
),
ComplicationType.LONG_TEXT,
dataSource3
).toWireComplicationProviderInfo()
)
}
)
lateinit var baseEditorSession: BaseEditorSession
lateinit var complicationDataSourceInfoRetriever: ComplicationDataSourceInfoRetriever
var requestPreviewComplicationDataCount = 0
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
listOf(leftComplication, rightComplication),
complicationDataSourceInfoRetrieverProvider = object :
TestComplicationDataSourceInfoRetrieverProvider() {
override fun getComplicationDataSourceInfoRetriever():
ComplicationDataSourceInfoRetriever {
complicationDataSourceInfoRetriever =
super.getComplicationDataSourceInfoRetriever()
return complicationDataSourceInfoRetriever
}
override fun requestPreviewComplicationData(
dataSourceComponent: ComponentName,
complicationType: Int,
previewComplicationDataCallback: IPreviewComplicationDataCallback
): Boolean {
// Force close the third time this is invoked in response to
// openComplicationDataSourceChooser and a result being selected. The previous two
// invocations where done to prime the map for getComplicationsPreviewData().
if (++requestPreviewComplicationDataCount == 3) {
baseEditorSession.forceClose()
} else {
previewComplicationDataCallback.updateComplicationData(
ShortTextComplicationData.Builder(
PlainComplicationText.Builder("TestData").build(),
ComplicationText.EMPTY
).build().asWireComplicationData()
)
}
return true
}
}
)
scenario.onActivity { activity ->
baseEditorSession = activity.editorSession as BaseEditorSession
}
try {
runBlocking {
withContext(baseEditorSession.coroutineScope.coroutineContext) {
baseEditorSession.openComplicationDataSourceChooser(RIGHT_COMPLICATION_ID)
// Make sure everything that was going to run has run.
baseEditorSession.coroutineScope.coroutineContext.job.join()
fail("Should have failed with a JobCancellationException")
}
}
} catch (e: Exception) {
assertThat(e.toString()).contains("kotlinx.coroutines.JobCancellationException")
}
// Ensure the complicationDataSourceInfoRetriever was closed despite forceClose() being
// called.
assertThat(complicationDataSourceInfoRetriever.closed).isTrue()
}
@Test
public fun getComplicationsPreviewData() {
val scenario = createOnWatchFaceEditingTestActivity(
listOf(colorStyleSetting, watchHandStyleSetting),
listOf(leftComplication, rightComplication)
)
scenario.onActivity { activity ->
runBlocking {
val previewData = activity.editorSession.complicationsPreviewData.value
assertThat(previewData.size).isEqualTo(2)
assertThat(previewData[LEFT_COMPLICATION_ID])
.isInstanceOf(ShortTextComplicationData::class.java)
val leftComplicationData =
previewData[LEFT_COMPLICATION_ID] as ShortTextComplicationData
assertThat(
leftComplicationData.text.getTextAt(
ApplicationProvider.getApplicationContext<Context>().resources,
Instant.EPOCH
)
).isEqualTo("Left")
assertThat(previewData[RIGHT_COMPLICATION_ID])
.isInstanceOf(LongTextComplicationData::class.java)
val rightComplicationData =
previewData[RIGHT_COMPLICATION_ID] as LongTextComplicationData
assertThat(
rightComplicationData.text.getTextAt(
ApplicationProvider.getApplicationContext<Context>().resources,
Instant.EPOCH
)
).isEqualTo("Right")
}
}
}
public fun getComplicationsPreviewData_withEmptyBackgroundComplication() {
val scenario = createOnWatchFaceEditingTestActivity(
listOf(colorStyleSetting, watchHandStyleSetting),
listOf(leftComplication, backgroundComplication)
)
scenario.onActivity { activity ->
runBlocking {
val previewData = activity.editorSession.complicationsPreviewData.value
assertThat(previewData.size).isEqualTo(2)
assertThat(previewData[LEFT_COMPLICATION_ID])
.isInstanceOf(ShortTextComplicationData::class.java)
val leftComplicationData =
previewData[LEFT_COMPLICATION_ID] as ShortTextComplicationData
assertThat(
leftComplicationData.text.getTextAt(
ApplicationProvider.getApplicationContext<Context>().resources,
Instant.EPOCH
)
).isEqualTo("Left")
// TestComplicationDataSourceInfoRetrieverProvider isn't configured with a data
// source for the background complication which means it behaves as if it was an
// empty complication as far as fetching preview data is concerned.
assertThat(previewData[BACKGROUND_COMPLICATION_ID])
.isInstanceOf(EmptyComplicationData::class.java)
}
}
}
@SuppressLint("NewApi") // EditorRequest
@Test
public fun testComponentNameMismatch() {
val testComponentName = ComponentName("test.package", "test.class")
val watchFaceId = WatchFaceId("ID-1")
OnWatchFaceEditingTestActivity.complicationDataSourceInfoRetrieverProvider =
TestComplicationDataSourceInfoRetrieverProvider()
val scenario: ActivityScenario<OnWatchFaceEditingTestActivity> = ActivityScenario.launch(
WatchFaceEditorContract().createIntent(
ApplicationProvider.getApplicationContext<Context>(),
EditorRequest(
testComponentName,
testEditorPackageName,
null,
watchFaceId,
null,
null
)
).apply {
component = ComponentName(
ApplicationProvider.getApplicationContext<Context>(),
OnWatchFaceEditingTestActivity::class.java
)
}
)
scenario.onActivity { activity ->
val mockWatchFaceHostApi = mock(WatchFaceHostApi::class.java)
val handler = Handler(Looper.myLooper()!!)
`when`(mockWatchFaceHostApi.getUiThreadHandler()).thenReturn(handler)
`when`(mockWatchFaceHostApi.getContext()).thenReturn(
ApplicationProvider.getApplicationContext<Context>()
)
val watchState = MutableWatchState().asWatchState()
val currentUserStyleRepository =
CurrentUserStyleRepository(UserStyleSchema(emptyList()))
val mockSurfaceHolder = mock(SurfaceHolder::class.java)
val mockSurface = mock(Surface::class.java)
`when`(mockSurfaceHolder.surface).thenReturn(mockSurface)
`when`(mockSurfaceHolder.surfaceFrame).thenReturn(Rect())
`when`(mockSurface.isValid).thenReturn(false)
// Construct a WatchFaceImpl which creates a delegate whose ComponentName doesn't match
// testComponentName.
WatchFaceImpl(
WatchFace(
WatchFaceType.DIGITAL,
object : Renderer.CanvasRenderer(
mockSurfaceHolder,
currentUserStyleRepository,
watchState, CanvasType.SOFTWARE,
16
) {
override fun render(
canvas: Canvas,
bounds: Rect,
zonedDateTime: ZonedDateTime
) {}
override fun renderHighlightLayer(
canvas: Canvas,
bounds: Rect,
zonedDateTime: ZonedDateTime
) {
}
}
),
mockWatchFaceHostApi,
watchState,
currentUserStyleRepository,
ComplicationSlotsManager(emptyList(), currentUserStyleRepository),
BroadcastsObserver(
watchState,
mockWatchFaceHostApi,
CompletableDeferred(),
CoroutineScope(handler.asCoroutineDispatcher())
),
null
)
assertThat(activity.onCreateException).isInstanceOf(IllegalStateException::class.java)
assertThat(activity.onCreateException.message).isEqualTo(
"Expected ComponentInfo{test.package/test.class} to be created but " +
"got ComponentInfo{androidx.wear.watchface.editor.test/" +
"android.app.Application}"
)
}
}
@Test
public fun watchfaceSupportsHeadlessEditing() {
val mockPackageManager = Mockito.mock(PackageManager::class.java)
`when`(
mockPackageManager.getServiceInfo(
ComponentName("test.package", EditorRequest.WATCHFACE_CONTROL_SERVICE),
PackageManager.GET_META_DATA
)
).thenReturn(
ServiceInfo().apply {
metaData = Bundle().apply {
putInt(EditorRequest.ANDROIDX_WATCHFACE_API_VERSION, 4)
}
}
)
assertThat(
EditorRequest.supportsWatchFaceHeadlessEditing(mockPackageManager, "test.package")
).isTrue()
}
@Test
public fun watchfaceSupportsHeadlessEditing_oldApi() {
val mockPackageManager = Mockito.mock(PackageManager::class.java)
`when`(
mockPackageManager.getServiceInfo(
ComponentName("test.package", EditorRequest.WATCHFACE_CONTROL_SERVICE),
PackageManager.GET_META_DATA
)
).thenReturn(
ServiceInfo().apply {
metaData = Bundle().apply {
putInt(EditorRequest.ANDROIDX_WATCHFACE_API_VERSION, 3)
}
}
)
assertThat(
EditorRequest.supportsWatchFaceHeadlessEditing(mockPackageManager, "test.package")
).isFalse()
}
@Test
public fun cantAssignUnrelatedUserStyle() {
val redOption = ListOption(Option.Id("red"), "Red", icon = null)
val greenOption = ListOption(Option.Id("green"), "Green", icon = null)
val colorStyleList = listOf(redOption, greenOption)
val watchColorSetting = UserStyleSetting.ListUserStyleSetting(
UserStyleSetting.Id("color_id"),
"Color",
"Watch face color", /* icon = */
null,
colorStyleList,
listOf(WatchFaceLayer.COMPLICATIONS_OVERLAY)
)
val scenario = createOnWatchFaceEditingTestActivity(
listOf(colorStyleSetting, watchHandStyleSetting),
listOf(leftComplication, rightComplication)
)
scenario.onActivity { activity ->
try {
// Trying to set an unrelated UserStyle should fail.
assertFailsWith<IllegalArgumentException> {
activity.editorSession.userStyle.value =
UserStyle(mapOf(watchColorSetting to greenOption))
}
} finally {
activity.editorSession.close()
activity.finish()
}
}
}
@Test
public fun cantAssignUnrelatedUserStyle_compareAndSet() {
val redOption = ListOption(Option.Id("red"), "Red", icon = null)
val greenOption = ListOption(Option.Id("green"), "Green", icon = null)
val colorStyleList = listOf(redOption, greenOption)
val watchColorSetting = UserStyleSetting.ListUserStyleSetting(
UserStyleSetting.Id("color_id"),
"Color",
"Watch face color", /* icon = */
null,
colorStyleList,
listOf(WatchFaceLayer.COMPLICATIONS_OVERLAY)
)
val scenario = createOnWatchFaceEditingTestActivity(
listOf(colorStyleSetting, watchHandStyleSetting),
listOf(leftComplication, rightComplication)
)
scenario.onActivity { activity ->
try {
// Trying to set an unrelated UserStyle should fail.
assertFailsWith<IllegalArgumentException> {
// NB update uses compareAndSet under the hood.
activity.editorSession.userStyle.update {
UserStyle(mapOf(watchColorSetting to greenOption))
}
}
} finally {
activity.editorSession.close()
activity.finish()
}
}
}
}
@SuppressLint("NewApi") // icon.type
internal fun assertEquals(
expected: ComplicationDataSourceInfo?,
actual: ComplicationDataSourceInfo?
) = when (expected) {
null -> assertThat(actual).isNull()
else -> {
assertThat(actual).isNotNull()
checkNotNull(actual)
assertThat(actual.appName).isEqualTo(expected.appName)
assertThat(actual.name).isEqualTo(expected.name)
// Check the type as a proxy for it being the same icon.
assertThat(actual.icon.type).isEqualTo(expected.icon.type)
assertThat(actual.componentName).isEqualTo(expected.componentName)
}
}