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