blob: f4daaec5738805f16bafaf57ef9b47988cd35b0b [file] [log] [blame]
/*
* Copyright 2020 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 android.support.wearable.complications;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.BadParcelableException;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Container for complication data of all types.
*
* <p>A {@link androidx.wear.watchface.complications.ComplicationProviderService} should create
* instances of
* this class using {@link ComplicationData.Builder} and send them to the complication system in
* response to
* {@link androidx.wear.watchface.complications.ComplicationProviderService#onComplicationRequest}.
* Depending on the type of complication data, some fields will be required and some will be
* optional - see the documentation for each type, and for the builder's set methods, for details.
*
* <p>A watch face will receive instances of this class as long as providers are configured.
*
* <p>When rendering the complication data for a given time, the watch face should first call {@link
* #isActiveAt} to determine whether the data is valid at that time. See the documentation for each
* of the complication types below for details of which fields are expected to be displayed.
*
* @hide
*/
@SuppressLint("BanParcelableUsage")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final class ComplicationData implements Parcelable, Serializable {
private static final String TAG = "ComplicationData";
/** @hide */
@IntDef({
TYPE_EMPTY,
TYPE_NOT_CONFIGURED,
TYPE_SHORT_TEXT,
TYPE_LONG_TEXT,
TYPE_RANGED_VALUE,
TYPE_ICON,
TYPE_SMALL_IMAGE,
TYPE_LARGE_IMAGE,
TYPE_NO_PERMISSION,
TYPE_NO_DATA
})
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Retention(RetentionPolicy.SOURCE)
public @interface ComplicationType {
}
/**
* Type sent when a complication does not have a provider configured. The system will send data
* of this type to watch faces when the user has not chosen a provider for an active
* complication, and the watch face has not set a default provider. Providers cannot send data
* of this type.
*
* <p>No fields may be populated for complication data of this type.
*/
public static final int TYPE_NOT_CONFIGURED = 1;
/**
* Type sent when the user has specified that an active complication should have no provider,
* i.e. when the user has chosen "Empty" in the provider chooser. Providers cannot send data of
* this type.
*
* <p>No fields may be populated for complication data of this type.
*/
public static final int TYPE_EMPTY = 2;
/**
* Type that can be sent by any provider, regardless of the configured type, when the provider
* has no data to be displayed. Watch faces may choose whether to render this in some way or
* leave the slot empty.
*
* <p>No fields may be populated for complication data of this type.
*/
public static final int TYPE_NO_DATA = 10;
/**
* 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 short text may be accompanied
* by an icon or a short title (or both, but if both are provided then a watch face may choose
* to display only one).
*
* <p>The <i>short text</i> field is required for this type, and is expected to always be
* displayed.
*
* <p>The <i>icon</i> (and <i>burnInProtectionIcon</i>) and <i>short title</i> fields are
* optional for this type. If only one of these is provided, it is expected that it will be
* displayed. If both are provided, it is expected that one of these will be displayed.
*/
public static final int TYPE_SHORT_TEXT = 3;
/**
* 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.
*
* <p>The <i>long text</i> field is required for this type, and is expected to always be
* displayed.
*
* <p>The <i>long title</i> field is optional for this type. If provided, it is expected that
* this field will be displayed.
*
* <p>The <i>icon</i> (and <i>burnInProtectionIcon</i>) and <i>small image</i> fields are also
* optional for this type. If provided, at least one of these should be displayed.
*/
public static final int TYPE_LONG_TEXT = 4;
/**
* 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.
*
* <p>The <i>value</i>, <i>min value</i>, and <i>max value</i> fields are required for this
* type, and the value within the range is expected to always be displayed.
*
* <p>The <i>icon</i> (and <i>burnInProtectionIcon</i>), <i>short title</i>, and <i>short
* text</i> fields are optional for this type. The watch face may choose which of these fields
* to display, if any.
*/
public static final int TYPE_RANGED_VALUE = 5;
/**
* Type used for complications which consist only of a tintable icon.
*
* <p>The <i>icon</i> field is required for this type, and is expected to always be displayed,
* unless the device is in ambient mode with burn-in protection enabled, in which case the
* <i>burnInProtectionIcon</i> field should be used instead.
*
* <p>The contentDescription field is recommended for this type. Use it to describe what data
* the icon represents. If the icon is purely stylistic, and does not convey any information to
* the user, then enter the empty string as the contentDescription.
*
* <p>No other fields are valid for this type.
*/
public static final int TYPE_ICON = 6;
/**
* Type used for complications which consist only of a small image.
*
* <p>The <i>small image</i> field is required for this type, and is expected to always be
* displayed, unless the device is in ambient mode, in which case either nothing or the
* <i>burnInProtectionSmallImage</i> field may be used instead.
*
* <p>The contentDescription field is recommended for this type. Use it to describe what data
* the image represents. If the image is purely stylistic, and does not convey any information
* to the user, then enter the empty string as the contentDescription.
*
* <p>No other fields are valid for this type.
*/
public static final int TYPE_SMALL_IMAGE = 7;
/**
* Type used for complications which consist only of a large image. A large image here is one
* that could be used to fill the watch face, for example as the background.
*
* <p>The <i>large image</i> field is required for this type, and is expected to always be
* displayed, unless the device is in ambient mode.
*
* <p>The contentDescription field is recommended for this type. Use it to describe what data
* the image represents. If the image is purely stylistic, and does not convey any information
* to the user, then enter the empty string as the contentDescription.
*
* <p>No other fields are valid for this type.
*/
public static final int TYPE_LARGE_IMAGE = 8;
/**
* Type sent by the system when the watch face does not have permission to receive complication
* data.
*
* <p>Fields will be populated to allow the data to be rendered as if it were of {@link
* #TYPE_SHORT_TEXT} or {@link #TYPE_ICON} for consistency and convenience, but watch faces may
* render this as they see fit.
*
* <p>It is recommended that, where possible, tapping on the complication when in this state
* should trigger a permission request.
*/
public static final int TYPE_NO_PERMISSION = 9;
/** @hide */
@IntDef({IMAGE_STYLE_PHOTO, IMAGE_STYLE_ICON})
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Retention(RetentionPolicy.SOURCE)
public @interface ImageStyle {
}
/**
* Style for small 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.
*
* <p>This is the default value.
*/
public static final int IMAGE_STYLE_PHOTO = 1;
/**
* Style for small images that have a transparent background and are expected to be drawn
* entirely within the space available, such as a launcher icon. Watch faces may add padding
* when drawing these images, but should never crop these images. Icons may be recolored to fit
* the complication style.
*/
public static final int IMAGE_STYLE_ICON = 2;
private static final String FIELD_START_TIME = "START_TIME";
private static final String FIELD_END_TIME = "END_TIME";
private static final String FIELD_SHORT_TITLE = "SHORT_TITLE";
private static final String FIELD_SHORT_TEXT = "SHORT_TEXT";
private static final String FIELD_LONG_TITLE = "LONG_TITLE";
private static final String FIELD_LONG_TEXT = "LONG_TEXT";
private static final String FIELD_VALUE = "VALUE";
private static final String FIELD_MIN_VALUE = "MIN_VALUE";
private static final String FIELD_MAX_VALUE = "MAX_VALUE";
private static final String FIELD_ICON = "ICON";
private static final String FIELD_ICON_BURN_IN_PROTECTION = "ICON_BURN_IN_PROTECTION";
private static final String FIELD_SMALL_IMAGE = "SMALL_IMAGE";
private static final String FIELD_SMALL_IMAGE_BURN_IN_PROTECTION =
"SMALL_IMAGE_BURN_IN_PROTECTION";
private static final String FIELD_LARGE_IMAGE = "LARGE_IMAGE";
private static final String FIELD_TAP_ACTION = "TAP_ACTION";
private static final String FIELD_TAP_ACTION_LOST = "FIELD_TAP_ACTION_LOST";
private static final String FIELD_IMAGE_STYLE = "IMAGE_STYLE";
private static final String FIELD_TIMELINE_START_TIME = "TIMELINE_START_TIME";
private static final String FIELD_TIMELINE_END_TIME = "TIMELINE_END_TIME";
private static final String FIELD_TIMELINE_ENTRIES = "TIMELINE";
private static final String FIELD_TIMELINE_ENTRY_TYPE = "TIMELINE_ENTRY_TYPE";
private static final String FIELD_PLACEHOLDER_FIELDS = "PLACEHOLDER_FIELDS";
private static final String FIELD_PLACEHOLDER_TYPE = "PLACEHOLDER_TYPE";
private static final String FIELD_DATA_SOURCE = "FIELD_DATA_SOURCE";
// Originally it was planned to support both content and image content descriptions.
private static final String FIELD_CONTENT_DESCRIPTION = "IMAGE_CONTENT_DESCRIPTION";
// Used for validation. REQUIRED_FIELDS[i] is an array containing all the fields which must be
// populated for @ComplicationType i.
private static final String[][] REQUIRED_FIELDS = {
null,
{}, // NOT_CONFIGURED
{}, // EMPTY
{FIELD_SHORT_TEXT}, // SHORT_TEXT
{FIELD_LONG_TEXT}, // LONG_TEXT
{FIELD_VALUE, FIELD_MIN_VALUE, FIELD_MAX_VALUE}, // RANGED_VALUE
{FIELD_ICON}, // ICON
{FIELD_SMALL_IMAGE, FIELD_IMAGE_STYLE}, // SMALL_IMAGE
{FIELD_LARGE_IMAGE}, // LARGE_IMAGE
{}, // TYPE_NO_PERMISSION
{}, // TYPE_NO_DATA
};
// Used for validation. OPTIONAL_FIELDS[i] is an array containing all the fields which are
// valid but not required for type i.
private static final String[][] OPTIONAL_FIELDS = {
null,
{}, // NOT_CONFIGURED
{}, // EMPTY
{
FIELD_SHORT_TITLE,
FIELD_ICON,
FIELD_ICON_BURN_IN_PROTECTION,
FIELD_TAP_ACTION,
FIELD_CONTENT_DESCRIPTION,
FIELD_DATA_SOURCE
}, // SHORT_TEXT
{
FIELD_LONG_TITLE,
FIELD_ICON,
FIELD_ICON_BURN_IN_PROTECTION,
FIELD_SMALL_IMAGE,
FIELD_SMALL_IMAGE_BURN_IN_PROTECTION,
FIELD_IMAGE_STYLE,
FIELD_TAP_ACTION,
FIELD_CONTENT_DESCRIPTION,
FIELD_DATA_SOURCE
}, // LONG_TEXT
{
FIELD_SHORT_TEXT,
FIELD_SHORT_TITLE,
FIELD_ICON,
FIELD_ICON_BURN_IN_PROTECTION,
FIELD_TAP_ACTION,
FIELD_CONTENT_DESCRIPTION,
FIELD_DATA_SOURCE
}, // RANGED_VALUE
{
FIELD_TAP_ACTION,
FIELD_ICON_BURN_IN_PROTECTION,
FIELD_CONTENT_DESCRIPTION,
FIELD_DATA_SOURCE
}, // ICON
{
FIELD_TAP_ACTION,
FIELD_SMALL_IMAGE_BURN_IN_PROTECTION,
FIELD_CONTENT_DESCRIPTION,
FIELD_DATA_SOURCE
}, // SMALL_IMAGE
{
FIELD_TAP_ACTION, FIELD_CONTENT_DESCRIPTION, FIELD_DATA_SOURCE
}, // LARGE_IMAGE
{
FIELD_SHORT_TEXT,
FIELD_SHORT_TITLE,
FIELD_ICON,
FIELD_ICON_BURN_IN_PROTECTION,
FIELD_CONTENT_DESCRIPTION,
FIELD_DATA_SOURCE
}, // TYPE_NO_PERMISSION
{ // TYPE_NO_DATA
FIELD_CONTENT_DESCRIPTION,
FIELD_ICON,
FIELD_ICON_BURN_IN_PROTECTION,
FIELD_IMAGE_STYLE,
FIELD_LARGE_IMAGE,
FIELD_LONG_TEXT,
FIELD_LONG_TITLE,
FIELD_MAX_VALUE,
FIELD_MIN_VALUE,
FIELD_PLACEHOLDER_FIELDS,
FIELD_PLACEHOLDER_TYPE,
FIELD_SHORT_TEXT,
FIELD_SHORT_TITLE,
FIELD_SMALL_IMAGE,
FIELD_SMALL_IMAGE_BURN_IN_PROTECTION,
FIELD_TAP_ACTION,
FIELD_VALUE,
FIELD_DATA_SOURCE
}
};
@NonNull
public static final Creator<ComplicationData> CREATOR =
new Creator<ComplicationData>() {
@SuppressLint("SyntheticAccessor")
@NonNull
@Override
public ComplicationData createFromParcel(@NonNull Parcel source) {
return new ComplicationData(source);
}
@NonNull
@Override
public ComplicationData[] newArray(int size) {
return new ComplicationData[size];
}
};
@ComplicationType
private final int mType;
private final Bundle mFields;
ComplicationData(@NonNull Builder builder) {
mType = builder.mType;
mFields = builder.mFields;
}
ComplicationData(int type, Bundle fields) {
mType = type;
mFields = fields;
mFields.setClassLoader(getClass().getClassLoader());
}
private ComplicationData(@NonNull Parcel in) {
mType = in.readInt();
mFields = in.readBundle(getClass().getClassLoader());
}
@RequiresApi(api = Build.VERSION_CODES.P)
private static class SerializedForm implements Serializable {
private static final int VERSION_NUMBER = 6;
@NonNull
ComplicationData mComplicationData;
SerializedForm() {
}
SerializedForm(@NonNull ComplicationData complicationData) {
mComplicationData = complicationData;
}
@SuppressLint("SyntheticAccessor") // For mComplicationData.mFields
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.writeInt(VERSION_NUMBER);
int type = mComplicationData.getType();
oos.writeInt(type);
if (isFieldValidForType(FIELD_LONG_TEXT, type)) {
oos.writeObject(mComplicationData.getLongText());
}
if (isFieldValidForType(FIELD_LONG_TITLE, type)) {
oos.writeObject(mComplicationData.getLongTitle());
}
if (isFieldValidForType(FIELD_SHORT_TEXT, type)) {
oos.writeObject(mComplicationData.getShortText());
}
if (isFieldValidForType(FIELD_SHORT_TITLE, type)) {
oos.writeObject(mComplicationData.getShortTitle());
}
if (isFieldValidForType(FIELD_CONTENT_DESCRIPTION, type)) {
oos.writeObject(mComplicationData.getContentDescription());
}
if (isFieldValidForType(FIELD_ICON, type)) {
oos.writeObject(IconSerializableHelper.create(mComplicationData.getIcon()));
}
if (isFieldValidForType(FIELD_ICON_BURN_IN_PROTECTION, type)) {
oos.writeObject(
IconSerializableHelper.create(mComplicationData.getBurnInProtectionIcon()));
}
if (isFieldValidForType(FIELD_SMALL_IMAGE, type)) {
oos.writeObject(IconSerializableHelper.create(mComplicationData.getSmallImage()));
}
if (isFieldValidForType(FIELD_SMALL_IMAGE_BURN_IN_PROTECTION, type)) {
oos.writeObject(IconSerializableHelper.create(
mComplicationData.getBurnInProtectionSmallImage()));
}
if (isFieldValidForType(FIELD_IMAGE_STYLE, type)) {
oos.writeInt(mComplicationData.getSmallImageStyle());
}
if (isFieldValidForType(FIELD_LARGE_IMAGE, type)) {
oos.writeObject(IconSerializableHelper.create(mComplicationData.getLargeImage()));
}
if (isFieldValidForType(FIELD_VALUE, type)) {
oos.writeFloat(mComplicationData.getRangedValue());
}
if (isFieldValidForType(FIELD_MIN_VALUE, type)) {
oos.writeFloat(mComplicationData.getRangedMinValue());
}
if (isFieldValidForType(FIELD_MAX_VALUE, type)) {
oos.writeFloat(mComplicationData.getRangedMaxValue());
}
if (isFieldValidForType(FIELD_START_TIME, type)) {
oos.writeLong(mComplicationData.getStartDateTimeMillis());
}
if (isFieldValidForType(FIELD_END_TIME, type)) {
oos.writeLong(mComplicationData.getEndDateTimeMillis());
}
if (isFieldValidForType(FIELD_DATA_SOURCE, type)) {
ComponentName componentName = mComplicationData.getDataSource();
if (componentName == null) {
oos.writeUTF("");
} else {
oos.writeUTF(componentName.flattenToString());
}
}
// TapAction unfortunately can't be serialized, instead we record if we've lost it.
oos.writeBoolean(mComplicationData.hasTapAction()
|| mComplicationData.getTapActionLostDueToSerialization());
long start = mComplicationData.mFields.getLong(FIELD_TIMELINE_START_TIME, -1);
oos.writeLong(start);
long end = mComplicationData.mFields.getLong(FIELD_TIMELINE_END_TIME, -1);
oos.writeLong(end);
if (isFieldValidForType(FIELD_PLACEHOLDER_FIELDS, type)) {
ComplicationData placeholder = mComplicationData.getPlaceholder();
if (placeholder == null) {
oos.writeBoolean(false);
} else {
oos.writeBoolean(true);
new SerializedForm(placeholder).writeObject(oos);
}
}
// This has to be last, since it's recursive.
List<ComplicationData> timeline = mComplicationData.getTimelineEntries();
int timelineLength = (timeline != null) ? timeline.size() : 0;
oos.writeInt(timelineLength);
if (timeline != null) {
for (ComplicationData data : timeline) {
new SerializedForm(data).writeObject(oos);
}
}
}
@SuppressLint("SyntheticAccessor") // For mComplicationData.mFields
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
int versionNumber = ois.readInt();
if (versionNumber != VERSION_NUMBER) {
// Give up if there's a version skew.
throw new IOException("Unsupported serialization version number " + versionNumber);
}
int type = ois.readInt();
Bundle fields = new Bundle();
if (isFieldValidForType(FIELD_LONG_TEXT, type)) {
fields.putParcelable(FIELD_LONG_TEXT, (ComplicationText) ois.readObject());
}
if (isFieldValidForType(FIELD_LONG_TITLE, type)) {
fields.putParcelable(FIELD_LONG_TITLE, (ComplicationText) ois.readObject());
}
if (isFieldValidForType(FIELD_SHORT_TEXT, type)) {
fields.putParcelable(FIELD_SHORT_TEXT, (ComplicationText) ois.readObject());
}
if (isFieldValidForType(FIELD_SHORT_TITLE, type)) {
fields.putParcelable(FIELD_SHORT_TITLE, (ComplicationText) ois.readObject());
}
if (isFieldValidForType(FIELD_CONTENT_DESCRIPTION, type)) {
fields.putParcelable(FIELD_CONTENT_DESCRIPTION,
(ComplicationText) ois.readObject());
}
if (isFieldValidForType(FIELD_ICON, type)) {
fields.putParcelable(FIELD_ICON, IconSerializableHelper.read(ois));
}
if (isFieldValidForType(FIELD_ICON_BURN_IN_PROTECTION, type)) {
fields.putParcelable(FIELD_ICON_BURN_IN_PROTECTION,
IconSerializableHelper.read(ois));
}
if (isFieldValidForType(FIELD_SMALL_IMAGE, type)) {
fields.putParcelable(FIELD_SMALL_IMAGE, IconSerializableHelper.read(ois));
}
if (isFieldValidForType(FIELD_SMALL_IMAGE_BURN_IN_PROTECTION, type)) {
fields.putParcelable(FIELD_SMALL_IMAGE_BURN_IN_PROTECTION,
IconSerializableHelper.read(ois));
}
if (isFieldValidForType(FIELD_IMAGE_STYLE, type)) {
fields.putInt(FIELD_IMAGE_STYLE, ois.readInt());
}
if (isFieldValidForType(FIELD_LARGE_IMAGE, type)) {
fields.putParcelable(FIELD_LARGE_IMAGE, IconSerializableHelper.read(ois));
}
if (isFieldValidForType(FIELD_VALUE, type)) {
fields.putFloat(FIELD_VALUE, ois.readFloat());
}
if (isFieldValidForType(FIELD_MIN_VALUE, type)) {
fields.putFloat(FIELD_MIN_VALUE, ois.readFloat());
}
if (isFieldValidForType(FIELD_MAX_VALUE, type)) {
fields.putFloat(FIELD_MAX_VALUE, ois.readFloat());
}
if (isFieldValidForType(FIELD_START_TIME, type)) {
fields.putLong(FIELD_START_TIME, ois.readLong());
}
if (isFieldValidForType(FIELD_END_TIME, type)) {
fields.putLong(FIELD_END_TIME, ois.readLong());
}
if (isFieldValidForType(FIELD_DATA_SOURCE, type)) {
String componentName = ois.readUTF();
if (componentName.isEmpty()) {
fields.remove(FIELD_DATA_SOURCE);
} else {
fields.putParcelable(
FIELD_DATA_SOURCE, ComponentName.unflattenFromString(componentName));
}
}
if (ois.readBoolean()) {
fields.putBoolean(FIELD_TAP_ACTION_LOST, true);
}
long start = ois.readLong();
if (start != -1) {
fields.putLong(FIELD_TIMELINE_START_TIME, start);
}
long end = ois.readLong();
if (end != -1) {
fields.putLong(FIELD_TIMELINE_END_TIME, end);
}
if (isFieldValidForType(FIELD_PLACEHOLDER_FIELDS, type)) {
if (ois.readBoolean()) {
SerializedForm serializedPlaceholder = new SerializedForm();
serializedPlaceholder.readObject(ois);
fields.putInt(FIELD_PLACEHOLDER_TYPE,
serializedPlaceholder.mComplicationData.mType);
fields.putBundle(FIELD_PLACEHOLDER_FIELDS,
serializedPlaceholder.mComplicationData.mFields);
}
}
int timelineLength = ois.readInt();
if (timelineLength != 0) {
Parcelable[] parcels = new Parcelable[timelineLength];
for (int i = 0; i < timelineLength; i++) {
SerializedForm entry = new SerializedForm();
entry.readObject(ois);
parcels[i] = entry.mComplicationData.mFields;
}
fields.putParcelableArray(FIELD_TIMELINE_ENTRIES, parcels);
}
mComplicationData = new ComplicationData(type, fields);
}
Object readResolve() {
return mComplicationData;
}
}
@RequiresApi(api = Build.VERSION_CODES.P)
Object writeReplace() {
return new SerializedForm(this);
}
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("Use SerializedForm");
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(mType);
dest.writeBundle(mFields);
}
/**
* Returns the type of this complication data.
*
* <p>Will be one of {@link #TYPE_SHORT_TEXT}, {@link #TYPE_LONG_TEXT}, {@link
* #TYPE_RANGED_VALUE}, {@link #TYPE_ICON}, {@link #TYPE_SMALL_IMAGE}, {@link
* #TYPE_LARGE_IMAGE}, {@link #TYPE_NOT_CONFIGURED}, {@link #TYPE_EMPTY}, {@link
* #TYPE_NO_PERMISSION}, or {@link #TYPE_NO_DATA}.
*/
@ComplicationType
public int getType() {
return mType;
}
/**
* Returns true if the complication is active and should be displayed at the given time. If this
* returns false, the complication should not be displayed.
*
* <p>This must be checked for any time for which the complication will be displayed.
*/
public boolean isActiveAt(long dateTimeMillis) {
return dateTimeMillis >= mFields.getLong(FIELD_START_TIME, 0)
&& dateTimeMillis <= mFields.getLong(FIELD_END_TIME, Long.MAX_VALUE);
}
/**
* TapAction unfortunately can't be serialized. Returns true if tapAction has been lost due to
* serialization (e.g. due to being read from the local cache). The next complication update
* from the system would replace this with one with a tapAction.
*/
public boolean getTapActionLostDueToSerialization() {
return mFields.getBoolean(FIELD_TAP_ACTION_LOST);
}
/**
* For timeline entries. Returns the epoch second at which this timeline entry becomes
* valid or `null` if it's not set.
*/
@Nullable
public Long getTimelineStartEpochSecond() {
long expiresAt = mFields.getLong(FIELD_TIMELINE_START_TIME, -1);
if (expiresAt == -1) {
return null;
} else {
return expiresAt;
}
}
/**
* For timeline entries. Sets the epoch second at which this timeline entry becomes invalid
* or clears the field if instant is `null`.
*/
public void setTimelineStartEpochSecond(@Nullable Long epochSecond) {
if (epochSecond == null) {
mFields.remove(FIELD_TIMELINE_START_TIME);
} else {
mFields.putLong(FIELD_TIMELINE_START_TIME, epochSecond);
}
}
/**
* For timeline entries. Returns the epoch second at which this timeline entry becomes invalid
* or `null` if it's not set.
*/
@Nullable
public Long getTimelineEndEpochSecond() {
long expiresAt = mFields.getLong(FIELD_TIMELINE_END_TIME, -1);
if (expiresAt == -1) {
return null;
} else {
return expiresAt;
}
}
/**
* For timeline entries. Sets the epoch second at which this timeline entry becomes invalid,
* or clears the field if instant is `null`.
*/
public void setTimelineEndEpochSecond(@Nullable Long epochSecond) {
if (epochSecond == null) {
mFields.remove(FIELD_TIMELINE_END_TIME);
} else {
mFields.putLong(FIELD_TIMELINE_END_TIME, epochSecond);
}
}
/** Returns the list of {@link ComplicationData} timeline entries. */
@Nullable
@SuppressWarnings("deprecation")
public List<ComplicationData> getTimelineEntries() {
Parcelable[] bundles = mFields.getParcelableArray(FIELD_TIMELINE_ENTRIES);
if (bundles == null) {
return null;
}
ArrayList<ComplicationData> entries = new ArrayList<>();
for (Parcelable parcelable : bundles) {
Bundle bundle = (Bundle) parcelable;
bundle.setClassLoader(getClass().getClassLoader());
// Use the serialized FIELD_TIMELINE_ENTRY_TYPE or the outer type if it's not there.
// Usually the timeline entry type will be the same as the outer type, unless an entry
// contains NoDataComplicationData.
int type = bundle.getInt(FIELD_TIMELINE_ENTRY_TYPE, mType);
entries.add(new ComplicationData(type, (Bundle) parcelable));
}
return entries;
}
/** Sets the list of {@link ComplicationData} timeline entries. */
public void setTimelineEntryCollection(@Nullable Collection<ComplicationData> timelineEntries) {
if (timelineEntries == null) {
mFields.remove(FIELD_TIMELINE_ENTRIES);
} else {
mFields.putParcelableArray(
FIELD_TIMELINE_ENTRIES,
timelineEntries.stream().map(
e -> {
// This supports timeline entry of NoDataComplicationData.
e.mFields.putInt(FIELD_TIMELINE_ENTRY_TYPE, e.mType);
return e.mFields;
}
).toArray(Parcelable[]::new));
}
}
/**
* Sets the {@link ComponentName} of the ComplicationDataSourceService that provided this
* ComplicationData.
*/
public void setDataSource(@Nullable ComponentName provider) {
mFields.putParcelable(FIELD_DATA_SOURCE, provider);
}
/**
* Gets the {@link ComponentName} of the ComplicationDataSourceService that provided this
* ComplicationData.
*/
@Nullable
@SuppressWarnings("deprecation") // The safer alternative is not available on Wear OS yet.
public ComponentName getDataSource() {
return (ComponentName) mFields.getParcelable(FIELD_DATA_SOURCE);
}
/**
* Returns true if the ComplicationData contains a ranged max value. I.e. if
* {@link #getRangedValue} can succeed.
*/
public boolean hasRangedValue() {
try {
return isFieldValidForType(FIELD_VALUE, mType);
} catch (BadParcelableException e) {
return false;
}
}
/**
* Returns the <i>value</i> field for this complication.
*
* <p>Valid only if the type of this complication data is {@link #TYPE_RANGED_VALUE}.
* Otherwise returns zero.
*/
public float getRangedValue() {
checkFieldValidForTypeWithoutThrowingException(FIELD_VALUE, mType);
return mFields.getFloat(FIELD_VALUE);
}
/**
* Returns true if the ComplicationData contains a ranged max value. I.e. if
* {@link #getRangedMinValue} can succeed.
*/
public boolean hasRangedMinValue() {
try {
return isFieldValidForType(FIELD_MIN_VALUE, mType);
} catch (BadParcelableException e) {
return false;
}
}
/**
* Returns the <i>min value</i> field for this complication.
*
* <p>Valid only if the type of this complication data is {@link #TYPE_RANGED_VALUE}.
* Otherwise returns zero.
*/
public float getRangedMinValue() {
checkFieldValidForTypeWithoutThrowingException(FIELD_MIN_VALUE, mType);
return mFields.getFloat(FIELD_MIN_VALUE);
}
/**
* Returns true if the ComplicationData contains a ranged max value. I.e. if
* {@link #getRangedMaxValue} can succeed.
*/
public boolean hasRangedMaxValue() {
try {
return isFieldValidForType(FIELD_MAX_VALUE, mType);
} catch (BadParcelableException e) {
return false;
}
}
/**
* Returns the <i>max value</i> field for this complication.
*
* <p>Valid only if the type of this complication data is {@link #TYPE_RANGED_VALUE}.
* Otherwise returns zero.
*/
public float getRangedMaxValue() {
checkFieldValidForTypeWithoutThrowingException(FIELD_MAX_VALUE, mType);
return mFields.getFloat(FIELD_MAX_VALUE);
}
/**
* Returns true if the ComplicationData contains a short title. I.e. if {@link #getShortTitle}
* can succeed.
*/
@SuppressWarnings("deprecation")
public boolean hasShortTitle() {
try {
return isFieldValidForType(FIELD_SHORT_TITLE, mType)
&& (mFields.getParcelable(FIELD_SHORT_TITLE) != null);
} catch (BadParcelableException e) {
return false;
}
}
/**
* Returns the <i>short title</i> field for this complication, or {@code null} if no value was
* provided for the field.
*
* <p>The value is provided as a {@link ComplicationText} object, from which the text to display
* can be obtained for a given point in time.
*
* <p>The length of the text, including any time-dependent values at any valid time, is expected
* to not exceed seven characters. When using this text, the watch face should be able to
* display any string of up to seven characters (reducing the text size appropriately if the
* string is very wide). Although not expected, it is possible that strings of more than seven
* characters might be seen, in which case they may be truncated.
*
* <p>Valid only if the type of this complication data is {@link #TYPE_SHORT_TEXT}, {@link
* #TYPE_RANGED_VALUE}, or {@link #TYPE_NO_PERMISSION}.
* Otherwise returns null.
*/
@Nullable
public ComplicationText getShortTitle() {
checkFieldValidForTypeWithoutThrowingException(FIELD_SHORT_TITLE, mType);
return getParcelableField(FIELD_SHORT_TITLE);
}
/**
* Returns true if the ComplicationData contains short text. I.e. if {@link #getShortText} can
* succeed.
*/
@SuppressWarnings("deprecation")
public boolean hasShortText() {
try {
return isFieldValidForType(FIELD_SHORT_TEXT, mType)
&& (mFields.getParcelable(FIELD_SHORT_TEXT) != null);
} catch (BadParcelableException e) {
return false;
}
}
/**
* Returns the <i>short text</i> field for this complication, or {@code null} if no value was
* provided for the field.
*
* <p>The value is provided as a {@link ComplicationText} object, from which the text to display
* can be obtained for a given point in time.
*
* <p>The length of the text, including any time-dependent values at any valid time, is expected
* to not exceed seven characters. When using this text, the watch face should be able to
* display any string of up to seven characters (reducing the text size appropriately if the
* string is very wide). Although not expected, it is possible that strings of more than seven
* characters might be seen, in which case they may be truncated.
*
* <p>Valid only if the type of this complication data is {@link #TYPE_SHORT_TEXT}, {@link
* #TYPE_RANGED_VALUE}, or {@link #TYPE_NO_PERMISSION}.
* Otherwise returns null.
*/
@Nullable
public ComplicationText getShortText() {
checkFieldValidForTypeWithoutThrowingException(FIELD_SHORT_TEXT, mType);
return getParcelableField(FIELD_SHORT_TEXT);
}
/**
* Returns true if the ComplicationData contains a long title. I.e. if {@link #getLongTitle}
* can succeed.
*/
@SuppressWarnings("deprecation")
public boolean hasLongTitle() {
try {
return isFieldValidForType(FIELD_LONG_TITLE, mType)
&& (mFields.getParcelable(FIELD_LONG_TITLE) != null);
} catch (BadParcelableException e) {
return false;
}
}
/**
* Returns the <i>long title</i> field for this complication, or {@code null} if no value was
* provided for the field.
*
* <p>The value is provided as a {@link ComplicationText} object, from which the text to display
* can be obtained for a given point in time.
*
* <p>Valid only if the type of this complication data is {@link #TYPE_LONG_TEXT}.
* Otherwise returns null.
*/
@Nullable
public ComplicationText getLongTitle() {
checkFieldValidForTypeWithoutThrowingException(FIELD_LONG_TITLE, mType);
return getParcelableField(FIELD_LONG_TITLE);
}
/**
* Returns true if the ComplicationData contains long text. I.e. if {@link #getLongText} can
* succeed.
*/
@SuppressWarnings("deprecation")
public boolean hasLongText() {
try {
return isFieldValidForType(FIELD_LONG_TEXT, mType)
&& (mFields.getParcelable(FIELD_LONG_TEXT) != null);
} catch (BadParcelableException e) {
return false;
}
}
/**
* Returns the <i>long text</i> field for this complication.
*
* <p>The value is provided as a {@link ComplicationText} object, from which the text to display
* can be obtained for a given point in time.
*
* <p>Valid only if the type of this complication data is {@link #TYPE_LONG_TEXT}.
* Otherwise returns null.
*/
@Nullable
public ComplicationText getLongText() {
checkFieldValidForTypeWithoutThrowingException(FIELD_LONG_TEXT, mType);
return getParcelableField(FIELD_LONG_TEXT);
}
/**
* Returns true if the ComplicationData contains an Icon. I.e. if {@link #getIcon} can succeed.
*/
@SuppressWarnings("deprecation")
public boolean hasIcon() {
try {
return isFieldValidForType(FIELD_ICON, mType)
&& (mFields.getParcelable(FIELD_ICON) != null);
} catch (BadParcelableException e) {
return false;
}
}
/**
* Returns the <i>icon</i> field for this complication, or {@code null} if no value was provided
* for the field. The image returned is expected to be single-color and so may be tinted to
* whatever color the watch face requires (but note that {@link Drawable#mutate()} should be
* called before drawables are tinted).
*
* <p>If the device is in ambient mode, and utilises burn-in protection, then the result of
* {@link #getBurnInProtectionIcon} must be used instead of this.
*
* <p>Valid for the types {@link #TYPE_SHORT_TEXT}, {@link #TYPE_LONG_TEXT}, {@link
* #TYPE_RANGED_VALUE}, {@link #TYPE_ICON}, or {@link #TYPE_NO_PERMISSION}.
* Otherwise returns null.
*/
@Nullable
public Icon getIcon() {
checkFieldValidForTypeWithoutThrowingException(FIELD_ICON, mType);
return getParcelableField(FIELD_ICON);
}
/**
* Returns true if the ComplicationData contains a burn in protection Icon. I.e. if
* {@link #getBurnInProtectionIcon} can succeed.
*/
@SuppressWarnings("deprecation")
public boolean hasBurnInProtectionIcon() {
try {
return isFieldValidForType(FIELD_ICON_BURN_IN_PROTECTION, mType)
&& (mFields.getParcelable(FIELD_ICON_BURN_IN_PROTECTION) != null);
} catch (BadParcelableException e) {
return false;
}
}
/**
* Returns the burn-in protection version of the <i>icon</i> field for this complication, or
* {@code null} if no such icon was provided. The image returned is expected to be an outline
* image suitable for use in ambient mode on screens with burn-in protection. The image is also
* expected to be single-color and so may be tinted to whatever color the watch face requires
* (but note that {@link Drawable#mutate()} should be called before drawables are tinted, and
* that the color used should be suitable for ambient mode with burn-in protection).
*
* <p>If the device is in ambient mode, and utilises burn-in protection, then the result of this
* method must be used instead of the result of {@link #getIcon}.
*
* <p>Valid for the types {@link #TYPE_SHORT_TEXT}, {@link #TYPE_LONG_TEXT}, {@link
* #TYPE_RANGED_VALUE}, {@link #TYPE_ICON}, or {@link #TYPE_NO_PERMISSION}.
* Otherwise returns null.
*/
@Nullable
public Icon getBurnInProtectionIcon() {
checkFieldValidForTypeWithoutThrowingException(FIELD_ICON_BURN_IN_PROTECTION, mType);
return getParcelableField(FIELD_ICON_BURN_IN_PROTECTION);
}
/**
* Returns true if the ComplicationData contains a small image. I.e. if {@link #getSmallImage}
* can succeed.
*/
@SuppressWarnings("deprecation")
public boolean hasSmallImage() {
try {
return isFieldValidForType(FIELD_SMALL_IMAGE, mType)
&& (mFields.getParcelable(FIELD_SMALL_IMAGE) != null);
} catch (BadParcelableException e) {
return false;
}
}
/**
* Returns the <i>small image</i> field for this complication, or {@code null} if no value was
* provided for the field.
*
* <p>This may be either a {@link #IMAGE_STYLE_PHOTO photo style} image, which is expected to
* fill the space available, or an {@link #IMAGE_STYLE_ICON icon style} image, which should be
* drawn entirely within the space available. Use {@link #getSmallImageStyle} to determine which
* of these applies.
*
* <p>As this may be any image, it is unlikely to be suitable for display in ambient mode when
* burn-in protection is enabled, or in low-bit ambient mode, and should not be rendered under
* these circumstances.
*
* <p>Valid for the types {@link #TYPE_LONG_TEXT} and {@link #TYPE_SMALL_IMAGE}.
* Otherwise returns null.
*/
@Nullable
public Icon getSmallImage() {
checkFieldValidForTypeWithoutThrowingException(FIELD_SMALL_IMAGE, mType);
return getParcelableField(FIELD_SMALL_IMAGE);
}
/**
* Returns true if the ComplicationData contains a burn in protection small image. I.e. if
* {@link #getBurnInProtectionSmallImage} can succeed.
*
* @throws IllegalStateException for invalid types
*/
@SuppressWarnings("deprecation")
public boolean hasBurnInProtectionSmallImage() {
try {
return isFieldValidForType(FIELD_SMALL_IMAGE_BURN_IN_PROTECTION, mType)
&& (mFields.getParcelable(FIELD_SMALL_IMAGE_BURN_IN_PROTECTION) != null);
} catch (BadParcelableException e) {
return false;
}
}
/**
* Returns the burn-in protection version of the <i>small image</i> field for this complication,
* or {@code null} if no such icon was provided. The image returned is expected to be an outline
* image suitable for use in ambient mode on screens with burn-in protection. The image is also
* expected to be single-color and so may be tinted to whatever color the watch face requires
* (but note that {@link Drawable#mutate()} should be called before drawables are tinted, and
* that the color used should be suitable for ambient mode with burn-in protection).
*
* <p>If the device is in ambient mode, and utilises burn-in protection, then the result of this
* method must be used instead of the result of {@link #getSmallImage()}.
*
* <p>Valid for the types {@link #TYPE_LONG_TEXT} and {@link #TYPE_SMALL_IMAGE}.
* Otherwise returns null.
*/
@Nullable
public Icon getBurnInProtectionSmallImage() {
checkFieldValidForTypeWithoutThrowingException(FIELD_SMALL_IMAGE_BURN_IN_PROTECTION, mType);
return getParcelableField(FIELD_SMALL_IMAGE_BURN_IN_PROTECTION);
}
/**
* Returns the <i>small image style</i> field for this complication.
*
* <p>The result of this method should be taken in to account when drawing a small image
* complication.
*
* <p>Valid only for types that contain small images, i.e. {@link #TYPE_SMALL_IMAGE} and {@link
* #TYPE_LONG_TEXT}.
* Otherwise returns zero.
*
* @see #IMAGE_STYLE_PHOTO which can be cropped but not recolored.
* @see #IMAGE_STYLE_ICON which can be recolored but not cropped.
*/
@ImageStyle
public int getSmallImageStyle() {
checkFieldValidForTypeWithoutThrowingException(FIELD_IMAGE_STYLE, mType);
return mFields.getInt(FIELD_IMAGE_STYLE);
}
/**
* Returns true if the ComplicationData contains a large image. I.e. if {@link #getLargeImage}
* can succeed.
*/
@SuppressWarnings("deprecation")
public boolean hasLargeImage() {
try {
return isFieldValidForType(FIELD_LARGE_IMAGE, mType)
&& (mFields.getParcelable(FIELD_LARGE_IMAGE) != null);
} catch (BadParcelableException e) {
return false;
}
}
/**
* Returns the <i>large image</i> field for this complication. This image is expected to be of a
* suitable size to fill the screen of the watch.
*
* <p>As this may be any image, it is unlikely to be suitable for display in ambient mode when
* burn-in protection is enabled, or in low-bit ambient mode, and should not be rendered under
* these circumstances.
*
* <p>Valid only if the type of this complication data is {@link #TYPE_LARGE_IMAGE}.
* Otherwise returns null.
*/
@Nullable
public Icon getLargeImage() {
checkFieldValidForTypeWithoutThrowingException(FIELD_LARGE_IMAGE, mType);
return getParcelableField(FIELD_LARGE_IMAGE);
}
/**
* Returns true if the ComplicationData contains a tap action. I.e. if {@link #getTapAction}
* can succeed.
*/
@SuppressWarnings("deprecation")
public boolean hasTapAction() {
try {
return isFieldValidForType(FIELD_TAP_ACTION, mType)
&& (mFields.getParcelable(FIELD_TAP_ACTION) != null);
} catch (BadParcelableException e) {
return false;
}
}
/**
* Returns the <i>tap action</i> field for this complication. The result is a {@link
* PendingIntent} that should be fired if the complication is tapped on, assuming the
* complication is tappable, or {@code null} if no tap action has been specified.
*
* <p>Valid for all non-empty types.
* Otherwise returns null.
*/
@Nullable
public PendingIntent getTapAction() {
checkFieldValidForTypeWithoutThrowingException(FIELD_TAP_ACTION, mType);
return getParcelableField(FIELD_TAP_ACTION);
}
/**
* Returns true if the ComplicationData contains a content description. I.e. if
* {@link #getContentDescription} can succeed.
*/
@SuppressWarnings("deprecation")
public boolean hasContentDescription() {
try {
return isFieldValidForType(FIELD_CONTENT_DESCRIPTION, mType)
&& (mFields.getParcelable(FIELD_CONTENT_DESCRIPTION) != null);
} catch (BadParcelableException e) {
return false;
}
}
/**
* Returns the <i>content description </i> field for this complication, for screen readers. This
* usually describes the image, but may also describe the overall complication.
*
* <p>Valid for all non-empty types.
*/
@Nullable
public ComplicationText getContentDescription() {
checkFieldValidForTypeWithoutThrowingException(FIELD_CONTENT_DESCRIPTION, mType);
return getParcelableField(FIELD_CONTENT_DESCRIPTION);
}
/**
* Returns the placeholder ComplicationData if there is one or `null`.
*/
@Nullable
public ComplicationData getPlaceholder() {
checkFieldValidForType(FIELD_PLACEHOLDER_FIELDS, mType);
checkFieldValidForType(FIELD_PLACEHOLDER_TYPE, mType);
if (!mFields.containsKey(FIELD_PLACEHOLDER_FIELDS)
|| !mFields.containsKey(FIELD_PLACEHOLDER_TYPE)) {
return null;
}
return new ComplicationData(mFields.getInt(FIELD_PLACEHOLDER_TYPE),
mFields.getBundle(FIELD_PLACEHOLDER_FIELDS));
}
/**
* Returns the start time for this complication data (i.e. the first time at which it should
* be considered active and displayed), this may be 0. See also {@link #isActiveAt(long)}.
*/
public long getStartDateTimeMillis() {
return mFields.getLong(FIELD_START_TIME, 0);
}
/**
* Returns the end time for this complication data (i.e. the last time at which it should be
* considered active and displayed), this may be {@link Long#MAX_VALUE}. See also {@link
* #isActiveAt(long)}.
*/
public long getEndDateTimeMillis() {
return mFields.getLong(FIELD_END_TIME, Long.MAX_VALUE);
}
/**
* Returns true if the complication data contains at least one text field with a value that may
* change based on the current time.
*/
public boolean isTimeDependent() {
return isTimeDependentField(FIELD_SHORT_TEXT)
|| isTimeDependentField(FIELD_SHORT_TITLE)
|| isTimeDependentField(FIELD_LONG_TEXT)
|| isTimeDependentField(FIELD_LONG_TITLE);
}
private boolean isTimeDependentField(String field) {
ComplicationText text = getParcelableField(field);
return text != null && text.isTimeDependent();
}
static boolean isFieldValidForType(String field, @ComplicationType int type) {
for (String requiredField : REQUIRED_FIELDS[type]) {
if (requiredField.equals(field)) {
return true;
}
}
for (String optionalField : OPTIONAL_FIELDS[type]) {
if (optionalField.equals(field)) {
return true;
}
}
return false;
}
private static boolean isTypeSupported(int type) {
return 1 <= type && type <= REQUIRED_FIELDS.length;
}
/**
* The unparceling logic needs to remain backward compatible.
*/
private static void checkFieldValidForTypeWithoutThrowingException(
String field, @ComplicationType int type) {
if (!isTypeSupported(type)) {
Log.w(TAG, "Type " + type + " can not be recognized");
return;
}
if (!isFieldValidForType(field, type)) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Field " + field + " is not supported for type " + type);
}
}
}
private static void checkFieldValidForType(String field, @ComplicationType int type) {
if (!isTypeSupported(type)) {
throw new IllegalStateException("Type " + type + " can not be recognized");
}
if (!isFieldValidForType(field, type)) {
throw new IllegalStateException(
"Field " + field + " is not supported for type " + type);
}
}
@SuppressWarnings({"TypeParameterUnusedInFormals", "deprecation"})
private <T extends Parcelable> T getParcelableField(String field) {
try {
return mFields.getParcelable(field);
} catch (BadParcelableException e) {
Log.w(
TAG,
"Could not unparcel ComplicationData. Provider apps must exclude wearable "
+ "support complication classes from proguard.",
e);
return null;
}
}
@NonNull
@Override
public String toString() {
return "ComplicationData{" + "mType=" + mType + ", mFields=" + mFields + '}';
}
/** Builder class for {@link ComplicationData}. */
public static final class Builder {
@ComplicationType
final int mType;
final Bundle mFields;
/** Creates a builder from given {@link ComplicationData}, copying its type and data. */
@SuppressLint("SyntheticAccessor")
public Builder(@NonNull ComplicationData data) {
mType = data.getType();
mFields = (Bundle) data.mFields.clone();
}
public Builder(@ComplicationType int type) {
mType = type;
mFields = new Bundle();
if (type == TYPE_SMALL_IMAGE || type == TYPE_LONG_TEXT) {
setSmallImageStyle(IMAGE_STYLE_PHOTO);
}
}
/**
* Sets the start time for this complication data. This is optional for any type.
*
* <p>The complication data will be considered inactive (i.e. should not be displayed) if
* the current time is less than the start time. If not specified, the data is considered
* active for all time up to the end time (or always active if end time is also not
* specified).
*
* <p>Returns this Builder to allow chaining.
*/
@NonNull
public Builder setStartDateTimeMillis(long startDateTimeMillis) {
mFields.putLong(FIELD_START_TIME, startDateTimeMillis);
return this;
}
/**
* Removes the start time for this complication data.
*
* <p>Returns this Builder to allow chaining.
*/
@NonNull
public Builder clearStartDateTime() {
mFields.remove(FIELD_START_TIME);
return this;
}
/**
* Sets the end time for this complication data. This is optional for any type.
*
* <p>The complication data will be considered inactive (i.e. should not be displayed) if
* the current time is greater than the end time. If not specified, the data is considered
* active for all time after the start time (or always active if start time is also not
* specified).
*
* <p>Returns this Builder to allow chaining.
*/
@NonNull
public Builder setEndDateTimeMillis(long endDateTimeMillis) {
mFields.putLong(FIELD_END_TIME, endDateTimeMillis);
return this;
}
/**
* Removes the end time for this complication data.
*
* <p>Returns this Builder to allow chaining.
*/
@NonNull
public Builder clearEndDateTime() {
mFields.remove(FIELD_END_TIME);
return this;
}
/**
* Sets the <i>value</i> field. This is required for the {@link #TYPE_RANGED_VALUE} type,
* and is not valid for any other type. A {@link #TYPE_RANGED_VALUE} complication
* visually presents a single value, which is usually a percentage. E.g. you
* have completed 70% of today's target of 10000 steps.
*
* <p>Returns this Builder to allow chaining.
*
* @throws IllegalStateException if this field is not valid for the complication type
*/
@NonNull
public Builder setRangedValue(float value) {
putFloatField(FIELD_VALUE, value);
return this;
}
/**
* Sets the <i>min value</i> field. This is required for the {@link #TYPE_RANGED_VALUE}
* type, and is not valid for any other type. A {@link #TYPE_RANGED_VALUE} complication
* visually presents a single value, which is usually a percentage. E.g. you have
* completed 70% of today's target of 10000 steps.
*
* <p>Returns this Builder to allow chaining.
*
* @throws IllegalStateException if this field is not valid for the complication type
*/
@NonNull
public Builder setRangedMinValue(float minValue) {
putFloatField(FIELD_MIN_VALUE, minValue);
return this;
}
/**
* Sets the <i>max value</i> field. This is required for the {@link #TYPE_RANGED_VALUE}
* type, and is not valid for any other type.A {@link #TYPE_RANGED_VALUE} complication
* visually presents a single value, which is usually a percentage. E.g. you have
* completed 70% of today's target of 10000 steps.
*
* <p>Returns this Builder to allow chaining.
*
* @throws IllegalStateException if this field is not valid for the complication type
*/
@NonNull
public Builder setRangedMaxValue(float maxValue) {
putFloatField(FIELD_MAX_VALUE, maxValue);
return this;
}
/**
* Sets the <i>long title</i> field. This is optional for the {@link #TYPE_LONG_TEXT} type,
* and is not valid for any other type.
*
* <p>The value must be provided as a {@link ComplicationText} object, so that
* time-dependent values may be included.
*
* <p>Returns this Builder to allow chaining.
*
* @throws IllegalStateException if this field is not valid for the complication type
*/
@NonNull
public Builder setLongTitle(@Nullable ComplicationText longTitle) {
putOrRemoveField(FIELD_LONG_TITLE, longTitle);
return this;
}
/**
* Sets the <i>long text</i> field. This is required for the {@link #TYPE_LONG_TEXT} type,
* and is not valid for any other type.
*
* <p>The value must be provided as a {@link ComplicationText} object, so that
* time-dependent values may be included.
*
* <p>Returns this Builder to allow chaining.
*
* @throws IllegalStateException if this field is not valid for the complication type
*/
@NonNull
public Builder setLongText(@Nullable ComplicationText longText) {
putOrRemoveField(FIELD_LONG_TEXT, longText);
return this;
}
/**
* Sets the <i>short title</i> field. This is valid for the {@link #TYPE_SHORT_TEXT}, {@link
* #TYPE_RANGED_VALUE}, and {@link #TYPE_NO_PERMISSION} types, and is not valid for any
* other type.
*
* <p>The value must be provided as a {@link ComplicationText} object, so that
* time-dependent values may be included.
*
* <p>The length of the text, including any time-dependent values, should not exceed seven
* characters. If it does, the text may be truncated by the watch face or might not fit in
* the complication.
*
* <p>Returns this Builder to allow chaining.
*
* @throws IllegalStateException if this field is not valid for the complication type
*/
@NonNull
public Builder setShortTitle(@Nullable ComplicationText shortTitle) {
putOrRemoveField(FIELD_SHORT_TITLE, shortTitle);
return this;
}
/**
* Sets the <i>short text</i> field. This is required for the {@link #TYPE_SHORT_TEXT} type,
* is optional for the {@link #TYPE_RANGED_VALUE} and {@link #TYPE_NO_PERMISSION} types, and
* is not valid for any other type.
*
* <p>The value must be provided as a {@link ComplicationText} object, so that
* time-dependent values may be included.
*
* <p>The length of the text, including any time-dependent values, should not exceed seven
* characters. If it does, the text may be truncated by the watch face or might not fit in
* the complication.
*
* <p>Returns this Builder to allow chaining.
*
* @throws IllegalStateException if this field is not valid for the complication type
*/
@NonNull
public Builder setShortText(@Nullable ComplicationText shortText) {
putOrRemoveField(FIELD_SHORT_TEXT, shortText);
return this;
}
/**
* Sets the <i>icon</i> field. This is required for the {@link #TYPE_ICON} type, and is
* optional for the {@link #TYPE_SHORT_TEXT}, {@link #TYPE_LONG_TEXT}, {@link
* #TYPE_RANGED_VALUE}, and {@link #TYPE_NO_PERMISSION} types.
*
* <p>The provided image must be single-color, so that watch faces can tint it as required.
*
* <p>If the icon provided here is not suitable for display in ambient mode with burn-in
* protection (e.g. if it includes solid blocks of pixels), then a burn-in safe version of
* the icon must be provided via {@link #setBurnInProtectionIcon}.
*
* <p>Returns this Builder to allow chaining.
*
* @throws IllegalStateException if this field is not valid for the complication type
*/
@NonNull
public Builder setIcon(@Nullable Icon icon) {
putOrRemoveField(FIELD_ICON, icon);
return this;
}
/**
* Sets the burn-in protection version of the <i>icon</i> field. This should be provided if
* the <i>icon</i> field is provided, unless the main icon is already safe for use with
* burn-in protection. This icon should have fewer lit pixels, and should use darker
* colors to prevent LCD burn in issues.
*
* <p>The provided image must be single-color, so that watch faces can tint it as required.
*
* <p>The provided image must not contain solid blocks of pixels - it should instead be
* composed of outlines or lines only.
*
* <p>If this field is set, the <i>icon</i> field must also be set.
*
* <p>Returns this Builder to allow chaining.
*
* @throws IllegalStateException if this field is not valid for the complication type
*/
@NonNull
public Builder setBurnInProtectionIcon(@Nullable Icon icon) {
putOrRemoveField(FIELD_ICON_BURN_IN_PROTECTION, icon);
return this;
}
/**
* Sets the <i>small image</i> field. This is required for the {@link #TYPE_SMALL_IMAGE}
* type, and is optional for the {@link #TYPE_LONG_TEXT} type.
*
* <p>Returns this Builder to allow chaining.
*
* @throws IllegalStateException if this field is not valid for the complication type
*/
@NonNull
public Builder setSmallImage(@Nullable Icon smallImage) {
putOrRemoveField(FIELD_SMALL_IMAGE, smallImage);
return this;
}
/**
* Sets the burn-in protection version of the <i>small image</i> field. This should be
* provided if the <i>small image</i> field is provided, unless the main small image is
* already safe for use with burn-in protection.
*
* <p>The provided image must not contain solid blocks of pixels - it should instead be
* composed of outlines or lines only.
*
* <p>If this field is set, the <i>small image</i> field must also be set.
*
* <p>Returns this Builder to allow chaining.
*
* @throws IllegalStateException if this field is not valid for the complication type
*/
@NonNull
public Builder setBurnInProtectionSmallImage(@Nullable Icon smallImage) {
putOrRemoveField(FIELD_SMALL_IMAGE_BURN_IN_PROTECTION, smallImage);
return this;
}
/**
* Sets the display style for this complication data. This is valid only for types that
* contain small images, i.e. {@link #TYPE_SMALL_IMAGE} and {@link #TYPE_LONG_TEXT}.
*
* <p>This affects how watch faces will draw the image in the complication.
*
* <p>If not specified, the default is {@link #IMAGE_STYLE_PHOTO}.
*
* @throws IllegalStateException if this field is not valid for the complication type
* @see #IMAGE_STYLE_PHOTO which can be cropped but not recolored.
* @see #IMAGE_STYLE_ICON which can be recolored but not cropped.
*/
@NonNull
public Builder setSmallImageStyle(@ImageStyle int imageStyle) {
putIntField(FIELD_IMAGE_STYLE, imageStyle);
return this;
}
/**
* Sets the <i>large image</i> field. This is required for the {@link #TYPE_LARGE_IMAGE}
* type, and is not valid for any other type.
*
* <p>The provided image should be suitably sized to fill the screen of the watch.
*
* <p>Returns this Builder to allow chaining.
*
* @throws IllegalStateException if this field is not valid for the complication type
*/
@NonNull
public Builder setLargeImage(@Nullable Icon largeImage) {
putOrRemoveField(FIELD_LARGE_IMAGE, largeImage);
return this;
}
/**
* Sets the <i>tap action</i> field. This is optional for any non-empty type.
*
* <p>The provided {@link PendingIntent} may be fired if the complication is tapped on. Note
* that some complications might not be tappable, in which case this field will be ignored.
*
* <p>Returns this Builder to allow chaining.
*/
@NonNull
public Builder setTapAction(@Nullable PendingIntent pendingIntent) {
putOrRemoveField(FIELD_TAP_ACTION, pendingIntent);
return this;
}
/**
* Sets the <i>content description</i> field for accessibility. This is optional for any
* non-empty type. It is recommended to provide a content description whenever the
* data includes an image.
*
* <p>The provided text will be read aloud by a Text-to-speech converter for users who may
* be vision-impaired. It will be read aloud in addition to any long, short, or range text
* in the complication.
*
* <p>If using to describe an image/icon that is purely stylistic and doesn't convey any
* information to the user, you may set the image content description to an empty string
* ("").
*
* <p>Returns this Builder to allow chaining.
*/
@NonNull
public Builder setContentDescription(@Nullable ComplicationText description) {
putOrRemoveField(FIELD_CONTENT_DESCRIPTION, description);
return this;
}
/**
* Sets whether or not this ComplicationData has been serialized.
*
* <p>Returns this Builder to allow chaining.
*/
@NonNull
public Builder setTapActionLostDueToSerialization(boolean tapActionLostDueToSerialization) {
if (tapActionLostDueToSerialization) {
mFields.putBoolean(FIELD_TAP_ACTION_LOST, tapActionLostDueToSerialization);
}
return this;
}
/**
* Sets the placeholder.
*
* <p>Returns this Builder to allow chaining.
*/
@SuppressLint("SyntheticAccessor")
@NonNull
public Builder setPlaceholder(@Nullable ComplicationData placeholder) {
if (placeholder == null) {
mFields.remove(FIELD_PLACEHOLDER_FIELDS);
mFields.remove(FIELD_PLACEHOLDER_TYPE);
} else {
ComplicationData.checkFieldValidForType(FIELD_PLACEHOLDER_FIELDS, mType);
mFields.putBundle(FIELD_PLACEHOLDER_FIELDS, placeholder.mFields);
putIntField(FIELD_PLACEHOLDER_TYPE, placeholder.mType);
}
return this;
}
/**
* Sets the {@link ComponentName} of the ComplicationDataSourceService that provided this
* ComplicationData. Generally this field should be set and is only nullable for backwards
* compatibility.
*
* <p>Returns this Builder to allow chaining.
*/
@NonNull
public Builder setDataSource(@Nullable ComponentName provider) {
putOrRemoveField(FIELD_DATA_SOURCE, provider);
return this;
}
/**
* Constructs and returns {@link ComplicationData} with the provided fields. All required
* fields must be populated before this method is called.
*
* @throws IllegalStateException if the required fields have not been populated
*/
@NonNull
@SuppressLint("SyntheticAccessor")
public ComplicationData build() {
// Validate.
for (String requiredField : REQUIRED_FIELDS[mType]) {
if (!mFields.containsKey(requiredField)) {
throw new IllegalStateException(
"Field " + requiredField + " is required for type " + mType);
}
if (mFields.containsKey(FIELD_ICON_BURN_IN_PROTECTION)
&& !mFields.containsKey(FIELD_ICON)) {
throw new IllegalStateException(
"Field ICON must be provided when field ICON_BURN_IN_PROTECTION is"
+ " provided.");
}
if (mFields.containsKey(FIELD_SMALL_IMAGE_BURN_IN_PROTECTION)
&& !mFields.containsKey(FIELD_SMALL_IMAGE)) {
throw new IllegalStateException(
"Field SMALL_IMAGE must be provided when field"
+ " SMALL_IMAGE_BURN_IN_PROTECTION is provided.");
}
}
return new ComplicationData(this);
}
@SuppressLint("SyntheticAccessor")
private void putIntField(@NonNull String field, int value) {
ComplicationData.checkFieldValidForType(field, mType);
mFields.putInt(field, value);
}
@SuppressLint("SyntheticAccessor")
private void putFloatField(@NonNull String field, float value) {
ComplicationData.checkFieldValidForType(field, mType);
mFields.putFloat(field, value);
}
/** Sets the field with obj or removes it if null. */
@SuppressLint("SyntheticAccessor")
private void putOrRemoveField(@NonNull String field, @Nullable Object obj) {
ComplicationData.checkFieldValidForType(field, mType);
if (obj == null) {
mFields.remove(field);
return;
}
if (obj instanceof String) {
mFields.putString(field, (String) obj);
} else if (obj instanceof Parcelable) {
mFields.putParcelable(field, (Parcelable) obj);
} else {
throw new IllegalArgumentException("Unexpected object type: " + obj.getClass());
}
}
}
}