Titlecasing time zone summary
Additional fix: 1. Fixed the SpannableUtil.getResourcesText to actually preserve Spannable (TtsSpan in this use case) during formatting. Bug: 185453652 Test: make RunSettingsRoboTests ROBOTEST_FILTER=com.android.settings.datetime.timezone Change-Id: Iae5e1d4261ec0a34222ae1d042c7f3f027f2e512
This commit is contained in:
@@ -18,14 +18,72 @@ package com.android.settings.datetime.timezone;
|
||||
|
||||
import android.annotation.StringRes;
|
||||
import android.content.res.Resources;
|
||||
import android.icu.text.CaseMap;
|
||||
import android.icu.text.Edits;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Formattable;
|
||||
import java.util.FormattableFlags;
|
||||
import java.util.Formatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
public class SpannableUtil {
|
||||
private static final String TAG = "SpannableUtil";
|
||||
|
||||
private static class SpannableFormattable implements Formattable {
|
||||
|
||||
private final Spannable mSpannable;
|
||||
|
||||
private SpannableFormattable(Spannable spannable) {
|
||||
this.mSpannable = spannable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void formatTo(Formatter formatter, int flags, int width, int precision) {
|
||||
CharSequence s = handlePrecision(mSpannable, precision);
|
||||
s = handleWidth(s, width, (flags & FormattableFlags.LEFT_JUSTIFY) != 0);
|
||||
try {
|
||||
formatter.out().append(s);
|
||||
} catch (IOException e) {
|
||||
// The error should never occur because formatter.out() returns
|
||||
// SpannableStringBuilder which doesn't throw IOException.
|
||||
Log.e(TAG, "error in SpannableFormattable", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static CharSequence handlePrecision(CharSequence s, int precision) {
|
||||
if (precision != -1 && precision < s.length()) {
|
||||
return s.subSequence(0, precision);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private static CharSequence handleWidth(CharSequence s, int width, boolean isLeftJustify) {
|
||||
if (width == -1) {
|
||||
return s;
|
||||
}
|
||||
int diff = width - s.length();
|
||||
if (diff <= 0) {
|
||||
return s;
|
||||
}
|
||||
SpannableStringBuilder sb = new SpannableStringBuilder();
|
||||
if (!isLeftJustify) {
|
||||
sb.append(" ".repeat(diff));
|
||||
}
|
||||
sb.append(s);
|
||||
if (isLeftJustify) {
|
||||
sb.append(" ".repeat(diff));
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@class Resources} has no method to format string resource with {@class Spannable} a
|
||||
@@ -35,7 +93,56 @@ public class SpannableUtil {
|
||||
Object... args) {
|
||||
final Locale locale = res.getConfiguration().getLocales().get(0);
|
||||
final SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
// Formatter converts CharSequence to String by calling toString() if an arg isn't
|
||||
// Formattable. Wrap Spannable by SpannableFormattable to preserve Spannable objects.
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (args[i] instanceof Spannable) {
|
||||
args[i] = new SpannableFormattable((Spannable) args[i]);
|
||||
}
|
||||
}
|
||||
new Formatter(builder, locale).format(res.getString(resId), args);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static final CaseMap.Title TITLE_CASE_MAP =
|
||||
CaseMap.toTitle().sentences().noLowercase();
|
||||
|
||||
/**
|
||||
* Titlecasing {@link CharSequence} and {@link Spannable} by using {@link CaseMap.Title}.
|
||||
*/
|
||||
public static CharSequence titleCaseSentences(Locale locale, CharSequence src) {
|
||||
if (src instanceof Spannable) {
|
||||
return applyCaseMapToSpannable(locale, TITLE_CASE_MAP, (Spannable) src);
|
||||
} else {
|
||||
return TITLE_CASE_MAP.apply(locale, null, src);
|
||||
}
|
||||
}
|
||||
|
||||
private static Spannable applyCaseMapToSpannable(Locale locale, CaseMap.Title caseMap,
|
||||
Spannable src) {
|
||||
Edits edits = new Edits();
|
||||
SpannableStringBuilder dest = new SpannableStringBuilder();
|
||||
caseMap.apply(locale, null, src, dest, edits);
|
||||
if (!edits.hasChanges()) {
|
||||
return src;
|
||||
}
|
||||
Edits.Iterator iterator = edits.getCoarseChangesIterator();
|
||||
List<int[]> changes = new ArrayList<>();
|
||||
while (iterator.next()) {
|
||||
int[] change = new int[] {
|
||||
iterator.sourceIndex(), // 0
|
||||
iterator.oldLength(), // 1
|
||||
iterator.destinationIndex(), // 2
|
||||
iterator.newLength(), // 3
|
||||
};
|
||||
changes.add(change);
|
||||
}
|
||||
// Replacement starts from the end to avoid shifting the source index during replacement
|
||||
Collections.reverse(changes);
|
||||
SpannableStringBuilder result = new SpannableStringBuilder(src);
|
||||
for (int[] c : changes) {
|
||||
result.replace(c[0], c[0] + c[1], dest, c[2], c[2] + c[3]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@@ -17,6 +17,7 @@
|
||||
package com.android.settings.datetime.timezone;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.icu.text.DateFormat;
|
||||
import android.icu.text.DisplayContext;
|
||||
import android.icu.text.SimpleDateFormat;
|
||||
@@ -32,6 +33,7 @@ import java.time.Instant;
|
||||
import java.time.zone.ZoneOffsetTransition;
|
||||
import java.time.zone.ZoneRules;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
public class TimeZoneInfoPreferenceController extends BasePreferenceController {
|
||||
|
||||
@@ -82,14 +84,15 @@ public class TimeZoneInfoPreferenceController extends BasePreferenceController {
|
||||
private CharSequence formatInfo(TimeZoneInfo item) {
|
||||
final CharSequence offsetAndName = formatOffsetAndName(item);
|
||||
final TimeZone timeZone = item.getTimeZone();
|
||||
if (!timeZone.observesDaylightTime()) {
|
||||
return mContext.getString(R.string.zone_info_footer_no_dst, offsetAndName);
|
||||
ZoneOffsetTransition nextDstTransition = null;
|
||||
if (timeZone.observesDaylightTime()) {
|
||||
nextDstTransition = findNextDstTransition(item);
|
||||
}
|
||||
if (nextDstTransition == null || !timeZone.observesDaylightTime()) {
|
||||
return SpannableUtil.getResourcesText(mContext.getResources(),
|
||||
R.string.zone_info_footer_no_dst, offsetAndName);
|
||||
}
|
||||
|
||||
final ZoneOffsetTransition nextDstTransition = findNextDstTransition(item);
|
||||
if (nextDstTransition == null) { // No future transition
|
||||
return mContext.getString(R.string.zone_info_footer_no_dst, offsetAndName);
|
||||
}
|
||||
final boolean toDst = getDSTSavings(timeZone, nextDstTransition.getInstant()) != 0;
|
||||
String timeType = toDst ? item.getDaylightName() : item.getStandardName();
|
||||
if (timeType == null) {
|
||||
@@ -103,8 +106,24 @@ public class TimeZoneInfoPreferenceController extends BasePreferenceController {
|
||||
final Calendar transitionTime = Calendar.getInstance(timeZone);
|
||||
transitionTime.setTimeInMillis(nextDstTransition.getInstant().toEpochMilli());
|
||||
final String date = mDateFormat.format(transitionTime);
|
||||
return SpannableUtil.getResourcesText(mContext.getResources(),
|
||||
R.string.zone_info_footer, offsetAndName, timeType, date);
|
||||
return createFooterString(offsetAndName, timeType, date);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param offsetAndName {@Spannable} styled text information should be preserved. See
|
||||
* {@link #formatInfo} and {@link com.android.settingslib.datetime.ZoneGetter#getGmtOffsetText}.
|
||||
*
|
||||
*/
|
||||
private CharSequence createFooterString(CharSequence offsetAndName, String timeType,
|
||||
String date) {
|
||||
Resources res = mContext.getResources();
|
||||
Locale locale = res.getConfiguration().getLocales().get(0);
|
||||
CharSequence secondSentence = SpannableUtil.titleCaseSentences(locale,
|
||||
SpannableUtil.getResourcesText(res, R.string.zone_info_footer_second_sentence,
|
||||
timeType, date));
|
||||
|
||||
return SpannableUtil.titleCaseSentences(locale, SpannableUtil.getResourcesText(res,
|
||||
R.string.zone_info_footer_first_sentence, offsetAndName, secondSentence));
|
||||
}
|
||||
|
||||
private ZoneOffsetTransition findNextDstTransition(TimeZoneInfo timeZoneInfo) {
|
||||
|
Reference in New Issue
Block a user