UserStyleSetting.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.style

import android.annotation.SuppressLint
import android.content.ContentResolver
import android.content.Context
import android.content.res.Resources
import android.graphics.BitmapFactory
import android.graphics.drawable.Icon
import android.os.Build
import android.util.TypedValue
import androidx.annotation.Px
import androidx.annotation.RestrictTo
import androidx.annotation.StringRes
import androidx.wear.watchface.complications.ComplicationSlotBounds
import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay
import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption
import androidx.wear.watchface.style.data.BooleanOptionWireFormat
import androidx.wear.watchface.style.data.BooleanUserStyleSettingWireFormat
import androidx.wear.watchface.style.data.ComplicationOverlayWireFormat
import androidx.wear.watchface.style.data.ComplicationsOptionWireFormat
import androidx.wear.watchface.style.data.ComplicationsUserStyleSettingWireFormat
import androidx.wear.watchface.style.data.CustomValueOptionWireFormat
import androidx.wear.watchface.style.data.CustomValueUserStyleSettingWireFormat
import androidx.wear.watchface.style.data.DoubleRangeOptionWireFormat
import androidx.wear.watchface.style.data.DoubleRangeUserStyleSettingWireFormat
import androidx.wear.watchface.style.data.ListOptionWireFormat
import androidx.wear.watchface.style.data.ListUserStyleSettingWireFormat
import androidx.wear.watchface.style.data.LongRangeOptionWireFormat
import androidx.wear.watchface.style.data.LongRangeUserStyleSettingWireFormat
import androidx.wear.watchface.style.data.OptionWireFormat
import androidx.wear.watchface.style.data.UserStyleSettingWireFormat
import java.io.InputStream
import java.nio.ByteBuffer
import java.security.InvalidParameterException

/** Wrapper around either a [CharSequence] or a string resource. */
internal sealed class DisplayText {
    abstract fun toCharSequence(): CharSequence

    override fun toString(): String = toCharSequence().toString()

    class CharSequenceDisplayText(private val charSequence: CharSequence) : DisplayText() {
        override fun toCharSequence() = charSequence
    }

    class ResourceDisplayText(
        private val resources: Resources,
        @StringRes private val id: Int
    ) : DisplayText() {
        override fun toCharSequence() = resources.getString(id)
    }
}

/**
 * Watch faces often have user configurable styles, the definition of what is a style is left up to
 * the watch face but it typically incorporates a variety of settings such as: color, visual theme
 * for watch hands, font, tick shape, complication slots, audio elements, etc...
 *
 * A UserStyleSetting represents one of these dimensions. See also [UserStyleSchema] which defines
 * the list of UserStyleSettings provided by the watch face.
 *
 * Styling data gets shared with the companion phone to support editors (typically over bluetooth),
 * as a result the size of serialized UserStyleSettings could become an issue if large.
 *
 * @param id Identifier for the element, must be unique. Styling data gets shared with the companion
 * (typically via bluetooth) so size is a consideration and short ids are encouraged. There is a
 * maximum length see [UserStyleSetting.Id.MAX_LENGTH].
 * @param icon Icon for use in the style selection UI.
 * @param options List of options for this UserStyleSetting. Depending on the type of
 * UserStyleSetting this may be an exhaustive list, or just examples to populate a ListView
 * in case the UserStyleSetting isn't supported by the UI (e.g. a new WatchFace with an old
 * Companion).
 * @param defaultOptionIndex The default option index, used if nothing has been selected
 * within the [options] list.
 * @param affectedWatchFaceLayers Used by the style configuration UI. Describes which rendering
 * layers this style affects.
 */
