InteractiveWatchFaceClient.kt

/*
 * Copyright 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.wear.watchface.client

import android.app.PendingIntent
import android.graphics.Bitmap
import android.os.Handler
import android.os.HandlerThread
import android.os.RemoteException
import android.support.wearable.watchface.SharedMemoryImage
import androidx.annotation.AnyThread
import androidx.annotation.Px
import androidx.annotation.RequiresApi
import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.complications.data.toApiComplicationText
import androidx.wear.watchface.utility.TraceEvent
import androidx.wear.watchface.ComplicationSlot
import androidx.wear.watchface.ComplicationSlotsManager
import androidx.wear.watchface.ContentDescriptionLabel
import androidx.wear.watchface.RenderParameters
import androidx.wear.watchface.TapType
import androidx.wear.watchface.control.IInteractiveWatchFace
import androidx.wear.watchface.control.data.WatchFaceRenderParams
import androidx.wear.watchface.ComplicationSlotBoundsType
import androidx.wear.watchface.control.IWatchfaceReadyListener
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
import androidx.wear.watchface.data.WatchUiState
import androidx.wear.watchface.style.UserStyle
import androidx.wear.watchface.style.UserStyleSchema
import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting
import androidx.wear.watchface.style.UserStyleData
import java.time.Instant
import java.util.concurrent.Executor

/**
 * Controls a stateful remote interactive watch face. Typically this will be used for the current
 * active watch face.
 *
 * Note clients should call [close] when finished.
 */
public interface InteractiveWatchFaceClient : AutoCloseable {
    /**
     * Sends new [ComplicationData] to the watch face. Note this doesn't have to be a full update,
     * it's possible to update just one complication at a time, but doing so may result in a less
     * visually clean transition.
     *
     * @param slotIdToComplicationData The [ComplicationData] for each
     * [androidx.wear.watchface.ComplicationSlot].
     */
    @Throws(RemoteException::class)
    public fun updateComplicationData(slotIdToComplicationData: Map<Int, ComplicationData>)

    /**
     * Renders the watchface to a shared memory backed [Bitmap] with the given settings.
     *
     * @param renderParameters The [RenderParameters] to draw with.
     * @param instant The [Instant] render with.
     * @param userStyle Optional [UserStyle] to render with, if null the current style is used.
     * @param idAndComplicationData Map of complication ids to [ComplicationData] to render with, or
     * if null then the existing complication data if any is used.
     * @return A shared memory backed [Bitmap] containing a screenshot of the watch  face with the
     * given settings.
     */
    @RequiresApi(27)
    @Throws(RemoteException::class)
    public fun renderWatchFaceToBitmap(
        renderParameters: RenderParameters,
        instant: Instant,
        userStyle: UserStyle?,
        idAndComplicationData: Map<Int, ComplicationData>?
    ): Bitmap

    /** The UTC reference preview time for this watch face in milliseconds since the epoch. */
    @get:Throws(RemoteException::class)
    public val previewReferenceInstant: Instant

    /**
     * The watchface's [OverlayStyle] which configures the system status overlay on
     * Wear 3.0 and beyond. Note for older watch faces which don't support this, the default value
     * will be returned.
     */
    @get:Throws(RemoteException::class)
    public val overlayStyle: OverlayStyle
        // Default implementation, overridden below.
        get() = OverlayStyle()

    /**
     * Renames this instance to [newInstanceId] (must be unique, usually this would be different
     * from the old ID but that's not a requirement). Sets the current [UserStyle] and clears
     * any complication data. Setting the new UserStyle may have a side effect of enabling or
     * disabling complicationSlots, which will be visible via [ComplicationSlotState.isEnabled].
     *
     * NB [setWatchUiState] and [updateWatchFaceInstance] can be called in any order.
     */
    @Throws(RemoteException::class)
    public fun updateWatchFaceInstance(newInstanceId: String, userStyle: UserStyle)

