| /* |
| * 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.Build |
| import android.os.Bundle |
| import android.support.wearable.complications.ComplicationData as WireComplicationData |
| import androidx.annotation.ColorInt |
| import androidx.annotation.IntDef |
| import androidx.annotation.Px |
| import androidx.annotation.RestrictTo |
| import androidx.annotation.UiThread |
| import androidx.annotation.VisibleForTesting |
| import androidx.annotation.WorkerThread |
| import androidx.wear.watchface.RenderParameters.HighlightedElement |
| import androidx.wear.watchface.complications.ComplicationDataSourceInfo |
| 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.ComplicationDisplayPolicies |
| import androidx.wear.watchface.complications.data.ComplicationExperimental |
| 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.complications.data.toApiComplicationData |
| import androidx.wear.watchface.data.BoundingArcWireFormat |
| import androidx.wear.watchface.style.UserStyleSetting |
| import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting |
| import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay |
| import java.lang.Integer.min |
| import java.time.Instant |
| import java.time.LocalDateTime |
| import java.time.ZoneId |
| import java.time.ZonedDateTime |
| import java.util.Objects |
| import kotlin.math.abs |
| import kotlin.math.atan2 |
| import kotlin.math.cos |
| import kotlin.math.sin |
| import kotlinx.coroutines.flow.MutableStateFlow |
| import kotlinx.coroutines.flow.StateFlow |
| |
| /** |
| * 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]. |
| */ |
| @JvmDefaultWithCompatibility |
| 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 [ComplicationSlotBoundsTypeIntDef] of the complication |
| * @param zonedDateTime The [ZonedDateTime] to render the highlight with |
| * @param color The color to render the highlight with |
| */ |
| // TODO(b/230364881): Deprecate this when BoundingArc is no longer experimental. |
| public fun drawHighlight( |
| canvas: Canvas, |
| bounds: Rect, |
| @ComplicationSlotBoundsTypeIntDef boundsType: Int, |
| zonedDateTime: ZonedDateTime, |
| @ColorInt color: 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 [ComplicationSlotBoundsTypeIntDef] of the complication |
| * @param zonedDateTime The [ZonedDateTime] to render the highlight with |
| * @param color The color to render the highlight with |
| */ |
| @ComplicationExperimental |
| public fun drawHighlight( |
| canvas: Canvas, |
| bounds: Rect, |
| @ComplicationSlotBoundsTypeIntDef boundsType: Int, |
| zonedDateTime: ZonedDateTime, |
| @ColorInt color: Int, |
| boundingArc: BoundingArc? |
| ) { |
| drawHighlight(canvas, bounds, boundsType, zonedDateTime, color) |
| } |
| |
| /** 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. */ |
| @JvmDefaultWithCompatibility |
| 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. |
| * @param includeMargins Whether or not the margins should be included |
| */ |
| @Suppress("DEPRECATION") |
| public fun hitTest( |
| complicationSlot: ComplicationSlot, |
| screenBounds: Rect, |
| @Px x: Int, |
| @Px y: Int, |
| includeMargins: Boolean |
| ): Boolean = hitTest(complicationSlot, screenBounds, x, y) |
| |
| /** |
| * 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. |
| */ |
| @Deprecated( |
| "hitTest without specifying includeMargins is deprecated", |
| replaceWith = ReplaceWith("hitTest(ComplicationSlot, Rect, Int, Int, Boolean)") |
| ) |
| public fun hitTest( |
| complicationSlot: ComplicationSlot, |
| screenBounds: Rect, |
| @Px x: Int, |
| @Px y: Int |
| ): Boolean = hitTest(complicationSlot, screenBounds, x, y, false) |
| } |
| |
| /** |
| * Default [ComplicationTapFilter] for [ComplicationSlotBoundsType.ROUND_RECT] complicationSlots. |
| */ |
| public class RoundRectComplicationTapFilter : ComplicationTapFilter { |
| override fun hitTest( |
| complicationSlot: ComplicationSlot, |
| screenBounds: Rect, |
| @Px x: Int, |
| @Px y: Int, |
| includeMargins: Boolean |
| ): Boolean = complicationSlot.computeBounds(screenBounds, includeMargins).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, |
| includeMargins: Boolean |
| ): Boolean = false |
| } |
| |
| @IntDef( |
| value = |
| [ |
| ComplicationSlotBoundsType.ROUND_RECT, |
| ComplicationSlotBoundsType.BACKGROUND, |
| ComplicationSlotBoundsType.EDGE |
| ] |
| ) |
| @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
| public annotation class ComplicationSlotBoundsTypeIntDef |
| |
| /** The various types of [ComplicationSlot] bounds. */ |
| public object ComplicationSlotBoundsType { |
| /** 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 |
| } |
| |
| /** |
| * In combination with a bounding [Rect], BoundingArc describes the geometry of an edge |
| * complication. |
| * |
| * @property startAngle The staring angle of the arc in degrees (0 degrees = 12 o'clock position). |
| * @property totalAngle The total angle of the arc on degrees. |
| * @property thickness The thickness of the arc as a fraction of min(boundingRect.width, |
| * boundingRect.height). |
| */ |
| @ComplicationExperimental |
| public class BoundingArc(val startAngle: Float, val totalAngle: Float, @Px val thickness: Float) { |
| /** |
| * Detects whether the supplied point falls within the edge complication's arc. |
| * |
| * @param rect The bounding [Rect] of the edge complication |
| * @param x The x-coordinate of the point to test in pixels |
| * @param y The y-coordinate of the point to test in pixels |
| * @return Whether or not the point is within the arc |
| */ |
| fun hitTest(rect: Rect, @Px x: Float, @Px y: Float): Boolean { |
| val width = rect.width() |
| val height = rect.height() |
| val thicknessPx = min(width, height).toDouble() * thickness |
| |
| val halfWidth = width.toDouble() * 0.5 |
| val halfHeight = height.toDouble() * 0.5 |
| |
| // Rotate to a local coordinate space where the y axis is in the middle of the arc |
| var x0 = (x - rect.left).toDouble() - halfWidth |
| var y0 = (y - rect.top).toDouble() - halfHeight |
| val angle = startAngle + 0.5f * totalAngle |
| val rotAngle = -Math.toRadians(angle.toDouble()) |
| x0 = x0 * cos(rotAngle) - y0 * sin(rotAngle) + halfWidth |
| y0 = x0 * sin(rotAngle) + y0 * cos(rotAngle) + halfHeight |
| |
| // Copied from WearCurvedTextView... |
| val radius2 = min(width, height).toDouble() / 2.0 |
| val radius1 = radius2 - thicknessPx |
| val dx = x0 - (width.toDouble() / 2.0) |
| val dy = y0 - (height.toDouble() / 2.0) |
| val r2 = dx * dx + dy * dy |
| if (r2 < radius1 * radius1 || r2 > radius2 * radius2) { |
| return false |
| } |
| |
| // Since we are symmetrical on the Y-axis, we can constrain the angle to the x>=0 quadrants. |
| return Math.toDegrees(atan2(abs(dx), -dy)) < (totalAngle / 2.0) |
| } |
| |
| override fun toString(): String { |
| return "ArcParams(startAngle=$startAngle, totalArcAngle=$totalAngle, " + |
| "thickness=$thickness)" |
| } |
| |
| override fun equals(other: Any?): Boolean { |
| if (this === other) return true |
| if (javaClass != other?.javaClass) return false |
| |
| other as BoundingArc |
| |
| if (startAngle != other.startAngle) return false |
| if (totalAngle != other.totalAngle) return false |
| if (thickness != other.thickness) return false |
| |
| return true |
| } |
| |
| override fun hashCode(): Int { |
| var result = startAngle.hashCode() |
| result = 31 * result + totalAngle.hashCode() |
| result = 31 * result + thickness.hashCode() |
| return result |
| } |
| |
| @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
| fun toWireFormat() = BoundingArcWireFormat(startAngle, totalAngle, thickness) |
| } |
| |
| /** |
| * 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]. |
| * |
| * Taps on the watch are tested first against each ComplicationSlot's |
| * [ComplicationSlotBounds.perComplicationTypeBounds] for the relevant [ComplicationType]. Its |
| * assumed that [ComplicationSlotBounds.perComplicationTypeBounds] don't overlap. If no intersection |
| * was found then taps are checked against [ComplicationSlotBounds.perComplicationTypeBounds] |
| * expanded by [ComplicationSlotBounds.perComplicationTypeMargins]. Expanded bounds can overlap so |
| * the [ComplicationSlot] with the lowest id that intersects the coordinates, if any, is selected. |
| * |
| * @property id The Watch Face's ID for the complication slot. |
| * @property boundsType The [ComplicationSlotBoundsTypeIntDef] of the complication slot. |
| * @property canvasComplicationFactory The [CanvasComplicationFactory] used to generate a |
| * [CanvasComplication] for rendering the complication. The factory allows us to decouple |
| * ComplicationSlot from potentially expensive asset loading. |
| * @property 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. |
| * @property 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. |
| * @property tapFilter The [ComplicationTapFilter] used to determine whether or not a tap hit the |
| * complication slot. |
| */ |
| public class ComplicationSlot |
| /** |
| * Constructs a [ComplicationSlot]. |
| * |
| * @param accessibilityTraversalIndex Used to sort Complications when generating accessibility |
| * content description labels. |
| * @param bounds The complication slot's [ComplicationSlotBounds]. |
| * @param supportedTypes The list of [ComplicationType]s accepted by this complication slot, must be |
| * non-empty. During complication data source selection, each item in this list is compared in |
| * turn with entries from a data source's data source's supported types. The first matching entry |
| * from `supportedTypes` is chosen. If there are no matches then that data source is not eligible |
| * to be selected in this slot. |
| * @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 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. |
| */ |
| @ComplicationExperimental |
| internal constructor( |
| public val id: Int, |
| accessibilityTraversalIndex: Int, |
| @ComplicationSlotBoundsTypeIntDef public val boundsType: Int, |
| bounds: ComplicationSlotBounds, |
| public val canvasComplicationFactory: CanvasComplicationFactory, |
| public val 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, |
| nameResourceId: Int?, |
| screenReaderNameResourceId: Int?, |
| // TODO(b/230364881): This should really be public but some metalava bug is preventing |
| // @ComplicationExperimental from working on the getter so it's currently hidden. |
| @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public val boundingArc: BoundingArc? |
| ) { |
| /** |
| * 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() |
| } |
| } |
| } |
| ) |
| } |
| |
| private var lastComplicationUpdate = Instant.EPOCH |
| |
| @VisibleForTesting |
| internal class ComplicationDataHistoryEntry( |
| val complicationData: ComplicationData, |
| val time: Instant |
| ) |
| |
| /** |
| * There doesn't seem to be a convenient ring buffer in the standard library so implement our |
| * own one. |
| */ |
| @VisibleForTesting |
| internal class RingBuffer(val size: Int) : Iterable<ComplicationDataHistoryEntry> { |
| private val entries = arrayOfNulls<ComplicationDataHistoryEntry>(size) |
| private var readIndex = 0 |
| private var writeIndex = 0 |
| |
| fun push(entry: ComplicationDataHistoryEntry) { |
| writeIndex = (writeIndex + 1) % size |
| if (writeIndex == readIndex) { |
| readIndex = (readIndex + 1) % size |
| } |
| entries[writeIndex] = entry |
| } |
| |
| override fun iterator() = |
| object : Iterator<ComplicationDataHistoryEntry> { |
| var iteratorReadIndex = readIndex |
| |
| override fun hasNext() = iteratorReadIndex != writeIndex |
| |
| override fun next(): ComplicationDataHistoryEntry { |
| iteratorReadIndex = (iteratorReadIndex + 1) % size |
| return entries[iteratorReadIndex]!! |
| } |
| } |
| } |
| |
| /** |
| * In userdebug builds maintain a history of the last [MAX_COMPLICATION_HISTORY_ENTRIES]-1 |
| * complications sent by the system, which is logged in dumpsys to help debug complication |
| * issues. |
| */ |
| @VisibleForTesting |
| internal val complicationHistory = |
| if (Build.TYPE.equals("userdebug")) { |
| RingBuffer(MAX_COMPLICATION_HISTORY_ENTRIES) |
| } else { |
| null |
| } |
| |
| init { |
| require(id >= 0) { "id must be >= 0" } |
| require(accessibilityTraversalIndex >= 0) { "accessibilityTraversalIndex must be >= 0" } |
| } |
| |
| public companion object { |
| /** The maximum number of entries in [complicationHistory] plus one. */ |
| private const val MAX_COMPLICATION_HISTORY_ENTRIES = 50 |
| |
| internal val unitSquare = RectF(0f, 0f, 1f, 1f) |
| |
| internal val screenLockedFallback = NoDataComplicationData() |
| |
| /** |
| * 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 |
| @OptIn(ComplicationExperimental::class) |
| 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(), |
| null |
| ) |
| |
| /** |
| * 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 |
| @OptIn(ComplicationExperimental::class) |
| 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(), |
| null |
| ) |
| |
| /** |
| * 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 hit detection in an editor for [ComplicationSlot]s created with this method is not |
| * supported. |
| * |
| * @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. |
| */ |
| // TODO(b/230364881): Deprecate when BoundingArc is no longer experimental. |
| @JvmStatic |
| @OptIn(ComplicationExperimental::class) |
| 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, |
| null |
| ) |
| |
| /** |
| * Constructs a [Builder] for a complication with bounds type |
| * [ComplicationSlotBoundsType.EDGE], whose contents are contained within [boundingArc]. |
| * |
| * @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. |
| */ |
| @JvmStatic |
| @JvmOverloads |
| @ComplicationExperimental |
| @Suppress("UnavailableSymbol") |
| public fun createEdgeComplicationSlotBuilder( |
| id: Int, |
| canvasComplicationFactory: CanvasComplicationFactory, |
| supportedTypes: List<ComplicationType>, |
| defaultDataSourcePolicy: DefaultComplicationDataSourcePolicy, |
| bounds: ComplicationSlotBounds, |
| @Suppress("HiddenTypeParameter") boundingArc: BoundingArc, |
| complicationTapFilter: ComplicationTapFilter = |
| object : ComplicationTapFilter { |
| override fun hitTest( |
| complicationSlot: ComplicationSlot, |
| screenBounds: Rect, |
| x: Int, |
| y: Int, |
| @Suppress("UNUSED_PARAMETER") includeMargins: Boolean |
| ) = |
| boundingArc.hitTest( |
| complicationSlot.computeBounds(screenBounds), |
| x.toFloat(), |
| y.toFloat() |
| ) |
| } |
| ): Builder = |
| Builder( |
| id, |
| canvasComplicationFactory, |
| supportedTypes, |
| defaultDataSourcePolicy, |
| ComplicationSlotBoundsType.EDGE, |
| bounds, |
| complicationTapFilter, |
| boundingArc |
| ) |
| } |
| |
| /** Builder for constructing [ComplicationSlot]s. */ |
| @OptIn(ComplicationExperimental::class) |
| public class Builder |
| /** |
| * Constructs a [Builder]. |
| * |
| * @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 [ComplicationSlotBoundsTypeIntDef] of the complication. |
| * @param bounds The complication's [ComplicationSlotBounds]. |
| * @param complicationTapFilter The [ComplicationTapFilter] used to perform hit testing for this |
| * complication. |
| */ |
| internal constructor( |
| private val id: Int, |
| private val canvasComplicationFactory: CanvasComplicationFactory, |
| private val supportedTypes: List<ComplicationType>, |
| private var defaultDataSourcePolicy: DefaultComplicationDataSourcePolicy, |
| @ComplicationSlotBoundsTypeIntDef private val boundsType: Int, |
| private val bounds: ComplicationSlotBounds, |
| private val complicationTapFilter: ComplicationTapFilter, |
| private val boundingArc: BoundingArc? |
| ) { |
| 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 { |
| require(defaultDataSourceType in supportedTypes) { |
| "Can't set $defaultDataSourceType because it's not in the supportedTypes list:" + |
| " $supportedTypes" |
| } |
| 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 { |
| require( |
| defaultDataSourcePolicy.primaryDataSourceDefaultType == null || |
| defaultDataSourcePolicy.primaryDataSourceDefaultType in supportedTypes |
| ) { |
| "defaultDataSourcePolicy.primaryDataSourceDefaultType " + |
| "${defaultDataSourcePolicy.primaryDataSourceDefaultType} must be in the" + |
| " supportedTypes list: $supportedTypes" |
| } |
| |
| require( |
| defaultDataSourcePolicy.secondaryDataSourceDefaultType == null || |
| defaultDataSourcePolicy.secondaryDataSourceDefaultType in supportedTypes |
| ) { |
| "defaultDataSourcePolicy.secondaryDataSourceDefaultType " + |
| "${defaultDataSourcePolicy.secondaryDataSourceDefaultType} must be in the" + |
| " supportedTypes list: $supportedTypes" |
| } |
| |
| require( |
| defaultDataSourcePolicy.systemDataSourceFallbackDefaultType == |
| ComplicationType.NOT_CONFIGURED || |
| defaultDataSourcePolicy.systemDataSourceFallbackDefaultType in supportedTypes |
| ) { |
| "defaultDataSourcePolicy.systemDataSourceFallbackDefaultType " + |
| "${defaultDataSourcePolicy.systemDataSourceFallbackDefaultType} must be in " + |
| "the supportedTypes list: $supportedTypes" |
| } |
| |
| return ComplicationSlot( |
| id, |
| accessibilityTraversalIndex, |
| boundsType, |
| bounds, |
| canvasComplicationFactory, |
| supportedTypes, |
| defaultDataSourcePolicy, |
| defaultDataSourceType, |
| initiallyEnabled, |
| configExtras, |
| fixedComplicationDataSource, |
| complicationTapFilter, |
| nameResourceId, |
| screenReaderNameResourceId, |
| boundingArc |
| ) |
| } |
| } |
| |
| 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 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 nameResourceIdDirty = true |
| |
| /** |
| * The optional 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. |
| */ |
| public var nameResourceId: Int? = nameResourceId |
| @Suppress("AutoBoxing") @UiThread get |
| @UiThread |
| internal set(value) { |
| require(value != 0) |
| if (field == value) { |
| return |
| } |
| field = value |
| nameResourceIdDirty = true |
| } |
| |
| internal var screenReaderNameResourceIdDirty = true |
| |
| /** |
| * The optional 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 var screenReaderNameResourceId: Int? = screenReaderNameResourceId |
| @Suppress("AutoBoxing") @UiThread get |
| @UiThread |
| internal set(value) { |
| if (field == value) { |
| return |
| } |
| field = value |
| screenReaderNameResourceIdDirty = true |
| } |
| |
| internal var dataDirty = true |
| |
| /** |
| * The data set by [setComplicationData] (and then selected by |
| * [selectComplicationDataForInstant]). Exposed by [complicationData] unless |
| * [frozenDataSourceForEdit] is set. |
| */ |
| private var selectedData: ComplicationData = NoDataComplicationData() |
| |
| private data class FrozenDataSourceForEdit( |
| val from: ComplicationDataSourceInfo?, |
| val to: ComplicationDataSourceInfo?, |
| ) |
| |
| /** |
| * Marks the slot frozen, so [complicationData] only returns [EmptyComplicationData]. |
| * |
| * This reduces the chances of the slot showing the previous complication momentarily when the |
| * user finishes editing. |
| * |
| * Memorizing from/to edited data source because we need to avoid clearing the complication data |
| * when the data source is the same, because the platform doesn't re-fetch complications when |
| * only updating configuration. |
| */ |
| private var frozenDataSourceForEdit: FrozenDataSourceForEdit? = null |
| |
| /** |
| * The [androidx.wear.watchface.complications.data.ComplicationData] associated with the |
| * [ComplicationSlot]. This defaults to [NoDataComplicationData]. |
| * |
| * If the slot is frozen for edit, this is set to [EmptyComplicationData]. |
| */ |
| // Can be described as: |
| // selectedData.combine(frozenDataSourceForEdit) { data, frozenDataSource -> |
| // if (frozenDataSource == null) data else EmptyComplicationData() |
| // } |
| // but some flows depend on this StateFlow updating immediately after selectedData was changed, |
| // and Flow.combine() doesn't ensure that. |
| public val complicationData: StateFlow<ComplicationData> = MutableStateFlow(selectedData) |
| |
| /** |
| * 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<WireComplicationData>? = null |
| |
| /** |
| * Sets the current [ComplicationData] and if it's a timeline, the correct override for |
| * [instant] is chosen. Any images associated with the complication are loaded asynchronously |
| * and the complication history is updated. |
| */ |
| internal fun setComplicationData( |
| complicationData: ComplicationData, |
| instant: Instant, |
| forceLoad: Boolean = false, |
| ) { |
| complicationHistory?.push(ComplicationDataHistoryEntry(complicationData, instant)) |
| setTimelineData(complicationData, instant) |
| selectComplicationDataForInstant(instant, forceUpdate = true, forceLoad = forceLoad) |
| } |
| |
| /** |
| * Sets the current [ComplicationData] and if it's a timeline, the correct override for |
| * [instant] is chosen. Any images are loaded synchronously. The complication history is not |
| * updated. |
| * |
| * Returns a restoration function. |
| */ |
| internal fun setComplicationDataForScreenshot( |
| complicationData: ComplicationData, |
| instant: Instant |
| ): AutoCloseable { |
| val originalComplicationData = timelineComplicationData |
| val originalInstant = lastComplicationUpdate |
| val restore = AutoCloseable { |
| // Avoid overwriting a change made by someone else, can still race. |
| if (timelineComplicationData !== complicationData) return@AutoCloseable |
| setTimelineData(originalComplicationData, originalInstant) |
| selectComplicationDataForInstant(originalInstant, forceUpdate = true) |
| } |
| |
| try { |
| setTimelineData(complicationData, instant) |
| selectComplicationDataForInstant(instant, forceUpdate = true, forceLoad = true) |
| } catch (e: Throwable) { |
| // Cleanup on failure. |
| restore.close() |
| throw e |
| } |
| return restore |
| } |
| |
| private fun setTimelineData(data: ComplicationData, instant: Instant) { |
| lastComplicationUpdate = instant |
| timelineComplicationData = data |
| timelineEntries = data.asWireComplicationData().timelineEntries?.toList() |
| } |
| |
| private fun loadData(data: ComplicationData, loadDrawablesAsynchronous: Boolean = false) { |
| renderer.loadData(data, loadDrawablesAsynchronous = loadDrawablesAsynchronous) |
| (complicationData as MutableStateFlow<ComplicationData>).value = data |
| } |
| |
| /** |
| * If the current [ComplicationData] is a timeline, the correct override for [instant] is |
| * chosen. |
| */ |
| internal fun selectComplicationDataForInstant( |
| instant: Instant, |
| forceUpdate: Boolean, |
| forceLoad: Boolean = false, |
| ) { |
| var previousShortest = Long.MAX_VALUE |
| val time = instant.epochSecond |
| var best = timelineComplicationData |
| |
| // Select the shortest valid timeline entry. |
| timelineEntries?.let { |
| for (wireEntry in it) { |
| 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 = wireEntry.toApiComplicationData() |
| } |
| } |
| } |
| } |
| |
| // If the screen is locked and our policy is to not display it when locked then select |
| // screenLockedFallback instead. |
| if ( |
| (best.displayPolicy and ComplicationDisplayPolicies.DO_NOT_SHOW_WHEN_DEVICE_LOCKED) != |
| 0 && complicationSlotsManager.watchState.isLocked.value |
| ) { |
| best = screenLockedFallback // This is NoDataComplicationData. |
| } |
| |
| // When b/323483515 is fixed, go back to using regular equality rather than reference |
| // equality. |
| if (!forceUpdate && selectedData === best) return |
| |
| val frozen = frozenDataSourceForEdit != null |
| if (!frozen || forceLoad) { |
| loadData(best, loadDrawablesAsynchronous = !forceLoad) |
| } else { |
| // Restoring frozen slot to empty in case it was changed for screenshot. |
| loadData(EmptyComplicationData()) |
| } |
| selectedData = best |
| |
| // forceUpdate is used for screenshots, don't set the dirty flag for those. |
| if (!forceUpdate) dataDirty = true |
| } |
| |
| /** Sets [frozenDataSourceForEdit]. */ |
| internal fun freezeForEdit( |
| from: ComplicationDataSourceInfo?, |
| to: ComplicationDataSourceInfo?, |
| ) { |
| val previous = frozenDataSourceForEdit |
| // Keeping the original "from" of the first edit. |
| frozenDataSourceForEdit = FrozenDataSourceForEdit(from = previous?.from ?: from, to = to) |
| // If this is the first freeze, render EmptyComplicationData. |
| if (previous == null) loadData(EmptyComplicationData()) |
| } |
| |
| /** Unsets [frozenDataSourceForEdit]. */ |
| internal fun unfreezeForEdit(clearData: Boolean) { |
| val frozenDataSourceForEdit = frozenDataSourceForEdit ?: return |
| // Clearing the previously selected data if needed. |
| if ( |
| clearData && |
| frozenDataSourceForEdit.from?.componentName != |
| frozenDataSourceForEdit.to?.componentName |
| ) { |
| setComplicationData(EmptyComplicationData(), Instant.now()) |
| } |
| this.frozenDataSourceForEdit = null |
| // Re-load current/new data immediately |
| // (especially in case of new data skipped loading in selectComplicationDataForInstant). |
| loadData(selectedData) |
| } |
| |
| /** |
| * 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 |
| @OptIn(ComplicationExperimental::class) |
| 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, |
| boundingArc |
| ) |
| } |
| is HighlightedElement.ComplicationSlot -> { |
| if (highlightedElement.id == id) { |
| renderer.drawHighlight( |
| canvas, |
| bounds, |
| boundsType, |
| zonedDateTime, |
| renderParameters.highlightLayer.highlightTint, |
| boundingArc |
| ) |
| } |
| } |
| is HighlightedElement.UserStyle -> { |
| // Nothing |
| } |
| null -> { |
| // Nothing |
| } |
| } |
| } |
| |
| internal fun init(invalidateListener: InvalidateListener, isHeadless: Boolean) { |
| this.invalidateListener = invalidateListener |
| |
| if (isHeadless) { |
| timelineComplicationData = EmptyComplicationData() |
| selectedData = EmptyComplicationData() |
| (complicationData as MutableStateFlow<ComplicationData>).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]. |
| * @param applyMargins Whether or not the margins should be applied to the computed [Rect]. |
| */ |
| @JvmOverloads |
| @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
| public fun computeBounds( |
| screen: Rect, |
| complicationType: ComplicationType, |
| applyMargins: Boolean = false |
| ): Rect { |
| val unitSquareBounds = |
| RectF(complicationSlotBounds.perComplicationTypeBounds[complicationType]!!) |
| if (applyMargins) { |
| val unitSquareMargins = |
| complicationSlotBounds.perComplicationTypeMargins[complicationType]!! |
| // Apply the margins |
| unitSquareBounds.set( |
| unitSquareBounds.left - unitSquareMargins.left, |
| unitSquareBounds.top - unitSquareMargins.top, |
| unitSquareBounds.right + unitSquareMargins.right, |
| unitSquareBounds.bottom + unitSquareMargins.bottom |
| ) |
| } |
| 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. |
| * @param applyMargins Whether or not the margins should be applied to the computed [Rect]. |
| */ |
| @JvmOverloads |
| public fun computeBounds(screen: Rect, applyMargins: Boolean = false): Rect = |
| computeBounds(screen, complicationData.value.type, applyMargins) |
| |
| @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("timelineComplicationData=$timelineComplicationData") |
| writer.println("timelineEntries=" + timelineEntries?.joinToString()) |
| writer.println("data=${renderer.getData()}") |
| @OptIn(ComplicationExperimental::class) writer.println("boundingArc=$boundingArc") |
| writer.println("complicationSlotBounds=$complicationSlotBounds") |
| writer.println("lastComplicationUpdate=$lastComplicationUpdate") |
| writer.println("data history") |
| complicationHistory?.let { |
| writer.increaseIndent() |
| for (entry in it) { |
| val localDateTime = LocalDateTime.ofInstant(entry.time, ZoneId.systemDefault()) |
| writer.println("${entry.complicationData} @ $localDateTime") |
| } |
| writer.decreaseIndent() |
| } |
| writer.decreaseIndent() |
| } |
| |
| override fun equals(other: Any?): Boolean { |
| if (this === other) return true |
| if (javaClass != other?.javaClass) return false |
| |
| other as ComplicationSlot |
| |
| if (id != other.id) return false |
| if (accessibilityTraversalIndex != other.accessibilityTraversalIndex) return false |
| if (boundsType != other.boundsType) return false |
| if (complicationSlotBounds != other.complicationSlotBounds) return false |
| if ( |
| supportedTypes.size != other.supportedTypes.size || |
| !supportedTypes.containsAll(other.supportedTypes) |
| ) |
| return false |
| if (defaultDataSourcePolicy != other.defaultDataSourcePolicy) return false |
| if (initiallyEnabled != other.initiallyEnabled) return false |
| if (fixedComplicationDataSource != other.fixedComplicationDataSource) return false |
| if (nameResourceId != other.nameResourceId) return false |
| if (screenReaderNameResourceId != other.screenReaderNameResourceId) return false |
| @OptIn(ComplicationExperimental::class) if (boundingArc != other.boundingArc) return false |
| |
| return true |
| } |
| |
| override fun hashCode(): Int { |
| @OptIn(ComplicationExperimental::class) |
| return Objects.hash( |
| id, |
| accessibilityTraversalIndex, |
| boundsType, |
| complicationSlotBounds, |
| supportedTypes.sorted(), |
| defaultDataSourcePolicy, |
| initiallyEnabled, |
| fixedComplicationDataSource, |
| nameResourceId, |
| screenReaderNameResourceId, |
| boundingArc |
| ) |
| } |
| } |