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