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

Commit 3542244c authored by Lei Yu's avatar Lei Yu Committed by Android (Google) Code Review
Browse files

Merge "Make Timezone selection and selected value has same logic"

parents 57cdb077 c6a3274f
Loading
Loading
Loading
Loading
+121 −84
Original line number Diff line number Diff line
@@ -40,23 +40,47 @@ import java.util.Map;
import java.util.Set;
import java.util.TimeZone;

/**
 * ZoneGetter is the utility class to get time zone and zone list, and both of them have display
 * name in time zone. In this class, we will keep consistency about display names for all
 * the methods.
 *
 * The display name chosen for each zone entry depends on whether the zone is one associated
 * with the country of the user's chosen locale. For "local" zones we prefer the "long name"
 * (e.g. "Europe/London" -> "British Summer Time" for people in the UK). For "non-local"
 * zones we prefer the exemplar location (e.g. "Europe/London" -> "London" for English
 * speakers from outside the UK). This heuristic is based on the fact that people are
 * typically familiar with their local timezones and exemplar locations don't always match
 * modern-day expectations for people living in the country covered. Large countries like
 * China that mostly use a single timezone (olson id: "Asia/Shanghai") may not live near
 * "Shanghai" and prefer the long name over the exemplar location. The only time we don't
 * follow this policy for local zones is when Android supplies multiple olson IDs to choose
 * from and the use of a zone's long name leads to ambiguity. For example, at the time of
 * writing Android lists 5 olson ids for Australia which collapse to 2 different zone names
 * in winter but 4 different zone names in summer. The ambiguity leads to the users
 * selecting the wrong olson ids.
 *
 */
public class ZoneGetter {
    private static final String TAG = "ZoneGetter";

    private static final String XMLTAG_TIMEZONE = "timezone";

    public static final String KEY_ID = "id";  // value: String
    public static final String KEY_DISPLAYNAME = "name";  // value: String
    public static final String KEY_GMT = "gmt";  // value: String
    public static final String KEY_OFFSET = "offset";  // value: int (Integer)

    private ZoneGetter() {}
    private static final String XMLTAG_TIMEZONE = "timezone";

