Home | History | Annotate | Download | only in items
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.setupwizardlib.items;
     18 
     19 import android.content.Context;
     20 import android.util.AttributeSet;
     21 import android.util.Log;
     22 import android.util.SparseIntArray;
     23 
     24 import java.util.ArrayList;
     25 import java.util.List;
     26 
     27 public class ItemGroup extends AbstractItemHierarchy implements ItemInflater.ItemParent,
     28         ItemHierarchy.Observer {
     29 
     30     /* static section */
     31 
     32     private static final String TAG = "ItemGroup";
     33 
     34     /**
     35      * Binary search for the closest value that's smaller than or equal to {@code value}, and
     36      * return the corresponding key.
     37      */
     38     private static int binarySearch(SparseIntArray array, int value) {
     39         final int size = array.size();
     40         int lo = 0;
     41         int hi = size - 1;
     42 
     43         while (lo <= hi) {
     44             final int mid = (lo + hi) >>> 1;
     45             final int midVal = array.valueAt(mid);
     46 
     47             if (midVal < value) {
     48                 lo = mid + 1;
     49             } else if (midVal > value) {
     50                 hi = mid - 1;
     51             } else {
     52                 return array.keyAt(mid);  // value found
     53             }
     54         }
     55         // Value not found. Return the last item before our search range, which is the closest
     56         // value smaller than the value we are looking for.
     57         return array.keyAt(lo - 1);
     58     }
     59 
     60     /**
     61      * Same as {@link List#indexOf(Object)}, but using identity comparison rather than
     62      * {@link Object#equals(Object)}.
     63      */
     64     private static <T> int identityIndexOf(List<T> list, T object) {
     65         final int count = list.size();
     66         for (int i = 0; i < count; i++) {
     67             if (list.get(i) == object) {
     68                 return i;
     69             }
     70         }
     71         return -1;
     72     }
     73 
     74     /* non-static section */
     75 
     76     private List<ItemHierarchy> mChildren = new ArrayList<>();
     77 
     78     /**
     79      * A mapping from the index of an item hierarchy in mChildren, to the first position in which
     80      * the corresponding child hierarchy represents. For example:
     81      *
     82      *   ItemHierarchy                 Item               Item Position
     83      *       Index
     84      *
     85      *         0            [         Wi-Fi AP 1       ]        0
     86      *                      |         Wi-Fi AP 2       |        1
     87      *                      |         Wi-Fi AP 3       |        2
     88      *                      |         Wi-Fi AP 4       |        3
     89      *                      [         Wi-Fi AP 5       ]        4
     90      *
     91      *         1            [  <Empty Item Hierarchy>  ]
     92      *
     93      *         2            [     Use cellular data    ]        5
     94      *
     95      *         3            [       Don't connect      ]        6
     96      *
     97      * For this example of Wi-Fi screen, the following mapping will be produced:
     98      *     [ 0 -> 0 | 2 -> 5 | 3 -> 6 ]
     99      *
    100      * Also note how ItemHierarchy index 1 is not present in the map, because it is empty.
    101      *
    102      * ItemGroup uses this map to look for which ItemHierarchy an item at a given position belongs
    103      * to.
    104      */
    105     private SparseIntArray mHierarchyStart = new SparseIntArray();
    106 
    107     private int mCount = 0;
    108     private boolean mDirty = false;
    109 
    110     public ItemGroup() {
    111         super();
    112     }
    113 
    114     public ItemGroup(Context context, AttributeSet attrs) {
    115         // Constructor for XML inflation
    116         super(context, attrs);
    117     }
    118 
    119     /**
    120      * Add a child hierarchy to this item group.
    121      */
    122     @Override
    123     public void addChild(ItemHierarchy child) {
    124         mDirty = true;
    125         mChildren.add(child);
    126         child.registerObserver(this);
    127 
    128         final int count = child.getCount();
    129         if (count > 0) {
    130             notifyItemRangeInserted(getChildPosition(child), count);
    131         }
    132     }
    133 
    134     /**
    135      * Remove a previously added child from this item group.
    136      *
    137      * @return True if there is a match for the child and it is removed. False if the child could
    138      *         not be found in our list of child hierarchies.
    139      */
    140     public boolean removeChild(ItemHierarchy child) {
    141         final int childIndex = identityIndexOf(mChildren, child);
    142         final int childPosition = getChildPosition(childIndex);
    143         mDirty = true;
    144         if (childIndex != -1) {
    145             final int childCount = child.getCount();
    146             mChildren.remove(childIndex);
    147             child.unregisterObserver(this);
    148             if (childCount > 0) {
    149                 notifyItemRangeRemoved(childPosition, childCount);
    150             }
    151             return true;
    152         }
    153         return false;
    154     }
    155 
    156     /**
    157      * Remove all children from this hierarchy.
    158      */
    159     public void clear() {
    160         if (mChildren.size() == 0) {
    161             return;
    162         }
    163 
    164         final int numRemoved = getCount();
    165 
    166         for (ItemHierarchy item : mChildren) {
    167             item.unregisterObserver(this);
    168         }
    169         mDirty = true;
    170         mChildren.clear();
    171         notifyItemRangeRemoved(0, numRemoved);
    172     }
    173 
    174     @Override
    175     public int getCount() {
    176         updateDataIfNeeded();
    177         return mCount;
    178     }
    179 
    180     @Override
    181     public IItem getItemAt(int position) {
    182         int itemIndex = getItemIndex(position);
    183         ItemHierarchy item = mChildren.get(itemIndex);
    184         int subpos = position - mHierarchyStart.get(itemIndex);
    185         return item.getItemAt(subpos);
    186     }
    187 
    188     @Override
    189     public void onChanged(ItemHierarchy hierarchy) {
    190         // Need to set dirty, because our children may have gotten more items.
    191         mDirty = true;
    192         notifyChanged();
    193     }
    194 
    195     /**
    196      * @return The "Item Position" of the given child, or -1 if the child is not found. If the given
    197      *         child is empty, position of the next visible item is returned.
    198      */
    199     private int getChildPosition(ItemHierarchy child) {
    200         // Check the identity of the child rather than using .equals(), because here we want
    201         // to find the index of the instance itself rather than something that equals to it.
    202         return getChildPosition(identityIndexOf(mChildren, child));
    203     }
    204 
    205     private int getChildPosition(int childIndex) {
    206         updateDataIfNeeded();
    207         if (childIndex != -1) {
    208             int childPos = -1;
    209             int childCount = mChildren.size();
    210             for (int i = childIndex; childPos < 0 && i < childCount; i++) {
    211                 // Find the position of the first visible child after childIndex. This is required
    212                 // when removing the last item from a nested ItemGroup.
    213                 childPos = mHierarchyStart.get(i, -1);
    214             }
    215             if (childPos < 0) {
    216                 // If the last item in a group is being removed, there will be no visible item.
    217                 // In that case return the count instead, since that is where the item would have
    218                 // been if the child is not empty.
    219                 childPos = getCount();
    220             }
    221             return childPos;
    222         }
    223         return -1;
    224     }
    225 
    226     @Override
    227     public void onItemRangeChanged(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
    228         // No need to set dirty because onItemRangeChanged does not include any structural changes.
    229         final int childPosition = getChildPosition(itemHierarchy);
    230         if (childPosition >= 0) {
    231             notifyItemRangeChanged(childPosition + positionStart, itemCount);
    232         } else {
    233             Log.e(TAG, "Unexpected child change " + itemHierarchy);
    234         }
    235     }
    236 
    237     @Override
    238     public void onItemRangeInserted(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
    239         mDirty = true;
    240         final int childPosition = getChildPosition(itemHierarchy);
    241         if (childPosition >= 0) {
    242             notifyItemRangeInserted(childPosition + positionStart, itemCount);
    243         } else {
    244             Log.e(TAG, "Unexpected child insert " + itemHierarchy);
    245         }
    246     }
    247 
    248     @Override
    249     public void onItemRangeMoved(ItemHierarchy itemHierarchy, int fromPosition, int toPosition,
    250             int itemCount) {
    251         mDirty = true;
    252         final int childPosition = getChildPosition(itemHierarchy);
    253         if (childPosition >= 0) {
    254             notifyItemRangeMoved(childPosition + fromPosition, childPosition + toPosition,
    255                     itemCount);
    256         } else {
    257             Log.e(TAG, "Unexpected child move " + itemHierarchy);
    258         }
    259     }
    260 
    261     @Override
    262     public void onItemRangeRemoved(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
    263         mDirty = true;
    264         final int childPosition = getChildPosition(itemHierarchy);
    265         if (childPosition >= 0) {
    266             notifyItemRangeRemoved(childPosition + positionStart, itemCount);
    267         } else {
    268             Log.e(TAG, "Unexpected child remove " + itemHierarchy);
    269         }
    270     }
    271 
    272     @Override
    273     public ItemHierarchy findItemById(int id) {
    274         if (id == getId()) {
    275             return this;
    276         }
    277         for (ItemHierarchy child : mChildren) {
    278             ItemHierarchy childFindItem = child.findItemById(id);
    279             if (childFindItem != null) {
    280                 return childFindItem;
    281             }
    282         }
    283         return null;
    284     }
    285 
    286     /**
    287      * If dirty, this method will recalculate the number of items and mHierarchyStart.
    288      */
    289     private void updateDataIfNeeded() {
    290         if (mDirty) {
    291             mCount = 0;
    292             mHierarchyStart.clear();
    293             for (int itemIndex = 0; itemIndex < mChildren.size(); itemIndex++) {
    294                 ItemHierarchy item = mChildren.get(itemIndex);
    295                 if (item.getCount() > 0) {
    296                     mHierarchyStart.put(itemIndex, mCount);
    297                 }
    298                 mCount += item.getCount();
    299             }
    300             mDirty = false;
    301         }
    302     }
    303 
    304     /**
    305      * Use binary search to locate the item hierarchy a position is contained in.
    306      *
    307      * @return Index of the item hierarchy which is responsible for the item at {@code position}.
    308      */
    309     private int getItemIndex(int position) {
    310         updateDataIfNeeded();
    311         if (position < 0 || position >= mCount) {
    312             throw new IndexOutOfBoundsException("size=" + mCount + "; index=" + position);
    313         }
    314         int result = binarySearch(mHierarchyStart, position);
    315         if (result < 0) {
    316             throw new IllegalStateException("Cannot have item start index < 0");
    317         }
    318         return result;
    319     }
    320 }
    321