Image.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.complications.data

import android.graphics.drawable.Icon
import android.os.Build
import androidx.annotation.RequiresApi

/**
 * A simple, monochromatic image that can be tinted by the watch face.
 *
 * An ambient alternative is provided that may be shown instead of the regular image while the
 * watch is not active.
 *
 * @param[image] the image itself
 * @param[ambientImage] the image to be shown when the device is in ambient mode to save power or
 * avoid burn in
 */
public class MonochromaticImage internal constructor(
    public val image: Icon,
    public val ambientImage: Icon?
) {
    /**
     * Builder for [MonochromaticImage].
     *
     * @param[image] the [Icon] representing the image
     */
    public class Builder(private var image: Icon) {
        private var ambientImage: Icon? = null

        /**
         * Sets a different image for when the device is ambient mode to save power and prevent
         * burn in. If no ambient variant is provided, the watch face may not show anything while
         * in ambient mode.
         */
        public fun setAmbientImage(ambientImage: Icon?): Builder = apply {
            this.ambientImage = ambientImage
        }

        /** Constructs the [MonochromaticImage]. */
        public fun build(): MonochromaticImage = MonochromaticImage(image, ambientImage)
    }

    /** Adds a [MonochromaticImage] to a builder for [WireComplicationData]. */
    internal fun addToWireComplicationData(builder: WireComplicationDataBuilder) = builder.apply {
        setIcon(image)
        setBurnInProtectionIcon(ambientImage)
    }

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

        other as MonochromaticImage

        if (!if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                IconHelperP.equals(image, other.image)
            } else {
                IconHelperBeforeP.equals(image, other.image)
            }
        ) return false

        if (!if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                IconHelperP.equals(ambientImage, other.ambientImage)
            } else {
                IconHelperBeforeP.equals(ambientImage, other.ambientImage)
            }
        ) return false

        return true
    }

    override fun hashCode(): Int {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            var result = IconHelperP.hashCode(image)
            result = 31 * result + IconHelperP.hashCode(ambientImage)
            result
        } else {
            var result = IconHelperBeforeP.hashCode(image)
            result = 31 * result + IconHelperBeforeP.hashCode(ambientImage)
            result
        }
    }

    override fun toString(): String {
        return "MonochromaticImage(image=$image, ambientImage=$ambientImage)"
    }
}

/**
 * The type of image being provided.
 *
 * This is used to guide rendering on the watch face.
 */
public enum class SmallImageType {
    /**
     * Type for images that have a transparent background and are expected to be drawn
     * entirely within the space available, such as a launcher image. Watch faces may add padding
     * when drawing these images, but should never crop these images. Icons may be tinted to fit
     * the complication style.
     */
    ICON,

    /**
     * Type for images which are photos that are expected to fill the space available. Images
     * of this style may be cropped to fit the shape of the complication - in particular, the image
     * may be cropped to a circle. Photos my not be recolored.
     */
    PHOTO
}

/**
 * An image that is expected to cover a small fraction of a watch face occupied by a single
 * complication.
 *
 * An ambient alternative is provided that may be shown instead of the regular image while the
 * watch is not active.
 *
 * @param[image] the image itself
 * @param[type] the style of the image provided, to guide how it should be displayed
 * @param[ambientImage] the image to be shown when the device is in ambient mode to save power or
 * avoid burn in
 */
public class SmallImage internal constructor(
    public val image: Icon,
    public val type: SmallImageType,
    public val ambientImage: Icon?
) {
    /**
     * Builder for [MonochromaticImage].
     *
     * @param[image] the [Icon] representing the image
     * @param[type] the style of the image provided, to guide how it should be displayed
     */
    public class Builder(private val image: Icon, private val type: SmallImageType) {
        private var ambientImage: Icon? = null

        /**
         * Sets a different image for when the device is ambient mode to save power and prevent
         * burn in. If no ambient variant is provided, the watch face may not show anything while
         * in ambient mode.
         */
        public fun setAmbientImage(ambientImage: Icon?): Builder = apply {
            this.ambientImage = ambientImage
        }

        /** Builds a [SmallImage]. */
        public fun build(): SmallImage = SmallImage(image, type, ambientImage)
    }

    /** Adds a [SmallImage] to a builder for [WireComplicationData]. */
    internal fun addToWireComplicationData(builder: WireComplicationDataBuilder) = builder.apply {
        setSmallImage(image)
        setSmallImageStyle(
            when (type) {
                SmallImageType.ICON -> WireComplicationData.IMAGE_STYLE_ICON
                SmallImageType.PHOTO -> WireComplicationData.IMAGE_STYLE_PHOTO
            }
        )
        setBurnInProtectionSmallImage(ambientImage)
    }

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

        other as SmallImage

        if (type != other.type) return false

        if (!if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                IconHelperP.equals(image, other.image)
            } else {
                IconHelperBeforeP.equals(image, other.image)
            }
        ) return false

        if (!if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                IconHelperP.equals(ambientImage, other.ambientImage)
            } else {
                IconHelperBeforeP.equals(ambientImage, other.ambientImage)
            }
        ) return false
        return true
    }

    override fun hashCode(): Int {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            var result = IconHelperP.hashCode(image)
            result = 31 * result + type.hashCode()
            result = 31 * result + IconHelperP.hashCode(ambientImage)
            result
        } else {
            var result = IconHelperBeforeP.hashCode(image)
            result = 31 * result + type.hashCode()
            result = 31 * result + IconHelperBeforeP.hashCode(ambientImage)
            result
        }
    }

    override fun toString(): String {
        return "SmallImage(image=$image, type=$type, ambientImage=$ambientImage)"
    }
}

@RequiresApi(Build.VERSION_CODES.P)
internal class IconHelperP {
    companion object {
        fun equals(a: Icon?, b: Icon?): Boolean {
            if (a == null) {
                return b == null
            }
            if (b == null) {
                return false
            }
            if (a.type != b.type) return false
            when (a.type) {
                Icon.TYPE_RESOURCE -> {
                    if (a.resId != b.resId) return false
                    if (a.resPackage != b.resPackage) return false
                }
                Icon.TYPE_URI -> {
                    if (a.uri.toString() != b.uri.toString()) return false
                }
                else -> {
                    if (a != b) return false
                }
            }
            return true
        }

        fun hashCode(a: Icon?): Int {
            if (a == null) return 0
            when (a.type) {
                Icon.TYPE_RESOURCE -> {
                    var result = a.type.hashCode()
                    result = 31 * result + a.resId.hashCode()
                    result = 31 * result + a.resPackage.hashCode()
                    return result
                }

                Icon.TYPE_URI -> {
                    var result = a.type.hashCode()
                    result = 31 * result + a.uri.toString().hashCode()
                    return result
                }

                else -> return a.hashCode()
            }
        }
    }
}

internal class IconHelperBeforeP {
    companion object {
        fun equals(a: Icon?, b: Icon?): Boolean = (a == b)

        fun hashCode(a: Icon?): Int = a?.hashCode() ?: 0
    }
}