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