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