Home | History | Annotate | Download | only in customize
      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 }
    572