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 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 }