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