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 
CityModel(Context context, SharedPreferences prefs, SettingsModel settingsModel)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 
addCityListener(CityListener cityListener)94     void addCityListener(CityListener cityListener) {
95         mCityListeners.add(cityListener);
96     }
97 
removeCityListener(CityListener cityListener)98     void removeCityListener(CityListener cityListener) {
99         mCityListeners.remove(cityListener);
100     }
101 
102     /**
103      * @return a list of all cities in their display order
104      */
getAllCities()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      */
getHomeCity()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      */
getUnselectedCities()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      */
getSelectedCities()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      */
setSelectedCities(Collection<City> cities)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      */
getCityIndexComparator()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      */
getCitySort()205     CitySort getCitySort() {
206         return mSettingsModel.getCitySort();
207     }
208 
209     /**
210      * Adjust the order in which cities are sorted.
211      */
toggleCitySort()212     void toggleCitySort() {
213         mSettingsModel.toggleCitySort();
214 
215         // Clear caches affected by this update.
216         mAllCities = null;
217         mUnselectedCities = null;
218     }
219 
getCityMap()220     private Map<String, City> getCityMap() {
221         if (mCityMap == null) {
222             mCityMap = CityDAO.getCities(mContext);
223         }
224 
225         return mCityMap;
226     }
227 
getCitySortComparator()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 
fireCitiesChanged(List<City> oldCities, List<City> newCities)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
onReceive(Context context, Intent intent)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
onSharedPreferenceChanged(SharedPreferences prefs, String key)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