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