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