    public static String getTimeZoneOffsetAndName(TimeZone tz, Date now) {
        Locale locale = Locale.getDefault();
        String gmtString = getGmtOffsetString(locale, tz, now);
        TimeZoneNames timeZoneNames = TimeZoneNames.getInstance(locale);
        String zoneNameString = getZoneLongName(timeZoneNames, tz, now);
    public static String getTimeZoneOffsetAndName(Context context, TimeZone tz, Date now) {
        final Locale locale = Locale.getDefault();
        final String gmtString = getGmtOffsetString(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,
                useExemplarLocationForLocalNames, tz, tz.getID());
        if (zoneNameString == null) {
            return gmtString;
        }
@@ -69,82 +93,20 @@ public class ZoneGetter {
        final Locale locale = Locale.getDefault();
        final Date now = new Date();
        final TimeZoneNames timeZoneNames = TimeZoneNames.getInstance(locale);

        // The display name chosen for each zone entry depends on whether the zone is one associated
        // with the country of the user's chosen locale. For "local" zones we prefer the "long name"
        // (e.g. "Europe/London" -> "British Summer Time" for people in the UK). For "non-local"
        // zones we prefer the exemplar location (e.g. "Europe/London" -> "London" for English
        // speakers from outside the UK). This heuristic is based on the fact that people are
        // typically familiar with their local timezones and exemplar locations don't always match
        // modern-day expectations for people living in the country covered. Large countries like
        // China that mostly use a single timezone (olson id: "Asia/Shanghai") may not live near
        // "Shanghai" and prefer the long name over the exemplar location. The only time we don't
        // follow this policy for local zones is when Android supplies multiple olson IDs to choose
        // from and the use of a zone's long name leads to ambiguity. For example, at the time of
        // writing Android lists 5 olson ids for Australia which collapse to 2 different zone names
        // in winter but 4 different zone names in summer. The ambiguity leads to the users
        // selecting the wrong olson ids.

        // Get the list of olson ids to display to the user.
        List<String> olsonIdsToDisplayList = readTimezonesToDisplay(context);

        // Store the information we are going to need more than once.
        final int zoneCount = olsonIdsToDisplayList.size();
        final String[] olsonIdsToDisplay = new String[zoneCount];
        final TimeZone[] timeZones = new TimeZone[zoneCount];
        final String[] gmtOffsetStrings = new String[zoneCount];
        for (int i = 0; i < zoneCount; i++) {
            String olsonId = olsonIdsToDisplayList.get(i);
            olsonIdsToDisplay[i] = olsonId;
            TimeZone tz = TimeZone.getTimeZone(olsonId);
            timeZones[i] = tz;
            gmtOffsetStrings[i] = getGmtOffsetString(locale, tz, now);
        }

        // Create a lookup of local zone IDs.
        Set<String> localZoneIds = new HashSet<String>();
        for (String olsonId : libcore.icu.TimeZoneNames.forLocale(locale)) {
            localZoneIds.add(olsonId);
        }
        final ZoneGetterData data = new ZoneGetterData(context);

        // Work out whether the display names we would show by default would be ambiguous.
        Set<String> localZoneNames = new HashSet<String>();
        boolean useExemplarLocationForLocalNames = false;
        for (int i = 0; i < zoneCount; i++) {
            String olsonId = olsonIdsToDisplay[i];
            if (localZoneIds.contains(olsonId)) {
                TimeZone tz = timeZones[i];
                String displayName = getZoneLongName(timeZoneNames, tz, now);
                if (displayName == null) {
                    displayName = gmtOffsetStrings[i];
                }
                boolean nameIsUnique = localZoneNames.add(displayName);
                if (!nameIsUnique) {
                    useExemplarLocationForLocalNames = true;
                    break;
                }
            }
        }
        final boolean useExemplarLocationForLocalNames =
                shouldUseExemplarLocationForLocalNames(data, timeZoneNames);

        // Generate the list of zone entries to return.
        List<Map<String, Object>> zones = new ArrayList<Map<String, Object>>();
        for (int i = 0; i < zoneCount; i++) {
            String olsonId = olsonIdsToDisplay[i];
            TimeZone tz = timeZones[i];
            String gmtOffsetString = gmtOffsetStrings[i];
        for (int i = 0; i < data.zoneCount; i++) {
            TimeZone tz = data.timeZones[i];
            String gmtOffsetString = data.gmtOffsetStrings[i];

            boolean isLocalZoneId = localZoneIds.contains(olsonId);
            boolean preferLongName = isLocalZoneId && !useExemplarLocationForLocalNames;
            String displayName;
            if (preferLongName) {
                displayName = getZoneLongName(timeZoneNames, tz, now);
            } else {
                displayName = timeZoneNames.getExemplarLocationName(tz.getID());
                if (displayName == null || displayName.isEmpty()) {
                    // getZoneExemplarLocation can return null. Fall back to the long name.
                    displayName = getZoneLongName(timeZoneNames, tz, now);
                }
            }
            String displayName = getTimeZoneDisplayName(data, timeZoneNames,
                    useExemplarLocationForLocalNames, tz, data.olsonIdsToDisplay[i]);
            if (displayName == null  || displayName.isEmpty()) {
                displayName = gmtOffsetString;
            }
@@ -198,12 +160,54 @@ public class ZoneGetter {
        return olsonIds;
    }

    private static boolean shouldUseExemplarLocationForLocalNames(ZoneGetterData data,
            TimeZoneNames timeZoneNames) {
        final Set<String> localZoneNames = new HashSet<String>();
        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);
                if (displayName == null) {
                    displayName = data.gmtOffsetStrings[i];
                }
                final boolean nameIsUnique = localZoneNames.add(displayName);
                if (!nameIsUnique) {
                    return true;
                }
            }
        }

        return false;
    }

    private static String 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;
        String displayName;

        if (preferLongName) {
            displayName = getZoneLongName(timeZoneNames, tz, now);
        } else {
            displayName = timeZoneNames.getExemplarLocationName(tz.getID());
            if (displayName == null || displayName.isEmpty()) {
                // getZoneExemplarLocation can return null. Fall back to the long name.
                displayName = getZoneLongName(timeZoneNames, tz, now);
            }
        }

        return displayName;
    }

    /**
     * Returns the long name for the timezone for the given locale at the time specified.
     * Can return {@code null}.
     */
    private static String getZoneLongName(TimeZoneNames names, TimeZone tz, Date now) {
        TimeZoneNames.NameType nameType =
        final TimeZoneNames.NameType nameType =
                tz.inDaylightTime(now) ? TimeZoneNames.NameType.LONG_DAYLIGHT
                        : TimeZoneNames.NameType.LONG_STANDARD;
        return names.getDisplayName(tz.getID(), nameType, now.getTime());
@@ -211,15 +215,48 @@ public class ZoneGetter {

    private static String getGmtOffsetString(Locale locale, TimeZone tz, Date now) {
        // Use SimpleDateFormat to format the GMT+00:00 string.
        SimpleDateFormat gmtFormatter = new SimpleDateFormat("ZZZZ");
        final SimpleDateFormat gmtFormatter = new SimpleDateFormat("ZZZZ");
        gmtFormatter.setTimeZone(tz);
        String gmtString = gmtFormatter.format(now);

        // Ensure that the "GMT+" stays with the "00:00" even if the digits are RTL.
        BidiFormatter bidiFormatter = BidiFormatter.getInstance();
        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;
    }

    private static final class ZoneGetterData {
        public final String[] olsonIdsToDisplay;
        public final String[] gmtOffsetStrings;
        public final TimeZone[] timeZones;
        public final Set<String> localZoneIds;
        public final int zoneCount;

        public ZoneGetterData(Context context) {
            final Locale locale = Locale.getDefault();
            final Date now = new Date();
            final List<String> olsonIdsToDisplayList = readTimezonesToDisplay(context);

            // Load all the data needed to display time zones
            zoneCount = olsonIdsToDisplayList.size();
            olsonIdsToDisplay = new String[zoneCount];
            timeZones = new TimeZone[zoneCount];
            gmtOffsetStrings = new String[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);
            }

            // Create a lookup of local zone IDs.
            localZoneIds = new HashSet<String>();
            for (String olsonId : libcore.icu.TimeZoneNames.forLocale(locale)) {
                localZoneIds.add(olsonId);
            }
        }
    }
}
 No newline at end of file
+1 −0
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@
    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
    <uses-permission android:name="android.permission.MANAGE_USERS" />
    <uses-permission android:name="android.permission.MANAGE_NETWORK_POLICY"/>
    <uses-permission android:name="android.permission.SET_TIME_ZONE" />

    <application>
        <uses-library android:name="android.test.runner" />
+68 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */
package com.android.settingslib.utils;

import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.support.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.*;
import com.android.settingslib.datetime.ZoneGetter;

import static junit.framework.Assert.assertTrue;

@RunWith(AndroidJUnit4.class)
@SmallTest
public class ZoneGetterTest {
    private static final String TIME_ZONE_LONDON_ID = "Europe/London";
    private static final String TIME_ZONE_LA_ID = "America/Los_Angeles";
    private Locale mLocaleEnUs;
    private Calendar mCalendar;

    @Before
    public void setUp() {
        mLocaleEnUs = new Locale("en", "us");
        Locale.setDefault(mLocaleEnUs);
        mCalendar = new GregorianCalendar(2016, 9, 1);
    }

    @Test
    public void getTimeZoneOffsetAndName_setLondon_returnLondon() {
        // Check it will ends with 'London', not 'British Summer Time' or sth else
        testTimeZoneOffsetAndNameInner(TIME_ZONE_LONDON_ID, "London");
    }

    @Test
    public void getTimeZoneOffsetAndName_setLosAngeles_returnPacificDaylightTime() {
        // Check it will ends with 'Pacific Daylight Time', not 'Los_Angeles'
        testTimeZoneOffsetAndNameInner(TIME_ZONE_LA_ID, "Pacific Daylight Time");
    }

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

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

        assertTrue(timeZoneString.endsWith(expectedName));
    }

}