    /**
     * Renames this instance to [newInstanceId] (must be unique, usually this would be different
     * from the old ID but that's not a requirement). Sets the current [UserStyle] represented as a
     * [UserStyleData> and clears any complication data. Setting the new UserStyle may have a
     * side effect of enabling or disabling complicationSlots, which will be visible via
     * [ComplicationSlotState.isEnabled].
     */
    @Throws(RemoteException::class)
    public fun updateWatchFaceInstance(newInstanceId: String, userStyle: UserStyleData)

    /** Returns the ID of this watch face instance. */
    @get:Throws(RemoteException::class)
    public val instanceId: String

    /** The watch face's [UserStyleSchema]. */
    @get:Throws(RemoteException::class)
    public val userStyleSchema: UserStyleSchema

    /**
     * Map of [androidx.wear.watchface.ComplicationSlot] ids to [ComplicationSlotState] for each
     * [ComplicationSlot] registered with the  watch face's [ComplicationSlotsManager]. The
     * ComplicationSlotState is based on the initial state of each
     * [androidx.wear.watchface.ComplicationSlot] plus any overrides from a
     * [ComplicationSlotsUserStyleSetting]. As a consequence ComplicationSlotState may update based
     * on style changes.
     */
    @get:Throws(RemoteException::class)
    public val complicationSlotsState: Map<Int, ComplicationSlotState>

    /**
     * Returns the ID of the [androidx.wear.watchface.ComplicationSlot] at the given coordinates or
     * `null` if there isn't one.
     *
     * Note this currently doesn't support Edge complications.
     */
    @SuppressWarnings("AutoBoxing")
    @Throws(RemoteException::class)
    public fun getComplicationIdAt(@Px x: Int, @Px y: Int): Int? =
        complicationSlotsState.asSequence().firstOrNull {
            it.value.isEnabled && when (it.value.boundsType) {
                ComplicationSlotBoundsType.ROUND_RECT -> it.value.bounds.contains(x, y)
                ComplicationSlotBoundsType.BACKGROUND -> false
                ComplicationSlotBoundsType.EDGE -> false
                else -> false
            }
        }?.key

    public companion object {
        /** Indicates a "down" touch event on the watch face. */
        public const val TAP_TYPE_DOWN: Int = IInteractiveWatchFace.TAP_TYPE_DOWN

        /**
         * Indicates that a previous [TAP_TYPE_DOWN] event has been canceled. This generally happens
         * when the watch face is touched but then a move or long press occurs.
         */
        public const val TAP_TYPE_CANCEL: Int = IInteractiveWatchFace.TAP_TYPE_CANCEL

        /**
         * Indicates that an "up" event on the watch face has occurred that has not been consumed by
         * another activity. A [TAP_TYPE_DOWN] always occur first. This event will not occur if a
         * [TAP_TYPE_CANCEL] is sent.
         */
        public const val TAP_TYPE_UP: Int = IInteractiveWatchFace.TAP_TYPE_UP
    }

    /**
     * Sends a tap event to the watch face for processing.
     *
     * @param xPosition The x-coordinate of the tap in pixels
     * @param yPosition The y-coordinate of the tap in pixels
     * @param tapType The [TapType] of the event
     */
    @Throws(RemoteException::class)
    public fun sendTouchEvent(@Px xPosition: Int, @Px yPosition: Int, @TapType tapType: Int)

    /**
     * Sends a tap event to the watch face for processing and returns a [PendingIntent] if the user
     * tapped on complication, which should be sent on the watch face's behalf. The reason for using
     * this is for 5 seconds after tapping the home button, background applications (such as the
     * watch face) are not allowed by the framework to send intents which can look broken from the
     * user's point of view.
     *
     * Note older watch faces don't support this method and will always return `null`. In this case
     * the watch face will have attempted to send any intent and no further action is needed by the
     * caller. You can use [supportsPendingIntentForTouchEvent] to determine if the watch face
     * supports getPendingIntentForTouchEvent.
     *
     * @param xPosition The x-coordinate of the tap in pixels
     * @param yPosition The y-coordinate of the tap in pixels
     * @param tapType The [TapType] of the event
     * @return A [PendingIntent] to be fired or `null`
     */
    @Throws(RemoteException::class)
    public fun getPendingIntentForTouchEvent(
        @Px xPosition: Int,
        @Px yPosition: Int,
        @TapType tapType: Int
    ): PendingIntent? = null

