/*
* 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.graphics.Canvas
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.os.Bundle
import androidx.annotation.ColorInt
import androidx.annotation.IntDef
import androidx.annotation.Px
import androidx.annotation.RestrictTo
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.wear.watchface.complications.ComplicationSlotBounds
import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.complications.data.EmptyComplicationData
import androidx.wear.watchface.complications.data.NoDataComplicationData
import androidx.wear.watchface.RenderParameters.HighlightedElement
import androidx.wear.watchface.complications.data.toApiComplicationData
import androidx.wear.watchface.style.UserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.time.Instant
import java.time.ZonedDateTime
/**
* Interface for rendering complicationSlots onto a [Canvas]. These should be created by
* [CanvasComplicationFactory.create]. If state needs to be shared with the [Renderer] that should
* be set up inside [onRendererCreated].
*/
public interface CanvasComplication {
/** Interface for observing when a [CanvasComplication] needs the screen to be redrawn. */
public interface InvalidateCallback {
/** Signals that the complication needs to be redrawn. Can be called on any thread. */
public fun onInvalidate()
}
/**
* Called once on a background thread before any subsequent UI thread rendering to inform the
* CanvasComplication of the [Renderer] which is useful if they need to share state. Note the
* [Renderer] is created asynchronously which is why we can't pass it in via
* [CanvasComplicationFactory.create] as it may not be available at that time.
*/
@WorkerThread
public fun onRendererCreated(renderer: Renderer) {}
/**
* Draws the complication defined by [getData] 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 zonedDateTime The [ZonedDateTime] to render with
* @param renderParameters The current [RenderParameters]
* @param slotId The Id of the [ComplicationSlot] being rendered
*/
@UiThread
public fun render(
canvas: Canvas,
bounds: Rect,
zonedDateTime: ZonedDateTime,
renderParameters: RenderParameters,
slotId: Int
)
/**
* Draws a highlight for a [ComplicationSlotBoundsType.ROUND_RECT] complication. The default
* implementation does this by drawing a dashed line around the complication, other visual
* effects may be used if desired.
*
* @param canvas The [Canvas] to render into
* @param bounds A [Rect] describing the bounds of the complication
* @param boundsType The [ComplicationSlotBoundsType] of the complication
* @param zonedDateTime The [ZonedDateTime] to render the highlight with
* @param color The color to render the highlight with
*/
public fun drawHighlight(
canvas: Canvas,
bounds: Rect,
@ComplicationSlotBoundsType boundsType: Int,
zonedDateTime: ZonedDateTime,
@ColorInt color: Int
)
/** Returns the [ComplicationData] to render with. */
public fun getData(): ComplicationData
/**
* Sets the [ComplicationData] to render with and loads any [Drawable]s contained within the
* ComplicationData. You can choose whether this is done synchronously or asynchronously via
* [loadDrawablesAsynchronous]. When any asynchronous loading has completed
* [InvalidateCallback.onInvalidate] must be called.
*
* @param complicationData The [ComplicationData] to render with
* @param loadDrawablesAsynchronous Whether or not any drawables should be loaded asynchronously
*/
public fun loadData(complicationData: ComplicationData, loadDrawablesAsynchronous: Boolean)
}
/** Interface for determining whether a tap hits a complication. */
public interface ComplicationTapFilter {
/**
* Performs a hit test, returning `true` if the supplied coordinates in pixels are within the
* the provided [complicationSlot] scaled to [screenBounds].
*
* @param complicationSlot The [ComplicationSlot] to perform a hit test for.
* @param screenBounds A [Rect] describing the bounds of the display.
* @param x The screen space X coordinate in pixels.
* @param y The screen space Y coordinate in pixels.
*/
public fun hitTest(
complicationSlot: ComplicationSlot,
screenBounds: Rect,
@Px x: Int,
@Px y: Int
): Boolean
}
/** Default [ComplicationTapFilter] for [ComplicationSlotBoundsType.ROUND_RECT] complicationSlots. */
public class RoundRectComplicationTapFilter : ComplicationTapFilter {
override fun hitTest(
complicationSlot: ComplicationSlot,
screenBounds: Rect,
@Px x: Int,
@Px y: Int
): Boolean = complicationSlot.computeBounds(screenBounds).contains(x, y)
}
/** Default [ComplicationTapFilter] for [ComplicationSlotBoundsType.BACKGROUND] complicationSlots. */
public class BackgroundComplicationTapFilter : ComplicationTapFilter {
override fun hitTest(
complicationSlot: ComplicationSlot,
screenBounds: Rect,
@Px x: Int,
@Px y: Int
): Boolean = false
}
/** @hide */
@IntDef(
value = [
ComplicationSlotBoundsType.ROUND_RECT,
ComplicationSlotBoundsType.BACKGROUND,
ComplicationSlotBoundsType.EDGE
]
)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public annotation class ComplicationSlotBoundsType {
public companion object {
/** The default, most complication slots are either circular or rounded rectangles. */
public const val ROUND_RECT: Int = 0
/**
* For a full screen image complication slot drawn behind the watch face. Note you can only
* have a single background complication slot.
*/
public const val BACKGROUND: Int = 1
/** For edge of screen complication slots. */
public const val EDGE: Int = 2
}
}
/**
* Represents the slot an individual complication on the screen may go in. The number of
* ComplicationSlots is fixed (see [ComplicationSlotsManager]) but ComplicationSlots can be
* enabled or disabled via [UserStyleSetting.ComplicationSlotsUserStyleSetting].
*
* @param id The Watch Face's ID for the complication slot.
* @param accessibilityTraversalIndex Used to sort Complications when generating accessibility
* content description labels.
* @param boundsType The [ComplicationSlotBoundsType] of the complication slot.
* @param bounds The complication slot's [ComplicationSlotBounds].
* @param canvasComplicationFactory The [CanvasComplicationFactory] used to generate a
* [CanvasComplication] for rendering the complication. The factory allows us to decouple
* ComplicationSlot from potentially expensive asset loading.
* @param supportedTypes The list of [ComplicationType]s accepted by this complication slot. Used
* during complication data source selection, this list should be non-empty.
* @param defaultPolicy The [DefaultComplicationDataSourcePolicy] which controls the
* initial complication data source when the watch face is first installed.
* @param defaultDataSourceType The default [ComplicationType] for the default complication data
* source.
* @param initiallyEnabled At creation a complication slot is either enabled or disabled. This
* can be overridden by a [ComplicationSlotsUserStyleSetting] (see
* [ComplicationSlotOverlay.enabled]).
* Editors need to know the initial state of a complication slot to predict the effects of making a
* style change.
* @param configExtras Extras to be merged into the Intent sent when invoking the complication data
* source chooser activity. This features is intended for OEM watch faces where they have elements
* that behave like a complication but are in fact entirely watch face specific.
* @param fixedComplicationDataSource Whether or not the complication data source is fixed (i.e.
* can't be changed by the user). This is useful for watch faces built around specific
* complications.
* @param tapFilter The [ComplicationTapFilter] used to determine whether or not a tap hit the
* complication slot.
* @param nameResourceId The ID of string resource (or `null` if absent) to identify the
* complication slot on screen in an editor. These strings should be short (perhaps 10 characters
* max) E.g. complication slots named 'left' and 'right' might be shown by the editor in a list from
* which the user selects a complication slot for editing.
* @param screenReaderNameResourceId The ID of a string resource (or `null` if absent) for use by a
* watch face editor to identify the complication slot in a screen reader. While similar to
* [nameResourceId] this string can be longer and should be more descriptive. E.g. saying
* 'left complication' rather than just 'left'.
*/
public class ComplicationSlot internal constructor(
public val id: Int,
accessibilityTraversalIndex: Int,
@ComplicationSlotBoundsType public val boundsType: Int,
bounds: ComplicationSlotBounds,
public val canvasComplicationFactory: CanvasComplicationFactory,
supportedTypes: List<ComplicationType>,
defaultPolicy: DefaultComplicationDataSourcePolicy,
defaultDataSourceType: ComplicationType,
@get:JvmName("isInitiallyEnabled")
public val initiallyEnabled: Boolean,
configExtras: Bundle,
@get:JvmName("isFixedComplicationDataSource")
public val fixedComplicationDataSource: Boolean,
public val tapFilter: ComplicationTapFilter,
@get:Suppress("AutoBoxing")
public val nameResourceId: Int?,
@get:Suppress("AutoBoxing")
public val screenReaderNameResourceId: Int?
) {
/**
* The [ComplicationSlotsManager] this is attached to. Only set after the
* [ComplicationSlotsManager] has been created.
*/
internal lateinit var complicationSlotsManager: ComplicationSlotsManager
/**
* Extras to be merged into the Intent sent when invoking the complication data source chooser
* activity.
*/
public var configExtras: Bundle = configExtras
set(value) {
field = value
complicationSlotsManager.configExtrasChangeCallback
?.onComplicationSlotConfigExtrasChanged()
}
/**
* The [CanvasComplication] used to render the complication. This can't be used until after
* [WatchFaceService.createWatchFace] has completed.
*/
public val renderer: CanvasComplication by lazy {
canvasComplicationFactory.create(
complicationSlotsManager.watchState,
object : CanvasComplication.InvalidateCallback {
override fun onInvalidate() {
if (this@ComplicationSlot::invalidateListener.isInitialized) {
invalidateListener.onInvalidate()
}
}
}
)
}
init {
require(id >= 0) { "id must be >= 0" }
require(accessibilityTraversalIndex >= 0) {
"accessibilityTraversalIndex must be >= 0"
}
}
public companion object {
internal val unitSquare = RectF(0f, 0f, 1f, 1f)
/**
* Constructs a [Builder] for a complication with bounds type
* [ComplicationSlotBoundsType.ROUND_RECT]. This is the most common type of complication. These
* can be tapped by the user to trigger the associated intent.
*
* @param id The watch face's ID for this complication. Can be any integer but should be
* unique within the watch face.
* @param canvasComplicationFactory The [CanvasComplicationFactory] to supply the
* [CanvasComplication] to use for rendering. Note renderers should not be shared between
* complicationSlots.
* @param supportedTypes The types of complication supported by this ComplicationSlot. Used
* during complication, this list should be non-empty.
* @param defaultDataSourcePolicy The [DefaultComplicationDataSourcePolicy] used to select
* the initial complication data source when the watch is first installed.
* @param bounds The complication's [ComplicationSlotBounds].
*/
@JvmStatic
public fun createRoundRectComplicationSlotBuilder(
id: Int,
canvasComplicationFactory: CanvasComplicationFactory,
supportedTypes: List<ComplicationType>,
defaultDataSourcePolicy: DefaultComplicationDataSourcePolicy,
bounds: ComplicationSlotBounds
): Builder = Builder(
id,
canvasComplicationFactory,
supportedTypes,
defaultDataSourcePolicy,
ComplicationSlotBoundsType.ROUND_RECT,
bounds,
RoundRectComplicationTapFilter()
)
/**
* Constructs a [Builder] for a complication with bound type
* [ComplicationSlotBoundsType.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 complicationSlots.
*
* @param id The watch face's ID for this complication. Can be any integer but should be
* unique within the watch face.
* @param canvasComplicationFactory The [CanvasComplicationFactory] to supply the
* [CanvasComplication] to use for rendering. Note renderers should not be shared between
* complicationSlots.
* @param supportedTypes The types of complication supported by this ComplicationSlot. Used
* during complication, this list should be non-empty.
* @param defaultDataSourcePolicy The [DefaultComplicationDataSourcePolicy] used to select
* the initial complication data source when the watch is first installed.
*/
@JvmStatic
public fun createBackgroundComplicationSlotBuilder(
id: Int,
canvasComplicationFactory: CanvasComplicationFactory,
supportedTypes: List<ComplicationType>,
defaultDataSourcePolicy: DefaultComplicationDataSourcePolicy
): Builder = Builder(
id,
canvasComplicationFactory,
supportedTypes,
defaultDataSourcePolicy,
ComplicationSlotBoundsType.BACKGROUND,
ComplicationSlotBounds(RectF(0f, 0f, 1f, 1f)),
BackgroundComplicationTapFilter()
)
/**
* Constructs a [Builder] for a complication with bounds type
* [ComplicationSlotBoundsType.EDGE].
*
* An edge complication is drawn around the border of the display and has custom hit test
* logic (see [complicationTapFilter]). When tapped the associated intent is
* dispatched. Edge complicationSlots should have a custom [renderer] with
* [CanvasComplication.drawHighlight] overridden.
*
* Note we don't support edge complication hit testing from an editor.
*
* @param id The watch face's ID for this complication. Can be any integer but should be
* unique within the watch face.
* @param canvasComplicationFactory The [CanvasComplicationFactory] to supply the
* [CanvasComplication] to use for rendering. Note renderers should not be shared between
* complicationSlots.
* @param supportedTypes The types of complication supported by this ComplicationSlot. Used
* during complication, this list should be non-empty.
* @param defaultDataSourcePolicy The [DefaultComplicationDataSourcePolicy] used to select
* the initial complication data source when the watch is first installed.
* @param bounds The complication's [ComplicationSlotBounds]. Its likely the bounding rect
* will be much larger than the complication and shouldn't directly be used for hit testing.
* @param complicationTapFilter The [ComplicationTapFilter] used to determine whether or
* not a tap hit the complication.
*/
@JvmStatic
public fun createEdgeComplicationSlotBuilder(
id: Int,
canvasComplicationFactory: CanvasComplicationFactory,
supportedTypes: List<ComplicationType>,
defaultDataSourcePolicy: DefaultComplicationDataSourcePolicy,
bounds: ComplicationSlotBounds,
complicationTapFilter: ComplicationTapFilter
): Builder = Builder(
id,
canvasComplicationFactory,
supportedTypes,
defaultDataSourcePolicy,
ComplicationSlotBoundsType.EDGE,
bounds,
complicationTapFilter
)
}
/**
* Builder for constructing [ComplicationSlot]s.
*
* @param id The watch face's ID for this complication. Can be any integer but should be unique
* within the watch face.
* @param canvasComplicationFactory The [CanvasComplicationFactory] to supply the
* [CanvasComplication] to use for rendering. Note renderers should not be shared between
* complicationSlots.
* @param supportedTypes The types of complication supported by this ComplicationSlot. Used
* during complication, this list should be non-empty.
* @param defaultDataSourcePolicy The [DefaultComplicationDataSourcePolicy] used to select
* the initial complication data source when the watch is first installed.
* @param boundsType The [ComplicationSlotBoundsType] of the complication.
* @param bounds The complication's [ComplicationSlotBounds].
* @param complicationTapFilter The [ComplicationTapFilter] used to perform hit testing for this
* complication.
*/
public class Builder internal constructor(
private val id: Int,
private val canvasComplicationFactory: CanvasComplicationFactory,
private val supportedTypes: List<ComplicationType>,
private var defaultDataSourcePolicy: DefaultComplicationDataSourcePolicy,
@ComplicationSlotBoundsType private val boundsType: Int,
private val bounds: ComplicationSlotBounds,
private val complicationTapFilter: ComplicationTapFilter
) {
private var accessibilityTraversalIndex = id
private var defaultDataSourceType = ComplicationType.NOT_CONFIGURED
private var initiallyEnabled = true
private var configExtras: Bundle = Bundle.EMPTY
private var fixedComplicationDataSource = false
private var nameResourceId: Int? = null
private var screenReaderNameResourceId: Int? = null
init {
require(id >= 0) { "id must be >= 0" }
}
/**
* Sets the initial value used to sort Complications when generating accessibility content
* description labels. By default this is [id].
*/
public fun setAccessibilityTraversalIndex(accessibilityTraversalIndex: Int): Builder {
this.accessibilityTraversalIndex = accessibilityTraversalIndex
require(accessibilityTraversalIndex >= 0) {
"accessibilityTraversalIndex must be >= 0"
}
return this
}
/**
* Sets the initial [ComplicationType] to use with the initial complication data source.
* Note care should be taken to ensure [defaultDataSourceType] is compatible with the
* [DefaultComplicationDataSourcePolicy].
*/
@Deprecated("Instead set DefaultComplicationDataSourcePolicy" +
".systemDataSourceFallbackDefaultType.")
public fun setDefaultDataSourceType(
defaultDataSourceType: ComplicationType
): Builder {
defaultDataSourcePolicy = when {
defaultDataSourcePolicy.secondaryDataSource != null ->
DefaultComplicationDataSourcePolicy(
defaultDataSourcePolicy.primaryDataSource!!,
defaultDataSourcePolicy.primaryDataSourceDefaultType
?: defaultDataSourceType,
defaultDataSourcePolicy.secondaryDataSource!!,
defaultDataSourcePolicy.secondaryDataSourceDefaultType
?: defaultDataSourceType,
defaultDataSourcePolicy.systemDataSourceFallback,
defaultDataSourceType
)
defaultDataSourcePolicy.primaryDataSource != null ->
DefaultComplicationDataSourcePolicy(
defaultDataSourcePolicy.primaryDataSource!!,
defaultDataSourcePolicy.primaryDataSourceDefaultType
?: defaultDataSourceType,
defaultDataSourcePolicy.systemDataSourceFallback,
defaultDataSourceType
)
else -> DefaultComplicationDataSourcePolicy(
defaultDataSourcePolicy.systemDataSourceFallback,
defaultDataSourceType
)
}
this.defaultDataSourceType = defaultDataSourceType
return this
}
/**
* Whether the complication is initially enabled or not (by default its enabled). This can
* be overridden by [ComplicationSlotsUserStyleSetting].
*/
public fun setEnabled(enabled: Boolean): Builder {
this.initiallyEnabled = enabled
return this
}
/**
* Sets optional extras to be merged into the Intent sent when invoking the complication
* data source chooser activity.
*/
public fun setConfigExtras(extras: Bundle): Builder {
this.configExtras = extras
return this
}
/**
* Whether or not the complication source is fixed (i.e. the user can't change it).
*/
@Suppress("MissingGetterMatchingBuilder")
public fun setFixedComplicationDataSource(fixedComplicationDataSource: Boolean): Builder {
this.fixedComplicationDataSource = fixedComplicationDataSource
return this
}
/**
* If non-null sets the ID of a string resource containing the name of this complication
* slot, for use visually in an editor. This resource should be short and should not contain
* the word "Complication". E.g. "Left" for the left complication.
*/
public fun setNameResourceId(
@Suppress("AutoBoxing") nameResourceId: Int?
): Builder {
this.nameResourceId = nameResourceId
return this
}
/**
* If non-null sets the ID of a string resource containing the name of this complication
* slot, for use by a screen reader. This resource should be a short sentence. E.g.
* "Left complication" for the left complication.
*/
public fun setScreenReaderNameResourceId(
@Suppress("AutoBoxing") screenReaderNameResourceId: Int?
): Builder {
this.screenReaderNameResourceId = screenReaderNameResourceId
return this
}
/** Constructs the [ComplicationSlot]. */
public fun build(): ComplicationSlot = ComplicationSlot(
id,
accessibilityTraversalIndex,
boundsType,
bounds,
canvasComplicationFactory,
supportedTypes,
defaultDataSourcePolicy,
defaultDataSourceType,
initiallyEnabled,
configExtras,
fixedComplicationDataSource,
complicationTapFilter,
nameResourceId,
screenReaderNameResourceId
)
}
internal interface InvalidateListener {
/** Requests redraw. Can be called on any thread */
fun onInvalidate()
}
private lateinit var invalidateListener: InvalidateListener
internal var complicationBoundsDirty = true
/**
* The complication's [ComplicationSlotBounds] 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 complicationSlotBounds: ComplicationSlotBounds = bounds
@UiThread
get
@UiThread
internal set(value) {
require(boundsType != ComplicationSlotBoundsType.BACKGROUND)
if (field == value) {
return
}
field = value
complicationBoundsDirty = true
}
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
}
internal var supportedTypesDirty = true
/** The types of complicationSlots 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
}
internal var defaultDataSourcePolicyDirty = true
/**
* The [DefaultComplicationDataSourcePolicy] which defines the default complicationSlots
* providers selected when the user hasn't yet made a choice. See also [defaultDataSourceType].
*/
public var defaultDataSourcePolicy: DefaultComplicationDataSourcePolicy = defaultPolicy
@UiThread
get
@UiThread
internal set(value) {
if (field == value) {
return
}
field = value
defaultDataSourcePolicyDirty = true
}
internal var defaultDataSourceTypeDirty = true
/**
* The default [ComplicationType] to use alongside [defaultDataSourcePolicy].
*/
@Deprecated("Use DefaultComplicationDataSourcePolicy." +
"systemDataSourceFallbackDefaultType instead")
public var defaultDataSourceType: ComplicationType = defaultDataSourceType
@UiThread
get
@UiThread
internal set(value) {
if (field == value) {
return
}
field = value
defaultDataSourceTypeDirty = true
}
internal var accessibilityTraversalIndexDirty = true
/**
* This is used to determine the order in which accessibility labels for the watch face are
* read to the user. Accessibility labels are automatically generated for the time and
* complicationSlots. See also [Renderer.additionalContentDescriptionLabels].
*/
public var accessibilityTraversalIndex: Int = accessibilityTraversalIndex
@UiThread
get
@UiThread
internal set(value) {
require(value >= 0) {
"accessibilityTraversalIndex must be >= 0"
}
if (field == value) {
return
}
field = value
accessibilityTraversalIndexDirty = true
}
internal var dataDirty = true
/**
* The [androidx.wear.watchface.complications.data.ComplicationData] associated with the
* [ComplicationSlot]. This defaults to [NoDataComplicationData].
*/
public val complicationData: StateFlow<ComplicationData> =
MutableStateFlow(NoDataComplicationData())
/**
* The complication data sent by the system. This may contain a timeline out of which
* [complicationData] is selected.
*/
private var timelineComplicationData: ComplicationData = NoDataComplicationData()
private var timelineEntries: List<ComplicationData>? = null
/**
* Sets the current [ComplicationData] and if it's a timeline, the correct override for
* [instant] is chosen.
*/
internal fun setComplicationData(
complicationData: ComplicationData,
loadDrawablesAsynchronous: Boolean,
instant: Instant
) {
timelineComplicationData = complicationData
timelineEntries = complicationData.asWireComplicationData().timelineEntries?.map {
it.toApiComplicationData()
}
selectComplicationDataForInstant(instant, loadDrawablesAsynchronous, true)
}
/**
* If the current [ComplicationData] is a timeline, the correct override for [instant] is
* chosen.
*/
internal fun selectComplicationDataForInstant(
instant: Instant,
loadDrawablesAsynchronous: Boolean,
forceUpdate: Boolean
) {
var previousShortest = Long.MAX_VALUE
val time = instant.epochSecond
var best = timelineComplicationData
// Select the shortest valid timeline entry.
timelineEntries?.let {
for (entry in it) {
val wireEntry = entry.asWireComplicationData()
val start = wireEntry.timelineStartEpochSecond
val end = wireEntry.timelineEndEpochSecond
if (start != null && end != null && time >= start && time < end) {
val duration = end - start
if (duration < previousShortest) {
previousShortest = duration
best = entry
}
}
}
}
if (forceUpdate || complicationData.value != best) {
(complicationData as MutableStateFlow).value = best
renderer.loadData(best, loadDrawablesAsynchronous)
}
}
/**
* Whether or not the complication should be considered active and should be rendered at the
* specified time.
*/
public fun isActiveAt(instant: Instant): Boolean {
return when (complicationData.value.type) {
ComplicationType.NO_DATA -> false
ComplicationType.NO_PERMISSION -> false
ComplicationType.EMPTY -> false
else -> complicationData.value.validTimeRange.contains(instant)
}
}
/**
* Watch faces should use this method to render a complication. Note the system may call this.
*
* @param canvas The [Canvas] to render into
* @param zonedDateTime The [ZonedDateTime] to render with
* @param renderParameters The current [RenderParameters]
*/
@UiThread
public fun render(
canvas: Canvas,
zonedDateTime: ZonedDateTime,
renderParameters: RenderParameters
) {
val bounds = computeBounds(Rect(0, 0, canvas.width, canvas.height))
renderer.render(canvas, bounds, zonedDateTime, renderParameters, id)
}
/**
* Watch faces should use this method to render non-fixed complicationSlots for any highlight
* layer pass. Note the system may call this.
*
* @param canvas The [Canvas] to render into
* @param zonedDateTime The [ZonedDateTime] to render with
* @param renderParameters The current [RenderParameters]
*/
@UiThread
public fun renderHighlightLayer(
canvas: Canvas,
zonedDateTime: ZonedDateTime,
renderParameters: RenderParameters
) {
// It's only sensible to render a highlight for non-fixed ComplicationSlots because you
// can't edit fixed complicationSlots.
if (fixedComplicationDataSource) {
return
}
val bounds = computeBounds(Rect(0, 0, canvas.width, canvas.height))
when (val highlightedElement = renderParameters.highlightLayer?.highlightedElement) {
is HighlightedElement.AllComplicationSlots -> {
renderer.drawHighlight(
canvas,
bounds,
boundsType,
zonedDateTime,
renderParameters.highlightLayer.highlightTint
)
}
is HighlightedElement.ComplicationSlot -> {
if (highlightedElement.id == id) {
renderer.drawHighlight(
canvas,
bounds,
boundsType,
zonedDateTime,
renderParameters.highlightLayer.highlightTint
)
}
}
is HighlightedElement.UserStyle -> {
// Nothing
}
null -> {
// Nothing
}
}
}
internal fun init(invalidateListener: InvalidateListener, isHeadless: Boolean) {
this.invalidateListener = invalidateListener
if (isHeadless) {
timelineComplicationData = EmptyComplicationData()
(complicationData as MutableStateFlow).value = EmptyComplicationData()
}
}
/**
* Computes the bounds of the complication by converting the unitSquareBounds of the specified
* [complicationType] to pixels based on the [screen]'s dimensions.
*
* @param screen A [Rect] describing the dimensions of the screen.
* @param complicationType The [ComplicationType] to use when looking up the slot's
* [ComplicationSlotBounds.perComplicationTypeBounds].
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun computeBounds(screen: Rect, complicationType: ComplicationType): Rect {
val unitSquareBounds = complicationSlotBounds.perComplicationTypeBounds[complicationType]!!
unitSquareBounds.intersect(unitSquare)
// We add 0.5 to make toInt() round to the nearest whole number rather than truncating.
return Rect(
(0.5f + unitSquareBounds.left * screen.width()).toInt(),
(0.5f + unitSquareBounds.top * screen.height()).toInt(),
(0.5f + unitSquareBounds.right * screen.width()).toInt(),
(0.5f + unitSquareBounds.bottom * screen.height()).toInt()
)
}
/**
* Computes the bounds of the complication by converting the unitSquareBounds of the current
* complication type to pixels based on the [screen]'s dimensions.
*
* @param screen A [Rect] describing the dimensions of the screen.
*/
public fun computeBounds(screen: Rect): Rect =
computeBounds(screen, complicationData.value.type)
@UiThread
internal fun dump(writer: IndentingPrintWriter) {
writer.println("ComplicationSlot $id:")
writer.increaseIndent()
writer.println("fixedComplicationDataSource=$fixedComplicationDataSource")
writer.println("enabled=$enabled")
writer.println("boundsType=$boundsType")
writer.println("configExtras=$configExtras")
writer.println("supportedTypes=${supportedTypes.joinToString { it.toString() }}")
writer.println("initiallyEnabled=$initiallyEnabled")
writer.println(
"defaultDataSourcePolicy.primaryDataSource=${defaultDataSourcePolicy.primaryDataSource}"
)
writer.println("defaultDataSourcePolicy.primaryDataSourceDefaultDataSourceType=" +
defaultDataSourcePolicy.primaryDataSourceDefaultType)
writer.println(
"defaultDataSourcePolicy.secondaryDataSource=" +
defaultDataSourcePolicy.secondaryDataSource
)
writer.println("defaultDataSourcePolicy.secondaryDataSourceDefaultDataSourceType=" +
defaultDataSourcePolicy.secondaryDataSourceDefaultType)
writer.println(
"defaultDataSourcePolicy.systemDataSourceFallback=" +
defaultDataSourcePolicy.systemDataSourceFallback
)
writer.println("defaultDataSourcePolicy.systemDataSourceFallbackDefaultType=" +
defaultDataSourcePolicy.systemDataSourceFallbackDefaultType)
writer.println("data=${renderer.getData()}")
val bounds = complicationSlotBounds.perComplicationTypeBounds.map {
"${it.key} -> ${it.value}"
}
writer.println("bounds=[$bounds]")
writer.decreaseIndent()
}
}