1 /* 2 * Copyright (C) 2006 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.settings; 18 19 import android.app.Activity; 20 import android.app.AlarmManager; 21 import android.app.ListFragment; 22 import android.content.Context; 23 import android.content.res.XmlResourceParser; 24 import android.os.Bundle; 25 import android.util.Log; 26 import android.view.LayoutInflater; 27 import android.view.Menu; 28 import android.view.MenuInflater; 29 import android.view.MenuItem; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.widget.ListView; 33 import android.widget.SimpleAdapter; 34 35 import org.xmlpull.v1.XmlPullParserException; 36 37 import java.text.SimpleDateFormat; 38 import java.util.ArrayList; 39 import java.util.Calendar; 40 import java.util.Collections; 41 import java.util.Comparator; 42 import java.util.Date; 43 import java.util.HashMap; 44 import java.util.HashSet; 45 import java.util.List; 46 import java.util.Locale; 47 import java.util.Map; 48 import java.util.TimeZone; 49 import libcore.icu.ICU; 50 import libcore.icu.TimeZoneNames; 51 52 /** 53 * The class displaying a list of time zones that match a filter string 54 * such as "Africa", "Europe", etc. Choosing an item from the list will set 55 * the time zone. Pressing Back without choosing from the list will not 56 * result in a change in the time zone setting. 57 */ 58 public class ZonePicker extends ListFragment { 59 private static final String TAG = "ZonePicker"; 60 61 public static interface ZoneSelectionListener { 62 // You can add any argument if you really need it... 63 public void onZoneSelected(TimeZone tz); 64 } 65 66 private static final String KEY_ID = "id"; // value: String 67 private static final String KEY_DISPLAYNAME = "name"; // value: String 68 private static final String KEY_GMT = "gmt"; // value: String 69 private static final String KEY_OFFSET = "offset"; // value: int (Integer) 70 private static final String XMLTAG_TIMEZONE = "timezone"; 71 72 private static final int HOURS_1 = 60 * 60000; 73 74 private static final int MENU_TIMEZONE = Menu.FIRST+1; 75 private static final int MENU_ALPHABETICAL = Menu.FIRST; 76 77 private boolean mSortedByTimezone; 78 79 private SimpleAdapter mTimezoneSortedAdapter; 80 private SimpleAdapter mAlphabeticalAdapter; 81 82 private ZoneSelectionListener mListener; 83 84 /** 85 * Constructs an adapter with TimeZone list. Sorted by TimeZone in default. 86 * 87 * @param sortedByName use Name for sorting the list. 88 */ 89 public static SimpleAdapter constructTimezoneAdapter(Context context, 90 boolean sortedByName) { 91 return constructTimezoneAdapter(context, sortedByName, 92 R.layout.date_time_setup_custom_list_item_2); 93 } 94 95 /** 96 * Constructs an adapter with TimeZone list. Sorted by TimeZone in default. 97 * 98 * @param sortedByName use Name for sorting the list. 99 */ 100 public static SimpleAdapter constructTimezoneAdapter(Context context, 101 boolean sortedByName, int layoutId) { 102 final String[] from = new String[] {KEY_DISPLAYNAME, KEY_GMT}; 103 final int[] to = new int[] {android.R.id.text1, android.R.id.text2}; 104 105 final String sortKey = (sortedByName ? KEY_DISPLAYNAME : KEY_OFFSET); 106 final MyComparator comparator = new MyComparator(sortKey); 107 ZoneGetter zoneGetter = new ZoneGetter(); 108 final List<HashMap<String, Object>> sortedList = zoneGetter.getZones(context); 109 Collections.sort(sortedList, comparator); 110 final SimpleAdapter adapter = new SimpleAdapter(context, 111 sortedList, 112 layoutId, 113 from, 114 to); 115 116 return adapter; 117 } 118 119 /** 120 * Searches {@link TimeZone} from the given {@link SimpleAdapter} object, and returns 121 * the index for the TimeZone. 122 * 123 * @param adapter SimpleAdapter constructed by 124 * {@link #constructTimezoneAdapter(Context, boolean)}. 125 * @param tz TimeZone to be searched. 126 * @return Index for the given TimeZone. -1 when there's no corresponding list item. 127 * returned. 128 */ 129 public static int getTimeZoneIndex(SimpleAdapter adapter, TimeZone tz) { 130 final String defaultId = tz.getID(); 131 final int listSize = adapter.getCount(); 132 for (int i = 0; i < listSize; i++) { 133 // Using HashMap<String, Object> induces unnecessary warning. 134 final HashMap<?,?> map = (HashMap<?,?>)adapter.getItem(i); 135 final String id = (String)map.get(KEY_ID); 136 if (defaultId.equals(id)) { 137 // If current timezone is in this list, move focus to it 138 return i; 139 } 140 } 141 return -1; 142 } 143 144 /** 145 * @param item one of items in adapters. The adapter should be constructed by 146 * {@link #constructTimezoneAdapter(Context, boolean)}. 147 * @return TimeZone object corresponding to the item. 148 */ 149 public static TimeZone obtainTimeZoneFromItem(Object item) { 150 return TimeZone.getTimeZone((String)((Map<?, ?>)item).get(KEY_ID)); 151 } 152 153 @Override 154 public void onActivityCreated(Bundle savedInstanceState) { 155 super.onActivityCreated(savedInstanceState); 156 157 final Activity activity = getActivity(); 158 mTimezoneSortedAdapter = constructTimezoneAdapter(activity, false); 159 mAlphabeticalAdapter = constructTimezoneAdapter(activity, true); 160 161 // Sets the adapter 162 setSorting(true); 163 setHasOptionsMenu(true); 164 } 165 166 @Override 167 public View onCreateView(LayoutInflater inflater, ViewGroup container, 168 Bundle savedInstanceState) { 169 final View view = super.onCreateView(inflater, container, savedInstanceState); 170 final ListView list = (ListView) view.findViewById(android.R.id.list); 171 Utils.forcePrepareCustomPreferencesList(container, view, list, false); 172 return view; 173 } 174 175 @Override 176 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 177 menu.add(0, MENU_ALPHABETICAL, 0, R.string.zone_list_menu_sort_alphabetically) 178 .setIcon(android.R.drawable.ic_menu_sort_alphabetically); 179 menu.add(0, MENU_TIMEZONE, 0, R.string.zone_list_menu_sort_by_timezone) 180 .setIcon(R.drawable.ic_menu_3d_globe); 181 super.onCreateOptionsMenu(menu, inflater); 182 } 183 184 @Override 185 public void onPrepareOptionsMenu(Menu menu) { 186 if (mSortedByTimezone) { 187 menu.findItem(MENU_TIMEZONE).setVisible(false); 188 menu.findItem(MENU_ALPHABETICAL).setVisible(true); 189 } else { 190 menu.findItem(MENU_TIMEZONE).setVisible(true); 191 menu.findItem(MENU_ALPHABETICAL).setVisible(false); 192 } 193 } 194 195 @Override 196 public boolean onOptionsItemSelected(MenuItem item) { 197 switch (item.getItemId()) { 198 199 case MENU_TIMEZONE: 200 setSorting(true); 201 return true; 202 203 case MENU_ALPHABETICAL: 204 setSorting(false); 205 return true; 206 207 default: 208 return false; 209 } 210 } 211 212 public void setZoneSelectionListener(ZoneSelectionListener listener) { 213 mListener = listener; 214 } 215 216 private void setSorting(boolean sortByTimezone) { 217 final SimpleAdapter adapter = 218 sortByTimezone ? mTimezoneSortedAdapter : mAlphabeticalAdapter; 219 setListAdapter(adapter); 220 mSortedByTimezone = sortByTimezone; 221 final int defaultIndex = getTimeZoneIndex(adapter, TimeZone.getDefault()); 222 if (defaultIndex >= 0) { 223 setSelection(defaultIndex); 224 } 225 } 226 227 static class ZoneGetter { 228 private final List<HashMap<String, Object>> mZones = 229 new ArrayList<HashMap<String, Object>>(); 230 private final HashSet<String> mLocalZones = new HashSet<String>(); 231 private final Date mNow = Calendar.getInstance().getTime(); 232 private final SimpleDateFormat mZoneNameFormatter = new SimpleDateFormat("zzzz"); 233 234 private List<HashMap<String, Object>> getZones(Context context) { 235 for (String olsonId : TimeZoneNames.forLocale(Locale.getDefault())) { 236 mLocalZones.add(olsonId); 237 } 238 try { 239 XmlResourceParser xrp = context.getResources().getXml(R.xml.timezones); 240 while (xrp.next() != XmlResourceParser.START_TAG) { 241 continue; 242 } 243 xrp.next(); 244 while (xrp.getEventType() != XmlResourceParser.END_TAG) { 245 while (xrp.getEventType() != XmlResourceParser.START_TAG) { 246 if (xrp.getEventType() == XmlResourceParser.END_DOCUMENT) { 247 return mZones; 248 } 249 xrp.next(); 250 } 251 if (xrp.getName().equals(XMLTAG_TIMEZONE)) { 252 String olsonId = xrp.getAttributeValue(0); 253 addTimeZone(olsonId); 254 } 255 while (xrp.getEventType() != XmlResourceParser.END_TAG) { 256 xrp.next(); 257 } 258 xrp.next(); 259 } 260 xrp.close(); 261 } catch (XmlPullParserException xppe) { 262 Log.e(TAG, "Ill-formatted timezones.xml file"); 263 } catch (java.io.IOException ioe) { 264 Log.e(TAG, "Unable to read timezones.xml file"); 265 } 266 return mZones; 267 } 268 269 private void addTimeZone(String olsonId) { 270 // We always need the "GMT-07:00" string. 271 final TimeZone tz = TimeZone.getTimeZone(olsonId); 272 273 // For the display name, we treat time zones within the country differently 274 // from other countries' time zones. So in en_US you'd get "Pacific Daylight Time" 275 // but in de_DE you'd get "Los Angeles" for the same time zone. 276 String displayName; 277 if (mLocalZones.contains(olsonId)) { 278 // Within a country, we just use the local name for the time zone. 279 mZoneNameFormatter.setTimeZone(tz); 280 displayName = mZoneNameFormatter.format(mNow); 281 } else { 282 // For other countries' time zones, we use the exemplar location. 283 final String localeName = Locale.getDefault().toString(); 284 displayName = TimeZoneNames.getExemplarLocation(localeName, olsonId); 285 } 286 287 final HashMap<String, Object> map = new HashMap<String, Object>(); 288 map.put(KEY_ID, olsonId); 289 map.put(KEY_DISPLAYNAME, displayName); 290 map.put(KEY_GMT, DateTimeSettings.getTimeZoneText(tz, false)); 291 map.put(KEY_OFFSET, tz.getOffset(mNow.getTime())); 292 293 mZones.add(map); 294 } 295 } 296 297 @Override 298 public void onListItemClick(ListView listView, View v, int position, long id) { 299 // Ignore extra clicks 300 if (!isResumed()) return; 301 final Map<?, ?> map = (Map<?, ?>)listView.getItemAtPosition(position); 302 final String tzId = (String) map.get(KEY_ID); 303 304 // Update the system timezone value 305 final Activity activity = getActivity(); 306 final AlarmManager alarm = (AlarmManager) activity.getSystemService(Context.ALARM_SERVICE); 307 alarm.setTimeZone(tzId); 308 final TimeZone tz = TimeZone.getTimeZone(tzId); 309 if (mListener != null) { 310 mListener.onZoneSelected(tz); 311 } else { 312 getActivity().onBackPressed(); 313 } 314 } 315 316 private static class MyComparator implements Comparator<HashMap<?, ?>> { 317 private String mSortingKey; 318 319 public MyComparator(String sortingKey) { 320 mSortingKey = sortingKey; 321 } 322 323 public void setSortingKey(String sortingKey) { 324 mSortingKey = sortingKey; 325 } 326 327 public int compare(HashMap<?, ?> map1, HashMap<?, ?> map2) { 328 Object value1 = map1.get(mSortingKey); 329 Object value2 = map2.get(mSortingKey); 330 331 /* 332 * This should never happen, but just in-case, put non-comparable 333 * items at the end. 334 */ 335 if (!isComparable(value1)) { 336 return isComparable(value2) ? 1 : 0; 337 } else if (!isComparable(value2)) { 338 return -1; 339 } 340 341 return ((Comparable) value1).compareTo(value2); 342 } 343 344 private boolean isComparable(Object value) { 345 return (value != null) && (value instanceof Comparable); 346 } 347 } 348 } 349