package tk.wasdennnoch.androidn_ify.systemui.qs; import android.content.Context; import android.graphics.Canvas; import android.graphics.drawable.ColorDrawable; import android.support.annotation.NonNull; import android.support.v7.util.DiffUtil; import android.support.v7.util.ListUpdateCallback; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.RelativeLayout; import android.widget.TextView; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import de.robv.android.xposed.XposedHelpers; import tk.wasdennnoch.androidn_ify.R; import tk.wasdennnoch.androidn_ify.XposedHook; import tk.wasdennnoch.androidn_ify.systemui.notifications.StatusBarHeaderHooks; import tk.wasdennnoch.androidn_ify.utils.ConfigUtils; import tk.wasdennnoch.androidn_ify.utils.ResourceUtils; import static tk.wasdennnoch.androidn_ify.systemui.qs.QSTileHostHooks.KEY_EDIT_TILEVIEW; import static tk.wasdennnoch.androidn_ify.systemui.qs.customize.QSCustomizer.MODE_EDIT_SECURE; import static tk.wasdennnoch.androidn_ify.systemui.qs.customize.QSCustomizer.MODE_NORMAL; @SuppressWarnings("WeakerAccess") public class TileAdapter extends RecyclerView.Adapter<TileAdapter.TileViewHolder> { protected static final String PACKAGE_SYSTEMUI = XposedHook.PACKAGE_SYSTEMUI; public static final long MOVE_DURATION = 150; private static final long DRAG_LENGTH = 100; private static final float DRAG_SCALE = 1.2f; public static final float TILE_ASPECT = 1.2f; private static final String TAG = "TileAdapter"; private final ItemTouchHelper mItemTouchHelper; private final ListUpdateCallback mTileUpdateCallback = new TileUpdateCallback(); private List<String> mSecureTiles = new ArrayList<>(); private List<String> mPreviousSpecs = new ArrayList<>(); private List<String> mTileSpecs = new ArrayList<>(); protected ArrayList<Object> mRecords = new ArrayList<>(); protected ArrayList<ViewGroup> mTileViews = new ArrayList<>(); protected final Context mContext; protected final int mCellHeight; protected final int mCellWidth; private ResourceUtils mRes; public int mDividerIndex; private int mMode = MODE_NORMAL; public TileAdapter.TileViewHolder mCurrentDrag; private boolean mIsInvalid = true; public TileAdapter(Context context) { mContext = context; //mQsPanel = qsPanel; mCellHeight = mContext.getResources().getDimensionPixelSize(mContext.getResources().getIdentifier("qs_tile_height", "dimen", PACKAGE_SYSTEMUI)); mCellWidth = (int) (mCellHeight * TILE_ASPECT); ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() { @Override public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.START | ItemTouchHelper.END; if (mMode == MODE_EDIT_SECURE || viewHolder.getItemViewType() == 1) { dragFlags = 0; } return makeMovementFlags(dragFlags, 0); } @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { return onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition()); } @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { } @Override public boolean isLongPressDragEnabled() { return true; } @Override public boolean isItemViewSwipeEnabled() { return false; } @Override public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { if (mCurrentDrag != null) { mCurrentDrag.stopDrag(); mCurrentDrag = null; } if (viewHolder != null) { mCurrentDrag = (TileViewHolder) viewHolder; mCurrentDrag.startDrag(); } try { notifyItemChanged(mDividerIndex); } catch (Throwable ignore) { } super.onSelectedChanged(viewHolder, actionState); } @Override public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { ((TileViewHolder) viewHolder).stopDrag(); super.clearView(recyclerView, viewHolder); } }; mItemTouchHelper = new CustomItemTouchHelper(mCallbacks); XposedHelpers.setIntField(mCallbacks, "mCachedMaxScrollSpeed", ResourceUtils.getInstance(mContext).getDimensionPixelSize(R.dimen.lib_item_touch_helper_max_drag_scroll_per_frame)); } private void init(ArrayList<Object> records, Context context) { mIsInvalid = false; mRes = ResourceUtils.getInstance(context); setRecords(records); mTileSpecs = convertToSpecs(); mPreviousSpecs.addAll(mTileSpecs); if (ConfigUtils.M && QSTileHostHooks.mSecureTiles != null) mSecureTiles.addAll(QSTileHostHooks.mSecureTiles); addDivider(); addAvailableTiles(); mDividerIndex = mTileViews.indexOf(null); } public void reInit(ArrayList<Object> records, Context context) { mRecords.clear(); mPreviousSpecs.clear(); mTileSpecs.clear(); mTileViews.clear(); mSecureTiles.clear(); init(records, context); } public void setMode(int mode) { mMode = mode; notifyDataSetChanged(); } public int getMode() { return mMode; } private void addDivider() { mTileSpecs.add(null); //mRecords.add(null); mTileViews.add(null); } private void addAvailableTiles() { // TODO completely remove AvailableTileAdapter AvailableTileAdapter mAvailableTileAdapter = new AvailableTileAdapter(mRecords, mContext); int count = mAvailableTileAdapter.getItemCount(); for (int i = 0; i < count; i++) { mTileSpecs.add((String) mAvailableTileAdapter.mRecords.get(i)); mTileViews.add(mAvailableTileAdapter.mTileViews.get(i)); } } public void setRecords(ArrayList<Object> records) { mTileViews.clear(); mRecords.clear(); for (int i = 0; i < records.size(); i++) { Object tilerecord = records.get(i); final Object tile = XposedHelpers.getObjectField(tilerecord, "tile"); addTile(i, tile); mRecords.add(tilerecord); } } private void addTile(int i, Object tile) { RelativeLayout.LayoutParams tileViewLp = new RelativeLayout.LayoutParams(mCellWidth, mCellHeight); tileViewLp.addRule(RelativeLayout.CENTER_IN_PARENT); ViewGroup tileView = (ViewGroup) XposedHelpers.callMethod(tile, "createTileView", mContext); tileView.setLayoutParams(tileViewLp); try { XposedHelpers.callMethod(tileView, "setDual", false); } catch (Throwable t) {// CM13 try { XposedHelpers.callMethod(tileView, "setDual", false, false); } catch (Throwable ignore) { // Other ROMs } } try { XposedHelpers.callMethod(tileView, "onStateChanged", XposedHelpers.callMethod(tile, "getState")); } catch (Throwable t) { XposedHelpers.callMethod(tileView, "onStateChanged", XposedHelpers.getObjectField(tile, "mState")); } XposedHelpers.setAdditionalInstanceField(tile, KEY_EDIT_TILEVIEW, tileView); mTileViews.add(i, tileView); } private int getWidth() { return StatusBarHeaderHooks.mQsPanel.getWidth() / 3; } private GridLayoutManager.LayoutParams generateLayoutParams() { int i = getWidth(); return new GridLayoutManager.LayoutParams(i, (int) (getWidth() / TILE_ASPECT)); } @Override public TileViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { Context context = parent.getContext(); View itemView; if (viewType == 1) { ResourceUtils res = ResourceUtils.getInstance(context); LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); itemView = inflater.inflate(res.getLayout(R.layout.qs_customize_divider), parent, false); } else { itemView = new RelativeLayout(parent.getContext()); itemView.setLayoutParams(generateLayoutParams()); } return new TileViewHolder(itemView); } @Override public void onBindViewHolder(TileViewHolder holder, int position) { if (holder.getItemViewType() == 1) { TextView textView = (TextView) holder.itemView.findViewById(android.R.id.title); int textId; if (mCurrentDrag == null) { textId = R.string.drag_to_add_tiles; } else { textId = R.string.drag_to_remove_tiles; } textView.setText(mRes.getString(textId)); } else { ViewGroup tileView = mTileViews.get(position); if (tileView.getParent() != null) ((ViewGroup) tileView.getParent()).removeView(tileView); holder.setTileView(tileView); holder.setBackgroundColor(mMode == MODE_EDIT_SECURE && mSecureTiles.contains(mTileSpecs.get(position)) ? 0x30FFFFFF : 0); } } @Override public int getItemCount() { return (mMode == MODE_EDIT_SECURE) ? mDividerIndex : mTileViews.size(); } @Override public int getItemViewType(int i) { return mTileViews.get(i) != null ? 0 : 1; } public RecyclerView.ItemDecoration getItemDecoration() { return mDecoration; } public GridLayoutManager.SpanSizeLookup getSizeLookup() { return mSizeLookup; } public boolean onItemMove(int fromPosition, int toPosition) { if (mMode == MODE_EDIT_SECURE) return false; if (fromPosition > mDividerIndex && toPosition > mDividerIndex) return false; if (fromPosition < mDividerIndex && mDividerIndex < 2) return false; move(fromPosition, toPosition, mTileViews); move(fromPosition, toPosition, mTileSpecs); mDividerIndex = mTileViews.indexOf(null); notifyItemMoved(fromPosition, toPosition); return true; } @SuppressWarnings("unchecked") private void move(int from, int to, List list) { int addIndex; if (from > to) { addIndex = to; } else { addIndex = to + 1; } list.add(addIndex, list.get(from)); int removeIndex = from; if (from > to) { removeIndex = from + 1; } list.remove(removeIndex); } private void addTile(int position) { onItemMove(position, mDividerIndex); } private void removeTile(int position) { onItemMove(position, mDividerIndex); } public ItemTouchHelper getItemTouchHelper() { return mItemTouchHelper; } public void invalidate() { mIsInvalid = true; } public boolean isInvalid() { return mIsInvalid; } public class TileViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { protected RelativeLayout mItemView; protected TextView mTextView; public TileViewHolder(View itemView) { super(itemView); if (itemView instanceof RelativeLayout) mItemView = (RelativeLayout) itemView; else if (itemView instanceof TextView) mTextView = (TextView) itemView; } private void setTileView(ViewGroup tileView) { if (mItemView == null) return; mItemView.removeAllViews(); mItemView.addView(tileView); tileView.setClickable(true); tileView.setOnClickListener(this); } public void startDrag() { if (mItemView == null) return; mItemView.animate().setDuration(DRAG_LENGTH).scaleX(DRAG_SCALE).scaleY(DRAG_SCALE); try { ((View) XposedHelpers.callMethod(mItemView.getChildAt(0), "labelView")).animate().setDuration(100L).alpha(0.0F); } catch (Throwable ignore) { } } public void stopDrag() { if (mItemView == null) return; mItemView.animate().setDuration(DRAG_LENGTH).scaleX(1.0F).scaleY(1.0F); try { ((View) XposedHelpers.callMethod(mItemView.getChildAt(0), "labelView")).animate().setDuration(100L).alpha(1.0F); } catch (Throwable ignore) { } } @Override public void onClick(View v) { int position = getAdapterPosition(); if (mMode == MODE_EDIT_SECURE) { String spec = mTileSpecs.get(position); if (mSecureTiles.contains(spec)) mSecureTiles.remove(spec); else mSecureTiles.add(spec); notifyItemChanged(position); } else { if (position < mDividerIndex) removeTile(position); else addTile(position); } } public void setBackgroundColor(int color) { mItemView.setBackgroundColor(color); } } public void saveChanges() { saveSecureTiles(); saveTiles(getAddedTileSpecs(), false); } public void resetTiles() { mSecureTiles.clear(); saveSecureTiles(); List<String> newSpces = new ArrayList<>(); newSpces.addAll(getDefaultTiles()); saveTiles(newSpces, true); } public static List<String> getDefaultTiles() { return Arrays.asList("wifi", "cell", "battery", "dnd", "flashlight", "rotation", "bt", "airplane", "location"); } private void saveTiles(List<String> tileSpecs, boolean update) { XposedHook.logD(TAG, "saveTiles called with update: " + update + " and specs " + TextUtils.join(", ", tileSpecs)); List<String> oldTiles = update ? getAddedTileSpecs() : QSTileHostHooks.mTileSpecs; if (oldTiles == null || !oldTiles.equals(tileSpecs)) { if (!compareSpecs(tileSpecs)) invalidate(); QSTileHostHooks.saveTileSpecs(mContext, tileSpecs); if (update) { QSTileHostHooks.mTileSpecs = tileSpecs; calcDiff(tileSpecs).dispatchUpdatesTo(mTileUpdateCallback); } return; } XposedHook.logD(TAG, "saveTiles: No changes to save"); } private void saveSecureTiles() { if (!ConfigUtils.M) return; XposedHook.logD(TAG, "saveSecureTiles called"); if (!QSTileHostHooks.mSecureTiles.equals(mSecureTiles)) { QSTileHostHooks.saveSecureTileSpecs(mContext, mSecureTiles); } XposedHook.logD(TAG, "saveSecureTiles: No changes to save"); } private boolean compareSpecs(List<String> newSpecs) { if (mPreviousSpecs == null || newSpecs == null) return false; int oldSize = mPreviousSpecs.size(); int newSize = newSpecs.size(); if (oldSize != newSize) return false; if (mPreviousSpecs.equals(newSpecs)) return true; for (String spec : mPreviousSpecs) if (!newSpecs.contains(spec)) return false; return true; } private DiffUtil.DiffResult calcDiff(List<String> specs) { List<String> newSpecs = new ArrayList<>(); newSpecs.addAll(specs); newSpecs.add(null); for (String spec : mTileSpecs) { if (spec == null) continue; if (newSpecs.contains(spec)) continue; newSpecs.add(spec); } return DiffUtil.calculateDiff(new TileSpecDiffCallback(mTileSpecs, newSpecs)); } public List<String> getAddedTileSpecs() { List<String> specs = new ArrayList<>(); for (String spec : mTileSpecs) { if (spec == null) return specs; specs.add(spec); } return specs; } @NonNull private List<String> convertToSpecs() { List<String> tileSpecs = new ArrayList<>(); for (int i = 0; i < mRecords.size(); i++) { Object tilerecord = mRecords.get(i); if (tilerecord == null) return tileSpecs; Object tile = XposedHelpers.getObjectField(tilerecord, "tile"); tileSpecs.add((String) XposedHelpers.getAdditionalInstanceField(tile, QSTileHostHooks.TILE_SPEC_NAME)); } return tileSpecs; } public void handleStateChanged(Object qsTile, Object state) { ViewGroup tileView = (ViewGroup) XposedHelpers.getAdditionalInstanceField(qsTile, KEY_EDIT_TILEVIEW); if (tileView != null) { XposedHelpers.callMethod(tileView, "onStateChanged", state); } } private final GridLayoutManager.SpanSizeLookup mSizeLookup = new GridLayoutManager.SpanSizeLookup() { public int getSpanSize(int i) { return (getItemViewType(i) == 1) ? 3 : 1; } }; private final RecyclerView.ItemDecoration mDecoration = new RecyclerView.ItemDecoration() { private final ColorDrawable mDrawable = new ColorDrawable(0xff384248); @Override public void onDraw(Canvas canvas, RecyclerView recyclerview, RecyclerView.State state) { int count = recyclerview.getChildCount(); View child; for (int i = 0; i < count; i++) { child = recyclerview.getChildAt(i); if (recyclerview.getChildViewHolder(child).getAdapterPosition() >= mDividerIndex) { RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams(); int childTop = child.getTop(); int topMargin = layoutParams.topMargin; int childTranslationY = Math.round(child.getTranslationY()); mDrawable.setBounds(0, childTop + topMargin + childTranslationY, recyclerview.getWidth(), recyclerview.getBottom()); mDrawable.draw(canvas); } } } }; private class CustomItemTouchHelper extends ItemTouchHelper { public CustomItemTouchHelper(Callback callback) { super(callback); } @Override public void attachToRecyclerView(RecyclerView recyclerView) { try { RecyclerView oldRecyclerView = (RecyclerView) XposedHelpers.getObjectField(this, "mRecyclerView"); if (oldRecyclerView == recyclerView) { return; // nothing to do } if (oldRecyclerView != null) { XposedHelpers.findMethodBestMatch(ItemTouchHelper.class, "destroyCallbacks").invoke(this); } XposedHelpers.setObjectField(this, "mRecyclerView", recyclerView); if (recyclerView != null) { XposedHelpers.findMethodBestMatch(ItemTouchHelper.class, "setupCallbacks").invoke(this); } } catch (Throwable t) { XposedHook.logE(TAG, "Error attaching ItemTouchCallback to RecyclerView", t); } } } private class TileSpecDiffCallback extends DiffUtil.Callback { private List<String> mOldSpecs; private List<String> mNewSpecs; public TileSpecDiffCallback(List<String> oldSpecs, List<String> newSpecs) { mOldSpecs = oldSpecs; mNewSpecs = newSpecs; } @Override public int getOldListSize() { return mOldSpecs.size(); } @Override public int getNewListSize() { return mNewSpecs.size(); } @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { Object oldItem = mOldSpecs.get(oldItemPosition); Object newItem = mNewSpecs.get(newItemPosition); if (oldItem == null || newItem == null) return oldItem == newItem; return oldItem.equals(newItem); } @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { return areItemsTheSame(oldItemPosition, newItemPosition); } } private class TileUpdateCallback implements ListUpdateCallback { @Override public void onInserted(int position, int count) { XposedHook.logW(TAG, "TileUpdateCallback: onInserted called with position=" + position + " and count=" + count); } @Override public void onRemoved(int position, int count) { XposedHook.logW(TAG, "TileUpdateCallback: onRemoved called with position=" + position + " and count=" + count); } @Override public void onMoved(int fromPosition, int toPosition) { onItemMove(fromPosition, toPosition); } @Override public void onChanged(int position, int count, Object payload) { XposedHook.logW(TAG, "TileUpdateCallback: onChanged called with position=" + position + ", count=" + count + " and payload=" + payload); } } }