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