java.lang.Object
↳androidx.wear.ongoing.Status
Gradle dependencies
compile group: 'androidx.wear', name: 'wear-ongoing', version: '1.0.0'
- groupId: androidx.wear
- artifactId: wear-ongoing
- version: 1.0.0
Artifact androidx.wear:wear-ongoing:1.0.0 it located at Google repository (https://maven.google.com/)
Overview
Base class to represent the status of an Ongoing Activity and render it.
A status is composed of Parts, and they are joined together with a template.
Note that for backwards compatibility reasons the code rendering this status message may not
have all of the [Part] classes that are available in later versions of the library.
Templates that do not have values for all of the named parts will not be used.
The template list will be iterated through looking for the first template with all matching named
parts available, this will be selected for rendering the status.
To provide for backwards compatibility, you should provide one (or more) fallback templates which
use status parts from earlier versions of the API. e.g. TextPart, TimerPart & StopwatchPart
The status and part classes here use timestamps for updating the displayed representation of the
status, in cases when this is needed (chronometers), as returned by
Summary
Methods |
---|
public static Status | forPart(Status.Part part)
Convenience method for creating a Status with no template and a single Part. |
public long | getNextChangeTimeMillis(long fromTimeMillis)
Returns the next time this status could have a different rendering. |
public Status.Part | getPart(java.lang.String name)
Returns the value of the part with the given name. |
public java.util.Set<java.lang.String> | getPartNames()
|
public java.util.List<java.lang.CharSequence> | getTemplates()
|
public java.lang.CharSequence | getText(Context context, long timeNowMillis)
Returns a textual representation of this status at the given time. |
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Methods
Convenience method for creating a Status with no template and a single Part.
Parameters:
part: The only Part that composes this status.
Returns:
A new Status with just one Part.
public java.util.List<java.lang.CharSequence>
getTemplates()
Returns:
the list of templates that this status has.
public java.util.Set<java.lang.String>
getPartNames()
Returns:
the names of the parts provide to this status.
Returns the value of the part with the given name.
Parameters:
name: the name to lookup.
Returns:
the part with the given name, can be null.
public java.lang.CharSequence
getText(Context context, long timeNowMillis)
Returns a textual representation of this status at the given time. The first template that
has all required information will be used, and each part will be used in their respective
placeholder/s.
Parameters:
context: may be used for internationalization. Only used while this method
executed.
timeNowMillis: the timestamp of the time we want to display, usually now, as
Returns:
the rendered text, for best compatibility, display using a TextView.
public long
getNextChangeTimeMillis(long fromTimeMillis)
Returns the next time this status could have a different rendering.
There is no guarantee that the rendering will change at the returned time (for example, if
some information in the status is not rendered).
Parameters:
fromTimeMillis: current time, usually now as returned by
. In most cases
getText and getNextChangeTimeMillis should be called
with the exact same timestamp, so changes are not missed.
Returns:
the next time (counting from fromTimeMillis) that this status may produce a
different result when calling getText().
Source
/*
* 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 androidx.wear.ongoing;
import android.content.Context;
import android.text.SpannableStringBuilder;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Base class to represent the status of an Ongoing Activity and render it.
* <p>
* A status is composed of Parts, and they are joined together with a template.
* <p>
* Note that for backwards compatibility reasons the code rendering this status message may not
* have all of the [Part] classes that are available in later versions of the library.
* Templates that do not have values for all of the named parts will not be used.
* The template list will be iterated through looking for the first template with all matching named
* parts available, this will be selected for rendering the status.
* <p>
* To provide for backwards compatibility, you should provide one (or more) fallback templates which
* use status parts from earlier versions of the API. e.g. TextPart, TimerPart & StopwatchPart
* <p>
* The status and part classes here use timestamps for updating the displayed representation of the
* status, in cases when this is needed (chronometers), as returned by
* {@link android.os.SystemClock#elapsedRealtime()}
*/
public final class Status implements TimeDependentText {
@NonNull
final List<CharSequence> mTemplates;
@NonNull
private final Map<String, StatusPart> mParts;
/**
* Abstract class to represent An Ongoing activity status or part of it.
* <p>
* Parts are used to create complex statuses, that may contain several timers, placeholders for
* text, etc. They may also be used to convey information to the system about this Ongoing
* Activity.
*/
public abstract static class Part implements TimeDependentText {
// Hide constructor.
Part() {
}
@Nullable
StatusPart toVersionedParcelable() {
return null;
}
@Nullable
static Part fromVersionedParcelable(@Nullable StatusPart vp) {
if (vp == null) {
return null;
}
if (vp instanceof TextStatusPart) {
return new TextPart((TextStatusPart) vp);
} else if (vp instanceof TimerStatusPart) {
TimerStatusPart tsp = (TimerStatusPart) vp;
return tsp.mCountDown ? new TimerPart(tsp) : new StopwatchPart(tsp);
} else {
return null;
}
}
}
/**
* An Ongoing activity status (or part of it) representing a plain, static text.
* <p>
* Available since wear-ongoing:1.0.0
*/
public static final class TextPart extends Part {
@NonNull
private final TextStatusPart mPart;
TextPart(@NonNull TextStatusPart part) {
mPart = part;
}
/**
* Create a Part representing a static text.
*/
public TextPart(@NonNull String str) {
mPart = new TextStatusPart(str);
}
@Override
@NonNull
StatusPart toVersionedParcelable() {
return mPart;
}
/**
* See {@link TimeDependentText#getText(Context, long)}
*/
@NonNull
@Override
public CharSequence getText(@NonNull Context context, long timeNowMillis) {
return mPart.getText(context, timeNowMillis);
}
/**
* See {@link TimeDependentText#getNextChangeTimeMillis(long)}
*/
@Override
public long getNextChangeTimeMillis(long fromTimeMillis) {
return mPart.getNextChangeTimeMillis(fromTimeMillis);
}
@Override
public int hashCode() {
return mPart.hashCode();
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof TextPart)) return false;
return mPart.equals(((TextPart) obj).mPart);
}
}
/**
* Base class for {@link TimerPart} and {@link StopwatchPart}, defines the getters but can't
* be created directly, create one of those instead.
*/
public abstract static class TimerOrStopwatchPart extends Part {
@NonNull
private final TimerStatusPart mPart;
TimerOrStopwatchPart(@NonNull TimerStatusPart part) {
mPart = part;
}
/**
* @return the time at which this Timer or Stopwatch will display 0, will usually be in the
* past for a stopwatch and in the future for timers.
*/
public long getTimeZeroMillis() {
return mPart.mTimeZeroMillis;
}
/**
* @return {@code false} if this is a stopwatch or {@code true} if this is a timer.
*/
public boolean isCountDown() {
return mPart.mCountDown;
}
/**
* Determines if this Timer or Stopwatch is paused. i.e. the display representation will
* not change over time.
*
* @return {@code true} if this is paused, {@code false} if it's running.
*/
public boolean isPaused() {
return mPart.isPaused();
}
/**
* @return the timestamp of the time when this was paused. Use
* {@link #isPaused()} to determine if this is paused or not.
*/
public long getPausedAtMillis() {
return mPart.mPausedAtMillis;
}
/**
* Determines if this has a total duration set.
*
* @return {@code true} if this the total duration was set, {@code false} if not.
*/
public boolean hasTotalDuration() {
return mPart.mTotalDurationMillis >= 0L;
}
/**
* @return the total duration of this timer/stopwatch, if set. Use
* {@link #hasTotalDuration()} to determine if this has a duration set.
*/
public long getTotalDurationMillis() {
return mPart.mTotalDurationMillis;
}
@Override
@NonNull
StatusPart toVersionedParcelable() {
return mPart;
}
@Override
public int hashCode() {
return mPart.hashCode();
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof TimerOrStopwatchPart)) return false;
return mPart.equals(((TimerOrStopwatchPart) obj).mPart);
}
/**
* See {@link TimeDependentText#getText(Context, long)}
*/
@NonNull
@Override
public CharSequence getText(@NonNull Context context, long timeNowMillis) {
return mPart.getText(context, timeNowMillis);
}
/**
* See {@link TimeDependentText#getNextChangeTimeMillis(long)}
*/
@Override
public long getNextChangeTimeMillis(long fromTimeMillis) {
return mPart.getNextChangeTimeMillis(fromTimeMillis);
}
}
/**
* An Ongoing activity status (or part of it) representing a timer.
* <p>
* Available since wear-ongoing:1.0.0
*/
public static final class TimerPart extends TimerOrStopwatchPart {
TimerPart(@NonNull TimerStatusPart part) {
super(part);
}
/**
* Create a Part representing a timer.
*
* @param timeZeroMillis timestamp of the time at the future in which this Timer
* should display 0.
* @param pausedAtMillis timestamp of the time when this timer was paused. Or
* {@code -1L} if this timer is running.
* @param totalDurationMillis total duration of this timer, useful to display as a
* progress bar or similar.
*/
public TimerPart(long timeZeroMillis, long pausedAtMillis,
long totalDurationMillis) {
super(new TimerStatusPart(
timeZeroMillis,
/* countDown = */ true,
pausedAtMillis,
totalDurationMillis
));
}
/**
* Create a Part representing a timer.
*
* @param timeZeroMillis timestamp of the time at the future in which this Timer
* should display 0.
* @param pausedAtMillis timestamp of the time when this timer was paused. Or
* {@code -1L} if this timer is running.
*/
public TimerPart(long timeZeroMillis, long pausedAtMillis) {
this(timeZeroMillis, pausedAtMillis, TimerStatusPart.LONG_DEFAULT);
}
/**
* Create a Part representing a timer.
*
* @param timeZeroMillis timestamp of the time at the future in which this Timer
* should display 0.
*/
public TimerPart(long timeZeroMillis) {
this(timeZeroMillis, TimerStatusPart.LONG_DEFAULT);
}
}
/**
* An Ongoing activity status (or part of it) representing a stopwatch
* <p>
* Available since wear-ongoing:1.0.0
*/
public static final class StopwatchPart extends TimerOrStopwatchPart {
StopwatchPart(@NonNull TimerStatusPart part) {
super(part);
}
/**
* Create a Part representing a stopwatch.
*
* @param timeZeroMillis timestamp of the time at which this stopwatch started
* running.
* @param pausedAtMillis timestamp of the time when this stopwatch was paused. Or
* {@code -1L} if this stopwatch is running.
* @param totalDurationMillis total duration of this stopwatch, useful to display as a
* progress bar or similar.
*/
public StopwatchPart(long timeZeroMillis, long pausedAtMillis, long totalDurationMillis) {
super(new TimerStatusPart(
timeZeroMillis,
/* countDown = */ false,
pausedAtMillis,
totalDurationMillis
));
}
/**
* Create a Part representing a stopwatch.
*
* @param timeZeroMillis timestamp of the time at which this stopwatch started
* running.
* @param pausedAtMillis timestamp of the time when this stopwatch was paused. Or
* {@code -1L} if this stopwatch is running.
*/
public StopwatchPart(long timeZeroMillis, long pausedAtMillis) {
this(timeZeroMillis, pausedAtMillis, TimerStatusPart.LONG_DEFAULT);
}
/**
* Create a Part representing a stopwatch.
*
* @param timeZeroMillis timestamp of the time at which this stopwatch started
* running.
*/
public StopwatchPart(long timeZeroMillis) {
this(timeZeroMillis, TimerStatusPart.LONG_DEFAULT);
}
}
// Name of the {@link StatusPart} created when using {@link OngoingActivityStatus.forPart()}
private static final String DEFAULT_STATUS_PART_NAME = "defaultStatusPartName";
// Basic constructor used by the Builder
@VisibleForTesting
Status(
@Nullable List<CharSequence> templates,
@NonNull Map<String, StatusPart> parts
) {
mTemplates = templates;
mParts = parts;
}
OngoingActivityStatus toVersionedParcelable() {
return new OngoingActivityStatus(mTemplates, mParts);
}
static Status fromVersionedParcelable(OngoingActivityStatus vp) {
return new Status(vp.mTemplates, vp.mParts);
}
/**
* Convenience method for creating a Status with no template and a single Part.
*
* @param part The only Part that composes this status.
* @return A new {@link Status} with just one Part.
*/
@NonNull
public static Status forPart(@NonNull Part part) {
// Create an OngoingActivityStatus using only this part and the default template.
return new Status.Builder().addPart(DEFAULT_STATUS_PART_NAME, part).build();
}
/**
* Helper to Build OngoingActivityStatus instances.
*
* Templates can be specified, to specify how to render the parts and any surrounding
* text/format.
* If no template is specified, a default template that concatenates all parts separated
* by space is used.
*/
public static final class Builder {
private List<CharSequence> mTemplates = new ArrayList<>();
private CharSequence mDefaultTemplate = "";
private Map<String, StatusPart> mParts = new HashMap<>();
public Builder() {
}
/**
* Add a template to use for this status. Placeholders can be defined with #name#
* To produce a '#', use '##' in the template.
* If multiple templates are specified, the first one (in the order they where added by
* calling this method) that has all required fields is used.
* If no template is specified, a default template that concatenates all parts separated
* by space is used.
*
* @param template the template to be added
* @return this builder, to chain calls.
*/
@NonNull
public Builder addTemplate(@NonNull CharSequence template) {
mTemplates.add(template);
return this;
}
/**
* Add a part to be inserted in the placeholders.
*
* @param name the name of this part. In the template, use this name surrounded by '#'
* to reference it, e.g. here "track" and in the template "#track#"
* @param part The part that will be rendered in the specified position/s in the template.
* @return this builder, to chain calls.
*/
@NonNull
@SuppressWarnings("MissingGetterMatchingBuilder")
// We don't want a getter getParts()
public Builder addPart(@NonNull String name, @NonNull Part part) {
mParts.put(name, part.toVersionedParcelable());
mDefaultTemplate += (mDefaultTemplate.length() > 0 ? " " : "") + "#" + name + "#";
return this;
}
/**
* Build an OngoingActivityStatus with the given parameters.
*
* @return the built OngoingActivityStatus
*/
@NonNull
public Status build() {
List<CharSequence> templates = mTemplates.isEmpty() ? Arrays.asList(mDefaultTemplate)
: mTemplates;
// Verify that the last template can be rendered by every SysUI.
// Verify that all templates have all required parts.
Map<String, CharSequence> base = new HashMap<>();
Map<String, CharSequence> all = new HashMap<>();
for (Map.Entry<String, StatusPart> me : mParts.entrySet()) {
if (me.getValue() instanceof TextStatusPart
|| me.getValue() instanceof TimerStatusPart) {
base.put(me.getKey(), "");
}
all.put(me.getKey(), "");
}
if (processTemplate(templates.get(templates.size() - 1), base) == null) {
throw new IllegalStateException("For backwards compatibility reasons the last "
+ "templateThe should only use TextStatusPart & TimerStatusPart");
}
for (CharSequence template : templates) {
if (processTemplate(template, all) == null) {
throw new IllegalStateException("The template \"" + template + "\" is missing"
+ " some parts for rendering.");
}
}
return new Status(templates, mParts);
}
}
/**
* @return the list of templates that this status has.
*/
@NonNull
public List<CharSequence> getTemplates() {
return mTemplates;
}
/**
* @return the names of the parts provide to this status.
*/
@NonNull
public Set<String> getPartNames() {
return Collections.unmodifiableSet(mParts.keySet());
}
/**
* Returns the value of the part with the given name.
*
* @param name the name to lookup.
* @return the part with the given name, can be null.
*/
@Nullable
public Part getPart(@NonNull String name) {
return Part.fromVersionedParcelable(mParts.get(name));
}
/**
* Process a template and replace placeholders with the provided values.
* Placeholders are named, delimited by '#'. For example: '#name#'
* To produce a '#' in the output, use '##' in the template.
*
* @param template The template to use as base.
* @param values The values to replace the placeholders in the template with.
* @return The template with the placeholders replaced, or null if the template references a
* value that it's not present (or null).
*/
@Nullable
static CharSequence processTemplate(@NonNull CharSequence template,
@NonNull Map<String, CharSequence> values) {
SpannableStringBuilder ssb = new SpannableStringBuilder(template);
int opening = -1;
for (int i = 0; i < ssb.length(); i++) {
if (ssb.charAt(i) == '#') {
if (opening >= 0) {
// Replace '##' with '#'
// Replace '#varName#' with the value from the map.
CharSequence replaceWith =
opening == i - 1 ? "#" :
values.get(ssb.subSequence(opening + 1, i).toString());
if (replaceWith == null) {
return null;
}
ssb.replace(opening, i + 1, replaceWith);
i = opening + replaceWith.length() - 1;
opening = -1;
} else {
opening = i;
}
}
}
return ssb;
}
/**
* Returns a textual representation of this status at the given time. The first template that
* has all required information will be used, and each part will be used in their respective
* placeholder/s.
*
* @param context may be used for internationalization. Only used while this method
* executed.
* @param timeNowMillis the timestamp of the time we want to display, usually now, as
* @return the rendered text, for best compatibility, display using a TextView.
*/
@NonNull
@Override
public CharSequence getText(@NonNull Context context, long timeNowMillis) {
Map<String, CharSequence> texts = new HashMap<>();
for (Map.Entry<String, StatusPart> me : mParts.entrySet()) {
CharSequence text = me.getValue().getText(context, timeNowMillis);
texts.put(me.getKey(), text);
}
for (CharSequence template : mTemplates) {
CharSequence ret = processTemplate(template, texts);
if (ret != null) {
return ret;
}
}
return "";
}
/**
* Returns the next time this status could have a different rendering.
* There is no guarantee that the rendering will change at the returned time (for example, if
* some information in the status is not rendered).
*
* @param fromTimeMillis current time, usually now as returned by
* {@link android.os.SystemClock#elapsedRealtime()}. In most cases
* {@code getText} and {@code getNextChangeTimeMillis} should be called
* with the exact same timestamp, so changes are not missed.
* @return the next time (counting from fromTimeMillis) that this status may produce a
* different result when calling getText().
*/
@Override
public long getNextChangeTimeMillis(long fromTimeMillis) {
long ret = Long.MAX_VALUE;
for (StatusPart part : mParts.values()) {
ret = Math.min(ret, part.getNextChangeTimeMillis(fromTimeMillis));
}
return ret;
}
}