diff --git a/src/com/android/settings/datetime/timezone/TimeZoneInfo.java b/src/com/android/settings/datetime/timezone/TimeZoneInfo.java index 96a20674de5..8cb1d4e2bed 100644 --- a/src/com/android/settings/datetime/timezone/TimeZoneInfo.java +++ b/src/com/android/settings/datetime/timezone/TimeZoneInfo.java @@ -15,9 +15,16 @@ */ package com.android.settings.datetime.timezone; +import android.icu.text.TimeZoneFormat; +import android.icu.text.TimeZoneNames; import android.icu.util.TimeZone; import android.text.TextUtils; +import com.android.settingslib.datetime.ZoneGetter; + +import java.util.Date; +import java.util.Locale; + /** * Data object containing information for displaying a time zone for the user to select. */ @@ -131,6 +138,51 @@ public class TimeZoneInfo { } return new TimeZoneInfo(this); } - } + + public static class Formatter { + private final Locale mLocale; + private final Date mNow; + private final TimeZoneFormat mTimeZoneFormat; + + public Formatter(Locale locale, Date now) { + mLocale = locale; + mNow = now; + mTimeZoneFormat = TimeZoneFormat.getInstance(locale); + } + + /** + * @param timeZoneId Olson time zone id + * @return TimeZoneInfo containing time zone names, exemplar locations and GMT offset + */ + public TimeZoneInfo format(String timeZoneId) { + TimeZone timeZone = TimeZone.getFrozenTimeZone(timeZoneId); + return format(timeZone); + } + + /** + * @param timeZone Olson time zone object + * @return TimeZoneInfo containing time zone names, exemplar locations and GMT offset + */ + public TimeZoneInfo format(TimeZone timeZone) { + final String id = timeZone.getID(); + final TimeZoneNames timeZoneNames = mTimeZoneFormat.getTimeZoneNames(); + final java.util.TimeZone javaTimeZone = android.icu.impl.TimeZoneAdapter.wrap(timeZone); + final CharSequence gmtOffset = ZoneGetter.getGmtOffsetText(mTimeZoneFormat, mLocale, + javaTimeZone, mNow); + return new TimeZoneInfo.Builder(timeZone) + .setGenericName(timeZoneNames.getDisplayName(id, + TimeZoneNames.NameType.LONG_GENERIC, mNow.getTime())) + .setStandardName(timeZoneNames.getDisplayName(id, + TimeZoneNames.NameType.LONG_STANDARD, mNow.getTime())) + .setDaylightName(timeZoneNames.getDisplayName(id, + TimeZoneNames.NameType.LONG_DAYLIGHT, mNow.getTime())) + .setExemplarLocation(timeZoneNames.getExemplarLocationName(id)) + .setGmtOffset(gmtOffset) + // TODO: move Item id to TimeZoneInfoAdapter + .setItemId(0) + .build(); + } + } + } diff --git a/src/com/android/settings/datetime/timezone/model/FilteredCountryTimeZones.java b/src/com/android/settings/datetime/timezone/model/FilteredCountryTimeZones.java new file mode 100644 index 00000000000..d1b6ed97142 --- /dev/null +++ b/src/com/android/settings/datetime/timezone/model/FilteredCountryTimeZones.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2018 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.settings.datetime.timezone.model; + +import libcore.util.CountryTimeZones; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Wrap {@class CountryTimeZones} to filter time zone that are shown in the picker. + */ +public class FilteredCountryTimeZones { + + private final CountryTimeZones mCountryTimeZones; + private final List mTimeZoneIds; + + public FilteredCountryTimeZones(CountryTimeZones countryTimeZones) { + mCountryTimeZones = countryTimeZones; + List timeZoneIds = countryTimeZones.getTimeZoneMappings().stream() + .filter(timeZoneMapping -> timeZoneMapping.showInPicker) + .map(timeZoneMapping -> timeZoneMapping.timeZoneId) + .collect(Collectors.toList()); + mTimeZoneIds = Collections.unmodifiableList(timeZoneIds); + } + + public List getTimeZoneIds() { + return mTimeZoneIds; + } + + public CountryTimeZones getCountryTimeZones() { + return mCountryTimeZones; + } + + public String getRegionId() { + return TimeZoneData.normalizeRegionId(mCountryTimeZones.getCountryIso()); + } +} diff --git a/src/com/android/settings/datetime/timezone/model/TimeZoneData.java b/src/com/android/settings/datetime/timezone/model/TimeZoneData.java new file mode 100644 index 00000000000..a863bfc7ab2 --- /dev/null +++ b/src/com/android/settings/datetime/timezone/model/TimeZoneData.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2018 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.settings.datetime.timezone.model; + +import android.support.annotation.VisibleForTesting; + +import libcore.util.CountryTimeZones; +import libcore.util.CountryZonesFinder; +import libcore.util.TimeZoneFinder; + +import java.lang.ref.WeakReference; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Wrapper of CountryZonesFinder to normalize the country code and only show the regions that are + * has time zone shown in the time zone picker. + * The constructor reads the data from underlying file, and this means it should not be called + * from the UI thread. + */ +public class TimeZoneData { + + private static WeakReference sCache = null; + + private final CountryZonesFinder mCountryZonesFinder; + private final Set mRegionIds; + + public static synchronized TimeZoneData getInstance() { + TimeZoneData data = sCache == null ? null : sCache.get(); + if (data != null) { + return data; + } + data = new TimeZoneData(); + sCache = new WeakReference<>(data); + return data; + } + + public TimeZoneData() { + this(TimeZoneFinder.getInstance().getCountryZonesFinder()); + } + + @VisibleForTesting + TimeZoneData(CountryZonesFinder countryZonesFinder) { + mCountryZonesFinder = countryZonesFinder; + mRegionIds = getNormalizedRegionIds(mCountryZonesFinder.lookupAllCountryIsoCodes()); + } + + public Set getRegionIds() { + return mRegionIds; + } + + public Set lookupCountryCodesForZoneId(String tzId) { + if (tzId == null) { + return Collections.emptySet(); + } + return mCountryZonesFinder.lookupCountryTimeZonesForZoneId(tzId).stream() + .filter(countryTimeZones -> + countryTimeZones.getTimeZoneMappings().stream() + .anyMatch(mapping -> + mapping.timeZoneId.equals(tzId) && mapping.showInPicker)) + .map(countryTimeZones -> normalizeRegionId(countryTimeZones.getCountryIso())) + .collect(Collectors.toSet()); + } + + public FilteredCountryTimeZones lookupCountryTimeZones(String regionId) { + CountryTimeZones finder = regionId == null ? null + : mCountryZonesFinder.lookupCountryTimeZones(regionId); + return finder == null ? null : new FilteredCountryTimeZones(finder); + } + + private static Set getNormalizedRegionIds(List regionIds) { + final Set result = new HashSet<>(regionIds.size()); + for (String regionId : regionIds) { + result.add(normalizeRegionId(regionId)); + } + return Collections.unmodifiableSet(result); + } + + // Uppercase ASCII is normalized for the purpose of using ICU API + public static String normalizeRegionId(String regionId) { + return regionId == null ? null : regionId.toUpperCase(Locale.US); + } +} diff --git a/src/com/android/settings/datetime/timezone/model/TimeZoneDataLoader.java b/src/com/android/settings/datetime/timezone/model/TimeZoneDataLoader.java new file mode 100644 index 00000000000..89908d87f7b --- /dev/null +++ b/src/com/android/settings/datetime/timezone/model/TimeZoneDataLoader.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 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.settings.datetime.timezone.model; + +import android.app.LoaderManager; +import android.content.Context; +import android.content.Loader; +import android.os.Bundle; + +import com.android.settingslib.utils.AsyncLoader; + +public class TimeZoneDataLoader extends AsyncLoader { + + public TimeZoneDataLoader(Context context) { + super(context); + } + + @Override + public TimeZoneData loadInBackground() { + // Heavy operation due to reading the underlying file + return new TimeZoneData(); + } + + @Override + protected void onDiscardResult(TimeZoneData result) { + // This class doesn't hold resource of the result. + } + + public interface OnDataReadyCallback { + void onTimeZoneDataReady(TimeZoneData data); + } + + public static class LoaderCreator implements LoaderManager.LoaderCallbacks { + + private final Context mContext; + private final OnDataReadyCallback mCallback; + + public LoaderCreator(Context context, OnDataReadyCallback callback) { + mContext = context; + mCallback = callback; + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + return new TimeZoneDataLoader(mContext); + } + + @Override + public void onLoadFinished(Loader loader, TimeZoneData data) { + if (mCallback != null) { + mCallback.onTimeZoneDataReady(data); + } + } + + @Override + public void onLoaderReset(Loader loader) { + //It's okay to keep the time zone data when loader is reset + } + } +} diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/TimeZoneInfoTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/TimeZoneInfoTest.java new file mode 100644 index 00000000000..6042dd3489b --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/timezone/TimeZoneInfoTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2018 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.settings.datetime.timezone; + + +import com.android.settings.TestConfig; +import com.android.settings.datetime.timezone.TimeZoneInfo.Formatter; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +import java.util.Date; +import java.util.Locale; + +import static com.google.common.truth.Truth.assertThat; + + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class TimeZoneInfoTest { + + @Test + public void testFormat() { + Date now = new Date(0L); // 00:00 1/1/1970 + Formatter formatter = new Formatter(Locale.US, now); + + TimeZoneInfo timeZoneInfo = formatter.format("America/Los_Angeles"); + assertThat(timeZoneInfo.getId()).isEqualTo("America/Los_Angeles"); + assertThat(timeZoneInfo.getExemplarLocation()).isEqualTo("Los Angeles"); + assertThat(timeZoneInfo.getGmtOffset().toString()).isEqualTo("GMT-08:00"); + assertThat(timeZoneInfo.getGenericName()).isEqualTo("Pacific Time"); + assertThat(timeZoneInfo.getStandardName()).isEqualTo("Pacific Standard Time"); + assertThat(timeZoneInfo.getDaylightName()).isEqualTo("Pacific Daylight Time"); + } +} diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/model/TimeZoneDataTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/model/TimeZoneDataTest.java new file mode 100644 index 00000000000..274860405e1 --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/timezone/model/TimeZoneDataTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2018 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.settings.datetime.timezone.model; + +import com.android.settings.TestConfig; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import libcore.util.CountryTimeZones; +import libcore.util.CountryZonesFinder; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class TimeZoneDataTest { + + private CountryZonesFinder mCountryZonesFinder; + + @Before + public void setUp() throws Exception { + mCountryZonesFinder = mock(CountryZonesFinder.class); + when(mCountryZonesFinder.lookupAllCountryIsoCodes()).thenReturn(new ArrayList<>()); + } + + @Test + public void testRegionsWithTimeZone() { + TimeZoneData timeZoneData = new TimeZoneData(mCountryZonesFinder); + CountryTimeZones countryTimeZones = mock(CountryTimeZones.class); + when(countryTimeZones.getTimeZoneMappings()).thenReturn(Collections.emptyList()); + when(mCountryZonesFinder.lookupCountryTimeZones("US")).thenReturn(countryTimeZones); + assertThat(timeZoneData.lookupCountryTimeZones("US").getCountryTimeZones()) + .isSameAs(countryTimeZones); + } + + @Test + public void testGetRegionIds() { + when(mCountryZonesFinder.lookupAllCountryIsoCodes()).thenReturn(Arrays.asList()); + TimeZoneData timeZoneData = new TimeZoneData(mCountryZonesFinder); + assertThat(timeZoneData.getRegionIds()).isEmpty(); + + when(mCountryZonesFinder.lookupAllCountryIsoCodes()).thenReturn(Arrays.asList("us", "GB")); + timeZoneData = new TimeZoneData(mCountryZonesFinder); + assertThat(timeZoneData.getRegionIds()).containsExactly("US", "GB"); + } + + @Test + public void testLookupCountryCodesForZoneId() { + TimeZoneData timeZoneData = new TimeZoneData(mCountryZonesFinder); + assertThat(timeZoneData.lookupCountryCodesForZoneId(null)).isEmpty(); + CountryTimeZones US = mock(CountryTimeZones.class); + when(US.getCountryIso()).thenReturn("us"); + when(US.getTimeZoneMappings()).thenReturn(Arrays.asList( + new CountryTimeZones.TimeZoneMapping("Unknown/Secret_City", true), + new CountryTimeZones.TimeZoneMapping("Unknown/Secret_City2", false) + )); + CountryTimeZones GB = mock(CountryTimeZones.class); + when(GB.getCountryIso()).thenReturn("gb"); + when(GB.getTimeZoneMappings()).thenReturn(Arrays.asList( + new CountryTimeZones.TimeZoneMapping("Unknown/Secret_City", true) + )); + when(mCountryZonesFinder.lookupCountryTimeZonesForZoneId("Unknown/Secret_City")) + .thenReturn(Arrays.asList(US, GB)); + assertThat(timeZoneData.lookupCountryCodesForZoneId("Unknown/Secret_City")) + .containsExactly("US", "GB"); + assertThat(timeZoneData.lookupCountryCodesForZoneId("Unknown/Secret_City2")).isEmpty(); + } +} diff --git a/tests/robotests/src/libcore/util/CountryTimeZones.java b/tests/robotests/src/libcore/util/CountryTimeZones.java new file mode 100644 index 00000000000..2087848609d --- /dev/null +++ b/tests/robotests/src/libcore/util/CountryTimeZones.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 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 libcore.util; + +import java.util.List; + +/** + * Empty implementation of CountryTimeZones for Robolectric test. + */ +public class CountryTimeZones { + public CountryTimeZones() { + } + + public final static class TimeZoneMapping { + public final String timeZoneId; + public final boolean showInPicker; + + public TimeZoneMapping(String timeZoneId, boolean showInPicker) { + this.timeZoneId = timeZoneId; + this.showInPicker = showInPicker; + } + } + + public List getTimeZoneMappings() { + return null; + } + + public String getCountryIso() { + return null; + } +} diff --git a/tests/robotests/src/libcore/util/CountryZonesFinder.java b/tests/robotests/src/libcore/util/CountryZonesFinder.java new file mode 100644 index 00000000000..51149ecb915 --- /dev/null +++ b/tests/robotests/src/libcore/util/CountryZonesFinder.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2018 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 libcore.util; + +import java.util.List; + +/** + * Empty implementation of CountryZonesFinder for Robolectric test. + */ +public class CountryZonesFinder { + public CountryZonesFinder(List countryTimeZonesList) {} + + public List lookupAllCountryIsoCodes() { + return null; + } + + public List lookupCountryTimeZonesForZoneId(String zoneId) { + return null; + } + + public CountryTimeZones lookupCountryTimeZones(String countryIso) { + return null; + } +}