diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index c78f4541384a0..f322eb7720db6 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -876,47 +876,53 @@ Overridden by %1$s - - About %1$s left - - About %1$s left based on your usage - - %1$s left until fully charged + + About %1$s left + + About %1$s left (%2$s) + + About %1$s left based on your usage + + About %1$s left based on your usage (%2$s) + + %1$s left - - %1$s left + + Will last until about about %1$s based on your usage (%2$s) + + Will last until about about %1$s based on your usage + + Will last until about about %1$s (%2$s) + + Will last until about about %1$s - - Less than %1$s remaining - - %1$s - Less than %2$s remaining + + Less than %1$s remaining + + Less than %1$s remaining (%2$s) - - %1$smore than %2$s remaining - - more than %1$s remaining + + More than %1$s remaining (%2$s) + + More than %1$s remaining - - phone may shutdown soon - - tablet may shutdown soon - - device may shutdown soon - - - %1$s - about %2$s left - - %1$s - about %2$s left based on your usage - - - %1$s - phone may shutdown soon - - %1$s - tablet may shutdown soon - - %1$s - device may shutdown soon + + Phone may shutdown soon + + Tablet may shutdown soon + + Device may shutdown soon + + Phone may shutdown soon (%1$s) + + Tablet may shutdown soon (%1$s) + + Device may shutdown soon (%1$s) %1$s - %2$s + + %1$s left until fully charged %1$s - %2$s until fully charged diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java b/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java index 346ca66bcb131..8b3da39440887 100644 --- a/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java +++ b/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java @@ -17,22 +17,30 @@ package com.android.settingslib.utils; import android.content.Context; +import android.icu.text.DateFormat; import android.icu.text.MeasureFormat; import android.icu.text.MeasureFormat.FormatWidth; import android.icu.util.Measure; import android.icu.util.MeasureUnit; import android.support.annotation.Nullable; import android.text.TextUtils; +import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.R; -import com.android.settingslib.utils.StringUtil; +import java.time.Clock; +import java.time.Instant; +import java.util.Calendar; +import java.util.Date; import java.util.Locale; import java.util.concurrent.TimeUnit; /** Utility class for keeping power related strings consistent**/ public class PowerUtil { + private static final long SEVEN_MINUTES_MILLIS = TimeUnit.MINUTES.toMillis(7); private static final long FIFTEEN_MINUTES_MILLIS = TimeUnit.MINUTES.toMillis(15); private static final long ONE_DAY_MILLIS = TimeUnit.DAYS.toMillis(1); + private static final long TWO_DAYS_MILLIS = TimeUnit.DAYS.toMillis(2); + private static final long ONE_HOUR_MILLIS = TimeUnit.HOURS.toMillis(1); /** * This method produces the text used in various places throughout the system to describe the @@ -57,11 +65,15 @@ public class PowerUtil { FIFTEEN_MINUTES_MILLIS, false /* withSeconds */); return getUnderFifteenString(context, timeString, percentageString); + } else if (drainTimeMs >= TWO_DAYS_MILLIS) { + // just say more than two day if over 48 hours + return getMoreThanTwoDaysString(context, percentageString); } else if (drainTimeMs >= ONE_DAY_MILLIS) { - // just say more than one day if over 24 hours - return getMoreThanOneDayString(context, percentageString); + // show remaining days & hours if more than a day + return getMoreThanOneDayString(context, drainTimeMs, + percentageString, basedOnUsage); } else { - // show a regular time remaining string + // show the time of day we think you'll run out return getRegularTimeRemainingString(context, drainTimeMs, percentageString, basedOnUsage); } @@ -83,34 +95,18 @@ public class PowerUtil { ? context.getString(R.string.power_remaining_less_than_duration_only, timeString) : context.getString( R.string.power_remaining_less_than_duration, - percentageString, - timeString); + timeString, + percentageString); } - private static String getMoreThanOneDayString(Context context, String percentageString) { - final Locale currentLocale = context.getResources().getConfiguration().getLocales().get(0); - final MeasureFormat frmt = MeasureFormat.getInstance(currentLocale, FormatWidth.SHORT); - - final Measure daysMeasure = new Measure(1, MeasureUnit.DAY); - - return TextUtils.isEmpty(percentageString) - ? context.getString(R.string.power_remaining_only_more_than_subtext, - frmt.formatMeasures(daysMeasure)) - : context.getString( - R.string.power_remaining_more_than_subtext, - percentageString, - frmt.formatMeasures(daysMeasure)); - } - - private static String getRegularTimeRemainingString(Context context, long drainTimeMs, + private static String getMoreThanOneDayString(Context context, long drainTimeMs, String percentageString, boolean basedOnUsage) { - // round to the nearest 15 min to not appear oversly precise - final long roundedTimeMs = roundToNearestThreshold(drainTimeMs, - FIFTEEN_MINUTES_MILLIS); + final long roundedTimeMs = roundToNearestThreshold(drainTimeMs, ONE_HOUR_MILLIS); CharSequence timeString = StringUtil.formatElapsedTime(context, roundedTimeMs, false /* withSeconds */); + if (TextUtils.isEmpty(percentageString)) { int id = basedOnUsage ? R.string.power_remaining_duration_only_enhanced @@ -120,7 +116,48 @@ public class PowerUtil { int id = basedOnUsage ? R.string.power_discharging_duration_enhanced : R.string.power_discharging_duration; - return context.getString(id, percentageString, timeString); + return context.getString(id, timeString, percentageString); + } + } + + private static String getMoreThanTwoDaysString(Context context, String percentageString) { + final Locale currentLocale = context.getResources().getConfiguration().getLocales().get(0); + final MeasureFormat frmt = MeasureFormat.getInstance(currentLocale, FormatWidth.SHORT); + + final Measure daysMeasure = new Measure(2, MeasureUnit.DAY); + + return TextUtils.isEmpty(percentageString) + ? context.getString(R.string.power_remaining_only_more_than_subtext, + frmt.formatMeasures(daysMeasure)) + : context.getString( + R.string.power_remaining_more_than_subtext, + frmt.formatMeasures(daysMeasure), + percentageString); + } + + private static String getRegularTimeRemainingString(Context context, long drainTimeMs, + String percentageString, boolean basedOnUsage) { + // Get the time of day we think device will die rounded to the nearest 15 min. + final long roundedTimeOfDayMs = + roundToNearestThreshold( + System.currentTimeMillis() + drainTimeMs, + FIFTEEN_MINUTES_MILLIS); + + // convert the time to a properly formatted string. + DateFormat fmt = DateFormat.getTimeInstance(DateFormat.SHORT); + Date date = Date.from(Instant.ofEpochMilli(roundedTimeOfDayMs)); + CharSequence timeString = fmt.format(date); + + if (TextUtils.isEmpty(percentageString)) { + int id = basedOnUsage + ? R.string.power_discharge_by_only_enhanced + : R.string.power_discharge_by_only; + return context.getString(id, timeString); + } else { + int id = basedOnUsage + ? R.string.power_discharge_by_enhanced + : R.string.power_discharge_by; + return context.getString(id, timeString, percentageString); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/StringUtil.java b/packages/SettingsLib/src/com/android/settingslib/utils/StringUtil.java index 45fdd78608366..68be2b4041b10 100644 --- a/packages/SettingsLib/src/com/android/settingslib/utils/StringUtil.java +++ b/packages/SettingsLib/src/com/android/settingslib/utils/StringUtil.java @@ -33,74 +33,74 @@ import java.util.Locale; /** Utility class for generally useful string methods **/ public class StringUtil { - public static final int SECONDS_PER_MINUTE = 60; - public static final int SECONDS_PER_HOUR = 60 * 60; - public static final int SECONDS_PER_DAY = 24 * 60 * 60; + public static final int SECONDS_PER_MINUTE = 60; + public static final int SECONDS_PER_HOUR = 60 * 60; + public static final int SECONDS_PER_DAY = 24 * 60 * 60; - /** - * Returns elapsed time for the given millis, in the following format: - * 2d 5h 40m 29s - * @param context the application context - * @param millis the elapsed time in milli seconds - * @param withSeconds include seconds? - * @return the formatted elapsed time - */ - public static CharSequence formatElapsedTime(Context context, double millis, - boolean withSeconds) { - SpannableStringBuilder sb = new SpannableStringBuilder(); - int seconds = (int) Math.floor(millis / 1000); - if (!withSeconds) { - // Round up. - seconds += 30; - } + /** + * Returns elapsed time for the given millis, in the following format: + * 2d 5h 40m 29s + * @param context the application context + * @param millis the elapsed time in milli seconds + * @param withSeconds include seconds? + * @return the formatted elapsed time + */ + public static CharSequence formatElapsedTime(Context context, double millis, + boolean withSeconds) { + SpannableStringBuilder sb = new SpannableStringBuilder(); + int seconds = (int) Math.floor(millis / 1000); + if (!withSeconds) { + // Round up. + seconds += 30; + } - int days = 0, hours = 0, minutes = 0; - if (seconds >= SECONDS_PER_DAY) { - days = seconds / SECONDS_PER_DAY; - seconds -= days * SECONDS_PER_DAY; - } - if (seconds >= SECONDS_PER_HOUR) { - hours = seconds / SECONDS_PER_HOUR; - seconds -= hours * SECONDS_PER_HOUR; - } - if (seconds >= SECONDS_PER_MINUTE) { - minutes = seconds / SECONDS_PER_MINUTE; - seconds -= minutes * SECONDS_PER_MINUTE; - } + int days = 0, hours = 0, minutes = 0; + if (seconds >= SECONDS_PER_DAY) { + days = seconds / SECONDS_PER_DAY; + seconds -= days * SECONDS_PER_DAY; + } + if (seconds >= SECONDS_PER_HOUR) { + hours = seconds / SECONDS_PER_HOUR; + seconds -= hours * SECONDS_PER_HOUR; + } + if (seconds >= SECONDS_PER_MINUTE) { + minutes = seconds / SECONDS_PER_MINUTE; + seconds -= minutes * SECONDS_PER_MINUTE; + } - final ArrayList measureList = new ArrayList(4); - if (days > 0) { - measureList.add(new Measure(days, MeasureUnit.DAY)); - } - if (hours > 0) { - measureList.add(new Measure(hours, MeasureUnit.HOUR)); - } - if (minutes > 0) { - measureList.add(new Measure(minutes, MeasureUnit.MINUTE)); - } - if (withSeconds && seconds > 0) { - measureList.add(new Measure(seconds, MeasureUnit.SECOND)); - } - if (measureList.size() == 0) { - // Everything addable was zero, so nothing was added. We add a zero. - measureList.add(new Measure(0, withSeconds ? MeasureUnit.SECOND : MeasureUnit.MINUTE)); - } - final Measure[] measureArray = measureList.toArray(new Measure[measureList.size()]); + final ArrayList measureList = new ArrayList(4); + if (days > 0) { + measureList.add(new Measure(days, MeasureUnit.DAY)); + } + if (hours > 0) { + measureList.add(new Measure(hours, MeasureUnit.HOUR)); + } + if (minutes > 0) { + measureList.add(new Measure(minutes, MeasureUnit.MINUTE)); + } + if (withSeconds && seconds > 0) { + measureList.add(new Measure(seconds, MeasureUnit.SECOND)); + } + if (measureList.size() == 0) { + // Everything addable was zero, so nothing was added. We add a zero. + measureList.add(new Measure(0, withSeconds ? MeasureUnit.SECOND : MeasureUnit.MINUTE)); + } + final Measure[] measureArray = measureList.toArray(new Measure[measureList.size()]); - final Locale locale = context.getResources().getConfiguration().locale; - final MeasureFormat measureFormat = MeasureFormat.getInstance( - locale, FormatWidth.NARROW); - sb.append(measureFormat.formatMeasures(measureArray)); + final Locale locale = context.getResources().getConfiguration().locale; + final MeasureFormat measureFormat = MeasureFormat.getInstance( + locale, FormatWidth.NARROW); + sb.append(measureFormat.formatMeasures(measureArray)); - if (measureArray.length == 1 && MeasureUnit.MINUTE.equals(measureArray[0].getUnit())) { - // Add ttsSpan if it only have minute value, because it will be read as "meters" - final TtsSpan ttsSpan = new TtsSpan.MeasureBuilder().setNumber(minutes) - .setUnit("minute").build(); - sb.setSpan(ttsSpan, 0, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } + if (measureArray.length == 1 && MeasureUnit.MINUTE.equals(measureArray[0].getUnit())) { + // Add ttsSpan if it only have minute value, because it will be read as "meters" + final TtsSpan ttsSpan = new TtsSpan.MeasureBuilder().setNumber(minutes) + .setUnit("minute").build(); + sb.setSpan(ttsSpan, 0, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } - return sb; - } + return sb; + } /** * Returns relative time for the given millis in the past, in a short format such as "2 days diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java index 9285148f7ae2a..c42ff083ff110 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java @@ -24,13 +24,18 @@ import android.content.Context; import com.android.settingslib.R; import com.android.settingslib.SettingsLibRobolectricTestRunner; +import java.time.Clock; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; import java.time.Duration; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowSettings.ShadowSystem; +import org.robolectric.shadows.ShadowSystemClock; @RunWith(SettingsLibRobolectricTestRunner.class) public class PowerUtilTest { @@ -39,8 +44,12 @@ public class PowerUtilTest { public static final long SEVENTEEN_MIN_MILLIS = Duration.ofMinutes(17).toMillis(); public static final long FIVE_MINUTES_MILLIS = Duration.ofMinutes(5).toMillis(); public static final long TEN_MINUTES_MILLIS = Duration.ofMinutes(10).toMillis(); - public static final long TWO_DAYS_MILLIS = Duration.ofDays(2).toMillis(); - public static final String ONE_DAY_FORMATTED = "1 day"; + public static final long THREE_DAYS_MILLIS = Duration.ofDays(3).toMillis(); + public static final long THIRTY_HOURS_MILLIS = Duration.ofHours(30).toMillis(); + public static final String TWO_DAYS_FORMATTED = "2 days"; + public static final String THIRTY_HOURS_FORMATTED = "1d 6h"; + public static final String NORMAL_CASE_EXPECTED_PREFIX = "Will last until about"; + public static final String ENHANCED_SUFFIX = "based on your usage"; private Context mContext; @@ -51,6 +60,7 @@ public class PowerUtilTest { } @Test + @Config(shadows = {ShadowSystemClock.class}) public void testGetBatteryRemainingStringFormatted_moreThanFifteenMinutes_withPercentage() { String info = PowerUtil.getBatteryRemainingStringFormatted(mContext, SEVENTEEN_MIN_MILLIS, @@ -62,15 +72,13 @@ public class PowerUtilTest { false /* basedOnUsage */); // We only add special mention for the long string - assertThat(info).isEqualTo(mContext.getString( - R.string.power_discharging_duration_enhanced, - TEST_BATTERY_LEVEL_10, - FIFTEEN_MIN_FORMATTED)); + assertThat(info).contains(NORMAL_CASE_EXPECTED_PREFIX); + assertThat(info).contains(ENHANCED_SUFFIX); + assertThat(info).contains("%"); // shortened string should not have extra text - assertThat(info2).isEqualTo(mContext.getString( - R.string.power_discharging_duration, - TEST_BATTERY_LEVEL_10, - FIFTEEN_MIN_FORMATTED)); + assertThat(info2).contains(NORMAL_CASE_EXPECTED_PREFIX); + assertThat(info2).doesNotContain(ENHANCED_SUFFIX); + assertThat(info2).contains("%"); } @Test @@ -84,14 +92,14 @@ public class PowerUtilTest { null /* percentageString */, false /* basedOnUsage */); - // We only add special mention for the long string - assertThat(info).isEqualTo(mContext.getString( - R.string.power_remaining_duration_only_enhanced, - FIFTEEN_MIN_FORMATTED)); + // We only have % when it is provided + assertThat(info).contains(NORMAL_CASE_EXPECTED_PREFIX); + assertThat(info).contains(ENHANCED_SUFFIX); + assertThat(info).doesNotContain("%"); // shortened string should not have extra text - assertThat(info2).isEqualTo(mContext.getString( - R.string.power_remaining_duration_only, - FIFTEEN_MIN_FORMATTED)); + assertThat(info2).contains(NORMAL_CASE_EXPECTED_PREFIX); + assertThat(info2).doesNotContain(ENHANCED_SUFFIX); + assertThat(info2).doesNotContain("%"); } @@ -107,12 +115,9 @@ public class PowerUtilTest { true /* basedOnUsage */); // additional battery percentage in this string - assertThat(info).isEqualTo(mContext.getString( - R.string.power_remaining_duration_shutdown_imminent, - TEST_BATTERY_LEVEL_10)); + assertThat(info).isEqualTo("Phone may shutdown soon (10%)"); // shortened string should not have percentage - assertThat(info2).isEqualTo(mContext.getString( - R.string.power_remaining_duration_only_shutdown_imminent)); + assertThat(info2).isEqualTo("Phone may shutdown soon"); } @Test @@ -127,35 +132,42 @@ public class PowerUtilTest { true /* basedOnUsage */); // shortened string should not have percentage - assertThat(info).isEqualTo(mContext.getString( - R.string.power_remaining_less_than_duration_only, - FIFTEEN_MIN_FORMATTED)); + assertThat(info).isEqualTo("Less than 15m remaining"); // Add percentage to string when provided - assertThat(info2).isEqualTo(mContext.getString( - R.string.power_remaining_less_than_duration, - TEST_BATTERY_LEVEL_10, - FIFTEEN_MIN_FORMATTED)); + assertThat(info2).isEqualTo("Less than 15m remaining (10%)"); } @Test - public void testGetBatteryRemainingStringFormatted_moreThanOneDay_usesCorrectString() { + public void testGetBatteryRemainingStringFormatted_betweenOneAndTwoDays_usesCorrectString() { String info = PowerUtil.getBatteryRemainingStringFormatted(mContext, - TWO_DAYS_MILLIS, + THIRTY_HOURS_MILLIS, null /* percentageString */, true /* basedOnUsage */); String info2 = PowerUtil.getBatteryRemainingStringFormatted(mContext, - TWO_DAYS_MILLIS, + THIRTY_HOURS_MILLIS, + TEST_BATTERY_LEVEL_10 /* percentageString */, + false /* basedOnUsage */); + + // We only add special mention for the long string + assertThat(info).isEqualTo("About 1d 6h left based on your usage"); + // shortened string should not have extra text + assertThat(info2).isEqualTo("About 1d 6h left (10%)"); + } + + @Test + public void testGetBatteryRemainingStringFormatted_moreThanTwoDays_usesCorrectString() { + String info = PowerUtil.getBatteryRemainingStringFormatted(mContext, + THREE_DAYS_MILLIS, + null /* percentageString */, + true /* basedOnUsage */); + String info2 = PowerUtil.getBatteryRemainingStringFormatted(mContext, + THREE_DAYS_MILLIS, TEST_BATTERY_LEVEL_10 /* percentageString */, true /* basedOnUsage */); // shortened string should not have percentage - assertThat(info).isEqualTo(mContext.getString( - R.string.power_remaining_only_more_than_subtext, - ONE_DAY_FORMATTED)); + assertThat(info).isEqualTo("More than 2 days remaining"); // Add percentage to string when provided - assertThat(info2).isEqualTo(mContext.getString( - R.string.power_remaining_more_than_subtext, - TEST_BATTERY_LEVEL_10, - ONE_DAY_FORMATTED)); + assertThat(info2).isEqualTo("More than 2 days remaining (10%)"); } }