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