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
compare(ListItem o1, ListItem o2)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 
ListItem(String name, int iconResource)114         public ListItem(String name, int iconResource) {
115             mName = name;
116             mIconResource = iconResource;
117             mIconLevel = 0;
118             mHasIconLevel = false;
119             mScanResult = null;
120         }
121 
ListItem(ScanResult scanResult)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 
getName()132         public String getName() {
133             return mName;
134         }
135 
getIconResource()136         int getIconResource() {
137             return mIconResource;
138         }
139 
getIconLevel()140         int getIconLevel() {
141             return mIconLevel;
142         }
143 
hasIconLevel()144         boolean hasIconLevel() {
145             return mHasIconLevel;
146         }
147 
getScanResult()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          */
getPinnedPosition()157         public int getPinnedPosition() {
158             return PinnedListItem.UNPINNED;
159         }
160 
161         @Override
toString()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
describeContents()185         public int describeContents() {
186             return 0;
187         }
188 
189         @Override
writeToParcel(Parcel dest, int flags)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
equals(Object o)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 
PinnedListItem( String name, int iconResource, int pinnedPosition, int pinnedPriority)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
getPinnedPosition()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          */
getPinnedPriority()239         public int getPinnedPriority() {
240             return mPinnedPriority;
241         }
242     }
243 
244     public interface Listener {
onListSelectionComplete(ListItem listItem)245         void onListSelectionComplete(ListItem listItem);
onListFocusChanged(ListItem listItem)246         void onListFocusChanged(ListItem listItem);
247     }
248 
249     private static interface ActionListener {
onClick(ListItem item)250         public void onClick(ListItem item);
onFocus(ListItem item)251         public void onFocus(ListItem item);
252     }
253 
254     private static class ListItemViewHolder extends RecyclerView.ViewHolder {
ListItemViewHolder(View v)255         public ListItemViewHolder(View v) {
256             super(v);
257         }
258 
init(ListItem item, View.OnClickListener onClick, View.OnFocusChangeListener onFocusChange)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 
VerticalListAdapter(ActionListener actionListener, List<ListItem> choices)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 
createClickListener(final ListItem item)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