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 }