1/*
2 * Copyright (C) 2017 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.datetime;
18
19import android.annotation.NonNull;
20import android.app.Activity;
21import android.app.AlarmManager;
22import android.app.ListFragment;
23import android.content.Context;
24import android.os.Bundle;
25import android.support.annotation.VisibleForTesting;
26import android.view.LayoutInflater;
27import android.view.Menu;
28import android.view.MenuInflater;
29import android.view.MenuItem;
30import android.view.View;
31import android.view.ViewGroup;
32import android.widget.ListView;
33import android.widget.SimpleAdapter;
34import android.widget.TextView;
35
36import com.android.internal.logging.nano.MetricsProto;
37import com.android.settings.R;
38import com.android.settings.overlay.FeatureFactory;
39import com.android.settingslib.core.instrumentation.Instrumentable;
40import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin;
41import com.android.settingslib.datetime.ZoneGetter;
42
43import java.text.Collator;
44import java.util.Collections;
45import java.util.Comparator;
46import java.util.HashMap;
47import java.util.List;
48import java.util.Map;
49import java.util.TimeZone;
50
51/**
52 * The class displaying a list of time zones that match a filter string
53 * such as "Africa", "Europe", etc. Choosing an item from the list will set
54 * the time zone. Pressing Back without choosing from the list will not
55 * result in a change in the time zone setting.
56 */
57public class ZonePicker extends ListFragment implements Instrumentable {
58
59    private static final int MENU_TIMEZONE = Menu.FIRST+1;
60    private static final int MENU_ALPHABETICAL = Menu.FIRST;
61    private VisibilityLoggerMixin mVisibilityLoggerMixin;
62
63    private boolean mSortedByTimezone;
64
65    private SimpleAdapter mTimezoneSortedAdapter;
66    private SimpleAdapter mAlphabeticalAdapter;
67
68    /**
69     * Constructs an adapter with TimeZone list. Sorted by TimeZone in default.
70     *
71     * @param sortedByName use Name for sorting the list.
72     */
73    public static SimpleAdapter constructTimezoneAdapter(Context context,
74            boolean sortedByName) {
75        return constructTimezoneAdapter(context, sortedByName,
76                R.layout.date_time_custom_list_item_2);
77    }
78
79    /**
80     * Constructs an adapter with TimeZone list. Sorted by TimeZone in default.
81     *
82     * @param sortedByName use Name for sorting the list.
83     */
84    public static SimpleAdapter constructTimezoneAdapter(Context context,
85            boolean sortedByName, int layoutId) {
86        final String[] from = new String[] {
87                ZoneGetter.KEY_DISPLAY_LABEL,
88                ZoneGetter.KEY_OFFSET_LABEL
89        };
90        final int[] to = new int[] {android.R.id.text1, android.R.id.text2};
91
92        final String sortKey = (sortedByName
93                ? ZoneGetter.KEY_DISPLAY_LABEL
94                : ZoneGetter.KEY_OFFSET);
95        final MyComparator comparator = new MyComparator(sortKey);
96        final List<Map<String, Object>> sortedList = ZoneGetter.getZonesList(context);
97        Collections.sort(sortedList, comparator);
98        final SimpleAdapter adapter = new SimpleAdapter(context,
99                sortedList,
100                layoutId,
101                from,
102                to);
103        adapter.setViewBinder(new TimeZoneViewBinder());
104        return adapter;
105    }
106
107    private static class TimeZoneViewBinder implements SimpleAdapter.ViewBinder {
108
109        /**
110         * Set the text to the given {@link CharSequence} as is, instead of calling toString, so
111         * that additional information stored in the CharSequence is, like spans added to a
112         * {@link android.text.SpannableString} are preserved.
113         */
114        @Override
115        public boolean setViewValue(View view, Object data, String textRepresentation) {
116            TextView textView = (TextView) view;
117            textView.setText((CharSequence) data);
118            return true;
119        }
120    }
121
122    /**
123     * Searches {@link TimeZone} from the given {@link SimpleAdapter} object, and returns
124     * the index for the TimeZone.
125     *
126     * @param adapter SimpleAdapter constructed by
127     * {@link #constructTimezoneAdapter(Context, boolean)}.
128     * @param tz TimeZone to be searched.
129     * @return Index for the given TimeZone. -1 when there's no corresponding list item.
130     * returned.
131     */
132    public static int getTimeZoneIndex(SimpleAdapter adapter, TimeZone tz) {
133        final String defaultId = tz.getID();
134        final int listSize = adapter.getCount();
135        for (int i = 0; i < listSize; i++) {
136            // Using HashMap<String, Object> induces unnecessary warning.
137            final HashMap<?,?> map = (HashMap<?,?>)adapter.getItem(i);
138            final String id = (String)map.get(ZoneGetter.KEY_ID);
139            if (defaultId.equals(id)) {
140                // If current timezone is in this list, move focus to it
141                return i;
142            }
143        }
144        return -1;
145    }
146
147    @Override
148    public int getMetricsCategory() {
149        return MetricsProto.MetricsEvent.ZONE_PICKER;
150    }
151
152    @Override
153    public void onActivityCreated(Bundle savedInstanceState) {
154        super.onActivityCreated(savedInstanceState);
155
156        final Activity activity = getActivity();
157        mTimezoneSortedAdapter = constructTimezoneAdapter(activity, false);
158        mAlphabeticalAdapter = constructTimezoneAdapter(activity, true);
159
160        // Sets the adapter
161        setSorting(true);
162        setHasOptionsMenu(true);
163        activity.setTitle(R.string.date_time_set_timezone);
164    }
165
166    @Override
167    public void onCreate(Bundle savedInstanceState) {
168        super.onCreate(savedInstanceState);
169        mVisibilityLoggerMixin = new VisibilityLoggerMixin(getMetricsCategory(),
170            FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider());
171    }
172
173    @Override
174    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
175            Bundle savedInstanceState) {
176        final View view = super.onCreateView(inflater, container, savedInstanceState);
177        final ListView list = view.findViewById(android.R.id.list);
178        prepareCustomPreferencesList(list);
179        return view;
180    }
181
182    @Override
183    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
184        menu.add(0, MENU_ALPHABETICAL, 0, R.string.zone_list_menu_sort_alphabetically)
185            .setIcon(android.R.drawable.ic_menu_sort_alphabetically);
186        menu.add(0, MENU_TIMEZONE, 0, R.string.zone_list_menu_sort_by_timezone)
187            .setIcon(R.drawable.ic_menu_3d_globe);
188        super.onCreateOptionsMenu(menu, inflater);
189    }
190
191    @Override
192    public void onPrepareOptionsMenu(Menu menu) {
193        if (mSortedByTimezone) {
194            menu.findItem(MENU_TIMEZONE).setVisible(false);
195            menu.findItem(MENU_ALPHABETICAL).setVisible(true);
196        } else {
197            menu.findItem(MENU_TIMEZONE).setVisible(true);
198            menu.findItem(MENU_ALPHABETICAL).setVisible(false);
199        }
200    }
201
202    @Override
203    public void onResume() {
204        super.onResume();
205        mVisibilityLoggerMixin.onResume();
206    }
207
208    @Override
209    public boolean onOptionsItemSelected(MenuItem item) {
210        switch (item.getItemId()) {
211
212            case MENU_TIMEZONE:
213                setSorting(true);
214                return true;
215
216            case MENU_ALPHABETICAL:
217                setSorting(false);
218                return true;
219
220            default:
221                return false;
222        }
223    }
224
225    static void prepareCustomPreferencesList(ListView list) {
226        list.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
227        list.setClipToPadding(false);
228        list.setDivider(null);
229    }
230
231    private void setSorting(boolean sortByTimezone) {
232        final SimpleAdapter adapter =
233                sortByTimezone ? mTimezoneSortedAdapter : mAlphabeticalAdapter;
234        setListAdapter(adapter);
235        mSortedByTimezone = sortByTimezone;
236        final int defaultIndex = getTimeZoneIndex(adapter, TimeZone.getDefault());
237        if (defaultIndex >= 0) {
238            setSelection(defaultIndex);
239        }
240    }
241
242    @Override
243    public void onListItemClick(ListView listView, View v, int position, long id) {
244        // Ignore extra clicks
245        if (!isResumed()) return;
246        final Map<?, ?> map = (Map<?, ?>)listView.getItemAtPosition(position);
247        final String tzId = (String) map.get(ZoneGetter.KEY_ID);
248
249        // Update the system timezone value
250        final Activity activity = getActivity();
251        final AlarmManager alarm = (AlarmManager) activity.getSystemService(Context.ALARM_SERVICE);
252        alarm.setTimeZone(tzId);
253
254        getActivity().onBackPressed();
255
256    }
257
258    @Override
259    public void onPause() {
260        super.onPause();
261        mVisibilityLoggerMixin.onPause();
262    }
263
264    @VisibleForTesting
265    static class MyComparator implements Comparator<Map<?, ?>> {
266        private final Collator mCollator;
267        private String mSortingKey;
268        private boolean mSortedByName;
269
270        public MyComparator(String sortingKey) {
271            mCollator = Collator.getInstance();
272            mSortingKey = sortingKey;
273            mSortedByName = ZoneGetter.KEY_DISPLAY_LABEL.equals(sortingKey);
274        }
275
276        public void setSortingKey(String sortingKey) {
277            mSortingKey = sortingKey;
278            mSortedByName = ZoneGetter.KEY_DISPLAY_LABEL.equals(sortingKey);
279        }
280
281        public int compare(Map<?, ?> map1, Map<?, ?> map2) {
282            Object value1 = map1.get(mSortingKey);
283            Object value2 = map2.get(mSortingKey);
284
285            /*
286             * This should never happen, but just in-case, put non-comparable
287             * items at the end.
288             */
289            if (!isComparable(value1)) {
290                return isComparable(value2) ? 1 : 0;
291            } else if (!isComparable(value2)) {
292                return -1;
293            }
294
295            if (mSortedByName) {
296                return mCollator.compare(value1, value2);
297            } else {
298                return ((Comparable) value1).compareTo(value2);
299            }
300        }
301
302        private boolean isComparable(Object value) {
303            return (value != null) && (value instanceof Comparable);
304        }
305    }
306}
307