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