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