blob: 60c24d095aa759f91fa7726b44fc278d2db4b3cf [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.android.systemui.qs.customize;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Canvas;
import android.graphics.drawable.ColorDrawable;
import android.os.Handler;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ItemDecoration;
import android.support.v7.widget.RecyclerView.State;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.MetricsProto;
import com.android.systemui.R;
import com.android.systemui.qs.QSIconView;
import com.android.systemui.qs.customize.TileAdapter.Holder;
import com.android.systemui.qs.customize.TileQueryHelper.TileInfo;
import com.android.systemui.qs.customize.TileQueryHelper.TileStateListener;
import com.android.systemui.qs.external.CustomTile;
import com.android.systemui.statusbar.phone.QSTileHost;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import java.util.ArrayList;
import java.util.List;
public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileStateListener {
private static final long DRAG_LENGTH = 100;
private static final float DRAG_SCALE = 1.2f;
public static final long MOVE_DURATION = 150;
private static final int TYPE_TILE = 0;
private static final int TYPE_EDIT = 1;
private static final int TYPE_ACCESSIBLE_DROP = 2;
private static final int TYPE_DIVIDER = 4;
private static final long EDIT_ID = 10000;
private static final long DIVIDER_ID = 20000;
private final Context mContext;
private final Handler mHandler = new Handler();
private final List<TileInfo> mTiles = new ArrayList<>();
private final ItemTouchHelper mItemTouchHelper;
private final AccessibilityManager mAccessibilityManager;
private int mEditIndex;
private int mTileDividerIndex;
private boolean mNeedsFocus;
private List<String> mCurrentSpecs;
private List<TileInfo> mOtherTiles;
private List<TileInfo> mAllTiles;
private Holder mCurrentDrag;
private boolean mAccessibilityMoving;
private int mAccessibilityFromIndex;
private QSTileHost mHost;
public TileAdapter(Context context) {
mContext = context;
mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
mItemTouchHelper = new ItemTouchHelper(mCallbacks);
}
public void setHost(QSTileHost host) {
mHost = host;
}
public ItemTouchHelper getItemTouchHelper() {
return mItemTouchHelper;
}
public ItemDecoration getItemDecoration() {
return mDecoration;
}
public void saveSpecs(QSTileHost host) {
List<String> newSpecs = new ArrayList<>();
for (int i = 0; i < mTiles.size() && mTiles.get(i) != null; i++) {
newSpecs.add(mTiles.get(i).spec);
}
host.changeTiles(mCurrentSpecs, newSpecs);
mCurrentSpecs = newSpecs;
}
public void setTileSpecs(List<String> currentSpecs) {
mCurrentSpecs = currentSpecs;
recalcSpecs();
}
@Override
public void onTilesChanged(List<TileInfo> tiles) {
mAllTiles = tiles;
recalcSpecs();
}
private void recalcSpecs() {
if (mCurrentSpecs == null || mAllTiles == null) {
return;
}
mOtherTiles = new ArrayList<TileInfo>(mAllTiles);
mTiles.clear();
for (int i = 0; i < mCurrentSpecs.size(); i++) {
final TileInfo tile = getAndRemoveOther(mCurrentSpecs.get(i));
if (tile != null) {
mTiles.add(tile);
}
}
mTiles.add(null);
for (int i = 0; i < mOtherTiles.size(); i++) {
final TileInfo tile = mOtherTiles.get(i);
if (tile.isSystem) {
mOtherTiles.remove(i--);
mTiles.add(tile);
}
}
mTileDividerIndex = mTiles.size();
mTiles.add(null);
mTiles.addAll(mOtherTiles);
updateDividerLocations();
notifyDataSetChanged();
}
private TileInfo getAndRemoveOther(String s) {
for (int i = 0; i < mOtherTiles.size(); i++) {
if (mOtherTiles.get(i).spec.equals(s)) {
return mOtherTiles.remove(i);
}
}
return null;
}
@Override
public int getItemViewType(int position) {
if (mAccessibilityMoving && position == mEditIndex - 1) {
return TYPE_ACCESSIBLE_DROP;
}
if (position == mTileDividerIndex) {
return TYPE_DIVIDER;
}
if (mTiles.get(position) == null) {
return TYPE_EDIT;
}
return TYPE_TILE;
}
@Override
public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
final Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
if (viewType == TYPE_DIVIDER) {
return new Holder(inflater.inflate(R.layout.qs_customize_tile_divider, parent, false));
}
if (viewType == TYPE_EDIT) {
return new Holder(inflater.inflate(R.layout.qs_customize_divider, parent, false));
}
FrameLayout frame = (FrameLayout) inflater.inflate(R.layout.qs_customize_tile_frame, parent,
false);
frame.addView(new CustomizeTileView(context, new QSIconView(context)));
return new Holder(frame);
}
@Override
public int getItemCount() {
return mTiles.size();
}
@Override
public boolean onFailedToRecycleView(Holder holder) {
holder.clearDrag();
return true;
}
@Override
public void onBindViewHolder(final Holder holder, int position) {
if (holder.getItemViewType() == TYPE_DIVIDER) {
holder.itemView.setVisibility(mTileDividerIndex < mTiles.size() - 1 ? View.VISIBLE
: View.INVISIBLE);
return;
}
if (holder.getItemViewType() == TYPE_EDIT) {
((TextView) holder.itemView.findViewById(android.R.id.title)).setText(
mCurrentDrag != null ? R.string.drag_to_remove_tiles
: R.string.drag_to_add_tiles);
return;
}
if (holder.getItemViewType() == TYPE_ACCESSIBLE_DROP) {
holder.mTileView.setClickable(true);
holder.mTileView.setFocusable(true);
holder.mTileView.setFocusableInTouchMode(true);
holder.mTileView.setVisibility(View.VISIBLE);
holder.mTileView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
holder.mTileView.setContentDescription(mContext.getString(
R.string.accessibility_qs_edit_position_label, position + 1));
holder.mTileView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
selectPosition(holder.getAdapterPosition(), v);
}
});
if (mNeedsFocus) {
// Wait for this to get laid out then set its focus.
holder.mTileView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
holder.mTileView.removeOnLayoutChangeListener(this);
holder.mTileView.requestFocus();
}
});
mNeedsFocus = false;
}
return;
}
TileInfo info = mTiles.get(position);
if (position > mEditIndex) {
info.state.contentDescription = mContext.getString(
R.string.accessibility_qs_edit_add_tile_label, info.state.label);
} else if (mAccessibilityMoving) {
info.state.contentDescription = mContext.getString(
R.string.accessibility_qs_edit_position_label, position + 1);
} else {
info.state.contentDescription = mContext.getString(
R.string.accessibility_qs_edit_tile_label, position + 1, info.state.label);
}
holder.mTileView.onStateChanged(info.state);
holder.mTileView.setAppLabel(info.appLabel);
holder.mTileView.setShowAppLabel(mTileDividerIndex > -1 && position > mTileDividerIndex);
if (mAccessibilityManager.isTouchExplorationEnabled()) {
final boolean selectable = !mAccessibilityMoving || position < mEditIndex;
holder.mTileView.setClickable(selectable);
holder.mTileView.setFocusable(selectable);
holder.mTileView.setImportantForAccessibility(selectable
? View.IMPORTANT_FOR_ACCESSIBILITY_YES
: View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
if (selectable) {
holder.mTileView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
if (mAccessibilityMoving) {
selectPosition(position, v);
} else {
if (position < mEditIndex) {
showAccessibilityDialog(position, v);
} else {
startAccessibleDrag(position);
}
}
}
});
}
}
}
private void selectPosition(int position, View v) {
// Remove the placeholder.
mAccessibilityMoving = false;
mTiles.remove(mEditIndex--);
notifyItemRemoved(mEditIndex - 1);
move(mAccessibilityFromIndex, position, v);
updateDividerLocations();
notifyDataSetChanged();
saveSpecs(mHost);
}
private void showAccessibilityDialog(final int position, final View v) {
TileInfo info = mTiles.get(position);
CharSequence[] options = new CharSequence[] {
mContext.getString(R.string.accessibility_qs_edit_move_tile, info.state.label),
mContext.getString(R.string.accessibility_qs_edit_remove_tile, info.state.label),
};
AlertDialog dialog = new Builder(mContext)
.setItems(options, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == 0) {
startAccessibleDrag(position);
} else {
move(position, mEditIndex, v);
}
}
}).setNegativeButton(android.R.string.cancel, null)
.create();
SystemUIDialog.setShowForAllUsers(dialog, true);
SystemUIDialog.applyFlags(dialog);
dialog.show();
}
private void startAccessibleDrag(int position) {
mAccessibilityMoving = true;
mNeedsFocus = true;
mAccessibilityFromIndex = position;
// Add placeholder for last slot.
mTiles.add(mEditIndex++, null);
notifyDataSetChanged();
}
public SpanSizeLookup getSizeLookup() {
return mSizeLookup;
}
private boolean move(int from, int to, View v) {
if (to >= mEditIndex) {
if (from < mEditIndex) {
// Removing a tile.
// Sort tiles into system/non-system groups.
TileInfo tile = mTiles.get(from);
if (tile.isSystem) {
if (to > mTileDividerIndex) {
to = mTileDividerIndex;
}
} else {
if (mTileDividerIndex == mTiles.size() - 1) {
notifyItemChanged(mTileDividerIndex);
}
if (to <= mTileDividerIndex) {
to = mTileDividerIndex;
}
}
} else {
if (to > mEditIndex) {
// Don't allow tiles to be dragged around when they aren't added.
to = from;
}
// Allow the case where to == mEditIndex to fall through and swap which
// side the tile is currently on.
// This lets the the cases where all tiles are on one side of the line
// work.
}
}
CharSequence fromLabel = mTiles.get(from).state.label;
move(from, to, mTiles);
updateDividerLocations();
if (to == from) {
return true;
}
CharSequence announcement;
if (to >= mEditIndex) {
MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_REMOVE_SPEC,
strip(mTiles.get(to)));
MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_REMOVE,
from);
announcement = mContext.getString(R.string.accessibility_qs_edit_tile_removed,
fromLabel);
} else if (from >= mEditIndex) {
MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_ADD_SPEC,
strip(mTiles.get(to)));
MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_ADD,
to);
announcement = mContext.getString(R.string.accessibility_qs_edit_tile_added,
fromLabel, (to + 1));
} else {
MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_MOVE_SPEC,
strip(mTiles.get(to)));
MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_MOVE,
to);
announcement = mContext.getString(R.string.accessibility_qs_edit_tile_moved,
fromLabel, (to + 1));
}
v.announceForAccessibility(announcement);
saveSpecs(mHost);
return true;
}
private void updateDividerLocations() {
// The first null is the edit tiles label, the second null is the tile divider.
// If there is no second null, then there are no non-system tiles.
mEditIndex = -1;
mTileDividerIndex = mTiles.size();
for (int i = 0; i < mTiles.size(); i++) {
if (mTiles.get(i) == null) {
if (mEditIndex == -1) {
mEditIndex = i;
} else {
mTileDividerIndex = i;
}
}
}
if (mTiles.size() - 1 == mTileDividerIndex) {
notifyItemChanged(mTileDividerIndex);
}
}
private static String strip(TileInfo tileInfo) {
String spec = tileInfo.spec;
if (spec.startsWith(CustomTile.PREFIX)) {
ComponentName component = CustomTile.getComponentFromSpec(spec);
return component.getPackageName();
}
return spec;
}
private <T> void move(int from, int to, List<T> list) {
list.add(to, list.remove(from));
notifyItemMoved(from, to);
notifyItemChanged(to);
}
public class Holder extends ViewHolder {
private CustomizeTileView mTileView;
public Holder(View itemView) {
super(itemView);
if (itemView instanceof FrameLayout) {
mTileView = (CustomizeTileView) ((FrameLayout) itemView).getChildAt(0);
mTileView.setBackground(null);
mTileView.getIcon().disableAnimation();
}
}
public void clearDrag() {
itemView.clearAnimation();
mTileView.findViewById(R.id.tile_label).clearAnimation();
mTileView.findViewById(R.id.tile_label).setAlpha(1);
mTileView.getAppLabel().clearAnimation();
mTileView.getAppLabel().setAlpha(.6f);
}
public void startDrag() {
itemView.animate()
.setDuration(DRAG_LENGTH)
.scaleX(DRAG_SCALE)
.scaleY(DRAG_SCALE);
mTileView.findViewById(R.id.tile_label).animate()
.setDuration(DRAG_LENGTH)
.alpha(0);
mTileView.getAppLabel().animate()
.setDuration(DRAG_LENGTH)
.alpha(0);
}
public void stopDrag() {
itemView.animate()
.setDuration(DRAG_LENGTH)
.scaleX(1)
.scaleY(1);
mTileView.findViewById(R.id.tile_label).animate()
.setDuration(DRAG_LENGTH)
.alpha(1);
mTileView.getAppLabel().animate()
.setDuration(DRAG_LENGTH)
.alpha(.6f);
}
}
private final SpanSizeLookup mSizeLookup = new SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
final int type = getItemViewType(position);
return type == TYPE_EDIT || type == TYPE_DIVIDER ? 3 : 1;
}
};
private final ItemDecoration mDecoration = new ItemDecoration() {
// TODO: Move this to resource.
private final ColorDrawable mDrawable = new ColorDrawable(0xff384248);
@Override
public void onDraw(Canvas c, RecyclerView parent, State state) {
super.onDraw(c, parent, state);
final int childCount = parent.getChildCount();
final int width = parent.getWidth();
final int bottom = parent.getBottom();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final ViewHolder holder = parent.getChildViewHolder(child);
if (holder.getAdapterPosition() < mEditIndex) {
continue;
}
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top = child.getTop() + params.topMargin +
Math.round(ViewCompat.getTranslationY(child));
// Draw full width, in case there aren't tiles all the way across.
mDrawable.setBounds(0, top, width, bottom);
mDrawable.draw(c);
break;
}
}
};
private final ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() {
@Override
public boolean isLongPressDragEnabled() {
return true;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
if (mCurrentDrag != null) {
mCurrentDrag.stopDrag();
mCurrentDrag = null;
}
if (viewHolder != null) {
mCurrentDrag = (Holder) viewHolder;
mCurrentDrag.startDrag();
}
mHandler.post(new Runnable() {
@Override
public void run() {
notifyItemChanged(mEditIndex);
}
});
}
@Override
public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) {
if (viewHolder.getItemViewType() == TYPE_EDIT) {
return makeMovementFlags(0, 0);
}
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.RIGHT
| ItemTouchHelper.LEFT;
return makeMovementFlags(dragFlags, 0);
}
@Override
public boolean onMove(RecyclerView recyclerView, ViewHolder viewHolder, ViewHolder target) {
int from = viewHolder.getAdapterPosition();
int to = target.getAdapterPosition();
return move(from, to, target.itemView);
}
@Override
public void onSwiped(ViewHolder viewHolder, int direction) {
}
};
}