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