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
15package com.android.systemui.qs.customize;
16
17import android.app.AlertDialog;
18import android.app.AlertDialog.Builder;
19import android.content.ComponentName;
20import android.content.Context;
21import android.content.DialogInterface;
22import android.content.res.TypedArray;
23import android.graphics.Canvas;
24import android.graphics.drawable.ColorDrawable;
25import android.os.Handler;
26import android.support.v4.view.ViewCompat;
27import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
28import android.support.v7.widget.RecyclerView;
29import android.support.v7.widget.RecyclerView.ItemDecoration;
30import android.support.v7.widget.RecyclerView.State;
31import android.support.v7.widget.RecyclerView.ViewHolder;
32import android.support.v7.widget.helper.ItemTouchHelper;
33import android.view.LayoutInflater;
34import android.view.View;
35import android.view.View.OnClickListener;
36import android.view.View.OnLayoutChangeListener;
37import android.view.ViewGroup;
38import android.view.accessibility.AccessibilityManager;
39import android.widget.FrameLayout;
40import android.widget.TextView;
41
42import com.android.internal.logging.MetricsLogger;
43import com.android.internal.logging.nano.MetricsProto;
44import com.android.systemui.R;
45import com.android.systemui.qs.QSTileHost;
46import com.android.systemui.qs.customize.TileAdapter.Holder;
47import com.android.systemui.qs.customize.TileQueryHelper.TileInfo;
48import com.android.systemui.qs.customize.TileQueryHelper.TileStateListener;
49import com.android.systemui.qs.external.CustomTile;
50import com.android.systemui.qs.tileimpl.QSIconViewImpl;
51import com.android.systemui.statusbar.phone.SystemUIDialog;
52
53import java.util.ArrayList;
54import java.util.List;
55
56public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileStateListener {
57    private static final int MIN_NUM_TILES = 6;
58    private static final long DRAG_LENGTH = 100;
59    private static final float DRAG_SCALE = 1.2f;
60    public static final long MOVE_DURATION = 150;
61
62    private static final int TYPE_TILE = 0;
63    private static final int TYPE_EDIT = 1;
64    private static final int TYPE_ACCESSIBLE_DROP = 2;
65    private static final int TYPE_DIVIDER = 4;
66
67    private static final long EDIT_ID = 10000;
68    private static final long DIVIDER_ID = 20000;
69
70    private static final int ACTION_NONE = 0;
71    private static final int ACTION_ADD = 1;
72    private static final int ACTION_MOVE = 2;
73
74    private final Context mContext;
75
76    private final Handler mHandler = new Handler();
77    private final List<TileInfo> mTiles = new ArrayList<>();
78    private final ItemTouchHelper mItemTouchHelper;
79    private final ItemDecoration mDecoration;
80    private final AccessibilityManager mAccessibilityManager;
81    private int mEditIndex;
82    private int mTileDividerIndex;
83    private boolean mNeedsFocus;
84    private List<String> mCurrentSpecs;
85    private List<TileInfo> mOtherTiles;
86    private List<TileInfo> mAllTiles;
87
88    private Holder mCurrentDrag;
89    private int mAccessibilityAction = ACTION_NONE;
90    private int mAccessibilityFromIndex;
91    private QSTileHost mHost;
92
93    public TileAdapter(Context context) {
94        mContext = context;
95        mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
96        mItemTouchHelper = new ItemTouchHelper(mCallbacks);
97        mDecoration = new TileItemDecoration(context);
98    }
99
100    public void setHost(QSTileHost host) {
101        mHost = host;
102    }
103
104    public ItemTouchHelper getItemTouchHelper() {
105        return mItemTouchHelper;
106    }
107
108    public ItemDecoration getItemDecoration() {
109        return mDecoration;
110    }
111
112    public void saveSpecs(QSTileHost host) {
113        List<String> newSpecs = new ArrayList<>();
114        for (int i = 0; i < mTiles.size() && mTiles.get(i) != null; i++) {
115            newSpecs.add(mTiles.get(i).spec);
116        }
117        host.changeTiles(mCurrentSpecs, newSpecs);
118        mCurrentSpecs = newSpecs;
119    }
120
121    public void resetTileSpecs(QSTileHost host, List<String> specs) {
122        // Notify the host so the tiles get removed callbacks.
123        host.changeTiles(mCurrentSpecs, specs);
124        setTileSpecs(specs);
125    }
126
127    public void setTileSpecs(List<String> currentSpecs) {
128        if (currentSpecs.equals(mCurrentSpecs)) {
129            return;
130        }
131        mCurrentSpecs = currentSpecs;
132        recalcSpecs();
133    }
134
135    @Override
136    public void onTilesChanged(List<TileInfo> tiles) {
137        mAllTiles = tiles;
138        recalcSpecs();
139    }
140
141    private void recalcSpecs() {
142        if (mCurrentSpecs == null || mAllTiles == null) {
143            return;
144        }
145        mOtherTiles = new ArrayList<TileInfo>(mAllTiles);
146        mTiles.clear();
147        for (int i = 0; i < mCurrentSpecs.size(); i++) {
148            final TileInfo tile = getAndRemoveOther(mCurrentSpecs.get(i));
149            if (tile != null) {
150                mTiles.add(tile);
151            }
152        }
153        mTiles.add(null);
154        for (int i = 0; i < mOtherTiles.size(); i++) {
155            final TileInfo tile = mOtherTiles.get(i);
156            if (tile.isSystem) {
157                mOtherTiles.remove(i--);
158                mTiles.add(tile);
159            }
160        }
161        mTileDividerIndex = mTiles.size();
162        mTiles.add(null);
163        mTiles.addAll(mOtherTiles);
164        updateDividerLocations();
165        notifyDataSetChanged();
166    }
167
168    private TileInfo getAndRemoveOther(String s) {
169        for (int i = 0; i < mOtherTiles.size(); i++) {
170            if (mOtherTiles.get(i).spec.equals(s)) {
171                return mOtherTiles.remove(i);
172            }
173        }
174        return null;
175    }
176
177    @Override
178    public int getItemViewType(int position) {
179        if (mAccessibilityAction == ACTION_ADD && position == mEditIndex - 1) {
180            return TYPE_ACCESSIBLE_DROP;
181        }
182        if (position == mTileDividerIndex) {
183            return TYPE_DIVIDER;
184        }
185        if (mTiles.get(position) == null) {
186            return TYPE_EDIT;
187        }
188        return TYPE_TILE;
189    }
190
191    @Override
192    public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
193        final Context context = parent.getContext();
194        LayoutInflater inflater = LayoutInflater.from(context);
195        if (viewType == TYPE_DIVIDER) {
196            return new Holder(inflater.inflate(R.layout.qs_customize_tile_divider, parent, false));
197        }
198        if (viewType == TYPE_EDIT) {
199            return new Holder(inflater.inflate(R.layout.qs_customize_divider, parent, false));
200        }
201        FrameLayout frame = (FrameLayout) inflater.inflate(R.layout.qs_customize_tile_frame, parent,
202                false);
203        frame.addView(new CustomizeTileView(context, new QSIconViewImpl(context)));
204        return new Holder(frame);
205    }
206
207    @Override
208    public int getItemCount() {
209        return mTiles.size();
210    }
211
212    @Override
213    public boolean onFailedToRecycleView(Holder holder) {
214        holder.clearDrag();
215        return true;
216    }
217
218    @Override
219    public void onBindViewHolder(final Holder holder, int position) {
220        if (holder.getItemViewType() == TYPE_DIVIDER) {
221            holder.itemView.setVisibility(mTileDividerIndex < mTiles.size() - 1 ? View.VISIBLE
222                    : View.INVISIBLE);
223            return;
224        }
225        if (holder.getItemViewType() == TYPE_EDIT) {
226            final int titleResId;
227            if (mCurrentDrag == null) {
228                titleResId = R.string.drag_to_add_tiles;
229            } else if (!canRemoveTiles() && mCurrentDrag.getAdapterPosition() < mEditIndex) {
230                titleResId = R.string.drag_to_remove_disabled;
231            } else {
232                titleResId = R.string.drag_to_remove_tiles;
233            }
234            ((TextView) holder.itemView.findViewById(android.R.id.title)).setText(titleResId);
235            return;
236        }
237        if (holder.getItemViewType() == TYPE_ACCESSIBLE_DROP) {
238            holder.mTileView.setClickable(true);
239            holder.mTileView.setFocusable(true);
240            holder.mTileView.setFocusableInTouchMode(true);
241            holder.mTileView.setVisibility(View.VISIBLE);
242            holder.mTileView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
243            holder.mTileView.setContentDescription(mContext.getString(
244                    R.string.accessibility_qs_edit_position_label, position + 1));
245            holder.mTileView.setOnClickListener(new OnClickListener() {
246                @Override
247                public void onClick(View v) {
248                    selectPosition(holder.getAdapterPosition(), v);
249                }
250            });
251            if (mNeedsFocus) {
252                // Wait for this to get laid out then set its focus.
253                // Ensure that tile gets laid out so we get the callback.
254                holder.mTileView.requestLayout();
255                holder.mTileView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
256                    @Override
257                    public void onLayoutChange(View v, int left, int top, int right, int bottom,
258                            int oldLeft, int oldTop, int oldRight, int oldBottom) {
259                        holder.mTileView.removeOnLayoutChangeListener(this);
260                        holder.mTileView.requestFocus();
261                    }
262                });
263                mNeedsFocus = false;
264            }
265            return;
266        }
267
268        TileInfo info = mTiles.get(position);
269
270        if (position > mEditIndex) {
271            info.state.contentDescription = mContext.getString(
272                    R.string.accessibility_qs_edit_add_tile_label, info.state.label);
273        } else if (mAccessibilityAction != ACTION_NONE) {
274            info.state.contentDescription = mContext.getString(
275                    R.string.accessibility_qs_edit_position_label, position + 1);
276        } else {
277            info.state.contentDescription = mContext.getString(
278                    R.string.accessibility_qs_edit_tile_label, position + 1, info.state.label);
279        }
280        holder.mTileView.handleStateChanged(info.state);
281        holder.mTileView.setShowAppLabel(position > mEditIndex && !info.isSystem);
282
283        if (mAccessibilityManager.isTouchExplorationEnabled()) {
284            final boolean selectable = mAccessibilityAction == ACTION_NONE || position < mEditIndex;
285            holder.mTileView.setClickable(selectable);
286            holder.mTileView.setFocusable(selectable);
287            holder.mTileView.setImportantForAccessibility(selectable
288                    ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
289                    : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
290            if (selectable) {
291                holder.mTileView.setOnClickListener(new OnClickListener() {
292                    @Override
293                    public void onClick(View v) {
294                        int position = holder.getAdapterPosition();
295                        if (mAccessibilityAction != ACTION_NONE) {
296                            selectPosition(position, v);
297                        } else {
298                            if (position < mEditIndex && canRemoveTiles()) {
299                                showAccessibilityDialog(position, v);
300                            } else {
301                                startAccessibleAdd(position);
302                            }
303                        }
304                    }
305                });
306            }
307        }
308    }
309
310    private boolean canRemoveTiles() {
311        return mCurrentSpecs.size() > MIN_NUM_TILES;
312    }
313
314    private void selectPosition(int position, View v) {
315        if (mAccessibilityAction == ACTION_ADD) {
316            // Remove the placeholder.
317            mTiles.remove(mEditIndex--);
318            notifyItemRemoved(mEditIndex);
319            // Don't remove items when the last position is selected.
320            if (position == mEditIndex - 1) position--;
321        }
322        mAccessibilityAction = ACTION_NONE;
323        move(mAccessibilityFromIndex, position, v);
324        notifyDataSetChanged();
325    }
326
327    private void showAccessibilityDialog(final int position, final View v) {
328        final TileInfo info = mTiles.get(position);
329        CharSequence[] options = new CharSequence[] {
330                mContext.getString(R.string.accessibility_qs_edit_move_tile, info.state.label),
331                mContext.getString(R.string.accessibility_qs_edit_remove_tile, info.state.label),
332        };
333        AlertDialog dialog = new Builder(mContext)
334                .setItems(options, new DialogInterface.OnClickListener() {
335                    @Override
336                    public void onClick(DialogInterface dialog, int which) {
337                        if (which == 0) {
338                            startAccessibleMove(position);
339                        } else {
340                            move(position, info.isSystem ? mEditIndex : mTileDividerIndex, v);
341                            notifyItemChanged(mTileDividerIndex);
342                            notifyDataSetChanged();
343                        }
344                    }
345                }).setNegativeButton(android.R.string.cancel, null)
346                .create();
347        SystemUIDialog.setShowForAllUsers(dialog, true);
348        SystemUIDialog.applyFlags(dialog);
349        dialog.show();
350    }
351
352    private void startAccessibleAdd(int position) {
353        mAccessibilityFromIndex = position;
354        mAccessibilityAction = ACTION_ADD;
355        // Add placeholder for last slot.
356        mTiles.add(mEditIndex++, null);
357        mNeedsFocus = true;
358        notifyDataSetChanged();
359    }
360
361    private void startAccessibleMove(int position) {
362        mAccessibilityFromIndex = position;
363        mAccessibilityAction = ACTION_MOVE;
364        notifyDataSetChanged();
365    }
366
367    public SpanSizeLookup getSizeLookup() {
368        return mSizeLookup;
369    }
370
371    private boolean move(int from, int to, View v) {
372        if (to == from) {
373            return true;
374        }
375        CharSequence fromLabel = mTiles.get(from).state.label;
376        move(from, to, mTiles);
377        updateDividerLocations();
378        if (to >= mEditIndex) {
379            MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_REMOVE_SPEC,
380                    strip(mTiles.get(to)));
381            MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_REMOVE,
382                    from);
383        } else if (from >= mEditIndex) {
384            MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_ADD_SPEC,
385                    strip(mTiles.get(to)));
386            MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_ADD,
387                    to);
388            v.announceForAccessibility(mContext.getString(R.string.accessibility_qs_edit_tile_added,
389                    fromLabel, (to + 1)));
390        } else {
391            MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_MOVE_SPEC,
392                    strip(mTiles.get(to)));
393            MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_MOVE,
394                    to);
395            v.announceForAccessibility(mContext.getString(R.string.accessibility_qs_edit_tile_moved,
396                    fromLabel, (to + 1)));
397        }
398        saveSpecs(mHost);
399        return true;
400    }
401
402    private void updateDividerLocations() {
403        // The first null is the edit tiles label, the second null is the tile divider.
404        // If there is no second null, then there are no non-system tiles.
405        mEditIndex = -1;
406        mTileDividerIndex = mTiles.size();
407        for (int i = 0; i < mTiles.size(); i++) {
408            if (mTiles.get(i) == null) {
409                if (mEditIndex == -1) {
410                    mEditIndex = i;
411                } else {
412                    mTileDividerIndex = i;
413                }
414            }
415        }
416        if (mTiles.size() - 1 == mTileDividerIndex) {
417            notifyItemChanged(mTileDividerIndex);
418        }
419    }
420
421    private static String strip(TileInfo tileInfo) {
422        String spec = tileInfo.spec;
423        if (spec.startsWith(CustomTile.PREFIX)) {
424            ComponentName component = CustomTile.getComponentFromSpec(spec);
425            return component.getPackageName();
426        }
427        return spec;
428    }
429
430    private <T> void move(int from, int to, List<T> list) {
431        list.add(to, list.remove(from));
432        notifyItemMoved(from, to);
433    }
434
435    public class Holder extends ViewHolder {
436        private CustomizeTileView mTileView;
437
438        public Holder(View itemView) {
439            super(itemView);
440            if (itemView instanceof FrameLayout) {
441                mTileView = (CustomizeTileView) ((FrameLayout) itemView).getChildAt(0);
442                mTileView.setBackground(null);
443                mTileView.getIcon().disableAnimation();
444            }
445        }
446
447        public void clearDrag() {
448            itemView.clearAnimation();
449            mTileView.findViewById(R.id.tile_label).clearAnimation();
450            mTileView.findViewById(R.id.tile_label).setAlpha(1);
451            mTileView.getAppLabel().clearAnimation();
452            mTileView.getAppLabel().setAlpha(.6f);
453        }
454
455        public void startDrag() {
456            itemView.animate()
457                    .setDuration(DRAG_LENGTH)
458                    .scaleX(DRAG_SCALE)
459                    .scaleY(DRAG_SCALE);
460            mTileView.findViewById(R.id.tile_label).animate()
461                    .setDuration(DRAG_LENGTH)
462                    .alpha(0);
463            mTileView.getAppLabel().animate()
464                    .setDuration(DRAG_LENGTH)
465                    .alpha(0);
466        }
467
468        public void stopDrag() {
469            itemView.animate()
470                    .setDuration(DRAG_LENGTH)
471                    .scaleX(1)
472                    .scaleY(1);
473            mTileView.findViewById(R.id.tile_label).animate()
474                    .setDuration(DRAG_LENGTH)
475                    .alpha(1);
476            mTileView.getAppLabel().animate()
477                    .setDuration(DRAG_LENGTH)
478                    .alpha(.6f);
479        }
480    }
481
482    private final SpanSizeLookup mSizeLookup = new SpanSizeLookup() {
483        @Override
484        public int getSpanSize(int position) {
485            final int type = getItemViewType(position);
486            return type == TYPE_EDIT || type == TYPE_DIVIDER ? 3 : 1;
487        }
488    };
489
490    private class TileItemDecoration extends ItemDecoration {
491        private final ColorDrawable mDrawable;
492
493        private TileItemDecoration(Context context) {
494            TypedArray ta =
495                    context.obtainStyledAttributes(new int[]{android.R.attr.colorSecondary});
496            mDrawable = new ColorDrawable(ta.getColor(0, 0));
497            ta.recycle();
498        }
499
500
501        @Override
502        public void onDraw(Canvas c, RecyclerView parent, State state) {
503            super.onDraw(c, parent, state);
504
505            final int childCount = parent.getChildCount();
506            final int width = parent.getWidth();
507            final int bottom = parent.getBottom();
508            for (int i = 0; i < childCount; i++) {
509                final View child = parent.getChildAt(i);
510                final ViewHolder holder = parent.getChildViewHolder(child);
511                if (holder.getAdapterPosition() < mEditIndex && !(child instanceof TextView)) {
512                    continue;
513                }
514
515                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
516                        .getLayoutParams();
517                final int top = child.getTop() + params.topMargin +
518                        Math.round(ViewCompat.getTranslationY(child));
519                // Draw full width, in case there aren't tiles all the way across.
520                mDrawable.setBounds(0, top, width, bottom);
521                mDrawable.draw(c);
522                break;
523            }
524        }
525    }
526
527    private final ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() {
528
529        @Override
530        public boolean isLongPressDragEnabled() {
531            return true;
532        }
533
534        @Override
535        public boolean isItemViewSwipeEnabled() {
536            return false;
537        }
538
539        @Override
540        public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
541            super.onSelectedChanged(viewHolder, actionState);
542            if (actionState != ItemTouchHelper.ACTION_STATE_DRAG) {
543                viewHolder = null;
544            }
545            if (viewHolder == mCurrentDrag) return;
546            if (mCurrentDrag != null) {
547                int position = mCurrentDrag.getAdapterPosition();
548                TileInfo info = mTiles.get(position);
549                mCurrentDrag.mTileView.setShowAppLabel(
550                        position > mEditIndex && !info.isSystem);
551                mCurrentDrag.stopDrag();
552                mCurrentDrag = null;
553            }
554            if (viewHolder != null) {
555                mCurrentDrag = (Holder) viewHolder;
556                mCurrentDrag.startDrag();
557            }
558            mHandler.post(new Runnable() {
559                @Override
560                public void run() {
561                    notifyItemChanged(mEditIndex);
562                }
563            });
564        }
565
566        @Override
567        public boolean canDropOver(RecyclerView recyclerView, ViewHolder current,
568                ViewHolder target) {
569            if (!canRemoveTiles() && current.getAdapterPosition() < mEditIndex) {
570                return target.getAdapterPosition() < mEditIndex;
571            }
572            return target.getAdapterPosition() <= mEditIndex + 1;
573        }
574
575        @Override
576        public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) {
577            if (viewHolder.getItemViewType() == TYPE_EDIT || viewHolder.getItemViewType() == TYPE_DIVIDER) {
578                return makeMovementFlags(0, 0);
579            }
580            int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.RIGHT
581                    | ItemTouchHelper.LEFT;
582            return makeMovementFlags(dragFlags, 0);
583        }
584
585        @Override
586        public boolean onMove(RecyclerView recyclerView, ViewHolder viewHolder, ViewHolder target) {
587            int from = viewHolder.getAdapterPosition();
588            int to = target.getAdapterPosition();
589            return move(from, to, target.itemView);
590        }
591
592        @Override
593        public void onSwiped(ViewHolder viewHolder, int direction) {
594        }
595    };
596}
597