[go: nahoru, domu]

blob: eef4f27942aba3b29f78afd27edd7a0083c0dbc0 [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.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContract
import androidx.annotation.Px
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.annotation.UiThread
import androidx.wear.complications.ComplicationHelperActivity
import androidx.wear.complications.ComplicationProviderInfo
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.EmptyComplicationData
import androidx.wear.complications.data.MonochromaticImage
import androidx.wear.complications.data.PlainComplicationText
import androidx.wear.complications.data.ShortTextComplicationData
import androidx.wear.complications.toApiComplicationProviderInfo
import androidx.wear.utility.AsyncTraceEvent
import androidx.wear.utility.TraceEvent
import androidx.wear.utility.launchWithTracing
import androidx.wear.watchface.DrawMode
import androidx.wear.watchface.RenderParameters
import androidx.wear.watchface.WatchFace
import androidx.wear.watchface.client.ComplicationState
import androidx.wear.watchface.client.EditorListener
import androidx.wear.watchface.client.EditorServiceClient
import androidx.wear.watchface.client.EditorState
import androidx.wear.watchface.client.HeadlessWatchFaceClient
import androidx.wear.watchface.client.WatchFaceId
import androidx.wear.watchface.data.ComplicationBoundsType
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
import androidx.wear.watchface.editor.data.EditorStateWireFormat
import androidx.wear.watchface.style.UserStyle
import androidx.wear.watchface.style.UserStyleData
import androidx.wear.watchface.style.UserStyleSchema
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import kotlin.jvm.Throws
private typealias WireComplicationProviderInfo =
android.support.wearable.complications.ComplicationProviderInfo
/**
* Interface for manipulating watch face state during an editing session for a watch face editing
* session. The editor should adjust [userStyle] and call [openComplicationProviderChooser] to
* configure the watch face and call [close] when done. This reports the updated [EditorState] to
* the [EditorListener]s registered via [EditorServiceClient.addListener]. Style changes applied
* during the editor session are temporary and will be reverted when the editor session completes.
* In the event that the editor sessions results in a new watch face configuration that will be
* subsequently reapplied when the new configuration is provided by the system.
*/
public abstract class EditorSession : AutoCloseable {
/** The [ComponentName] of the watch face being edited. */
public abstract 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.
*/
@get:RequiresApi(Build.VERSION_CODES.R)
public abstract val watchFaceId: WatchFaceId
/** The current [UserStyle]. Assigning to this will cause the style to update. However, styling
* changes to the watch face will be reverted upon exit. */
public abstract var userStyle: UserStyle
/** The UTC reference preview time for this watch face in milliseconds since the epoch. */
public abstract val previewReferenceTimeMillis: Long
/** The watch face's [UserStyleSchema]. */
public abstract val userStyleSchema: UserStyleSchema
/**
* Map of complication ids to [ComplicationState] for each complication slot. Note
* [ComplicationState] can change, typically in response to styling.
*/
public abstract val complicationsState: Map<Int, ComplicationState>
/**
* Whether any changes should be committed when the session is closed (defaults to `true`).
*
* Note due to SysUI requirements [EditorState] can't reliably be sent in the activity result
* because there are circumstances where [ComponentActivity.onStop] doesn't get called but the
* UX requires us to commit changes.
*
* Regardless of the value, on completion of the editor session, the original UserStyle is
* restored. Note we need SysUI's help to revert any complication provider changes. Caveat
* some providers have their own config (e.g. the world clock has a timezone setting) and
* that config currently can't be reverted.
*/
@get:UiThread
@get:JvmName("isCommitChangesOnClose")
@set:UiThread
public var commitChangesOnClose: Boolean = true
/**
* 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 an instance of
* [EmptyComplicationData]. Disabled complications are included. Note also unlike live data
* this is static per provider, but it may update (on the UiThread) as a result of
* [openComplicationProviderChooser].
*/
@UiThread
public abstract suspend fun getComplicationsPreviewData(): Map<Int, ComplicationData>
/**
* Returns a map of complication ids to [ComplicationProviderInfo] that represent the
* information available about the provider for each complication.
*
* A `null` [ComplicationProviderInfo] will be associated with a complication id if the
* complication is configured to show the empty complication provider.
*/
@UiThread
public abstract suspend fun getComplicationsProviderInfo(): Map<Int, ComplicationProviderInfo?>
/** The ID of the background complication or `null` if there isn't one. */
@get:SuppressWarnings("AutoBoxing")
public abstract val backgroundComplicationId: Int?
/**
* Returns the ID of the complication at the given coordinates or `null` if there isn't one.
* Only complications with [ComplicationBoundsType.ROUND_RECT] are supported by this function.
*/
@SuppressWarnings("AutoBoxing")
@UiThread
public abstract fun getComplicationIdAt(@Px x: Int, @Px y: Int): Int?
/**
* Renders the watch face to a [Bitmap] using the current [userStyle].
*
* @param renderParameters The [RenderParameters] to render with. Must be [DrawMode.INTERACTIVE]
* @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 abstract fun renderWatchFaceToBitmap(
renderParameters: RenderParameters,
calendarTimeMillis: Long,
idToComplicationData: Map<Int, ComplicationData>?
): Bitmap
/**
* Opens the complication provider chooser and returns the chosen complication provider
* for the given splot.
*
* The result returns `null` if the operation was cancelled and otherwise returned an
* instance of [ChosenComplicationProvider] that contains information about the chosen
* provider.
*
* If the complication provider was changed then the map returned by
* [getComplicationsPreviewData] is updated (on the UiThread).
*/
@UiThread
public abstract suspend fun openComplicationProviderChooser(complicationId: Int):
ChosenComplicationProvider?
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.
*
* @param activity The [ComponentActivity] associated with the [EditorSession].
* @param editIntent The [Intent] sent by SysUI to launch the editing session.
* @return Deferred<EditorSession?> which is resolved with either the [EditorSession] or
* `null` if it can't be constructed.
* @throws [TimeoutCancellationException] if it takes more than
* [EDITING_SESSION_TIMEOUT_MILLIS] milliseconds to create a watch face editor.
*/
@SuppressWarnings("ExecutorRegistration")
@JvmStatic
@UiThread
@Throws(TimeoutCancellationException::class)
public suspend fun createOnWatchEditingSession(
activity: ComponentActivity,
editIntent: Intent
): EditorSession = createOnWatchEditingSessionImpl(
activity,
editIntent,
object : ProviderInfoRetrieverProvider {
override fun getProviderInfoRetriever() = ProviderInfoRetriever(activity)
}
)
// Used by tests.
@Throws(TimeoutCancellationException::class)
internal suspend fun createOnWatchEditingSessionImpl(
activity: ComponentActivity,
editIntent: Intent,
providerInfoRetrieverProvider: ProviderInfoRetrieverProvider
): EditorSession = TraceEvent(
"EditorSession.createOnWatchEditingSessionAsyncImpl"
).use {
val coroutineScope =
CoroutineScope(Handler(Looper.getMainLooper()).asCoroutineDispatcher().immediate)
return EditorRequest.createFromIntent(editIntent).let { editorRequest ->
// We need to respect the lifecycle and register the ActivityResultListener now.
val session = OnWatchFaceEditorSessionImpl(
activity,
editorRequest.watchFaceComponentName,
editorRequest.watchFaceId,
editorRequest.initialUserStyle,
providerInfoRetrieverProvider,
coroutineScope
)
// But full initialization has to be deferred because
// [WatchFace.getOrCreateEditorDelegate] is async.
// Resolve only after init has been completed.
withContext(coroutineScope.coroutineContext) {
withTimeout(EDITING_SESSION_TIMEOUT_MILLIS) {
session.setEditorDelegate(
WatchFace.getOrCreateEditorDelegate(
editorRequest.watchFaceComponentName
).await()
)
// Resolve only after init has been completed.
session
}
}
}
}
/**
* Constructs an [EditorSession] for a remote watch face editor.
*
* @param activity The [ComponentActivity] associated with the EditorSession.
* @param editIntent The [Intent] sent by SysUI to launch the editing session.
* @param headlessWatchFaceClient The [HeadlessWatchFaceClient] to use for rendering etc...
* @return The [EditorSession] or `null` if it could not be constructed.
*/
@JvmStatic
@RequiresApi(27)
@UiThread
public fun createHeadlessEditingSession(
activity: ComponentActivity,
editIntent: Intent,
headlessWatchFaceClient: HeadlessWatchFaceClient
): EditorSession = TraceEvent("EditorSession.createHeadlessEditingSession").use {
EditorRequest.createFromIntent(editIntent).let {
HeadlessEditorSession(
activity,
headlessWatchFaceClient,
it.watchFaceComponentName,
it.watchFaceId,
it.initialUserStyle!!,
object : ProviderInfoRetrieverProvider {
override fun getProviderInfoRetriever() = ProviderInfoRetriever(activity)
},
CoroutineScope(
Handler(Looper.getMainLooper()).asCoroutineDispatcher().immediate
)
)
}
}
/** Timeout allowed for waiting for creating the watch face editing session. */
public const val EDITING_SESSION_TIMEOUT_MILLIS: Long = 4000L
}
}
/**
* The complication provider that was chosen by the user for a given complication id as a result
* to a call to [EditorSession.openComplicationProviderChooser].
*/
public class ChosenComplicationProvider(
/** The ID of the complication slot that was configured. */
public val complicationId: Int,
/** The provider that was chosen for this slot, or `null` if the empty provider was chosen. */
public val complicationProviderInfo: ComplicationProviderInfo?,
/** Any additional extras returned by provider chooser. */
public val extras: Bundle,
) {
override fun toString(): String =
"$complicationId,$complicationProviderInfo,${extras.asString()}"
}
// Helps inject mock ProviderInfoRetrievers for testing.
internal interface ProviderInfoRetrieverProvider {
fun getProviderInfoRetriever(): ProviderInfoRetriever
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public abstract class BaseEditorSession internal constructor(
private val activity: ComponentActivity,
private val providerInfoRetrieverProvider: ProviderInfoRetrieverProvider,
public val coroutineScope: CoroutineScope
) : EditorSession() {
protected var closed: Boolean = false
protected var forceClosed: Boolean = false
private val editorSessionTraceEvent = AsyncTraceEvent("EditorSession")
private val closeCallback = object : EditorService.CloseCallback() {
override fun onClose() {
forceClose()
}
}
private companion object {
/** Timeout for fetching ComplicationsPreviewData in [BaseEditorSession.close]. */
private const val CLOSE_BROADCAST_TIMEOUT_MILLIS = 500L
}
init {
EditorService.globalEditorService.addCloseCallback(closeCallback)
}
/**
* This is completed when [fetchComplicationsData] has called [getPreviewData] for each
* complication and each of those have been completed.
*/
private val deferredComplicationPreviewDataMap =
CompletableDeferred<MutableMap<Int, ComplicationData>>()
override suspend fun getComplicationsPreviewData(): Map<Int, ComplicationData> {
return deferredComplicationPreviewDataMap.await()
}
// This is completed when [fetchProviderInfo] has called [getProviderInfo] for each
// complication and each of those have been completed.
private val deferredComplicationsProviderInfoMap =
CompletableDeferred<MutableMap<Int, ComplicationProviderInfo?>>()
override suspend fun getComplicationsProviderInfo(): Map<Int, ComplicationProviderInfo?> =
deferredComplicationsProviderInfoMap.await()
/** Pending result for [openComplicationProviderChooser]. */
internal var pendingComplicationProviderChooserResult:
CompletableDeferred<ChosenComplicationProvider?>? = null
/** The id of the complication being configured due to [openComplicationProviderChooser]. */
private var pendingComplicationProviderId: Int = -1
private val chooseComplicationProvider =
activity.registerForActivityResult(ComplicationProviderChooserContract()) {
onComplicationProviderChooserResult(it)
}
internal fun onComplicationProviderChooserResult(
complicationProviderChooserResult: ComplicationProviderChooserResult?
) {
// Check if the user cancelled the provider chooser.
if (complicationProviderChooserResult == null) {
pendingComplicationProviderChooserResult!!.complete(null)
pendingComplicationProviderChooserResult = null
return
}
val providerInfoRetriever =
providerInfoRetrieverProvider.getProviderInfoRetriever()
coroutineScope.launchWithTracing(
"BaseEditorSession.onComplicationProviderChooserResult"
) {
try {
val complicationsProviderInfoMap = deferredComplicationsProviderInfoMap.await()
complicationsProviderInfoMap[pendingComplicationProviderId] =
complicationProviderChooserResult.providerInfo
val previewData = getPreviewData(
providerInfoRetriever,
complicationProviderChooserResult.providerInfo
)
val complicationPreviewDataMap = deferredComplicationPreviewDataMap.await()
if (previewData == null) {
complicationPreviewDataMap[pendingComplicationProviderId] =
EmptyComplicationData()
} else {
complicationPreviewDataMap[pendingComplicationProviderId] = previewData
}
pendingComplicationProviderChooserResult!!.complete(
ChosenComplicationProvider(
pendingComplicationProviderId,
complicationProviderChooserResult.providerInfo,
complicationProviderChooserResult.extras,
)
)
pendingComplicationProviderChooserResult = null
} finally {
// This gets called after the above coroutine has finished.
providerInfoRetriever.close()
}
}
}
override suspend fun openComplicationProviderChooser(
complicationId: Int
): ChosenComplicationProvider? = TraceEvent(
"BaseEditorSession.launchComplicationProviderChooser $complicationId"
).use {
requireNotClosed()
require(!complicationsState[complicationId]!!.fixedComplicationProvider) {
"Can't configure fixed complication ID $complicationId"
}
// If there's a previous openComplicationProviderChooser invocation in flight then wait for
// it to complete.
pendingComplicationProviderChooserResult?.await()
pendingComplicationProviderChooserResult = CompletableDeferred()
pendingComplicationProviderId = complicationId
chooseComplicationProvider.launch(
ComplicationProviderChooserRequest(
this,
complicationId,
watchFaceId.id
)
)
return pendingComplicationProviderChooserResult!!.await()
}
override val backgroundComplicationId: Int? by lazy {
requireNotClosed()
complicationsState.entries.firstOrNull {
it.value.boundsType == ComplicationBoundsType.BACKGROUND
}?.key
}
override fun getComplicationIdAt(@Px x: Int, @Px y: Int): Int? {
requireNotClosed()
return complicationsState.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? = TraceEvent("BaseEditorSession.getPreviewData").use {
if (providerInfo == null) {
return null
}
// Fetch preview ComplicationData if possible.
providerInfo.componentName?.let {
try {
providerInfoRetriever.retrievePreviewComplicationData(
it,
providerInfo.type
)
} catch (e: Exception) {
// Something went wrong, so use fallback preview data.
makeFallbackPreviewData(providerInfo)
}
} ?: makeFallbackPreviewData(providerInfo)
}
private fun makeFallbackPreviewData(
providerInfo: ComplicationProviderInfo
) =
ShortTextComplicationData.Builder(
PlainComplicationText.Builder(providerInfo.name).build(),
ComplicationText.EMPTY
).setMonochromaticImage(
MonochromaticImage.Builder(providerInfo.icon).build()
).build()
protected fun fetchComplicationsData() {
val providerInfoRetriever = providerInfoRetrieverProvider.getProviderInfoRetriever()
coroutineScope.launchWithTracing("BaseEditorSession.fetchComplicationsData") {
try {
// Unlikely but WCS could conceivably crash during this call. We could retry but it's
// not obvious if that'd succeed or if WCS session state is recoverable, it's probably
// better to crash and start over.
val providerInfoArray = providerInfoRetriever.retrieveProviderInfo(
watchFaceComponentName,
complicationsState.keys.toIntArray()
)
deferredComplicationsProviderInfoMap.complete(
extractComplicationsProviderInfoMap(providerInfoArray)?.toMutableMap()
?: mutableMapOf()
)
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.
)?.mapValues {
it.value.await() ?: EmptyComplicationData()
}?.toMutableMap() ?: mutableMapOf()
)
} finally {
providerInfoRetriever.close()
}
}
}
override fun close() {
// Silently do nothing if we've been force closed, this simplifies the editor activity.
if (forceClosed) {
return
}
requireNotClosed()
EditorService.globalEditorService.removeCloseCallback(closeCallback)
// We need to send the preview data which we obtain asynchronously.
coroutineScope.launchWithTracing("BaseEditorSession.close") {
try {
withTimeout(CLOSE_BROADCAST_TIMEOUT_MILLIS) {
EditorService.globalEditorService.broadcastEditorState(
EditorStateWireFormat(
watchFaceId.id,
userStyle.toWireFormat(),
getComplicationsPreviewData().map {
IdAndComplicationDataWireFormat(
it.key,
it.value.asWireComplicationData()
)
},
commitChangesOnClose
)
)
}
} catch (e: TimeoutCancellationException) {
// Ignore this, nothing we can do.
}
releaseResources()
closed = true
editorSessionTraceEvent.close()
coroutineScope.cancel()
}
}
@UiThread
internal fun forceClose() {
commitChangesOnClose = false
closed = true
forceClosed = true
releaseResources()
activity.finish()
EditorService.globalEditorService.removeCloseCallback(closeCallback)
editorSessionTraceEvent.close()
coroutineScope.cancel()
}
protected fun requireNotClosed() {
require(!closed or forceClosed) {
"EditorSession method called after close()"
}
}
@UiThread
protected abstract fun releaseResources()
}
internal class OnWatchFaceEditorSessionImpl(
activity: ComponentActivity,
override val watchFaceComponentName: ComponentName,
override val watchFaceId: WatchFaceId,
private val initialEditorUserStyle: UserStyleData?,
providerInfoRetrieverProvider: ProviderInfoRetrieverProvider,
coroutineScope: CoroutineScope
) : BaseEditorSession(activity, providerInfoRetrieverProvider, coroutineScope) {
private lateinit var editorDelegate: WatchFace.EditorDelegate
override val userStyleSchema by lazy {
requireNotClosed()
editorDelegate.userStyleSchema
}
override val previewReferenceTimeMillis by lazy { editorDelegate.previewReferenceTimeMillis }
override val complicationsState
get() = editorDelegate.complicationsManager.complications.mapValues {
requireNotClosed()
ComplicationState(
it.value.computeBounds(editorDelegate.screenBounds),
it.value.boundsType,
it.value.supportedTypes,
it.value.defaultProviderPolicy,
it.value.defaultProviderType,
it.value.enabled,
it.value.initiallyEnabled,
it.value.renderer.getData()?.type ?: ComplicationType.NO_DATA,
it.value.fixedComplicationProvider,
it.value.configExtras
)
}
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() {
requireNotClosed()
if (_userStyle == null) {
_userStyle = UserStyle(editorDelegate.userStyle)
}
return _userStyle!!
}
set(value) {
requireNotClosed()
_userStyle = value
editorDelegate.userStyle = UserStyle(value)
}
private lateinit var previousWatchFaceUserStyle: UserStyle
override fun renderWatchFaceToBitmap(
renderParameters: RenderParameters,
calendarTimeMillis: Long,
idToComplicationData: Map<Int, ComplicationData>?
): Bitmap {
requireNotClosed()
require(renderParameters.drawMode == DrawMode.INTERACTIVE) {
"Currently only DrawMode.INTERACTIVE is supported"
}
return editorDelegate.renderWatchFaceToBitmap(
renderParameters,
calendarTimeMillis,
idToComplicationData
)
}
override fun releaseResources() {
if (this::editorDelegate.isInitialized) {
editorDelegate.onDestroy()
}
// Revert any changes to the user style that was set during the editing session. The
// system will update the user style and communicate it to the active watch face if
// needed. This guarantees that the system is always the source of truth for the current
// style.
if (this::previousWatchFaceUserStyle.isInitialized) {
userStyle = previousWatchFaceUserStyle
}
}
fun setEditorDelegate(editorDelegate: WatchFace.EditorDelegate) {
this.editorDelegate = editorDelegate
previousWatchFaceUserStyle = UserStyle(editorDelegate.userStyle)
// 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.userStyle =
UserStyle(initialEditorUserStyle, editorDelegate.userStyleSchema)
}
fetchComplicationsData()
}
}
@RequiresApi(27)
internal class HeadlessEditorSession(
activity: ComponentActivity,
private val headlessWatchFaceClient: HeadlessWatchFaceClient,
override val watchFaceComponentName: ComponentName,
override val watchFaceId: WatchFaceId,
initialUserStyle: UserStyleData,
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 complicationsState = headlessWatchFaceClient.complicationsState
override fun renderWatchFaceToBitmap(
renderParameters: RenderParameters,
calendarTimeMillis: Long,
idToComplicationData: Map<Int, ComplicationData>?
): Bitmap {
requireNotClosed()
return headlessWatchFaceClient.renderWatchFaceToBitmap(
renderParameters,
calendarTimeMillis,
userStyle,
idToComplicationData
)
}
override fun releaseResources() {
headlessWatchFaceClient.close()
}
init {
fetchComplicationsData()
}
}
internal class ComplicationProviderChooserRequest(
internal val editorSession: EditorSession,
internal val complicationId: Int,
internal val instanceId: String?
)
internal class ComplicationProviderChooserResult(
/** The updated [ComplicationProviderInfo] or `null` if the empty provider was chosen. */
internal val providerInfo: ComplicationProviderInfo?,
/** Any additional extras returned by provider chooser. */
internal val extras: Bundle,
)
/**
* An [ActivityResultContract] for invoking the complication provider chooser. If the user
* cancels the provider chooser than the result will be `null`.
*/
internal class ComplicationProviderChooserContract : ActivityResultContract<
ComplicationProviderChooserRequest, ComplicationProviderChooserResult?>() {
internal companion object {
const val EXTRA_PROVIDER_INFO = "android.support.wearable.complications.EXTRA_PROVIDER_INFO"
/**
* Whether to invoke a test activity instead of the [ComplicationHelperActivity].
*
* To be used in tests.
*/
internal var useTestComplicationHelperActivity = false
}
override fun createIntent(context: Context, input: ComplicationProviderChooserRequest): Intent {
val intent = ComplicationHelperActivity.createProviderChooserHelperIntent(
context,
input.editorSession.watchFaceComponentName,
input.complicationId,
input.editorSession.complicationsState[input.complicationId]!!.supportedTypes,
input.instanceId
)
val complicationState = input.editorSession.complicationsState[input.complicationId]!!
intent.replaceExtras(
Bundle(complicationState.complicationConfigExtras).apply { putAll(intent.extras!!) }
)
if (useTestComplicationHelperActivity) {
intent.component = ComponentName(
"androidx.wear.watchface.editor.test",
"androidx.wear.watchface.editor.TestComplicationHelperActivity"
)
}
return intent
}
override fun parseResult(resultCode: Int, intent: Intent?) = intent?.let {
val extras = intent.extras?.let {
Bundle(it).apply { remove(EXTRA_PROVIDER_INFO) }
} ?: Bundle.EMPTY
ComplicationProviderChooserResult(
it.getParcelableExtra<android.support.wearable.complications.ComplicationProviderInfo>(
EXTRA_PROVIDER_INFO
)?.toApiComplicationProviderInfo(),
extras
)
}
}
/**
* Extracts a map from complication ID to the corresponding [ComplicationProviderInfo] from the
* given array of [ProviderInfoRetriever.ProviderInfo].
*/
internal fun extractComplicationsProviderInfoMap(
providerInfoArray: Array<ProviderInfoRetriever.ProviderInfo>?
): Map<Int, ComplicationProviderInfo?>? =
providerInfoArray?.associateBy(
{ it.watchFaceComplicationId },
{ it.info }
)
internal fun Bundle.asString() = keySet().map { "$it: ${get(it)}" }