    /** Whether or not the watch face supports [getPendingIntentForTouchEvent]. */
    public fun supportsPendingIntentForTouchEvent(): Boolean = false

    /**
     * Returns the [ContentDescriptionLabel]s describing the watch face, for the use by screen
     * readers.
     */
    @get:Throws(RemoteException::class)
    public val contentDescriptionLabels: List<ContentDescriptionLabel>

    /**
     * Updates the watch faces [WatchUiState]. NB [setWatchUiState] and [updateWatchFaceInstance]
     * can be called in any order.
     */
    @Throws(RemoteException::class)
    public fun setWatchUiState(watchUiState: androidx.wear.watchface.client.WatchUiState)

    /** Triggers watch face rendering into the surface when in ambient mode. */
    @Throws(RemoteException::class)
    public fun performAmbientTick()

    /**
     * Callback that observes when the client disconnects. Use [addClientDisconnectListener] to
     * register a ClientDisconnectListener.
     */
    public interface ClientDisconnectListener {
        /**
         * The client disconnected, typically due to the server side crashing. Note this is not
         * called in response to [close] being called on [InteractiveWatchFaceClient].
         */
        public fun onClientDisconnected()
    }

    /** Registers a [ClientDisconnectListener]. */
    @AnyThread
    public fun addClientDisconnectListener(listener: ClientDisconnectListener, executor: Executor)

    /**
     * Removes a [ClientDisconnectListener] previously registered by [addClientDisconnectListener].
     */
    @AnyThread
    public fun removeClientDisconnectListener(listener: ClientDisconnectListener)

    /** Returns true if the connection to the server side is alive. */
    @AnyThread
    public fun isConnectionAlive(): Boolean

    /**
     * Interface passed to [addOnWatchFaceReadyListener] which calls
     * [OnWatchFaceReadyListener.onWatchFaceReady] when the watch face is ready to render. Use
     * [addOnWatchFaceReadyListener] to register a OnWatchFaceReadyListener.
     */
    public fun interface OnWatchFaceReadyListener {
        /**
         * Called when the watchface is ready to render.
         *
         * Note in the event of the watch face disconnecting (e.g. due to a crash) this callback
         * will never fire. Use [ClientDisconnectListener] to observe disconnects.
         */
        public fun onWatchFaceReady()
    }

    /**
     * Registers a [OnWatchFaceReadyListener] which gets called when the watch face is ready to
     * render.
     *
     * Note in the event of the watch face disconnecting (e.g. due to a crash) the listener will
     * never get called. Use [ClientDisconnectListener] to observe disconnects.
     *
     * @param executor The [Executor] on which to run [OnWatchFaceReadyListener].
     * @param listener The [OnWatchFaceReadyListener] to run when the watchface is ready to render.
     */
    public fun addOnWatchFaceReadyListener(executor: Executor, listener: OnWatchFaceReadyListener)

    /**
     * Stops listening for events registered by [addOnWatchFaceReadyListener].
     */
    public fun removeOnWatchFaceReadyListener(listener: OnWatchFaceReadyListener)
}

