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