public sealed class UserStyleSetting private constructor(
    public val id: Id,
    private val displayNameInternal: DisplayText,
    private val descriptionInternal: DisplayText,
    public val icon: Icon?,
    public val options: List<Option>,
    public val defaultOptionIndex: Int,
    public val affectedWatchFaceLayers: Collection<WatchFaceLayer>
) {
    /** Localized human readable name for the element, used in the userStyle selection UI. */
    public val displayName: CharSequence
        get() = displayNameInternal.toCharSequence()

    /** Localized description string displayed under the displayName. */
    public val description: CharSequence
        get() = descriptionInternal.toCharSequence()

    /**
     * Estimates the wire size of the UserStyleSetting in bytes. This does not account for the
     * overhead of the serialization method. Where possible the exact wire size for any referenced
     * [Icon]s is used but this isn't possible in all cases and as a fallback width x height x 4
     * is used.
     *
     * Note this method can be slow.
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public fun estimateWireSizeInBytesAndValidateIconDimensions(
        context: Context,
        @Px maxWidth: Int,
        @Px maxHeight: Int
    ): Int {
        var sizeEstimate = id.value.length + displayName.length + description.length +
            4 /** [defaultOptionIndex] */ + affectedWatchFaceLayers.size * 4
        icon?.getWireSizeAndDimensions(context)?.let { wireSizeAndDimensions ->
            wireSizeAndDimensions.wireSizeBytes?.let {
                sizeEstimate += it
            }
            require(
                wireSizeAndDimensions.width <= maxWidth && wireSizeAndDimensions.height <= maxHeight
            ) {
                "UserStyleSetting id $id has a ${wireSizeAndDimensions.width} x " +
                    "${wireSizeAndDimensions.height} icon. This is too big, the maximum size is " +
                    "$maxWidth x $maxHeight."
            }
        }
        for (option in options) {
            sizeEstimate += option.estimateWireSizeInBytesAndValidateIconDimensions(
                context,
                maxWidth,
                maxHeight
            )
        }
        return sizeEstimate
    }

    /**
     * Machine readable identifier for [UserStyleSetting]s. The length of this identifier may not
     * exceed [MAX_LENGTH].
     */
    public class Id(public val value: String) {
        public companion object {
            /** Maximum length of the [value] field. */
            public const val MAX_LENGTH: Int = 40
        }

        init {
            require(value.length <= MAX_LENGTH) {
                "UserStyleSetting.value.length (${value.length}) must be less than MAX_LENGTH " +
                    "($MAX_LENGTH)"
            }
        }

        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false

            other as Id

            return value == other.value
        }

        override fun hashCode(): Int {
            return value.hashCode()
        }

        override fun toString(): String = value
    }

    public companion object {
        internal fun createFromWireFormat(
            wireFormat: UserStyleSettingWireFormat
        ): UserStyleSetting = when (wireFormat) {
            is BooleanUserStyleSettingWireFormat -> BooleanUserStyleSetting(wireFormat)

            is ComplicationsUserStyleSettingWireFormat ->
                ComplicationSlotsUserStyleSetting(wireFormat)

            is CustomValueUserStyleSettingWireFormat -> CustomValueUserStyleSetting(wireFormat)

            is DoubleRangeUserStyleSettingWireFormat -> DoubleRangeUserStyleSetting(wireFormat)

            is ListUserStyleSettingWireFormat -> ListUserStyleSetting(wireFormat)

            is LongRangeUserStyleSettingWireFormat -> LongRangeUserStyleSetting(wireFormat)

            else -> throw IllegalArgumentException(
                "Unknown StyleCategoryWireFormat " + wireFormat::javaClass.name
            )
        }
    }

    init {
        require(defaultOptionIndex >= 0 && defaultOptionIndex < options.size) {
            "defaultOptionIndex must be in the range [0 .. options.size)"
        }
    }

    internal fun getSettingOptionForId(id: ByteArray?) =
        if (id == null) {
            options[defaultOptionIndex]
        } else {
            getOptionForId(Option.Id(id))
        }

    private constructor(wireFormat: UserStyleSettingWireFormat) : this(
        Id(wireFormat.mId),
        DisplayText.CharSequenceDisplayText(wireFormat.mDisplayName),
        DisplayText.CharSequenceDisplayText(wireFormat.mDescription),
        wireFormat.mIcon,
        wireFormat.mOptions.map { Option.createFromWireFormat(it) },
        wireFormat.mDefaultOptionIndex,
        wireFormat.mAffectsLayers.map { WatchFaceLayer.values()[it] }
    )

    /** @hide */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
    public abstract fun toWireFormat(): UserStyleSettingWireFormat

    /** @hide */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
    public fun getWireFormatOptionsList(): List<OptionWireFormat> =
        options.map { it.toWireFormat() }

    /** Returns the default for when the user hasn't selected an option. */
    public val defaultOption: Option
        get() = options[defaultOptionIndex]

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as UserStyleSetting

        return id == other.id
    }

    override fun hashCode(): Int {
        return id.hashCode()
    }

    override fun toString(): String = "{${id.value} : " +
        options.joinToString(transform = { it.toString() }) + "}"

    /**
     * Represents a choice within a style setting which can either be an option from the list or a
     * an arbitrary value depending on the nature of the style setting.
     *
     * @property id Machine readable [Id] for the style setting. Identifier for the option (or the
     * option itself for [CustomValueUserStyleSetting.CustomValueOption]), must be unique within
     * the UserStyleSetting. Short ids are encouraged.
     */
    public abstract class Option(public val id: Id) {
        /**
         * Computes a lower bound estimate of the wire size of the Option in bytes. This does not
         * account for the overhead of the serialization method.
         */
        internal open fun estimateWireSizeInBytesAndValidateIconDimensions(
            context: Context,
            @Px maxWidth: Int,
            @Px maxHeight: Int
        ): Int = id.value.size

        // We don't want Option to be subclassed by users.
        @SuppressWarnings("HiddenAbstractMethod")
        internal abstract fun getUserStyleSettingClass(): Class<out UserStyleSetting>

        /**
         * Machine readable identifier for [Option]s. The length of this identifier may not exceed
         * [MAX_LENGTH].
         *
         * @param value The [ByteArray] value of this Id.
         */
        public class Id(public val value: ByteArray) {
            /**
             * Constructs an [Id] with a [String] encoded to a [ByteArray] by
             * [String.encodeToByteArray].
             */
            public constructor(value: String) : this(value.encodeToByteArray())

            public companion object {
                /** Maximum length of the [value] field. */
                public const val MAX_LENGTH: Int = 1024
            }

            init {
                require(value.size <= MAX_LENGTH) {
                    "Option.Id.value.size (${value.size}) must be less than MAX_LENGTH " +
                        "($MAX_LENGTH)"
                }
            }

            override fun equals(other: Any?): Boolean {
                if (this === other) return true
                if (javaClass != other?.javaClass) return false
                other as Id
                return value.contentEquals(other.value)
            }

            override fun hashCode(): Int {
                return value.contentHashCode()
            }

            override fun toString(): String =
                try {
                    value.decodeToString()
                } catch (e: Exception) {
                    value.toString()
                }
        }

        public companion object {
            /** @hide */
            @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
            public fun createFromWireFormat(
                wireFormat: OptionWireFormat
            ): Option =
                when (wireFormat) {
                    is BooleanOptionWireFormat ->
                        BooleanUserStyleSetting.BooleanOption.fromWireFormat(wireFormat)

                    is ComplicationsOptionWireFormat ->
                        ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(wireFormat)

                    is CustomValueOptionWireFormat ->
                        CustomValueUserStyleSetting.CustomValueOption(wireFormat)

                    is DoubleRangeOptionWireFormat ->
                        DoubleRangeUserStyleSetting.DoubleRangeOption(wireFormat)

                    is ListOptionWireFormat ->
                        ListUserStyleSetting.ListOption(wireFormat)

                    is LongRangeOptionWireFormat ->
                        LongRangeUserStyleSetting.LongRangeOption(wireFormat)

                    else -> throw IllegalArgumentException(
                        "Unknown StyleCategoryWireFormat.OptionWireFormat " +
                            wireFormat::javaClass.name
                    )
                }
        }

        /** @hide */
        @Suppress("HiddenAbstractMethod")
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
        public abstract fun toWireFormat(): OptionWireFormat

        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false

            other as Option

            return id == other.id
        }

        override fun hashCode(): Int {
            return id.hashCode()
        }

        override fun toString(): String =
            try {
                id.value.decodeToString()
            } catch (e: Exception) {
                id.value.toString()
            }
    }

    /**
     * Translates an option name into an option. This will need to be overridden for userStyle
     * categories that can't sensibly be fully enumerated (e.g. a full 24-bit color picker).
     *
     * @param optionId The [Option.Id] of the option
     * @return An [Option] corresponding to the name. This could either be one of the options from
     * [UserStyleSetting]s or a newly constructed Option depending on the nature of the
     * UserStyleSetting. If optionName is unrecognized then the default value for the setting should
     * be returned.
     */
    public open fun getOptionForId(optionId: Option.Id): Option =
        options.find { it.id.value.contentEquals(optionId.value) } ?: options[defaultOptionIndex]

    /** A BooleanUserStyleSetting represents a setting with a true and a false setting. */
    public class BooleanUserStyleSetting : UserStyleSetting {

        /**
         * Constructs a BooleanUserStyleSetting.
         *
         * @param id [Id] for the element, must be unique.
         * @param displayName Localized human readable name for the element, used in the userStyle
         * selection UI.
         * @param description Localized description string displayed under the displayName.
         * @param icon [Icon] for use in the userStyle selection UI. This gets sent to the
         * companion over bluetooth and should be small (ideally a few kb in size).
         * @param affectsWatchFaceLayers Used by the style configuration UI. Describes which watch
         * face rendering layers this style affects.
         * @param defaultValue The default value for this BooleanUserStyleSetting.
         * @hide
         */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        public constructor (
            id: Id,
            displayName: CharSequence,
            description: CharSequence,
            icon: Icon?,
            affectsWatchFaceLayers: Collection<WatchFaceLayer>,
            defaultValue: Boolean
        ) : super(
            id,
            DisplayText.CharSequenceDisplayText(displayName),
            DisplayText.CharSequenceDisplayText(description),
            icon,
            listOf(BooleanOption.TRUE, BooleanOption.FALSE),
            when (defaultValue) {
                true -> 0
                false -> 1
            },
            affectsWatchFaceLayers
        )

        /**
         * Constructs a BooleanUserStyleSetting where [BooleanUserStyleSetting.displayName] and
         * [BooleanUserStyleSetting.description] are specified as resources.
         *
         * @param id [Id] for the element, must be unique.
         * @param resources The [Resources] from which [displayNameResourceId] and
         * [descriptionResourceId] are loaded.
         * @param displayNameResourceId String resource id for a human readable name for the element,
         * used in the userStyle selection UI.
         * @param descriptionResourceId String resource id for a human readable description string
         * displayed under the displayName.
         * @param icon [Icon] for use in the userStyle selection UI. This gets sent to the
         * companion over bluetooth and should be small (ideally a few kb in size).
         * @param affectsWatchFaceLayers Used by the style configuration UI. Describes which watch
         * face rendering layers this style affects.
         * @param defaultValue The default value for this BooleanUserStyleSetting.
         */
        public constructor (
            id: Id,
            resources: Resources,
            @StringRes displayNameResourceId: Int,
            @StringRes descriptionResourceId: Int,
            icon: Icon?,
            affectsWatchFaceLayers: Collection<WatchFaceLayer>,
            defaultValue: Boolean
        ) : super(
            id,
            DisplayText.ResourceDisplayText(resources, displayNameResourceId),
            DisplayText.ResourceDisplayText(resources, descriptionResourceId),
            icon,
            listOf(BooleanOption.TRUE, BooleanOption.FALSE),
            when (defaultValue) {
                true -> 0
                false -> 1
            },
            affectsWatchFaceLayers
        )

        internal constructor(wireFormat: BooleanUserStyleSettingWireFormat) : super(wireFormat)

        /** @hide */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
        override fun toWireFormat(): BooleanUserStyleSettingWireFormat =
            BooleanUserStyleSettingWireFormat(
                id.value,
                displayName,
                description,
                icon,
                getWireFormatOptionsList(),
                defaultOptionIndex,
                affectedWatchFaceLayers.map { it.ordinal }
            )

        /** Returns the default value. */
        public fun getDefaultValue(): Boolean = (options[defaultOptionIndex] as BooleanOption).value

        /**
         * Represents a true or false option in the [BooleanUserStyleSetting].
         * @param value The boolean value this instance represents.
         */
        public class BooleanOption private constructor(
            public val value: Boolean
        ) : Option(
            Id(ByteArray(1).apply { this[0] = if (value) 1 else 0 })
        ) {
            /** @hide */
            @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
            override fun toWireFormat(): BooleanOptionWireFormat =
                BooleanOptionWireFormat(id.value)

            internal override fun getUserStyleSettingClass(): Class<out UserStyleSetting> =
                BooleanUserStyleSetting::class.java

            override fun toString(): String = if (id.value[0] == 1.toByte()) "true" else "false"

            public companion object {
                @JvmField
                public val TRUE = BooleanOption(true)

                @JvmField
                public val FALSE = BooleanOption(false)

                @JvmStatic
                public fun from(value: Boolean): BooleanOption {
                    return if (value) TRUE else FALSE
                }

                @JvmStatic
                internal fun fromWireFormat(
                    wireFormat: BooleanOptionWireFormat
                ): BooleanOption {
                    return from(wireFormat.mId[0] == 1.toByte())
                }
            }
        }
    }

    /**
     * ComplicationSlotsUserStyleSetting is the recommended [UserStyleSetting] for representing
     * complication slot configuration, options such as the number of active complication slots,
     * their location, etc... The [ComplicationSlotsOption] class allows you to apply a list of
     * [ComplicationSlotOverlay]s on top of the base config as specified by the
     * [androidx.wear.watchface.ComplicationSlot] constructor.
     *
     * The ComplicationsManager listens for style changes with this setting and when a
     * [ComplicationSlotsOption] is selected the overrides are automatically applied. Note its
     * suggested that the default [ComplicationSlotOverlay] (the first entry in the list) does
     * not apply any overrides. Only a single [ComplicationSlotsUserStyleSetting] is permitted in
     * the [UserStyleSchema].
     *
     * Not to be confused with complication data source selection.
     */
    public class ComplicationSlotsUserStyleSetting : UserStyleSetting {

        /**
         * Overrides to be applied to the corresponding androidx.wear.watchface.ComplicationSlot]'s
         * initial config (as specified in it's constructor) when the setting is selected.
         *
         * @param complicationSlotId The id of the [androidx.wear.watchface.ComplicationSlot] to
         * configure.
         * @param enabled If non null, whether the complication should be enabled for this
         * configuration. If null then no changes are made.
         * @param complicationSlotBounds If non null, the [ComplicationSlotBounds] for this
         * configuration. If null then no changes are made.
         * @param accessibilityTraversalIndex If non null the accessibility traversal index
         * for this configuration. This is used to determine the order in which accessibility labels
         * for the watch face are read to the user.
         */
        public class ComplicationSlotOverlay constructor(
            public val complicationSlotId: Int,
            @Suppress("AutoBoxing")
            @get:Suppress("AutoBoxing")
            @get:JvmName("isEnabled")
            public val enabled: Boolean? = null,
            public val complicationSlotBounds: ComplicationSlotBounds? = null,
            @SuppressWarnings("AutoBoxing")
            @get:SuppressWarnings("AutoBoxing")
            public val accessibilityTraversalIndex: Int? = null
        ) {
            /**
             * Constructs a [ComplicationSlotOverlay].Builder.
             *
             * @param complicationSlotId The id of the [androidx.wear.watchface.ComplicationSlot]
             * to configure.
             */
            public class Builder(
                private val complicationSlotId: Int
            ) {
                private var enabled: Boolean? = null
                private var complicationSlotBounds: ComplicationSlotBounds? = null
                private var accessibilityTraversalIndex: Int? = null

                /** Overrides the complication's enabled flag. */
                @Suppress("MissingGetterMatchingBuilder")
                public fun setEnabled(enabled: Boolean): Builder = apply {
                    this.enabled = enabled
                }

                /** Overrides the complication's per [ComplicationSlotBounds]. */
                public fun setComplicationSlotBounds(
                    complicationSlotBounds: ComplicationSlotBounds
                ): Builder = apply {
                    this.complicationSlotBounds = complicationSlotBounds
                }

                /**
                 * Overrides the [androidx.wear.watchface.ComplicationSlot]'s accessibility
                 * traversal index. This is used to sort
                 * [androidx.wear.watchface.ContentDescriptionLabel]s. If unset we will order the
                 * complications by their initial accessibilityTraversalIndex (usually the same
                 * as their id).
                 */
                public fun setAccessibilityTraversalIndex(accessibilityTraversalIndex: Int):
                    Builder = apply {
                        this.accessibilityTraversalIndex = accessibilityTraversalIndex
                    }

                public fun build(): ComplicationSlotOverlay =
                    ComplicationSlotOverlay(
                        complicationSlotId,
                        enabled,
                        complicationSlotBounds,
                        accessibilityTraversalIndex
                    )
            }

            internal constructor(
                wireFormat: ComplicationOverlayWireFormat
            ) : this(
                wireFormat.mComplicationSlotId,
                when (wireFormat.mEnabled) {
                    ComplicationOverlayWireFormat.ENABLED_UNKNOWN -> null
                    ComplicationOverlayWireFormat.ENABLED_YES -> true
                    ComplicationOverlayWireFormat.ENABLED_NO -> false
                    else -> throw InvalidParameterException(
                        "Unrecognised wireFormat.mEnabled " + wireFormat.mEnabled
                    )
                },
                wireFormat.mPerComplicationTypeBounds?.let {
                    ComplicationSlotBounds(
                        it.mapKeys { ComplicationType.fromWireType(it.key) }
                    )
                },
                wireFormat.accessibilityTraversalIndex
            )

            /**
             * Computes a lower bound estimate of the wire format size of this
             * ComplicationSlotOverlay.
             */
            internal fun estimateWireSizeInBytes(): Int {
                var estimate = 16 // Estimate for everything except complicationSlotBounds
                complicationSlotBounds?.let {
                    estimate += it.perComplicationTypeBounds.size * (4 + 16)
                }
                return estimate
            }

            internal fun toWireFormat() =
                ComplicationOverlayWireFormat(
                    complicationSlotId,
                    enabled,
                    complicationSlotBounds?.perComplicationTypeBounds?.mapKeys {
                        it.key.toWireComplicationType()
                    },
                    accessibilityTraversalIndex
                )
        }

        /**
         * Constructs a ComplicationSlotsUserStyleSetting.
         *
         * @param id [Id] for the element, must be unique.
         * @param displayName Localized human readable name for the element, used in the userStyle
         * selection UI.
         * @param description Localized description string displayed under the displayName.
         * @param icon [Icon] for use in the userStyle selection UI. This gets sent to the
         * companion over bluetooth and should be small (ideally a few kb in size).
         * @param complicationConfig The configuration for affected complications.
         * @param affectsWatchFaceLayers Used by the style configuration UI. Describes which watch
         * face rendering layers this style affects, must include
         * [WatchFaceLayer.COMPLICATIONS].
         * @param defaultOption The default option, used when data isn't persisted. Optional
         * parameter which defaults to the first element of [complicationConfig].
         * @hide
         */
        @JvmOverloads
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        public constructor (
            id: Id,
            displayName: CharSequence,
            description: CharSequence,
            icon: Icon?,
            complicationConfig: List<ComplicationSlotsOption>,
            affectsWatchFaceLayers: Collection<WatchFaceLayer>,
            defaultOption: ComplicationSlotsOption = complicationConfig.first()
        ) : super(
            id,
            DisplayText.CharSequenceDisplayText(displayName),
            DisplayText.CharSequenceDisplayText(description),
            icon,
            complicationConfig,
            complicationConfig.indexOf(defaultOption),
            affectsWatchFaceLayers
        ) {
            require(affectsWatchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
                "ComplicationSlotsUserStyleSetting must affect the complications layer"
            }
            requireUniqueOptionIds(id, complicationConfig)
        }

        /**
         * Constructs a ComplicationSlotsUserStyleSetting where
         * [ComplicationSlotsUserStyleSetting.displayName] and
         * [ComplicationSlotsUserStyleSetting.description] are specified as resources.
         *
         * @param id [Id] for the element, must be unique.
         * @param resources The [Resources] from which [displayNameResourceId] and
         * [descriptionResourceId] are loaded.
         * @param displayNameResourceId String resource id for a human readable name for the element,
         * used in the userStyle selection UI.
         * @param descriptionResourceId String resource id for a human readable description string
         * displayed under the displayName.
         * @param icon [Icon] for use in the userStyle selection UI. This gets sent to the
         * companion over bluetooth and should be small (ideally a few kb in size).
         * @param complicationConfig The configuration for affected complications.
         * @param affectsWatchFaceLayers Used by the style configuration UI. Describes which watch
         * face rendering layers this style affects, must include
         * [WatchFaceLayer.COMPLICATIONS].
         * @param defaultOption The default option, used when data isn't persisted. Optional
         * parameter which defaults to the first element of [complicationConfig].
         */
        public constructor (
            id: Id,
            resources: Resources,
            @StringRes displayNameResourceId: Int,
            @StringRes descriptionResourceId: Int,
            icon: Icon?,
            complicationConfig: List<ComplicationSlotsOption>,
            affectsWatchFaceLayers: Collection<WatchFaceLayer>,
            defaultOption: ComplicationSlotsOption = complicationConfig.first()
        ) : super(
            id,
            DisplayText.ResourceDisplayText(resources, displayNameResourceId),
            DisplayText.ResourceDisplayText(resources, descriptionResourceId),
            icon,
            complicationConfig,
            complicationConfig.indexOf(defaultOption),
            affectsWatchFaceLayers
        ) {
            require(affectsWatchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
                "ComplicationSlotsUserStyleSetting must affect the complications layer"
            }
            requireUniqueOptionIds(id, complicationConfig)
        }

        internal constructor(
            wireFormat: ComplicationsUserStyleSettingWireFormat
        ) : super(wireFormat)

        /** @hide */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
        override fun toWireFormat(): ComplicationsUserStyleSettingWireFormat =
            ComplicationsUserStyleSettingWireFormat(
                id.value,
                displayName,
                description,
                icon,
                getWireFormatOptionsList(),
                defaultOptionIndex,
                affectedWatchFaceLayers.map { it.ordinal }
            )

        /**
         * Represents an override to the initial [androidx.wear.watchface.ComplicationSlotsManager]
         * configuration.
         */
        public class ComplicationSlotsOption : Option {
            /**
             * Overlays to be applied when this ComplicationSlotsOption is selected. If this is empty
             * then the net result is the initial complication configuration.
             */
            public val complicationSlotOverlays: Collection<ComplicationSlotOverlay>

            /** Backing field for [displayName]. */
            private val displayNameInternal: DisplayText

            /** Localized human readable name for the setting, used in the style selection UI. */
            public val displayName: CharSequence
                get() = displayNameInternal.toCharSequence()

            /** Icon for use in the style selection UI. */
            public val icon: Icon?

            /**
             * Constructs a ComplicationSlotsUserStyleSetting.
             *
             * @param id [Id] for the element, must be unique.
             * @param displayName Localized human readable name for the element, used in the
             * userStyle selection UI.
             * @param icon [Icon] for use in the style selection UI. This gets sent to the
             * companion over bluetooth and should be small (ideally a few kb in size).
             * @param complicationSlotOverlays Overlays to be applied when this
             * ComplicationSlotsOption is selected. If this is empty then the net result is the
             * initial complication configuration.
             * @hide
             */
            @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
            public constructor(
                id: Id,
                displayName: CharSequence,
                icon: Icon?,
                complicationSlotOverlays: Collection<ComplicationSlotOverlay>
            ) : super(id) {
                this.complicationSlotOverlays = complicationSlotOverlays
                this.displayNameInternal = DisplayText.CharSequenceDisplayText(displayName)
                this.icon = icon
            }

            /**
             * Constructs a ComplicationSlotsUserStyleSetting with [displayName] constructed from
             * Resources.
             *
             * @param id [Id] for the element, must be unique.
             * @param resources The [Resources] from which [displayNameResourceId] is load.
             * @param displayNameResourceId String resource id for a human readable name for the
             * element, used in the userStyle selection UI.
             * @param icon [Icon] for use in the style selection UI. This gets sent to the
             * companion over bluetooth and should be small (ideally a few kb in size).
             * @param complicationSlotOverlays Overlays to be applied when this
             * ComplicationSlotsOption is selected. If this is empty then the net result is the
             * initial complication configuration.
             */
            public constructor(
                id: Id,
                resources: Resources,
                @StringRes displayNameResourceId: Int,
                icon: Icon?,
                complicationSlotOverlays: Collection<ComplicationSlotOverlay>
            ) : super(id) {
                this.complicationSlotOverlays = complicationSlotOverlays
                this.displayNameInternal =
                    DisplayText.ResourceDisplayText(resources, displayNameResourceId)
                this.icon = icon
            }

            internal constructor(
                wireFormat: ComplicationsOptionWireFormat
            ) : super(Id(wireFormat.mId)) {
                complicationSlotOverlays =
                    wireFormat.mComplicationOverlays.map { ComplicationSlotOverlay(it) }
                displayNameInternal = DisplayText.CharSequenceDisplayText(wireFormat.mDisplayName)
                icon = wireFormat.mIcon
            }

            internal override fun getUserStyleSettingClass(): Class<out UserStyleSetting> =
                ComplicationSlotsUserStyleSetting::class.java

            internal override fun estimateWireSizeInBytesAndValidateIconDimensions(
                context: Context,
                @Px maxWidth: Int,
                @Px maxHeight: Int
            ): Int {
                var sizeEstimate = id.value.size + displayName.length
                for (overlay in complicationSlotOverlays) {
                    sizeEstimate += overlay.estimateWireSizeInBytes()
                }
                icon?.getWireSizeAndDimensions(context)?.let { wireSizeAndDimensions ->
                    wireSizeAndDimensions.wireSizeBytes?.let {
                        sizeEstimate += it
                    }
                    require(
                        wireSizeAndDimensions.width <= maxWidth &&
                            wireSizeAndDimensions.height <= maxHeight
                    ) {
                        "ComplicationSlotsOption id $id has a ${wireSizeAndDimensions.width} x " +
                            "${wireSizeAndDimensions.height} icon. This is too big, the maximum " +
                            "size is $maxWidth x $maxHeight."
                    }
                }
                return sizeEstimate
            }

            /** @hide */
            @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
            override fun toWireFormat():
                ComplicationsOptionWireFormat =
                    ComplicationsOptionWireFormat(
                        id.value,
                        displayName,
                        icon,
                        complicationSlotOverlays.map { it.toWireFormat() }.toTypedArray()
                    )
        }
    }

    /**
     * A DoubleRangeUserStyleSetting represents a setting with a [Double] value in the range
     * `[minimumValue .. maximumValue]`.
     */
    public class DoubleRangeUserStyleSetting : UserStyleSetting {

        internal companion object {
            internal fun createOptionsList(
                minimumValue: Double,
                maximumValue: Double,
                defaultValue: Double
            ): List<DoubleRangeOption> {
                require(minimumValue < maximumValue)
                require(defaultValue >= minimumValue)
                require(defaultValue <= maximumValue)

                return if (defaultValue != minimumValue && defaultValue != maximumValue) {
                    listOf(
                        DoubleRangeOption(minimumValue),
                        DoubleRangeOption(defaultValue),
                        DoubleRangeOption(maximumValue)
                    )
                } else {
                    listOf(DoubleRangeOption(minimumValue), DoubleRangeOption(maximumValue))
                }
            }
        }

        /**
         * Constructs a DoubleRangeUserStyleSetting.
         *
         * @param id [Id] for the element, must be unique.
         * @param displayName Localized human readable name for the element, used in the user style
         * selection UI.
         * @param description Localized description string displayed under the displayName.
         * @param icon [Icon] for use in the style selection UI. This gets sent to the
         * companion over bluetooth and should be small (ideally a few kb in size).
         * @param minimumValue Minimum value (inclusive).
         * @param maximumValue Maximum value (inclusive).
         * @param affectsWatchFaceLayers Used by the style configuration UI. Describes which watch
         * face rendering layers this style affects.
         * @param defaultValue The default value for this DoubleRangeUserStyleSetting.
         * @hide
         */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        public constructor (
            id: Id,
            displayName: CharSequence,
            description: CharSequence,
            icon: Icon?,
            minimumValue: Double,
            maximumValue: Double,
            affectsWatchFaceLayers: Collection<WatchFaceLayer>,
            defaultValue: Double
        ) : super(
            id,
            DisplayText.CharSequenceDisplayText(displayName),
            DisplayText.CharSequenceDisplayText(description),
            icon,
            createOptionsList(minimumValue, maximumValue, defaultValue),
            // The index of defaultValue can only ever be 0 or 1.
            when (defaultValue) {
                minimumValue -> 0
                else -> 1
            },
            affectsWatchFaceLayers
        )

        /**
         * Constructs a DoubleRangeUserStyleSetting where
         * [DoubleRangeUserStyleSetting.displayName] and
         * [DoubleRangeUserStyleSetting.description] are specified as resources.
         *
         * @param id [Id] for the element, must be unique.
         * @param resources The [Resources] from which [displayNameResourceId] and
         * [descriptionResourceId] are loaded.
         * @param displayNameResourceId String resource id for a human readable name for the element,
         * used in the userStyle selection UI.
         * @param descriptionResourceId String resource id for a human readable description string
         * displayed under the displayName.
         * @param icon [Icon] for use in the userStyle selection UI. This gets sent to the
         * companion over bluetooth and should be small (ideally a few kb in size).
         * @param minimumValue Minimum value (inclusive).
         * @param maximumValue Maximum value (inclusive).
         * @param affectsWatchFaceLayers Used by the style configuration UI. Describes which watch
         * face rendering layers this style affects.
         * @param defaultValue The default value for this DoubleRangeUserStyleSetting.
         */
        public constructor (
            id: Id,
            resources: Resources,
            @StringRes displayNameResourceId: Int,
            @StringRes descriptionResourceId: Int,
            icon: Icon?,
            minimumValue: Double,
            maximumValue: Double,
            affectsWatchFaceLayers: Collection<WatchFaceLayer>,
            defaultValue: Double
        ) : super(
            id,
            DisplayText.ResourceDisplayText(resources, displayNameResourceId),
            DisplayText.ResourceDisplayText(resources, descriptionResourceId),
            icon,
            createOptionsList(minimumValue, maximumValue, defaultValue),
            // The index of defaultValue can only ever be 0 or 1.
            when (defaultValue) {
                minimumValue -> 0
                else -> 1
            },
            affectsWatchFaceLayers
        )

        internal constructor(wireFormat: DoubleRangeUserStyleSettingWireFormat) : super(wireFormat)

        /** @hide */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
        override fun toWireFormat(): DoubleRangeUserStyleSettingWireFormat =
            DoubleRangeUserStyleSettingWireFormat(
                id.value,
                displayName,
                description,
                icon,
                getWireFormatOptionsList(),
                defaultOptionIndex,
                affectedWatchFaceLayers.map { it.ordinal }
            )

        /** Represents an option as a [Double] in the range [minimumValue .. maximumValue]. */
        public class DoubleRangeOption : Option {
            /* The value for this option. Must be within the range [minimumValue .. maximumValue].*/
            public val value: Double

            /**
             * Constructs a DoubleRangeOption.
             *
             * @param value The value of this DoubleRangeOption
             */
            public constructor(value: Double) : super(
                Id(ByteArray(8).apply { ByteBuffer.wrap(this).putDouble(value) })
            ) {
                this.value = value
            }

            internal constructor(
                wireFormat: DoubleRangeOptionWireFormat
            ) : super(Id(wireFormat.mId)) {
                value = ByteBuffer.wrap(wireFormat.mId).double
            }

            internal override fun getUserStyleSettingClass(): Class<out UserStyleSetting> =
                DoubleRangeUserStyleSetting::class.java

            /** @hide */
            @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
            override fun toWireFormat(): DoubleRangeOptionWireFormat =
                DoubleRangeOptionWireFormat(id.value)

            override fun toString(): String = value.toString()
        }

        /** Returns the minimum value. */
        public val minimumValue: Double
            get() = (options.first() as DoubleRangeOption).value

        /** Returns the maximum value. */
        public val maximumValue: Double
            get() = (options.last() as DoubleRangeOption).value

        /** Returns the default value. */
        public val defaultValue: Double
            get() = (options[defaultOptionIndex] as DoubleRangeOption).value

        /** We support all values in the range [min ... max] not just min & max. */
        override fun getOptionForId(optionId: Option.Id): Option =
            options.find { it.id.value.contentEquals(optionId.value) } ?: checkedOptionForId(
                optionId.value
            )

        private fun checkedOptionForId(optionId: ByteArray): DoubleRangeOption {
            return try {
                val value = ByteBuffer.wrap(optionId).double
                if (value < minimumValue || value > maximumValue) {
                    options[defaultOptionIndex] as DoubleRangeOption
                } else {
                    DoubleRangeOption(value)
                }
            } catch (e: Exception) {
                options[defaultOptionIndex] as DoubleRangeOption
            }
        }
    }

    /** A ListStyleCategory represents a setting with options selected from a List. */
    public open class ListUserStyleSetting : UserStyleSetting {

        /**
         * Constructs a ListUserStyleSetting.
         *
         * @param id [Id] for the element, must be unique.
         * @param displayName Localized human readable name for the element, used in the userStyle
         * selection UI.
         * @param description Localized description string displayed under the displayName.
         * @param icon [Icon] for use in the userStyle selection UI. This gets sent to the
         * companion over bluetooth and should be small (ideally a few kb in size).
         * @param options List of all options for this ListUserStyleSetting.
         * @param affectsWatchFaceLayers Used by the style configuration UI. Describes which watch
         * face rendering layers this style affects.
         * @param defaultOption The default option, used when data isn't persisted.
         * @hide
         */
        @JvmOverloads
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        public constructor (
            id: Id,
            displayName: CharSequence,
            description: CharSequence,
            icon: Icon?,
            options: List<ListOption>,
            affectsWatchFaceLayers: Collection<WatchFaceLayer>,
            defaultOption: ListOption = options.first()
        ) : super(
            id,
            DisplayText.CharSequenceDisplayText(displayName),
            DisplayText.CharSequenceDisplayText(description),
            icon,
            options,
            options.indexOf(defaultOption),
            affectsWatchFaceLayers
        ) {
            requireUniqueOptionIds(id, options)
        }

        /**
         * Constructs a ListUserStyleSetting where [ListUserStyleSetting.displayName] and
         * [ListUserStyleSetting.description] are specified as resources.
         *
         * @param id [Id] for the element, must be unique.
         * @param resources The [Resources] from which [displayNameResourceId] and
         * [descriptionResourceId] are loaded.
         * @param displayNameResourceId String resource id for a human readable name for the element,
         * used in the userStyle selection UI.
         * @param descriptionResourceId String resource id for a human readable description string
         * displayed under the displayName.
         * @param icon [Icon] for use in the userStyle selection UI. This gets sent to the
         * companion over bluetooth and should be small (ideally a few kb in size).
         * @param options List of all options for this ListUserStyleSetting.
         * @param affectsWatchFaceLayers Used by the style configuration UI. Describes which watch
         * face rendering layers this style affects.
         * @param defaultOption The default option, used when data isn't persisted.
         */
        public constructor (
            id: Id,
            resources: Resources,
            @StringRes displayNameResourceId: Int,
            @StringRes descriptionResourceId: Int,
            icon: Icon?,
            options: List<ListOption>,
            affectsWatchFaceLayers: Collection<WatchFaceLayer>,
            defaultOption: ListOption = options.first()
        ) : super(
            id,
            DisplayText.ResourceDisplayText(resources, displayNameResourceId),
            DisplayText.ResourceDisplayText(resources, descriptionResourceId),
            icon,
            options,
            options.indexOf(defaultOption),
            affectsWatchFaceLayers
        ) {
            requireUniqueOptionIds(id, options)
        }

        internal constructor(wireFormat: ListUserStyleSettingWireFormat) : super(wireFormat)

        /** @hide */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
        override fun toWireFormat(): ListUserStyleSettingWireFormat =
            ListUserStyleSettingWireFormat(
                id.value,
                displayName,
                description,
                icon,
                getWireFormatOptionsList(),
                defaultOptionIndex,
                affectedWatchFaceLayers.map { it.ordinal }
            )

        /**
         * Represents choice within a [ListUserStyleSetting], these must be enumerated up front.
         */
        public class ListOption : Option {
            /** Backing field for [displayName]. */
            private val displayNameInternal: DisplayText

            /** Localized human readable name for the setting, used in the style selection UI. */
            public val displayName: CharSequence
                get() = displayNameInternal.toCharSequence()

            /** Icon for use in the style selection UI. */
            public val icon: Icon?

            /**
             * Constructs a ListOption.
             *
             * @param id The [Id] of this ListOption, must be unique within the
             * [ListUserStyleSetting].
             * @param displayName Localized human readable name for the setting, used in the style
             * selection UI.
             * @param icon [Icon] for use in the style selection UI. This gets sent to the
             * companion over bluetooth and should be small (ideally a few kb in size).
             * @hide
             */
            @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
            public constructor(id: Id, displayName: CharSequence, icon: Icon?) : super(id) {
                displayNameInternal = DisplayText.CharSequenceDisplayText(displayName)
                this.icon = icon
            }

            /**
             * Constructs a ListOption.
             *
             * @param id The [Id] of this ListOption, must be unique within the
             * [ListUserStyleSetting].
             * @param resources The [Resources] used to load [displayNameResourceId].
             * @param displayNameResourceId String resource id for a human readable name for the
             * setting, used in the style selection UI.
             * @param icon [Icon] for use in the style selection UI. This gets sent to the
             * companion over bluetooth and should be small (ideally a few kb in size).
             */
            public constructor(
                id: Id,
                resources: Resources,
                @StringRes displayNameResourceId: Int,
                icon: Icon?
            ) : super(id) {
                displayNameInternal =
                    DisplayText.ResourceDisplayText(resources, displayNameResourceId)
                this.icon = icon
            }

            internal constructor(
                wireFormat: ListOptionWireFormat
            ) : super(Id(wireFormat.mId)) {
                displayNameInternal = DisplayText.CharSequenceDisplayText(wireFormat.mDisplayName)
                icon = wireFormat.mIcon
            }

            internal override fun getUserStyleSettingClass(): Class<out UserStyleSetting> =
                ListUserStyleSetting::class.java

            internal override fun estimateWireSizeInBytesAndValidateIconDimensions(
                context: Context,
                @Px maxWidth: Int,
                @Px maxHeight: Int
            ): Int {
                var sizeEstimate = id.value.size + displayName.length
                icon?.getWireSizeAndDimensions(context)?.let { wireSizeAndDimensions ->
                    wireSizeAndDimensions.wireSizeBytes?.let {
                        sizeEstimate += it
                    }
                    require(
                        wireSizeAndDimensions.width <= maxWidth &&
                            wireSizeAndDimensions.height <= maxHeight
                    ) {
                        "ListOption id $id has a ${wireSizeAndDimensions.width} x " +
                            "${wireSizeAndDimensions.height} icon. This is too big, the maximum " +
                            "size is $maxWidth x $maxHeight."
                    }
                }
                return sizeEstimate
            }

            /** @hide */
            @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
            override fun toWireFormat(): ListOptionWireFormat =
                ListOptionWireFormat(
                    id.value,
                    displayName,
                    icon
                )
        }
    }

    /**
     * A LongRangeUserStyleSetting represents a setting with a [Long] value in the range
     * [minimumValue .. maximumValue].
     */
    public class LongRangeUserStyleSetting : UserStyleSetting {

        internal companion object {
            internal fun createOptionsList(
                minimumValue: Long,
                maximumValue: Long,
                defaultValue: Long
            ): List<LongRangeOption> {
                require(minimumValue < maximumValue)
                require(defaultValue >= minimumValue)
                require(defaultValue <= maximumValue)

                return if (defaultValue != minimumValue && defaultValue != maximumValue) {
                    listOf(
                        LongRangeOption(minimumValue),
                        LongRangeOption(defaultValue),
                        LongRangeOption(maximumValue)
                    )
                } else {
                    listOf(
                        LongRangeOption(minimumValue),
                        LongRangeOption(maximumValue)
                    )
                }
            }
        }

        /**
         * Constructs a LongRangeUserStyleSetting.
         *
         * @param id [Id] for the element, must be unique.
         * @param displayName Localized human readable name for the element, used in the userStyle
         * selection UI.
         * @param description Localized description string displayed under the displayName.
         * @param icon [Icon] for use in the userStyle selection UI. This gets sent to the
         * companion over bluetooth and should be small (ideally a few kb in size).
         * @param minimumValue Minimum value (inclusive).
         * @param maximumValue Maximum value (inclusive).
         * @param affectsWatchFaceLayers Used by the style configuration UI. Describes which watch
         * face rendering layers this style affects.
         * @param defaultValue The default value for this LongRangeUserStyleSetting.
         * @hide
         */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        public constructor (
            id: Id,
            displayName: CharSequence,
            description: CharSequence,
            icon: Icon?,
            minimumValue: Long,
            maximumValue: Long,
            affectsWatchFaceLayers: Collection<WatchFaceLayer>,
            defaultValue: Long
        ) : super(
            id,
            DisplayText.CharSequenceDisplayText(displayName),
            DisplayText.CharSequenceDisplayText(description),
            icon,
            createOptionsList(minimumValue, maximumValue, defaultValue),
            // The index of defaultValue can only ever be 0 or 1.
            when (defaultValue) {
                minimumValue -> 0
                else -> 1
            },
            affectsWatchFaceLayers
        )

        /**
         * Constructs a LongRangeUserStyleSetting where [LongRangeUserStyleSetting.displayName] and
         * [LongRangeUserStyleSetting.description] are specified as resources.
         *
         * @param id [Id] for the element, must be unique.
         * @param resources The [Resources] from which [displayNameResourceId] and
         * [descriptionResourceId] are loaded.
         * @param displayNameResourceId String resource id for a human readable name for the element,
         * used in the userStyle selection UI.
         * @param descriptionResourceId String resource id for a human readable description string
         * displayed under the displayName.
         * @param icon [Icon] for use in the userStyle selection UI. This gets sent to the
         * companion over bluetooth and should be small (ideally a few kb in size).
         * @param minimumValue Minimum value (inclusive).
         * @param maximumValue Maximum value (inclusive).
         * @param affectsWatchFaceLayers Used by the style configuration UI. Describes which watch
         * face rendering layers this style affects.
         * @param defaultValue The default value for this LongRangeUserStyleSetting.
         */
        public constructor (
            id: Id,
            resources: Resources,
            @StringRes displayNameResourceId: Int,
            @StringRes descriptionResourceId: Int,
            icon: Icon?,
            minimumValue: Long,
            maximumValue: Long,
            affectsWatchFaceLayers: Collection<WatchFaceLayer>,
            defaultValue: Long
        ) : super(
            id,
            DisplayText.ResourceDisplayText(resources, displayNameResourceId),
            DisplayText.ResourceDisplayText(resources, descriptionResourceId),
            icon,
            createOptionsList(minimumValue, maximumValue, defaultValue),
            // The index of defaultValue can only ever be 0 or 1.
            when (defaultValue) {
                minimumValue -> 0
                else -> 1
            },
            affectsWatchFaceLayers
        )

        internal constructor(wireFormat: LongRangeUserStyleSettingWireFormat) : super(wireFormat)

        /** @hide */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
        override fun toWireFormat(): LongRangeUserStyleSettingWireFormat =
            LongRangeUserStyleSettingWireFormat(
                id.value,
                displayName,
                description,
                icon,
                getWireFormatOptionsList(),
                defaultOptionIndex,
                affectedWatchFaceLayers.map { it.ordinal }
            )

        /**
         * Represents an option a [Long] in the range [minimumValue .. maximumValue].
         */
        public class LongRangeOption : Option {
            /* The value for this option. Must be within the range [minimumValue..maximumValue]. */
            public val value: Long

            /**
             * Constructs a LongRangeOption.
             *
             * @param value The value of this LongRangeOption
             */
            public constructor(value: Long) : super(
                Id(ByteArray(8).apply { ByteBuffer.wrap(this).putLong(value) })
            ) {
                this.value = value
            }

            internal constructor(
                wireFormat: LongRangeOptionWireFormat
            ) : super(Id(wireFormat.mId)) {
                value = ByteBuffer.wrap(wireFormat.mId).long
            }

            internal override fun getUserStyleSettingClass(): Class<out UserStyleSetting> =
                LongRangeUserStyleSetting::class.java

            /** @hide */
            @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
            override fun toWireFormat(): LongRangeOptionWireFormat =
                LongRangeOptionWireFormat(id.value)

            override fun toString(): String = value.toString()
        }

        /** The minimum value. */
        public val minimumValue: Long
            get() = (options.first() as LongRangeOption).value

        /** The maximum value. */
        public val maximumValue: Long
            get() = (options.last() as LongRangeOption).value

        /** The default value. */
        public val defaultValue: Long
            get() = (options[defaultOptionIndex] as LongRangeOption).value

        /**
         * We support all values in the range [min ... max] not just min & max.
         */
        override fun getOptionForId(optionId: Option.Id): Option =
            options.find { it.id.value.contentEquals(optionId.value) } ?: checkedOptionForId(
                optionId.value
            )

        private fun checkedOptionForId(optionId: ByteArray): LongRangeOption {
            return try {
                val value = ByteBuffer.wrap(optionId).long
                if (value < minimumValue || value > maximumValue) {
                    options[defaultOptionIndex] as LongRangeOption
                } else {
                    LongRangeOption(value)
                }
            } catch (e: Exception) {
                options[defaultOptionIndex] as LongRangeOption
            }
        }
    }

    /**
     * An application specific style setting. This style is ignored by the system editor. This is
     * expected to be used in conjunction with an on watch face editor. Only a single
     * [ComplicationSlotsUserStyleSetting] is permitted in the [UserStyleSchema].
     */
    public class CustomValueUserStyleSetting : UserStyleSetting {
        internal companion object {
            internal const val CUSTOM_VALUE_USER_STYLE_SETTING_ID = "CustomValue"
        }

        /**
         * Constructs a CustomValueUserStyleSetting.
         *
         * @param affectsWatchFaceLayers Used by the style configuration UI. Describes which watch
         * face rendering layers this style affects.
         * @param defaultValue The default value [ByteArray].
         */
        public constructor (
            affectsWatchFaceLayers: Collection<WatchFaceLayer>,
            defaultValue: ByteArray
        ) : super(
            Id(CUSTOM_VALUE_USER_STYLE_SETTING_ID),
            DisplayText.CharSequenceDisplayText(""),
            DisplayText.CharSequenceDisplayText(""),
            null,
            listOf(CustomValueOption(defaultValue)),
            0,
            affectsWatchFaceLayers
        )

        internal constructor(wireFormat: CustomValueUserStyleSettingWireFormat) : super(wireFormat)

        /** @hide */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
        override fun toWireFormat(): CustomValueUserStyleSettingWireFormat =
            CustomValueUserStyleSettingWireFormat(
                id.value,
                displayName,
                description,
                icon,
                getWireFormatOptionsList(),
                affectedWatchFaceLayers.map { it.ordinal }
            )

        /**
         * An application specific custom value. NB the [CustomValueOption.customValue] is the same
         * as the [CustomValueOption.id].
         */
        public class CustomValueOption : Option {
            /* The [ByteArray] value for this option which is the same as the [id]. */
            public val customValue: ByteArray
                get() = id.value

            /**
             * Constructs a CustomValueOption.
             *
             * @param customValue The [ByteArray] [id] and value of this CustomValueOption. This
             * may not exceed [Id.MAX_LENGTH].
             */
            public constructor(customValue: ByteArray) : super(Id(customValue))

            internal constructor(
                wireFormat: CustomValueOptionWireFormat
            ) : super(Id(wireFormat.mId))

            internal override fun getUserStyleSettingClass(): Class<out UserStyleSetting> =
                CustomValueUserStyleSetting::class.java

            /** @hide */
            @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
            override fun toWireFormat(): CustomValueOptionWireFormat =
                CustomValueOptionWireFormat(id.value)
        }

        override fun getOptionForId(optionId: Option.Id): Option =
            options.find { it.id.value.contentEquals(optionId.value) } ?: CustomValueOption(
                optionId.value
            )
    }
}

