2 * Copyright (C) 2016 The Android Open Source Project
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
7 * http://www.apache.org/licenses/LICENSE-2.0
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.
15 package com.android.systemui.qs.customize;
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;
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;
53 import java.util.ArrayList;
54 import java.util.List;
56 public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileStateListener {
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;
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;
67 private static final long EDIT_ID = 10000;
68 private static final long DIVIDER_ID = 20000;
70 private final Context mContext;
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;
84 private Holder mCurrentDrag;
85 private boolean mAccessibilityMoving;
86 private int mAccessibilityFromIndex;
87 private QSTileHost mHost;
89 public TileAdapter(Context context) {
91 mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
92 mItemTouchHelper = new ItemTouchHelper(mCallbacks);
93 mDecoration = new TileItemDecoration(context);
96 public void setHost(QSTileHost host) {
100 public ItemTouchHelper getItemTouchHelper() {
101 return mItemTouchHelper;
104 public ItemDecoration getItemDecoration() {
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);
113 host.changeTiles(mCurrentSpecs, newSpecs);
114 mCurrentSpecs = newSpecs;
117 public void setTileSpecs(List<String> currentSpecs) {
118 if (currentSpecs.equals(mCurrentSpecs)) {
121 mCurrentSpecs = currentSpecs;
126 public void onTilesChanged(List<TileInfo> tiles) {
131 private void recalcSpecs() {
132 if (mCurrentSpecs == null || mAllTiles == null) {
135 mOtherTiles = new ArrayList<TileInfo>(mAllTiles);
137 for (int i = 0; i < mCurrentSpecs.size(); i++) {
138 final TileInfo tile = getAndRemoveOther(mCurrentSpecs.get(i));
144 for (int i = 0; i < mOtherTiles.size(); i++) {
145 final TileInfo tile = mOtherTiles.get(i);
147 mOtherTiles.remove(i--);
151 mTileDividerIndex = mTiles.size();
153 mTiles.addAll(mOtherTiles);
154 updateDividerLocations();
155 notifyDataSetChanged();
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);
168 public int getItemViewType(int position) {
169 if (mAccessibilityMoving && position == mEditIndex - 1) {
170 return TYPE_ACCESSIBLE_DROP;
172 if (position == mTileDividerIndex) {
175 if (mTiles.get(position) == null) {
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));
188 if (viewType == TYPE_EDIT) {
189 return new Holder(inflater.inflate(R.layout.qs_customize_divider, parent, false));
191 FrameLayout frame = (FrameLayout) inflater.inflate(R.layout.qs_customize_tile_frame, parent,
193 frame.addView(new CustomizeTileView(context, new QSIconView(context)));
194 return new Holder(frame);
198 public int getItemCount() {
199 return mTiles.size();
203 public boolean onFailedToRecycleView(Holder holder) {
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
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);
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() {
231 public void onClick(View v) {
232 selectPosition(holder.getAdapterPosition(), v);
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() {
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();
252 TileInfo info = mTiles.get(position);
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);
261 info.state.contentDescription = mContext.getString(
262 R.string.accessibility_qs_edit_tile_label, position + 1, info.state.label);
264 holder.mTileView.onStateChanged(info.state);
265 holder.mTileView.setAppLabel(info.appLabel);
266 holder.mTileView.setShowAppLabel(position > mEditIndex && !info.isSystem);
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);
276 holder.mTileView.setOnClickListener(new OnClickListener() {
278 public void onClick(View v) {
279 int position = holder.getAdapterPosition();
280 if (mAccessibilityMoving) {
281 selectPosition(position, v);
283 if (position < mEditIndex) {
284 showAccessibilityDialog(position, v);
286 startAccessibleDrag(position);
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--;
303 move(mAccessibilityFromIndex, position, v);
304 notifyDataSetChanged();
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),
313 AlertDialog dialog = new Builder(mContext)
314 .setItems(options, new DialogInterface.OnClickListener() {
316 public void onClick(DialogInterface dialog, int which) {
318 startAccessibleDrag(position);
320 move(position, info.isSystem ? mEditIndex : mTileDividerIndex, v);
321 notifyItemChanged(mTileDividerIndex);
322 notifyDataSetChanged();
325 }).setNegativeButton(android.R.string.cancel, null)
327 SystemUIDialog.setShowForAllUsers(dialog, true);
328 SystemUIDialog.applyFlags(dialog);
332 private void startAccessibleDrag(int position) {
333 mAccessibilityMoving = true;
335 mAccessibilityFromIndex = position;
336 // Add placeholder for last slot.
337 mTiles.add(mEditIndex++, null);
338 notifyDataSetChanged();
341 public SpanSizeLookup getSizeLookup() {
345 private boolean move(int from, int to, View v) {
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,
358 announcement = mContext.getString(R.string.accessibility_qs_edit_tile_removed,
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,
365 announcement = mContext.getString(R.string.accessibility_qs_edit_tile_added,
366 fromLabel, (to + 1));
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,
372 announcement = mContext.getString(R.string.accessibility_qs_edit_tile_moved,
373 fromLabel, (to + 1));
375 v.announceForAccessibility(announcement);
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.
384 mTileDividerIndex = mTiles.size();
385 for (int i = 0; i < mTiles.size(); i++) {
386 if (mTiles.get(i) == null) {
387 if (mEditIndex == -1) {
390 mTileDividerIndex = i;
394 if (mTiles.size() - 1 == mTileDividerIndex) {
395 notifyItemChanged(mTileDividerIndex);
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();
408 private <T> void move(int from, int to, List<T> list) {
409 list.add(to, list.remove(from));
410 notifyItemMoved(from, to);
413 public class Holder extends ViewHolder {
414 private CustomizeTileView mTileView;
416 public Holder(View itemView) {
418 if (itemView instanceof FrameLayout) {
419 mTileView = (CustomizeTileView) ((FrameLayout) itemView).getChildAt(0);
420 mTileView.setBackground(null);
421 mTileView.getIcon().disableAnimation();
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);
433 public void startDrag() {
435 .setDuration(DRAG_LENGTH)
438 mTileView.findViewById(R.id.tile_label).animate()
439 .setDuration(DRAG_LENGTH)
441 mTileView.getAppLabel().animate()
442 .setDuration(DRAG_LENGTH)
446 public void stopDrag() {
448 .setDuration(DRAG_LENGTH)
451 mTileView.findViewById(R.id.tile_label).animate()
452 .setDuration(DRAG_LENGTH)
454 mTileView.getAppLabel().animate()
455 .setDuration(DRAG_LENGTH)
460 private final SpanSizeLookup mSizeLookup = new SpanSizeLookup() {
462 public int getSpanSize(int position) {
463 final int type = getItemViewType(position);
464 return type == TYPE_EDIT || type == TYPE_DIVIDER ? 3 : 1;
468 private class TileItemDecoration extends ItemDecoration {
469 private final ColorDrawable mDrawable;
471 private TileItemDecoration(Context context) {
473 context.obtainStyledAttributes(new int[]{android.R.attr.colorSecondary});
474 mDrawable = new ColorDrawable(ta.getColor(0, 0));
480 public void onDraw(Canvas c, RecyclerView parent, State state) {
481 super.onDraw(c, parent, state);
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)) {
493 final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
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);
505 private final ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() {
508 public boolean isLongPressDragEnabled() {
513 public boolean isItemViewSwipeEnabled() {
518 public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
519 super.onSelectedChanged(viewHolder, actionState);
520 if (actionState != ItemTouchHelper.ACTION_STATE_DRAG) {
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();
532 if (viewHolder != null) {
533 mCurrentDrag = (Holder) viewHolder;
534 mCurrentDrag.startDrag();
536 mHandler.post(new Runnable() {
539 notifyItemChanged(mEditIndex);
545 public boolean canDropOver(RecyclerView recyclerView, ViewHolder current,
547 return target.getAdapterPosition() <= mEditIndex + 1;
551 public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) {
552 if (viewHolder.getItemViewType() == TYPE_EDIT) {
553 return makeMovementFlags(0, 0);
555 int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.RIGHT
556 | ItemTouchHelper.LEFT;
557 return makeMovementFlags(dragFlags, 0);
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);
568 public void onSwiped(ViewHolder viewHolder, int direction) {