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.BroadcastReceiver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.IntentFilter; 23 import android.content.SharedPreferences; 24 import android.content.SharedPreferences.OnSharedPreferenceChangeListener; 25 import android.preference.PreferenceManager; 26 27 import com.android.deskclock.R; 28 import com.android.deskclock.Utils; 29 import com.android.deskclock.data.DataModel.CitySort; 30 import com.android.deskclock.settings.SettingsActivity; 31 32 import java.util.ArrayList; 33 import java.util.Collection; 34 import java.util.Collections; 35 import java.util.Comparator; 36 import java.util.List; 37 import java.util.Map; 38 import java.util.Set; 39 import java.util.TimeZone; 40 41 /** 42 * All {@link City} data is accessed via this model. 43 */ 44 final class CityModel { 45 46 private final Context mContext; 47 48 /** The model from which settings are fetched. */ 49 private final SettingsModel mSettingsModel; 50 51 /** 52 * Retain a hard reference to the shared preference observer to prevent it from being garbage 53 * collected. See {@link SharedPreferences#registerOnSharedPreferenceChangeListener} for detail. 54 */ 55 private final OnSharedPreferenceChangeListener mPreferenceListener = new PreferenceListener(); 56 57 /** Clears data structures containing data that is locale-sensitive. */ 58 private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver(); 59 60 /** Maps city ID to city instance. */ 61 private Map<String, City> mCityMap; 62 63 /** List of city instances in display order. */ 64 private List<City> mAllCities; 65 66 /** List of selected city instances in display order. */ 67 private List<City> mSelectedCities; 68 69 /** List of unselected city instances in display order. */ 70 private List<City> mUnselectedCities; 71 72 /** A city instance representing the home timezone of the user. */ 73 private City mHomeCity; 74 75 CityModel(Context context, SettingsModel settingsModel) { 76 mContext = context; 77 mSettingsModel = settingsModel; 78 79 // Clear caches affected by locale when locale changes. 80 final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); 81 mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter); 82 83 // Clear caches affected by preferences when preferences change. 84 final SharedPreferences prefs = Utils.getDefaultSharedPreferences(mContext); 85 prefs.registerOnSharedPreferenceChangeListener(mPreferenceListener); 86 } 87 88 /** 89 * @return a list of all cities in their display order 90 */ 91 List<City> getAllCities() { 92 if (mAllCities == null) { 93 // Create a set of selections to identify the unselected cities. 94 final List<City> selected = new ArrayList<>(getSelectedCities()); 95 96 // Sort the selected cities alphabetically by name. 97 Collections.sort(selected, new City.NameComparator()); 98 99 // Combine selected and unselected cities into a single list. 100 final List<City> allCities = new ArrayList<>(getCityMap().size()); 101 allCities.addAll(selected); 102 allCities.addAll(getUnselectedCities()); 103 mAllCities = Collections.unmodifiableList(allCities); 104 } 105 106 return mAllCities; 107 } 108 109 /** 110 * @param cityName the case-insensitive city name to search for 111 * @return the city with the given {@code cityName}; {@code null} if no such city exists 112 */ 113 City getCity(String cityName) { 114 cityName = cityName.toUpperCase(); 115 116 for (City city : getAllCities()) { 117 if (cityName.equals(city.getNameUpperCase())) { 118 return city; 119 } 120 } 121 122 return null; 123 } 124 125 /** 126 * @return a city representing the user's home timezone 127 */ 128 City getHomeCity() { 129 if (mHomeCity == null) { 130 final String name = mContext.getString(R.string.home_label); 131 final TimeZone timeZone = mSettingsModel.getHomeTimeZone(); 132 mHomeCity = new City(null, -1, null, name, name, timeZone.getID()); 133 } 134 135 return mHomeCity; 136 } 137 138 /** 139 * @return a list of cities not selected for display 140 */ 141 List<City> getUnselectedCities() { 142 if (mUnselectedCities == null) { 143 // Create a set of selections to identify the unselected cities. 144 final List<City> selected = new ArrayList<>(getSelectedCities()); 145 final Set<City> selectedSet = Utils.newArraySet(selected); 146 147 final Collection<City> all = getCityMap().values(); 148 final List<City> unselected = new ArrayList<>(all.size() - selectedSet.size()); 149 for (City city : all) { 150 if (!selectedSet.contains(city)) { 151 unselected.add(city); 152 } 153 } 154 155 // Sort the unselected cities according by the user's preferred sort. 156 Collections.sort(unselected, getCitySortComparator()); 157 mUnselectedCities = Collections.unmodifiableList(unselected); 158 } 159 160 return mUnselectedCities; 161 } 162 163 /** 164 * @return a list of cities selected for display 165 */ 166 List<City> getSelectedCities() { 167 if (mSelectedCities == null) { 168 final List<City> selectedCities = CityDAO.getSelectedCities(mContext, getCityMap()); 169 Collections.sort(selectedCities, new City.UtcOffsetComparator()); 170 mSelectedCities = Collections.unmodifiableList(selectedCities); 171 } 172 173 return mSelectedCities; 174 } 175 176 /** 177 * @param cities the new collection of cities selected for display by the user 178 */ 179 void setSelectedCities(Collection<City> cities) { 180 CityDAO.setSelectedCities(mContext, cities); 181 182 // Clear caches affected by this update. 183 mAllCities = null; 184 mSelectedCities = null; 185 mUnselectedCities = null; 186 187 // Broadcast the change to the selected cities for the benefit of widgets. 188 sendCitiesChangedBroadcast(); 189 } 190 191 /** 192 * @return a comparator used to locate index positions 193 */ 194 Comparator<City> getCityIndexComparator() { 195 final CitySort citySort = mSettingsModel.getCitySort(); 196 switch (citySort) { 197 case NAME: return new City.NameIndexComparator(); 198 case UTC_OFFSET: return new City.UtcOffsetIndexComparator(); 199 } 200 throw new IllegalStateException("unexpected city sort: " + citySort); 201 } 202 203 /** 204 * @return the order in which cities are sorted 205 */ 206 CitySort getCitySort() { 207 return mSettingsModel.getCitySort(); 208 } 209 210 /** 211 * Adjust the order in which cities are sorted. 212 */ 213 void toggleCitySort() { 214 mSettingsModel.toggleCitySort(); 215 216 // Clear caches affected by this update. 217 mAllCities = null; 218 mUnselectedCities = null; 219 } 220 221 private Map<String, City> getCityMap() { 222 if (mCityMap == null) { 223 mCityMap = CityDAO.getCities(mContext); 224 } 225 226 return mCityMap; 227 } 228 229 private Comparator<City> getCitySortComparator() { 230 final CitySort citySort = mSettingsModel.getCitySort(); 231 switch (citySort) { 232 case NAME: return new City.NameComparator(); 233 case UTC_OFFSET: return new City.UtcOffsetComparator(); 234 } 235 throw new IllegalStateException("unexpected city sort: " + citySort); 236 } 237 238 private void sendCitiesChangedBroadcast() { 239 mContext.sendBroadcast(new Intent(DataModel.ACTION_DIGITAL_WIDGET_CHANGED)); 240 } 241 242 /** 243 * Cached information that is locale-sensitive must be cleared in response to locale changes. 244 */ 245 private final class LocaleChangedReceiver extends BroadcastReceiver { 246 @Override 247 public void onReceive(Context context, Intent intent) { 248 mCityMap = null; 249 mHomeCity = null; 250 mAllCities = null; 251 mSelectedCities = null; 252 mUnselectedCities = null; 253 } 254 } 255 256 /** 257 * This receiver is notified when shared preferences change. Cached information built on 258 * preferences must be cleared. 259 */ 260 private final class PreferenceListener implements OnSharedPreferenceChangeListener { 261 @Override 262 public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { 263 switch (key) { 264 case SettingsActivity.KEY_HOME_TZ: 265 mHomeCity = null; 266 case SettingsActivity.KEY_AUTO_HOME_CLOCK: 267 sendCitiesChangedBroadcast(); 268 break; 269 } 270 } 271 } 272 }