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