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