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