/*
* 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.graphics.Bitmap
import android.os.Handler
import android.os.Looper
import android.support.wearable.complications.ComplicationProviderInfo
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContract
import androidx.annotation.Px
import androidx.annotation.RequiresApi
import androidx.annotation.UiThread
import androidx.versionedparcelable.ParcelUtils
import androidx.wear.complications.ComplicationHelperActivity
import androidx.wear.complications.ProviderInfoRetriever
import androidx.wear.complications.data.ComplicationData
import androidx.wear.complications.data.ComplicationText
import androidx.wear.complications.data.ComplicationType
import androidx.wear.complications.data.LongTextComplicationData
import androidx.wear.complications.data.MonochromaticImage
import androidx.wear.complications.data.ShortTextComplicationData
import androidx.wear.watchface.RenderParameters
import androidx.wear.watchface.WatchFace
import androidx.wear.watchface.client.ComplicationState
import androidx.wear.watchface.client.HeadlessWatchFaceClient
import androidx.wear.watchface.data.ComplicationBoundsType
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
import androidx.wear.watchface.style.UserStyle
import androidx.wear.watchface.style.UserStyleSchema
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
/**
* Interface for manipulating watch face state during an editing session for a watch face editing
* session. The editor should adjust [userStyle] and call [launchComplicationProviderChooser] to
* configure the watch face and call [Activity.setWatchRequestResult] to record the result.
*/
public interface EditorSession {
/** The [ComponentName] of the watch face being edited. */
public val watchFaceComponentName: ComponentName
/**
* Unique ID for the instance of the watch face being edited, only defined for Android R and
* beyond, it's `null` on Android P and earlier. Note each distinct [ComponentName] can have
* multiple instances.
*/
public val instanceId: String?
/** The current [UserStyle]. Assigning to this will cause the style to update. */
public var userStyle: UserStyle
/** The UTC reference preview time for this watch face in milliseconds since the epoch. */
public val previewReferenceTimeMillis: Long
/** The watch face's [UserStyleSchema]. */
public val userStyleSchema: UserStyleSchema
/**
* Map of complication ids to [ComplicationState] for each complication slot. Note
* [ComplicationState] can change, typically in response to styling.
*/
public val complicationState: Map<Int, ComplicationState>
/**
* Returns a map of complication ids to preview [ComplicationData] suitable for use in rendering
* the watch face. Note if a slot is configured to be empty then it will not appear in the map,
* however disabled complications are included. Note also unlike live data this is static per
* provider, but it may change (on the UIThread) as a result of
* [launchComplicationProviderChooser].
*/
public suspend fun getComplicationPreviewData(): Map<Int, ComplicationData>
/** The ID of the background complication or `null` if there isn't one. */
@get:SuppressWarnings("AutoBoxing")
public val backgroundComplicationId: Int?
/** Returns the ID of the complication at the given coordinates or `null` if there isn't one. */
@SuppressWarnings("AutoBoxing")
@UiThread
public fun getComplicationIdAt(@Px x: Int, @Px y: Int): Int?
/**
* Takes a screen shot of the watch face using the current [userStyle].
*
* @param renderParameters The [RenderParameters] to render with
* @param calendarTimeMillis The UTC time in milliseconds since the epoch to render with
* @param idToComplicationData The [ComplicationData] for each complication to render with
*/
@UiThread
public fun takeWatchFaceScreenshot(
renderParameters: RenderParameters,
calendarTimeMillis: Long,
idToComplicationData: Map<Int, ComplicationData>?
): Bitmap
/**
* Launches the complication provider chooser and returns `true` if the user made a selection or
* `false` if the activity was canceled.
*/
@UiThread
public suspend fun launchComplicationProviderChooser(complicationId: Int): Boolean
/** Should be called when the activity is going away so we can release resources. */
public fun onDestroy()
public companion object {
/**
* Constructs an [EditorSession] for an on watch face editor. This registers an activity
* result handler and so it must be called during an Activity or Fragment initialization
* path.
*/
@SuppressWarnings("ExecutorRegistration")
@JvmStatic
@UiThread
public fun createOnWatchEditingSessionAsync(
/** The [ComponentActivity] associated with the EditorSession. */
activity: ComponentActivity,
/** [Intent] sent by SysUI to launch the editing session. */
editIntent: Intent
): Deferred<EditorSession?> = createOnWatchEditingSessionAsyncImpl(
activity,
editIntent,
object : ProviderInfoRetrieverProvider {
override fun getProviderInfoRetriever() = ProviderInfoRetriever(activity)
}
)
// Used by tests.
internal fun createOnWatchEditingSessionAsyncImpl(
activity: ComponentActivity,
editIntent: Intent,
providerInfoRetrieverProvider: ProviderInfoRetrieverProvider
): Deferred<EditorSession?> {
val coroutineScope =
CoroutineScope(Handler(Looper.getMainLooper()).asCoroutineDispatcher())
return EditorRequest.createFromIntent(editIntent)?.let { editorRequest ->
// We need to respect the lifecycle and register the ActivityResultListener now.
val session = OnWatchFaceEditorSessionImpl(
activity,
editorRequest.watchFaceComponentName,
editorRequest.watchFaceInstanceId,
editorRequest.initialUserStyle,
providerInfoRetrieverProvider,
coroutineScope
)
// But full initialization has to be deferred because
// [WatchFace.getOrCreateEditorDelegate] is async.
coroutineScope.async {
session.setEditorDelegate(
WatchFace.getOrCreateEditorDelegate(
editorRequest.watchFaceComponentName
).await()!!
)
// Resolve the Deferred<EditorSession?> only after init has been completed.
session
}
} ?: CompletableDeferred(null)
}
/** Constructs an [EditorSession] for a remote watch face editor. */
@JvmStatic
@RequiresApi(27)
@UiThread
public fun createHeadlessEditingSession(
/** The [ComponentActivity] associated with the EditorSession. */
activity: ComponentActivity,
/** [Intent] sent by SysUI to launch the editing session. */
editIntent: Intent,
headlessWatchFaceClient: HeadlessWatchFaceClient
): EditorSession? =
EditorRequest.createFromIntent(editIntent)?.let {
HeadlessEditorSession(
activity,
headlessWatchFaceClient,
it.watchFaceComponentName,
it.watchFaceInstanceId,
it.initialUserStyle!!,
object : ProviderInfoRetrieverProvider {
override fun getProviderInfoRetriever() = ProviderInfoRetriever(activity)
},
CoroutineScope(Handler(Looper.getMainLooper()).asCoroutineDispatcher())
)
}
}
}
// Helps inject mock ProviderInfoRetrievers for testing.
internal interface ProviderInfoRetrieverProvider {
fun getProviderInfoRetriever(): ProviderInfoRetriever
}
internal abstract class BaseEditorSession(
protected val activity: ComponentActivity,
protected val providerInfoRetrieverProvider: ProviderInfoRetrieverProvider,
internal val coroutineScope: CoroutineScope
) : EditorSession {
// This is completed when [fetchComplicationPreviewData] has called [getPreviewData] for
// each complication and each of those have been completed.
private val deferredComplicationPreviewDataMap =
CompletableDeferred<MutableMap<Int, ComplicationData>>()
override suspend fun getComplicationPreviewData(): Map<Int, ComplicationData> {
return deferredComplicationPreviewDataMap.await()
}
// Pending result for [launchComplicationProviderChooser].
private var pendingComplicationProviderChooserResult: CompletableDeferred<Boolean>? = null
// The id of the complication being configured due to [launchComplicationProviderChooser].
private var pendingComplicationProviderId: Int = -1
private val chooseComplicationProvider =
activity.registerForActivityResult(ComplicationProviderChooserContract()) {
if (it != null) {
coroutineScope.launch {
// Update preview data.
val providerInfoRetriever =
providerInfoRetrieverProvider.getProviderInfoRetriever()
val previewData = getPreviewData(providerInfoRetriever, it.providerInfo)
val complicationPreviewDataMap = deferredComplicationPreviewDataMap.await()
if (previewData == null) {
complicationPreviewDataMap.remove(pendingComplicationProviderId)
} else {
complicationPreviewDataMap[pendingComplicationProviderId] = previewData
}
providerInfoRetriever.close()
pendingComplicationProviderChooserResult!!.complete(true)
pendingComplicationProviderChooserResult = null
}
} else {
pendingComplicationProviderChooserResult!!.complete(false)
pendingComplicationProviderChooserResult = null
}
}
override suspend fun launchComplicationProviderChooser(complicationId: Int): Boolean {
pendingComplicationProviderChooserResult = CompletableDeferred<Boolean>()
pendingComplicationProviderId = complicationId
chooseComplicationProvider.launch(
ComplicationProviderChooserRequest(this, complicationId)
)
return pendingComplicationProviderChooserResult!!.await()
}
override val backgroundComplicationId by lazy {
complicationState.entries.firstOrNull {
it.value.boundsType == ComplicationBoundsType.BACKGROUND
}?.key
}
override fun getComplicationIdAt(@Px x: Int, @Px y: Int): Int? =
complicationState.entries.firstOrNull {
it.value.isEnabled && when (it.value.boundsType) {
ComplicationBoundsType.ROUND_RECT -> it.value.bounds.contains(x, y)
ComplicationBoundsType.BACKGROUND -> false
ComplicationBoundsType.EDGE -> false
else -> false
}
}?.key
/**
* Returns the provider's preview [ComplicationData] if possible or fallback preview data based
* on provider icon and name if not. If the slot is configured to be empty then it will return
* `null`.
*
* Note providerInfoRetriever.requestPreviewComplicationData which requires R will never be
* called pre R because providerInfo.providerComponentName is only non null from R onwards.
*/
@SuppressLint("NewApi")
internal suspend fun getPreviewData(
providerInfoRetriever: ProviderInfoRetriever,
providerInfo: ComplicationProviderInfo?
): ComplicationData? {
if (providerInfo == null) {
return null
}
// Fetch preview ComplicationData if possible.
return providerInfo.providerComponentName?.let {
providerInfoRetriever.requestPreviewComplicationData(
it,
ComplicationType.fromWireType(providerInfo.complicationType)
)
} ?: makeFallbackPreviewData(providerInfo)
}
private fun makeFallbackPreviewData(
providerInfo: ComplicationProviderInfo
) = when {
providerInfo.providerName == null -> null
providerInfo.providerIcon == null ->
LongTextComplicationData.Builder(
ComplicationText.plain(providerInfo.providerName!!)
).build()
else ->
ShortTextComplicationData.Builder(
ComplicationText.plain(providerInfo.providerName!!)
).setMonochromaticImage(
MonochromaticImage.Builder(providerInfo.providerIcon!!).build()
).build()
}
protected fun fetchComplicationPreviewData() {
coroutineScope.launch {
val providerInfoRetriever = providerInfoRetrieverProvider.getProviderInfoRetriever()
val providerInfoArray = providerInfoRetriever.retrieveProviderInfo(
watchFaceComponentName,
complicationState.keys.toIntArray()
)
deferredComplicationPreviewDataMap.complete(
// Parallel fetch preview ComplicationData.
providerInfoArray?.associateBy(
{ it.watchFaceComplicationId },
{ async { getPreviewData(providerInfoRetriever, it.info) } }
// Coerce to a Map<Int, ComplicationData> omitting null values.
// If mapNotNullValues existed we would use it here.
)?.filterValues {
it.await() != null
}?.mapValues {
it.value.await()!!
}?.toMutableMap() ?: mutableMapOf()
)
providerInfoRetriever.close()
}
}
}
internal class OnWatchFaceEditorSessionImpl(
activity: ComponentActivity,
override val watchFaceComponentName: ComponentName,
override val instanceId: String?,
private val initialEditorUserStyle: Map<String, String>?,
providerInfoRetrieverProvider: ProviderInfoRetrieverProvider,
coroutineScope: CoroutineScope
) : BaseEditorSession(activity, providerInfoRetrieverProvider, coroutineScope) {
private lateinit var editorDelegate: WatchFace.EditorDelegate
override val userStyleSchema by lazy {
editorDelegate.userStyleRepository.schema
}
override val previewReferenceTimeMillis by lazy { editorDelegate.previewReferenceTimeMillis }
override val complicationState
get() = editorDelegate.complicationsManager.complications.mapValues {
ComplicationState(
it.value.computeBounds(editorDelegate.screenBounds),
it.value.boundsType,
it.value.supportedTypes,
it.value.defaultProviderPolicy,
it.value.defaultProviderType,
it.value.enabled,
it.value.renderer.idAndData?.complicationData?.type
?: ComplicationType.NO_DATA
)
}
private var _userStyle: UserStyle? = null
// We make a deep copy of the style because assigning to it can otherwise have unexpected
// side effects (it would apply to the active watch face).
override var userStyle: UserStyle
get() {
if (_userStyle == null) {
_userStyle = UserStyle(editorDelegate.userStyleRepository.userStyle)
}
return _userStyle!!
}
set(value) {
_userStyle = value
editorDelegate.userStyleRepository.userStyle = UserStyle(value)
}
override fun takeWatchFaceScreenshot(
renderParameters: RenderParameters,
calendarTimeMillis: Long,
idToComplicationData: Map<Int, ComplicationData>?
) = editorDelegate.takeScreenshot(
renderParameters,
calendarTimeMillis,
idToComplicationData
)
override fun onDestroy() {
editorDelegate.onDestroy()
}
fun setEditorDelegate(editorDelegate: WatchFace.EditorDelegate) {
this.editorDelegate = editorDelegate
// Apply any initial style from the intent. Note we don't restore the previous style at
// the end since we assume we're editing the current active watchface.
if (initialEditorUserStyle != null) {
editorDelegate.userStyleRepository.userStyle =
UserStyle(initialEditorUserStyle, editorDelegate.userStyleRepository.schema)
}
fetchComplicationPreviewData()
}
}
@RequiresApi(27)
internal class HeadlessEditorSession(
activity: ComponentActivity,
private val headlessWatchFaceClient: HeadlessWatchFaceClient,
override val watchFaceComponentName: ComponentName,
override val instanceId: String?,
initialUserStyle: Map<String, String>,
providerInfoRetrieverProvider: ProviderInfoRetrieverProvider,
coroutineScope: CoroutineScope
) : BaseEditorSession(activity, providerInfoRetrieverProvider, coroutineScope) {
override val userStyleSchema = headlessWatchFaceClient.userStyleSchema
override var userStyle = UserStyle(initialUserStyle, userStyleSchema)
override val previewReferenceTimeMillis = headlessWatchFaceClient.previewReferenceTimeMillis
override val complicationState = headlessWatchFaceClient.complicationState
override fun takeWatchFaceScreenshot(
renderParameters: RenderParameters,
calendarTimeMillis: Long,
idToComplicationData: Map<Int, ComplicationData>?
) = headlessWatchFaceClient.takeWatchFaceScreenshot(
renderParameters,
100,
calendarTimeMillis,
userStyle,
idToComplicationData
)
override fun onDestroy() {
headlessWatchFaceClient.close()
}
init {
fetchComplicationPreviewData()
}
}
internal class ComplicationProviderChooserRequest(
internal val editorSession: EditorSession,
internal val complicationId: Int
)
internal class ComplicationProviderChooserResult(
/** The updated [ComplicationProviderInfo] or `null` if the operation was canceled. */
internal val providerInfo: ComplicationProviderInfo?
)
/** An [ActivityResultContract] for invoking the complication provider chooser. */
internal class ComplicationProviderChooserContract : ActivityResultContract<
ComplicationProviderChooserRequest, ComplicationProviderChooserResult>() {
internal companion object {
const val EXTRA_PROVIDER_INFO = "android.support.wearable.complications.EXTRA_PROVIDER_INFO"
}
override fun createIntent(context: Context, input: ComplicationProviderChooserRequest): Intent =
ComplicationHelperActivity.createProviderChooserHelperIntent(
context,
input.editorSession.watchFaceComponentName,
input.complicationId,
input.editorSession.complicationState[input.complicationId]!!.supportedTypes
)
override fun parseResult(resultCode: Int, intent: Intent?): ComplicationProviderChooserResult {
return ComplicationProviderChooserResult(intent?.getParcelableExtra(EXTRA_PROVIDER_INFO))
}
}
/** Sets the [Activity]s result with [EditorResult]. */
public suspend fun Activity.setWatchRequestResult(editorSession: EditorSession) {
setResult(
Activity.RESULT_OK,
Intent().apply {
putExtra(
USER_STYLE_KEY,
ParcelUtils.toParcelable(editorSession.userStyle.toWireFormat())
)
putExtra(
PREVIEW_COMPLICATIONS_KEY,
editorSession.getComplicationPreviewData().map {
ParcelUtils.toParcelable(
IdAndComplicationDataWireFormat(
it.key,
it.value.asWireComplicationData()
)
)
}.toTypedArray()
)
putExtra(INSTANCE_ID_KEY, editorSession.instanceId)
}
)
}