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