134142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux/* 234142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * Copyright (C) 2015 The Android Open Source Project 334142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * 434142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * Licensed under the Apache License, Version 2.0 (the "License"); 534142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * you may not use this file except in compliance with the License. 634142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * You may obtain a copy of the License at 734142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * 834142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * http://www.apache.org/licenses/LICENSE-2.0 934142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * 1034142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * Unless required by applicable law or agreed to in writing, software 1134142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * distributed under the License is distributed on an "AS IS" BASIS, 1234142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1334142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * See the License for the specific language governing permissions and 1434142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * limitations under the License. 1534142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux */ 1634142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 1734142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieuxpackage com.android.deskclock.data; 1834142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 1934142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieuximport android.content.Context; 2034142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieuximport android.content.SharedPreferences; 2134142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieuximport android.content.res.Resources; 226ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassenimport android.content.res.TypedArray; 2334142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieuximport android.support.annotation.VisibleForTesting; 24d7a9174aae6afbdee0209215756b72e2edcf49b9Fan Zhangimport android.text.TextUtils; 2534142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieuximport android.util.ArrayMap; 2634142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 2734142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieuximport com.android.deskclock.R; 2834142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 2934142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieuximport java.util.ArrayList; 3034142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieuximport java.util.Collection; 3134142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieuximport java.util.Collections; 3234142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieuximport java.util.List; 336ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassenimport java.util.Locale; 3434142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieuximport java.util.Map; 35603fe1de9633d2831042b23f3a86328f09db34f4James Lemieuximport java.util.TimeZone; 3634142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieuximport java.util.regex.Matcher; 3734142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieuximport java.util.regex.Pattern; 3834142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 3934142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux/** 4034142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * This class encapsulates the transfer of data between {@link City} domain objects and their 4134142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * permanent storage in {@link Resources} and {@link SharedPreferences}. 4234142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux */ 4334142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieuxfinal class CityDAO { 4434142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 4502e72bcfe62fce69715c1dbd8eea7826070c0660James Lemieux /** Regex to match numeric index values when parsing city names. */ 46603fe1de9633d2831042b23f3a86328f09db34f4James Lemieux private static final Pattern NUMERIC_INDEX_REGEX = Pattern.compile("\\d+"); 4734142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 4802e72bcfe62fce69715c1dbd8eea7826070c0660James Lemieux /** Key to a preference that stores the number of selected cities. */ 4934142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux private static final String NUMBER_OF_CITIES = "number_of_cities"; 5034142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 5102e72bcfe62fce69715c1dbd8eea7826070c0660James Lemieux /** Prefix for a key to a preference that stores the id of a selected city. */ 5234142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux private static final String CITY_ID = "city_id_"; 5334142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 5434142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux private CityDAO() {} 5534142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 5634142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux /** 5734142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * @param cityMap maps city ids to city instances 5834142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * @return the list of city ids selected for display by the user 5934142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux */ 60ff62e7fa903e3b6b11d0443543725c1351ab289dJames Lemieux static List<City> getSelectedCities(SharedPreferences prefs, Map<String, City> cityMap) { 6134142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux final int size = prefs.getInt(NUMBER_OF_CITIES, 0); 6234142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux final List<City> selectedCities = new ArrayList<>(size); 6334142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 6434142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux for (int i = 0; i < size; i++) { 6534142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux final String id = prefs.getString(CITY_ID + i, null); 6634142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux final City city = cityMap.get(id); 6734142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux if (city != null) { 6834142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux selectedCities.add(city); 6934142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux } 7034142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux } 7134142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 7234142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux return selectedCities; 7334142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux } 7434142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 7534142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux /** 7634142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * @param cities the collection of cities selected for display by the user 7734142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux */ 78ff62e7fa903e3b6b11d0443543725c1351ab289dJames Lemieux static void setSelectedCities(SharedPreferences prefs, Collection<City> cities) { 7934142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux final SharedPreferences.Editor editor = prefs.edit(); 8034142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux editor.putInt(NUMBER_OF_CITIES, cities.size()); 8134142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 8234142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux int count = 0; 8334142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux for (City city : cities) { 8434142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux editor.putString(CITY_ID + count, city.getId()); 8534142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux count++; 8634142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux } 8734142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 8834142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux editor.apply(); 8934142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux } 9034142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 9134142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux /** 9234142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * @return the domain of cities from which the user may choose a world clock 9334142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux */ 9402e72bcfe62fce69715c1dbd8eea7826070c0660James Lemieux static Map<String, City> getCities(Context context) { 9534142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux final Resources resources = context.getResources(); 966ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen final TypedArray cityStrings = resources.obtainTypedArray(R.array.city_ids); 97532f03a22d9e83bac6cba62e52b11a7ddb8ae2f5Justin Klaassen final int citiesCount = cityStrings.length(); 986ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen 996ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen final Map<String, City> cities = new ArrayMap<>(citiesCount); 1006ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen try { 1016ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen for (int i = 0; i < citiesCount; ++i) { 1026ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen // Attempt to locate the resource id defining the city as a string. 1036ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen final int cityResourceId = cityStrings.getResourceId(i, 0); 1046ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen if (cityResourceId == 0) { 1056ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen final String message = String.format(Locale.ENGLISH, 1066ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen "Unable to locate city resource id for index %d", i); 1076ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen throw new IllegalStateException(message); 1086ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen } 1096ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen 1106ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen final String id = resources.getResourceEntryName(cityResourceId); 1116ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen final String cityString = cityStrings.getString(i); 1126ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen if (cityString == null) { 1136ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen final String message = String.format("Unable to locate city with id %s", id); 1146ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen throw new IllegalStateException(message); 1156ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen } 1166ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen 1176ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen // Attempt to parse the time zone from the city entry. 1186ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen final String[] cityParts = cityString.split("[|]"); 1196ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen if (cityParts.length != 2) { 1206ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen final String message = String.format( 1216ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen "Error parsing malformed city %s", cityString); 1226ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen throw new IllegalStateException(message); 1236ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen } 1246ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen 12584b60fe3a95f07ee793d880ca754389159b4929eJames Lemieux final City city = createCity(id, cityParts[0], cityParts[1]); 126ce11825b7e71aea5f08e69a17d3ff9c279fc20a0Christine Franks // Skip cities whose timezone cannot be resolved. 127ce11825b7e71aea5f08e69a17d3ff9c279fc20a0Christine Franks if (city != null) { 128ce11825b7e71aea5f08e69a17d3ff9c279fc20a0Christine Franks cities.put(id, city); 1296ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen } 130603fe1de9633d2831042b23f3a86328f09db34f4James Lemieux } 1316ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen } finally { 1326ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen cityStrings.recycle(); 13334142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux } 1346ce9a7950fcbc55ab14bc48c264ecc48a5b06ef1Justin Klaassen 13534142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux return Collections.unmodifiableMap(cities); 13634142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux } 13734142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 13834142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux /** 13934142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux * @param id unique identifier for city 140d7a9174aae6afbdee0209215756b72e2edcf49b9Fan Zhang * @param formattedName "[index string]=[name]" or "[index string]=[name]:[phonetic name]", 141603fe1de9633d2831042b23f3a86328f09db34f4James Lemieux * If [index string] is empty, use the first character of name as index, 142603fe1de9633d2831042b23f3a86328f09db34f4James Lemieux * If phonetic name is empty, use the name itself as phonetic name. 143ce11825b7e71aea5f08e69a17d3ff9c279fc20a0Christine Franks * @param tzId the string id of the timezone a given city is located in 14434142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux */ 14534142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux @VisibleForTesting 146ce11825b7e71aea5f08e69a17d3ff9c279fc20a0Christine Franks static City createCity(String id, String formattedName, String tzId) { 147ce11825b7e71aea5f08e69a17d3ff9c279fc20a0Christine Franks final TimeZone tz = TimeZone.getTimeZone(tzId); 148ce11825b7e71aea5f08e69a17d3ff9c279fc20a0Christine Franks // If the time zone lookup fails, GMT is returned. No cities actually map to GMT. 149ce11825b7e71aea5f08e69a17d3ff9c279fc20a0Christine Franks if ("GMT".equals(tz.getID())) { 150ce11825b7e71aea5f08e69a17d3ff9c279fc20a0Christine Franks return null; 151ce11825b7e71aea5f08e69a17d3ff9c279fc20a0Christine Franks } 152ce11825b7e71aea5f08e69a17d3ff9c279fc20a0Christine Franks 15334142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux final String[] parts = formattedName.split("[=:]"); 15434142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux final String name = parts[1]; 155603fe1de9633d2831042b23f3a86328f09db34f4James Lemieux // Extract index string from input, use the first character of city name as the index string 156603fe1de9633d2831042b23f3a86328f09db34f4James Lemieux // if one is not explicitly provided. 157d7a9174aae6afbdee0209215756b72e2edcf49b9Fan Zhang final String indexString = TextUtils.isEmpty(parts[0]) 158603fe1de9633d2831042b23f3a86328f09db34f4James Lemieux ? name.substring(0, 1) : parts[0]; 15934142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux final String phoneticName = parts.length == 3 ? parts[2] : name; 16034142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 161603fe1de9633d2831042b23f3a86328f09db34f4James Lemieux final Matcher matcher = NUMERIC_INDEX_REGEX.matcher(indexString); 16234142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux final int index = matcher.find() ? Integer.parseInt(matcher.group()) : -1; 16334142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux 164603fe1de9633d2831042b23f3a86328f09db34f4James Lemieux return new City(id, index, indexString, name, phoneticName, tz); 16534142b1d0f2445bbd606bb490dfef6c078c630eaJames Lemieux } 16602e72bcfe62fce69715c1dbd8eea7826070c0660James Lemieux}