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

Commit 1d6e9cfd authored by Victor Chang's avatar Victor Chang Committed by Gerrit Code Review
Browse files

Merge "Titlecasing time zone summary"

parents 0edc7fb3 20e9b768
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -608,8 +608,10 @@
    <string name="zone_info_exemplar_location_and_offset"><xliff:g id="exemplar_location" example="Los Angeles">%1$s</xliff:g> (<xliff:g id="offset" example="GMT-08:00">%2$s</xliff:g>)</string>
    <!-- Label describing a time zone offset and name[CHAR LIMIT=NONE] -->
    <string name="zone_info_offset_and_name"><xliff:g id="time_type" example="Pacific Time">%2$s</xliff:g> (<xliff:g id="offset" example="GMT-08:00">%1$s</xliff:g>)</string>
    <!-- Label describing a time zone and changes to DST or standard time [CHAR LIMIT=NONE] -->
    <string name="zone_info_footer">Uses <xliff:g id="offset_and_name" example="Pacific Time (GMT-08:00)">%1$s</xliff:g>. <xliff:g id="dst_time_type" example="Pacific Daylight Time">%2$s</xliff:g> starts on <xliff:g id="transition_date" example="Mar 11 2018">%3$s</xliff:g>.</string>
    <!-- Label describing a time zone and a follow-up sentence [CHAR LIMIT=NONE] -->
    <string name="zone_info_footer_first_sentence">Uses <xliff:g id="offset_and_name" example="Pacific Time (GMT-08:00)">%1$s</xliff:g>. <xliff:g id="second_sentence" example="Pacific Daylight Time starts on Mar 11 2018.">%2$s</xliff:g></string>
    <!-- Label describing the upcoming daylight savings time change [CHAR LIMIT=NONE] -->
    <string name="zone_info_footer_second_sentence"><xliff:g id="dst_time_type" example="Pacific Daylight Time">%1$s</xliff:g> starts on <xliff:g id="transition_date" example="Mar 11 2018">%2$s</xliff:g>.</string>
    <!-- Label describing a time zone without DST [CHAR LIMIT=NONE] -->
    <string name="zone_info_footer_no_dst">Uses <xliff:g id="offset_and_name" example="GMT-08:00 Pacific Time">%1$s</xliff:g>. No daylight savings time.</string>
    <!-- Describes the time type "daylight savings time" (used in zone_change_to_from_dst, when no zone specific name is available) -->
+107 −0
Original line number Diff line number Diff line
@@ -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;
    }
}
+27 −8
Original line number Diff line number Diff line
@@ -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);
        }

        final ZoneOffsetTransition nextDstTransition = findNextDstTransition(item);
        if (nextDstTransition == null) { // No future transition
            return mContext.getString(R.string.zone_info_footer_no_dst, offsetAndName);
        if (nextDstTransition == null || !timeZone.observesDaylightTime()) {
            return SpannableUtil.getResourcesText(mContext.getResources(),
                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) {
+70 −2
Original line number Diff line number Diff line
@@ -18,23 +18,91 @@ package com.android.settings.datetime.timezone;

import static com.google.common.truth.Truth.assertThat;

import android.icu.text.TimeZoneFormat;
import android.text.Spannable;
import android.text.SpannableStringBuilder;

import com.android.settings.R;
import com.android.settingslib.datetime.ZoneGetter;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;

import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

@RunWith(RobolectricTestRunner.class)
public class SpannableUtilTest {

    @Test
    public void testFormat() {
    public void testGetResourceText() {
        CharSequence gmtString = getGmtString("GMT+00:00");

        Spannable spannable = SpannableUtil.getResourcesText(
                RuntimeEnvironment.application.getResources(), R.string.zone_info_offset_and_name,
                "GMT+00:00", "UTC");
                gmtString, "UTC");
        assertThat(spannable.toString()).isEqualTo("UTC (GMT+00:00)");

        // Verify that the spans are kept.
        Object[] spans = ((Spannable) gmtString).getSpans(0, gmtString.length(), Object.class);
        Object[] newSpans = spannable.getSpans(0, spannable.length(), Object.class);
        assertThat(newSpans.length).isEqualTo(spans.length);
        assertThat(newSpans).isEqualTo(spans);
    }

    private static CharSequence getGmtString(String tzId) {
        Locale locale = Locale.US;
        TimeZoneFormat timeZoneFormat = TimeZoneFormat.getInstance(locale);
        TimeZone gmtZone = TimeZone.getTimeZone(tzId);
        Date date = new Date(0);
        return ZoneGetter.getGmtOffsetText(timeZoneFormat, locale, gmtZone, date);
    }
    /**
     * Verify the assumption on the GMT string used in {@link #testGetResourceText()}
     */
    @Test
    public void testGetGmtString() {
        // Create a GMT string and verify the assumptions
        CharSequence gmtString = getGmtString("GMT+00:00");
        assertThat(gmtString.toString()).isEqualTo("GMT+00:00");
        assertThat(gmtString).isInstanceOf(Spannable.class);
        Object[] spans = ((Spannable) gmtString).getSpans(0, gmtString.length(), Object.class);
        assertThat(spans).isNotEmpty();

        assertThat(getGmtString("GMT-08:00").toString()).isEqualTo("GMT-08:00");
    }

    @Test
    public void testTitleCaseSentences_enUS() {
        Locale locale = Locale.US;
        CharSequence titleCasedFirstSentence = SpannableUtil.titleCaseSentences(locale,
                "pacific Daylight Time starts on Mar 11 2018.");
        assertThat(titleCasedFirstSentence.toString())
            .isEqualTo("Pacific Daylight Time starts on Mar 11 2018.");

        SpannableStringBuilder sb = new SpannableStringBuilder()
                .append("uses ")
                .append("Pacific Time (")
                .append(getGmtString("GMT-08:00"))
                .append("). ")
                .append(titleCasedFirstSentence);

        assertThat(sb.toString()).isEqualTo(
                "uses Pacific Time (GMT-08:00). Pacific Daylight Time starts on Mar 11 2018.");

        Object[] spans = sb.getSpans(0, sb.length(), Object.class);
        assertThat(spans).isNotEmpty();

        CharSequence titledOutput = SpannableUtil.titleCaseSentences(Locale.US, sb);
        assertThat(titledOutput.toString()).isEqualTo(
                "Uses Pacific Time (GMT-08:00). Pacific Daylight Time starts on Mar 11 2018.");
        assertThat(titledOutput).isInstanceOf(Spannable.class);
        Object[] newSpans = ((Spannable) titledOutput).getSpans(0, titledOutput.length(),
                Object.class);
        assertThat(newSpans.length).isEqualTo(spans.length);
        assertThat(newSpans).isEqualTo(spans);
    }
}