EditorSession.kt

/*
 * 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.concurrent.futures.ResolvableFuture
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.style.UserStyle
import androidx.wear.watchface.style.UserStyleSchema
import com.google.common.util.concurrent.ListenableFuture
import java.util.concurrent.Executor

/**
 * 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

    /**
     * The instance id of the watch face being edited. 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>

    /**
     * [ListenableFuture] for 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 val complicationPreviewData: ListenableFuture<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 a [ListenableFuture] which
     * resolves with `true` if the user made a selection or `false` if the activity was canceled.
     */
    @UiThread
    public fun launchComplicationProviderChooser(complicationId: Int): ListenableFuture<Boolean>

    public companion object {
        /** Constructs an [EditorSession] for an on watch face editor. */
        @SuppressWarnings("ExecutorRegistration")
        @JvmStatic
        @UiThread
        public fun createOnWatchEditingSession(
            /** The [ComponentActivity] associated with the EditorSession. */
            activity: ComponentActivity,

            /** [Intent] sent by SysUI to launch the editing session. */
            editIntent: Intent
        ): EditorSession? =
            EditorRequest.createFromIntent(editIntent)?.let { editorRequest ->
                WatchFace.getEditorDelegate(editorRequest.watchFaceComponentName)?.let {
                    OnWatchFaceEditorSessionImpl(
                        activity,
                        editorRequest.watchFaceComponentName,
                        editorRequest.watchFaceInstanceId,
                        editorRequest.initialUserStyle,
                        it
                    )
                }
            }

        /** 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!!
                )
            }
    }
}

internal abstract class BaseEditorSession(
    protected val activity: ComponentActivity,
) : EditorSession {

    // NB this map is only modified on the UI thread.
    private val complicationPreviewDataInternal = HashMap<Int, ComplicationData>()

    // This future is resolved when [fetchComplicationPreviewData] has called [getPreviewData] for
    // each complication and each of those future has been resolved.
    private val complicationPreviewDataFuture =
        ResolvableFuture.create<Map<Int, ComplicationData>>()

    override val complicationPreviewData: ResolvableFuture<Map<Int, ComplicationData>> =
        complicationPreviewDataFuture

    // The future returned by [launchComplicationProviderChooser].
    private var pendingFuture: ResolvableFuture<Boolean>? = null

    // The id of the complication being configured due to [launchComplicationProviderChooser].
    private var pendingComplicationProviderId: Int = -1

    private val mainThreadExecutor: Executor = object : Executor {
        private val handler: Handler = Handler(Looper.getMainLooper())
        override fun execute(command: Runnable) {
            if (handler.looper !== Looper.myLooper()) {
                handler.post(command)
            } else {
                command.run()
            }
        }
    }

    private val chooseComplicationProvider =
        activity.registerForActivityResult(ComplicationProviderChooserContract()) {
            if (it != null) {
                // Update preview data and then resolve [pendingFuture].
                val providerInfoRetriever = ProviderInfoRetriever(activity)
                val previewDataListener = getPreviewData(
                    providerInfoRetriever,
                    it.providerInfo
                )
                previewDataListener.addListener(
                    {
                        val previewData = previewDataListener.get()
                        if (previewData == null) {
                            complicationPreviewDataInternal.remove(pendingComplicationProviderId)
                        } else {
                            complicationPreviewDataInternal[pendingComplicationProviderId] =
                                previewData
                        }
                        providerInfoRetriever.close()
                        pendingFuture!!.set(true)
                    },
                    mainThreadExecutor
                )
            } else {
                pendingFuture!!.set(false)
            }
            pendingFuture = null
        }

    override fun launchComplicationProviderChooser(complicationId: Int): ListenableFuture<Boolean> {
        val future = ResolvableFuture.create<Boolean>()
        pendingFuture = future
        pendingComplicationProviderId = complicationId
        chooseComplicationProvider.launch(
            ComplicationProviderChooserRequest(this, complicationId)
        )
        return future
    }

    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 a future that resolves with 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 the future will resolve to `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 fun getPreviewData(
        providerInfoRetriever: ProviderInfoRetriever,
        providerInfo: ComplicationProviderInfo?
    ): ListenableFuture<ComplicationData?> {
        if (providerInfo == null) {
            return ResolvableFuture.create<ComplicationData>().apply {
                set(null)
            }
        }

        providerInfo.providerComponentName?.let {
            return providerInfoRetriever.requestPreviewComplicationData(
                it,
                ComplicationType.fromWireType(providerInfo.complicationType)
            )
        }

        // Generate fallback preview data.
        return ResolvableFuture.create<ComplicationData>().apply {
            val providerIcon = providerInfo.providerIcon
            val providerName = providerInfo.providerName
            set(
                when {
                    providerName == null -> null

                    providerIcon == null ->
                        LongTextComplicationData.Builder(ComplicationText.plain(providerName))
                            .build()

                    else ->
                        ShortTextComplicationData.Builder(ComplicationText.plain(providerName))
                            .setImage(
                                MonochromaticImage.Builder(providerIcon).build()
                            )
                            .build()
                }
            )
        }
    }

    protected fun fetchComplicationPreviewData() {
        val providerInfoRetriever = ProviderInfoRetriever(activity)
        val providerInfoFuture = providerInfoRetriever.retrieveProviderInfo(
            watchFaceComponentName,
            complicationState.keys.toIntArray()
        )
        providerInfoFuture.addListener(
            {
                providerInfoFuture.get()?.let {
                    // We can use a regular int here because we use [mainThreadExecutor].
                    var countDown = it.size
                    for (providerInfo in it) {
                        val previewDataListener = getPreviewData(
                            providerInfoRetriever,
                            providerInfo.info
                        )
                        previewDataListener.addListener(
                            {
                                previewDataListener.get()?.let { previewData ->
                                    complicationPreviewDataInternal[
                                        providerInfo.watchFaceComplicationId
                                    ] = previewData
                                }
                                // If we've generated preview data for all the complications we can
                                // resolve the future.
                                if (--countDown == 0) {
                                    complicationPreviewDataFuture.set(
                                        complicationPreviewDataInternal
                                    )
                                }
                            },
                            mainThreadExecutor
                        )
                    }
                }
                providerInfoRetriever.close()
            },
            { runnable -> runnable.run() }
        )
    }
}

internal class OnWatchFaceEditorSessionImpl(
    activity: ComponentActivity,
    override val watchFaceComponentName: ComponentName,
    override val instanceId: String,
    initialEditorUserStyle: Map<String, String>?,
    private val editorDelegate: WatchFace.EditorDelegate
) : BaseEditorSession(activity) {
    override val userStyleSchema = editorDelegate.userStyleRepository.schema

    override val previewReferenceTimeMillis = 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
            )
        }

    // 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(editorDelegate.userStyleRepository.userStyle)
        set(value) {
            field = value
            editorDelegate.userStyleRepository.userStyle = UserStyle(value)
        }

    override fun takeWatchFaceScreenshot(
        renderParameters: RenderParameters,
        calendarTimeMillis: Long,
        idToComplicationData: Map<Int, ComplicationData>?
    ) = editorDelegate.takeScreenshot(
        renderParameters,
        calendarTimeMillis,
        idToComplicationData
    )

    init {
        // 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>
) : BaseEditorSession(activity) {
    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
    )

    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 fun Activity.setWatchRequestResult(editorSession: EditorSession) {
    setResult(
        Activity.RESULT_OK,
        Intent().apply {
            putExtra(
                EditorResult.USER_STYLE_KEY,
                ParcelUtils.toParcelable(editorSession.userStyle.toWireFormat())
            )
        }
    )
}