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