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