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.support.annotation.VisibleForTesting; 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 import android.widget.TextView; 35 36 import com.android.internal.logging.nano.MetricsProto; 37 import com.android.settings.R; 38 import com.android.settings.overlay.FeatureFactory; 39 import com.android.settingslib.core.instrumentation.Instrumentable; 40 import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin; 41 import com.android.settingslib.datetime.ZoneGetter; 42 43 import java.text.Collator; 44 import java.util.Collections; 45 import java.util.Comparator; 46 import java.util.HashMap; 47 import java.util.List; 48 import java.util.Map; 49 import 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 */ 57 public 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