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