UserStyleFlavors.kt

/*
 * Copyright 2022 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.content.res.Resources
import android.content.res.XmlResourceParser
import androidx.annotation.RestrictTo
import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
import androidx.wear.watchface.complications.IllegalNodeException
import androidx.wear.watchface.complications.getIntRefAttribute
import androidx.wear.watchface.complications.getStringRefAttribute
import androidx.wear.watchface.complications.hasValue
import androidx.wear.watchface.complications.iterate
import androidx.wear.watchface.style.data.UserStyleFlavorWireFormat
import androidx.wear.watchface.style.data.UserStyleFlavorsWireFormat
import java.io.IOException
import org.xmlpull.v1.XmlPullParserException

/**
 * Represents user specified preset of watch face.
 *
 * @param id An arbitrary string that uniquely identifies a flavor within the set of flavors
 *   supported by the watch face.
 * @param style Style info of the flavor represented by [UserStyleData].
 * @param complications Specifies complication data source policy represented by
 *   [DefaultComplicationDataSourcePolicy] for each [ComplicationSlot.id] presented in map. For
 *   absent complication slots default policies are used.
 */
public class UserStyleFlavor(
    public val id: String,
    public val style: UserStyleData,
    public val complications: Map<Int, DefaultComplicationDataSourcePolicy>
) {
    /** Constructs UserStyleFlavor based on [UserStyle] specified. */
    constructor(
        id: String,
        style: UserStyle,
        complications: Map<Int, DefaultComplicationDataSourcePolicy>
    ) : this(id, style.toUserStyleData(), complications) {}

    /** @hide */
    @Suppress("ShowingMemberInHiddenClass")
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    constructor(
        wireFormat: UserStyleFlavorWireFormat
    ) : this(
        wireFormat.mId,
        UserStyleData(wireFormat.mStyle),
        wireFormat.mComplications.mapValues { DefaultComplicationDataSourcePolicy(it.value) }
    ) {}

    /** @hide */
    @Suppress("ShowingMemberInHiddenClass")
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    fun toWireFormat() =
        UserStyleFlavorWireFormat(
            id,
            style.toWireFormat(),
            complications.mapValues { it.value.toWireFormat() }
        )

    override fun toString(): String = "UserStyleFlavor[$id: $style, $complications]"

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

        other as UserStyleFlavor

        if (id != other.id) return false
        if (style != other.style) return false
        if (complications != other.complications) return false

        return true
    }

    override fun hashCode(): Int {
        var result = id.hashCode()
        result = 31 * result + style.hashCode()
        result = 31 * result + complications.hashCode()
        return result
    }

    internal companion object {
        /** @hide */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        @Throws(IOException::class, XmlPullParserException::class)
        fun inflate(
            resources: Resources,
            parser: XmlResourceParser,
            schema: UserStyleSchema
        ): UserStyleFlavor {
            require(parser.name == "UserStyleFlavor") { "Expected a UserStyleFlavor node" }

            val flavorId = getStringRefAttribute(resources, parser, "id")
            require(flavorId != null) { "UserStyleFlavor must have an id" }

            val userStyle = schema.getDefaultUserStyle().toMutableUserStyle()
            val complications = mutableMapOf<Int, DefaultComplicationDataSourcePolicy>()
            parser.iterate {
                when (parser.name) {
                    "StyleOption" -> {
                        val id = getStringRefAttribute(resources, parser, "id")
                        require(id != null) { "StyleOption must have an id" }

                        require(parser.hasValue("value")) { "value is required for BooleanOption" }
                        val value = getStringRefAttribute(resources, parser, "value")

                        val setting = schema[UserStyleSetting.Id(id)]
                        require(setting != null) { "no setting found for id $id" }
                        when (setting) {
                            is UserStyleSetting.BooleanUserStyleSetting -> {
                                userStyle[setting] =
                                    UserStyleSetting.BooleanUserStyleSetting.BooleanOption.from(
                                        value!!.toBoolean()
                                    )
                            }
                            is UserStyleSetting.DoubleRangeUserStyleSetting -> {
                                userStyle[setting] =
                                    UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption(
                                        value!!.toDouble()
                                    )
                            }
                            is UserStyleSetting.LongRangeUserStyleSetting -> {
                                userStyle[setting] =
                                    UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption(
                                        value!!.toLong()
                                    )
                            }
                            else -> {
                                userStyle[setting] =
                                    setting.getOptionForId(UserStyleSetting.Option.Id(value!!))
                            }
                        }
                    }
                    "ComplicationPolicy" -> {
                        val id = getIntRefAttribute(resources, parser, "slotId")
                        require(id != null) { "slotId is required for ComplicationPolicy" }

                        val policy =
                            DefaultComplicationDataSourcePolicy.inflate(
                                resources,
                                parser,
                                "ComplicationPolicy"
                            )

                        complications[id] = policy
                    }
                    else -> throw IllegalNodeException(parser)
                }
            }

            return UserStyleFlavor(
                flavorId,
                userStyle.toUserStyle().toUserStyleData(),
                complications.toMap()
            )
        }
    }
}

/**
 * Collection of watch face flavors, represented by [UserStyleFlavor] class.
 *
 * @param flavors List of flavors.
 */
public class UserStyleFlavors(public val flavors: List<UserStyleFlavor>) {
    /** Constructs empty flavors collection. */
    constructor() : this(emptyList()) {}

    /** @hide */
    @Suppress("ShowingMemberInHiddenClass")
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    constructor(
        wireFormat: UserStyleFlavorsWireFormat
    ) : this(wireFormat.mFlavors.map { UserStyleFlavor(it) }) {}

    /** @hide */
    @Suppress("ShowingMemberInHiddenClass")
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    fun toWireFormat() = UserStyleFlavorsWireFormat(flavors.map { it.toWireFormat() })

    override fun toString(): String = "$flavors"

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

        other as UserStyleFlavors

        if (flavors != other.flavors) return false

        return true
    }

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

    /** @hide */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    companion object {
        @Throws(IOException::class, XmlPullParserException::class)
        fun inflate(
            resources: Resources,
            parser: XmlResourceParser,
            schema: UserStyleSchema
        ): UserStyleFlavors {
            require(parser.name == "UserStyleFlavors") { "Expected a UserStyleFlavors node" }

            val flavors = ArrayList<UserStyleFlavor>()
            parser.iterate {
                when (parser.name) {
                    "UserStyleFlavor" ->
                        flavors.add(UserStyleFlavor.inflate(resources, parser, schema))
                    else -> throw IllegalNodeException(parser)
                }
            }

            return UserStyleFlavors(flavors)
        }
    }
}