blob: 353c6b62a8166582ddb53c2d20734dde72cbcc8c [file] [log] [blame]
Jason Monk5db8a412015-10-21 15:16:23 -07001/*
Jason Monk62b63a02016-02-02 15:15:31 -05002 * Copyright (C) 2016 The Android Open Source Project
Jason Monk5db8a412015-10-21 15:16:23 -07003 *
Jason Monk62b63a02016-02-02 15:15:31 -05004 * 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
Jason Monk5db8a412015-10-21 15:16:23 -07006 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
Jason Monk62b63a02016-02-02 15:15:31 -05009 * 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.
Jason Monk5db8a412015-10-21 15:16:23 -070013 */
14
15package com.android.systemui.qs.customize;
16
Jason Monk39c98e62016-03-16 09:18:35 -040017import android.app.AlertDialog;
18import android.app.AlertDialog.Builder;
Jason Monk5db8a412015-10-21 15:16:23 -070019import android.content.Context;
Jason Monk39c98e62016-03-16 09:18:35 -040020import android.content.DialogInterface;
Jason Monk62b63a02016-02-02 15:15:31 -050021import android.graphics.Canvas;
22import android.graphics.drawable.ColorDrawable;
Jason Monk1c2fea82016-03-11 11:33:36 -050023import android.os.Handler;
Jason Monk62b63a02016-02-02 15:15:31 -050024import android.support.v4.view.ViewCompat;
25import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
26import android.support.v7.widget.RecyclerView;
27import android.support.v7.widget.RecyclerView.ItemDecoration;
28import android.support.v7.widget.RecyclerView.State;
29import android.support.v7.widget.RecyclerView.ViewHolder;
30import android.support.v7.widget.helper.ItemTouchHelper;
Jason Monk5db8a412015-10-21 15:16:23 -070031import android.view.LayoutInflater;
32import android.view.View;
Jason Monk39c98e62016-03-16 09:18:35 -040033import android.view.View.OnClickListener;
34import android.view.View.OnLayoutChangeListener;
Jason Monk5db8a412015-10-21 15:16:23 -070035import android.view.ViewGroup;
Jason Monk39c98e62016-03-16 09:18:35 -040036import android.view.accessibility.AccessibilityManager;
Jason Monk62b63a02016-02-02 15:15:31 -050037import android.widget.FrameLayout;
Jason Monk24dbd512016-03-16 14:56:42 -040038import android.widget.TextView;
Jason Monk5db8a412015-10-21 15:16:23 -070039import com.android.systemui.R;
Jason Monk62b63a02016-02-02 15:15:31 -050040import com.android.systemui.qs.QSIconView;
41import com.android.systemui.qs.QSTileView;
42import com.android.systemui.qs.customize.TileAdapter.Holder;
43import com.android.systemui.qs.customize.TileQueryHelper.TileInfo;
44import com.android.systemui.qs.customize.TileQueryHelper.TileStateListener;
Jason Monk5db8a412015-10-21 15:16:23 -070045import com.android.systemui.statusbar.phone.QSTileHost;
Jason Monk39c98e62016-03-16 09:18:35 -040046import com.android.systemui.statusbar.phone.SystemUIDialog;
Jason Monk5db8a412015-10-21 15:16:23 -070047
48import java.util.ArrayList;
Jason Monk5db8a412015-10-21 15:16:23 -070049import java.util.List;
50
Jason Monk62b63a02016-02-02 15:15:31 -050051public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileStateListener {
Jason Monk5db8a412015-10-21 15:16:23 -070052
Jason Monk62b63a02016-02-02 15:15:31 -050053 private static final long DRAG_LENGTH = 100;
54 private static final float DRAG_SCALE = 1.2f;
55 public static final long MOVE_DURATION = 150;
Jason Monk5db8a412015-10-21 15:16:23 -070056
Jason Monk62b63a02016-02-02 15:15:31 -050057 private static final int TYPE_TILE = 0;
58 private static final int TYPE_EDIT = 1;
Jason Monk39c98e62016-03-16 09:18:35 -040059 private static final int TYPE_ACCESSIBLE_DROP = 2;
Jason Monk62b63a02016-02-02 15:15:31 -050060
Jason Monk5db8a412015-10-21 15:16:23 -070061 private final Context mContext;
62
Jason Monk1c2fea82016-03-11 11:33:36 -050063 private final Handler mHandler = new Handler();
Jason Monk62b63a02016-02-02 15:15:31 -050064 private final List<TileInfo> mTiles = new ArrayList<>();
Jason Monkb53b6c52016-02-24 17:25:49 -050065 private final ItemTouchHelper mItemTouchHelper;
Jason Monk39c98e62016-03-16 09:18:35 -040066 private final AccessibilityManager mAccessibilityManager;
Jason Monk62b63a02016-02-02 15:15:31 -050067 private int mDividerIndex;
Jason Monk39c98e62016-03-16 09:18:35 -040068 private boolean mNeedsFocus;
Jason Monk62b63a02016-02-02 15:15:31 -050069 private List<String> mCurrentSpecs;
70 private List<TileInfo> mOtherTiles;
71 private List<TileInfo> mAllTiles;
Jason Monk5db8a412015-10-21 15:16:23 -070072
Jason Monk62b63a02016-02-02 15:15:31 -050073 private Holder mCurrentDrag;
Jason Monk39c98e62016-03-16 09:18:35 -040074 private boolean mAccessibilityMoving;
75 private int mAccessibilityFromIndex;
Jason Monk62b63a02016-02-02 15:15:31 -050076
77 public TileAdapter(Context context) {
Jason Monk5db8a412015-10-21 15:16:23 -070078 mContext = context;
Jason Monk39c98e62016-03-16 09:18:35 -040079 mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
Jason Monkb53b6c52016-02-24 17:25:49 -050080 mItemTouchHelper = new ItemTouchHelper(mCallbacks);
Jason Monk62b63a02016-02-02 15:15:31 -050081 setHasStableIds(true);
Jason Monk5db8a412015-10-21 15:16:23 -070082 }
83
84 @Override
85 public long getItemId(int position) {
Jason Monk62b63a02016-02-02 15:15:31 -050086 return mTiles.get(position) != null ? mAllTiles.indexOf(mTiles.get(position)) : -1;
87 }
88
Jason Monkb53b6c52016-02-24 17:25:49 -050089 public ItemTouchHelper getItemTouchHelper() {
90 return mItemTouchHelper;
Jason Monk62b63a02016-02-02 15:15:31 -050091 }
92
93 public ItemDecoration getItemDecoration() {
94 return mDecoration;
95 }
96
97 public void saveSpecs(QSTileHost host) {
98 List<String> newSpecs = new ArrayList<>();
99 for (int i = 0; mTiles.get(i) != null; i++) {
100 newSpecs.add(mTiles.get(i).spec);
101 }
102 host.changeTiles(mCurrentSpecs, newSpecs);
103 setTileSpecs(newSpecs);
104 }
105
106 public void setTileSpecs(List<String> currentSpecs) {
107 mCurrentSpecs = currentSpecs;
108 recalcSpecs();
Jason Monk5db8a412015-10-21 15:16:23 -0700109 }
110
111 @Override
Jason Monk62b63a02016-02-02 15:15:31 -0500112 public void onTilesChanged(List<TileInfo> tiles) {
113 mAllTiles = tiles;
114 recalcSpecs();
Jason Monk5db8a412015-10-21 15:16:23 -0700115 }
116
Jason Monk62b63a02016-02-02 15:15:31 -0500117 private void recalcSpecs() {
118 if (mCurrentSpecs == null || mAllTiles == null) {
119 return;
Jason Monk5db8a412015-10-21 15:16:23 -0700120 }
Jason Monk62b63a02016-02-02 15:15:31 -0500121 mOtherTiles = new ArrayList<TileInfo>(mAllTiles);
122 mTiles.clear();
123 for (int i = 0; i < mCurrentSpecs.size(); i++) {
Jason Monkcb654cb2016-02-25 15:43:07 -0500124 final TileInfo tile = getAndRemoveOther(mCurrentSpecs.get(i));
125 if (tile != null) {
126 mTiles.add(tile);
127 }
Jason Monk5db8a412015-10-21 15:16:23 -0700128 }
Jason Monk62b63a02016-02-02 15:15:31 -0500129 mTiles.add(null);
130 mTiles.addAll(mOtherTiles);
131 mDividerIndex = mTiles.indexOf(null);
132 notifyDataSetChanged();
133 }
Jason Monk5db8a412015-10-21 15:16:23 -0700134
Jason Monk62b63a02016-02-02 15:15:31 -0500135 private TileInfo getAndRemoveOther(String s) {
136 for (int i = 0; i < mOtherTiles.size(); i++) {
137 if (mOtherTiles.get(i).spec.equals(s)) {
138 return mOtherTiles.remove(i);
Jason Monk5db8a412015-10-21 15:16:23 -0700139 }
Jason Monk62b63a02016-02-02 15:15:31 -0500140 }
141 return null;
142 }
143
144 @Override
145 public int getItemViewType(int position) {
Jason Monk39c98e62016-03-16 09:18:35 -0400146 if (mAccessibilityMoving && position == mDividerIndex - 1) {
147 return TYPE_ACCESSIBLE_DROP;
148 }
Jason Monk62b63a02016-02-02 15:15:31 -0500149 if (mTiles.get(position) == null) {
150 return TYPE_EDIT;
151 }
152 return TYPE_TILE;
153 }
154
155 @Override
156 public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
157 final Context context = parent.getContext();
158 LayoutInflater inflater = LayoutInflater.from(context);
Jason Monk39c98e62016-03-16 09:18:35 -0400159 if (viewType == TYPE_EDIT) {
Jason Monk62b63a02016-02-02 15:15:31 -0500160 return new Holder(inflater.inflate(R.layout.qs_customize_divider, parent, false));
161 }
162 FrameLayout frame = (FrameLayout) inflater.inflate(R.layout.qs_customize_tile_frame, parent,
163 false);
164 frame.addView(new QSTileView(context, new QSIconView(context)));
165 return new Holder(frame);
166 }
167
168 @Override
169 public int getItemCount() {
170 return mTiles.size();
171 }
172
173 @Override
Jason Monk39c98e62016-03-16 09:18:35 -0400174 public void onBindViewHolder(final Holder holder, final int position) {
Jason Monk24dbd512016-03-16 14:56:42 -0400175 if (holder.getItemViewType() == TYPE_EDIT) {
176 ((TextView) holder.itemView.findViewById(android.R.id.title)).setText(
177 mCurrentDrag != null ? R.string.drag_to_remove_tiles
178 : R.string.drag_to_add_tiles);
179 return;
180 }
Jason Monk39c98e62016-03-16 09:18:35 -0400181 if (holder.getItemViewType() == TYPE_ACCESSIBLE_DROP) {
182 holder.mTileView.setClickable(true);
183 holder.mTileView.setFocusable(true);
184 holder.mTileView.setFocusableInTouchMode(true);
185 holder.mTileView.setVisibility(View.VISIBLE);
186 holder.mTileView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
187 holder.mTileView.setContentDescription(mContext.getString(
188 R.string.accessibility_qs_edit_position_label, position + 1));
189 holder.mTileView.setOnClickListener(new OnClickListener() {
190 @Override
191 public void onClick(View v) {
192 selectPosition(position, v);
193 }
194 });
195 if (mNeedsFocus) {
196 // Wait for this to get laid out then set its focus.
197 holder.mTileView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
198 @Override
199 public void onLayoutChange(View v, int left, int top, int right, int bottom,
200 int oldLeft, int oldTop, int oldRight, int oldBottom) {
201 holder.mTileView.removeOnLayoutChangeListener(this);
202 holder.mTileView.requestFocus();
203 }
204 });
205 mNeedsFocus = false;
206 }
207 return;
208 }
Jason Monk62b63a02016-02-02 15:15:31 -0500209
210 TileInfo info = mTiles.get(position);
Jason Monk39c98e62016-03-16 09:18:35 -0400211
212 if (position > mDividerIndex) {
213 info.state.contentDescription = mContext.getString(
214 R.string.accessibility_qs_edit_add_tile_label, info.state.label);
215 } else if (mAccessibilityMoving) {
216 info.state.contentDescription = mContext.getString(
217 R.string.accessibility_qs_edit_position_label, position + 1);
218 } else {
219 info.state.contentDescription = mContext.getString(
220 R.string.accessibility_qs_edit_tile_label, position + 1, info.state.label);
221 }
Jason Monk62b63a02016-02-02 15:15:31 -0500222 holder.mTileView.onStateChanged(info.state);
Jason Monk39c98e62016-03-16 09:18:35 -0400223
224 if (mAccessibilityManager.isTouchExplorationEnabled()) {
225 final boolean selectable = !mAccessibilityMoving || position < mDividerIndex;
226 holder.mTileView.setClickable(selectable);
227 holder.mTileView.setFocusable(selectable);
228 holder.mTileView.setImportantForAccessibility(selectable
229 ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
230 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
231 if (selectable) {
232 holder.mTileView.setOnClickListener(new OnClickListener() {
233 @Override
234 public void onClick(View v) {
235 if (mAccessibilityMoving) {
236 selectPosition(position, v);
237 } else {
238 if (position < mDividerIndex) {
239 showAccessibilityDialog(position, v);
240 } else {
241 startAccessibleDrag(position);
242 }
243 }
244 }
245 });
246 }
247 }
248 }
249
250 private void selectPosition(int position, View v) {
251 // Remove the placeholder.
252 mTiles.remove(mDividerIndex--);
253 mAccessibilityMoving = false;
254 move(mAccessibilityFromIndex, position, v);
255 notifyDataSetChanged();
256 }
257
258 private void showAccessibilityDialog(final int position, final View v) {
259 TileInfo info = mTiles.get(position);
260 CharSequence[] options = new CharSequence[] {
261 mContext.getString(R.string.accessibility_qs_edit_move_tile, info.state.label),
262 mContext.getString(R.string.accessibility_qs_edit_remove_tile, info.state.label),
263 };
264 AlertDialog dialog = new Builder(mContext)
265 .setItems(options, new DialogInterface.OnClickListener() {
266 @Override
267 public void onClick(DialogInterface dialog, int which) {
268 if (which == 0) {
269 startAccessibleDrag(position);
270 } else {
271 move(position, mDividerIndex, v);
272 }
273 }
274 }).setNegativeButton(android.R.string.cancel, null)
275 .create();
276 SystemUIDialog.setShowForAllUsers(dialog, true);
277 SystemUIDialog.applyFlags(dialog);
278 dialog.show();
279 }
280
281 private void startAccessibleDrag(int position) {
282 mAccessibilityMoving = true;
283 mNeedsFocus = true;
284 mAccessibilityFromIndex = position;
285 // Add placeholder for last slot.
286 mTiles.add(mDividerIndex++, null);
287 notifyDataSetChanged();
Jason Monk62b63a02016-02-02 15:15:31 -0500288 }
289
290 public SpanSizeLookup getSizeLookup() {
291 return mSizeLookup;
292 }
293
Jason Monk39c98e62016-03-16 09:18:35 -0400294 private boolean move(int from, int to, View v) {
295 if (to > mDividerIndex) {
296 if (from >= mDividerIndex) {
297 return false;
298 }
299 }
300 CharSequence fromLabel = mTiles.get(from).state.label;
301 move(from, to, mTiles);
302 mDividerIndex = mTiles.indexOf(null);
303 notifyItemChanged(from);
304 notifyItemMoved(from, to);
305 CharSequence announcement;
306 if (to >= mDividerIndex) {
307 announcement = mContext.getString(R.string.accessibility_qs_edit_tile_removed,
308 fromLabel);
309 } else if (from >= mDividerIndex) {
310 announcement = mContext.getString(R.string.accessibility_qs_edit_tile_added,
311 fromLabel, (to + 1));
312 } else {
313 announcement = mContext.getString(R.string.accessibility_qs_edit_tile_moved,
314 fromLabel, (to + 1));
315 }
316 v.announceForAccessibility(announcement);
317 return true;
318 }
319
320 private <T> void move(int from, int to, List<T> list) {
321 list.add(from > to ? to : to + 1, list.get(from));
322 list.remove(from > to ? from + 1 : from);
323 }
324
Jason Monk62b63a02016-02-02 15:15:31 -0500325 public class Holder extends ViewHolder {
326 private QSTileView mTileView;
327
328 public Holder(View itemView) {
329 super(itemView);
330 if (itemView instanceof FrameLayout) {
331 mTileView = (QSTileView) ((FrameLayout) itemView).getChildAt(0);
Jason Monk04fd2492016-02-29 14:29:26 -0500332 mTileView.setBackground(null);
Jason Monk1aec93f2016-03-01 09:39:30 -0500333 mTileView.getIcon().disableAnimation();
Jason Monk5db8a412015-10-21 15:16:23 -0700334 }
Jason Monk5db8a412015-10-21 15:16:23 -0700335 }
336
Jason Monk62b63a02016-02-02 15:15:31 -0500337 public void startDrag() {
338 itemView.animate()
339 .setDuration(DRAG_LENGTH)
340 .scaleX(DRAG_SCALE)
341 .scaleY(DRAG_SCALE);
342 mTileView.findViewById(R.id.tile_label).animate()
343 .setDuration(DRAG_LENGTH)
344 .alpha(0);
345 }
346
347 public void stopDrag() {
348 itemView.animate()
349 .setDuration(DRAG_LENGTH)
350 .scaleX(1)
351 .scaleY(1);
352 mTileView.findViewById(R.id.tile_label).animate()
353 .setDuration(DRAG_LENGTH)
354 .alpha(1);
Jason Monk5db8a412015-10-21 15:16:23 -0700355 }
356 }
357
Jason Monk62b63a02016-02-02 15:15:31 -0500358 private final SpanSizeLookup mSizeLookup = new SpanSizeLookup() {
Jason Monk5db8a412015-10-21 15:16:23 -0700359 @Override
Jason Monk62b63a02016-02-02 15:15:31 -0500360 public int getSpanSize(int position) {
361 return getItemViewType(position) == TYPE_EDIT ? 3 : 1;
362 }
363 };
364
365 private final ItemDecoration mDecoration = new ItemDecoration() {
366 // TODO: Move this to resource.
367 private final ColorDrawable mDrawable = new ColorDrawable(0xff384248);
368
369 @Override
370 public void onDraw(Canvas c, RecyclerView parent, State state) {
371 super.onDraw(c, parent, state);
372
373 final int childCount = parent.getChildCount();
374 final int width = parent.getWidth();
375 final int bottom = parent.getBottom();
376 for (int i = 0; i < childCount; i++) {
377 final View child = parent.getChildAt(i);
378 final ViewHolder holder = parent.getChildViewHolder(child);
379 if (holder.getAdapterPosition() < mDividerIndex) {
Jason Monk5db8a412015-10-21 15:16:23 -0700380 continue;
381 }
Jason Monk62b63a02016-02-02 15:15:31 -0500382
383 final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
384 .getLayoutParams();
385 final int top = child.getTop() + params.topMargin +
386 Math.round(ViewCompat.getTranslationY(child));
387 // Draw full width, in case there aren't tiles all the way across.
388 mDrawable.setBounds(0, top, width, bottom);
389 mDrawable.draw(c);
390 break;
Jason Monk5db8a412015-10-21 15:16:23 -0700391 }
Jason Monk62b63a02016-02-02 15:15:31 -0500392 }
393 };
394
395 private final ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() {
396
397 @Override
398 public boolean isLongPressDragEnabled() {
399 return true;
Jason Monk5db8a412015-10-21 15:16:23 -0700400 }
401
402 @Override
Jason Monk62b63a02016-02-02 15:15:31 -0500403 public boolean isItemViewSwipeEnabled() {
404 return false;
Jason Monk5db8a412015-10-21 15:16:23 -0700405 }
Jason Monk5db8a412015-10-21 15:16:23 -0700406
Jason Monk62b63a02016-02-02 15:15:31 -0500407 @Override
408 public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
409 super.onSelectedChanged(viewHolder, actionState);
410 if (mCurrentDrag != null) {
411 mCurrentDrag.stopDrag();
Jason Monk24dbd512016-03-16 14:56:42 -0400412 mCurrentDrag = null;
Jason Monk62b63a02016-02-02 15:15:31 -0500413 }
414 if (viewHolder != null) {
415 mCurrentDrag = (Holder) viewHolder;
416 mCurrentDrag.startDrag();
417 }
Jason Monk1c2fea82016-03-11 11:33:36 -0500418 mHandler.post(new Runnable() {
419 @Override
420 public void run() {
421 notifyItemChanged(mDividerIndex);
422 }
423 });
Jason Monk62b63a02016-02-02 15:15:31 -0500424 }
425
426 @Override
427 public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) {
428 if (viewHolder.getItemViewType() == TYPE_EDIT) {
429 return makeMovementFlags(0, 0);
430 }
431 int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.RIGHT
432 | ItemTouchHelper.LEFT;
433 return makeMovementFlags(dragFlags, 0);
434 }
435
436 @Override
437 public boolean onMove(RecyclerView recyclerView, ViewHolder viewHolder, ViewHolder target) {
438 int from = viewHolder.getAdapterPosition();
439 int to = target.getAdapterPosition();
Jason Monk39c98e62016-03-16 09:18:35 -0400440 return move(from, to, target.itemView);
Jason Monk62b63a02016-02-02 15:15:31 -0500441 }
442
443 @Override
444 public void onSwiped(ViewHolder viewHolder, int direction) {
445 }
446 };
Jason Monk5db8a412015-10-21 15:16:23 -0700447}