1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.systemui.qs.customize; 16 17 import android.content.ComponentName; 18 import android.content.Context; 19 import android.content.res.Resources; 20 import android.content.res.TypedArray; 21 import android.graphics.Canvas; 22 import android.graphics.Rect; 23 import android.graphics.drawable.Drawable; 24 import android.os.Handler; 25 import android.view.LayoutInflater; 26 import android.view.View; 27 import android.view.View.OnClickListener; 28 import android.view.View.OnLayoutChangeListener; 29 import android.view.ViewGroup; 30 import android.widget.FrameLayout; 31 import android.widget.TextView; 32 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 import androidx.core.view.AccessibilityDelegateCompat; 36 import androidx.core.view.ViewCompat; 37 import androidx.recyclerview.widget.GridLayoutManager; 38 import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup; 39 import androidx.recyclerview.widget.ItemTouchHelper; 40 import androidx.recyclerview.widget.RecyclerView; 41 import androidx.recyclerview.widget.RecyclerView.ItemDecoration; 42 import androidx.recyclerview.widget.RecyclerView.State; 43 import androidx.recyclerview.widget.RecyclerView.ViewHolder; 44 45 import com.android.internal.logging.UiEventLogger; 46 import com.android.systemui.FontSizeUtils; 47 import com.android.systemui.flags.FeatureFlags; 48 import com.android.systemui.flags.Flags; 49 import com.android.systemui.qs.QSEditEvent; 50 import com.android.systemui.qs.QSHost; 51 import com.android.systemui.qs.TileLayout; 52 import com.android.systemui.qs.customize.TileAdapter.Holder; 53 import com.android.systemui.qs.customize.TileQueryHelper.TileInfo; 54 import com.android.systemui.qs.customize.TileQueryHelper.TileStateListener; 55 import com.android.systemui.qs.dagger.QSScope; 56 import com.android.systemui.qs.dagger.QSThemedContext; 57 import com.android.systemui.qs.external.CustomTile; 58 import com.android.systemui.qs.tileimpl.QSTileViewImpl; 59 import com.android.systemui.res.R; 60 61 import java.util.ArrayList; 62 import java.util.List; 63 import java.util.Objects; 64 65 import javax.inject.Inject; 66 67 /** */ 68 @QSScope 69 public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileStateListener { 70 private static final long DRAG_LENGTH = 100; 71 private static final float DRAG_SCALE = 1.2f; 72 public static final long MOVE_DURATION = 150; 73 74 private static final int TYPE_TILE = 0; 75 private static final int TYPE_EDIT = 1; 76 private static final int TYPE_ACCESSIBLE_DROP = 2; 77 private static final int TYPE_HEADER = 3; 78 private static final int TYPE_DIVIDER = 4; 79 80 private static final long EDIT_ID = 10000; 81 private static final long DIVIDER_ID = 20000; 82 83 private static final int ACTION_NONE = 0; 84 private static final int ACTION_ADD = 1; 85 private static final int ACTION_MOVE = 2; 86 87 private static final int NUM_COLUMNS_ID = R.integer.quick_settings_num_columns; 88 89 private final Context mContext; 90 91 private final Handler mHandler = new Handler(); 92 private final List<TileInfo> mTiles = new ArrayList<>(); 93 private final ItemTouchHelper mItemTouchHelper; 94 private ItemDecoration mDecoration; 95 private final MarginTileDecoration mMarginDecoration; 96 private final int mMinNumTiles; 97 private final QSHost mHost; 98 private int mEditIndex; 99 private int mTileDividerIndex; 100 private int mFocusIndex; 101 102 private boolean mNeedsFocus; 103 @Nullable 104 private List<String> mCurrentSpecs; 105 @Nullable 106 private List<TileInfo> mOtherTiles; 107 @Nullable 108 private List<TileInfo> mAllTiles; 109 110 @Nullable 111 private Holder mCurrentDrag; 112 private int mAccessibilityAction = ACTION_NONE; 113 private int mAccessibilityFromIndex; 114 private final UiEventLogger mUiEventLogger; 115 private final AccessibilityDelegateCompat mAccessibilityDelegate; 116 @Nullable 117 private RecyclerView mRecyclerView; 118 private int mNumColumns; 119 120 private TextView mTempTextView; 121 private int mMinTileViewHeight; 122 private final boolean mIsSmallLandscapeLockscreenEnabled; 123 124 @Inject TileAdapter( @SThemedContext Context context, QSHost qsHost, UiEventLogger uiEventLogger, FeatureFlags featureFlags)125 public TileAdapter( 126 @QSThemedContext Context context, 127 QSHost qsHost, 128 UiEventLogger uiEventLogger, 129 FeatureFlags featureFlags) { 130 mContext = context; 131 mHost = qsHost; 132 mUiEventLogger = uiEventLogger; 133 mItemTouchHelper = new ItemTouchHelper(mCallbacks); 134 mDecoration = new TileItemDecoration(context); 135 mMarginDecoration = new MarginTileDecoration(); 136 mMinNumTiles = context.getResources().getInteger(R.integer.quick_settings_min_num_tiles); 137 mIsSmallLandscapeLockscreenEnabled = 138 featureFlags.isEnabled(Flags.LOCKSCREEN_ENABLE_LANDSCAPE); 139 mNumColumns = useSmallLandscapeLockscreenResources() 140 ? context.getResources().getInteger( 141 R.integer.small_land_lockscreen_quick_settings_num_columns) 142 : context.getResources().getInteger(NUM_COLUMNS_ID); 143 mAccessibilityDelegate = new TileAdapterDelegate(); 144 mSizeLookup.setSpanIndexCacheEnabled(true); 145 mTempTextView = new TextView(context); 146 mMinTileViewHeight = context.getResources().getDimensionPixelSize(R.dimen.qs_tile_height); 147 } 148 149 @Override onAttachedToRecyclerView(@onNull RecyclerView recyclerView)150 public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { 151 mRecyclerView = recyclerView; 152 } 153 154 @Override onDetachedFromRecyclerView(@onNull RecyclerView recyclerView)155 public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { 156 mRecyclerView = null; 157 } 158 159 /** 160 * Update the number of columns to show, from resources. 161 * 162 * @return {@code true} if the number of columns changed, {@code false} otherwise 163 */ updateNumColumns()164 public boolean updateNumColumns() { 165 int numColumns = useSmallLandscapeLockscreenResources() 166 ? mContext.getResources().getInteger( 167 R.integer.small_land_lockscreen_quick_settings_num_columns) 168 : mContext.getResources().getInteger(NUM_COLUMNS_ID); 169 if (numColumns != mNumColumns) { 170 mNumColumns = numColumns; 171 return true; 172 } else { 173 return false; 174 } 175 } 176 177 // TODO (b/293252410) remove condition here when flag is launched 178 // Instead update quick_settings_num_columns and quick_settings_max_rows to be the same as 179 // the small_land_lockscreen_quick_settings_num_columns or 180 // small_land_lockscreen_quick_settings_max_rows respectively whenever 181 // is_small_screen_landscape is true. 182 // Then, only use quick_settings_num_columns and quick_settings_max_rows. useSmallLandscapeLockscreenResources()183 private boolean useSmallLandscapeLockscreenResources() { 184 return mIsSmallLandscapeLockscreenEnabled 185 && mContext.getResources().getBoolean(R.bool.is_small_screen_landscape); 186 } 187 getNumColumns()188 public int getNumColumns() { 189 return mNumColumns; 190 } 191 getItemTouchHelper()192 public ItemTouchHelper getItemTouchHelper() { 193 return mItemTouchHelper; 194 } 195 getItemDecoration()196 public ItemDecoration getItemDecoration() { 197 return mDecoration; 198 } 199 getMarginItemDecoration()200 public ItemDecoration getMarginItemDecoration() { 201 return mMarginDecoration; 202 } 203 changeHalfMargin(int halfMargin)204 public void changeHalfMargin(int halfMargin) { 205 mMarginDecoration.setHalfMargin(halfMargin); 206 } 207 saveSpecs(QSHost host)208 public void saveSpecs(QSHost host) { 209 List<String> newSpecs = new ArrayList<>(); 210 clearAccessibilityState(); 211 for (int i = 1; i < mTiles.size() && mTiles.get(i) != null; i++) { 212 newSpecs.add(mTiles.get(i).spec); 213 } 214 host.changeTilesByUser(mCurrentSpecs, newSpecs); 215 mCurrentSpecs = newSpecs; 216 } 217 clearAccessibilityState()218 private void clearAccessibilityState() { 219 mNeedsFocus = false; 220 if (mAccessibilityAction == ACTION_ADD) { 221 // Remove blank tile from last spot 222 mTiles.remove(--mEditIndex); 223 // Update the tile divider position 224 notifyDataSetChanged(); 225 } 226 mAccessibilityAction = ACTION_NONE; 227 } 228 229 /** */ resetTileSpecs(List<String> specs)230 public void resetTileSpecs(List<String> specs) { 231 // Notify the host so the tiles get removed callbacks. 232 mHost.changeTilesByUser(mCurrentSpecs, specs); 233 setTileSpecs(specs); 234 } 235 setTileSpecs(List<String> currentSpecs)236 public void setTileSpecs(List<String> currentSpecs) { 237 if (currentSpecs.equals(mCurrentSpecs)) { 238 return; 239 } 240 mCurrentSpecs = currentSpecs; 241 recalcSpecs(); 242 } 243 244 @Override onTilesChanged(List<TileInfo> tiles)245 public void onTilesChanged(List<TileInfo> tiles) { 246 mAllTiles = tiles; 247 recalcSpecs(); 248 } 249 recalcSpecs()250 private void recalcSpecs() { 251 if (mCurrentSpecs == null || mAllTiles == null) { 252 return; 253 } 254 mOtherTiles = new ArrayList<TileInfo>(mAllTiles); 255 mTiles.clear(); 256 mTiles.add(null); 257 for (int i = 0; i < mCurrentSpecs.size(); i++) { 258 final TileInfo tile = getAndRemoveOther(mCurrentSpecs.get(i)); 259 if (tile != null) { 260 mTiles.add(tile); 261 } 262 } 263 mTiles.add(null); 264 for (int i = 0; i < mOtherTiles.size(); i++) { 265 final TileInfo tile = mOtherTiles.get(i); 266 if (tile.isSystem) { 267 mOtherTiles.remove(i--); 268 mTiles.add(tile); 269 } 270 } 271 mTileDividerIndex = mTiles.size(); 272 mTiles.add(null); 273 mTiles.addAll(mOtherTiles); 274 updateDividerLocations(); 275 notifyDataSetChanged(); 276 } 277 278 @Nullable getAndRemoveOther(String s)279 private TileInfo getAndRemoveOther(String s) { 280 for (int i = 0; i < mOtherTiles.size(); i++) { 281 if (mOtherTiles.get(i).spec.equals(s)) { 282 return mOtherTiles.remove(i); 283 } 284 } 285 return null; 286 } 287 288 @Override getItemViewType(int position)289 public int getItemViewType(int position) { 290 if (position == 0) { 291 return TYPE_HEADER; 292 } 293 if (mAccessibilityAction == ACTION_ADD && position == mEditIndex - 1) { 294 return TYPE_ACCESSIBLE_DROP; 295 } 296 if (position == mTileDividerIndex) { 297 return TYPE_DIVIDER; 298 } 299 if (mTiles.get(position) == null) { 300 return TYPE_EDIT; 301 } 302 return TYPE_TILE; 303 } 304 305 @Override onCreateViewHolder(ViewGroup parent, int viewType)306 public Holder onCreateViewHolder(ViewGroup parent, int viewType) { 307 final Context context = parent.getContext(); 308 LayoutInflater inflater = LayoutInflater.from(context); 309 if (viewType == TYPE_HEADER) { 310 View v = inflater.inflate(R.layout.qs_customize_header, parent, false); 311 v.setMinimumHeight(calculateHeaderMinHeight(context)); 312 return new Holder(v); 313 } 314 if (viewType == TYPE_DIVIDER) { 315 return new Holder(inflater.inflate(R.layout.qs_customize_tile_divider, parent, false)); 316 } 317 if (viewType == TYPE_EDIT) { 318 return new Holder(inflater.inflate(R.layout.qs_customize_divider, parent, false)); 319 } 320 FrameLayout frame = (FrameLayout) inflater.inflate(R.layout.qs_customize_tile_frame, parent, 321 false); 322 if (com.android.systemui.Flags.qsTileFocusState()) { 323 frame.setClipChildren(false); 324 } 325 View view = new CustomizeTileView(context); 326 frame.addView(view); 327 return new Holder(frame); 328 } 329 330 @Override getItemCount()331 public int getItemCount() { 332 return mTiles.size(); 333 } 334 getItemCountForAccessibility()335 public int getItemCountForAccessibility() { 336 if (mAccessibilityAction == ACTION_MOVE) { 337 return mEditIndex; 338 } else { 339 return getItemCount(); 340 } 341 } 342 343 @Override onFailedToRecycleView(Holder holder)344 public boolean onFailedToRecycleView(Holder holder) { 345 holder.stopDrag(); 346 holder.clearDrag(); 347 return true; 348 } 349 setSelectableForHeaders(View view)350 private void setSelectableForHeaders(View view) { 351 final boolean selectable = mAccessibilityAction == ACTION_NONE; 352 view.setFocusable(selectable); 353 view.setImportantForAccessibility(selectable 354 ? View.IMPORTANT_FOR_ACCESSIBILITY_YES 355 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 356 view.setFocusableInTouchMode(selectable); 357 } 358 359 @Override onBindViewHolder(final Holder holder, int position)360 public void onBindViewHolder(final Holder holder, int position) { 361 if (holder.mTileView != null) { 362 holder.mTileView.setMinimumHeight(mMinTileViewHeight); 363 } 364 365 if (holder.getItemViewType() == TYPE_HEADER) { 366 setSelectableForHeaders(holder.itemView); 367 return; 368 } 369 if (holder.getItemViewType() == TYPE_DIVIDER) { 370 holder.itemView.setVisibility(mTileDividerIndex < mTiles.size() - 1 ? View.VISIBLE 371 : View.INVISIBLE); 372 return; 373 } 374 if (holder.getItemViewType() == TYPE_EDIT) { 375 final String titleText; 376 Resources res = mContext.getResources(); 377 if (mCurrentDrag == null) { 378 titleText = res.getString(R.string.drag_to_add_tiles); 379 } else if (!canRemoveTiles() && mCurrentDrag.getAdapterPosition() < mEditIndex) { 380 titleText = res.getString(R.string.drag_to_remove_disabled, mMinNumTiles); 381 } else { 382 titleText = res.getString(R.string.drag_to_remove_tiles); 383 } 384 385 ((TextView) holder.itemView.findViewById(android.R.id.title)).setText(titleText); 386 setSelectableForHeaders(holder.itemView); 387 388 return; 389 } 390 if (holder.getItemViewType() == TYPE_ACCESSIBLE_DROP) { 391 holder.mTileView.setClickable(true); 392 holder.mTileView.setFocusable(true); 393 holder.mTileView.setFocusableInTouchMode(true); 394 holder.mTileView.setVisibility(View.VISIBLE); 395 holder.mTileView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 396 holder.mTileView.setContentDescription(mContext.getString( 397 R.string.accessibility_qs_edit_tile_add_to_position, position)); 398 holder.mTileView.setOnClickListener(new OnClickListener() { 399 @Override 400 public void onClick(View v) { 401 selectPosition(holder.getLayoutPosition()); 402 } 403 }); 404 focusOnHolder(holder); 405 return; 406 } 407 408 TileInfo info = mTiles.get(position); 409 410 final boolean selectable = 0 < position && position < mEditIndex; 411 if (selectable && mAccessibilityAction == ACTION_ADD) { 412 info.state.contentDescription = mContext.getString( 413 R.string.accessibility_qs_edit_tile_add_to_position, position); 414 } else if (selectable && mAccessibilityAction == ACTION_MOVE) { 415 info.state.contentDescription = mContext.getString( 416 R.string.accessibility_qs_edit_tile_move_to_position, position); 417 } else if (!selectable && (mAccessibilityAction == ACTION_MOVE 418 || mAccessibilityAction == ACTION_ADD)) { 419 info.state.contentDescription = mContext.getString( 420 R.string.accessibilit_qs_edit_tile_add_move_invalid_position); 421 } else { 422 info.state.contentDescription = info.state.label; 423 } 424 info.state.expandedAccessibilityClassName = ""; 425 426 CustomizeTileView tileView = 427 Objects.requireNonNull( 428 holder.getTileAsCustomizeView(), "The holder must have a tileView"); 429 tileView.changeState(info.state); 430 tileView.setShowAppLabel(position > mEditIndex && !info.isSystem); 431 // Don't show the side view for third party tiles, as we don't have the actual state. 432 tileView.setShowSideView(position < mEditIndex || info.isSystem); 433 holder.mTileView.setSelected(true); 434 holder.mTileView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 435 holder.mTileView.setClickable(true); 436 holder.mTileView.setOnClickListener(null); 437 holder.mTileView.setFocusable(true); 438 holder.mTileView.setFocusableInTouchMode(true); 439 holder.mTileView.setAccessibilityTraversalBefore(View.NO_ID); 440 441 if (mAccessibilityAction != ACTION_NONE) { 442 holder.mTileView.setClickable(selectable); 443 holder.mTileView.setFocusable(selectable); 444 holder.mTileView.setFocusableInTouchMode(selectable); 445 // holder.mTileView.setImportantForAccessibility(selectable 446 // ? View.IMPORTANT_FOR_ACCESSIBILITY_YES 447 // : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 448 if (selectable) { 449 holder.mTileView.setOnClickListener(new OnClickListener() { 450 @Override 451 public void onClick(View v) { 452 int position = holder.getLayoutPosition(); 453 if (position == RecyclerView.NO_POSITION) return; 454 if (mAccessibilityAction != ACTION_NONE) { 455 selectPosition(position); 456 } 457 } 458 }); 459 } 460 } 461 if (position == mFocusIndex) { 462 focusOnHolder(holder); 463 } 464 } 465 466 private void focusOnHolder(Holder holder) { 467 if (mNeedsFocus) { 468 // Wait for this to get laid out then set its focus. 469 // Ensure that tile gets laid out so we get the callback. 470 holder.mTileView.requestLayout(); 471 holder.mTileView.addOnLayoutChangeListener(new OnLayoutChangeListener() { 472 @Override 473 public void onLayoutChange(View v, int left, int top, int right, int bottom, 474 int oldLeft, int oldTop, int oldRight, int oldBottom) { 475 holder.mTileView.removeOnLayoutChangeListener(this); 476 holder.mTileView.requestAccessibilityFocus(); 477 } 478 }); 479 mNeedsFocus = false; 480 mFocusIndex = RecyclerView.NO_POSITION; 481 } 482 } 483 484 private boolean canRemoveTiles() { 485 return mCurrentSpecs.size() > mMinNumTiles; 486 } 487 488 private void selectPosition(int position) { 489 if (mAccessibilityAction == ACTION_ADD) { 490 // Remove the placeholder. 491 mTiles.remove(mEditIndex--); 492 } 493 mAccessibilityAction = ACTION_NONE; 494 move(mAccessibilityFromIndex, position, false); 495 mFocusIndex = position; 496 mNeedsFocus = true; 497 notifyDataSetChanged(); 498 } 499 500 private void startAccessibleAdd(int position) { 501 mAccessibilityFromIndex = position; 502 mAccessibilityAction = ACTION_ADD; 503 // Add placeholder for last slot. 504 mTiles.add(mEditIndex++, null); 505 // Update the tile divider position 506 mTileDividerIndex++; 507 mFocusIndex = mEditIndex - 1; 508 final int focus = mFocusIndex; 509 mNeedsFocus = true; 510 if (mRecyclerView != null) { 511 mRecyclerView.post(() -> { 512 final RecyclerView recyclerView = mRecyclerView; 513 if (recyclerView != null) { 514 recyclerView.smoothScrollToPosition(focus); 515 } 516 }); 517 } 518 notifyDataSetChanged(); 519 } 520 521 private void startAccessibleMove(int position) { 522 mAccessibilityFromIndex = position; 523 mAccessibilityAction = ACTION_MOVE; 524 mFocusIndex = position; 525 mNeedsFocus = true; 526 notifyDataSetChanged(); 527 } 528 529 private boolean canRemoveFromPosition(int position) { 530 return canRemoveTiles() && isCurrentTile(position); 531 } 532 533 private boolean isCurrentTile(int position) { 534 return position < mEditIndex; 535 } 536 537 private boolean canAddFromPosition(int position) { 538 return position > mEditIndex; 539 } 540 541 private boolean addFromPosition(int position) { 542 if (!canAddFromPosition(position)) return false; 543 move(position, mEditIndex); 544 return true; 545 } 546 547 private boolean removeFromPosition(int position) { 548 if (!canRemoveFromPosition(position)) return false; 549 TileInfo info = mTiles.get(position); 550 move(position, info.isSystem ? mEditIndex : mTileDividerIndex); 551 return true; 552 } 553 554 public SpanSizeLookup getSizeLookup() { 555 return mSizeLookup; 556 } 557 558 private boolean move(int from, int to) { 559 return move(from, to, true); 560 } 561 562 private boolean move(int from, int to, boolean notify) { 563 if (to == from) { 564 return true; 565 } 566 move(from, to, mTiles, notify); 567 updateDividerLocations(); 568 if (to >= mEditIndex) { 569 mUiEventLogger.log(QSEditEvent.QS_EDIT_REMOVE, 0, strip(mTiles.get(to))); 570 } else if (from >= mEditIndex) { 571 mUiEventLogger.log(QSEditEvent.QS_EDIT_ADD, 0, strip(mTiles.get(to))); 572 } else { 573 mUiEventLogger.log(QSEditEvent.QS_EDIT_MOVE, 0, strip(mTiles.get(to))); 574 } 575 saveSpecs(mHost); 576 return true; 577 } 578 updateDividerLocations()579 private void updateDividerLocations() { 580 // The first null is the header label (index 0) so we can skip it, 581 // the second null is the edit tiles label, the third null is the tile divider. 582 // If there is no third null, then there are no non-system tiles. 583 mEditIndex = -1; 584 mTileDividerIndex = mTiles.size(); 585 for (int i = 1; i < mTiles.size(); i++) { 586 if (mTiles.get(i) == null) { 587 if (mEditIndex == -1) { 588 mEditIndex = i; 589 } else { 590 mTileDividerIndex = i; 591 } 592 } 593 } 594 if (mTiles.size() - 1 == mTileDividerIndex) { 595 notifyItemChanged(mTileDividerIndex); 596 } 597 } 598 strip(TileInfo tileInfo)599 private static String strip(TileInfo tileInfo) { 600 String spec = tileInfo.spec; 601 if (spec.startsWith(CustomTile.PREFIX)) { 602 ComponentName component = CustomTile.getComponentFromSpec(spec); 603 return component.getPackageName(); 604 } 605 return spec; 606 } 607 move(int from, int to, List<T> list, boolean notify)608 private <T> void move(int from, int to, List<T> list, boolean notify) { 609 list.add(to, list.remove(from)); 610 if (notify) { 611 notifyItemMoved(from, to); 612 } 613 } 614 615 public class Holder extends ViewHolder { 616 @Nullable private QSTileViewImpl mTileView; 617 Holder(View itemView)618 public Holder(View itemView) { 619 super(itemView); 620 if (itemView instanceof FrameLayout) { 621 mTileView = (QSTileViewImpl) ((FrameLayout) itemView).getChildAt(0); 622 mTileView.getIcon().disableAnimation(); 623 mTileView.setTag(this); 624 ViewCompat.setAccessibilityDelegate(mTileView, mAccessibilityDelegate); 625 } 626 } 627 628 @Nullable getTileAsCustomizeView()629 public CustomizeTileView getTileAsCustomizeView() { 630 return (CustomizeTileView) mTileView; 631 } 632 clearDrag()633 public void clearDrag() { 634 itemView.clearAnimation(); 635 itemView.setScaleX(1); 636 itemView.setScaleY(1); 637 } 638 startDrag()639 public void startDrag() { 640 itemView.animate() 641 .setDuration(DRAG_LENGTH) 642 .scaleX(DRAG_SCALE) 643 .scaleY(DRAG_SCALE); 644 } 645 stopDrag()646 public void stopDrag() { 647 itemView.animate() 648 .setDuration(DRAG_LENGTH) 649 .scaleX(1) 650 .scaleY(1); 651 } 652 canRemove()653 boolean canRemove() { 654 return canRemoveFromPosition(getLayoutPosition()); 655 } 656 canAdd()657 boolean canAdd() { 658 return canAddFromPosition(getLayoutPosition()); 659 } 660 toggleState()661 void toggleState() { 662 if (canAdd()) { 663 add(); 664 } else { 665 remove(); 666 } 667 } 668 add()669 private void add() { 670 if (addFromPosition(getLayoutPosition())) { 671 itemView.announceForAccessibility( 672 itemView.getContext().getText(R.string.accessibility_qs_edit_tile_added)); 673 } 674 } 675 remove()676 private void remove() { 677 if (removeFromPosition(getLayoutPosition())) { 678 itemView.announceForAccessibility( 679 itemView.getContext().getText(R.string.accessibility_qs_edit_tile_removed)); 680 } 681 } 682 isCurrentTile()683 boolean isCurrentTile() { 684 return TileAdapter.this.isCurrentTile(getLayoutPosition()); 685 } 686 startAccessibleAdd()687 void startAccessibleAdd() { 688 TileAdapter.this.startAccessibleAdd(getLayoutPosition()); 689 } 690 startAccessibleMove()691 void startAccessibleMove() { 692 TileAdapter.this.startAccessibleMove(getLayoutPosition()); 693 } 694 canTakeAccessibleAction()695 boolean canTakeAccessibleAction() { 696 return mAccessibilityAction == ACTION_NONE; 697 } 698 } 699 700 private final SpanSizeLookup mSizeLookup = new SpanSizeLookup() { 701 @Override 702 public int getSpanSize(int position) { 703 final int type = getItemViewType(position); 704 if (type == TYPE_EDIT || type == TYPE_DIVIDER || type == TYPE_HEADER) { 705 return mNumColumns; 706 } else { 707 return 1; 708 } 709 } 710 }; 711 712 private class TileItemDecoration extends ItemDecoration { 713 private final Drawable mDrawable; 714 TileItemDecoration(Context context)715 private TileItemDecoration(Context context) { 716 mDrawable = context.getDrawable(R.drawable.qs_customize_tile_decoration); 717 } 718 719 @Override onDraw(Canvas c, RecyclerView parent, State state)720 public void onDraw(Canvas c, RecyclerView parent, State state) { 721 super.onDraw(c, parent, state); 722 723 final int childCount = parent.getChildCount(); 724 final int width = parent.getWidth(); 725 final int bottom = parent.getBottom(); 726 for (int i = 0; i < childCount; i++) { 727 final View child = parent.getChildAt(i); 728 final ViewHolder holder = parent.getChildViewHolder(child); 729 // Do not draw background for the holder that's currently being dragged 730 if (holder == mCurrentDrag) { 731 continue; 732 } 733 // Do not draw background for holders before the edit index (header and current 734 // tiles) 735 if (holder.getAdapterPosition() == 0 || 736 holder.getAdapterPosition() < mEditIndex && !(child instanceof TextView)) { 737 continue; 738 } 739 740 final int top = child.getTop() + Math.round(ViewCompat.getTranslationY(child)); 741 mDrawable.setBounds(0, top, width, bottom); 742 mDrawable.draw(c); 743 break; 744 } 745 } 746 } 747 748 private static class MarginTileDecoration extends ItemDecoration { 749 private int mHalfMargin; 750 setHalfMargin(int halfMargin)751 public void setHalfMargin(int halfMargin) { 752 mHalfMargin = halfMargin; 753 } 754 755 @Override getItemOffsets(@onNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull State state)756 public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, 757 @NonNull RecyclerView parent, @NonNull State state) { 758 if (parent.getLayoutManager() == null) return; 759 760 GridLayoutManager lm = ((GridLayoutManager) parent.getLayoutManager()); 761 int column = ((GridLayoutManager.LayoutParams) view.getLayoutParams()).getSpanIndex(); 762 763 if (view instanceof TextView) { 764 super.getItemOffsets(outRect, view, parent, state); 765 } else { 766 if (column != 0 && column != lm.getSpanCount() - 1) { 767 // In a column that's not leftmost or rightmost (half of the margin between 768 // columns). 769 outRect.left = mHalfMargin; 770 outRect.right = mHalfMargin; 771 } else { 772 // Leftmost or rightmost column 773 if (parent.isLayoutRtl()) { 774 if (column == 0) { 775 // Rightmost column 776 outRect.left = mHalfMargin; 777 outRect.right = 0; 778 } else { 779 // Leftmost column 780 outRect.left = 0; 781 outRect.right = mHalfMargin; 782 } 783 } else { 784 // Non RTL 785 if (column == 0) { 786 // Leftmost column 787 outRect.left = 0; 788 outRect.right = mHalfMargin; 789 } else { 790 // Rightmost column 791 outRect.left = mHalfMargin; 792 outRect.right = 0; 793 } 794 } 795 } 796 } 797 } 798 } 799 800 private final ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() { 801 802 @Override 803 public boolean isLongPressDragEnabled() { 804 return true; 805 } 806 807 @Override 808 public boolean isItemViewSwipeEnabled() { 809 return false; 810 } 811 812 @Override 813 public void onSelectedChanged(ViewHolder viewHolder, int actionState) { 814 super.onSelectedChanged(viewHolder, actionState); 815 if (actionState != ItemTouchHelper.ACTION_STATE_DRAG) { 816 viewHolder = null; 817 } 818 if (viewHolder == mCurrentDrag) return; 819 if (mCurrentDrag != null) { 820 int position = mCurrentDrag.getAdapterPosition(); 821 if (position == RecyclerView.NO_POSITION) return; 822 TileInfo info = mTiles.get(position); 823 ((CustomizeTileView) mCurrentDrag.mTileView).setShowAppLabel( 824 position > mEditIndex && !info.isSystem); 825 mCurrentDrag.stopDrag(); 826 mCurrentDrag = null; 827 } 828 if (viewHolder != null) { 829 mCurrentDrag = (Holder) viewHolder; 830 mCurrentDrag.startDrag(); 831 } 832 mHandler.post(new Runnable() { 833 @Override 834 public void run() { 835 notifyItemChanged(mEditIndex); 836 } 837 }); 838 } 839 840 @Override 841 public boolean canDropOver(RecyclerView recyclerView, ViewHolder current, 842 ViewHolder target) { 843 final int position = target.getAdapterPosition(); 844 if (position == 0 || position == RecyclerView.NO_POSITION){ 845 return false; 846 } 847 if (!canRemoveTiles() && current.getAdapterPosition() < mEditIndex) { 848 return position < mEditIndex; 849 } 850 return position <= mEditIndex + 1; 851 } 852 853 @Override 854 public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) { 855 switch (viewHolder.getItemViewType()) { 856 case TYPE_EDIT: 857 case TYPE_DIVIDER: 858 case TYPE_HEADER: 859 // Fall through 860 return makeMovementFlags(0, 0); 861 default: 862 int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN 863 | ItemTouchHelper.RIGHT | ItemTouchHelper.LEFT; 864 return makeMovementFlags(dragFlags, 0); 865 } 866 } 867 868 @Override 869 public boolean onMove(RecyclerView recyclerView, ViewHolder viewHolder, ViewHolder target) { 870 int from = viewHolder.getAdapterPosition(); 871 int to = target.getAdapterPosition(); 872 if (from == 0 || from == RecyclerView.NO_POSITION || 873 to == 0 || to == RecyclerView.NO_POSITION) { 874 return false; 875 } 876 return move(from, to); 877 } 878 879 @Override 880 public void onSwiped(ViewHolder viewHolder, int direction) { 881 } 882 883 // Just in case, make sure to animate to base state. 884 @Override 885 public void clearView(@NonNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder) { 886 ((Holder) viewHolder).stopDrag(); 887 super.clearView(recyclerView, viewHolder); 888 } 889 }; 890 calculateHeaderMinHeight(Context context)891 private static int calculateHeaderMinHeight(Context context) { 892 Resources res = context.getResources(); 893 // style used in qs_customize_header.xml for the Toolbar 894 TypedArray toolbarStyle = context.obtainStyledAttributes( 895 R.style.QSCustomizeToolbar, com.android.internal.R.styleable.Toolbar); 896 int buttonStyle = toolbarStyle.getResourceId( 897 com.android.internal.R.styleable.Toolbar_navigationButtonStyle, 0); 898 toolbarStyle.recycle(); 899 int buttonMinWidth = 0; 900 if (buttonStyle != 0) { 901 TypedArray t = context.obtainStyledAttributes(buttonStyle, android.R.styleable.View); 902 buttonMinWidth = t.getDimensionPixelSize(android.R.styleable.View_minWidth, 0); 903 t.recycle(); 904 } 905 return res.getDimensionPixelSize(R.dimen.qs_panel_padding_top) 906 + res.getDimensionPixelSize(R.dimen.brightness_mirror_height) 907 + res.getDimensionPixelSize(R.dimen.qs_brightness_margin_top) 908 + res.getDimensionPixelSize(R.dimen.qs_brightness_margin_bottom) 909 - buttonMinWidth 910 - res.getDimensionPixelSize(R.dimen.qs_tile_margin_top_bottom); 911 } 912 913 /** 914 * Re-estimate the tile view height based under current font scaling. Like 915 * {@link TileLayout#estimateCellHeight()}, the tile view height would be estimated with 2 916 * labels as general case. 917 */ reloadTileHeight()918 public void reloadTileHeight() { 919 final int minHeight = mContext.getResources().getDimensionPixelSize(R.dimen.qs_tile_height); 920 FontSizeUtils.updateFontSize(mTempTextView, R.dimen.qs_tile_text_size); 921 int unspecifiedSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 922 mTempTextView.measure(unspecifiedSpec, unspecifiedSpec); 923 int padding = mContext.getResources().getDimensionPixelSize(R.dimen.qs_tile_padding); 924 int estimatedTileViewHeight = mTempTextView.getMeasuredHeight() * 2 + padding * 2; 925 mMinTileViewHeight = Math.max(minHeight, estimatedTileViewHeight); 926 } 927 928 } 929