Home | History | Annotate | Download | only in datetime
      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