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