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