internal fun requireUniqueOptionIds(
    setting: UserStyleSetting.Id,
    options: List<UserStyleSetting.Option>
) {
    val uniqueIds = HashSet<UserStyleSetting.Option.Id>()
    for (option in options) {
        require(uniqueIds.add(option.id)) {
            "duplicated option id: ${option.id} in $setting"
        }
    }
}

internal class WireSizeAndDimensions(
    val wireSizeBytes: Int?,
    val width: Int,
    val height: Int
)

@SuppressLint("ClassVerificationFailure", "ResourceType")
internal fun Icon.getWireSizeAndDimensions(context: Context): WireSizeAndDimensions {
    // Where possible use the exact wire size.
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
        when (type) {
            Icon.TYPE_RESOURCE -> {
                return getWireSizeAndDimensionsFromStream(
                    context.resources.openRawResource(resId, TypedValue()),
                    context.resources
                )
            }

            Icon.TYPE_URI -> {
                if (uri.scheme == ContentResolver.SCHEME_CONTENT) {
                    context.contentResolver.openInputStream(uri)?.let {
                        return getWireSizeAndDimensionsFromStream(it, context.resources)
                    }
                }
            }

            Icon.TYPE_URI_ADAPTIVE_BITMAP -> {
                if (uri.scheme == ContentResolver.SCHEME_CONTENT) {
                    context.contentResolver.openInputStream(uri)?.let {
                        return getWireSizeAndDimensionsFromStream(it, context.resources)
                    }
                }
            }
        }
    }

    // Fall back to loading the full drawable (comparatively expensive). We can't provide the
    // wire size in this instance.
    val drawable = loadDrawable(context)
    return WireSizeAndDimensions(null, drawable.minimumWidth, drawable.minimumHeight)
}

private fun getWireSizeAndDimensionsFromStream(
    stream: InputStream,
    resources: Resources
): WireSizeAndDimensions {
    try {
        val wireSize = stream.available()
        val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
        BitmapFactory.decodeResourceStream(
            resources,
            TypedValue(),
            stream,
            null,
            options
        )
        return WireSizeAndDimensions(wireSize, options.outWidth, options.outHeight)
    } finally {
        stream.close()
    }
}