XmlSchemaAndComplicationSlotsDefinition.kt

/*
 * Copyright 2021 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.content.ComponentName
import android.content.pm.PackageManager
import android.content.res.Resources
import android.content.res.XmlResourceParser
import android.os.Bundle
import androidx.annotation.RestrictTo
import androidx.wear.watchface.complications.ComplicationSlotBounds
import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
import androidx.wear.watchface.complications.NAMESPACE_APP
import androidx.wear.watchface.complications.data.ComplicationExperimental
import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.complications.hasValue
import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyleSchema
import androidx.wear.watchface.style.getIntRefAttribute
import androidx.wear.watchface.style.moveToStart
import org.xmlpull.v1.XmlPullParser
import kotlin.jvm.Throws

/** @hide */
@OptIn(WatchFaceFlavorsExperimental::class, ComplicationExperimental::class)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class XmlSchemaAndComplicationSlotsDefinition(
    val schema: UserStyleSchema?,
    val complicationSlots: List<ComplicationSlotStaticData>,
    val flavors: UserStyleFlavors?
) {
    companion object {
        @Throws(PackageManager.NameNotFoundException::class)
        public fun inflate(
            resources: Resources,
            parser: XmlResourceParser
        ): XmlSchemaAndComplicationSlotsDefinition {
            parser.moveToStart("XmlWatchFace")

            var schema: UserStyleSchema? = null
            var flavors: UserStyleFlavors? = null
            val outerDepth = parser.depth

            var type = parser.next()

            // Parse the XmlWatchFace declaration.
            val complicationSlots = ArrayList<ComplicationSlotStaticData>()
            do {
                if (type == XmlPullParser.START_TAG) {
                    when (parser.name) {
                        "UserStyleSchema" -> {
                            schema = UserStyleSchema.inflate(resources, parser)
                        }
                        "ComplicationSlot" -> {
                            complicationSlots.add(
                                ComplicationSlotStaticData.inflate(resources, parser)
                            )
                        }
                        "UserStyleFlavors" -> {
                            require(schema != null) {
                                "A UserStyleFlavors node requires a previous UserStyleSchema node"
                            }
                            flavors = UserStyleFlavors.inflate(resources, parser, schema)
                        }
                        else -> throw IllegalArgumentException(
                            "Unexpected node ${parser.name} at line ${parser.lineNumber}"
                        )
                    }
                }
                type = parser.next()
            } while (type != XmlPullParser.END_DOCUMENT && parser.depth > outerDepth)

            parser.close()

            return XmlSchemaAndComplicationSlotsDefinition(schema, complicationSlots, flavors)
        }
    }

    public class ComplicationSlotStaticData(
        val slotId: Int,
        val accessibilityTraversalIndex: Int?,
        @ComplicationSlotBoundsType val boundsType: Int,
        val bounds: ComplicationSlotBounds,
        val supportedTypes: List<ComplicationType>,
        val defaultDataSourcePolicy: DefaultComplicationDataSourcePolicy,
        val initiallyEnabled: Boolean,
        val fixedComplicationDataSource: Boolean,
        val nameResourceId: Int?,
        val screenReaderNameResourceId: Int?,
        val boundingArc: BoundingArc?
    ) {
        companion object {
            fun inflateDefaultComplicationDataSourcePolicy(
                parser: XmlResourceParser,
                nodeName: String,
            ): DefaultComplicationDataSourcePolicy {
                val primaryDataSource =
                    parser.getAttributeValue(NAMESPACE_APP, "primaryDataSource")?.let {
                        ComponentName.unflattenFromString(it)
                    }
                val primaryDataSourceDefaultType =
                    if (parser.hasValue("primaryDataSourceDefaultType")) {
                        ComplicationType.fromWireType(
                            parser.getAttributeIntValue(
                                NAMESPACE_APP,
                                "primaryDataSourceDefaultType",
                                0
                            )
                        )
                    } else {
                        null
                    }
                val secondaryDataSource =
                    parser.getAttributeValue(NAMESPACE_APP, "secondaryDataSource")?.let {
                        ComponentName.unflattenFromString(it)
                    }

                val secondaryDataSourceDefaultType =
                    if (parser.hasValue("secondaryDataSourceDefaultType")) {
                        ComplicationType.fromWireType(
                            parser.getAttributeIntValue(
                                NAMESPACE_APP,
                                "secondaryDataSourceDefaultType",
                                0
                            )
                        )
                    } else {
                        null
                    }

                require(parser.hasValue("systemDataSourceFallback")) {
                    "A $nodeName must have a systemDataSourceFallback attribute"
                }
                val systemDataSourceFallback = parser.getAttributeIntValue(
                    NAMESPACE_APP, "systemDataSourceFallback", 0)
                require(parser.hasValue("systemDataSourceFallbackDefaultType")) {
                    "A $nodeName must have a systemDataSourceFallbackDefaultType attribute"
                }
                val systemDataSourceFallbackDefaultType = ComplicationType.fromWireType(
                    parser.getAttributeIntValue(
                        NAMESPACE_APP, "systemDataSourceFallbackDefaultType", 0))
                return when {
                    secondaryDataSource != null -> {
                        require(primaryDataSource != null) {
                            "If a secondaryDataSource is specified, a primaryDataSource must be too"
                        }
                        require(primaryDataSourceDefaultType != null) {
                            "If a secondaryDataSource is specified, a " +
                                "primaryDataSourceDefaultType must be too"
                        }
                        require(secondaryDataSourceDefaultType != null) {
                            "If a secondaryDataSource is specified, a " +
                                "secondaryDataSourceDefaultType must be too"
                        }
                        DefaultComplicationDataSourcePolicy(
                            primaryDataSource,
                            primaryDataSourceDefaultType,
                            secondaryDataSource,
                            secondaryDataSourceDefaultType,
                            systemDataSourceFallback,
                            systemDataSourceFallbackDefaultType
                        )
                    }

                    primaryDataSource != null -> {
                        require(primaryDataSourceDefaultType != null) {
                            "If a primaryDataSource is specified, a " +
                                "primaryDataSourceDefaultType must be too"
                        }
                        DefaultComplicationDataSourcePolicy(
                            primaryDataSource,
                            primaryDataSourceDefaultType,
                            systemDataSourceFallback,
                            systemDataSourceFallbackDefaultType
                        )
                    }

                    else -> {
                        DefaultComplicationDataSourcePolicy(
                            systemDataSourceFallback,
                            systemDataSourceFallbackDefaultType
                        )
                    }
                }
            }

            fun inflate(
                resources: Resources,
                parser: XmlResourceParser
            ): ComplicationSlotStaticData {
                require(parser.name == "ComplicationSlot") {
                    "Expected a UserStyleSchema node"
                }
                val slotId = getIntRefAttribute(resources, parser, "slotId")
                require(slotId != null) {
                    "A ComplicationSlot must have a slotId attribute"
                }
                val accessibilityTraversalIndex = if (
                    parser.hasValue("accessibilityTraversalIndex")
                ) {
                    parser.getAttributeIntValue(
                        NAMESPACE_APP,
                        "accessibilityTraversalIndex",
                        0
                    )
                } else {
                    null
                }
                require(parser.hasValue("boundsType")) {
                    "A ComplicationSlot must have a boundsType attribute"
                }
                val boundsType = when (
                    parser.getAttributeIntValue(NAMESPACE_APP, "boundsType", 0)
                ) {
                    0 -> ComplicationSlotBoundsType.ROUND_RECT
                    1 -> ComplicationSlotBoundsType.BACKGROUND
                    2 -> ComplicationSlotBoundsType.EDGE
                    else -> throw IllegalArgumentException("Unknown boundsType")
                }

                require(parser.hasValue("supportedTypes")) {
                    "A ComplicationSlot must have a supportedTypes attribute"
                }
                val supportedTypes =
                    parser.getAttributeIntValue(NAMESPACE_APP, "supportedTypes", 0)
                val supportedTypesList = ArrayList<ComplicationType>()
                if ((supportedTypes and 0x1) != 0) {
                    supportedTypesList.add(ComplicationType.SHORT_TEXT)
                }
                if ((supportedTypes and 0x2) != 0) {
                    supportedTypesList.add(ComplicationType.LONG_TEXT)
                }
                if ((supportedTypes and 0x4) != 0) {
                    supportedTypesList.add(ComplicationType.RANGED_VALUE)
                }
                if ((supportedTypes and 0x8) != 0) {
                    supportedTypesList.add(ComplicationType.MONOCHROMATIC_IMAGE)
                }
                if ((supportedTypes and 0x10) != 0) {
                    supportedTypesList.add(ComplicationType.SMALL_IMAGE)
                }
                if ((supportedTypes and 0x20) != 0) {
                    supportedTypesList.add(ComplicationType.PHOTO_IMAGE)
                }

                val defaultComplicationDataSourcePolicy =
                    inflateDefaultComplicationDataSourcePolicy(parser, "ComplicationSlot")

                val initiallyEnabled = parser.getAttributeBooleanValue(
                    NAMESPACE_APP,
                    "initiallyEnabled",
                    true
                )
                val fixedComplicationDataSource = parser.getAttributeBooleanValue(
                    NAMESPACE_APP,
                    "fixedComplicationDataSource",
                    false
                )
                val nameResourceId =
                    if (parser.hasValue("name")) {
                        parser.getAttributeResourceValue(NAMESPACE_APP, "name", 0)
                    } else {
                        null
                    }
                val screenReaderNameResourceId =
                    if (parser.hasValue("screenReaderName")) {
                        parser.getAttributeResourceValue(NAMESPACE_APP, "screenReaderName", 0)
                    } else {
                        null
                    }
                val boundingArc = if (parser.hasValue("startArcAngle")) {
                    BoundingArc(
                        parser.getAttributeFloatValue(NAMESPACE_APP, "startArcAngle", 0f),
                        parser.getAttributeFloatValue(NAMESPACE_APP, "totalArcAngle", 0f),
                        parser.getAttributeFloatValue(NAMESPACE_APP, "arcThickness", 0f)
                    )
                } else {
                    null
                }
                val bounds = ComplicationSlotBounds.inflate(resources, parser)
                require(bounds != null) {
                    "ComplicationSlot must have either one ComplicationSlotBounds child node or " +
                        "one per ComplicationType."
                }
                return ComplicationSlotStaticData(
                    slotId,
                    accessibilityTraversalIndex,
                    boundsType,
                    bounds,
                    supportedTypesList,
                    defaultComplicationDataSourcePolicy,
                    initiallyEnabled,
                    fixedComplicationDataSource,
                    nameResourceId,
                    screenReaderNameResourceId,
                    boundingArc
                )
            }
        }
    }

    fun buildComplicationSlotsManager(
        currentUserStyleRepository: CurrentUserStyleRepository,
        complicationSlotInflationFactory: ComplicationSlotInflationFactory?
    ): ComplicationSlotsManager {
        if (complicationSlots.isEmpty()) {
            return ComplicationSlotsManager(emptyList(), currentUserStyleRepository)
        }

        require(complicationSlotInflationFactory != null) {
            "You must override WatchFaceService.getComplicationSlotInflationFactory to provide " +
            "additional details needed to inflate ComplicationSlotsManager"
        }

        return ComplicationSlotsManager(
            complicationSlots.map {
                ComplicationSlot(
                    it.slotId,
                    it.accessibilityTraversalIndex ?: it.slotId,
                    it.boundsType,
                    it.bounds,
                    complicationSlotInflationFactory.getCanvasComplicationFactory(it.slotId),
                    it.supportedTypes,
                    it.defaultDataSourcePolicy,
                    it.defaultDataSourcePolicy.systemDataSourceFallbackDefaultType,
                    it.initiallyEnabled,
                    Bundle(),
                    it.fixedComplicationDataSource,
                    when (it.boundsType) {
                        ComplicationSlotBoundsType.ROUND_RECT -> RoundRectComplicationTapFilter()
                        ComplicationSlotBoundsType.BACKGROUND -> BackgroundComplicationTapFilter()
                        ComplicationSlotBoundsType.EDGE ->
                            complicationSlotInflationFactory.getEdgeComplicationTapFilter(it.slotId)
                        else -> throw UnsupportedOperationException(
                            "Unknown boundsType ${it.boundsType}"
                        )
                    },
                    it.nameResourceId,
                    it.screenReaderNameResourceId,
                    it.boundingArc
                )
            },
            currentUserStyleRepository
        )
    }
}