ComplicationDataTimeline.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.datasource

import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.complications.data.NoDataComplicationData
import java.time.Instant

/** The wire format for [ComplicationData]. */
internal typealias WireComplicationData = android.support.wearable.complications.ComplicationData

/**
 * A time interval, typically used to describe the validity period of a [TimelineEntry].
 *
 * @param start The [Instant] when this TimeInterval becomes valid
 * @param end The [Instant] when this TimeInterval becomes invalid, must be after [start]
 */
public class TimeInterval(
    public var start: Instant,
    public var end: Instant
) {
    init {
        require(start < end) { "start must be before end" }
    }

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

        other as TimeInterval

        if (start != other.start) return false
        if (end != other.end) return false

        return true
    }

    override fun hashCode(): Int {
        var result = start.hashCode()
        result = 31 * result + end.hashCode()
        return result
    }

    override fun toString(): String {
        return "TimeInterval(start=$start, end=$end)"
    }
}

/**
 * One piece of renderable content along with the time that it is valid for.
 *
 * @param validity [TimeInterval] describing the validity period for this timeline entry.
 * @param complicationData The renderable [ComplicationData].
 */
public class TimelineEntry(
    public var validity: TimeInterval,
    public var complicationData: ComplicationData
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as TimelineEntry

        if (validity != other.validity) return false
        if (complicationData != other.complicationData) return false

        return true
    }

    override fun hashCode(): Int {
        var result = validity.hashCode()
        result = 31 * result + complicationData.hashCode()
        return result
    }

    override fun toString(): String {
        return "TimelineEntry(validity=$validity, complicationData=$complicationData)"
    }
}

/**
 * A collection of TimelineEntry items.
 *
 * This allows a sequence of [ComplicationData] to be delivered to the watch face which can be
 * cached and updated automatically. E.g. today's weather forecast at various times or multiple
 * upcoming calendar events.
 *
 * In the case where the validity periods of TimelineEntry items overlap, the item with the
 * *shortest* validity period will be shown. If none are valid then the [defaultComplicationData]
 * will be shown.  This allows a complication datasource to show a "default", and override it at set
 * points without having to explicitly insert the default [ComplicationData] between the each
 * "override".
 *
 * The complication to render from a timeline is selected each time the watch face is rendered,
 * however the presence of a timeline does not trigger any extra frames to be rendered. Most watch
 * faces render at least once per minute at the top of the minute so complication updates should be
 * timely.
 *
 * Note older watch faces only support [defaultComplicationData], and v1.1 of wear-watchface is
 * required to support [timelineEntries].
 *
 * @param defaultComplicationData The default [ComplicationData] to be displayed
 * @param timelineEntries A collection of "overrides" to be displayed at certain times
 */
public class ComplicationDataTimeline(
    public val defaultComplicationData: ComplicationData,
    public val timelineEntries: Collection<TimelineEntry>
) {
    init {
        for (entry in timelineEntries) {
            val complicationData = entry.complicationData
            if (complicationData is NoDataComplicationData) {
                require(complicationData.placeholder == null ||
                    complicationData.placeholder!!.type == defaultComplicationData.type
                ) {
                    "TimelineEntry's placeholder types must match the defaultComplicationData. " +
                        "Found ${complicationData.placeholder!!.type} expected " +
                        "${defaultComplicationData.type}."
                }
            } else {
                require(
                    complicationData.type == defaultComplicationData.type
                ) {
                    "TimelineEntry's complicationData must have the same type as the " +
                        "defaultComplicationData or be NoDataComplicationData. Found " +
                        "${complicationData.type} expected ${defaultComplicationData.type}."
                }

                require(!complicationData.hasPlaceholderFields()) {
                    "Placeholder values may only be used in the context of " +
                        "NoDataComplicationData.placeholder ComplicationData."
                }
            }
        }
    }

    internal fun asWireComplicationData(): WireComplicationData {
        val wireTimelineEntries = timelineEntries.map { timelineEntry ->
            timelineEntry.complicationData.asWireComplicationData().apply {
                timelineStartEpochSecond = timelineEntry.validity.start.epochSecond
                timelineEndEpochSecond = timelineEntry.validity.end.epochSecond
            }
        }
        return defaultComplicationData.asWireComplicationData().apply {
            setTimelineEntryCollection(wireTimelineEntries)
        }
    }

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

        other as ComplicationDataTimeline

        if (defaultComplicationData != other.defaultComplicationData) return false
        if (timelineEntries != other.timelineEntries) return false

        return true
    }

    override fun hashCode(): Int {
        var result = defaultComplicationData.hashCode()
        result = 31 * result + timelineEntries.hashCode()
        return result
    }

    override fun toString(): String {
        return "ComplicationDataTimeline(defaultComplicationData=$defaultComplicationData, " +
            "timelineEntries=$timelineEntries)"
    }
}