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.support.v17.leanback.widget.FacetProvider; 31 import android.support.v17.leanback.widget.ItemAlignmentFacet; 32 import android.support.v17.leanback.widget.ItemAlignmentFacet.ItemAlignmentDef; 33 import android.support.v17.leanback.widget.VerticalGridView; 34 import android.support.v7.util.SortedList; 35 import android.support.v7.widget.RecyclerView; 36 import android.support.v7.widget.util.SortedListAdapterCallback; 37 import android.text.TextUtils; 38 import android.util.DisplayMetrics; 39 import android.view.LayoutInflater; 40 import android.view.View; 41 import android.view.ViewGroup; 42 import android.view.ViewTreeObserver.OnPreDrawListener; 43 import android.view.inputmethod.InputMethodManager; 44 import android.widget.ImageView; 45 import android.widget.TextView; 46 47 import com.android.tv.settings.R; 48 import com.android.tv.settings.connectivity.WifiSecurity; 49 import com.android.tv.settings.util.AccessibilityHelper; 50 51 import java.util.ArrayList; 52 import java.util.Comparator; 53 import java.util.List; 54 import java.util.TreeSet; 55 56 /** 57 * Displays a UI for selecting a wifi network from a list in the "wizard" style. 58 */ 59 public class SelectFromListWizardFragment extends Fragment { 60 61 public static class ListItemComparator implements Comparator<ListItem> { 62 @Override compare(ListItem o1, ListItem o2)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 ListItem(String name, int iconResource)118 public ListItem(String name, int iconResource) { 119 mName = name; 120 mIconResource = iconResource; 121 mIconLevel = 0; 122 mHasIconLevel = false; 123 mScanResult = null; 124 } 125 ListItem(ScanResult scanResult)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 getName()136 public String getName() { 137 return mName; 138 } 139 getIconResource()140 int getIconResource() { 141 return mIconResource; 142 } 143 getIconLevel()144 int getIconLevel() { 145 return mIconLevel; 146 } 147 hasIconLevel()148 boolean hasIconLevel() { 149 return mHasIconLevel; 150 } 151 getScanResult()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 */ getPinnedPosition()161 public int getPinnedPosition() { 162 return PinnedListItem.UNPINNED; 163 } 164 165 @Override toString()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 describeContents()189 public int describeContents() { 190 return 0; 191 } 192 193 @Override writeToParcel(Parcel dest, int flags)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 equals(Object o)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 PinnedListItem( String name, int iconResource, int pinnedPosition, int pinnedPriority)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 getPinnedPosition()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 */ getPinnedPriority()244 public int getPinnedPriority() { 245 return mPinnedPriority; 246 } 247 } 248 249 public interface Listener { onListSelectionComplete(ListItem listItem)250 void onListSelectionComplete(ListItem listItem); onListFocusChanged(ListItem listItem)251 void onListFocusChanged(ListItem listItem); 252 } 253 254 private static interface ActionListener { onClick(ListItem item)255 public void onClick(ListItem item); onFocus(ListItem item)256 public void onFocus(ListItem item); 257 } 258 259 private static class ListItemViewHolder extends RecyclerView.ViewHolder implements 260 FacetProvider { ListItemViewHolder(View v)261 public ListItemViewHolder(View v) { 262 super(v); 263 } 264 init(ListItem item, View.OnClickListener onClick, View.OnFocusChangeListener onFocusChange)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 getFacet(Class facet)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 VerticalListAdapter(ActionListener actionListener, List<ListItem> choices)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 createClickListener(final ListItem item)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