content: /*
   * Copyright (C) 2017 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.internal.widget;
  
  import android.util.Log;
  import android.view.View;
  import android.view.ViewGroup;
  
  import java.util.ArrayList;
  import java.util.List;
  
  /**
   * Helper class to manage children.
   * <p>
   * It wraps a RecyclerView and adds ability to hide some children. There are two sets of methods
   * provided by this class. <b>Regular</b> methods are the ones that replicate ViewGroup methods
   * like getChildAt, getChildCount etc. These methods ignore hidden children.
   * <p>
   * When RecyclerView needs direct access to the view group children, it can call unfiltered
   * methods like get getUnfilteredChildCount or getUnfilteredChildAt.
   */
  class ChildHelper {
  
      private static final boolean DEBUG = false;
  
      private static final String TAG = "ChildrenHelper";
  
      final Callback mCallback;
  
      final Bucket mBucket;
  
      final List<View> mHiddenViews;
  
      ChildHelper(Callback callback) {
          mCallback = callback;
          mBucket = new Bucket();
          mHiddenViews = new ArrayList<View>();
      }
  
      /**
       * Marks a child view as hidden
       *
       * @param child  View to hide.
       */
      private void hideViewInternal(View child) {
          mHiddenViews.add(child);
          mCallback.onEnteredHiddenState(child);
      }
  
      /**
       * Unmarks a child view as hidden.
       *
       * @param child  View to hide.
       */
      private boolean unhideViewInternal(View child) {
          if (mHiddenViews.remove(child)) {
              mCallback.onLeftHiddenState(child);
              return true;
          } else {
              return false;
          }
      }
  
      /**
       * Adds a view to the ViewGroup
       *
       * @param child  View to add.
       * @param hidden If set to true, this item will be invisible from regular methods.
       */
      void addView(View child, boolean hidden) {
          addView(child, -1, hidden);
      }
  
      /**
       * Add a view to the ViewGroup at an index
       *
       * @param child  View to add.
       * @param index  Index of the child from the regular perspective (excluding hidden views).
       *               ChildHelper offsets this index to actual ViewGroup index.
       * @param hidden If set to true, this item will be invisible from regular methods.
       */
      void addView(View child, int index, boolean hidden) {
          final int offset;
          if (index < 0) {
              offset = mCallback.getChildCount();
          } else {
              offset = getOffset(index);
          }
          mBucket.insert(offset, hidden);
          if (hidden) {
              hideViewInternal(child);
          }
          mCallback.addView(child, offset);
          if (DEBUG) {
              Log.d(TAG, "addViewAt " + index + ",h:" + hidden + ", " + this);
          }
      }
  
      private int getOffset(int index) {
          if (index < 0) {
              return -1; //anything below 0 won't work as diff will be undefined.
          }
          final int limit = mCallback.getChildCount();
          int offset = index;
          while (offset < limit) {
              final int removedBefore = mBucket.countOnesBefore(offset);
              final int diff = index - (offset - removedBefore);
              if (diff == 0) {
                  while (mBucket.get(offset)) { // ensure this offset is not hidden
                      offset++;
                  }
                  return offset;
              } else {
                  offset += diff;
              }
          }
          return -1;
      }
  
      /**
       * Removes the provided View from underlying RecyclerView.
       *
       * @param view The view to remove.
       */
      void removeView(View view) {
          int index = mCallback.indexOfChild(view);
          if (index < 0) {
              return;
          }
          if (mBucket.remove(index)) {
              unhideViewInternal(view);
          }
          mCallback.removeViewAt(index);
          if (DEBUG) {
              Log.d(TAG, "remove View off:" + index + "," + this);
          }
      }
  
      /**
       * Removes the view at the provided index from RecyclerView.
       *
       * @param index Index of the child from the regular perspective (excluding hidden views).
       *              ChildHelper offsets this index to actual ViewGroup index.
       */
      void removeViewAt(int index) {
          final int offset = getOffset(index);
          final View view = mCallback.getChildAt(offset);
          if (view == null) {
              return;
          }
          if (mBucket.remove(offset)) {
              unhideViewInternal(view);
          }
          mCallback.removeViewAt(offset);
          if (DEBUG) {
              Log.d(TAG, "removeViewAt " + index + ", off:" + offset + ", " + this);
          }
      }
  
      /**
       * Returns the child at provided index.
       *
       * @param index Index of the child to return in regular perspective.
       */
      View getChildAt(int index) {
          final int offset = getOffset(index);
          return mCallback.getChildAt(offset);
      }
  
      /**
       * Removes all views from the ViewGroup including the hidden ones.
       */
      void removeAllViewsUnfiltered() {
          mBucket.reset();
          for (int i = mHiddenViews.size() - 1; i >= 0; i--) {
              mCallback.onLeftHiddenState(mHiddenViews.get(i));
              mHiddenViews.remove(i);
          }
          mCallback.removeAllViews();
          if (DEBUG) {
              Log.d(TAG, "removeAllViewsUnfiltered");
          }
      }
  
      /**
       * This can be used to find a disappearing view by position.
       *
       * @param position The adapter position of the item.
       * @return         A hidden view with a valid ViewHolder that matches the position.
       */
      View findHiddenNonRemovedView(int position) {
          final int count = mHiddenViews.size();
          for (int i = 0; i < count; i++) {
              final View view = mHiddenViews.get(i);
              RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
              if (holder.getLayoutPosition() == position
                      && !holder.isInvalid()
                      && !holder.isRemoved()) {
                  return view;
              }
          }
          return null;
      }
  
      /**
       * Attaches the provided view to the underlying ViewGroup.
       *
       * @param child        Child to attach.
       * @param index        Index of the child to attach in regular perspective.
       * @param layoutParams LayoutParams for the child.
       * @param hidden       If set to true, this item will be invisible to the regular methods.
       */
      void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams,
              boolean hidden) {
          final int offset;
          if (index < 0) {
              offset = mCallback.getChildCount();
          } else {
              offset = getOffset(index);
          }
          mBucket.insert(offset, hidden);
          if (hidden) {
              hideViewInternal(child);
          }
          mCallback.attachViewToParent(child, offset, layoutParams);
          if (DEBUG) {
              Log.d(TAG, "attach view to parent index:" + index + ",off:" + offset + ","
                      + "h:" + hidden + ", " + this);
          }
      }
  
      /**
       * Returns the number of children that are not hidden.
       *
       * @return Number of children that are not hidden.
       * @see #getChildAt(int)
       */
      int getChildCount() {
          return mCallback.getChildCount() - mHiddenViews.size();
      }
  
      /**
       * Returns the total number of children.
       *
       * @return The total number of children including the hidden views.
       * @see #getUnfilteredChildAt(int)
       */
      int getUnfilteredChildCount() {
          return mCallback.getChildCount();
      }
  
      /**
       * Returns a child by ViewGroup offset. ChildHelper won't offset this index.
       *
       * @param index ViewGroup index of the child to return.
       * @return The view in the provided index.
       */
      View getUnfilteredChildAt(int index) {
          return mCallback.getChildAt(index);
      }
  
      /**
       * Detaches the view at the provided index.
       *
       * @param index Index of the child to return in regular perspective.
       */
      void detachViewFromParent(int index) {
          final int offset = getOffset(index);
          mBucket.remove(offset);
          mCallback.detachViewFromParent(offset);
          if (DEBUG) {
              Log.d(TAG, "detach view from parent " + index + ", off:" + offset);
          }
      }
  
      /**
       * Returns the index of the child in regular perspective.
       *
       * @param child The child whose index will be returned.
       * @return The regular perspective index of the child or -1 if it does not exists.
       */
      int indexOfChild(View child) {
          final int index = mCallback.indexOfChild(child);
          if (index == -1) {
              return -1;
          }
          if (mBucket.get(index)) {
              if (DEBUG) {
                  throw new IllegalArgumentException("cannot get index of a hidden child");
              } else {
                  return -1;
              }
          }
          // reverse the index
          return index - mBucket.countOnesBefore(index);
      }
  
      /**
       * Returns whether a View is visible to LayoutManager or not.
       *
       * @param view The child view to check. Should be a child of the Callback.
       * @return True if the View is not visible to LayoutManager
       */
      boolean isHidden(View view) {
          return mHiddenViews.contains(view);
      }
  
      /**
       * Marks a child view as hidden.
       *
       * @param view The view to hide.
       */
      void hide(View view) {
          final int offset = mCallback.indexOfChild(view);
          if (offset < 0) {
              throw new IllegalArgumentException("view is not a child, cannot hide " + view);
          }
          if (DEBUG && mBucket.get(offset)) {
              throw new RuntimeException("trying to hide same view twice, how come ? " + view);
          }
          mBucket.set(offset);
          hideViewInternal(view);
          if (DEBUG) {
              Log.d(TAG, "hiding child " + view + " at offset " + offset + ", " + this);
          }
      }
  
      /**
       * Moves a child view from hidden list to regular list.
       * Calling this method should probably be followed by a detach, otherwise, it will suddenly
       * show up in LayoutManager's children list.
       *
       * @param view The hidden View to unhide
       */
      void unhide(View view) {
          final int offset = mCallback.indexOfChild(view);
          if (offset < 0) {
              throw new IllegalArgumentException("view is not a child, cannot hide " + view);
          }
          if (!mBucket.get(offset)) {
              throw new RuntimeException("trying to unhide a view that was not hidden" + view);
          }
          mBucket.clear(offset);
          unhideViewInternal(view);
      }
  
      @Override
      public String toString() {
          return mBucket.toString() + ", hidden list:" + mHiddenViews.size();
      }
  
      /**
       * Removes a view from the ViewGroup if it is hidden.
       *
       * @param view The view to remove.
       * @return True if the View is found and it is hidden. False otherwise.
       */
      boolean removeViewIfHidden(View view) {
          final int index = mCallback.indexOfChild(view);
          if (index == -1) {
              if (unhideViewInternal(view) && DEBUG) {
                  throw new IllegalStateException("view is in hidden list but not in view group");
              }
              return true;
          }
          if (mBucket.get(index)) {
              mBucket.remove(index);
              if (!unhideViewInternal(view) && DEBUG) {
                  throw new IllegalStateException(
                          "removed a hidden view but it is not in hidden views list");
              }
              mCallback.removeViewAt(index);
              return true;
          }
          return false;
      }
  
      /**
       * Bitset implementation that provides methods to offset indices.
       */
      static class Bucket {
  
          static final int BITS_PER_WORD = Long.SIZE;
  
          static final long LAST_BIT = 1L << (Long.SIZE - 1);
  
          long mData = 0;
  
          Bucket mNext;
  
          void set(int index) {
              if (index >= BITS_PER_WORD) {
                  ensureNext();
                  mNext.set(index - BITS_PER_WORD);
              } else {
                  mData |= 1L << index;
              }
          }
  
          private void ensureNext() {
              if (mNext == null) {
                  mNext = new Bucket();
              }
          }
  
          void clear(int index) {
              if (index >= BITS_PER_WORD) {
                  if (mNext != null) {
                      mNext.clear(index - BITS_PER_WORD);
                  }
              } else {
                  mData &= ~(1L << index);
              }
  
          }
  
          boolean get(int index) {
              if (index >= BITS_PER_WORD) {
                  ensureNext();
                  return mNext.get(index - BITS_PER_WORD);
              } else {
                  return (mData & (1L << index)) != 0;
              }
          }
  
          void reset() {
              mData = 0;
              if (mNext != null) {
                  mNext.reset();
              }
          }
  
          void insert(int index, boolean value) {
              if (index >= BITS_PER_WORD) {
                  ensureNext();
                  mNext.insert(index - BITS_PER_WORD, value);
              } else {
                  final boolean lastBit = (mData & LAST_BIT) != 0;
                  long mask = (1L << index) - 1;
                  final long before = mData & mask;
                  final long after = ((mData & ~mask)) << 1;
                  mData = before | after;
                  if (value) {
                      set(index);
                  } else {
                      clear(index);
                  }
                  if (lastBit || mNext != null) {
                      ensureNext();
                      mNext.insert(0, lastBit);
                  }
              }
          }
  
          boolean remove(int index) {
              if (index >= BITS_PER_WORD) {
                  ensureNext();
                  return mNext.remove(index - BITS_PER_WORD);
              } else {
                  long mask = (1L << index);
                  final boolean value = (mData & mask) != 0;
                  mData &= ~mask;
                  mask = mask - 1;
                  final long before = mData & mask;
                  // cannot use >> because it adds one.
                  final long after = Long.rotateRight(mData & ~mask, 1);
                  mData = before | after;
                  if (mNext != null) {
                      if (mNext.get(0)) {
                          set(BITS_PER_WORD - 1);
                      }
                      mNext.remove(0);
                  }
                  return value;
              }
          }
  
          int countOnesBefore(int index) {
              if (mNext == null) {
                  if (index >= BITS_PER_WORD) {
                      return Long.bitCount(mData);
                  }
                  return Long.bitCount(mData & ((1L << index) - 1));
              }
              if (index < BITS_PER_WORD) {
                  return Long.bitCount(mData & ((1L << index) - 1));
              } else {
                  return mNext.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData);
              }
          }
  
          @Override
          public String toString() {
              return mNext == null ? Long.toBinaryString(mData)
                      : mNext.toString() + "xx" + Long.toBinaryString(mData);
          }
      }
  
      interface Callback {
  
          int getChildCount();
  
          void addView(View child, int index);
  
          int indexOfChild(View view);
  
          void removeViewAt(int index);
  
          View getChildAt(int offset);
  
          void removeAllViews();
  
          RecyclerView.ViewHolder getChildViewHolder(View view);
  
          void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams);
  
          void detachViewFromParent(int offset);
  
          void onEnteredHiddenState(View child);
  
          void onLeftHiddenState(View child);
      }
  }
  
  