blob: 1a3a85abf574038a84c8560885e96a347777b910 [file] [log] [blame]
/*
* 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.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
/**
* 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.
*/
@Throws(RemoteException::class)
public fun sendTouchEvent(@Px xPosition: Int, @Px yPosition: Int, @TapType tapType: Int)
/**
* 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 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 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)
}
}
}