ComplicationSlot.kt

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

package androidx.wear.watchface

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 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.RenderParameters.HighlightedElement
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.ComplicationDataExpressionEvaluator
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.Duration
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit.SECONDS
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
import kotlinx.coroutines.launch

/**
 * 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 [ComplicationSlotBoundsType] 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,
        @ComplicationSlotBoundsType 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 [ComplicationSlotBoundsType] 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,
        @ComplicationSlotBoundsType 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
}

/** @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
    }
}

/**
 * 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
    }

    /** @hide */
    @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.
 * @param accessibilityTraversalIndex Used to sort Complications when generating accessibility
 * content description labels.
 * @property boundsType The [ComplicationSlotBoundsType] of the complication slot.
 * @param bounds The complication slot's [ComplicationSlotBounds].
 * @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.
 * @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.
 * @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.
 * @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.
 * @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
@ComplicationExperimental internal constructor(
    public val id: Int,
    accessibilityTraversalIndex: Int,
    @ComplicationSlotBoundsType 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.
    /** @hide */
    @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 val wearSdkVersion by lazy { complicationSlotsManager.watchFaceHostApi.wearSdkVersion }

    private var lastComplicationUpdate = Instant.EPOCH

    private 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.
     */
    private 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, which is logged in dumpsys to help debug complication issues.
     */
    private 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
        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
        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
        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
        public fun createEdgeComplicationSlotBuilder(
            id: Int,
            canvasComplicationFactory: CanvasComplicationFactory,
            supportedTypes: List<ComplicationType>,
            defaultDataSourcePolicy: DefaultComplicationDataSourcePolicy,
            bounds: ComplicationSlotBounds,
            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.
     *
     * @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.
     */
    @OptIn(ComplicationExperimental::class)
    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 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 {
            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,
            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

    private var lastExpressionEvaluator: ComplicationDataExpressionEvaluator? = null

    private var unevaluatedComplicationData: ComplicationData = NoDataComplicationData()

    /**
     * The [androidx.wear.watchface.complications.data.ComplicationData] associated with the
     * [ComplicationSlot]. This defaults to [NoDataComplicationData].
     */
    public val complicationData: StateFlow<ComplicationData> =
        MutableStateFlow(unevaluatedComplicationData)

    /**
     * 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.
     */
    internal fun setComplicationData(
        complicationData: ComplicationData,
        loadDrawablesAsynchronous: Boolean,
        instant: Instant
    ) {
        lastComplicationUpdate = instant
        complicationHistory?.push(ComplicationDataHistoryEntry(complicationData, instant))
        timelineComplicationData = complicationData
        timelineEntries = complicationData.asWireComplicationData().timelineEntries?.toList()
        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 (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.
        }

        if (wearSdkVersion >= Build.VERSION_CODES.TIRAMISU) {
            best.evaluateAndLoadUpdates(loadDrawablesAsynchronous)
            // Loading synchronously if forced.
            if (forceUpdate) best.load(loadDrawablesAsynchronous)
        } else {
            // Avoid expression evaluation pre-T as it may be redacted by the old platform.
            if (forceUpdate || complicationData.value != best) best.load(loadDrawablesAsynchronous)
        }
    }

    private fun ComplicationData.load(loadDrawablesAsynchronous: Boolean) {
        renderer.loadData(this, loadDrawablesAsynchronous)
        (complicationData as MutableStateFlow).value = this
    }

    /**
     * Creates a [ComplicationDataExpressionEvaluator] and monitors for updates, sending them to the
     * [renderer].
     *
     * Ignores new data that has equivalent expression (see [ComplicationData.equalsUnevaluated]).
     * While the data is first being evaluated, sends [NoDataComplicationData] to the renderer.
     */
    private fun ComplicationData.evaluateAndLoadUpdates(
        loadDrawablesAsynchronous: Boolean,
    ) {
        if (unevaluatedComplicationData equalsUnevaluated this) return
        unevaluatedComplicationData = this
        // Reverting to NoData while evaluating.
        NoDataComplicationData().load(loadDrawablesAsynchronous)
        lastExpressionEvaluator?.close()
        // TODO(b/260065006): Do we need to close the evaluator on destroy?
        lastExpressionEvaluator =
            ComplicationDataExpressionEvaluator(asWireComplicationData())
                .apply {
                    init()
                    loadUpdates(loadDrawablesAsynchronous)
                }
    }

    /**
     * Monitors evaluated expression updates and sends them to the [renderer].
     *
     * If this is the first evaluation, loads the data immediately. Otherwise, triggers watchface
     * invalidation on the next top of the second.
     */
    private fun ComplicationDataExpressionEvaluator.loadUpdates(
        loadDrawablesAsynchronous: Boolean
    ) {
        complicationSlotsManager.watchFaceHostApi.getUiThreadCoroutineScope().launch {
            data.collect { evaluatedWireData ->
                if (evaluatedWireData == null) return@collect // Not yet evaluated.
                val evaluatedData = evaluatedWireData.toApiComplicationData()
                if (complicationData.value is NoDataComplicationData) {
                    // Loading now if it's the first update.
                    evaluatedData.load(loadDrawablesAsynchronous)
                } else {
                    // Loading in the next frame on further updates.
                    (complicationData as MutableStateFlow).value = evaluatedData
                    complicationSlotsManager.watchFaceHostApi.postInvalidate(
                        durationUntilNextForcedFrame()
                    )
                }
            }
        }
    }

    /** Returns the duration until the next top of the second. */
    private fun durationUntilNextForcedFrame(): Duration {
        val now = Instant.ofEpochMilli(
            complicationSlotsManager.watchFaceHostApi.systemTimeProvider.getSystemTimeMillis()
        )
        return Duration.between(now, (now + Duration.ofSeconds(1)).truncatedTo(SECONDS))
    }

    /**
     * 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()
            unevaluatedComplicationData = EmptyComplicationData()
            (complicationData as MutableStateFlow).value = unevaluatedComplicationData
        }
    }

    /**
     * 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].
     * @hide
     */
    @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
        )
    }
}