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}