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