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

Commit 66e28940 authored by Caitlin Shkuratov's avatar Caitlin Shkuratov
Browse files

DateTimeView: Add additional display configuration options.

Right now, DateTimeView has one display configuration option called
"showRelative":
 - showRelative=false: Times will be shown as absolute times, like
   "9:47" or "9/4/24"
 - showRelative=true: Times will be shown as relative from the current
   time. If the time is 7 minutes in the past, "7m" is used. If the time
   is 2 hours in the future, "in 2h" is used.

This CL adds additional dislpay configuration options, used only if
showRelative=true:

 - disambiguationText:
     If "future" is included, times in the future will have an
     additional "in " at the beginning of the string (e.g. "in 7m")

     If "past" is included, times in the past will have an additional "
     ago" at the end of the string (e.g. "7m ago")

 - relativeTimeUnitDisplayLength:
    If "shortest", use a single character to denote the unit. "m" for
    minutes, "h" for hours, etc. (This is how it works today.)

    If "medium", use 2-3 characters to denote the unit. "min" for
    minutes", "hr" for hours, etc.

    This is an enum so that we could easily add a "long" or "full" value
    if needed later.

Why this is needed: Notifications currently use DateTimeView to show the
time the notification was posted. In a future release, we're adding
chips in the status bar for ongoing notifications, and those chips also
need to show the time the notification was posted. There's two
important differences for the time in the status bar chip:

 1) The notifications that will get status bar chips will likely be for
    *upcoming* events, not past ones, so we expect most notification
    times to be in the future. Since there's so little space in the
    status bar, we want to use just "7m" instead of "in 7m" for those
    future times.

 2) Some status bar chips will be displaying *distance* information, not
    *time* information, like in navigation situations where a user needs
    to take a turn. Single-character time units can be confused with
    single-character distances units - specifically "m" could mean either
    minutes or meters. So, we'd like the time unit used in the status
    bar chip to be slightly longer.

Bug: 364653005
Test: Create a DateTimeView with disambiguationText=past and
relativeTimeUnitDisplayLength=medium, verify strings (see screenshots in
bug)
Test: atest DateTimeViewTest
Flag: android.view.flags.date_time_view_relative_time_display_configs

Change-Id: I8266d36b3b61d1f64ba854b6cd09c2e509179be1
parent 0825a072
Loading
Loading
Loading
Loading
+9 −1
Original line number Diff line number Diff line
@@ -141,3 +141,11 @@ flag {
    bug: "294922229"
    is_fixed_read_only: true
}

flag {
    name: "date_time_view_relative_time_display_configs"
    namespace: "systemui"
    description: "Enables DateTimeView to use additional display configurations for relative time"
    bug: "364653005"
    is_fixed_read_only: true
}
+198 −29
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.YEAR_IN_MILLIS;

import android.annotation.IntDef;
import android.app.ActivityThread;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.BroadcastReceiver;
@@ -41,6 +42,8 @@ import android.widget.RemoteViews.RemoteView;

