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