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