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