OSDN Git Service

DO NOT MERGE. Grant MMS Uri permissions as the calling UID.
[android-x86/frameworks-base.git] / packages / SystemUI / src / com / android / systemui / qs / customize / TileAdapter.java
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
15 package com.android.systemui.qs.customize;
16
17 import android.app.AlertDialog;
18 import android.app.AlertDialog.Builder;
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.DialogInterface;
22 import android.content.res.TypedArray;
23 import android.graphics.Canvas;
24 import android.graphics.drawable.ColorDrawable;
25 import android.os.Handler;
26 import android.support.v4.view.ViewCompat;
27 import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
28 import android.support.v7.widget.RecyclerView;
29 import android.support.v7.widget.RecyclerView.ItemDecoration;
30 import android.support.v7.widget.RecyclerView.State;
31 import android.support.v7.widget.RecyclerView.ViewHolder;
32 import android.support.v7.widget.helper.ItemTouchHelper;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.View.OnClickListener;
36 import android.view.View.OnLayoutChangeListener;
37 import android.view.ViewGroup;
38 import android.view.accessibility.AccessibilityManager;
39 import android.widget.FrameLayout;
40 import android.widget.TextView;
41
42 import com.android.internal.logging.MetricsLogger;
43 import com.android.internal.logging.MetricsProto;
44 import com.android.systemui.R;
45 import com.android.systemui.qs.QSIconView;
46 import com.android.systemui.qs.customize.TileAdapter.Holder;
47 import com.android.systemui.qs.customize.TileQueryHelper.TileInfo;
48 import com.android.systemui.qs.customize.TileQueryHelper.TileStateListener;
49 import com.android.systemui.qs.external.CustomTile;
50 import com.android.systemui.statusbar.phone.QSTileHost;
51 import com.android.systemui.statusbar.phone.SystemUIDialog;
52
53 import java.util.ArrayList;
54 import java.util.List;
55
56 public 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 }