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