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

Commit 66549382 authored by Maurice Lam's avatar Maurice Lam Committed by Android (Google) Code Review
Browse files

Merge "Fix TTS for GMT offset"

parents b2db1591 ebc050f1
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -863,4 +863,6 @@
    <!-- Content description for drawer menu button [CHAR_LIMIT=30]-->
    <string name="content_description_menu_button">Menu</string>

    <!-- Label for Greenwich mean time, used in a string like GMT+05:00. [CHAR LIMIT=NONE] -->
    <string name="time_zone_gmt">GMT</string>
</resources>
+100 −34
Original line number Diff line number Diff line
@@ -19,9 +19,13 @@ package com.android.settingslib.datetime;
import android.content.Context;
import android.content.res.XmlResourceParser;
import android.icu.text.TimeZoneNames;
import android.text.BidiFormatter;
import android.text.TextDirectionHeuristics;
import android.support.v4.text.BidiFormatter;
import android.support.v4.text.TextDirectionHeuristicsCompat;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.style.TtsSpan;
import android.util.Log;
import android.view.View;

@@ -29,7 +33,6 @@ import com.android.settingslib.R;

import org.xmlpull.v1.XmlPullParserException;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
@@ -65,28 +68,41 @@ public class ZoneGetter {
    private static final String TAG = "ZoneGetter";

    public static final String KEY_ID = "id";  // value: String

    /**
     * @deprecated Use {@link #KEY_DISPLAY_LABEL} instead.
     */
    @Deprecated
    public static final String KEY_DISPLAYNAME = "name";  // value: String

    public static final String KEY_DISPLAY_LABEL = "display_label"; // value: CharSequence

    /**
     * @deprecated Use {@link #KEY_OFFSET_LABEL} instead.
     */
    @Deprecated
    public static final String KEY_GMT = "gmt";  // value: String
    public static final String KEY_OFFSET = "offset";  // value: int (Integer)
    public static final String KEY_OFFSET_LABEL = "offset_label";  // value: CharSequence

    private static final String XMLTAG_TIMEZONE = "timezone";

    public static String getTimeZoneOffsetAndName(Context context, TimeZone tz, Date now) {
    public static CharSequence getTimeZoneOffsetAndName(Context context, TimeZone tz, Date now) {
        final Locale locale = Locale.getDefault();
        final String gmtString = getGmtOffsetString(locale, tz, now);
        final CharSequence gmtText = getGmtOffsetText(context, locale, tz, now);
        final TimeZoneNames timeZoneNames = TimeZoneNames.getInstance(locale);
        final ZoneGetterData data = new ZoneGetterData(context);

        final boolean useExemplarLocationForLocalNames =
                shouldUseExemplarLocationForLocalNames(data, timeZoneNames);
        final String zoneNameString = getTimeZoneDisplayName(data, timeZoneNames,
        final CharSequence zoneName = getTimeZoneDisplayName(data, timeZoneNames,
                useExemplarLocationForLocalNames, tz, tz.getID());
        if (zoneNameString == null) {
            return gmtString;
        if (zoneName == null) {
            return gmtText;
        }

        // We don't use punctuation here to avoid having to worry about localizing that too!
        return gmtString + " " + zoneNameString;
        return TextUtils.concat(gmtText, " ", zoneName);
    }

    public static List<Map<String, Object>> getZonesList(Context context) {
@@ -103,28 +119,30 @@ public class ZoneGetter {
        List<Map<String, Object>> zones = new ArrayList<Map<String, Object>>();
        for (int i = 0; i < data.zoneCount; i++) {
            TimeZone tz = data.timeZones[i];
            String gmtOffsetString = data.gmtOffsetStrings[i];
            CharSequence gmtOffsetText = data.gmtOffsetTexts[i];

            String displayName = getTimeZoneDisplayName(data, timeZoneNames,
            CharSequence displayName = getTimeZoneDisplayName(data, timeZoneNames,
                    useExemplarLocationForLocalNames, tz, data.olsonIdsToDisplay[i]);
            if (displayName == null  || displayName.isEmpty()) {
                displayName = gmtOffsetString;
            if (TextUtils.isEmpty(displayName)) {
                displayName = gmtOffsetText;
            }

            int offsetMillis = tz.getOffset(now.getTime());
            Map<String, Object> displayEntry =
                    createDisplayEntry(tz, gmtOffsetString, displayName, offsetMillis);
                    createDisplayEntry(tz, gmtOffsetText, displayName, offsetMillis);
            zones.add(displayEntry);
        }
        return zones;
    }

    private static Map<String, Object> createDisplayEntry(
            TimeZone tz, String gmtOffsetString, String displayName, int offsetMillis) {
        Map<String, Object> map = new HashMap<String, Object>();
            TimeZone tz, CharSequence gmtOffsetText, CharSequence displayName, int offsetMillis) {
        Map<String, Object> map = new HashMap<>();
        map.put(KEY_ID, tz.getID());
        map.put(KEY_DISPLAYNAME, displayName);
        map.put(KEY_GMT, gmtOffsetString);
        map.put(KEY_DISPLAYNAME, displayName.toString());
        map.put(KEY_DISPLAY_LABEL, displayName);
        map.put(KEY_GMT, gmtOffsetText.toString());
        map.put(KEY_OFFSET_LABEL, gmtOffsetText);
        map.put(KEY_OFFSET, offsetMillis);
        return map;
    }
@@ -162,15 +180,15 @@ public class ZoneGetter {

    private static boolean shouldUseExemplarLocationForLocalNames(ZoneGetterData data,
            TimeZoneNames timeZoneNames) {
        final Set<String> localZoneNames = new HashSet<String>();
        final Set<CharSequence> localZoneNames = new HashSet<>();
        final Date now = new Date();
        for (int i = 0; i < data.zoneCount; i++) {
            final String olsonId = data.olsonIdsToDisplay[i];
            if (data.localZoneIds.contains(olsonId)) {
                final TimeZone tz = data.timeZones[i];
                String displayName = getZoneLongName(timeZoneNames, tz, now);
                CharSequence displayName = getZoneLongName(timeZoneNames, tz, now);
                if (displayName == null) {
                    displayName = data.gmtOffsetStrings[i];
                    displayName = data.gmtOffsetTexts[i];
                }
                final boolean nameIsUnique = localZoneNames.add(displayName);
                if (!nameIsUnique) {
@@ -182,8 +200,9 @@ public class ZoneGetter {
        return false;
    }

    private static String getTimeZoneDisplayName(ZoneGetterData data, TimeZoneNames timeZoneNames,
            boolean useExemplarLocationForLocalNames, TimeZone tz, String olsonId) {
    private static CharSequence getTimeZoneDisplayName(ZoneGetterData data,
            TimeZoneNames timeZoneNames, boolean useExemplarLocationForLocalNames, TimeZone tz,
            String olsonId) {
        final Date now = new Date();
        final boolean isLocalZoneId = data.localZoneIds.contains(olsonId);
        final boolean preferLongName = isLocalZoneId && !useExemplarLocationForLocalNames;
@@ -213,23 +232,70 @@ public class ZoneGetter {
        return names.getDisplayName(tz.getID(), nameType, now.getTime());
    }

    private static String getGmtOffsetString(Locale locale, TimeZone tz, Date now) {
        // Use SimpleDateFormat to format the GMT+00:00 string.
        final SimpleDateFormat gmtFormatter = new SimpleDateFormat("ZZZZ");
        gmtFormatter.setTimeZone(tz);
        String gmtString = gmtFormatter.format(now);
    private static void appendWithTtsSpan(SpannableStringBuilder builder, CharSequence content,
            TtsSpan span) {
        int start = builder.length();
        builder.append(content);
        builder.setSpan(span, start, builder.length(), 0);
    }

    private static String twoDigits(int input) {
        StringBuilder builder = new StringBuilder(3);
        if (input < 0) builder.append('-');
        String string = Integer.toString(Math.abs(input));
        if (string.length() == 1) builder.append("0");
        builder.append(string);
        return builder.toString();
    }

    /**
     * Get the GMT offset text label for the given time zone, in the format "GMT-08:00". This will
     * also add TTS spans to give hints to the text-to-speech engine for the type of data it is.
     *
     * @param context The context which the string is displayed in.
     * @param locale The locale which the string is displayed in. This should be the same as the
     *               locale of the context.
     * @param tz Time zone to get the GMT offset from.
     * @param now The current time, used to tell whether daylight savings is active.
     * @return A CharSequence suitable for display as the offset label of {@code tz}.
     */
    private static CharSequence getGmtOffsetText(Context context, Locale locale, TimeZone tz,
            Date now) {
        SpannableStringBuilder builder = new SpannableStringBuilder();

        appendWithTtsSpan(builder, "GMT",
                new TtsSpan.TextBuilder(context.getString(R.string.time_zone_gmt)).build());

        int offsetMillis = tz.getOffset(now.getTime());
        if (offsetMillis >= 0) {
            appendWithTtsSpan(builder, "+", new TtsSpan.VerbatimBuilder("+").build());
        }

        final int offsetHours = (int) (offsetMillis / DateUtils.HOUR_IN_MILLIS);
        appendWithTtsSpan(builder, twoDigits(offsetHours),
                new TtsSpan.MeasureBuilder().setNumber(offsetHours).setUnit("hour").build());

        builder.append(":");

        final int offsetMinutes = (int) (offsetMillis / DateUtils.MINUTE_IN_MILLIS);
        final int offsetMinutesRemaining = Math.abs(offsetMinutes) % 60;
        appendWithTtsSpan(builder, twoDigits(offsetMinutesRemaining),
                new TtsSpan.MeasureBuilder().setNumber(offsetMinutesRemaining)
                        .setUnit("minute").build());

        CharSequence gmtText = new SpannableString(builder);

        // Ensure that the "GMT+" stays with the "00:00" even if the digits are RTL.
        final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
        boolean isRtl = TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL;
        gmtString = bidiFormatter.unicodeWrap(gmtString,
                isRtl ? TextDirectionHeuristics.RTL : TextDirectionHeuristics.LTR);
        return gmtString;
        gmtText = bidiFormatter.unicodeWrap(gmtText,
                isRtl ? TextDirectionHeuristicsCompat.RTL : TextDirectionHeuristicsCompat.LTR);
        return gmtText;
    }

    private static final class ZoneGetterData {
        public final String[] olsonIdsToDisplay;
        public final String[] gmtOffsetStrings;
        public final CharSequence[] gmtOffsetTexts;
        public final TimeZone[] timeZones;
        public final Set<String> localZoneIds;
        public final int zoneCount;
@@ -243,13 +309,13 @@ public class ZoneGetter {
            zoneCount = olsonIdsToDisplayList.size();
            olsonIdsToDisplay = new String[zoneCount];
            timeZones = new TimeZone[zoneCount];
            gmtOffsetStrings = new String[zoneCount];
            gmtOffsetTexts = new CharSequence[zoneCount];
            for (int i = 0; i < zoneCount; i++) {
                final String olsonId = olsonIdsToDisplayList.get(i);
                olsonIdsToDisplay[i] = olsonId;
                final TimeZone tz = TimeZone.getTimeZone(olsonId);
                timeZones[i] = tz;
                gmtOffsetStrings[i] = getGmtOffsetString(locale, tz, now);
                gmtOffsetTexts[i] = getGmtOffsetText(context, locale, tz, now);
            }

            // Create a lookup of local zone IDs.
+32 −2
Original line number Diff line number Diff line
@@ -19,6 +19,9 @@ import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.support.test.filters.SmallTest;
import android.text.Spanned;
import android.text.style.TtsSpan;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -55,14 +58,41 @@ public class ZoneGetterTest {
        testTimeZoneOffsetAndNameInner(TIME_ZONE_LA_ID, "Pacific Daylight Time");
    }

    @Test
    public void getZonesList_checkTypes() {
        final List<Map<String, Object>> zones =
                ZoneGetter.getZonesList(InstrumentationRegistry.getContext());
        for (Map<String, Object> zone : zones) {
            assertTrue(zone.get(ZoneGetter.KEY_DISPLAYNAME) instanceof String);
            assertTrue(zone.get(ZoneGetter.KEY_DISPLAY_LABEL) instanceof CharSequence);
            assertTrue(zone.get(ZoneGetter.KEY_OFFSET) instanceof Integer);
            assertTrue(zone.get(ZoneGetter.KEY_OFFSET_LABEL) instanceof CharSequence);
            assertTrue(zone.get(ZoneGetter.KEY_ID) instanceof String);
            assertTrue(zone.get(ZoneGetter.KEY_GMT) instanceof String);
        }
    }

    @Test
    public void getTimeZoneOffsetAndName_withTtsSpan() {
        final Context context = InstrumentationRegistry.getContext();
        final TimeZone timeZone = TimeZone.getTimeZone(TIME_ZONE_LA_ID);

        CharSequence timeZoneString = ZoneGetter.getTimeZoneOffsetAndName(context, timeZone,
                mCalendar.getTime());
        assertTrue("Time zone string should be spanned", timeZoneString instanceof Spanned);
        assertTrue("Time zone display name should have TTS spans",
                ((Spanned) timeZoneString).getSpans(
                    0, timeZoneString.length(), TtsSpan.class).length > 0);
    }

    private void testTimeZoneOffsetAndNameInner(String timeZoneId, String expectedName) {
        final Context context = InstrumentationRegistry.getContext();
        final TimeZone timeZone = TimeZone.getTimeZone(timeZoneId);

        String timeZoneString = ZoneGetter.getTimeZoneOffsetAndName(context, timeZone,
        CharSequence timeZoneString = ZoneGetter.getTimeZoneOffsetAndName(context, timeZone,
                mCalendar.getTime());

        assertTrue(timeZoneString.endsWith(expectedName));
        assertTrue(timeZoneString.toString().endsWith(expectedName));
    }

}