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