Home | History | Annotate | Download | only in data
      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