public final class

Status

extends java.lang.Object

implements TimeDependentText

 java.lang.Object

↳androidx.wear.ongoing.Status

Gradle dependencies

compile group: 'androidx.wear', name: 'wear-ongoing', version: '1.1.0-alpha01'

  • groupId: androidx.wear
  • artifactId: wear-ongoing
  • version: 1.1.0-alpha01

Artifact androidx.wear:wear-ongoing:1.1.0-alpha01 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 StatusforPart(Status.Part part)

Convenience method for creating a Status with no template and a single Part.

public longgetNextChangeTimeMillis(long fromTimeMillis)

Returns the next time this status could have a different rendering.

public Status.PartgetPart(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.CharSequencegetText(Context context, long timeNowMillis)

Returns a textual representation of this status at the given time.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Methods

public static Status forPart(Status.Part part)

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.

public Status.Part getPart(java.lang.String name)

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;
    }
}