• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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 android.text.TextUtils
24 import android.util.ArrayMap
25 import androidx.annotation.VisibleForTesting
26 
27 import com.android.deskclock.R
28 
29 import java.util.Locale
30 import java.util.regex.Pattern
31 import java.util.TimeZone
32 
33 /**
34  * This class encapsulates the transfer of data between [City] domain objects and their
35  * permanent storage in [Resources] and [SharedPreferences].
36  */
37 internal object CityDAO {
38     /** Regex to match numeric index values when parsing city names.  */
39     private val NUMERIC_INDEX_REGEX = Pattern.compile("\\d+")
40 
41     /** Key to a preference that stores the number of selected cities.  */
42     private const val NUMBER_OF_CITIES = "number_of_cities"
43 
44     /** Prefix for a key to a preference that stores the id of a selected city.  */
45     private const val CITY_ID = "city_id_"
46 
47     /**
48      * @param cityMap maps city ids to city instances
49      * @return the list of city ids selected for display by the user
50      */
getSelectedCitiesnull51     fun getSelectedCities(prefs: SharedPreferences, cityMap: Map<String, City>): List<City> {
52         val size: Int = prefs.getInt(NUMBER_OF_CITIES, 0)
53         val selectedCities: MutableList<City> = ArrayList(size)
54 
55         for (i in 0 until size) {
56             val id: String? = prefs.getString(CITY_ID + i, null)
57             val city = cityMap[id]
58             if (city != null) {
59                 selectedCities.add(city)
60             }
61         }
62 
63         return selectedCities
64     }
65 
66     /**
67      * @param cities the collection of cities selected for display by the user
68      */
setSelectedCitiesnull69     fun setSelectedCities(prefs: SharedPreferences, cities: Collection<City>) {
70         val editor: SharedPreferences.Editor = prefs.edit()
71         editor.putInt(NUMBER_OF_CITIES, cities.size)
72 
73         for ((count, city) in cities.withIndex()) {
74             editor.putString(CITY_ID + count, city.id)
75         }
76 
77         editor.apply()
78     }
79 
80     /**
81      * @return the domain of cities from which the user may choose a world clock
82      */
getCitiesnull83     fun getCities(context: Context): Map<String, City> {
84         val resources: Resources = context.getResources()
85         val cityStrings: TypedArray = resources.obtainTypedArray(R.array.city_ids)
86         val citiesCount: Int = cityStrings.length()
87 
88         val cities: MutableMap<String, City> = ArrayMap(citiesCount)
89         try {
90             for (i in 0 until citiesCount) {
91                 // Attempt to locate the resource id defining the city as a string.
92                 val cityResourceId: Int = cityStrings.getResourceId(i, 0)
93                 if (cityResourceId == 0) {
94                     val message = String.format(Locale.ENGLISH,
95                             "Unable to locate city resource id for index %d", i)
96                     throw IllegalStateException(message)
97                 }
98 
99                 val id: String = resources.getResourceEntryName(cityResourceId)
100                 val cityString: String? = cityStrings.getString(i)
101                 if (cityString == null) {
102                     val message = String.format("Unable to locate city with id %s", id)
103                     throw IllegalStateException(message)
104                 }
105 
106                 // Attempt to parse the time zone from the city entry.
107                 val cityParts = cityString.split("[|]".toRegex()).toTypedArray()
108                 if (cityParts.size != 2) {
109                     val message = String.format(
110                             "Error parsing malformed city %s", cityString)
111                     throw IllegalStateException(message)
112                 }
113 
114                 val city = createCity(id, cityParts[0], cityParts[1])
115                 // Skip cities whose timezone cannot be resolved.
116                 if (city != null) {
117                     cities[id] = city
118                 }
119             }
120         } finally {
121             cityStrings.recycle()
122         }
123 
124         return cities
125     }
126 
127     /**
128      * @param id unique identifier for city
129      * @param formattedName "[index string]=[name]" or "[index string]=[name]:[phonetic name]",
130      * If [index string] is empty, use the first character of name as index,
131      * If phonetic name is empty, use the name itself as phonetic name.
132      * @param tzId the string id of the timezone a given city is located in
133      */
134     @VisibleForTesting
createCitynull135     fun createCity(id: String?, formattedName: String, tzId: String?): City? {
136         val tz = TimeZone.getTimeZone(tzId)
137         // If the time zone lookup fails, GMT is returned. No cities actually map to GMT.
138         if ("GMT" == tz.id) {
139             return null
140         }
141 
142         val parts = formattedName.split("[=:]".toRegex()).toTypedArray()
143         val name = parts[1]
144         // Extract index string from input, use the first character of city name as the index string
145         // if one is not explicitly provided.
146         val indexString = if (TextUtils.isEmpty(parts[0])) name.substring(0, 1) else parts[0]
147         val phoneticName = if (parts.size == 3) parts[2] else name
148 
149         val matcher = NUMERIC_INDEX_REGEX.matcher(indexString)
150         val index = if (matcher.find()) matcher.group().toInt() else -1
151 
152         return City(id, index, indexString, name, phoneticName, tz)
153     }
154 }