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.nano.MetricsProto;
     44 import com.android.systemui.R;
     45 import com.android.systemui.qs.tileimpl.QSIconViewImpl;
     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.qs.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 resetTileSpecs(QSTileHost host, List<String> specs) {
    118         // Notify the host so the tiles get removed callbacks.
    119         host.changeTiles(mCurrentSpecs, specs);
    120         setTileSpecs(specs);
    121     }
    122 
    123     public void setTileSpecs(List<String> currentSpecs) {
    124         if (currentSpecs.equals(mCurrentSpecs)) {
    125             return;
    126         }
    127         mCurrentSpecs = currentSpecs;
    128         recalcSpecs();
    129     }
    130 
    131     @Override
    132     public void onTilesChanged(List<TileInfo> tiles) {
    133         mAllTiles = tiles;
    134         recalcSpecs();
    135     }
    136 
    137     private void recalcSpecs() {
    138         if (mCurrentSpecs == null || mAllTiles == null) {
    139             return;
    140         }
    141         mOtherTiles = new ArrayList<TileInfo>(mAllTiles);
    142         mTiles.clear();
    143         for (int i = 0; i < mCurrentSpecs.size(); i++) {
    144             final TileInfo tile = getAndRemoveOther(mCurrentSpecs.get(i));
    145             if (tile != null) {
    146                 mTiles.add(tile);
    147             }
    148         }
    149         mTiles.add(null);
    150         for (int i = 0; i < mOtherTiles.size(); i++) {
    151             final TileInfo tile = mOtherTiles.get(i);
    152             if (tile.isSystem) {
    153                 mOtherTiles.remove(i--);
    154                 mTiles.add(tile);
    155             }
    156         }
    157         mTileDividerIndex = mTiles.size();
    158         mTiles.add(null);
    159         mTiles.addAll(mOtherTiles);
    160         updateDividerLocations();
    161         notifyDataSetChanged();
    162     }
    163 
    164     private TileInfo getAndRemoveOther(String s) {
    165         for (int i = 0; i < mOtherTiles.size(); i++) {
    166             if (mOtherTiles.get(i).spec.equals(s)) {
    167                 return mOtherTiles.remove(i);
    168             }
    169         }
    170         return null;
    171     }
    172 
    173     @Override
    174     public int getItemViewType(int position) {
    175         if (mAccessibilityMoving && position == mEditIndex - 1) {
    176             return TYPE_ACCESSIBLE_DROP;
    177         }
    178         if (position == mTileDividerIndex) {
    179             return TYPE_DIVIDER;
    180         }
    181         if (mTiles.get(position) == null) {
    182             return TYPE_EDIT;
    183         }
    184         return TYPE_TILE;
    185     }
    186 
    187     @Override
    188     public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
    189         final Context context = parent.getContext();
    190         LayoutInflater inflater = LayoutInflater.from(context);
    191         if (viewType == TYPE_DIVIDER) {
    192             return new Holder(inflater.inflate(R.layout.qs_customize_tile_divider, parent, false));
    193         }
    194         if (viewType == TYPE_EDIT) {
    195             return new Holder(inflater.inflate(R.layout.qs_customize_divider, parent, false));
    196         }
    197         FrameLayout frame = (FrameLayout) inflater.inflate(R.layout.qs_customize_tile_frame, parent,
    198                 false);
    199         frame.addView(new CustomizeTileView(context, new QSIconViewImpl(context)));
    200         return new Holder(frame);
    201     }
    202 
    203     @Override
    204     public int getItemCount() {
    205         return mTiles.size();
    206     }
    207 
    208     @Override
    209     public boolean onFailedToRecycleView(Holder holder) {
    210         holder.clearDrag();
    211         return true;
    212     }
    213 
    214     @Override
    215     public void onBindViewHolder(final Holder holder, int position) {
    216         if (holder.getItemViewType() == TYPE_DIVIDER) {
    217             holder.itemView.setVisibility(mTileDividerIndex < mTiles.size() - 1 ? View.VISIBLE
    218                     : View.INVISIBLE);
    219             return;
    220         }
    221         if (holder.getItemViewType() == TYPE_EDIT) {
    222             ((TextView) holder.itemView.findViewById(android.R.id.title)).setText(
    223                     mCurrentDrag != null ? R.string.drag_to_remove_tiles
    224                     : R.string.drag_to_add_tiles);
    225             return;
    226         }
    227         if (holder.getItemViewType() == TYPE_ACCESSIBLE_DROP) {
    228             holder.mTileView.setClickable(true);
    229             holder.mTileView.setFocusable(true);
    230             holder.mTileView.setFocusableInTouchMode(true);
    231             holder.mTileView.setVisibility(View.VISIBLE);
    232             holder.mTileView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
    233             holder.mTileView.setContentDescription(mContext.getString(
    234                     R.string.accessibility_qs_edit_position_label, position + 1));
    235             holder.mTileView.setOnClickListener(new OnClickListener() {
    236                 @Override
    237                 public void onClick(View v) {
    238                     selectPosition(holder.getAdapterPosition(), v);
    239                 }
    240             });
    241             if (mNeedsFocus) {
    242                 // Wait for this to get laid out then set its focus.
    243                 // Ensure that tile gets laid out so we get the callback.
    244                 holder.mTileView.requestLayout();
    245                 holder.mTileView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
    246                     @Override
    247                     public void onLayoutChange(View v, int left, int top, int right, int bottom,
    248                             int oldLeft, int oldTop, int oldRight, int oldBottom) {
    249                         holder.mTileView.removeOnLayoutChangeListener(this);
    250                         holder.mTileView.requestFocus();
    251                     }
    252                 });
    253                 mNeedsFocus = false;
    254             }
    255             return;
    256         }
    257 
    258         TileInfo info = mTiles.get(position);
    259 
    260         if (position > mEditIndex) {
    261             info.state.contentDescription = mContext.getString(
    262                     R.string.accessibility_qs_edit_add_tile_label, info.state.label);
    263         } else if (mAccessibilityMoving) {
    264             info.state.contentDescription = mContext.getString(
    265                     R.string.accessibility_qs_edit_position_label, position + 1);
    266         } else {
    267             info.state.contentDescription = mContext.getString(
    268                     R.string.accessibility_qs_edit_tile_label, position + 1, info.state.label);
    269         }
    270         holder.mTileView.onStateChanged(info.state);
    271         holder.mTileView.setAppLabel(info.appLabel);
    272         holder.mTileView.setShowAppLabel(position > mEditIndex && !info.isSystem);
    273 
    274         if (mAccessibilityManager.isTouchExplorationEnabled()) {
    275             final boolean selectable = !mAccessibilityMoving || position < mEditIndex;
    276             holder.mTileView.setClickable(selectable);
    277             holder.mTileView.setFocusable(selectable);
    278             holder.mTileView.setImportantForAccessibility(selectable
    279                     ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
    280                     : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
    281             if (selectable) {
    282                 holder.mTileView.setOnClickListener(new OnClickListener() {
    283                     @Override
    284                     public void onClick(View v) {
    285                         int position = holder.getAdapterPosition();
    286                         if (mAccessibilityMoving) {
    287                             selectPosition(position, v);
    288                         } else {
    289                             if (position < mEditIndex) {
    290                                 showAccessibilityDialog(position, v);
    291                             } else {
    292                                 startAccessibleDrag(position);
    293                             }
    294                         }
    295                     }
    296                 });
    297             }
    298         }
    299     }
    300 
    301     private void selectPosition(int position, View v) {
    302         // Remove the placeholder.
    303         mAccessibilityMoving = false;
    304         mTiles.remove(mEditIndex--);
    305         notifyItemRemoved(mEditIndex - 1);
    306         // Don't remove items when the last position is selected.
    307         if (position == mEditIndex) position--;
    308 
    309         move(mAccessibilityFromIndex, position, v);
    310 
    311         notifyDataSetChanged();
    312     }
    313 
    314     private void showAccessibilityDialog(final int position, final View v) {
    315         final TileInfo info = mTiles.get(position);
    316         CharSequence[] options = new CharSequence[] {
    317                 mContext.getString(R.string.accessibility_qs_edit_move_tile, info.state.label),
    318                 mContext.getString(R.string.accessibility_qs_edit_remove_tile, info.state.label),
    319         };
    320         AlertDialog dialog = new Builder(mContext)
    321                 .setItems(options, new DialogInterface.OnClickListener() {
    322                     @Override
    323                     public void onClick(DialogInterface dialog, int which) {
    324                         if (which == 0) {
    325                             startAccessibleDrag(position);
    326                         } else {
    327                             move(position, info.isSystem ? mEditIndex : mTileDividerIndex, v);
    328                             notifyItemChanged(mTileDividerIndex);
    329                             notifyDataSetChanged();
    330                         }
    331                     }
    332                 }).setNegativeButton(android.R.string.cancel, null)
    333                 .create();
    334         SystemUIDialog.setShowForAllUsers(dialog, true);
    335         SystemUIDialog.applyFlags(dialog);
    336         dialog.show();
    337     }
    338 
    339     private void startAccessibleDrag(int position) {
    340         mAccessibilityMoving = true;
    341         mNeedsFocus = true;
    342         mAccessibilityFromIndex = position;
    343         // Add placeholder for last slot.
    344         mTiles.add(mEditIndex++, null);
    345         notifyDataSetChanged();
    346     }
    347 
    348     public SpanSizeLookup getSizeLookup() {
    349         return mSizeLookup;
    350     }
    351 
    352     private boolean move(int from, int to, View v) {
    353         if (to == from) {
    354             return true;
    355         }
    356         CharSequence fromLabel = mTiles.get(from).state.label;
    357         move(from, to, mTiles);
    358         updateDividerLocations();
    359         CharSequence announcement;
    360         if (to >= mEditIndex) {
    361             MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_REMOVE_SPEC,
    362                     strip(mTiles.get(to)));
    363             MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_REMOVE,
    364                     from);
    365             announcement = mContext.getString(R.string.accessibility_qs_edit_tile_removed,
    366                     fromLabel);
    367         } else if (from >= mEditIndex) {
    368             MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_ADD_SPEC,
    369                     strip(mTiles.get(to)));
    370             MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_ADD,
    371                     to);
    372             announcement = mContext.getString(R.string.accessibility_qs_edit_tile_added,
    373                     fromLabel, (to + 1));
    374         } else {
    375             MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_MOVE_SPEC,
    376                     strip(mTiles.get(to)));
    377             MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_MOVE,
    378                     to);
    379             announcement = mContext.getString(R.string.accessibility_qs_edit_tile_moved,
    380                     fromLabel, (to + 1));
    381         }
    382         v.announceForAccessibility(announcement);
    383         saveSpecs(mHost);
    384         return true;
    385     }
    386 
    387     private void updateDividerLocations() {
    388         // The first null is the edit tiles label, the second null is the tile divider.
    389         // If there is no second null, then there are no non-system tiles.
    390         mEditIndex = -1;
    391         mTileDividerIndex = mTiles.size();
    392         for (int i = 0; i < mTiles.size(); i++) {
    393             if (mTiles.get(i) == null) {
    394                 if (mEditIndex == -1) {
    395                     mEditIndex = i;
    396                 } else {
    397                     mTileDividerIndex = i;
    398                 }
    399             }
    400         }
    401         if (mTiles.size() - 1 == mTileDividerIndex) {
    402             notifyItemChanged(mTileDividerIndex);
    403         }
    404     }
    405 
    406     private static String strip(TileInfo tileInfo) {
    407         String spec = tileInfo.spec;
    408         if (spec.startsWith(CustomTile.PREFIX)) {
    409             ComponentName component = CustomTile.getComponentFromSpec(spec);
    410             return component.getPackageName();
    411         }
    412         return spec;
    413     }
    414 
    415     private <T> void move(int from, int to, List<T> list) {
    416         list.add(to, list.remove(from));
    417         notifyItemMoved(from, to);
    418     }
    419 
    420     public class Holder extends ViewHolder {
    421         private CustomizeTileView mTileView;
    422 
    423         public Holder(View itemView) {
    424             super(itemView);
    425             if (itemView instanceof FrameLayout) {
    426                 mTileView = (CustomizeTileView) ((FrameLayout) itemView).getChildAt(0);
    427                 mTileView.setBackground(null);
    428                 mTileView.getIcon().disableAnimation();
    429             }
    430         }
    431 
    432         public void clearDrag() {
    433             itemView.clearAnimation();
    434             mTileView.findViewById(R.id.tile_label).clearAnimation();
    435             mTileView.findViewById(R.id.tile_label).setAlpha(1);
    436             mTileView.getAppLabel().clearAnimation();
    437             mTileView.getAppLabel().setAlpha(.6f);
    438         }
    439 
    440         public void startDrag() {
    441             itemView.animate()
    442                     .setDuration(DRAG_LENGTH)
    443                     .scaleX(DRAG_SCALE)
    444                     .scaleY(DRAG_SCALE);
    445             mTileView.findViewById(R.id.tile_label).animate()
    446                     .setDuration(DRAG_LENGTH)
    447                     .alpha(0);
    448             mTileView.getAppLabel().animate()
    449                     .setDuration(DRAG_LENGTH)
    450                     .alpha(0);
    451         }
    452 
    453         public void stopDrag() {
    454             itemView.animate()
    455                     .setDuration(DRAG_LENGTH)
    456                     .scaleX(1)
    457                     .scaleY(1);
    458             mTileView.findViewById(R.id.tile_label).animate()
    459                     .setDuration(DRAG_LENGTH)
    460                     .alpha(1);
    461             mTileView.getAppLabel().animate()
    462                     .setDuration(DRAG_LENGTH)
    463                     .alpha(.6f);
    464         }
    465     }
    466 
    467     private final SpanSizeLookup mSizeLookup = new SpanSizeLookup() {
    468         @Override
    469         public int getSpanSize(int position) {
    470             final int type = getItemViewType(position);
    471             return type == TYPE_EDIT || type == TYPE_DIVIDER ? 3 : 1;
    472         }
    473     };
    474 
    475     private class TileItemDecoration extends ItemDecoration {
    476         private final ColorDrawable mDrawable;
    477 
    478         private TileItemDecoration(Context context) {
    479             TypedArray ta =
    480                     context.obtainStyledAttributes(new int[]{android.R.attr.colorSecondary});
    481             mDrawable = new ColorDrawable(ta.getColor(0, 0));
    482             ta.recycle();
    483         }
    484 
    485 
    486         @Override
    487         public void onDraw(Canvas c, RecyclerView parent, State state) {
    488             super.onDraw(c, parent, state);
    489 
    490             final int childCount = parent.getChildCount();
    491             final int width = parent.getWidth();
    492             final int bottom = parent.getBottom();
    493             for (int i = 0; i < childCount; i++) {
    494                 final View child = parent.getChildAt(i);
    495                 final ViewHolder holder = parent.getChildViewHolder(child);
    496                 if (holder.getAdapterPosition() < mEditIndex && !(child instanceof TextView)) {
    497                     continue;
    498                 }
    499 
    500                 final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
    501                         .getLayoutParams();
    502                 final int top = child.getTop() + params.topMargin +
    503                         Math.round(ViewCompat.getTranslationY(child));
    504                 // Draw full width, in case there aren't tiles all the way across.
    505                 mDrawable.setBounds(0, top, width, bottom);
    506                 mDrawable.draw(c);
    507                 break;
    508             }
    509         }
    510     };
    511 
    512     private final ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() {
    513 
    514         @Override
    515         public boolean isLongPressDragEnabled() {
    516             return true;
    517         }
    518 
    519         @Override
    520         public boolean isItemViewSwipeEnabled() {
    521             return false;
    522         }
    523 
    524         @Override
    525         public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
    526             super.onSelectedChanged(viewHolder, actionState);
    527             if (actionState != ItemTouchHelper.ACTION_STATE_DRAG) {
    528                 viewHolder = null;
    529             }
    530             if (viewHolder == mCurrentDrag) return;
    531             if (mCurrentDrag != null) {
    532                 int position = mCurrentDrag.getAdapterPosition();
    533                 TileInfo info = mTiles.get(position);
    534                 mCurrentDrag.mTileView.setShowAppLabel(
    535                         position > mEditIndex && !info.isSystem);
    536                 mCurrentDrag.stopDrag();
    537                 mCurrentDrag = null;
    538             }
    539             if (viewHolder != null) {
    540                 mCurrentDrag = (Holder) viewHolder;
    541                 mCurrentDrag.startDrag();
    542             }
    543             mHandler.post(new Runnable() {
    544                 @Override
    545                 public void run() {
    546                     notifyItemChanged(mEditIndex);
    547                 }
    548             });
    549         }
    550 
    551         @Override
    552         public boolean canDropOver(RecyclerView recyclerView, ViewHolder current,
    553                 ViewHolder target) {
    554             return target.getAdapterPosition() <= mEditIndex + 1;
    555         }
    556 
    557         @Override
    558         public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) {
    559             if (viewHolder.getItemViewType() == TYPE_EDIT) {
    560                 return makeMovementFlags(0, 0);
    561             }
    562             int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.RIGHT
    563                     | ItemTouchHelper.LEFT;
    564             return makeMovementFlags(dragFlags, 0);
    565         }
    566 
    567         @Override
    568         public boolean onMove(RecyclerView recyclerView, ViewHolder viewHolder, ViewHolder target) {
    569             int from = viewHolder.getAdapterPosition();
    570             int to = target.getAdapterPosition();
    571             return move(from, to, target.itemView);
    572         }
    573 
    574         @Override
    575         public void onSwiped(ViewHolder viewHolder, int direction) {
    576         }
    577     };
    578 }
    579