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.content.ComponentName;
18 import android.content.Context;
19 import android.content.res.Resources;
20 import android.graphics.Canvas;
21 import android.graphics.Rect;
22 import android.graphics.drawable.Drawable;
23 import android.os.Handler;
24 import android.view.LayoutInflater;
25 import android.view.View;
26 import android.view.View.OnClickListener;
27 import android.view.View.OnLayoutChangeListener;
28 import android.view.ViewGroup;
29 import android.widget.FrameLayout;
30 import android.widget.TextView;
31 
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 import androidx.core.view.AccessibilityDelegateCompat;
35 import androidx.core.view.ViewCompat;
36 import androidx.recyclerview.widget.GridLayoutManager;
37 import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup;
38 import androidx.recyclerview.widget.ItemTouchHelper;
39 import androidx.recyclerview.widget.RecyclerView;
40 import androidx.recyclerview.widget.RecyclerView.ItemDecoration;
41 import androidx.recyclerview.widget.RecyclerView.State;
42 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
43 
44 import com.android.internal.logging.UiEventLogger;
45 import com.android.systemui.R;
46 import com.android.systemui.qs.QSEditEvent;
47 import com.android.systemui.qs.QSTileHost;
48 import com.android.systemui.qs.customize.TileAdapter.Holder;
49 import com.android.systemui.qs.customize.TileQueryHelper.TileInfo;
50 import com.android.systemui.qs.customize.TileQueryHelper.TileStateListener;
51 import com.android.systemui.qs.dagger.QSScope;
52 import com.android.systemui.qs.dagger.QSThemedContext;
53 import com.android.systemui.qs.external.CustomTile;
54 import com.android.systemui.qs.tileimpl.QSIconViewImpl;
55 import com.android.systemui.qs.tileimpl.QSTileViewImpl;
56 
57 import java.util.ArrayList;
58 import java.util.List;
59 
60 import javax.inject.Inject;
61 
62 /** */
63 @QSScope
64 public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileStateListener {
65     private static final long DRAG_LENGTH = 100;
66     private static final float DRAG_SCALE = 1.2f;
67     public static final long MOVE_DURATION = 150;
68 
69     private static final int TYPE_TILE = 0;
70     private static final int TYPE_EDIT = 1;
71     private static final int TYPE_ACCESSIBLE_DROP = 2;
72     private static final int TYPE_HEADER = 3;
73     private static final int TYPE_DIVIDER = 4;
74 
75     private static final long EDIT_ID = 10000;
76     private static final long DIVIDER_ID = 20000;
77 
78     private static final int ACTION_NONE = 0;
79     private static final int ACTION_ADD = 1;
80     private static final int ACTION_MOVE = 2;
81 
82     private static final int NUM_COLUMNS_ID = R.integer.quick_settings_num_columns;
83 
84     private final Context mContext;
85 
86     private final Handler mHandler = new Handler();
87     private final List<TileInfo> mTiles = new ArrayList<>();
88     private final ItemTouchHelper mItemTouchHelper;
89     private ItemDecoration mDecoration;
90     private final MarginTileDecoration mMarginDecoration;
91     private final int mMinNumTiles;
92     private final QSTileHost mHost;
93     private int mEditIndex;
94     private int mTileDividerIndex;
95     private int mFocusIndex;
96 
97     private boolean mNeedsFocus;
98     private List<String> mCurrentSpecs;
99     private List<TileInfo> mOtherTiles;
100     private List<TileInfo> mAllTiles;
101 
102     private Holder mCurrentDrag;
103     private int mAccessibilityAction = ACTION_NONE;
104     private int mAccessibilityFromIndex;
105     private final UiEventLogger mUiEventLogger;
106     private final AccessibilityDelegateCompat mAccessibilityDelegate;
107     private RecyclerView mRecyclerView;
108     private int mNumColumns;
109 
110     @Inject
TileAdapter( @SThemedContext Context context, QSTileHost qsHost, UiEventLogger uiEventLogger)111     public TileAdapter(
112             @QSThemedContext Context context,
113             QSTileHost qsHost,
114             UiEventLogger uiEventLogger) {
115         mContext = context;
116         mHost = qsHost;
117         mUiEventLogger = uiEventLogger;
118         mItemTouchHelper = new ItemTouchHelper(mCallbacks);
119         mDecoration = new TileItemDecoration(context);
120         mMarginDecoration = new MarginTileDecoration();
121         mMinNumTiles = context.getResources().getInteger(R.integer.quick_settings_min_num_tiles);
122         mNumColumns = context.getResources().getInteger(NUM_COLUMNS_ID);
123         mAccessibilityDelegate = new TileAdapterDelegate();
124         mSizeLookup.setSpanIndexCacheEnabled(true);
125     }
126 
127     @Override
onAttachedToRecyclerView(@onNull RecyclerView recyclerView)128     public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
129         mRecyclerView = recyclerView;
130     }
131 
132     @Override
onDetachedFromRecyclerView(@onNull RecyclerView recyclerView)133     public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
134         mRecyclerView = null;
135     }
136 
137     /**
138      * Update the number of columns to show, from resources.
139      *
140      * @return {@code true} if the number of columns changed, {@code false} otherwise
141      */
updateNumColumns()142     public boolean updateNumColumns() {
143         int numColumns = mContext.getResources().getInteger(NUM_COLUMNS_ID);
144         if (numColumns != mNumColumns) {
145             mNumColumns = numColumns;
146             return true;
147         } else {
148             return false;
149         }
150     }
151 
getNumColumns()152     public int getNumColumns() {
153         return mNumColumns;
154     }
155 
getItemTouchHelper()156     public ItemTouchHelper getItemTouchHelper() {
157         return mItemTouchHelper;
158     }
159 
getItemDecoration()160     public ItemDecoration getItemDecoration() {
161         return mDecoration;
162     }
163 
getMarginItemDecoration()164     public ItemDecoration getMarginItemDecoration() {
165         return mMarginDecoration;
166     }
167 
changeHalfMargin(int halfMargin)168     public void changeHalfMargin(int halfMargin) {
169         mMarginDecoration.setHalfMargin(halfMargin);
170     }
171 
saveSpecs(QSTileHost host)172     public void saveSpecs(QSTileHost host) {
173         List<String> newSpecs = new ArrayList<>();
174         clearAccessibilityState();
175         for (int i = 1; i < mTiles.size() && mTiles.get(i) != null; i++) {
176             newSpecs.add(mTiles.get(i).spec);
177         }
178         host.changeTiles(mCurrentSpecs, newSpecs);
179         mCurrentSpecs = newSpecs;
180     }
181 
clearAccessibilityState()182     private void clearAccessibilityState() {
183         if (mAccessibilityAction == ACTION_ADD) {
184             // Remove blank tile from last spot
185             mTiles.remove(--mEditIndex);
186             // Update the tile divider position
187             notifyDataSetChanged();
188         }
189         mAccessibilityAction = ACTION_NONE;
190     }
191 
192     /** */
resetTileSpecs(List<String> specs)193     public void resetTileSpecs(List<String> specs) {
194         // Notify the host so the tiles get removed callbacks.
195         mHost.changeTiles(mCurrentSpecs, specs);
196         setTileSpecs(specs);
197     }
198 
setTileSpecs(List<String> currentSpecs)199     public void setTileSpecs(List<String> currentSpecs) {
200         if (currentSpecs.equals(mCurrentSpecs)) {
201             return;
202         }
203         mCurrentSpecs = currentSpecs;
204         recalcSpecs();
205     }
206 
207     @Override
onTilesChanged(List<TileInfo> tiles)208     public void onTilesChanged(List<TileInfo> tiles) {
209         mAllTiles = tiles;
210         recalcSpecs();
211     }
212 
recalcSpecs()213     private void recalcSpecs() {
214         if (mCurrentSpecs == null || mAllTiles == null) {
215             return;
216         }
217         mOtherTiles = new ArrayList<TileInfo>(mAllTiles);
218         mTiles.clear();
219         mTiles.add(null);
220         for (int i = 0; i < mCurrentSpecs.size(); i++) {
221             final TileInfo tile = getAndRemoveOther(mCurrentSpecs.get(i));
222             if (tile != null) {
223                 mTiles.add(tile);
224             }
225         }
226         mTiles.add(null);
227         for (int i = 0; i < mOtherTiles.size(); i++) {
228             final TileInfo tile = mOtherTiles.get(i);
229             if (tile.isSystem) {
230                 mOtherTiles.remove(i--);
231                 mTiles.add(tile);
232             }
233         }
234         mTileDividerIndex = mTiles.size();
235         mTiles.add(null);
236         mTiles.addAll(mOtherTiles);
237         updateDividerLocations();
238         notifyDataSetChanged();
239     }
240 
getAndRemoveOther(String s)241     private TileInfo getAndRemoveOther(String s) {
242         for (int i = 0; i < mOtherTiles.size(); i++) {
243             if (mOtherTiles.get(i).spec.equals(s)) {
244                 return mOtherTiles.remove(i);
245             }
246         }
247         return null;
248     }
249 
250     @Override
getItemViewType(int position)251     public int getItemViewType(int position) {
252         if (position == 0) {
253             return TYPE_HEADER;
254         }
255         if (mAccessibilityAction == ACTION_ADD && position == mEditIndex - 1) {
256             return TYPE_ACCESSIBLE_DROP;
257         }
258         if (position == mTileDividerIndex) {
259             return TYPE_DIVIDER;
260         }
261         if (mTiles.get(position) == null) {
262             return TYPE_EDIT;
263         }
264         return TYPE_TILE;
265     }
266 
267     @Override
onCreateViewHolder(ViewGroup parent, int viewType)268     public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
269         final Context context = parent.getContext();
270         LayoutInflater inflater = LayoutInflater.from(context);
271         if (viewType == TYPE_HEADER) {
272             return new Holder(inflater.inflate(R.layout.qs_customize_header, parent, false));
273         }
274         if (viewType == TYPE_DIVIDER) {
275             return new Holder(inflater.inflate(R.layout.qs_customize_tile_divider, parent, false));
276         }
277         if (viewType == TYPE_EDIT) {
278             return new Holder(inflater.inflate(R.layout.qs_customize_divider, parent, false));
279         }
280         FrameLayout frame = (FrameLayout) inflater.inflate(R.layout.qs_customize_tile_frame, parent,
281                 false);
282         View view = new CustomizeTileView(context, new QSIconViewImpl(context));
283         frame.addView(view);
284         return new Holder(frame);
285     }
286 
287     @Override
getItemCount()288     public int getItemCount() {
289         return mTiles.size();
290     }
291 
292     @Override
onFailedToRecycleView(Holder holder)293     public boolean onFailedToRecycleView(Holder holder) {
294         holder.stopDrag();
295         holder.clearDrag();
296         return true;
297     }
298 
setSelectableForHeaders(View view)299     private void setSelectableForHeaders(View view) {
300         final boolean selectable = mAccessibilityAction == ACTION_NONE;
301         view.setFocusable(selectable);
302         view.setImportantForAccessibility(selectable
303                 ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
304                 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
305         view.setFocusableInTouchMode(selectable);
306     }
307 
308     @Override
onBindViewHolder(final Holder holder, int position)309     public void onBindViewHolder(final Holder holder, int position) {
310         if (holder.getItemViewType() == TYPE_HEADER) {
311             setSelectableForHeaders(holder.itemView);
312             return;
313         }
314         if (holder.getItemViewType() == TYPE_DIVIDER) {
315             holder.itemView.setVisibility(mTileDividerIndex < mTiles.size() - 1 ? View.VISIBLE
316                     : View.INVISIBLE);
317             return;
318         }
319         if (holder.getItemViewType() == TYPE_EDIT) {
320             final String titleText;
321             Resources res = mContext.getResources();
322             if (mCurrentDrag == null) {
323                 titleText = res.getString(R.string.drag_to_add_tiles);
324             } else if (!canRemoveTiles() && mCurrentDrag.getAdapterPosition() < mEditIndex) {
325                 titleText = res.getString(R.string.drag_to_remove_disabled, mMinNumTiles);
326             } else {
327                 titleText = res.getString(R.string.drag_to_remove_tiles);
328             }
329 
330             ((TextView) holder.itemView.findViewById(android.R.id.title)).setText(titleText);
331             setSelectableForHeaders(holder.itemView);
332 
333             return;
334         }
335         if (holder.getItemViewType() == TYPE_ACCESSIBLE_DROP) {
336             holder.mTileView.setClickable(true);
337             holder.mTileView.setFocusable(true);
338             holder.mTileView.setFocusableInTouchMode(true);
339             holder.mTileView.setVisibility(View.VISIBLE);
340             holder.mTileView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
341             holder.mTileView.setContentDescription(mContext.getString(
342                     R.string.accessibility_qs_edit_tile_add_to_position, position));
343             holder.mTileView.setOnClickListener(new OnClickListener() {
344                 @Override
345                 public void onClick(View v) {
346                     selectPosition(holder.getLayoutPosition());
347                 }
348             });
349             focusOnHolder(holder);
350             return;
351         }
352 
353         TileInfo info = mTiles.get(position);
354 
355         final boolean selectable = 0 < position && position < mEditIndex;
356         if (selectable && mAccessibilityAction == ACTION_ADD) {
357             info.state.contentDescription = mContext.getString(
358                     R.string.accessibility_qs_edit_tile_add_to_position, position);
359         } else if (selectable && mAccessibilityAction == ACTION_MOVE) {
360             info.state.contentDescription = mContext.getString(
361                     R.string.accessibility_qs_edit_tile_move_to_position, position);
362         } else {
363             info.state.contentDescription = info.state.label;
364         }
365         info.state.expandedAccessibilityClassName = "";
366 
367         // The holder has a tileView, therefore this call is not null
368         holder.getTileAsCustomizeView().changeState(info.state);
369         holder.getTileAsCustomizeView().setShowAppLabel(position > mEditIndex && !info.isSystem);
370         // Don't show the side view for third party tiles, as we don't have the actual state.
371         holder.getTileAsCustomizeView().setShowSideView(position < mEditIndex || info.isSystem);
372         holder.mTileView.setSelected(true);
373         holder.mTileView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
374         holder.mTileView.setClickable(true);
375         holder.mTileView.setOnClickListener(null);
376         holder.mTileView.setFocusable(true);
377         holder.mTileView.setFocusableInTouchMode(true);
378 
379         if (mAccessibilityAction != ACTION_NONE) {
380             holder.mTileView.setClickable(selectable);
381             holder.mTileView.setFocusable(selectable);
382             holder.mTileView.setFocusableInTouchMode(selectable);
383             holder.mTileView.setImportantForAccessibility(selectable
384                     ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
385                     : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
386             if (selectable) {
387                 holder.mTileView.setOnClickListener(new OnClickListener() {
388                     @Override
389                     public void onClick(View v) {
390                         int position = holder.getLayoutPosition();
391                         if (position == RecyclerView.NO_POSITION) return;
392                         if (mAccessibilityAction != ACTION_NONE) {
393                             selectPosition(position);
394                         }
395                     }
396                 });
397             }
398         }
399         if (position == mFocusIndex) {
400             focusOnHolder(holder);
401         }
402     }
403 
404     private void focusOnHolder(Holder holder) {
405         if (mNeedsFocus) {
406             // Wait for this to get laid out then set its focus.
407             // Ensure that tile gets laid out so we get the callback.
408             holder.mTileView.requestLayout();
409             holder.mTileView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
410                 @Override
411                 public void onLayoutChange(View v, int left, int top, int right, int bottom,
412                         int oldLeft, int oldTop, int oldRight, int oldBottom) {
413                     holder.mTileView.removeOnLayoutChangeListener(this);
414                     holder.mTileView.requestFocus();
415                     if (mAccessibilityAction == ACTION_NONE) {
416                         holder.mTileView.clearFocus();
417                     }
418                 }
419             });
420             mNeedsFocus = false;
421             mFocusIndex = RecyclerView.NO_POSITION;
422         }
423     }
424 
425     private boolean canRemoveTiles() {
426         return mCurrentSpecs.size() > mMinNumTiles;
427     }
428 
429     private void selectPosition(int position) {
430         if (mAccessibilityAction == ACTION_ADD) {
431             // Remove the placeholder.
432             mTiles.remove(mEditIndex--);
433         }
434         mAccessibilityAction = ACTION_NONE;
435         move(mAccessibilityFromIndex, position, false);
436         mFocusIndex = position;
437         mNeedsFocus = true;
438         notifyDataSetChanged();
439     }
440 
441     private void startAccessibleAdd(int position) {
442         mAccessibilityFromIndex = position;
443         mAccessibilityAction = ACTION_ADD;
444         // Add placeholder for last slot.
445         mTiles.add(mEditIndex++, null);
446         // Update the tile divider position
447         mTileDividerIndex++;
448         mFocusIndex = mEditIndex - 1;
449         mNeedsFocus = true;
450         if (mRecyclerView != null) {
451             mRecyclerView.post(() -> mRecyclerView.smoothScrollToPosition(mFocusIndex));
452         }
453         notifyDataSetChanged();
454     }
455 
456     private void startAccessibleMove(int position) {
457         mAccessibilityFromIndex = position;
458         mAccessibilityAction = ACTION_MOVE;
459         mFocusIndex = position;
460         mNeedsFocus = true;
461         notifyDataSetChanged();
462     }
463 
464     private boolean canRemoveFromPosition(int position) {
465         return canRemoveTiles() && isCurrentTile(position);
466     }
467 
468     private boolean isCurrentTile(int position) {
469         return position < mEditIndex;
470     }
471 
472     private boolean canAddFromPosition(int position) {
473         return position > mEditIndex;
474     }
475 
476     private boolean addFromPosition(int position) {
477         if (!canAddFromPosition(position)) return false;
478         move(position, mEditIndex);
479         return true;
480     }
481 
482     private boolean removeFromPosition(int position) {
483         if (!canRemoveFromPosition(position)) return false;
484         TileInfo info = mTiles.get(position);
485         move(position, info.isSystem ? mEditIndex : mTileDividerIndex);
486         return true;
487     }
488 
489     public SpanSizeLookup getSizeLookup() {
490         return mSizeLookup;
491     }
492 
493     private boolean move(int from, int to) {
494         return move(from, to, true);
495     }
496 
497     private boolean move(int from, int to, boolean notify) {
498         if (to == from) {
499             return true;
500         }
501         move(from, to, mTiles, notify);
502         updateDividerLocations();
503         if (to >= mEditIndex) {
504             mUiEventLogger.log(QSEditEvent.QS_EDIT_REMOVE, 0, strip(mTiles.get(to)));
505         } else if (from >= mEditIndex) {
506             mUiEventLogger.log(QSEditEvent.QS_EDIT_ADD, 0, strip(mTiles.get(to)));
507         } else {
508             mUiEventLogger.log(QSEditEvent.QS_EDIT_MOVE, 0, strip(mTiles.get(to)));
509         }
510         saveSpecs(mHost);
511         return true;
512     }
513 
updateDividerLocations()514     private void updateDividerLocations() {
515         // The first null is the header label (index 0) so we can skip it,
516         // the second null is the edit tiles label, the third null is the tile divider.
517         // If there is no third null, then there are no non-system tiles.
518         mEditIndex = -1;
519         mTileDividerIndex = mTiles.size();
520         for (int i = 1; i < mTiles.size(); i++) {
521             if (mTiles.get(i) == null) {
522                 if (mEditIndex == -1) {
523                     mEditIndex = i;
524                 } else {
525                     mTileDividerIndex = i;
526                 }
527             }
528         }
529         if (mTiles.size() - 1 == mTileDividerIndex) {
530             notifyItemChanged(mTileDividerIndex);
531         }
532     }
533 
strip(TileInfo tileInfo)534     private static String strip(TileInfo tileInfo) {
535         String spec = tileInfo.spec;
536         if (spec.startsWith(CustomTile.PREFIX)) {
537             ComponentName component = CustomTile.getComponentFromSpec(spec);
538             return component.getPackageName();
539         }
540         return spec;
541     }
542 
move(int from, int to, List<T> list, boolean notify)543     private <T> void move(int from, int to, List<T> list, boolean notify) {
544         list.add(to, list.remove(from));
545         if (notify) {
546             notifyItemMoved(from, to);
547         }
548     }
549 
550     public class Holder extends ViewHolder {
551         private QSTileViewImpl mTileView;
552 
Holder(View itemView)553         public Holder(View itemView) {
554             super(itemView);
555             if (itemView instanceof FrameLayout) {
556                 mTileView = (QSTileViewImpl) ((FrameLayout) itemView).getChildAt(0);
557                 mTileView.getIcon().disableAnimation();
558                 mTileView.setTag(this);
559                 ViewCompat.setAccessibilityDelegate(mTileView, mAccessibilityDelegate);
560             }
561         }
562 
563         @Nullable
getTileAsCustomizeView()564         public CustomizeTileView getTileAsCustomizeView() {
565             return (CustomizeTileView) mTileView;
566         }
567 
clearDrag()568         public void clearDrag() {
569             itemView.clearAnimation();
570             itemView.setScaleX(1);
571             itemView.setScaleY(1);
572         }
573 
startDrag()574         public void startDrag() {
575             itemView.animate()
576                     .setDuration(DRAG_LENGTH)
577                     .scaleX(DRAG_SCALE)
578                     .scaleY(DRAG_SCALE);
579         }
580 
stopDrag()581         public void stopDrag() {
582             itemView.animate()
583                     .setDuration(DRAG_LENGTH)
584                     .scaleX(1)
585                     .scaleY(1);
586         }
587 
canRemove()588         boolean canRemove() {
589             return canRemoveFromPosition(getLayoutPosition());
590         }
591 
canAdd()592         boolean canAdd() {
593             return canAddFromPosition(getLayoutPosition());
594         }
595 
toggleState()596         void toggleState() {
597             if (canAdd()) {
598                 add();
599             } else {
600                 remove();
601             }
602         }
603 
add()604         private void add() {
605             if (addFromPosition(getLayoutPosition())) {
606                 itemView.announceForAccessibility(
607                         itemView.getContext().getText(R.string.accessibility_qs_edit_tile_added));
608             }
609         }
610 
remove()611         private void remove() {
612             if (removeFromPosition(getLayoutPosition())) {
613                 itemView.announceForAccessibility(
614                         itemView.getContext().getText(R.string.accessibility_qs_edit_tile_removed));
615             }
616         }
617 
isCurrentTile()618         boolean isCurrentTile() {
619             return TileAdapter.this.isCurrentTile(getLayoutPosition());
620         }
621 
startAccessibleAdd()622         void startAccessibleAdd() {
623             TileAdapter.this.startAccessibleAdd(getLayoutPosition());
624         }
625 
startAccessibleMove()626         void startAccessibleMove() {
627             TileAdapter.this.startAccessibleMove(getLayoutPosition());
628         }
629 
canTakeAccessibleAction()630         boolean canTakeAccessibleAction() {
631             return mAccessibilityAction == ACTION_NONE;
632         }
633     }
634 
635     private final SpanSizeLookup mSizeLookup = new SpanSizeLookup() {
636         @Override
637         public int getSpanSize(int position) {
638             final int type = getItemViewType(position);
639             if (type == TYPE_EDIT || type == TYPE_DIVIDER || type == TYPE_HEADER) {
640                 return mNumColumns;
641             } else {
642                 return 1;
643             }
644         }
645     };
646 
647     private class TileItemDecoration extends ItemDecoration {
648         private final Drawable mDrawable;
649 
TileItemDecoration(Context context)650         private TileItemDecoration(Context context) {
651             mDrawable = context.getDrawable(R.drawable.qs_customize_tile_decoration);
652         }
653 
654         @Override
onDraw(Canvas c, RecyclerView parent, State state)655         public void onDraw(Canvas c, RecyclerView parent, State state) {
656             super.onDraw(c, parent, state);
657 
658             final int childCount = parent.getChildCount();
659             final int width = parent.getWidth();
660             final int bottom = parent.getBottom();
661             for (int i = 0; i < childCount; i++) {
662                 final View child = parent.getChildAt(i);
663                 final ViewHolder holder = parent.getChildViewHolder(child);
664                 // Do not draw background for the holder that's currently being dragged
665                 if (holder == mCurrentDrag) {
666                     continue;
667                 }
668                 // Do not draw background for holders before the edit index (header and current
669                 // tiles)
670                 if (holder.getAdapterPosition() == 0 ||
671                         holder.getAdapterPosition() < mEditIndex && !(child instanceof TextView)) {
672                     continue;
673                 }
674 
675                 final int top = child.getTop() + Math.round(ViewCompat.getTranslationY(child));
676                 mDrawable.setBounds(0, top, width, bottom);
677                 mDrawable.draw(c);
678                 break;
679             }
680         }
681     }
682 
683     private static class MarginTileDecoration extends ItemDecoration {
684         private int mHalfMargin;
685 
setHalfMargin(int halfMargin)686         public void setHalfMargin(int halfMargin) {
687             mHalfMargin = halfMargin;
688         }
689 
690         @Override
getItemOffsets(@onNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull State state)691         public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
692                 @NonNull RecyclerView parent, @NonNull State state) {
693             if (parent.getLayoutManager() == null) return;
694 
695             GridLayoutManager lm = ((GridLayoutManager) parent.getLayoutManager());
696             int column = ((GridLayoutManager.LayoutParams) view.getLayoutParams()).getSpanIndex();
697 
698             if (view instanceof TextView) {
699                 super.getItemOffsets(outRect, view, parent, state);
700             } else {
701                 if (column != 0 && column != lm.getSpanCount() - 1) {
702                     // In a column that's not leftmost or rightmost (half of the margin between
703                     // columns).
704                     outRect.left = mHalfMargin;
705                     outRect.right = mHalfMargin;
706                 } else {
707                     // Leftmost or rightmost column
708                     if (parent.isLayoutRtl()) {
709                         if (column == 0) {
710                             // Rightmost column
711                             outRect.left = mHalfMargin;
712                             outRect.right = 0;
713                         } else {
714                             // Leftmost column
715                             outRect.left = 0;
716                             outRect.right = mHalfMargin;
717                         }
718                     } else {
719                         // Non RTL
720                         if (column == 0) {
721                             // Leftmost column
722                             outRect.left = 0;
723                             outRect.right = mHalfMargin;
724                         } else {
725                             // Rightmost column
726                             outRect.left = mHalfMargin;
727                             outRect.right = 0;
728                         }
729                     }
730                 }
731             }
732         }
733     }
734 
735     private final ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() {
736 
737         @Override
738         public boolean isLongPressDragEnabled() {
739             return true;
740         }
741 
742         @Override
743         public boolean isItemViewSwipeEnabled() {
744             return false;
745         }
746 
747         @Override
748         public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
749             super.onSelectedChanged(viewHolder, actionState);
750             if (actionState != ItemTouchHelper.ACTION_STATE_DRAG) {
751                 viewHolder = null;
752             }
753             if (viewHolder == mCurrentDrag) return;
754             if (mCurrentDrag != null) {
755                 int position = mCurrentDrag.getAdapterPosition();
756                 if (position == RecyclerView.NO_POSITION) return;
757                 TileInfo info = mTiles.get(position);
758                 ((CustomizeTileView) mCurrentDrag.mTileView).setShowAppLabel(
759                         position > mEditIndex && !info.isSystem);
760                 mCurrentDrag.stopDrag();
761                 mCurrentDrag = null;
762             }
763             if (viewHolder != null) {
764                 mCurrentDrag = (Holder) viewHolder;
765                 mCurrentDrag.startDrag();
766             }
767             mHandler.post(new Runnable() {
768                 @Override
769                 public void run() {
770                     notifyItemChanged(mEditIndex);
771                 }
772             });
773         }
774 
775         @Override
776         public boolean canDropOver(RecyclerView recyclerView, ViewHolder current,
777                 ViewHolder target) {
778             final int position = target.getAdapterPosition();
779             if (position == 0 || position == RecyclerView.NO_POSITION){
780                 return false;
781             }
782             if (!canRemoveTiles() && current.getAdapterPosition() < mEditIndex) {
783                 return position < mEditIndex;
784             }
785             return position <= mEditIndex + 1;
786         }
787 
788         @Override
789         public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) {
790             switch (viewHolder.getItemViewType()) {
791                 case TYPE_EDIT:
792                 case TYPE_DIVIDER:
793                 case TYPE_HEADER:
794                     // Fall through
795                     return makeMovementFlags(0, 0);
796                 default:
797                     int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN
798                             | ItemTouchHelper.RIGHT | ItemTouchHelper.LEFT;
799                     return makeMovementFlags(dragFlags, 0);
800             }
801         }
802 
803         @Override
804         public boolean onMove(RecyclerView recyclerView, ViewHolder viewHolder, ViewHolder target) {
805             int from = viewHolder.getAdapterPosition();
806             int to = target.getAdapterPosition();
807             if (from == 0 || from == RecyclerView.NO_POSITION ||
808                     to == 0 || to == RecyclerView.NO_POSITION) {
809                 return false;
810             }
811             return move(from, to);
812         }
813 
814         @Override
815         public void onSwiped(ViewHolder viewHolder, int direction) {
816         }
817 
818         // Just in case, make sure to animate to base state.
819         @Override
820         public void clearView(@NonNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder) {
821             ((Holder) viewHolder).stopDrag();
822             super.clearView(recyclerView, viewHolder);
823         }
824     };
825 }
826