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.net.wifi.ScanResult; 24 import android.net.wifi.WifiManager; 25 import android.os.Bundle; 26 import android.os.Handler; 27 import android.os.Parcel; 28 import android.os.Parcelable; 29 import android.support.v17.leanback.widget.VerticalGridView; 30 import android.support.v7.util.SortedList; 31 import android.support.v7.widget.RecyclerView; 32 import android.support.v7.widget.util.SortedListAdapterCallback; 33 import android.view.LayoutInflater; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.view.ViewTreeObserver.OnPreDrawListener; 37 import android.view.inputmethod.InputMethodManager; 38 import android.widget.AdapterView; 39 import android.widget.FrameLayout; 40 import android.widget.ImageView; 41 import android.widget.TextView; 42 43 import com.android.tv.settings.R; 44 import com.android.tv.settings.connectivity.WifiSecurity; 45 import com.android.tv.settings.util.AccessibilityHelper; 46 47 import java.util.ArrayList; 48 import java.util.Comparator; 49 import java.util.List; 50 import java.util.TreeSet; 51 52 /** 53 * Displays a UI for selecting a wifi network from a list in the "wizard" style. 54 */ 55 public class SelectFromListWizardFragment extends Fragment { 56 57 public static class ListItemComparator implements Comparator<ListItem> { 58 @Override compare(ListItem o1, ListItem o2)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 ListItem(String name, int iconResource)114 public ListItem(String name, int iconResource) { 115 mName = name; 116 mIconResource = iconResource; 117 mIconLevel = 0; 118 mHasIconLevel = false; 119 mScanResult = null; 120 } 121 ListItem(ScanResult scanResult)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 getName()132 public String getName() { 133 return mName; 134 } 135 getIconResource()136 int getIconResource() { 137 return mIconResource; 138 } 139 getIconLevel()140 int getIconLevel() { 141 return mIconLevel; 142 } 143 hasIconLevel()144 boolean hasIconLevel() { 145 return mHasIconLevel; 146 } 147 getScanResult()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 */ getPinnedPosition()157 public int getPinnedPosition() { 158 return PinnedListItem.UNPINNED; 159 } 160 161 @Override toString()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 describeContents()185 public int describeContents() { 186 return 0; 187 } 188 189 @Override writeToParcel(Parcel dest, int flags)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 equals(Object o)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 PinnedListItem( String name, int iconResource, int pinnedPosition, int pinnedPriority)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 getPinnedPosition()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 */ getPinnedPriority()239 public int getPinnedPriority() { 240 return mPinnedPriority; 241 } 242 } 243 244 public interface Listener { onListSelectionComplete(ListItem listItem)245 void onListSelectionComplete(ListItem listItem); onListFocusChanged(ListItem listItem)246 void onListFocusChanged(ListItem listItem); 247 } 248 249 private static interface ActionListener { onClick(ListItem item)250 public void onClick(ListItem item); onFocus(ListItem item)251 public void onFocus(ListItem item); 252 } 253 254 private static class ListItemViewHolder extends RecyclerView.ViewHolder { ListItemViewHolder(View v)255 public ListItemViewHolder(View v) { 256 super(v); 257 } 258 init(ListItem item, View.OnClickListener onClick, View.OnFocusChangeListener onFocusChange)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 VerticalListAdapter(ActionListener actionListener, List<ListItem> choices)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 createClickListener(final ListItem item)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