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