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