Define Utils.formatRelativeTime() and use it
Previously, relative times were formatted using formatElapsedTime() (appending translations of "ago" to them), sometimes resulting in grammatically hard-to-understand or unnatural localizations. Now we use ICU's RelativeDateTimeFormatter, which uses grammatically correct and natural localizations from CLDR data. Bug: 64507689 Bug: 64605781 Bug: 64556849 Bug: 64550172 Test: make -j RunSettingsRoboTests Change-Id: Ia2d098b190ab99e7748ef6f03b919f5c6174ba7d
This commit is contained in:
@@ -3633,8 +3633,6 @@
|
|||||||
<string name="recent_app_category_title">Recently opened apps</string>
|
<string name="recent_app_category_title">Recently opened apps</string>
|
||||||
<!-- Preference title for showing all apps on device [CHAR_LIMIT=50]-->
|
<!-- Preference title for showing all apps on device [CHAR_LIMIT=50]-->
|
||||||
<string name="see_all_apps_title">See all <xliff:g id="count" example="3">%1$d</xliff:g> apps</string>
|
<string name="see_all_apps_title">See all <xliff:g id="count" example="3">%1$d</xliff:g> apps</string>
|
||||||
<!-- Preference summary for each recently used app, which is the time since last used, i.e. "7 h 20 min ago". Note: ^1 should be used in all translations [CHAR_LIMIT=60] -->
|
|
||||||
<string name="recent_app_summary"><xliff:g id="time">^1</xliff:g> ago</string>
|
|
||||||
|
|
||||||
<!-- Warning that appears below the unknown sources switch in settings -->
|
<!-- Warning that appears below the unknown sources switch in settings -->
|
||||||
<string name="install_all_warning" product="tablet">
|
<string name="install_all_warning" product="tablet">
|
||||||
@@ -4644,12 +4642,10 @@
|
|||||||
<!-- Title for the cellular network in power use UI(i.e. Mobile network scanning: 30% of battery usage) [CHAR_LIMIT=40] -->
|
<!-- Title for the cellular network in power use UI(i.e. Mobile network scanning: 30% of battery usage) [CHAR_LIMIT=40] -->
|
||||||
<string name="device_cellular_network">Mobile network scanning</string>
|
<string name="device_cellular_network">Mobile network scanning</string>
|
||||||
|
|
||||||
<!-- Label for time since last full charge in power use UI, i.e. "7 h 20 min ago". Note: ^1 should be used in all translations [CHAR_LIMIT=60] -->
|
|
||||||
<string name="power_last_full_charge_summary"><xliff:g id="time">^1</xliff:g> ago</string>
|
|
||||||
<!-- Label for list of apps using battery in power use UI. Note: ^1 should be used in all translations[CHAR_LIMIT=120] -->
|
<!-- Label for list of apps using battery in power use UI. Note: ^1 should be used in all translations[CHAR_LIMIT=120] -->
|
||||||
<string name="power_usage_list_summary">App usage since full charge (<xliff:g id="time">^1</xliff:g> ago)</string>
|
<string name="power_usage_list_summary">App usage since full charge (<xliff:g id="relative_time">^1</xliff:g>)</string>
|
||||||
<!-- Label for device components using battery in power use UI. Note: ^1 should be used in all translations[CHAR_LIMIT=120] -->
|
<!-- Label for device components using battery in power use UI. Note: ^1 should be used in all translations[CHAR_LIMIT=120] -->
|
||||||
<string name="power_usage_list_summary_device">Device usage since full charge (<xliff:g id="time">^1</xliff:g> ago)</string>
|
<string name="power_usage_list_summary_device">Device usage since full charge (<xliff:g id="relative_time">^1</xliff:g>)</string>
|
||||||
<!-- Description for the screen usage item [CHAR_LIMIT=120] -->
|
<!-- Description for the screen usage item [CHAR_LIMIT=120] -->
|
||||||
<string name="screen_usage_summary">Amount of time screen has been on since full charge</string>
|
<string name="screen_usage_summary">Amount of time screen has been on since full charge</string>
|
||||||
<!-- Label for list of different types using battery in power use UI [CHAR_LIMIT=60] -->
|
<!-- Label for list of different types using battery in power use UI [CHAR_LIMIT=60] -->
|
||||||
|
@@ -52,8 +52,11 @@ import android.graphics.Bitmap;
|
|||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
import android.hardware.fingerprint.FingerprintManager;
|
import android.hardware.fingerprint.FingerprintManager;
|
||||||
import android.icu.text.MeasureFormat;
|
import android.icu.text.MeasureFormat;
|
||||||
|
import android.icu.text.RelativeDateTimeFormatter;
|
||||||
|
import android.icu.text.RelativeDateTimeFormatter.RelativeUnit;
|
||||||
import android.icu.util.Measure;
|
import android.icu.util.Measure;
|
||||||
import android.icu.util.MeasureUnit;
|
import android.icu.util.MeasureUnit;
|
||||||
|
import android.icu.util.ULocale;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.net.LinkProperties;
|
import android.net.LinkProperties;
|
||||||
import android.net.Network;
|
import android.net.Network;
|
||||||
@@ -860,6 +863,48 @@ public final class Utils extends com.android.settingslib.Utils {
|
|||||||
return sb;
|
return sb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns relative time for the given millis in the past, in a short format such as "2 days
|
||||||
|
* ago", "5 hr. ago", "40 min. ago", or "29 sec. ago".
|
||||||
|
*
|
||||||
|
* <p>The unit is chosen to have good information value while only using one unit. So 27 hours
|
||||||
|
* and 50 minutes would be formatted as "28 hr. ago", while 50 hours would be formatted as
|
||||||
|
* "2 days ago".
|
||||||
|
*
|
||||||
|
* @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 formatRelativeTime(Context context, double millis,
|
||||||
|
boolean withSeconds) {
|
||||||
|
final int seconds = (int) Math.floor(millis / 1000);
|
||||||
|
final RelativeUnit unit;
|
||||||
|
final int value;
|
||||||
|
if (withSeconds && seconds < 2 * SECONDS_PER_MINUTE) {
|
||||||
|
unit = RelativeUnit.SECONDS;
|
||||||
|
value = seconds;
|
||||||
|
} else if (seconds < 2 * SECONDS_PER_HOUR) {
|
||||||
|
unit = RelativeUnit.MINUTES;
|
||||||
|
value = (seconds + SECONDS_PER_MINUTE / 2) / SECONDS_PER_MINUTE;
|
||||||
|
} else if (seconds < 2 * SECONDS_PER_DAY) {
|
||||||
|
unit = RelativeUnit.HOURS;
|
||||||
|
value = (seconds + SECONDS_PER_HOUR / 2) / SECONDS_PER_HOUR;
|
||||||
|
} else {
|
||||||
|
unit = RelativeUnit.DAYS;
|
||||||
|
value = (seconds + SECONDS_PER_DAY / 2) / SECONDS_PER_DAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Locale locale = context.getResources().getConfiguration().locale;
|
||||||
|
final RelativeDateTimeFormatter formatter = RelativeDateTimeFormatter.getInstance(
|
||||||
|
ULocale.forLocale(locale),
|
||||||
|
null /* default NumberFormat */,
|
||||||
|
RelativeDateTimeFormatter.Style.SHORT,
|
||||||
|
android.icu.text.DisplayContext.CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE);
|
||||||
|
|
||||||
|
return formatter.format(value, RelativeDateTimeFormatter.Direction.LAST, unit);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queries for the UserInfo of a user. Returns null if the user doesn't exist (was removed).
|
* Queries for the UserInfo of a user. Returns null if the user doesn't exist (was removed).
|
||||||
* @param userManager Instance of UserManager
|
* @param userManager Instance of UserManager
|
||||||
|
@@ -235,10 +235,8 @@ public class RecentAppsPreferenceController extends AbstractPreferenceController
|
|||||||
pref.setKey(pkgName);
|
pref.setKey(pkgName);
|
||||||
pref.setTitle(appEntry.label);
|
pref.setTitle(appEntry.label);
|
||||||
pref.setIcon(mIconDrawableFactory.getBadgedIcon(appEntry.info));
|
pref.setIcon(mIconDrawableFactory.getBadgedIcon(appEntry.info));
|
||||||
pref.setSummary(TextUtils.expandTemplate(
|
pref.setSummary(Utils.formatRelativeTime(mContext,
|
||||||
mContext.getResources().getText(R.string.recent_app_summary),
|
System.currentTimeMillis() - stat.getLastTimeUsed(), false));
|
||||||
Utils.formatElapsedTime(mContext,
|
|
||||||
System.currentTimeMillis() - stat.getLastTimeUsed(), false)));
|
|
||||||
pref.setOrder(i);
|
pref.setOrder(i);
|
||||||
pref.setOnPreferenceClickListener(preference -> {
|
pref.setOnPreferenceClickListener(preference -> {
|
||||||
AppInfoBase.startAppInfoFragment(InstalledAppDetails.class,
|
AppInfoBase.startAppInfoFragment(InstalledAppDetails.class,
|
||||||
|
@@ -533,7 +533,7 @@ public class PowerUsageSummary extends PowerUsageBase implements
|
|||||||
updateScreenPreference();
|
updateScreenPreference();
|
||||||
updateLastFullChargePreference(lastFullChargeTime);
|
updateLastFullChargePreference(lastFullChargeTime);
|
||||||
|
|
||||||
final CharSequence timeSequence = Utils.formatElapsedTime(context, lastFullChargeTime,
|
final CharSequence timeSequence = Utils.formatRelativeTime(context, lastFullChargeTime,
|
||||||
false);
|
false);
|
||||||
final int resId = mShowAllApps ? R.string.power_usage_list_summary_device
|
final int resId = mShowAllApps ? R.string.power_usage_list_summary_device
|
||||||
: R.string.power_usage_list_summary;
|
: R.string.power_usage_list_summary;
|
||||||
@@ -682,10 +682,8 @@ public class PowerUsageSummary extends PowerUsageBase implements
|
|||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void updateLastFullChargePreference(long timeMs) {
|
void updateLastFullChargePreference(long timeMs) {
|
||||||
final CharSequence timeSequence = Utils.formatElapsedTime(getContext(), timeMs, false);
|
final CharSequence timeSequence = Utils.formatRelativeTime(getContext(), timeMs, false);
|
||||||
mLastFullChargePref.setSubtitle(
|
mLastFullChargePref.setSubtitle(timeSequence);
|
||||||
TextUtils.expandTemplate(getText(R.string.power_last_full_charge_summary),
|
|
||||||
timeSequence));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
@@ -173,6 +173,105 @@ public class UtilsTest {
|
|||||||
assertThat(ttsSpans[0].getType()).isEqualTo(TtsSpan.TYPE_MEASURE);
|
assertThat(ttsSpans[0].getType()).isEqualTo(TtsSpan.TYPE_MEASURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFormatRelativeTime_WithSeconds_ShowSeconds() {
|
||||||
|
final double testMillis = 40 * DateUtils.SECOND_IN_MILLIS;
|
||||||
|
final String expectedTime = "40 sec. ago";
|
||||||
|
|
||||||
|
assertThat(Utils.formatRelativeTime(mContext, testMillis, true).toString()).isEqualTo(
|
||||||
|
expectedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFormatRelativeTime_NoSeconds_DoNotShowSeconds() {
|
||||||
|
final double testMillis = 40 * DateUtils.SECOND_IN_MILLIS;
|
||||||
|
final String expectedTime = "1 min. ago";
|
||||||
|
|
||||||
|
assertThat(Utils.formatRelativeTime(mContext, testMillis, false).toString()).isEqualTo(
|
||||||
|
expectedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFormatRelativeTime_LessThanTwoMinutes_withSeconds() {
|
||||||
|
final double testMillis = 119 * DateUtils.SECOND_IN_MILLIS;
|
||||||
|
final String expectedTime = "119 sec. ago";
|
||||||
|
|
||||||
|
assertThat(Utils.formatRelativeTime(mContext, testMillis, true).toString()).isEqualTo(
|
||||||
|
expectedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFormatRelativeTime_LessThanTwoMinutes_NoSeconds() {
|
||||||
|
final double testMillis = 119 * DateUtils.SECOND_IN_MILLIS;
|
||||||
|
final String expectedTime = "2 min. ago";
|
||||||
|
|
||||||
|
assertThat(Utils.formatRelativeTime(mContext, testMillis, false).toString()).isEqualTo(
|
||||||
|
expectedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFormatRelativeTime_TwoMinutes_withSeconds() {
|
||||||
|
final double testMillis = 2 * DateUtils.MINUTE_IN_MILLIS;
|
||||||
|
final String expectedTime = "2 min. ago";
|
||||||
|
|
||||||
|
assertThat(Utils.formatRelativeTime(mContext, testMillis, true).toString()).isEqualTo(
|
||||||
|
expectedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFormatRelativeTime_LessThanTwoHours_withSeconds() {
|
||||||
|
final double testMillis = 119 * DateUtils.MINUTE_IN_MILLIS;
|
||||||
|
final String expectedTime = "119 min. ago";
|
||||||
|
|
||||||
|
assertThat(Utils.formatRelativeTime(mContext, testMillis, true).toString()).isEqualTo(
|
||||||
|
expectedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFormatRelativeTime_TwoHours_withSeconds() {
|
||||||
|
final double testMillis = 2 * DateUtils.HOUR_IN_MILLIS;
|
||||||
|
final String expectedTime = "2 hr. ago";
|
||||||
|
|
||||||
|
assertThat(Utils.formatRelativeTime(mContext, testMillis, true).toString()).isEqualTo(
|
||||||
|
expectedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFormatRelativeTime_LessThanTwoDays_withSeconds() {
|
||||||
|
final double testMillis = 47 * DateUtils.HOUR_IN_MILLIS;
|
||||||
|
final String expectedTime = "47 hr. ago";
|
||||||
|
|
||||||
|
assertThat(Utils.formatRelativeTime(mContext, testMillis, true).toString()).isEqualTo(
|
||||||
|
expectedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFormatRelativeTime_TwoDays_withSeconds() {
|
||||||
|
final double testMillis = 2 * DateUtils.DAY_IN_MILLIS;
|
||||||
|
final String expectedTime = "2 days ago";
|
||||||
|
|
||||||
|
assertThat(Utils.formatRelativeTime(mContext, testMillis, true).toString()).isEqualTo(
|
||||||
|
expectedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFormatRelativeTime_FormatZero_WithSeconds() {
|
||||||
|
final double testMillis = 0;
|
||||||
|
final String expectedTime = "0 sec. ago";
|
||||||
|
|
||||||
|
assertThat(Utils.formatRelativeTime(mContext, testMillis, true).toString()).isEqualTo(
|
||||||
|
expectedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFormatRelativeTime_FormatZero_NoSeconds() {
|
||||||
|
final double testMillis = 0;
|
||||||
|
final String expectedTime = "0 min. ago";
|
||||||
|
|
||||||
|
assertThat(Utils.formatRelativeTime(mContext, testMillis, false).toString()).isEqualTo(
|
||||||
|
expectedTime);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testInitializeVolumeDoesntBreakOnNullVolume() {
|
public void testInitializeVolumeDoesntBreakOnNullVolume() {
|
||||||
VolumeInfo info = new VolumeInfo("id", 0, new DiskInfo("id", 0), "");
|
VolumeInfo info = new VolumeInfo("id", 0, new DiskInfo("id", 0), "");
|
||||||
|
@@ -249,8 +249,6 @@ public class RecentAppsPreferenceControllerTest {
|
|||||||
when(mUsageStatsManager.queryUsageStats(anyInt(), anyLong(), anyLong()))
|
when(mUsageStatsManager.queryUsageStats(anyInt(), anyLong(), anyLong()))
|
||||||
.thenReturn(stats);
|
.thenReturn(stats);
|
||||||
|
|
||||||
when(mMockContext.getResources().getText(eq(R.string.recent_app_summary)))
|
|
||||||
.thenReturn(mContext.getResources().getText(R.string.recent_app_summary));
|
|
||||||
final Configuration configuration = new Configuration();
|
final Configuration configuration = new Configuration();
|
||||||
configuration.locale = Locale.US;
|
configuration.locale = Locale.US;
|
||||||
when(mMockContext.getResources().getConfiguration()).thenReturn(configuration);
|
when(mMockContext.getResources().getConfiguration()).thenReturn(configuration);
|
||||||
@@ -258,7 +256,7 @@ public class RecentAppsPreferenceControllerTest {
|
|||||||
mController = new RecentAppsPreferenceController(mMockContext, mAppState, null);
|
mController = new RecentAppsPreferenceController(mMockContext, mAppState, null);
|
||||||
mController.displayPreference(mScreen);
|
mController.displayPreference(mScreen);
|
||||||
|
|
||||||
verify(mCategory).addPreference(argThat(summaryMatches("0m ago")));
|
verify(mCategory).addPreference(argThat(summaryMatches("0 min. ago")));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ArgumentMatcher<Preference> summaryMatches(String expected) {
|
private static ArgumentMatcher<Preference> summaryMatches(String expected) {
|
||||||
|
@@ -411,18 +411,11 @@ public class PowerUsageSummaryTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUpdateLastFullChargePreference_showCorrectSummary() {
|
public void testUpdateLastFullChargePreference_showCorrectSummary() {
|
||||||
final CharSequence formattedString = mRealContext.getText(
|
|
||||||
R.string.power_last_full_charge_summary);
|
|
||||||
final CharSequence timeSequence = Utils.formatElapsedTime(mRealContext,
|
|
||||||
TIME_SINCE_LAST_FULL_CHARGE_MS, false);
|
|
||||||
final CharSequence expectedSummary = TextUtils.expandTemplate(
|
|
||||||
formattedString, timeSequence);
|
|
||||||
doReturn(formattedString).when(mFragment).getText(R.string.power_last_full_charge_summary);
|
|
||||||
doReturn(mRealContext).when(mFragment).getContext();
|
doReturn(mRealContext).when(mFragment).getContext();
|
||||||
|
|
||||||
mFragment.updateLastFullChargePreference(TIME_SINCE_LAST_FULL_CHARGE_MS);
|
mFragment.updateLastFullChargePreference(TIME_SINCE_LAST_FULL_CHARGE_MS);
|
||||||
|
|
||||||
assertThat(mLastFullChargePref.getSubtitle()).isEqualTo(expectedSummary);
|
assertThat(mLastFullChargePref.getSubtitle()).isEqualTo("2 hr. ago");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
Reference in New Issue
Block a user