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