Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bbb4c36f authored by Matías Hernández's avatar Matías Hernández Committed by Ibrahim Yilmaz
Browse files

Support system-clock-based Chronometer (for Notification MetricStyle)

This also required adding support for Instant parameters in RemoteViews.

Fixes: 435150348
Test: atest ChronometerTest RemoteViewsTest RemoteViewsSerializersTest
Flag: android.app.api_metric_style
Change-Id: I3220022d44e03caaba1b4e1994a76a84da5c3dca
parent 4bbd5e07
Loading
Loading
Loading
Loading
+15 −18
Original line number Diff line number Diff line
@@ -11841,13 +11841,21 @@ public class Notification implements Parcelable
                            && timeDifference.getPausedDuration() == null) {
                        contentView.setViewVisibility(metricView.textValueId(), View.GONE);
                        contentView.setViewVisibility(metricView.chronometerId(), View.VISIBLE);
                        contentView.setBoolean(
                                metricView.chronometerId(), "setStarted", true);
                        contentView.setChronometerCountDown(
                                metricView.chronometerId(), timeDifference.mCountDown);
                        contentView.setLong(
                                metricView.chronometerId(),
                                "setBase", calculateBase(timeDifference));
                                metricView.chronometerId(), timeDifference.isTimer());
                        if (timeDifference.getZeroTime() != null) {
                            contentView.setChronometer(metricView.chronometerId(),
                                    timeDifference.getZeroTime(), /* format= */ null,
                                    /* started= */ true);
                        } else if (timeDifference.getZeroElapsedRealtime() != null) {
                            contentView.setChronometer(metricView.chronometerId(),
                                    timeDifference.getZeroElapsedRealtime(), /* format= */ null,
                                    /* started= */ true);
                        } else {
                            throw new IllegalStateException(
                                    "No zeroTime for running TimeDifference in " + metric);
                        }
                        // TODO(b/434910979): implement format support for Chronometer.
                    } else {
                        contentView.setViewVisibility(metricView.chronometerId(), View.GONE);
@@ -11860,17 +11868,6 @@ public class Notification implements Parcelable
            }
            return contentView;
        }
        // TODO(b/435150348): Add Instant support to Chronometer..
        private long calculateBase(@NonNull Metric.TimeDifference timeDifference) {
            if (timeDifference.mZeroTime != null) {
                return getElapsedRealtimeClock().getAsLong()
                        + (timeDifference.mZeroTime.toEpochMilli() - getSystemClock().millis());
            } else if (timeDifference.mZeroElapsedRealtime != null) {
                return timeDifference.mZeroElapsedRealtime;
            } else {
                throw new IllegalStateException("None of mZeroTime, mZeroElapsedRealtime set!");
            }
        }
        private record MetricView(int containerId,
                           int labelId,
+75 −9
Original line number Diff line number Diff line
@@ -16,6 +16,10 @@

package android.widget;

import static java.util.Objects.requireNonNull;

import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
@@ -25,6 +29,7 @@ import android.icu.util.Measure;
import android.icu.util.MeasureUnit;
import android.net.Uri;
import android.os.SystemClock;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.util.Log;
@@ -33,11 +38,15 @@ import android.view.inspector.InspectableProperty;
import android.widget.RemoteViews.RemoteView;

import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;

import java.time.Instant;
import java.time.InstantSource;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.IllegalFormatException;
import java.util.Locale;
import java.util.function.LongSupplier;

/**
 * Class that implements a simple timer.
@@ -72,7 +81,11 @@ public class Chronometer extends TextView {

    }

    private final LongSupplier mElapsedRealtimeClock;
    private final InstantSource mSystemClock;

    private long mBase;
    private Instant mBaseInstant;
    private long mNow; // the currently displayed time
    private boolean mVisible;
    private boolean mStarted;
@@ -112,7 +125,17 @@ public class Chronometer extends TextView {
    }

    public Chronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        this(context, SystemClock::elapsedRealtime, InstantSource.system(), attrs,
                defStyleAttr, defStyleRes);
    }

    /** @hide */
    @VisibleForTesting
    public Chronometer(Context context, LongSupplier elapsedRealtimeClock,
            InstantSource systemClock, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mElapsedRealtimeClock = requireNonNull(elapsedRealtimeClock);
        mSystemClock = requireNonNull(systemClock);

        final TypedArray a = context.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, defStyleRes);
@@ -126,7 +149,7 @@ public class Chronometer extends TextView {
    }

    private void init() {
        mBase = SystemClock.elapsedRealtime();
        mBase = mElapsedRealtimeClock.getAsLong();
        updateText(mBase);
    }

@@ -140,7 +163,7 @@ public class Chronometer extends TextView {
    @android.view.RemotableViewMethod
    public void setCountDown(boolean countDown) {
        mCountDown = countDown;
        updateText(SystemClock.elapsedRealtime());
        updateText(mElapsedRealtimeClock.getAsLong());
    }

    /**
@@ -170,15 +193,35 @@ public class Chronometer extends TextView {
    }

    /**
     * Set the time that the count-up timer is in reference to.
     *
     * @param base Use the {@link SystemClock#elapsedRealtime} time base.
     * Set the time that the count-up timer is in reference to (in the
     * {@link SystemClock#elapsedRealtime} time base).
     */
    @android.view.RemotableViewMethod
    public void setBase(long base) {
    public void setBase(@ElapsedRealtimeLong long base) {
        mBase = base;
        mBaseInstant = null;

        dispatchChronometerTick();
        updateText(SystemClock.elapsedRealtime());
        updateText(mElapsedRealtimeClock.getAsLong());
    }

    /**
     * Set the {@link Instant} that the count-up timer is in reference to.
     *
     * @hide
     */
    @android.view.RemotableViewMethod
    public void setBase(@NonNull Instant base) {
        mBaseInstant = requireNonNull(base);
        mBase = instantToElapsedRealtime(base);

        dispatchChronometerTick();
        updateText(mElapsedRealtimeClock.getAsLong());
    }

    private long instantToElapsedRealtime(Instant instant) {
        return mElapsedRealtimeClock.getAsLong()
                + (instant.toEpochMilli() - mSystemClock.millis());
    }

    /**
@@ -287,7 +330,14 @@ public class Chronometer extends TextView {
        updateRunning();
    }

    /** @hide */
    @VisibleForTesting
    public void updateText() {
        updateText(mElapsedRealtimeClock.getAsLong());
    }

    private synchronized void updateText(long now) {
        updateBaseTimeIfSystemClockChanged();
        mNow = now;
        long seconds = Math.round((mCountDown ? mBase - now - 499 : now - mBase) / 1000f);
        boolean negative = false;
@@ -321,11 +371,27 @@ public class Chronometer extends TextView {
        setText(text);
    }

    private static final long SIGNIFICANT_DRIFT_MILLIS = 500;

    private void updateBaseTimeIfSystemClockChanged() {
        if (mBaseInstant == null) {
            return;
        }
        long baseInstantToElapsedRealtime = instantToElapsedRealtime(mBaseInstant);
        long clockChange = Math.abs(mBase - baseInstantToElapsedRealtime);
        if (clockChange > SIGNIFICANT_DRIFT_MILLIS) {
            Log.d(TAG, TextUtils.formatSimple(
                    "Detected system clock change of %s millis; adjusting mBase (%s -> %s)",
                    clockChange, mBase, baseInstantToElapsedRealtime));
            mBase = baseInstantToElapsedRealtime;
        }
    }

    private void updateRunning() {
        boolean running = mVisible && mStarted && isShown();
        if (running != mRunning) {
            if (running) {
                updateText(SystemClock.elapsedRealtime());
                updateText(mElapsedRealtimeClock.getAsLong());
                dispatchChronometerTick();
                postTickOnNextSecond();
            } else {
@@ -339,7 +405,7 @@ public class Chronometer extends TextView {
        @Override
        public void run() {
            if (mRunning) {
                updateText(SystemClock.elapsedRealtime());
                updateText(mElapsedRealtimeClock.getAsLong());
                dispatchChronometerTick();
                postTickOnNextSecond();
            }
+80 −1
Original line number Diff line number Diff line
@@ -147,6 +147,7 @@ import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
@@ -2169,6 +2170,8 @@ public class RemoteViews implements Parcelable, Filter {
                return Icon.class;
            case BaseReflectionAction.BLEND_MODE:
                return BlendMode.class;
            case BaseReflectionAction.INSTANT:
                return Instant.class;
            default:
                return null;
        }
@@ -2733,6 +2736,7 @@ public class RemoteViews implements Parcelable, Filter {
        static final int COLOR_STATE_LIST = 15;
        static final int ICON = 16;
        static final int BLEND_MODE = 17;
        static final int INSTANT = 18;

        @UnsupportedAppUsage
        String mMethodName;
@@ -2960,6 +2964,13 @@ public class RemoteViews implements Parcelable, Filter {
                case BLEND_MODE:
                    this.mValue = BlendMode.fromValue(in.readInt());
                    break;
                case INSTANT:
                    if (in.readInt() == 1) {
                        mValue = Instant.ofEpochSecond(in.readLong(), in.readInt());
                    } else {
                        mValue = null;
                    }
                    break;
                default:
                    break;
            }
@@ -3013,6 +3024,15 @@ public class RemoteViews implements Parcelable, Filter {
                case ICON:
                    out.writeTypedObject((Parcelable) this.mValue, flags);
                    break;
                case INSTANT:
                    if (mValue != null) {
                        out.writeInt(1);
                        out.writeLong(((Instant) this.mValue).getEpochSecond());
                        out.writeInt(((Instant) this.mValue).getNano());
                    } else {
                        out.writeInt(0);
                    }
                    break;
                default:
                    break;
            }
@@ -3108,6 +3128,9 @@ public class RemoteViews implements Parcelable, Filter {
                        writeIconToProto(out, appResources, (Icon) this.mValue,
                                RemoteViewsProto.ReflectionAction.ICON_VALUE);
                        break;
                    case INSTANT:
                        writeInstantToProto(out, (Instant) this.mValue,
                                RemoteViewsProto.ReflectionAction.INSTANT_VALUE);
                    case BUNDLE:
                    case INTENT:
                    default:
@@ -3202,6 +3225,10 @@ public class RemoteViews implements Parcelable, Filter {
                                BlendMode.fromValue(in.readInt(
                                        RemoteViewsProto.ReflectionAction.BLEND_MODE_VALUE)));
                        break;
                    case (int) RemoteViewsProto.ReflectionAction.INSTANT_VALUE:
                        values.put(RemoteViewsProto.ReflectionAction.INSTANT_VALUE,
                                createInstantFromProto(in,
                                        RemoteViewsProto.ReflectionAction.INSTANT_VALUE));
                    default:
                        Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n"
                                + ProtoUtils.currentFieldToString(in));
@@ -3279,6 +3306,9 @@ public class RemoteViews implements Parcelable, Filter {
                                RemoteViewsProto.ReflectionAction.ICON_VALUE)).create(context,
                                resources, rootData, depth);
                        break;
                    case INSTANT:
                        value = (Instant) values.get(
                                RemoteViewsProto.ReflectionAction.INSTANT_VALUE);
                    case BUNDLE:
                    case INTENT:
                    default:
@@ -6880,7 +6910,7 @@ public class RemoteViews implements Parcelable, Filter {
    }

    /**
     * Equivalent to calling {@link Chronometer#setBase Chronometer.setBase},
     * Equivalent to calling {@link Chronometer#setBase(long)},
     * {@link Chronometer#setFormat Chronometer.setFormat},
     * and {@link Chronometer#start Chronometer.start()} or
     * {@link Chronometer#stop Chronometer.stop()}.
@@ -6901,6 +6931,29 @@ public class RemoteViews implements Parcelable, Filter {
        setBoolean(viewId, "setStarted", started);
    }

    /**
     * Equivalent to calling {@link Chronometer#setBase(Instant)},
     * {@link Chronometer#setFormat Chronometer.setFormat},
     * and {@link Chronometer#start Chronometer.start()} or
     * {@link Chronometer#stop Chronometer.stop()}.
     *
     * @param viewId The id of the {@link Chronometer} to change
     * @param base The instant at which the timer would have (or will) read 0:00.  This
     *             time should be based off of {@link java.time.InstantSource#system()}.
     * @param format The Chronometer format string, or null to
     *               simply display the timer value.
     * @param started True if you want the clock to be started, false if not.
     *
     * @see #setChronometerCountDown(int, boolean)
     *
     * @hide
     */
    public void setChronometer(@IdRes int viewId, Instant base, String format, boolean started) {
        setInstant(viewId, "setBase", base);
        setString(viewId, "setFormat", format);
        setBoolean(viewId, "setStarted", started);
    }

    /**
     * Equivalent to calling {@link Chronometer#setCountDown(boolean) Chronometer.setCountDown} on
     * the chronometer with the given viewId.
@@ -7891,6 +7944,19 @@ public class RemoteViews implements Parcelable, Filter {
        addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.BLEND_MODE, value));
    }

    /**
     * Call a method taking one {@link Instant} on a view in the layout for this RemoteViews.
     *
     * @param viewId The id of the view on which to call the method.
     * @param methodName The name of the method to call.
     * @param value The value to pass to the method.
     *
     * @hide
     */
    public void setInstant(@IdRes int viewId, String methodName, Instant value) {
        addAction(new ReflectionAction(viewId, methodName, BaseReflectionAction.INSTANT, value));
    }

    /**
     * Call a method taking one Bundle on a view in the layout for this RemoteViews.
     *
@@ -10678,4 +10744,17 @@ public class RemoteViews implements Parcelable, Filter {
        return cs;
    }

    private static void writeInstantToProto(ProtoOutputStream out, Instant instant, long fieldId) {
        long token = out.start(fieldId);
        RemoteViewsSerializers.writeInstantToProto(out, instant);
        out.end(token);
    }

    private static Instant createInstantFromProto(ProtoInputStream in, long fieldId)
            throws Exception {
        long token = in.start(fieldId);
        Instant instant = RemoteViewsSerializers.createInstantFromProto(in);
        in.end(token);
        return instant;
    }
}
+31 −0
Original line number Diff line number Diff line
@@ -15,6 +15,8 @@
 */
package android.widget;

import static android.util.proto.ProtoInputStream.NO_MORE_FIELDS;

import static com.android.text.flags.Flags.FLAG_NO_BREAK_NO_HYPHENATION_SPAN;
import static com.android.text.flags.Flags.noBreakNoHyphenationSpan;

@@ -75,6 +77,7 @@ import androidx.annotation.NonNull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
@@ -229,6 +232,34 @@ public class RemoteViewsSerializers {
        };
    }

    /** Write {@link Instant} to proto. */
    public static void writeInstantToProto(@NonNull ProtoOutputStream out,
            @NonNull Instant instant) {
        out.write(RemoteViewsProto.Instant.SECONDS, instant.getEpochSecond());
        out.write(RemoteViewsProto.Instant.NANOS, instant.getNano());
    }

    /** Create {@link Instant} from proto. */
    public static Instant createInstantFromProto(@NonNull ProtoInputStream in) throws IOException {
        long seconds = 0;
        int nanos = 0;
        while (in.nextField() != NO_MORE_FIELDS) {
            switch (in.getFieldNumber()) {
                case (int) RemoteViewsProto.Instant.SECONDS:
                    seconds = in.readLong(RemoteViewsProto.Instant.SECONDS);
                    break;
                case (int) RemoteViewsProto.Instant.NANOS:
                    nanos = in.readInt(RemoteViewsProto.Instant.NANOS);
                    break;
                default:
                    Log.w(TAG, "Unhandled field while reading Instant proto!\n"
                            + ProtoUtils.currentFieldToString(in));
            }
        }

        return Instant.ofEpochSecond(seconds, nanos);
    }

    public static void writeCharSequenceToProto(@NonNull ProtoOutputStream out,
            @NonNull CharSequence cs) {
        out.write(RemoteViewsProto.CharSequence.TEXT, cs.toString());
+15 −0
Original line number Diff line number Diff line
@@ -94,6 +94,20 @@ message RemoteViewsProto {
        };
    }

    /** A java.util.time.Instant. */
    message Instant {
      // Represents seconds of UTC time since Unix epoch
      // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
      // 9999-12-31T23:59:59Z inclusive.
      optional int64 seconds = 1;

      // Non-negative fractions of a second at nanosecond resolution. Negative
      // second values with fractions must still have non-negative nanos values
      // that count forward in time. Must be from 0 to 999,999,999
      // inclusive.
      optional int32 nanos = 2;
    }

    /**
     * Represents a CharSequence with Spans.
     */
@@ -386,6 +400,7 @@ message RemoteViewsProto {
            android.content.res.ColorStateListProto color_state_list_value = 16;
            Icon icon_value = 17;
            int32 blend_mode_value = 18;
            Instant instant_value = 19;
            // Intent and Bundle values are excluded.
        }
    }
Loading