/** Controls a stateful remote interactive watch face. */
internal class InteractiveWatchFaceClientImpl internal constructor(
    private val iInteractiveWatchFace: IInteractiveWatchFace
) : InteractiveWatchFaceClient {

    private val lock = Any()
    private val disconnectListeners =
        HashMap<InteractiveWatchFaceClient.ClientDisconnectListener, Executor>()
    private val readyListeners =
        HashMap<InteractiveWatchFaceClient.OnWatchFaceReadyListener, Executor>()
    private var watchfaceReadyListenerRegistered = false
    private var closed = false

    init {
        iInteractiveWatchFace.asBinder().linkToDeath(
            {
                var listenerCopy:
                    HashMap<InteractiveWatchFaceClient.ClientDisconnectListener, Executor>

                synchronized(lock) {
                    listenerCopy = HashMap(disconnectListeners)
                }

                for ((listener, executor) in listenerCopy) {
                    executor.execute {
                        listener.onClientDisconnected()
                    }
                }
            },
            0
        )
    }

    override fun updateComplicationData(
        slotIdToComplicationData: Map<Int, ComplicationData>
    ) = TraceEvent("InteractiveWatchFaceClientImpl.updateComplicationData").use {
        iInteractiveWatchFace.updateComplicationData(
            slotIdToComplicationData.map {
                IdAndComplicationDataWireFormat(it.key, it.value.asWireComplicationData())
            }
        )
    }

    @RequiresApi(27)
    override fun renderWatchFaceToBitmap(
        renderParameters: RenderParameters,
        instant: Instant,
        userStyle: UserStyle?,
        idAndComplicationData: Map<Int, ComplicationData>?
    ): Bitmap = TraceEvent("InteractiveWatchFaceClientImpl.renderWatchFaceToBitmap").use {
        SharedMemoryImage.ashmemReadImageBundle(
            iInteractiveWatchFace.renderWatchFaceToBitmap(
                WatchFaceRenderParams(
                    renderParameters.toWireFormat(),
                    instant.toEpochMilli(),
                    userStyle?.toWireFormat(),
                    idAndComplicationData?.map {
                        IdAndComplicationDataWireFormat(
                            it.key,
                            it.value.asWireComplicationData()
                        )
                    }
                )
            )
        )
    }

    override val previewReferenceInstant: Instant
        get() = Instant.ofEpochMilli(iInteractiveWatchFace.previewReferenceTimeMillis)

    override val overlayStyle: OverlayStyle
        get() {
            return if (iInteractiveWatchFace.apiVersion >= 4) {
                val wireFormat = iInteractiveWatchFace.watchFaceOverlayStyle
                OverlayStyle(
                    wireFormat.backgroundColor,
                    wireFormat.foregroundColor
                )
            } else {
                OverlayStyle(null, null)
            }
        }

    override fun updateWatchFaceInstance(newInstanceId: String, userStyle: UserStyle) = TraceEvent(
        "InteractiveWatchFaceClientImpl.updateInstance"
    ).use {
        iInteractiveWatchFace.updateWatchfaceInstance(newInstanceId, userStyle.toWireFormat())
    }

    override fun updateWatchFaceInstance(
        newInstanceId: String,
        userStyle: UserStyleData
    ) = TraceEvent(
        "InteractiveWatchFaceClientImpl.updateInstance"
    ).use {
        iInteractiveWatchFace.updateWatchfaceInstance(
            newInstanceId,
            userStyle.toWireFormat()
        )
    }

    override val instanceId: String
        get() = iInteractiveWatchFace.instanceId

    override val userStyleSchema: UserStyleSchema
        get() = UserStyleSchema(iInteractiveWatchFace.userStyleSchema)

    override val complicationSlotsState: Map<Int, ComplicationSlotState>
        get() = iInteractiveWatchFace.complicationDetails.associateBy(
            { it.id },
            { ComplicationSlotState(it.complicationState) }
        )

    override fun close() = TraceEvent("InteractiveWatchFaceClientImpl.close").use {
        iInteractiveWatchFace.release()
        synchronized(lock) {
            closed = true
        }
    }

    override fun sendTouchEvent(
        xPosition: Int,
        yPosition: Int,
        @TapType tapType: Int
    ) = TraceEvent("InteractiveWatchFaceClientImpl.sendTouchEvent").use {
        iInteractiveWatchFace.sendTouchEvent(xPosition, yPosition, tapType)
    }

    override fun getPendingIntentForTouchEvent(
        @Px xPosition: Int,
        @Px yPosition: Int,
        @TapType tapType: Int
    ) = TraceEvent("InteractiveWatchFaceClientImpl.sendTouchEvent").use {
        if (iInteractiveWatchFace.apiVersion >= 3) {
            iInteractiveWatchFace.getPendingIntentForTouchEvent(xPosition, yPosition, tapType)
        } else {
            iInteractiveWatchFace.sendTouchEvent(xPosition, yPosition, tapType)
            null
        }
    }

    override fun supportsPendingIntentForTouchEvent(): Boolean =
        iInteractiveWatchFace.apiVersion >= 3

    override val contentDescriptionLabels: List<ContentDescriptionLabel>
        get() = iInteractiveWatchFace.contentDescriptionLabels.map {
            ContentDescriptionLabel(
                it.text.toApiComplicationText(),
                it.bounds,
                it.tapAction
            )
        }

    override fun setWatchUiState(
        watchUiState: androidx.wear.watchface.client.WatchUiState
    ) = TraceEvent(
        "InteractiveWatchFaceClientImpl.setSystemState"
    ).use {
        iInteractiveWatchFace.setWatchUiState(
            WatchUiState(
                watchUiState.inAmbientMode,
                watchUiState.interruptionFilter
            )
        )
    }

    override fun performAmbientTick() = TraceEvent(
        "InteractiveWatchFaceClientImpl.performAmbientTick"
    ).use {
        iInteractiveWatchFace.ambientTickUpdate()
    }

    override fun addClientDisconnectListener(
        listener: InteractiveWatchFaceClient.ClientDisconnectListener,
        executor: Executor
    ) {
        synchronized(lock) {
            require(!disconnectListeners.contains(listener)) {
                "Don't call addClientDisconnectListener multiple times for the same listener"
            }
            disconnectListeners.put(listener, executor)
        }
    }

    override fun removeClientDisconnectListener(
        listener: InteractiveWatchFaceClient.ClientDisconnectListener
    ) {
        synchronized(lock) {
            disconnectListeners.remove(listener)
        }
    }

    override fun isConnectionAlive() =
        iInteractiveWatchFace.asBinder().isBinderAlive && synchronized(lock) { !closed }

    private fun registerWatchfaceReadyListener() {
        if (watchfaceReadyListenerRegistered) {
            return
        }
        if (iInteractiveWatchFace.apiVersion >= 2) {
            iInteractiveWatchFace.addWatchfaceReadyListener(
                object : IWatchfaceReadyListener.Stub() {
                    override fun getApiVersion(): Int = IWatchfaceReadyListener.API_VERSION

                    override fun onWatchfaceReady() {
                        this@InteractiveWatchFaceClientImpl.onWatchFaceReady()
                    }
                }
            )
        } else {
            // We can emulate this on an earlier API by using a call to get userStyleSchema that
            // will block until the watch face is ready. to Avoid blocking the current thread we
            // spin up a temporary thread.
            val thread = HandlerThread("addWatchFaceReadyListener")
            thread.start()
            val handler = Handler(thread.looper)
            handler.post {
                iInteractiveWatchFace.userStyleSchema
                this@InteractiveWatchFaceClientImpl.onWatchFaceReady()
                thread.quitSafely()
            }
        }
        watchfaceReadyListenerRegistered = true
    }

    internal fun onWatchFaceReady() {
        var listenerCopy: HashMap<InteractiveWatchFaceClient.OnWatchFaceReadyListener, Executor>

        synchronized(lock) {
            listenerCopy = HashMap(readyListeners)
        }

        for ((listener, executor) in listenerCopy) {
            executor.execute {
                listener.onWatchFaceReady()
            }
        }
    }

    override fun addOnWatchFaceReadyListener(
        executor: Executor,
        listener: InteractiveWatchFaceClient.OnWatchFaceReadyListener
    ) {
        synchronized(lock) {
            require(!readyListeners.contains(listener)) {
                "Don't call addWatchFaceReadyListener multiple times for the same listener"
            }
            registerWatchfaceReadyListener()
            readyListeners.put(listener, executor)
        }
    }

    override fun removeOnWatchFaceReadyListener(
        listener: InteractiveWatchFaceClient.OnWatchFaceReadyListener
    ) {
        synchronized(lock) {
            readyListeners.remove(listener)
        }
    }
}