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
17package com.android.deskclock.data;
18
19import android.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.content.SharedPreferences;
24import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
25import android.preference.PreferenceManager;
26
27import com.android.deskclock.R;
28import com.android.deskclock.Utils;
29import com.android.deskclock.data.DataModel.CitySort;
30import com.android.deskclock.settings.SettingsActivity;
31
32import java.util.ArrayList;
33import java.util.Collection;
34import java.util.Collections;
35import java.util.Comparator;
36import java.util.List;
37import java.util.Map;
38import java.util.Set;
39import java.util.TimeZone;
40
41/**
42 * All {@link City} data is accessed via this model.
43 */
44final 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}