1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.deskclock.data;
18 
19 import android.content.Context;
20 import android.content.SharedPreferences;
21 import android.content.res.Resources;
22 import android.content.res.TypedArray;
23 import androidx.annotation.VisibleForTesting;
24 import android.text.TextUtils;
25 import android.util.ArrayMap;
26 
27 import com.android.deskclock.R;
28 
29 import java.util.ArrayList;
30 import java.util.Collection;
31 import java.util.Collections;
32 import java.util.List;
33 import java.util.Locale;
34 import java.util.Map;
35 import java.util.TimeZone;
36 import java.util.regex.Matcher;
37 import java.util.regex.Pattern;
38 
39 /**
40  * This class encapsulates the transfer of data between {@link City} domain objects and their
41  * permanent storage in {@link Resources} and {@link SharedPreferences}.
42  */
43 final class CityDAO {
44 
45     /** Regex to match numeric index values when parsing city names. */
46     private static final Pattern NUMERIC_INDEX_REGEX = Pattern.compile("\\d+");
47 
48     /** Key to a preference that stores the number of selected cities. */
49     private static final String NUMBER_OF_CITIES = "number_of_cities";
50 
51     /** Prefix for a key to a preference that stores the id of a selected city. */
52     private static final String CITY_ID = "city_id_";
53 
CityDAO()54     private CityDAO() {}
55 
56     /**
57      * @param cityMap maps city ids to city instances
58      * @return the list of city ids selected for display by the user
59      */
getSelectedCities(SharedPreferences prefs, Map<String, City> cityMap)60     static List<City> getSelectedCities(SharedPreferences prefs, Map<String, City> cityMap) {
61         final int size = prefs.getInt(NUMBER_OF_CITIES, 0);
62         final List<City> selectedCities = new ArrayList<>(size);
63 
64         for (int i = 0; i < size; i++) {
65             final String id = prefs.getString(CITY_ID + i, null);
66             final City city = cityMap.get(id);
67             if (city != null) {
68                 selectedCities.add(city);
69             }
70         }
71 
72         return selectedCities;
73     }
74 
75     /**
76      * @param cities the collection of cities selected for display by the user
77      */
setSelectedCities(SharedPreferences prefs, Collection<City> cities)78     static void setSelectedCities(SharedPreferences prefs, Collection<City> cities) {
79         final SharedPreferences.Editor editor = prefs.edit();
80         editor.putInt(NUMBER_OF_CITIES, cities.size());
81 
82         int count = 0;
83         for (City city : cities) {
84             editor.putString(CITY_ID + count, city.getId());
85             count++;
86         }
87 
88         editor.apply();
89     }
90 
91     /**
92      * @return the domain of cities from which the user may choose a world clock
93      */
getCities(Context context)94     static Map<String, City> getCities(Context context) {
95         final Resources resources = context.getResources();
96         final TypedArray cityStrings = resources.obtainTypedArray(R.array.city_ids);
97         final int citiesCount = cityStrings.length();
98 
99         final Map<String, City> cities = new ArrayMap<>(citiesCount);
100         try {
101             for (int i = 0; i < citiesCount; ++i) {
102                 // Attempt to locate the resource id defining the city as a string.
103                 final int cityResourceId = cityStrings.getResourceId(i, 0);
104                 if (cityResourceId == 0) {
105                     final String message = String.format(Locale.ENGLISH,
106                             "Unable to locate city resource id for index %d", i);
107                     throw new IllegalStateException(message);
108                 }
109 
110                 final String id = resources.getResourceEntryName(cityResourceId);
111                 final String cityString = cityStrings.getString(i);
112                 if (cityString == null) {
113                     final String message = String.format("Unable to locate city with id %s", id);
114                     throw new IllegalStateException(message);
115                 }
116 
117                 // Attempt to parse the time zone from the city entry.
118                 final String[] cityParts = cityString.split("[|]");
119                 if (cityParts.length != 2) {
120                     final String message = String.format(
121                             "Error parsing malformed city %s", cityString);
122                     throw new IllegalStateException(message);
123                 }
124 
125                 final City city = createCity(id, cityParts[0], cityParts[1]);
126                 // Skip cities whose timezone cannot be resolved.
127                 if (city != null) {
128                     cities.put(id, city);
129                 }
130             }
131         } finally {
132             cityStrings.recycle();
133         }
134 
135         return Collections.unmodifiableMap(cities);
136     }
137 
138     /**
139      * @param id unique identifier for city
140      * @param formattedName "[index string]=[name]" or "[index string]=[name]:[phonetic name]",
141      *                      If [index string] is empty, use the first character of name as index,
142      *                      If phonetic name is empty, use the name itself as phonetic name.
143      * @param tzId the string id of the timezone a given city is located in
144      */
145     @VisibleForTesting
createCity(String id, String formattedName, String tzId)146     static City createCity(String id, String formattedName, String tzId) {
147         final TimeZone tz = TimeZone.getTimeZone(tzId);
148         // If the time zone lookup fails, GMT is returned. No cities actually map to GMT.
149         if ("GMT".equals(tz.getID())) {
150             return null;
151         }
152 
153         final String[] parts = formattedName.split("[=:]");
154         final String name = parts[1];
155         // Extract index string from input, use the first character of city name as the index string
156         // if one is not explicitly provided.
157         final String indexString = TextUtils.isEmpty(parts[0])
158                 ? name.substring(0, 1) : parts[0];
159         final String phoneticName = parts.length == 3 ? parts[2] : name;
160 
161         final Matcher matcher = NUMERIC_INDEX_REGEX.matcher(indexString);
162         final int index = matcher.find() ? Integer.parseInt(matcher.group()) : -1;
163 
164         return new City(id, index, indexString, name, phoneticName, tz);
165     }
166 }