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.app.AlertDialog; 18 import android.app.AlertDialog.Builder; 19 import android.content.ComponentName; 20 import android.content.Context; 21 import android.content.DialogInterface; 22 import android.content.res.TypedArray; 23 import android.graphics.Canvas; 24 import android.graphics.drawable.ColorDrawable; 25 import android.os.Handler; 26 import android.support.v4.view.ViewCompat; 27 import android.support.v7.widget.GridLayoutManager.SpanSizeLookup; 28 import android.support.v7.widget.RecyclerView; 29 import android.support.v7.widget.RecyclerView.ItemDecoration; 30 import android.support.v7.widget.RecyclerView.State; 31 import android.support.v7.widget.RecyclerView.ViewHolder; 32 import android.support.v7.widget.helper.ItemTouchHelper; 33 import android.view.LayoutInflater; 34 import android.view.View; 35 import android.view.View.OnClickListener; 36 import android.view.View.OnLayoutChangeListener; 37 import android.view.ViewGroup; 38 import android.view.accessibility.AccessibilityManager; 39 import android.widget.FrameLayout; 40 import android.widget.TextView; 41 42 import com.android.internal.logging.MetricsLogger; 43 import com.android.internal.logging.nano.MetricsProto; 44 import com.android.systemui.R; 45 import com.android.systemui.qs.QSTileHost; 46 import com.android.systemui.qs.customize.TileAdapter.Holder; 47 import com.android.systemui.qs.customize.TileQueryHelper.TileInfo; 48 import com.android.systemui.qs.customize.TileQueryHelper.TileStateListener; 49 import com.android.systemui.qs.external.CustomTile; 50 import com.android.systemui.qs.tileimpl.QSIconViewImpl; 51 import com.android.systemui.statusbar.phone.SystemUIDialog; 52 53 import java.util.ArrayList; 54 import java.util.List; 55 56 public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileStateListener { 57 private static final int MIN_NUM_TILES = 6; 58 private static final long DRAG_LENGTH = 100; 59 private static final float DRAG_SCALE = 1.2f; 60 public static final long MOVE_DURATION = 150; 61 62 private static final int TYPE_TILE = 0; 63 private static final int TYPE_EDIT = 1; 64 private static final int TYPE_ACCESSIBLE_DROP = 2; 65 private static final int TYPE_DIVIDER = 4; 66 67 private static final long EDIT_ID = 10000; 68 private static final long DIVIDER_ID = 20000; 69 70 private static final int ACTION_NONE = 0; 71 private static final int ACTION_ADD = 1; 72 private static final int ACTION_MOVE = 2; 73 74 private final Context mContext; 75 76 private final Handler mHandler = new Handler(); 77 private final List<TileInfo> mTiles = new ArrayList<>(); 78 private final ItemTouchHelper mItemTouchHelper; 79 private final ItemDecoration mDecoration; 80 private final AccessibilityManager mAccessibilityManager; 81 private int mEditIndex; 82 private int mTileDividerIndex; 83 private boolean mNeedsFocus; 84 private List<String> mCurrentSpecs; 85 private List<TileInfo> mOtherTiles; 86 private List<TileInfo> mAllTiles; 87 88 private Holder mCurrentDrag; 89 private int mAccessibilityAction = ACTION_NONE; 90 private int mAccessibilityFromIndex; 91 private QSTileHost mHost; 92 TileAdapter(Context context)93 public TileAdapter(Context context) { 94 mContext = context; 95 mAccessibilityManager = context.getSystemService(AccessibilityManager.class); 96 mItemTouchHelper = new ItemTouchHelper(mCallbacks); 97 mDecoration = new TileItemDecoration(context); 98 } 99 setHost(QSTileHost host)100 public void setHost(QSTileHost host) { 101 mHost = host; 102 } 103 getItemTouchHelper()104 public ItemTouchHelper getItemTouchHelper() { 105 return mItemTouchHelper; 106 } 107 getItemDecoration()108 public ItemDecoration getItemDecoration() { 109 return mDecoration; 110 } 111 saveSpecs(QSTileHost host)112 public void saveSpecs(QSTileHost host) { 113 List<String> newSpecs = new ArrayList<>(); 114 for (int i = 0; i < mTiles.size() && mTiles.get(i) != null; i++) { 115 newSpecs.add(mTiles.get(i).spec); 116 } 117 host.changeTiles(mCurrentSpecs, newSpecs); 118 mCurrentSpecs = newSpecs; 119 } 120 resetTileSpecs(QSTileHost host, List<String> specs)121 public void resetTileSpecs(QSTileHost host, List<String> specs) { 122 // Notify the host so the tiles get removed callbacks. 123 host.changeTiles(mCurrentSpecs, specs); 124 setTileSpecs(specs); 125 } 126 setTileSpecs(List<String> currentSpecs)127 public void setTileSpecs(List<String> currentSpecs) { 128 if (currentSpecs.equals(mCurrentSpecs)) { 129 return; 130 } 131 mCurrentSpecs = currentSpecs; 132 recalcSpecs(); 133 } 134 135 @Override onTilesChanged(List<TileInfo> tiles)136 public void onTilesChanged(List<TileInfo> tiles) { 137 mAllTiles = tiles; 138 recalcSpecs(); 139 } 140 recalcSpecs()141 private void recalcSpecs() { 142 if (mCurrentSpecs == null || mAllTiles == null) { 143 return; 144 } 145 mOtherTiles = new ArrayList<TileInfo>(mAllTiles); 146 mTiles.clear(); 147 for (int i = 0; i < mCurrentSpecs.size(); i++) { 148 final TileInfo tile = getAndRemoveOther(mCurrentSpecs.get(i)); 149 if (tile != null) { 150 mTiles.add(tile); 151 } 152 } 153 mTiles.add(null); 154 for (int i = 0; i < mOtherTiles.size(); i++) { 155 final TileInfo tile = mOtherTiles.get(i); 156 if (tile.isSystem) { 157 mOtherTiles.remove(i--); 158 mTiles.add(tile); 159 } 160 } 161 mTileDividerIndex = mTiles.size(); 162 mTiles.add(null); 163 mTiles.addAll(mOtherTiles); 164 updateDividerLocations(); 165 notifyDataSetChanged(); 166 } 167 getAndRemoveOther(String s)168 private TileInfo getAndRemoveOther(String s) { 169 for (int i = 0; i < mOtherTiles.size(); i++) { 170 if (mOtherTiles.get(i).spec.equals(s)) { 171 return mOtherTiles.remove(i); 172 } 173 } 174 return null; 175 } 176 177 @Override getItemViewType(int position)178 public int getItemViewType(int position) { 179 if (mAccessibilityAction == ACTION_ADD && position == mEditIndex - 1) { 180 return TYPE_ACCESSIBLE_DROP; 181 } 182 if (position == mTileDividerIndex) { 183 return TYPE_DIVIDER; 184 } 185 if (mTiles.get(position) == null) { 186 return TYPE_EDIT; 187 } 188 return TYPE_TILE; 189 } 190 191 @Override onCreateViewHolder(ViewGroup parent, int viewType)192 public Holder onCreateViewHolder(ViewGroup parent, int viewType) { 193 final Context context = parent.getContext(); 194 LayoutInflater inflater = LayoutInflater.from(context); 195 if (viewType == TYPE_DIVIDER) { 196 return new Holder(inflater.inflate(R.layout.qs_customize_tile_divider, parent, false)); 197 } 198 if (viewType == TYPE_EDIT) { 199 return new Holder(inflater.inflate(R.layout.qs_customize_divider, parent, false)); 200 } 201 FrameLayout frame = (FrameLayout) inflater.inflate(R.layout.qs_customize_tile_frame, parent, 202 false); 203 frame.addView(new CustomizeTileView(context, new QSIconViewImpl(context))); 204 return new Holder(frame); 205 } 206 207 @Override getItemCount()208 public int getItemCount() { 209 return mTiles.size(); 210 } 211 212 @Override onFailedToRecycleView(Holder holder)213 public boolean onFailedToRecycleView(Holder holder) { 214 holder.clearDrag(); 215 return true; 216 } 217 218 @Override onBindViewHolder(final Holder holder, int position)219 public void onBindViewHolder(final Holder holder, int position) { 220 if (holder.getItemViewType() == TYPE_DIVIDER) { 221 holder.itemView.setVisibility(mTileDividerIndex < mTiles.size() - 1 ? View.VISIBLE 222 : View.INVISIBLE); 223 return; 224 } 225 if (holder.getItemViewType() == TYPE_EDIT) { 226 final int titleResId; 227 if (mCurrentDrag == null) { 228 titleResId = R.string.drag_to_add_tiles; 229 } else if (!canRemoveTiles() && mCurrentDrag.getAdapterPosition() < mEditIndex) { 230 titleResId = R.string.drag_to_remove_disabled; 231 } else { 232 titleResId = R.string.drag_to_remove_tiles; 233 } 234 ((TextView) holder.itemView.findViewById(android.R.id.title)).setText(titleResId); 235 return; 236 } 237 if (holder.getItemViewType() == TYPE_ACCESSIBLE_DROP) { 238 holder.mTileView.setClickable(true); 239 holder.mTileView.setFocusable(true); 240 holder.mTileView.setFocusableInTouchMode(true); 241 holder.mTileView.setVisibility(View.VISIBLE); 242 holder.mTileView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 243 holder.mTileView.setContentDescription(mContext.getString( 244 R.string.accessibility_qs_edit_position_label, position + 1)); 245 holder.mTileView.setOnClickListener(new OnClickListener() { 246 @Override 247 public void onClick(View v) { 248 selectPosition(holder.getAdapterPosition(), v); 249 } 250 }); 251 if (mNeedsFocus) { 252 // Wait for this to get laid out then set its focus. 253 // Ensure that tile gets laid out so we get the callback. 254 holder.mTileView.requestLayout(); 255 holder.mTileView.addOnLayoutChangeListener(new OnLayoutChangeListener() { 256 @Override 257 public void onLayoutChange(View v, int left, int top, int right, int bottom, 258 int oldLeft, int oldTop, int oldRight, int oldBottom) { 259 holder.mTileView.removeOnLayoutChangeListener(this); 260 holder.mTileView.requestFocus(); 261 } 262 }); 263 mNeedsFocus = false; 264 } 265 return; 266 } 267 268 TileInfo info = mTiles.get(position); 269 270 if (position > mEditIndex) { 271 info.state.contentDescription = mContext.getString( 272 R.string.accessibility_qs_edit_add_tile_label, info.state.label); 273 } else if (mAccessibilityAction != ACTION_NONE) { 274 info.state.contentDescription = mContext.getString( 275 R.string.accessibility_qs_edit_position_label, position + 1); 276 } else { 277 info.state.contentDescription = mContext.getString( 278 R.string.accessibility_qs_edit_tile_label, position + 1, info.state.label); 279 } 280 holder.mTileView.handleStateChanged(info.state); 281 holder.mTileView.setShowAppLabel(position > mEditIndex && !info.isSystem); 282 283 if (mAccessibilityManager.isTouchExplorationEnabled()) { 284 final boolean selectable = mAccessibilityAction == ACTION_NONE || position < mEditIndex; 285 holder.mTileView.setClickable(selectable); 286 holder.mTileView.setFocusable(selectable); 287 holder.mTileView.setImportantForAccessibility(selectable 288 ? View.IMPORTANT_FOR_ACCESSIBILITY_YES 289 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 290 if (selectable) { 291 holder.mTileView.setOnClickListener(new OnClickListener() { 292 @Override 293 public void onClick(View v) { 294 int position = holder.getAdapterPosition(); 295 if (mAccessibilityAction != ACTION_NONE) { 296 selectPosition(position, v); 297 } else { 298 if (position < mEditIndex && canRemoveTiles()) { 299 showAccessibilityDialog(position, v); 300 } else { 301 startAccessibleAdd(position); 302 } 303 } 304 } 305 }); 306 } 307 } 308 } 309 310 private boolean canRemoveTiles() { 311 return mCurrentSpecs.size() > MIN_NUM_TILES; 312 } 313 314 private void selectPosition(int position, View v) { 315 if (mAccessibilityAction == ACTION_ADD) { 316 // Remove the placeholder. 317 mTiles.remove(mEditIndex--); 318 notifyItemRemoved(mEditIndex); 319 // Don't remove items when the last position is selected. 320 if (position == mEditIndex - 1) position--; 321 } 322 mAccessibilityAction = ACTION_NONE; 323 move(mAccessibilityFromIndex, position, v); 324 notifyDataSetChanged(); 325 } 326 327 private void showAccessibilityDialog(final int position, final View v) { 328 final TileInfo info = mTiles.get(position); 329 CharSequence[] options = new CharSequence[] { 330 mContext.getString(R.string.accessibility_qs_edit_move_tile, info.state.label), 331 mContext.getString(R.string.accessibility_qs_edit_remove_tile, info.state.label), 332 }; 333 AlertDialog dialog = new Builder(mContext) 334 .setItems(options, new DialogInterface.OnClickListener() { 335 @Override 336 public void onClick(DialogInterface dialog, int which) { 337 if (which == 0) { 338 startAccessibleMove(position); 339 } else { 340 move(position, info.isSystem ? mEditIndex : mTileDividerIndex, v); 341 notifyItemChanged(mTileDividerIndex); 342 notifyDataSetChanged(); 343 } 344 } 345 }).setNegativeButton(android.R.string.cancel, null) 346 .create(); 347 SystemUIDialog.setShowForAllUsers(dialog, true); 348 SystemUIDialog.applyFlags(dialog); 349 dialog.show(); 350 } 351 352 private void startAccessibleAdd(int position) { 353 mAccessibilityFromIndex = position; 354 mAccessibilityAction = ACTION_ADD; 355 // Add placeholder for last slot. 356 mTiles.add(mEditIndex++, null); 357 mNeedsFocus = true; 358 notifyDataSetChanged(); 359 } 360 361 private void startAccessibleMove(int position) { 362 mAccessibilityFromIndex = position; 363 mAccessibilityAction = ACTION_MOVE; 364 notifyDataSetChanged(); 365 } 366 367 public SpanSizeLookup getSizeLookup() { 368 return mSizeLookup; 369 } 370 371 private boolean move(int from, int to, View v) { 372 if (to == from) { 373 return true; 374 } 375 CharSequence fromLabel = mTiles.get(from).state.label; 376 move(from, to, mTiles); 377 updateDividerLocations(); 378 if (to >= mEditIndex) { 379 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_REMOVE_SPEC, 380 strip(mTiles.get(to))); 381 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_REMOVE, 382 from); 383 } else if (from >= mEditIndex) { 384 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_ADD_SPEC, 385 strip(mTiles.get(to))); 386 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_ADD, 387 to); 388 v.announceForAccessibility(mContext.getString(R.string.accessibility_qs_edit_tile_added, 389 fromLabel, (to + 1))); 390 } else { 391 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_MOVE_SPEC, 392 strip(mTiles.get(to))); 393 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_MOVE, 394 to); 395 v.announceForAccessibility(mContext.getString(R.string.accessibility_qs_edit_tile_moved, 396 fromLabel, (to + 1))); 397 } 398 saveSpecs(mHost); 399 return true; 400 } 401 updateDividerLocations()402 private void updateDividerLocations() { 403 // The first null is the edit tiles label, the second null is the tile divider. 404 // If there is no second null, then there are no non-system tiles. 405 mEditIndex = -1; 406 mTileDividerIndex = mTiles.size(); 407 for (int i = 0; i < mTiles.size(); i++) { 408 if (mTiles.get(i) == null) { 409 if (mEditIndex == -1) { 410 mEditIndex = i; 411 } else { 412 mTileDividerIndex = i; 413 } 414 } 415 } 416 if (mTiles.size() - 1 == mTileDividerIndex) { 417 notifyItemChanged(mTileDividerIndex); 418 } 419 } 420 strip(TileInfo tileInfo)421 private static String strip(TileInfo tileInfo) { 422 String spec = tileInfo.spec; 423 if (spec.startsWith(CustomTile.PREFIX)) { 424 ComponentName component = CustomTile.getComponentFromSpec(spec); 425 return component.getPackageName(); 426 } 427 return spec; 428 } 429 move(int from, int to, List<T> list)430 private <T> void move(int from, int to, List<T> list) { 431 list.add(to, list.remove(from)); 432 notifyItemMoved(from, to); 433 } 434 435 public class Holder extends ViewHolder { 436 private CustomizeTileView mTileView; 437 Holder(View itemView)438 public Holder(View itemView) { 439 super(itemView); 440 if (itemView instanceof FrameLayout) { 441 mTileView = (CustomizeTileView) ((FrameLayout) itemView).getChildAt(0); 442 mTileView.setBackground(null); 443 mTileView.getIcon().disableAnimation(); 444 } 445 } 446 clearDrag()447 public void clearDrag() { 448 itemView.clearAnimation(); 449 mTileView.findViewById(R.id.tile_label).clearAnimation(); 450 mTileView.findViewById(R.id.tile_label).setAlpha(1); 451 mTileView.getAppLabel().clearAnimation(); 452 mTileView.getAppLabel().setAlpha(.6f); 453 } 454 startDrag()455 public void startDrag() { 456 itemView.animate() 457 .setDuration(DRAG_LENGTH) 458 .scaleX(DRAG_SCALE) 459 .scaleY(DRAG_SCALE); 460 mTileView.findViewById(R.id.tile_label).animate() 461 .setDuration(DRAG_LENGTH) 462 .alpha(0); 463 mTileView.getAppLabel().animate() 464 .setDuration(DRAG_LENGTH) 465 .alpha(0); 466 } 467 stopDrag()468 public void stopDrag() { 469 itemView.animate() 470 .setDuration(DRAG_LENGTH) 471 .scaleX(1) 472 .scaleY(1); 473 mTileView.findViewById(R.id.tile_label).animate() 474 .setDuration(DRAG_LENGTH) 475 .alpha(1); 476 mTileView.getAppLabel().animate() 477 .setDuration(DRAG_LENGTH) 478 .alpha(.6f); 479 } 480 } 481 482 private final SpanSizeLookup mSizeLookup = new SpanSizeLookup() { 483 @Override 484 public int getSpanSize(int position) { 485 final int type = getItemViewType(position); 486 return type == TYPE_EDIT || type == TYPE_DIVIDER ? 3 : 1; 487 } 488 }; 489 490 private class TileItemDecoration extends ItemDecoration { 491 private final ColorDrawable mDrawable; 492 TileItemDecoration(Context context)493 private TileItemDecoration(Context context) { 494 TypedArray ta = 495 context.obtainStyledAttributes(new int[]{android.R.attr.colorSecondary}); 496 mDrawable = new ColorDrawable(ta.getColor(0, 0)); 497 ta.recycle(); 498 } 499 500 501 @Override onDraw(Canvas c, RecyclerView parent, State state)502 public void onDraw(Canvas c, RecyclerView parent, State state) { 503 super.onDraw(c, parent, state); 504 505 final int childCount = parent.getChildCount(); 506 final int width = parent.getWidth(); 507 final int bottom = parent.getBottom(); 508 for (int i = 0; i < childCount; i++) { 509 final View child = parent.getChildAt(i); 510 final ViewHolder holder = parent.getChildViewHolder(child); 511 if (holder.getAdapterPosition() < mEditIndex && !(child instanceof TextView)) { 512 continue; 513 } 514 515 final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child 516 .getLayoutParams(); 517 final int top = child.getTop() + params.topMargin + 518 Math.round(ViewCompat.getTranslationY(child)); 519 // Draw full width, in case there aren't tiles all the way across. 520 mDrawable.setBounds(0, top, width, bottom); 521 mDrawable.draw(c); 522 break; 523 } 524 } 525 } 526 527 private final ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() { 528 529 @Override 530 public boolean isLongPressDragEnabled() { 531 return true; 532 } 533 534 @Override 535 public boolean isItemViewSwipeEnabled() { 536 return false; 537 } 538 539 @Override 540 public void onSelectedChanged(ViewHolder viewHolder, int actionState) { 541 super.onSelectedChanged(viewHolder, actionState); 542 if (actionState != ItemTouchHelper.ACTION_STATE_DRAG) { 543 viewHolder = null; 544 } 545 if (viewHolder == mCurrentDrag) return; 546 if (mCurrentDrag != null) { 547 int position = mCurrentDrag.getAdapterPosition(); 548 TileInfo info = mTiles.get(position); 549 mCurrentDrag.mTileView.setShowAppLabel( 550 position > mEditIndex && !info.isSystem); 551 mCurrentDrag.stopDrag(); 552 mCurrentDrag = null; 553 } 554 if (viewHolder != null) { 555 mCurrentDrag = (Holder) viewHolder; 556 mCurrentDrag.startDrag(); 557 } 558 mHandler.post(new Runnable() { 559 @Override 560 public void run() { 561 notifyItemChanged(mEditIndex); 562 } 563 }); 564 } 565 566 @Override 567 public boolean canDropOver(RecyclerView recyclerView, ViewHolder current, 568 ViewHolder target) { 569 if (!canRemoveTiles() && current.getAdapterPosition() < mEditIndex) { 570 return target.getAdapterPosition() < mEditIndex; 571 } 572 return target.getAdapterPosition() <= mEditIndex + 1; 573 } 574 575 @Override 576 public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) { 577 if (viewHolder.getItemViewType() == TYPE_EDIT || viewHolder.getItemViewType() == TYPE_DIVIDER) { 578 return makeMovementFlags(0, 0); 579 } 580 int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.RIGHT 581 | ItemTouchHelper.LEFT; 582 return makeMovementFlags(dragFlags, 0); 583 } 584 585 @Override 586 public boolean onMove(RecyclerView recyclerView, ViewHolder viewHolder, ViewHolder target) { 587 int from = viewHolder.getAdapterPosition(); 588 int to = target.getAdapterPosition(); 589 return move(from, to, target.itemView); 590 } 591 592 @Override 593 public void onSwiped(ViewHolder viewHolder, int direction) { 594 } 595 }; 596 } 597