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