import com.android.internal.R;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.text.DateFormat;
import java.time.Instant;
import java.time.LocalDate;
@@ -70,6 +73,23 @@ public class DateTimeView extends TextView {
    private static final int SHOW_TIME = 0;
    private static final int SHOW_MONTH_DAY_YEAR = 1;

    /** @hide */
    @IntDef(value = {UNIT_DISPLAY_LENGTH_SHORTEST, UNIT_DISPLAY_LENGTH_MEDIUM})
    @Retention(RetentionPolicy.SOURCE)
    public @interface UnitDisplayLength {}
    public static final int UNIT_DISPLAY_LENGTH_SHORTEST = 0;
    public static final int UNIT_DISPLAY_LENGTH_MEDIUM = 1;

    /** @hide */
    @IntDef(flag = true, value = {DISAMBIGUATION_TEXT_PAST, DISAMBIGUATION_TEXT_FUTURE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface DisambiguationTextMask {}
    public static final int DISAMBIGUATION_TEXT_PAST = 0x01;
    public static final int DISAMBIGUATION_TEXT_FUTURE = 0x02;

    private final boolean mCanUseRelativeTimeDisplayConfigs =
            android.view.flags.Flags.dateTimeViewRelativeTimeDisplayConfigs();

    private long mTimeMillis;
    // The LocalDateTime equivalent of mTimeMillis but truncated to minute, i.e. no seconds / nanos.
    private LocalDateTime mLocalTime;
@@ -81,6 +101,8 @@ public class DateTimeView extends TextView {
    private static final ThreadLocal<ReceiverInfo> sReceiverInfo = new ThreadLocal<ReceiverInfo>();
    private String mNowText;
    private boolean mShowRelativeTime;
    private int mRelativeTimeDisambiguationTextMask;
    private int mRelativeTimeUnitDisplayLength = UNIT_DISPLAY_LENGTH_SHORTEST;

    public DateTimeView(Context context) {
        this(context, null);
@@ -89,20 +111,23 @@ public class DateTimeView extends TextView {
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public DateTimeView(Context context, AttributeSet attrs) {
        super(context, attrs);
        final TypedArray a = context.obtainStyledAttributes(attrs,
                com.android.internal.R.styleable.DateTimeView, 0,
                0);

        final int N = a.getIndexCount();
        for (int i = 0; i < N; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case R.styleable.DateTimeView_showRelative:
                    boolean relative = a.getBoolean(i, false);
                    setShowRelativeTime(relative);
                    break;
            }
        final TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.DateTimeView, 0, 0);

        setShowRelativeTime(a.getBoolean(R.styleable.DateTimeView_showRelative, false));
        if (mCanUseRelativeTimeDisplayConfigs) {
            setRelativeTimeDisambiguationTextMask(
                    a.getInt(
                            R.styleable.DateTimeView_relativeTimeDisambiguationText,
                            // The original implementation showed disambiguation text for future
                            // times only, so continue with that default.
                            DISAMBIGUATION_TEXT_FUTURE));
            setRelativeTimeUnitDisplayLength(
                    a.getInt(
                            R.styleable.DateTimeView_relativeTimeUnitDisplayLength,
                            UNIT_DISPLAY_LENGTH_SHORTEST));
        }

        a.recycle();
    }

@@ -150,6 +175,29 @@ public class DateTimeView extends TextView {
        update();
    }

    /** See {@link R.styleable.DateTimeView_relativeTimeDisambiguationText}. */
    @android.view.RemotableViewMethod
    public void setRelativeTimeDisambiguationTextMask(
            @DisambiguationTextMask int disambiguationTextMask) {
        if (!mCanUseRelativeTimeDisplayConfigs) {
            return;
        }
        mRelativeTimeDisambiguationTextMask = disambiguationTextMask;
        updateNowText();
        update();
    }

    /** See {@link R.styleable.DateTimeView_relativeTimeUnitDisplayLength}. */
    @android.view.RemotableViewMethod
    public void setRelativeTimeUnitDisplayLength(@UnitDisplayLength int unitDisplayLength) {
        if (!mCanUseRelativeTimeDisplayConfigs) {
            return;
        }
        mRelativeTimeUnitDisplayLength = unitDisplayLength;
        updateNowText();
        update();
    }

    /**
     * Returns whether this view shows relative time
     *
@@ -264,17 +312,11 @@ public class DateTimeView extends TextView {
            return;
        } else if (duration < HOUR_IN_MILLIS) {
            count = (int)(duration / MINUTE_IN_MILLIS);
            result = getContext().getResources().getString(past
                    ? com.android.internal.R.string.duration_minutes_shortest
                    : com.android.internal.R.string.duration_minutes_shortest_future,
                    count);
            result = getContext().getResources().getString(getMinutesStringId(past), count);
            millisIncrease = MINUTE_IN_MILLIS;
        } else if (duration < DAY_IN_MILLIS) {
            count = (int)(duration / HOUR_IN_MILLIS);
            result = getContext().getResources().getString(past
                            ? com.android.internal.R.string.duration_hours_shortest
                            : com.android.internal.R.string.duration_hours_shortest_future,
                            count);
            result = getContext().getResources().getString(getHoursStringId(past), count);
            millisIncrease = HOUR_IN_MILLIS;
        } else if (duration < YEAR_IN_MILLIS) {
            // In weird cases it can become 0 because of daylight savings
@@ -283,10 +325,7 @@ public class DateTimeView extends TextView {
            LocalDateTime localNow = toLocalDateTime(now, zoneId);

            count = Math.max(Math.abs(dayDistance(localDateTime, localNow)), 1);
            result = getContext().getResources().getString(past
                    ? com.android.internal.R.string.duration_days_shortest
                    : com.android.internal.R.string.duration_days_shortest_future,
                    count);
            result = getContext().getResources().getString(getDaysStringId(past), count);
            if (past || count != 1) {
                mUpdateTimeMillis = computeNextMidnight(localNow, zoneId);
                millisIncrease = -1;
@@ -296,10 +335,7 @@ public class DateTimeView extends TextView {

        } else {
            count = (int)(duration / YEAR_IN_MILLIS);
            result = getContext().getResources().getString(past
                    ? com.android.internal.R.string.duration_years_shortest
                    : com.android.internal.R.string.duration_years_shortest_future,
                    count);
            result = getContext().getResources().getString(getYearsStringId(past), count);
            millisIncrease = YEAR_IN_MILLIS;
        }
        if (millisIncrease != -1) {
@@ -312,6 +348,139 @@ public class DateTimeView extends TextView {
        maybeSetText(result);
    }

    private int getMinutesStringId(boolean past) {
        if (!mCanUseRelativeTimeDisplayConfigs) {
            return past
                    ? com.android.internal.R.string.duration_minutes_shortest
                    : com.android.internal.R.string.duration_minutes_shortest_future;
        }

        if (mRelativeTimeUnitDisplayLength == UNIT_DISPLAY_LENGTH_SHORTEST) {
            if (past && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_PAST) != 0) {
                // "1m ago"
                return com.android.internal.R.string.duration_minutes_shortest_past;
            } else if (!past
                    && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_FUTURE) != 0) {
                // "in 1m"
                return com.android.internal.R.string.duration_minutes_shortest_future;
            } else {
                // "1m"
                return com.android.internal.R.string.duration_minutes_shortest;
            }
        } else { // UNIT_DISPLAY_LENGTH_MEDIUM
            if (past && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_PAST) != 0) {
                // "1min ago"
                return com.android.internal.R.string.duration_minutes_medium_past;
            } else if (!past
                    && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_FUTURE) != 0) {
                // "in 1min"
                return com.android.internal.R.string.duration_minutes_medium_future;
            } else {
                // "1min"
                return com.android.internal.R.string.duration_minutes_medium;
            }
        }
    }

    private int getHoursStringId(boolean past) {
        if (!mCanUseRelativeTimeDisplayConfigs) {
            return past
                    ? com.android.internal.R.string.duration_hours_shortest
                    : com.android.internal.R.string.duration_hours_shortest_future;
        }
        if (mRelativeTimeUnitDisplayLength == UNIT_DISPLAY_LENGTH_SHORTEST) {
            if (past && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_PAST) != 0) {
                // "1h ago"
                return com.android.internal.R.string.duration_hours_shortest_past;
            } else if (!past
                    && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_FUTURE) != 0) {
                // "in 1h"
                return com.android.internal.R.string.duration_hours_shortest_future;
            } else {
                // "1h"
                return com.android.internal.R.string.duration_hours_shortest;
            }
        } else { // UNIT_DISPLAY_LENGTH_MEDIUM
            if (past && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_PAST) != 0) {
                // "1hr ago"
                return com.android.internal.R.string.duration_hours_medium_past;
            } else if (!past
                    && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_FUTURE) != 0) {
                // "in 1hr"
                return com.android.internal.R.string.duration_hours_medium_future;
            } else {
                // "1hr"
                return com.android.internal.R.string.duration_hours_medium;
            }
        }
    }

    private int getDaysStringId(boolean past) {
        if (!mCanUseRelativeTimeDisplayConfigs) {
            return past
                    ? com.android.internal.R.string.duration_days_shortest
                    : com.android.internal.R.string.duration_days_shortest_future;
        }
        if (mRelativeTimeUnitDisplayLength == UNIT_DISPLAY_LENGTH_SHORTEST) {
            if (past && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_PAST) != 0) {
                // "1d ago"
                return com.android.internal.R.string.duration_days_shortest_past;
            } else if (!past
                    && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_FUTURE) != 0) {
                // "in 1d"
                return com.android.internal.R.string.duration_days_shortest_future;
            } else {
                // "1d"
                return com.android.internal.R.string.duration_days_shortest;
            }
        } else { // UNIT_DISPLAY_LENGTH_MEDIUM
            if (past && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_PAST) != 0) {
                // "1d ago"
                return com.android.internal.R.string.duration_days_medium_past;
            } else if (!past
                    && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_FUTURE) != 0) {
                // "in 1d"
                return com.android.internal.R.string.duration_days_medium_future;
            } else {
                // "1d"
                return com.android.internal.R.string.duration_days_medium;
            }
        }
    }

    private int getYearsStringId(boolean past) {
        if (!mCanUseRelativeTimeDisplayConfigs) {
            return past
                    ? com.android.internal.R.string.duration_years_shortest
                    : com.android.internal.R.string.duration_years_shortest_future;
        }
        if (mRelativeTimeUnitDisplayLength == UNIT_DISPLAY_LENGTH_SHORTEST) {
            if (past && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_PAST) != 0) {
                // "1y ago"
                return com.android.internal.R.string.duration_years_shortest_past;
            } else if (!past
                    && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_FUTURE) != 0) {
                // "in 1y"
                return com.android.internal.R.string.duration_years_shortest_future;
            } else {
                // "1y"
                return com.android.internal.R.string.duration_years_shortest;
            }
        } else { // UNIT_DISPLAY_LENGTH_MEDIUM
            if (past && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_PAST) != 0) {
                // "1y ago"
                return com.android.internal.R.string.duration_years_medium_past;
            } else if (!past
                    && (mRelativeTimeDisambiguationTextMask & DISAMBIGUATION_TEXT_FUTURE) != 0) {
                // "in 1y"
                return com.android.internal.R.string.duration_years_medium_future;
            } else {
                // "1y"
                return com.android.internal.R.string.duration_years_medium;
            }
        }
    }

    /**
     * Sets text only if the text has actually changed. This prevents needles relayouts of this
     * view when set to wrap_content.
+26 −0
Original line number Diff line number Diff line
@@ -10570,6 +10570,32 @@
    <declare-styleable name="DateTimeView">
        <attr name="showRelative" format="boolean" />
        <!-- For relative times, controls what kinds of times get disambiguation text.
             The default value is "future".
             Does nothing if showRelative=false.
             -->
        <attr name="relativeTimeDisambiguationText">
            <!-- Times in the past will have extra clarifying text indicating that the time is in
                 the past. For example, 1 minute ago is represented as "1m ago". If this flag is not
                 set, times in the past are represented as just "1m". -->
            <flag name="past" value="0x01" />
            <!-- Times in the future will have extra clarifying text indicating that the time is in
                 the future. For example, 1 minute in the future is represented as "in 1m". If this
                 flag is not set, times in the future are represented as just "1m". -->
            <flag name="future" value="0x02" />
        </attr>
        <!-- For relative times, sets the length of the time unit displayed (minutes, hours, etc.).
             Does nothing if showRelative=false.
             -->
        <attr name="relativeTimeUnitDisplayLength">
            <!-- The time unit will be shown as a short as possible (1 character if possible). -->
            <enum name="shortest" value="0" />
            <!-- The time unit will be shortened to a medium length (2-3 characters in general). -->
            <enum name="medium" value="1" />
        </attr>
    </declare-styleable>
    <declare-styleable name="ResolverDrawerLayout_LayoutParams">
+80 −0
Original line number Diff line number Diff line
@@ -3135,6 +3135,86 @@
        in <xliff:g id="count">%d</xliff:g>y
    </string>
    <!-- Phrase describing a time duration using minutes that is as short as possible, preferrably one character. This version should be a past point in time. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=14] -->
    <string name="duration_minutes_shortest_past">
        <xliff:g id="count">%d</xliff:g>m ago
    </string>
    <!-- Phrase describing a time duration using hours that is as short as possible, preferrably one character. This version should be a past point in time. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=14] -->
    <string name="duration_hours_shortest_past">
        <xliff:g id="count">%d</xliff:g>h ago
    </string>
    <!-- Phrase describing a time duration using days that is as short as possible, preferrably one character. This version should be a past point in time. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=14] -->
    <string name="duration_days_shortest_past">
        <xliff:g example="1" id="count">%d</xliff:g>d ago
    </string>
    <!-- Phrase describing a time duration using years that is as short as possible, preferrably one character. This version should be a past point in time. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=14] -->
    <string name="duration_years_shortest_past">
        <xliff:g id="count">%d</xliff:g>y ago
    </string>
    <!-- Phrase describing a time duration using minutes that is a medium length, preferrably two or three characters. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=8] -->
    <string name="duration_minutes_medium">
        <xliff:g id="count">%d</xliff:g>min
    </string>
    <!-- Phrase describing a time duration using hours that is a medium length, preferrably two or three characters. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=8] -->
    <string name="duration_hours_medium">
        <xliff:g id="count">%d</xliff:g>hr
    </string>
    <!-- Phrase describing a time duration using days that is a medium length, preferrably two or three characters. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=8] -->
    <string name="duration_days_medium">
       <xliff:g id="count">%d</xliff:g>d
    </string>
    <!-- Phrase describing a time duration using years that is a medium length, preferrably two or three characters. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=8] -->
    <string name="duration_years_medium">
        <xliff:g id="count">%d</xliff:g>yr
    </string>
    <!-- Phrase describing a time duration using minutes that is a medium length, preferrably two or three characters. This version should be a future point in time. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=18] -->
    <string name="duration_minutes_medium_future">
        in <xliff:g id="count">%d</xliff:g>min
    </string>
    <!-- Phrase describing a time duration using hours that is a medium length, preferrably two or three characters. This version should be a future point in time. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=18] -->
    <string name="duration_hours_medium_future">
        in <xliff:g id="count">%d</xliff:g>hr
    </string>
    <!-- Phrase describing a time duration using days that is a medium length, preferrably two or three characters. This version should be a future point in time. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=18] -->
    <string name="duration_days_medium_future">
        in <xliff:g example="1" id="count">%d</xliff:g>d
    </string>
    <!-- Phrase describing a time duration using years that is a medium length, preferrably two or three characters. This version should be a future point in time. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=18] -->
    <string name="duration_years_medium_future">
        in <xliff:g id="count">%d</xliff:g>yr
    </string>
    <!-- Phrase describing a time duration using minutes that is a medium length, preferrably two or three characters. This version should be a past point in time. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=18] -->
    <string name="duration_minutes_medium_past">
        <xliff:g id="count">%d</xliff:g>min ago
    </string>
    <!-- Phrase describing a time duration using hours that is a medium length, preferrably two or three characters. This version should be a past point in time. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=18] -->
    <string name="duration_hours_medium_past">
        <xliff:g id="count">%d</xliff:g>hr ago
    </string>
    <!-- Phrase describing a time duration using days that is a medium length, preferrably two or three characters. This version should be a past point in time. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=18] -->
    <string name="duration_days_medium_past">
        <xliff:g example="1" id="count">%d</xliff:g>d ago
    </string>
    <!-- Phrase describing a time duration using years that is a medium length, preferrably two or three characters. This version should be a past point in time. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=18] -->
    <string name="duration_years_medium_past">
        <xliff:g id="count">%d</xliff:g>yr ago
    </string>
    <!-- Phrase describing a relative time using minutes in the past that is not shown on the screen but used for accessibility. [CHAR LIMIT=NONE] -->
    <string name="duration_minutes_relative">{count, plural,
        =1 {# minute ago}
+17 −0
Original line number Diff line number Diff line
@@ -3359,6 +3359,23 @@
  <java-symbol type="string" name="duration_hours_shortest_future" />
  <java-symbol type="string" name="duration_days_shortest_future" />
  <java-symbol type="string" name="duration_years_shortest_future" />
  <java-symbol type="string" name="duration_minutes_shortest_past" />
  <java-symbol type="string" name="duration_hours_shortest_past" />
  <java-symbol type="string" name="duration_days_shortest_past" />
  <java-symbol type="string" name="duration_years_shortest_past" />

  <java-symbol type="string" name="duration_minutes_medium" />
  <java-symbol type="string" name="duration_hours_medium" />
  <java-symbol type="string" name="duration_days_medium" />
  <java-symbol type="string" name="duration_years_medium" />
  <java-symbol type="string" name="duration_minutes_medium_future" />
  <java-symbol type="string" name="duration_hours_medium_future" />
  <java-symbol type="string" name="duration_days_medium_future" />
  <java-symbol type="string" name="duration_years_medium_future" />
  <java-symbol type="string" name="duration_minutes_medium_past" />
  <java-symbol type="string" name="duration_hours_medium_past" />
  <java-symbol type="string" name="duration_days_medium_past" />
  <java-symbol type="string" name="duration_years_medium_past" />

  <java-symbol type="string" name="duration_minutes_relative" />
  <java-symbol type="string" name="duration_hours_relative" />
Loading