/*
* 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
import android.annotation.SuppressLint
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.icu.util.Calendar
import android.os.Bundle
import android.support.wearable.complications.ComplicationData
import androidx.annotation.ColorInt
import androidx.annotation.UiThread
import androidx.wear.complications.ComplicationBounds
import androidx.wear.complications.ComplicationHelperActivity
import androidx.wear.complications.DefaultComplicationProviderPolicy
import androidx.wear.complications.data.ComplicationType
import androidx.wear.complications.data.IdAndComplicationData
import androidx.wear.utility.TraceEvent
import androidx.wear.watchface.complications.rendering.ComplicationDrawable
import androidx.wear.watchface.data.ComplicationBoundsType
import androidx.wear.watchface.style.Layer
import androidx.wear.watchface.style.UserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting
/** Interface for rendering complications onto a [Canvas]. */
public interface CanvasComplication {
/**
* Called when the CanvasComplication attaches to a [Complication]. This will get called during
* [Complication] initialization and if [Complication.renderer] is assigned with this
* CanvasComplication.
*/
@UiThread
public fun onAttach(complication: Complication)
/**
* Called when the CanvasComplication detaches from a [Complication]. This will get called if
* [Complication.renderer] is assigned to a different CanvasComplication.
*/
@UiThread
public fun onDetach()
/**
* Draws the complication defined by [getIdAndData] into the canvas with the specified bounds.
* This will usually be called by user watch face drawing code, but the system may also call it
* for complication selection UI rendering. The width and height will be the same as that
* computed by computeBounds but the translation and canvas size may differ.
*
* @param canvas The [Canvas] to render into
* @param bounds A [Rect] describing the bounds of the complication
* @param calendar The current [Calendar]
* @param renderParameters The current [RenderParameters]
*/
@UiThread
public fun render(
canvas: Canvas,
bounds: Rect,
calendar: Calendar,
renderParameters: RenderParameters
)
/**
* Whether the complication should be drawn highlighted. This is to provide visual feedback when
* the user taps on a complication.
*/
@Suppress("INAPPLICABLE_JVM_NAME") // https://stackoverflow.com/questions/47504279
@get:JvmName("isHighlighted")
@set:JvmName("setIsHighlighted")
public var isHighlighted: Boolean
/** Returns the [IdAndComplicationData] to render with. */
public fun getIdAndData(): IdAndComplicationData?
/**
* Sets the [IdAndComplicationData] to render with.
*
* @param loadDrawablesAsynchronous Whether or not any drawables should be loaded asynchronously
**/
public fun setIdAndData(
idAndComplicationData: IdAndComplicationData?,
loadDrawablesAsynchronous: Boolean
)
/** The [IdAndComplicationData] should be cleared. */
public fun clearIdAndData()
}
/**
* A complication rendered with [ComplicationDrawable] which renders complications in a material
* design style. This renderer can't be shared by multiple complications.
*/
public open class CanvasComplicationDrawable(
/** The [ComplicationDrawable] to render with. */
drawable: ComplicationDrawable,
/**
* The watch's [WatchState] which contains details pertaining to (low-bit) ambient mode and
* burn in protection needed to render correctly.
*/
private val watchState: WatchState
) : CanvasComplication {
init {
drawable.callback = object :
Drawable.Callback {
override fun unscheduleDrawable(who: Drawable, what: Runnable) {}
@SuppressLint("SyntheticAccessor")
override fun invalidateDrawable(who: Drawable) {
attachedComplication?.invalidate()
}
override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {}
}
}
/** The [ComplicationDrawable] to render with. */
public var drawable: ComplicationDrawable = drawable
set(value) {
field = value
value.isInAmbientMode = watchState.isAmbient.value
value.isLowBitAmbient = watchState.hasLowBitAmbient
value.setBurnInProtection(watchState.hasBurnInProtection)
attachedComplication?.scheduleUpdateComplications()
}
private val isAmbientObserver = Observer<Boolean> {
drawable.isInAmbientMode = it
}
private var attachedComplication: Complication? = null
/** {@inheritDoc} */
override fun onAttach(complication: Complication) {
attachedComplication = complication
watchState.isAmbient.addObserver(isAmbientObserver)
}
/** {@inheritDoc} */
override fun onDetach() {
watchState.isAmbient.removeObserver(isAmbientObserver)
attachedComplication = null
}
/** {@inheritDoc} */
override fun render(
canvas: Canvas,
bounds: Rect,
calendar: Calendar,
renderParameters: RenderParameters
) {
when (renderParameters.layerParameters[Layer.COMPLICATIONS]) {
LayerMode.DRAW -> {
drawable.bounds = bounds
drawable.currentTimeMillis = calendar.timeInMillis
drawable.draw(canvas)
}
LayerMode.DRAW_OUTLINED -> {
drawable.bounds = bounds
drawable.currentTimeMillis = calendar.timeInMillis
val wasHighlighted = drawable.isHighlighted
drawable.isHighlighted =
renderParameters.selectedComplicationId == getIdAndData()?.complicationId
drawable.draw(canvas)
drawable.isHighlighted = wasHighlighted
// It's only sensible to render a highlight for non-background complications.
if (attachedComplication?.boundsType != ComplicationBoundsType.BACKGROUND) {
drawOutline(canvas, bounds, calendar, renderParameters.outlineTint)
}
}
LayerMode.HIDE -> return
}
}
/**
* Used (indirectly) by the editor, draws a dashed line around the complication unless the.
* [Complication] is fixed in which case it does nothing.
*/
public open fun drawOutline(
canvas: Canvas,
bounds: Rect,
calendar: Calendar,
@ColorInt color: Int
) {
if (!attachedComplication!!.fixedComplicationProvider) {
ComplicationOutlineRenderer.drawComplicationSelectOutline(
canvas,
bounds,
color
)
}
}
/**
* Whether or not the complication should be drawn highlighted. Used to provide visual feedback
* when the complication is tapped.
*/
override var isHighlighted: Boolean
@Suppress("INAPPLICABLE_JVM_NAME") // https://stackoverflow.com/questions/47504279
@JvmName("isHighlighted")
@UiThread
get() = drawable.isHighlighted
@Suppress("INAPPLICABLE_JVM_NAME") // https://stackoverflow.com/questions/47504279
@JvmName("setIsHighlighted")
@UiThread
set(value) {
drawable.isHighlighted = value
}
private var _idAndData: IdAndComplicationData? = null
override fun getIdAndData(): IdAndComplicationData? = _idAndData
override fun setIdAndData(
idAndComplicationData: IdAndComplicationData?,
loadDrawablesAsynchronous: Boolean
): Unit = TraceEvent("CanvasComplicationDrawable.setIdAndData").use {
_idAndData = idAndComplicationData
drawable.setComplicationData(
idAndComplicationData?.complicationData?.asWireComplicationData(),
loadDrawablesAsynchronous
)
}
override fun clearIdAndData() {
_idAndData = null
drawable.setComplicationData(null, false)
}
}
/**
* Represents a individual complication on the screen. The number of complications is fixed
* (see [ComplicationsManager]) but complications can be enabled or disabled via
* [UserStyleSetting.ComplicationsUserStyleSetting].
*/
public class Complication internal constructor(
internal val id: Int,
@ComplicationBoundsType public val boundsType: Int,
complicationBounds: ComplicationBounds,
canvasComplication: CanvasComplication,
supportedTypes: List<ComplicationType>,
defaultProviderPolicy: DefaultComplicationProviderPolicy,
defaultProviderType: ComplicationType,
/**
* The initial state of the complication. Note complications can be enabled / disabled by
* [UserStyleSetting.ComplicationsUserStyleSetting].
*/
@get:JvmName("isInitiallyEnabled")
public val initiallyEnabled: Boolean,
/** Extras to be merged into the Intent sent when invoking the provider chooser activity. */
public val complicationConfigExtras: Bundle?,
/** Whether or not the complication provider is fixed. */
@get:JvmName("isFixedComplicationProvider")
public val fixedComplicationProvider: Boolean
) {
public companion object {
internal val unitSquare = RectF(0f, 0f, 1f, 1f)
/**
* Constructs a [Builder] for a complication with bounds type
* [ComplicationBoundsType.ROUND_RECT]. This is the most common type of complication.
* These can be single tapped by the user to either trigger the associated intent or
* double tapped to open the provider selector.
*/
@JvmStatic
public fun createRoundRectComplicationBuilder(
/**
* The watch face's ID for this complication. Can be any integer but should be unique
* within the watch face.
*/
id: Int,
/**
* The [CanvasComplication] to use for rendering. Note renderers should not be shared
* between complications.
*/
renderer: CanvasComplication,
/**
* The types of complication supported by this Complication. Passed into
* [ComplicationHelperActivity.createProviderChooserHelperIntent] during complication
* configuration. This list should be non-empty.
*/
supportedTypes: List<ComplicationType>,
/**
* The [DefaultComplicationProviderPolicy] used to select the initial complication
* provider.
*/
defaultProviderPolicy: DefaultComplicationProviderPolicy,
/** The initial [ComplicationBounds]. */
complicationBounds: ComplicationBounds
): Builder = Builder(
id,
renderer,
supportedTypes,
defaultProviderPolicy,
ComplicationBoundsType.ROUND_RECT,
complicationBounds
)
/**
* Constructs a [Builder] for a complication with bound type
* [ComplicationBoundsType.BACKGROUND] whose bounds cover the entire screen. A background
* complication is for watch faces that wish to have a full screen user selectable
* backdrop. This sort of complication isn't clickable and at most one may be present in
* the list of complications.
*/
@JvmStatic
public fun createBackgroundComplicationBuilder(
/**
* The watch face's ID for this complication. Can be any integer but should be unique
* within the watch face.
*/
id: Int,
/**
* The [CanvasComplication] to use for rendering. Note renderers should not be shared
* between complications.
*/
renderer: CanvasComplication,
/**
* The types of complication supported by this Complication. Passed into
* [ComplicationHelperActivity.createProviderChooserHelperIntent] during complication
* configuration. This list should be non-empty.
*/
supportedTypes: List<ComplicationType>,
/**
* The [DefaultComplicationProviderPolicy] used to select the initial complication
* provider.
*/
defaultProviderPolicy: DefaultComplicationProviderPolicy
): Builder = Builder(
id,
renderer,
supportedTypes,
defaultProviderPolicy,
ComplicationBoundsType.BACKGROUND,
ComplicationBounds(RectF(0f, 0f, 1f, 1f))
)
}
/** Builder for constructing [Complication]s. */
public class Builder internal constructor(
private val id: Int,
private val renderer: CanvasComplication,
private val supportedTypes: List<ComplicationType>,
private val defaultProviderPolicy: DefaultComplicationProviderPolicy,
@ComplicationBoundsType private val boundsType: Int,
private val complicationBounds: ComplicationBounds
) {
private var defaultProviderType = ComplicationType.NOT_CONFIGURED
private var initiallyEnabled = true
private var complicationConfigExtras: Bundle? = null
private var fixedComplicationProvider = false
/**
* Sets the initial [ComplicationType] to use with the initial complication provider.
* Note care should be taken to ensure [defaultProviderType] is compatible with the
* [DefaultComplicationProviderPolicy].
*/
public fun setDefaultProviderType(
defaultProviderType: ComplicationType
): Builder {
this.defaultProviderType = defaultProviderType
return this
}
/**
* Whether the complication is initially enabled or not (by default its enabled). This can
* be overridden by [ComplicationsUserStyleSetting].
*/
public fun setEnabled(enabled: Boolean): Builder {
this.initiallyEnabled = enabled
return this
}
/**
* Sets optional extras to be merged into the Intent sent when invoking the provider chooser
* activity.
*/
public fun setComplicationConfigExtras(extras: Bundle?): Builder {
this.complicationConfigExtras = extras
return this
}
/**
* Whether or not the complication is fixed (i.e. the user can't change it).
*/
public fun setFixedComplicationProvider(fixedComplicationProvider: Boolean): Builder {
this.fixedComplicationProvider = fixedComplicationProvider
return this
}
/** Constructs the [Complication]. */
public fun build(): Complication = Complication(
id,
boundsType,
complicationBounds,
renderer,
supportedTypes,
defaultProviderPolicy,
defaultProviderType,
initiallyEnabled,
complicationConfigExtras,
fixedComplicationProvider
)
}
init {
canvasComplication.onAttach(this)
}
internal interface InvalidateListener {
/** Requests redraw. */
@UiThread
fun onInvalidate()
}
private lateinit var complicationsManager: ComplicationsManager
private lateinit var invalidateListener: InvalidateListener
internal var complicationBoundsDirty = true
/**
* The complication's [ComplicationBounds] which are converted to pixels during rendering.
*
* Note it's not allowed to change the bounds of a background complication because
* they are assumed to always cover the entire screen.
*/
public var complicationBounds: ComplicationBounds = complicationBounds
@UiThread
get
@UiThread
set(value) {
require(boundsType != ComplicationBoundsType.BACKGROUND)
if (field == value) {
return
}
field = value
complicationBoundsDirty = true
// The caller might modify a number of complications. For efficiency we need to coalesce
// these into one update task.
complicationsManager.scheduleUpdate()
}
internal var enabledDirty = true
/** Whether or not the complication should be drawn and accept taps. */
public var enabled: Boolean = initiallyEnabled
@JvmName("isEnabled")
@UiThread
get
@UiThread
internal set(value) {
if (field == value) {
return
}
field = value
enabledDirty = true
// The caller might enable/disable a number of complications. For efficiency we need
// to coalesce these into one update task.
if (this::complicationsManager.isInitialized) {
complicationsManager.scheduleUpdate()
}
}
/** The [CanvasComplication] used to render the complication. */
public var renderer: CanvasComplication = canvasComplication
@UiThread
get
@UiThread
set(value) {
if (field == value) {
return
}
renderer.onDetach()
value.setIdAndData(renderer.getIdAndData(), true)
field = value
value.onAttach(this)
}
internal var supportedTypesDirty = true
/** The types of complications the complication supports. Must be non-empty. */
public var supportedTypes: List<ComplicationType> = supportedTypes
@UiThread
get
@UiThread
internal set(value) {
if (field == value) {
return
}
require(value.isNotEmpty())
field = value
supportedTypesDirty = true
// The caller might modify a number of complications. For efficiency we need to
// coalesce these into one update task.
if (this::complicationsManager.isInitialized) {
complicationsManager.scheduleUpdate()
}
}
internal var defaultProviderPolicyDirty = true
/**
* The [DefaultComplicationProviderPolicy] which defines the default complications providers
* selected when the user hasn't yet made a choice. See also [defaultProviderType].
*/
public var defaultProviderPolicy: DefaultComplicationProviderPolicy = defaultProviderPolicy
@UiThread
get
@UiThread
internal set(value) {
if (field == value) {
return
}
field = value
defaultProviderPolicyDirty = true
// The caller might modify a number of complications. For efficiency we need to
// coalesce these into one update task.
if (this::complicationsManager.isInitialized) {
complicationsManager.scheduleUpdate()
}
}
internal var defaultProviderTypeDirty = true
/**
* The default [ComplicationData.ComplicationType] to use alongside [defaultProviderPolicy].
*/
public var defaultProviderType: ComplicationType = defaultProviderType
@UiThread
get
@UiThread
internal set(value) {
if (field == value) {
return
}
field = value
defaultProviderTypeDirty = true
// The caller might modify a number of complications. For efficiency we need to
// coalesce these into one update task.
if (this::complicationsManager.isInitialized) {
complicationsManager.scheduleUpdate()
}
}
internal var dataDirty = true
/**
* The [androidx.wear.complications.data.ComplicationData] associated with the [Complication].
*/
public val complicationData:
ObservableWatchData<androidx.wear.complications.data.ComplicationData> =
MutableObservableWatchData()
/**
* Whether or not the complication should be considered active and should be rendered at the
* specified time.
*/
public fun isActiveAt(dateTimeMillis: Long): Boolean {
if (!complicationData.hasValue()) {
return false
}
return when (complicationData.value.type) {
ComplicationType.NO_DATA -> false
ComplicationType.NO_PERMISSION -> false
ComplicationType.EMPTY -> false
else -> complicationData.value.isActiveAt(dateTimeMillis)
}
}
/**
* Watch faces should use this method to render a complication. Note the system may call this.
*
* @param canvas The [Canvas] to render into
* @param calendar The current [Calendar]
* @param renderParameters The current [RenderParameters]
*/
@UiThread
public fun render(
canvas: Canvas,
calendar: Calendar,
renderParameters: RenderParameters
) {
val bounds = computeBounds(Rect(0, 0, canvas.width, canvas.height))
renderer.render(canvas, bounds, calendar, renderParameters)
}
/**
* Sets whether the complication should be drawn highlighted or not. This is to provide visual
* feedback when the user taps on a complication.
*
* @param highlight Whether or not the complication should be drawn highlighted.
*/
internal fun setIsHighlighted(highlight: Boolean) {
renderer.isHighlighted = highlight
}
/**
* Requests redraw of the watch face. Useful when initialization is asynchronous, e.g. when
* loading a [Drawable].
*/
public fun invalidate() {
invalidateListener.onInvalidate()
}
internal fun init(
complicationsManager: ComplicationsManager,
invalidateListener: InvalidateListener
) {
this.complicationsManager = complicationsManager
this.invalidateListener = invalidateListener
}
internal fun scheduleUpdateComplications() {
// In tests this may not be initialized.
if (this::complicationsManager.isInitialized) {
// Update active complications to ensure accessibility data is up to date.
complicationsManager.scheduleUpdate()
}
}
/** Computes the bounds of the complication by converting the unitSquareBounds to pixels. */
public fun computeBounds(screen: Rect): Rect {
// Try the current type if there is one, otherwise fall back to the bounds for the default
// provider type.
val unitSquareBounds =
renderer.getIdAndData()?.let {
complicationBounds.perComplicationTypeBounds[it.complicationData.type]
} ?: complicationBounds.perComplicationTypeBounds[defaultProviderType]!!
unitSquareBounds.intersect(unitSquare)
return Rect(
(unitSquareBounds.left * screen.width()).toInt(),
(unitSquareBounds.top * screen.height()).toInt(),
(unitSquareBounds.right * screen.width()).toInt(),
(unitSquareBounds.bottom * screen.height()).toInt()
)
}
@UiThread
internal fun dump(writer: IndentingPrintWriter) {
writer.println("Complication $id:")
writer.increaseIndent()
writer.println("fixedComplicationProvider=$fixedComplicationProvider")
writer.println("enabled=$enabled")
writer.println("renderer.isHighlighted=${renderer.isHighlighted}")
writer.println("boundsType=$boundsType")
writer.println("complicationConfigExtras=$complicationConfigExtras")
writer.println("supportedTypes=${supportedTypes.joinToString { it.toString() }}")
writer.println("complicationConfigExtras=$complicationConfigExtras")
writer.println(
"defaultProviderPolicy.primaryProvider=${defaultProviderPolicy.primaryProvider}"
)
writer.println(
"defaultProviderPolicy.secondaryProvider=${defaultProviderPolicy.secondaryProvider}"
)
writer.println(
"defaultProviderPolicy.systemProviderFallback=" +
"${defaultProviderPolicy.systemProviderFallback}"
)
writer.println("data=${renderer.getIdAndData()?.complicationData}")
val bounds = complicationBounds.perComplicationTypeBounds.map {
"${it.key} -> ${it.value}"
}
writer.println("bounds=[$bounds]")
writer.decreaseIndent()
}
}