Home | History | Annotate | Download | only in setup
      1 /*
      2  * Copyright (C) 2014 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.tv.settings.connectivity.setup;
     18 
     19 import android.app.Activity;
     20 import android.app.Fragment;
     21 import android.content.Context;
     22 import android.content.res.Resources;
     23 import android.net.wifi.ScanResult;
     24 import android.net.wifi.WifiManager;
     25 import android.os.Bundle;
     26 import android.os.Handler;
     27 import android.os.Parcel;
     28 import android.os.Parcelable;
     29 import android.support.v17.leanback.widget.VerticalGridView;
     30 import android.support.v7.util.SortedList;
     31 import android.support.v7.widget.RecyclerView;
     32 import android.support.v7.widget.util.SortedListAdapterCallback;
     33 import android.view.LayoutInflater;
     34 import android.view.View;
     35 import android.view.ViewGroup;
     36 import android.view.ViewTreeObserver.OnPreDrawListener;
     37 import android.view.inputmethod.InputMethodManager;
     38 import android.widget.AdapterView;
     39 import android.widget.FrameLayout;
     40 import android.widget.ImageView;
     41 import android.widget.TextView;
     42 
     43 import com.android.tv.settings.R;
     44 import com.android.tv.settings.connectivity.WifiSecurity;
     45 import com.android.tv.settings.util.AccessibilityHelper;
     46 
     47 import java.util.ArrayList;
     48 import java.util.Comparator;
     49 import java.util.List;
     50 import java.util.TreeSet;
     51 
     52 /**
     53  * Displays a UI for selecting a wifi network from a list in the "wizard" style.
     54  */
     55 public class SelectFromListWizardFragment extends Fragment {
     56 
     57     public static class ListItemComparator implements Comparator<ListItem> {
     58         @Override
     59         public int compare(ListItem o1, ListItem o2) {
     60             int pinnedPos1 = o1.getPinnedPosition();
     61             int pinnedPos2 = o2.getPinnedPosition();
     62 
     63             if (pinnedPos1 != PinnedListItem.UNPINNED && pinnedPos2 == PinnedListItem.UNPINNED) {
     64                 if (pinnedPos1 == PinnedListItem.FIRST) return -1;
     65                 if (pinnedPos1 == PinnedListItem.LAST) return 1;
     66             }
     67 
     68             if (pinnedPos1 == PinnedListItem.UNPINNED && pinnedPos2 != PinnedListItem.UNPINNED) {
     69                 if (pinnedPos2 == PinnedListItem.FIRST) return 1;
     70                 if (pinnedPos2 == PinnedListItem.LAST) return -1;
     71             }
     72 
     73             if (pinnedPos1 != PinnedListItem.UNPINNED && pinnedPos2 != PinnedListItem.UNPINNED) {
     74                 if (pinnedPos1 == pinnedPos2) {
     75                     PinnedListItem po1 = (PinnedListItem) o1;
     76                     PinnedListItem po2 = (PinnedListItem) o2;
     77                     return po1.getPinnedPriority() - po2.getPinnedPriority();
     78                 }
     79                 if (pinnedPos1 == PinnedListItem.LAST) return 1;
     80 
     81                 return -1;
     82             }
     83 
     84             ScanResult o1ScanResult = o1.getScanResult();
     85             ScanResult o2ScanResult = o2.getScanResult();
     86             if (o1ScanResult == null) {
     87                 if (o2ScanResult == null) {
     88                     return 0;
     89                 } else {
     90                     return 1;
     91                 }
     92             } else {
     93                 if (o2ScanResult == null) {
     94                     return -1;
     95                 } else {
     96                     int levelDiff = o2ScanResult.level - o1ScanResult.level;
     97                     if (levelDiff != 0) {
     98                         return levelDiff;
     99                     }
    100                     return o1ScanResult.SSID.compareTo(o2ScanResult.SSID);
    101                 }
    102             }
    103         }
    104     }
    105 
    106     public static class ListItem implements Parcelable {
    107 
    108         private final String mName;
    109         private final int mIconResource;
    110         private final int mIconLevel;
    111         private final boolean mHasIconLevel;
    112         private final ScanResult mScanResult;
    113 
    114         public ListItem(String name, int iconResource) {
    115             mName = name;
    116             mIconResource = iconResource;
    117             mIconLevel = 0;
    118             mHasIconLevel = false;
    119             mScanResult = null;
    120         }
    121 
    122         public ListItem(ScanResult scanResult) {
    123             mName = scanResult.SSID;
    124             mIconResource = WifiSecurity.NONE == WifiSecurity.getSecurity(scanResult)
    125                     ? R.drawable.setup_wifi_signal_open
    126                     : R.drawable.setup_wifi_signal_lock;
    127             mIconLevel = WifiManager.calculateSignalLevel(scanResult.level, 4);
    128             mHasIconLevel = true;
    129             mScanResult = scanResult;
    130         }
    131 
    132         public String getName() {
    133             return mName;
    134         }
    135 
    136         int getIconResource() {
    137             return mIconResource;
    138         }
    139 
    140         int getIconLevel() {
    141             return mIconLevel;
    142         }
    143 
    144         boolean hasIconLevel() {
    145             return mHasIconLevel;
    146         }
    147 
    148         ScanResult getScanResult() {
    149             return mScanResult;
    150         }
    151 
    152         /**
    153          * Returns whether this item is pinned to the front/back of a sorted list.  Returns
    154          * PinnedListItem.UNPINNED if the item is not pinned.
    155          * @return  the pinned/unpinned setting for this item.
    156          */
    157         public int getPinnedPosition() {
    158             return PinnedListItem.UNPINNED;
    159         }
    160 
    161         @Override
    162         public String toString() {
    163             return mName;
    164         }
    165 
    166         public static Parcelable.Creator<ListItem> CREATOR = new Parcelable.Creator<ListItem>() {
    167 
    168             @Override
    169             public ListItem createFromParcel(Parcel source) {
    170                 ScanResult scanResult = source.readParcelable(ScanResult.class.getClassLoader());
    171                 if (scanResult == null) {
    172                     return new ListItem(source.readString(), source.readInt());
    173                 } else {
    174                     return new ListItem(scanResult);
    175                 }
    176             }
    177 
    178             @Override
    179             public ListItem[] newArray(int size) {
    180                 return new ListItem[size];
    181             }
    182         };
    183 
    184         @Override
    185         public int describeContents() {
    186             return 0;
    187         }
    188 
    189         @Override
    190         public void writeToParcel(Parcel dest, int flags) {
    191             dest.writeParcelable(mScanResult, flags);
    192             if (mScanResult == null) {
    193                 dest.writeString(mName);
    194                 dest.writeInt(mIconResource);
    195             }
    196         }
    197 
    198         @Override
    199         public boolean equals(Object o) {
    200             if (o instanceof ListItem) {
    201                 ListItem li = (ListItem) o;
    202                 if (mScanResult == null && li.mScanResult == null) {
    203                     return mName.equals(li.mName);
    204                 }
    205                 return (mScanResult != null && li.mScanResult != null && mName.equals(li.mName) &&
    206                         WifiSecurity.getSecurity(mScanResult)
    207                         == WifiSecurity.getSecurity(li.mScanResult));
    208             }
    209             return false;
    210         }
    211     }
    212 
    213     public static class PinnedListItem extends ListItem {
    214         public static final int UNPINNED = 0;
    215         public static final int FIRST = 1;
    216         public static final int LAST = 2;
    217 
    218         private int mPinnedPosition;
    219         private int mPinnedPriority;
    220 
    221         public PinnedListItem(
    222                 String name, int iconResource, int pinnedPosition, int pinnedPriority) {
    223             super(name, iconResource);
    224             mPinnedPosition = pinnedPosition;
    225             mPinnedPriority = pinnedPriority;
    226         }
    227 
    228         @Override
    229         public int getPinnedPosition() {
    230             return mPinnedPosition;
    231         }
    232 
    233         /**
    234          * Returns the priority for this item, which is used for ordering the item between pinned
    235          * items in a sorted list.  For example, if two items are pinned to the front of the list
    236          * (FIRST), the priority value is used to determine their ordering.
    237          * @return  the sorting priority for this item
    238          */
    239         public int getPinnedPriority() {
    240             return mPinnedPriority;
    241         }
    242     }
    243 
    244     public interface Listener {
    245         void onListSelectionComplete(ListItem listItem);
    246         void onListFocusChanged(ListItem listItem);
    247     }
    248 
    249     private static interface ActionListener {
    250         public void onClick(ListItem item);
    251         public void onFocus(ListItem item);
    252     }
    253 
    254     private static class ListItemViewHolder extends RecyclerView.ViewHolder {
    255         public ListItemViewHolder(View v) {
    256             super(v);
    257         }
    258 
    259         public void init(ListItem item, View.OnClickListener onClick,
    260                 View.OnFocusChangeListener onFocusChange) {
    261             TextView title = (TextView) itemView.findViewById(R.id.list_item_text);
    262             title.setText(item.getName());
    263             itemView.setOnClickListener(onClick);
    264             itemView.setOnFocusChangeListener(onFocusChange);
    265 
    266             int iconResource = item.getIconResource();
    267             ImageView icon = (ImageView) itemView.findViewById(R.id.list_item_icon);
    268             // Set the icon if there is one.
    269             if (iconResource == 0) {
    270                 icon.setVisibility(View.GONE);
    271                 return;
    272             }
    273             icon.setVisibility(View.VISIBLE);
    274             icon.setImageResource(iconResource);
    275             if (item.hasIconLevel()) {
    276                 icon.setImageLevel(item.getIconLevel());
    277             }
    278         }
    279     }
    280 
    281     private class VerticalListAdapter extends RecyclerView.Adapter {
    282         private SortedList mItems;
    283         private final ActionListener mActionListener;
    284 
    285         public VerticalListAdapter(ActionListener actionListener, List<ListItem> choices) {
    286             super();
    287             mActionListener = actionListener;
    288             ListItemComparator comparator = new ListItemComparator();
    289             mItems = new SortedList<ListItem>(
    290                     ListItem.class, new SortedListAdapterCallback<ListItem>(this) {
    291                         @Override
    292                         public int compare(ListItem t0, ListItem t1) {
    293                             return comparator.compare(t0, t1);
    294                         }
    295 
    296                         @Override
    297                         public boolean areContentsTheSame(ListItem oldItem, ListItem newItem) {
    298                             return comparator.compare(oldItem, newItem) == 0;
    299                         }
    300 
    301                         @Override
    302                         public boolean areItemsTheSame(ListItem item1, ListItem item2) {
    303                             return item1.equals(item2);
    304                         }
    305                     });
    306             mItems.addAll(choices.toArray(new ListItem[0]), false);
    307         }
    308 
    309         private View.OnClickListener createClickListener(final ListItem item) {
    310             return new View.OnClickListener() {
    311                 @Override
    312                 public void onClick(View v) {
    313                     if (v == null || v.getWindowToken() == null || mActionListener == null) {
    314                         return;
    315                     }
    316                     mActionListener.onClick(item);
    317                 }
    318             };
    319         }
    320 
    321         private View.OnFocusChangeListener createFocusListener(final ListItem item) {
    322             return new View.OnFocusChangeListener() {
    323                 @Override
    324                 public void onFocusChange(View v, boolean hasFocus) {
    325                     if (v == null || v.getWindowToken() == null || mActionListener == null
    326                             || !hasFocus) {
    327                         return;
    328                     }
    329                     mActionListener.onFocus(item);
    330                 }
    331             };
    332         }
    333 
    334         @Override
    335         public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    336             LayoutInflater inflater = (LayoutInflater) parent.getContext().getSystemService(
    337                     Context.LAYOUT_INFLATER_SERVICE);
    338             View v = inflater.inflate(R.layout.setup_list_item, parent, false);
    339             return new ListItemViewHolder(v);
    340         }
    341 
    342         @Override
    343         public void onBindViewHolder(RecyclerView.ViewHolder baseHolder, int position) {
    344             if (position >= mItems.size()) {
    345                 return;
    346             }
    347 
    348             ListItemViewHolder viewHolder = (ListItemViewHolder) baseHolder;
    349             ListItem item = (ListItem) mItems.get(position);
    350             viewHolder.init((ListItem) item, createClickListener(item), createFocusListener(item));
    351         }
    352 
    353         public SortedList<ListItem> getItems() {
    354             return mItems;
    355         }
    356 
    357         @Override
    358         public int getItemCount() {
    359             return mItems.size();
    360         }
    361 
    362         public void updateItems(List<ListItem> inputItems) {
    363             TreeSet<ListItem> newItemSet = new TreeSet<ListItem>(new ListItemComparator());
    364             for (ListItem item: inputItems) {
    365                 newItemSet.add(item);
    366             }
    367             ArrayList<ListItem> toRemove = new ArrayList<ListItem>();
    368             for (int j = 0 ; j < mItems.size(); j++) {
    369                 ListItem oldItem = (ListItem) mItems.get(j);
    370                 if (!newItemSet.contains(oldItem)) {
    371                     toRemove.add(oldItem);
    372                 }
    373             }
    374             for (ListItem item: toRemove) {
    375                 mItems.remove(item);
    376             }
    377             mItems.addAll(inputItems.toArray(new ListItem[0]), true);
    378         }
    379     }
    380 
    381     private static final String EXTRA_TITLE = "title";
    382     private static final String EXTRA_DESCRIPTION = "description";
    383     private static final String EXTRA_LIST_ELEMENTS = "list_elements";
    384     private static final String EXTRA_LAST_SELECTION = "last_selection";
    385     private static final int SELECT_ITEM_DELAY = 100;
    386 
    387     public static SelectFromListWizardFragment newInstance(String title, String description,
    388             ArrayList<ListItem> listElements, ListItem lastSelection) {
    389         SelectFromListWizardFragment fragment = new SelectFromListWizardFragment();
    390         Bundle args = new Bundle();
    391         args.putString(EXTRA_TITLE, title);
    392         args.putString(EXTRA_DESCRIPTION, description);
    393         args.putParcelableArrayList(EXTRA_LIST_ELEMENTS, listElements);
    394         args.putParcelable(EXTRA_LAST_SELECTION, lastSelection);
    395         fragment.setArguments(args);
    396         return fragment;
    397     }
    398 
    399     private Handler mHandler;
    400     private View mMainView;
    401     private VerticalGridView mListView;
    402     private String mLastSelectedName;
    403     private OnPreDrawListener mOnListPreDrawListener;
    404     private Runnable mSelectItemRunnable;
    405 
    406     private void updateSelected(String lastSelectionName) {
    407         SortedList<ListItem> items = ((VerticalListAdapter) mListView.getAdapter()).getItems();
    408         for (int i = 0; i < items.size(); i++) {
    409             ListItem item = (ListItem) items.get(i);
    410             if (lastSelectionName.equals(item.getName())) {
    411                 mListView.setSelectedPosition(i);
    412                 break;
    413             }
    414         }
    415         mLastSelectedName = lastSelectionName;
    416     }
    417 
    418     public void update(List<ListItem> listElements) {
    419         // We want keep the highlight on the same selected item from before the update.  This is
    420         // currently not possible (b/28120126).  So we post a runnable to run after the update
    421         // completes.
    422         if (mSelectItemRunnable != null) {
    423             mHandler.removeCallbacks(mSelectItemRunnable);
    424         }
    425 
    426         final String lastSelected = mLastSelectedName;
    427         mSelectItemRunnable = () -> {
    428             updateSelected(lastSelected);
    429             if (mOnListPreDrawListener != null) {
    430                 mListView.getViewTreeObserver().removeOnPreDrawListener(mOnListPreDrawListener);
    431                 mOnListPreDrawListener = null;
    432             }
    433             mSelectItemRunnable = null;
    434         };
    435 
    436         if (mOnListPreDrawListener != null) {
    437             mListView.getViewTreeObserver().removeOnPreDrawListener(mOnListPreDrawListener);
    438         }
    439 
    440         mOnListPreDrawListener = () -> {
    441             mHandler.removeCallbacks(mSelectItemRunnable);
    442             // Pre-draw can be called multiple times per update.  We delay the runnable to select
    443             // the item so that it will only run after the last pre-draw of this batch of update.
    444             mHandler.postDelayed(mSelectItemRunnable, SELECT_ITEM_DELAY);
    445             return true;
    446         };
    447 
    448         mListView.getViewTreeObserver().addOnPreDrawListener(mOnListPreDrawListener);
    449         ((VerticalListAdapter) mListView.getAdapter()).updateItems(listElements);
    450     }
    451 
    452     @Override
    453     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle icicle) {
    454         Resources resources = getContext().getResources();
    455 
    456         mHandler = new Handler();
    457         mMainView = inflater.inflate(R.layout.account_content_area, container, false);
    458 
    459         final ViewGroup descriptionArea = (ViewGroup) mMainView.findViewById(R.id.description);
    460         final View content = inflater.inflate(R.layout.wifi_content, descriptionArea, false);
    461         descriptionArea.addView(content);
    462 
    463         final ViewGroup actionArea = (ViewGroup) mMainView.findViewById(R.id.action);
    464 
    465         TextView titleText = (TextView) content.findViewById(R.id.title_text);
    466         TextView descriptionText = (TextView) content.findViewById(R.id.description_text);
    467 
    468         Bundle args = getArguments();
    469         String title = args.getString(EXTRA_TITLE);
    470         String description = args.getString(EXTRA_DESCRIPTION);
    471 
    472         boolean forceFocusable = AccessibilityHelper.forceFocusableViews(getActivity());
    473         if (title != null) {
    474             titleText.setText(title);
    475             titleText.setVisibility(View.VISIBLE);
    476             if (forceFocusable) {
    477                 titleText.setFocusable(true);
    478                 titleText.setFocusableInTouchMode(true);
    479             }
    480         } else {
    481             titleText.setVisibility(View.GONE);
    482         }
    483 
    484         if (description != null) {
    485             descriptionText.setText(description);
    486             descriptionText.setVisibility(View.VISIBLE);
    487             if (forceFocusable) {
    488                 descriptionText.setFocusable(true);
    489                 descriptionText.setFocusableInTouchMode(true);
    490             }
    491         } else {
    492             descriptionText.setVisibility(View.GONE);
    493         }
    494 
    495         ArrayList<ListItem> listItems = args.getParcelableArrayList(EXTRA_LIST_ELEMENTS);
    496 
    497         mListView =
    498                 (VerticalGridView) inflater.inflate(R.layout.setup_list_view, actionArea, false);
    499         mListView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE);
    500         mListView.setWindowAlignmentOffsetPercent(
    501                 resources.getFloat(R.dimen.setup_scroll_list_window_offset_percent));
    502 
    503         actionArea.addView(mListView);
    504         ActionListener actionListener = new ActionListener() {
    505             @Override
    506             public void onClick(ListItem item) {
    507                 Activity a = getActivity();
    508                 if (a instanceof Listener && isResumed()) {
    509                     ((Listener) a).onListSelectionComplete(item);
    510                 }
    511             }
    512 
    513             @Override
    514             public void onFocus(ListItem item) {
    515                 Activity a = getActivity();
    516                 mLastSelectedName = item.getName();
    517                 if (a instanceof Listener) {
    518                     ((Listener) a).onListFocusChanged(item);
    519                 }
    520             }
    521         };
    522         mListView.setAdapter(new VerticalListAdapter(actionListener, listItems));
    523 
    524         ListItem lastSelection = args.getParcelable(EXTRA_LAST_SELECTION);
    525         if (lastSelection != null) {
    526             updateSelected(lastSelection.getName());
    527         }
    528         return mMainView;
    529     }
    530 
    531     @Override
    532     public void onPause() {
    533         super.onPause();
    534         if (mSelectItemRunnable != null) {
    535             mHandler.removeCallbacks(mSelectItemRunnable);
    536             mSelectItemRunnable = null;
    537         }
    538         if (mOnListPreDrawListener != null) {
    539             mListView.getViewTreeObserver().removeOnPreDrawListener(mOnListPreDrawListener);
    540             mOnListPreDrawListener = null;
    541         }
    542     }
    543 
    544     @Override
    545     public void onResume() {
    546         super.onResume();
    547         mHandler.post(new Runnable() {
    548             @Override
    549             public void run() {
    550                 InputMethodManager inputMethodManager = (InputMethodManager) getActivity()
    551                         .getSystemService(Context.INPUT_METHOD_SERVICE);
    552                 inputMethodManager.hideSoftInputFromWindow(
    553                         mMainView.getApplicationWindowToken(), 0);
    554             }
    555         });
    556     }
    557 }
    558