blob: 85f41100acdf90e3d6e417cf457fb7133b9e7f8c [file] [log] [blame]
/*
* 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.app.PendingIntent
import android.graphics.drawable.Icon
import android.os.Build
import androidx.annotation.RestrictTo
import java.time.Instant
/** The wire format for [ComplicationData]. */
internal typealias WireComplicationData = android.support.wearable.complications.ComplicationData
/** The builder for [WireComplicationData]. */
internal typealias WireComplicationDataBuilder =
android.support.wearable.complications.ComplicationData.Builder
/**
* Base type for all different types of [ComplicationData] types.
*
* Please note to aid unit testing of ComplicationDataSourceServices, [equals], [hashCode] and
* [toString] have been overridden for all the types of ComplicationData, however due to the
* embedded [Icon] class we have to fall back to reference equality and hashing below API 28 and
* also for the [Icon]s that don't use either a resource or a uri (these should be rare but they
* can exist).
*/
public sealed class ComplicationData constructor(
public val type: ComplicationType,
public val tapAction: PendingIntent?,
internal var cachedWireComplicationData: WireComplicationData?,
/**
* Describes when the complication should be displayed.
*
* Whether the complication is active and should be displayed at the given time should be
* checked with [TimeRange.contains].
*/
public val validTimeRange: TimeRange = TimeRange.ALWAYS
) {
/**
* [tapAction] which is a [PendingIntent] unfortunately can't be serialized. This property is
* 'true' if tapAction has been lost due to serialization (typically because it has been cached
* locally). When 'true' the watch face should render the complication differently (e.g. as
* semi-transparent or grayed out) to signal to the user it can't be tapped. The system will
* subsequently deliver an updated complication, with a tapAction where applicable.
*/
@get:JvmName("isTapActionLostDueToSerialization")
public var tapActionLostDueToSerialization: Boolean =
cachedWireComplicationData?.tapActionLostDueToSerialization ?: false
/**
* Converts this value to [WireComplicationData] object used for serialization.
*
* This is only needed internally to convert to the underlying communication protocol.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public abstract fun asWireComplicationData(): WireComplicationData
internal fun createWireComplicationDataBuilder(): WireComplicationDataBuilder =
cachedWireComplicationData?.let {
WireComplicationDataBuilder(it)
} ?: WireComplicationDataBuilder(type.toWireComplicationType())
}
/**
* Type that can be sent by any complication data source, regardless of the configured type, when
* the complication data source has no data to be displayed. Watch faces may choose whether to
* render this in some way or leave the slot empty.
*/
public class NoDataComplicationData : ComplicationData(TYPE, null, null) {
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
override fun asWireComplicationData(): WireComplicationData = asPlainWireComplicationData(type)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return true
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
override fun toString(): String {
return "NoDataComplicationData()"
}
/** @hide */
public companion object {
/** The [ComplicationType] corresponding to objects of this type. */
@JvmField
public val TYPE: ComplicationType = ComplicationType.NO_DATA
}
}
/**
* Type sent when the user has specified that an active complication should have no complication
* data source, i.e. when the user has chosen "Empty" in the complication data source chooser.
* Complication data sources cannot send data of this type.
*/
public class EmptyComplicationData : ComplicationData(TYPE, null, null) {
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
override fun asWireComplicationData(): WireComplicationData = asPlainWireComplicationData(type)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return true
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
override fun toString(): String {
return "EmptyComplicationData()"
}
/** @hide */
public companion object {
/** The [ComplicationType] corresponding to objects of this type. */
@JvmField
public val TYPE: ComplicationType = ComplicationType.EMPTY
}
}
/**
* Type sent when a complication does not have a complication data source configured. The system
* will send data of this type to watch faces when the user has not chosen a complication data
* source for an active complication, and the watch face has not set a default complication data
* source. Complication data sources cannot send data of this type.
*/
public class NotConfiguredComplicationData : ComplicationData(TYPE, null, null) {
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
override fun asWireComplicationData(): WireComplicationData = asPlainWireComplicationData(type)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return true
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
override fun toString(): String {
return "NotConfiguredComplicationData()"
}
/** @hide */
public companion object {
/** The [ComplicationType] corresponding to objects of this type. */
@JvmField
public val TYPE: ComplicationType = ComplicationType.NOT_CONFIGURED
}
}
/**
* Type used for complications where the primary piece of data is a short piece of text
* (expected to be no more than seven characters in length). The text may be accompanied
* by an icon or a title or both.
*
* If only one of icon and title is provided, it is expected that it will be displayed. If both
* are provided, it is expected that at least one of these will be displayed.
*/
public class ShortTextComplicationData internal constructor(
public val text: ComplicationText,
public val title: ComplicationText?,
public val monochromaticImage: MonochromaticImage?,
public val contentDescription: ComplicationText?,
tapAction: PendingIntent?,
validTimeRange: TimeRange?,
cachedWireComplicationData: WireComplicationData?
) : ComplicationData(
TYPE, tapAction, cachedWireComplicationData, validTimeRange ?: TimeRange.ALWAYS
) {
/**
* Builder for [ShortTextComplicationData].
*
* You must at a minimum set the [text] and [contentDescription] fields.
*
* @param text The main localized [ComplicationText]. This must be less than 7 characters long
* @param contentDescription Localized description for use by screen readers
*/
public class Builder(
private val text: ComplicationText,
private var contentDescription: ComplicationText
) {
private var tapAction: PendingIntent? = null
private var validTimeRange: TimeRange? = null
private var title: ComplicationText? = null
private var monochromaticImage: MonochromaticImage? = null
private var cachedWireComplicationData: WireComplicationData? = null
/** Sets optional pending intent to be invoked when the complication is tapped. */
public fun setTapAction(tapAction: PendingIntent?): Builder = apply {
this.tapAction = tapAction
}
/** Sets optional time range during which the complication has to be shown. */
@Suppress("MissingGetterMatchingBuilder") // b/174052810
public fun setValidTimeRange(validTimeRange: TimeRange?): Builder = apply {
this.validTimeRange = validTimeRange
}
/** Sets optional title associated with the complication data. */
public fun setTitle(title: ComplicationText?): Builder = apply {
this.title = title
}
/** Sets optional icon associated with the complication data. */
public fun setMonochromaticImage(monochromaticImage: MonochromaticImage?): Builder = apply {
this.monochromaticImage = monochromaticImage
}
internal fun setCachedWireComplicationData(
cachedWireComplicationData: WireComplicationData?
): Builder = apply {
this.cachedWireComplicationData = cachedWireComplicationData
}
/** Builds the [ShortTextComplicationData]. */
public fun build(): ShortTextComplicationData =
ShortTextComplicationData(
text,
title,
monochromaticImage,
contentDescription,
tapAction,
validTimeRange,
cachedWireComplicationData
)
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
override fun asWireComplicationData(): WireComplicationData {
cachedWireComplicationData?.let {
return it
}
return createWireComplicationDataBuilder().apply {
setShortText(text.toWireComplicationText())
setShortTitle(title?.toWireComplicationText())
setContentDescription(
when (contentDescription) {
ComplicationText.EMPTY -> null
else -> contentDescription?.toWireComplicationText()
}
)
monochromaticImage?.addToWireComplicationData(this)
setTapAction(tapAction)
setValidTimeRange(validTimeRange, this)
setTapActionLostDueToSerialization(tapActionLostDueToSerialization)
}.build().also { cachedWireComplicationData = it }
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ShortTextComplicationData
if (text != other.text) return false
if (title != other.title) return false
if (monochromaticImage != other.monochromaticImage) return false
if (contentDescription != other.contentDescription) return false
if (tapActionLostDueToSerialization != other.tapActionLostDueToSerialization) return false
if (tapAction != other.tapAction) return false
if (validTimeRange != other.validTimeRange) return false
return true
}
override fun hashCode(): Int {
var result = text.hashCode()
result = 31 * result + (title?.hashCode() ?: 0)
result = 31 * result + (monochromaticImage?.hashCode() ?: 0)
result = 31 * result + (contentDescription?.hashCode() ?: 0)
result = 31 * result + tapActionLostDueToSerialization.hashCode()
result = 31 * result + (tapAction?.hashCode() ?: 0)
result = 31 * result + validTimeRange.hashCode()
return result
}
override fun toString(): String {
return "ShortTextComplicationData(text=$text, title=$title, " +
"monochromaticImage=$monochromaticImage, contentDescription=$contentDescription, " +
"tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
"tapAction=$tapAction, validTimeRange=$validTimeRange)"
}
/** @hide */
public companion object {
/** The [ComplicationType] corresponding to objects of this type. */
@JvmField
public val TYPE: ComplicationType = ComplicationType.SHORT_TEXT
/** The maximum length of [ShortTextComplicationData.text] in characters. */
@JvmField
public val MAX_TEXT_LENGTH = 7
}
}
/**
* Type used for complications where the primary piece of data is a piece of text. The text may
* be accompanied by an icon and/or a title.
*
* The text is expected to always be displayed.
*
* The title, if provided, it is expected that this field will be displayed.
*
* If at least one of the icon and image is provided, one of these should be displayed.
*/
public class LongTextComplicationData internal constructor(
public val text: ComplicationText,
public val title: ComplicationText?,
public val monochromaticImage: MonochromaticImage?,
public val smallImage: SmallImage?,
public val contentDescription: ComplicationText?,
tapAction: PendingIntent?,
validTimeRange: TimeRange?,
cachedWireComplicationData: WireComplicationData?
) : ComplicationData(
TYPE, tapAction, cachedWireComplicationData, validTimeRange ?: TimeRange.ALWAYS
) {
/**
* Builder for [LongTextComplicationData].
*
* You must at a minimum set the [text] and [contentDescription] fields.
*
* @param text Localized main [ComplicationText] to display within the complication. There
* isn't an explicit character limit but text may be truncated if too long
* @param contentDescription Localized description for use by screen readers
*/
public class Builder(
private val text: ComplicationText,
private var contentDescription: ComplicationText
) {
private var tapAction: PendingIntent? = null
private var validTimeRange: TimeRange? = null
private var title: ComplicationText? = null
private var monochromaticImage: MonochromaticImage? = null
private var smallImage: SmallImage? = null
private var cachedWireComplicationData: WireComplicationData? = null
/** Sets optional pending intent to be invoked when the complication is tapped. */
public fun setTapAction(tapAction: PendingIntent?): Builder = apply {
this.tapAction = tapAction
}
/** Sets optional time range during which the complication has to be shown. */
@Suppress("MissingGetterMatchingBuilder") // b/174052810
public fun setValidTimeRange(validTimeRange: TimeRange?): Builder = apply {
this.validTimeRange = validTimeRange
}
/** Sets optional title associated with the complication data. */
public fun setTitle(title: ComplicationText?): Builder = apply {
this.title = title
}
/** Sets optional image associated with the complication data. */
public fun setMonochromaticImage(icon: MonochromaticImage?): Builder = apply {
this.monochromaticImage = icon
}
/** Sets optional image associated with the complication data. */
public fun setSmallImage(smallImage: SmallImage?): Builder = apply {
this.smallImage = smallImage
}
internal fun setCachedWireComplicationData(
cachedWireComplicationData: WireComplicationData?
): Builder = apply {
this.cachedWireComplicationData = cachedWireComplicationData
}
/** Builds the [LongTextComplicationData]. */
public fun build(): LongTextComplicationData =
LongTextComplicationData(
text,
title,
monochromaticImage,
smallImage,
contentDescription,
tapAction,
validTimeRange,
cachedWireComplicationData
)
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
override fun asWireComplicationData(): WireComplicationData {
cachedWireComplicationData?.let {
return it
}
return createWireComplicationDataBuilder().apply {
setLongText(text.toWireComplicationText())
setLongTitle(title?.toWireComplicationText())
monochromaticImage?.addToWireComplicationData(this)
smallImage?.addToWireComplicationData(this)
setTapAction(tapAction)
setContentDescription(
when (contentDescription) {
ComplicationText.EMPTY -> null
else -> contentDescription?.toWireComplicationText()
}
)
setValidTimeRange(validTimeRange, this)
setTapActionLostDueToSerialization(tapActionLostDueToSerialization)
}.build().also { cachedWireComplicationData = it }
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as LongTextComplicationData
if (text != other.text) return false
if (title != other.title) return false
if (monochromaticImage != other.monochromaticImage) return false
if (smallImage != other.smallImage) return false
if (contentDescription != other.contentDescription) return false
if (tapActionLostDueToSerialization != other.tapActionLostDueToSerialization) return false
if (tapAction != other.tapAction) return false
if (validTimeRange != other.validTimeRange) return false
return true
}
override fun hashCode(): Int {
var result = text.hashCode()
result = 31 * result + (title?.hashCode() ?: 0)
result = 31 * result + (monochromaticImage?.hashCode() ?: 0)
result = 31 * result + (smallImage?.hashCode() ?: 0)
result = 31 * result + (contentDescription?.hashCode() ?: 0)
result = 31 * result + tapActionLostDueToSerialization.hashCode()
result = 31 * result + (tapAction?.hashCode() ?: 0)
result = 31 * result + validTimeRange.hashCode()
return result
}
override fun toString(): String {
return "LongTextComplicationData(text=$text, title=$title, " +
"monochromaticImage=$monochromaticImage, smallImage=$smallImage, " +
"contentDescription=$contentDescription), " +
"tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
"tapAction=$tapAction, validTimeRange=$validTimeRange)"
}
/** @hide */
public companion object {
/** The [ComplicationType] corresponding to objects of this type. */
@JvmField
public val TYPE: ComplicationType = ComplicationType.LONG_TEXT
}
}
/**
* Type used for complications including a numerical value within a range, such as a percentage.
* The value may be accompanied by an icon and/or short text and title.
*
* The [value], [min], and [max] fields are required for this type and the value within the
* range is expected to always be displayed.
*
* The icon, title, and text fields are optional and the watch face may choose which of these
* fields to display, if any.
*/
public class RangedValueComplicationData internal constructor(
public val value: Float,
public val min: Float,
public val max: Float,
public val monochromaticImage: MonochromaticImage?,
public val title: ComplicationText?,
public val text: ComplicationText?,
public val contentDescription: ComplicationText?,
tapAction: PendingIntent?,
validTimeRange: TimeRange?,
cachedWireComplicationData: WireComplicationData?
) : ComplicationData(
TYPE, tapAction, cachedWireComplicationData, validTimeRange ?: TimeRange.ALWAYS
) {
/**
* Builder for [RangedValueComplicationData].
*
* You must at a minimum set the [value], [min], [max] and [contentDescription] fields.
*
* @param value The value of the ranged complication which should be in the range
* [[min]] .. [[max]]
* @param min The minimum value
* @param max The maximum value
* @param contentDescription Localized description for use by screen readers
*/
public class Builder(
private val value: Float,
private val min: Float,
private val max: Float,
private var contentDescription: ComplicationText
) {
private var tapAction: PendingIntent? = null
private var validTimeRange: TimeRange? = null
private var monochromaticImage: MonochromaticImage? = null
private var title: ComplicationText? = null
private var text: ComplicationText? = null
private var cachedWireComplicationData: WireComplicationData? = null
/** Sets optional pending intent to be invoked when the complication is tapped. */
public fun setTapAction(tapAction: PendingIntent?): Builder = apply {
this.tapAction = tapAction
}
/** Sets optional time range during which the complication has to be shown. */
@Suppress("MissingGetterMatchingBuilder") // b/174052810
public fun setValidTimeRange(validTimeRange: TimeRange?): Builder = apply {
this.validTimeRange = validTimeRange
}
/** Sets optional icon associated with the complication data. */
public fun setMonochromaticImage(monochromaticImage: MonochromaticImage?): Builder = apply {
this.monochromaticImage = monochromaticImage
}
/** Sets optional title associated with the complication data. */
public fun setTitle(title: ComplicationText?): Builder = apply {
this.title = title
}
/** Sets optional title associated with the complication data. */
public fun setText(text: ComplicationText?): Builder = apply {
this.text = text
}
internal fun setCachedWireComplicationData(
cachedWireComplicationData: WireComplicationData?
): Builder = apply {
this.cachedWireComplicationData = cachedWireComplicationData
}
/** Builds the [RangedValueComplicationData]. */
public fun build(): RangedValueComplicationData =
RangedValueComplicationData(
value,
min,
max,
monochromaticImage,
title,
text,
contentDescription,
tapAction,
validTimeRange,
cachedWireComplicationData
)
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public override fun asWireComplicationData(): WireComplicationData {
cachedWireComplicationData?.let {
return it
}
return createWireComplicationDataBuilder().apply {
setRangedValue(value)
setRangedMinValue(min)
setRangedMaxValue(max)
monochromaticImage?.addToWireComplicationData(this)
setShortText(text?.toWireComplicationText())
setShortTitle(title?.toWireComplicationText())
setTapAction(tapAction)
setContentDescription(
when (contentDescription) {
ComplicationText.EMPTY -> null
else -> contentDescription?.toWireComplicationText()
}
)
setValidTimeRange(validTimeRange, this)
setTapActionLostDueToSerialization(tapActionLostDueToSerialization)
}.build().also { cachedWireComplicationData = it }
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as RangedValueComplicationData
if (value != other.value) return false
if (min != other.min) return false
if (max != other.max) return false
if (monochromaticImage != other.monochromaticImage) return false
if (title != other.title) return false
if (text != other.text) return false
if (contentDescription != other.contentDescription) return false
if (tapActionLostDueToSerialization != other.tapActionLostDueToSerialization) return false
if (tapAction != other.tapAction) return false
if (validTimeRange != other.validTimeRange) return false
return true
}
override fun hashCode(): Int {
var result = value.hashCode()
result = 31 * result + min.hashCode()
result = 31 * result + max.hashCode()
result = 31 * result + (monochromaticImage?.hashCode() ?: 0)
result = 31 * result + (title?.hashCode() ?: 0)
result = 31 * result + (text?.hashCode() ?: 0)
result = 31 * result + (contentDescription?.hashCode() ?: 0)
result = 31 * result + tapActionLostDueToSerialization.hashCode()
result = 31 * result + (tapAction?.hashCode() ?: 0)
result = 31 * result + validTimeRange.hashCode()
return result
}
override fun toString(): String {
return "RangedValueComplicationData(value=$value, min=$min, max=$max, " +
"monochromaticImage=$monochromaticImage, title=$title, text=$text, " +
"contentDescription=$contentDescription), " +
"tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
"tapAction=$tapAction, validTimeRange=$validTimeRange)"
}
/** @hide */
public companion object {
/** The [ComplicationType] corresponding to objects of this type. */
@JvmField
public val TYPE: ComplicationType = ComplicationType.RANGED_VALUE
}
}
/**
* Type used for complications which consist only of a [MonochromaticImage].
*
* The image is expected to always be displayed.
*
* The contentDescription field and is used to describe what data the icon represents. If the
* icon is purely stylistic, and does not convey any information to the user, then provide an
* empty content description. If no content description is provided, a generic content
* description will be used instead.
*/
public class MonochromaticImageComplicationData internal constructor(
public val monochromaticImage: MonochromaticImage,
public val contentDescription: ComplicationText?,
tapAction: PendingIntent?,
validTimeRange: TimeRange?,
cachedWireComplicationData: WireComplicationData?
) : ComplicationData(
TYPE, tapAction, cachedWireComplicationData, validTimeRange ?: TimeRange.ALWAYS
) {
/**
* Builder for [MonochromaticImageComplicationData].
*
* You must at a minimum set the [monochromaticImage] and [contentDescription] fields.
*
* @param monochromaticImage The [MonochromaticImage] to be displayed
* @param contentDescription Localized description for use by screen readers
*/
public class Builder(
private val monochromaticImage: MonochromaticImage,
private val contentDescription: ComplicationText
) {
private var tapAction: PendingIntent? = null
private var validTimeRange: TimeRange? = null
private var cachedWireComplicationData: WireComplicationData? = null
/** Sets optional pending intent to be invoked when the complication is tapped. */
public fun setTapAction(tapAction: PendingIntent?): Builder = apply {
this.tapAction = tapAction
}
/** Sets optional time range during which the complication has to be shown. */
@Suppress("MissingGetterMatchingBuilder") // b/174052810
public fun setValidTimeRange(validTimeRange: TimeRange?): Builder = apply {
this.validTimeRange = validTimeRange
}
internal fun setCachedWireComplicationData(
cachedWireComplicationData: WireComplicationData?
): Builder = apply {
this.cachedWireComplicationData = cachedWireComplicationData
}
/** Builds the [MonochromaticImageComplicationData]. */
public fun build(): MonochromaticImageComplicationData =
MonochromaticImageComplicationData(
monochromaticImage,
contentDescription,
tapAction,
validTimeRange,
cachedWireComplicationData
)
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
override fun asWireComplicationData(): WireComplicationData {
cachedWireComplicationData?.let {
return it
}
return createWireComplicationDataBuilder().apply {
monochromaticImage.addToWireComplicationData(this)
setContentDescription(
when (contentDescription) {
ComplicationText.EMPTY -> null
else -> contentDescription?.toWireComplicationText()
}
)
setTapAction(tapAction)
setValidTimeRange(validTimeRange, this)
setTapActionLostDueToSerialization(tapActionLostDueToSerialization)
}.build().also { cachedWireComplicationData = it }
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MonochromaticImageComplicationData
if (monochromaticImage != other.monochromaticImage) return false
if (contentDescription != other.contentDescription) return false
if (tapActionLostDueToSerialization != other.tapActionLostDueToSerialization) return false
if (tapAction != other.tapAction) return false
if (validTimeRange != other.validTimeRange) return false
return true
}
override fun hashCode(): Int {
var result = monochromaticImage.hashCode()
result = 31 * result + (contentDescription?.hashCode() ?: 0)
result = 31 * result + tapActionLostDueToSerialization.hashCode()
result = 31 * result + (tapAction?.hashCode() ?: 0)
result = 31 * result + validTimeRange.hashCode()
return result
}
override fun toString(): String {
return "MonochromaticImageComplicationData(monochromaticImage=$monochromaticImage, " +
"contentDescription=$contentDescription), " +
"tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
"tapAction=$tapAction, validTimeRange=$validTimeRange)"
}
/** @hide */
public companion object {
/** The [ComplicationType] corresponding to objects of this type. */
@JvmField
public val TYPE: ComplicationType = ComplicationType.MONOCHROMATIC_IMAGE
}
}
/**
* Type used for complications which consist only of a [SmallImage].
*
* The image is expected to always be displayed.
*
* The [contentDescription] field and is used to describe what data the icon represents. If the
* icon is purely stylistic, and does not convey any information to the user, then provide an
* empty content description. If no content description is provided, a generic content
* description will be used instead.
*/
public class SmallImageComplicationData internal constructor(
public val smallImage: SmallImage,
public val contentDescription: ComplicationText?,
tapAction: PendingIntent?,
validTimeRange: TimeRange?,
cachedWireComplicationData: WireComplicationData?
) : ComplicationData(
TYPE, tapAction, cachedWireComplicationData, validTimeRange ?: TimeRange.ALWAYS
) {
/**
* Builder for [SmallImageComplicationData].
*
* You must at a minimum set the [smallImage] and [contentDescription] fields.
*
* @param smallImage The [SmallImage] to be displayed
* @param contentDescription Localized description for use by screen readers
*/
public class Builder(
private val smallImage: SmallImage,
private val contentDescription: ComplicationText
) {
private var tapAction: PendingIntent? = null
private var validTimeRange: TimeRange? = null
private var cachedWireComplicationData: WireComplicationData? = null
/** Sets optional pending intent to be invoked when the complication is tapped. */
public fun setTapAction(tapAction: PendingIntent?): Builder = apply {
this.tapAction = tapAction
}
/** Sets optional time range during which the complication has to be shown. */
@Suppress("MissingGetterMatchingBuilder") // b/174052810
public fun setValidTimeRange(validTimeRange: TimeRange?): Builder = apply {
this.validTimeRange = validTimeRange
}
internal fun setCachedWireComplicationData(
cachedWireComplicationData: WireComplicationData?
): Builder = apply {
this.cachedWireComplicationData = cachedWireComplicationData
}
/** Builds the [MonochromaticImageComplicationData]. */
public fun build(): SmallImageComplicationData =
SmallImageComplicationData(
smallImage,
contentDescription,
tapAction,
validTimeRange,
cachedWireComplicationData
)
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
override fun asWireComplicationData(): WireComplicationData {
cachedWireComplicationData?.let {
return it
}
return createWireComplicationDataBuilder().apply {
smallImage.addToWireComplicationData(this)
setContentDescription(
when (contentDescription) {
ComplicationText.EMPTY -> null
else -> contentDescription?.toWireComplicationText()
}
)
setTapAction(tapAction)
setValidTimeRange(validTimeRange, this)
setTapActionLostDueToSerialization(tapActionLostDueToSerialization)
}.build().also { cachedWireComplicationData = it }
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SmallImageComplicationData
if (smallImage != other.smallImage) return false
if (contentDescription != other.contentDescription) return false
if (tapActionLostDueToSerialization != other.tapActionLostDueToSerialization) return false
if (tapAction != other.tapAction) return false
if (validTimeRange != other.validTimeRange) return false
return true
}
override fun hashCode(): Int {
var result = smallImage.hashCode()
result = 31 * result + (contentDescription?.hashCode() ?: 0)
result = 31 * result + tapActionLostDueToSerialization.hashCode()
result = 31 * result + (tapAction?.hashCode() ?: 0)
result = 31 * result + validTimeRange.hashCode()
return result
}
override fun toString(): String {
return "SmallImageComplicationData(smallImage=$smallImage, " +
"contentDescription=$contentDescription), " +
"tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
"tapAction=$tapAction, validTimeRange=$validTimeRange)"
}
/** @hide */
public companion object {
/** The [ComplicationType] corresponding to objects of this type. */
@JvmField
public val TYPE: ComplicationType = ComplicationType.SMALL_IMAGE
}
}
/**
* Type used for complications which consist only of an image that is expected to fill a large part
* of the watch face, large enough to be shown as either a background or as part of a high
* resolution complication.
*
* The image is expected to always be displayed. The image may be shown as the background, any
* other part of the watch face or within a complication. The image is large enough to be cover
* the entire screen. The image may be cropped to fit the watch face or complication.
*
* The [contentDescription] field and is used to describe what data the icon represents. If the
* icon is purely stylistic, and does not convey any information to the user, then provide an
* empty content description. If no content description is provided, a generic content
* description will be used instead.
*/
public class PhotoImageComplicationData internal constructor(
public val photoImage: Icon,
public val contentDescription: ComplicationText?,
tapAction: PendingIntent?,
validTimeRange: TimeRange?,
cachedWireComplicationData: WireComplicationData?
) : ComplicationData(
TYPE, tapAction, cachedWireComplicationData, validTimeRange ?: TimeRange.ALWAYS
) {
/**
* Builder for [PhotoImageComplicationData].
*
* You must at a minimum set the [photoImage] and [contentDescription] fields.
*
* @param photoImage The [Icon] to be displayed
* @param contentDescription Localized description for use by screen readers
*/
public class Builder(
private val photoImage: Icon,
private val contentDescription: ComplicationText
) {
private var tapAction: PendingIntent? = null
private var validTimeRange: TimeRange? = null
private var cachedWireComplicationData: WireComplicationData? = null
/** Sets optional pending intent to be invoked when the complication is tapped. */
@SuppressWarnings("MissingGetterMatchingBuilder") // See http://b/174052810
public fun setTapAction(tapAction: PendingIntent?): Builder = apply {
this.tapAction = tapAction
}
/** Sets optional time range during which the complication has to be shown. */
@SuppressWarnings("MissingGetterMatchingBuilder") // See http://b/174052810
public fun setValidTimeRange(validTimeRange: TimeRange?): Builder = apply {
this.validTimeRange = validTimeRange
}
internal fun setCachedWireComplicationData(
cachedWireComplicationData: WireComplicationData?
): Builder = apply {
this.cachedWireComplicationData = cachedWireComplicationData
}
/** Builds the [PhotoImageComplicationData]. */
public fun build(): PhotoImageComplicationData =
PhotoImageComplicationData(
photoImage,
contentDescription,
tapAction,
validTimeRange,
cachedWireComplicationData
)
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
override fun asWireComplicationData(): WireComplicationData {
cachedWireComplicationData?.let {
return it
}
return createWireComplicationDataBuilder().apply {
setLargeImage(photoImage)
setContentDescription(
when (contentDescription) {
ComplicationText.EMPTY -> null
else -> contentDescription?.toWireComplicationText()
}
)
setValidTimeRange(validTimeRange, this)
}.build().also { cachedWireComplicationData = it }
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PhotoImageComplicationData
if (!if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
IconHelperP.equals(photoImage, other.photoImage)
} else {
IconHelperBeforeP.equals(photoImage, other.photoImage)
}
) return false
if (contentDescription != other.contentDescription) return false
if (tapActionLostDueToSerialization != other.tapActionLostDueToSerialization) return false
if (tapAction != other.tapAction) return false
if (validTimeRange != other.validTimeRange) return false
return true
}
override fun hashCode(): Int {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
var result = IconHelperP.hashCode(photoImage)
result = 31 * result + (contentDescription?.hashCode() ?: 0)
result = 31 * result + tapActionLostDueToSerialization.hashCode()
result = 31 * result + (tapAction?.hashCode() ?: 0)
result = 31 * result + validTimeRange.hashCode()
result
} else {
var result = IconHelperBeforeP.hashCode(photoImage)
result = 31 * result + (contentDescription?.hashCode() ?: 0)
result = 31 * result + tapActionLostDueToSerialization.hashCode()
result = 31 * result + (tapAction?.hashCode() ?: 0)
result = 31 * result + validTimeRange.hashCode()
result
}
}
override fun toString(): String {
return "PhotoImageComplicationData(photoImage=$photoImage, " +
"contentDescription=$contentDescription), " +
"tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
"tapAction=$tapAction, validTimeRange=$validTimeRange)"
}
/** @hide */
public companion object {
/** The [ComplicationType] corresponding to objects of this type. */
@JvmField
public val TYPE: ComplicationType = ComplicationType.PHOTO_IMAGE
}
}
/**
* Type sent by the system when the watch face does not have permission to receive complication
* data.
*
* The text, title, and icon may be displayed by watch faces, but this is not required.
*
* It is recommended that, where possible, tapping on the complication when in this state
* should trigger a permission request. Note this is done by
* [androidx.wear.watchface.ComplicationSlotsManager] for androidx watch faces.
*/
public class NoPermissionComplicationData internal constructor(
public val text: ComplicationText?,
public val title: ComplicationText?,
public val monochromaticImage: MonochromaticImage?,
cachedWireComplicationData: WireComplicationData?
) : ComplicationData(TYPE, null, cachedWireComplicationData) {
/**
* Builder for [NoPermissionComplicationData].
*
* You must at a minimum set the [tapAction].
*/
public class Builder {
private var text: ComplicationText? = null
private var title: ComplicationText? = null
private var monochromaticImage: MonochromaticImage? = null
private var cachedWireComplicationData: WireComplicationData? = null
/** Sets optional text associated with the complication data. */
public fun setText(text: ComplicationText?): Builder = apply {
this.text = text
}
/** Sets optional title associated with the complication data. */
public fun setTitle(title: ComplicationText?): Builder = apply {
this.title = title
}
/** Sets optional icon associated with the complication data. */
public fun setMonochromaticImage(monochromaticImage: MonochromaticImage?): Builder = apply {
this.monochromaticImage = monochromaticImage
}
internal fun setCachedWireComplicationData(
cachedWireComplicationData: WireComplicationData?
): Builder = apply {
this.cachedWireComplicationData = cachedWireComplicationData
}
/** Builds the [NoPermissionComplicationData]. */
public fun build(): NoPermissionComplicationData =
NoPermissionComplicationData(
text,
title,
monochromaticImage,
cachedWireComplicationData
)
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
override fun asWireComplicationData(): WireComplicationData {
cachedWireComplicationData?.let {
return it
}
return createWireComplicationDataBuilder().apply {
setShortText(text?.toWireComplicationText())
setShortTitle(title?.toWireComplicationText())
monochromaticImage?.addToWireComplicationData(this)
}.build().also { cachedWireComplicationData = it }
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as NoPermissionComplicationData
if (text != other.text) return false
if (title != other.title) return false
if (monochromaticImage != other.monochromaticImage) return false
if (tapActionLostDueToSerialization != other.tapActionLostDueToSerialization) return false
if (tapAction != other.tapAction) return false
if (validTimeRange != other.validTimeRange) return false
return true
}
override fun hashCode(): Int {
var result = text?.hashCode() ?: 0
result = 31 * result + (title?.hashCode() ?: 0)
result = 31 * result + (monochromaticImage?.hashCode() ?: 0)
result = 31 * result + tapActionLostDueToSerialization.hashCode()
result = 31 * result + (tapAction?.hashCode() ?: 0)
result = 31 * result + validTimeRange.hashCode()
return result
}
override fun toString(): String {
return "NoPermissionComplicationData(text=$text, title=$title, " +
"monochromaticImage=$monochromaticImage, tapActionLostDueToSerialization=" +
"$tapActionLostDueToSerialization, tapAction=$tapAction, " +
"validTimeRange=$validTimeRange)"
}
/** @hide */
public companion object {
/** The [ComplicationType] corresponding to objects of this type. */
@JvmField
public val TYPE: ComplicationType = ComplicationType.NO_PERMISSION
}
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun WireComplicationData.toApiComplicationData(): ComplicationData {
val wireComplicationData = this
return when (type) {
NoDataComplicationData.TYPE.toWireComplicationType() -> NoDataComplicationData()
EmptyComplicationData.TYPE.toWireComplicationType() -> EmptyComplicationData()
NotConfiguredComplicationData.TYPE.toWireComplicationType() ->
NotConfiguredComplicationData()
ShortTextComplicationData.TYPE.toWireComplicationType() ->
ShortTextComplicationData.Builder(
shortText!!.toApiComplicationText(),
contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
).apply {
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
setTitle(shortTitle?.toApiComplicationText())
setMonochromaticImage(parseIcon())
setCachedWireComplicationData(wireComplicationData)
}.build()
LongTextComplicationData.TYPE.toWireComplicationType() ->
LongTextComplicationData.Builder(
longText!!.toApiComplicationText(),
contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
).apply {
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
setTitle(longTitle?.toApiComplicationText())
setMonochromaticImage(parseIcon())
setSmallImage(parseSmallImage())
setCachedWireComplicationData(wireComplicationData)
}.build()
RangedValueComplicationData.TYPE.toWireComplicationType() ->
RangedValueComplicationData.Builder(
value = rangedValue, min = rangedMinValue,
max = rangedMaxValue,
contentDescription = contentDescription?.toApiComplicationText()
?: ComplicationText.EMPTY
).apply {
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
setMonochromaticImage(parseIcon())
setTitle(shortTitle?.toApiComplicationText())
setText(shortText?.toApiComplicationText())
setCachedWireComplicationData(wireComplicationData)
}.build()
MonochromaticImageComplicationData.TYPE.toWireComplicationType() ->
MonochromaticImageComplicationData.Builder(
parseIcon()!!,
contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
).apply {
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
setCachedWireComplicationData(wireComplicationData)
}.build()
SmallImageComplicationData.TYPE.toWireComplicationType() ->
SmallImageComplicationData.Builder(
parseSmallImage()!!,
contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
).apply {
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
setCachedWireComplicationData(wireComplicationData)
}.build()
PhotoImageComplicationData.TYPE.toWireComplicationType() ->
PhotoImageComplicationData.Builder(
largeImage!!,
contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
).apply {
setValidTimeRange(parseTimeRange())
setCachedWireComplicationData(wireComplicationData)
}.build()
NoPermissionComplicationData.TYPE.toWireComplicationType() ->
NoPermissionComplicationData.Builder().apply {
setMonochromaticImage(parseIcon())
setTitle(shortTitle?.toApiComplicationText())
setText(shortText?.toApiComplicationText())
setCachedWireComplicationData(wireComplicationData)
}.build()
else -> NoDataComplicationData()
}
}
private fun WireComplicationData.parseTimeRange() =
if ((startDateTimeMillis == 0L) and (endDateTimeMillis == Long.MAX_VALUE)) {
null
} else {
TimeRange(
Instant.ofEpochMilli(startDateTimeMillis),
Instant.ofEpochMilli(endDateTimeMillis)
)
}
private fun WireComplicationData.parseIcon() =
icon?.let {
MonochromaticImage.Builder(it).apply {
setAmbientImage(burnInProtectionIcon)
}.build()
}
private fun WireComplicationData.parseSmallImage() =
smallImage?.let {
val imageStyle = when (smallImageStyle) {
WireComplicationData.IMAGE_STYLE_ICON -> SmallImageType.ICON
WireComplicationData.IMAGE_STYLE_PHOTO -> SmallImageType.PHOTO
else -> SmallImageType.PHOTO
}
SmallImage.Builder(it, imageStyle).apply {
setAmbientImage(burnInProtectionSmallImage)
}.build()
}
/** Some of the types, do not have any fields. This method provides a shorthard for that case. */
internal fun asPlainWireComplicationData(type: ComplicationType) =
WireComplicationDataBuilder(type.toWireComplicationType()).build()
internal fun setValidTimeRange(validTimeRange: TimeRange?, data: WireComplicationDataBuilder) {
validTimeRange?.let {
if (it.startDateTimeMillis > Instant.MIN) {
data.setStartDateTimeMillis(it.startDateTimeMillis.toEpochMilli())
}
if (it.endDateTimeMillis != Instant.MAX) {
data.setEndDateTimeMillis(it.endDateTimeMillis.toEpochMilli())
}
}
}