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.SparseIntArray; 22 23 import java.util.ArrayList; 24 import java.util.List; 25 26 public class ItemGroup extends AbstractItemHierarchy implements ItemInflater.ItemParent, 27 ItemHierarchy.Observer { 28 29 /* static section */ 30 31 /** 32 * Binary search for the closest value that's smaller than or equal to {@code value}, and 33 * return the corresponding key. 34 */ 35 private static int binarySearch(SparseIntArray array, int value) { 36 final int size = array.size(); 37 int lo = 0; 38 int hi = size - 1; 39 40 while (lo <= hi) { 41 final int mid = (lo + hi) >>> 1; 42 final int midVal = array.valueAt(mid); 43 44 if (midVal < value) { 45 lo = mid + 1; 46 } else if (midVal > value) { 47 hi = mid - 1; 48 } else { 49 return array.keyAt(mid); // value found 50 } 51 } 52 // Value not found. Return the last item before our search range, which is the closest 53 // value smaller than the value we are looking for. 54 return array.keyAt(lo - 1); 55 } 56 57 /* non-static section */ 58 59 private List<ItemHierarchy> mChildren = new ArrayList<>(); 60 61 /** 62 * A mapping from the index of an item hierarchy in mChildren, to the first position in which 63 * the corresponding child hierarchy represents. For example: 64 * 65 * ItemHierarchy Item Item Position 66 * Index 67 * 68 * 0 [ Wi-Fi AP 1 ] 0 69 * | Wi-Fi AP 2 | 1 70 * | Wi-Fi AP 3 | 2 71 * | Wi-Fi AP 4 | 3 72 * [ Wi-Fi AP 5 ] 4 73 * 74 * 1 [ <Empty Item Hierarchy> ] 75 * 76 * 2 [ Use cellular data ] 5 77 * 78 * 3 [ Don't connect ] 6 79 * 80 * For this example of Wi-Fi screen, the following mapping will be produced: 81 * [ 0 -> 0 | 2 -> 5 | 3 -> 6 ] 82 * 83 * Also note how ItemHierarchy index 1 is not present in the map, because it is empty. 84 * 85 * ItemGroup uses this map to look for which ItemHierarchy an item at a given position belongs 86 * to. 87 */ 88 private SparseIntArray mHierarchyStart = new SparseIntArray(); 89 90 private int mCount = 0; 91 private boolean mDirty = false; 92 93 public ItemGroup() { 94 super(); 95 } 96 97 public ItemGroup(Context context, AttributeSet attrs) { 98 // Constructor for XML inflation 99 super(context, attrs); 100 } 101 102 /** 103 * Add a child hierarchy to this item group. 104 */ 105 @Override 106 public void addChild(ItemHierarchy child) { 107 mChildren.add(child); 108 child.registerObserver(this); 109 onHierarchyChanged(); 110 } 111 112 /** 113 * Remove a previously added child from this item group. 114 * 115 * @return True if there is a match for the child and it is removed. False if the child could 116 * not be found in our list of child hierarchies. 117 */ 118 public boolean removeChild(ItemHierarchy child) { 119 if (mChildren.remove(child)) { 120 child.unregisterObserver(this); 121 onHierarchyChanged(); 122 return true; 123 } 124 return false; 125 } 126 127 /** 128 * Remove all children from this hierarchy. 129 */ 130 public void clear() { 131 if (mChildren.size() == 0) { 132 return; 133 } 134 135 for (ItemHierarchy item : mChildren) { 136 item.unregisterObserver(this); 137 } 138 mChildren.clear(); 139 onHierarchyChanged(); 140 } 141 142 @Override 143 public int getCount() { 144 updateDataIfNeeded(); 145 return mCount; 146 } 147 148 @Override 149 public IItem getItemAt(int position) { 150 int itemIndex = getItemIndex(position); 151 ItemHierarchy item = mChildren.get(itemIndex); 152 int subpos = position - mHierarchyStart.get(itemIndex); 153 return item.getItemAt(subpos); 154 } 155 156 @Override 157 public void onChanged(ItemHierarchy hierarchy) { 158 // Need to set dirty, because our children may have gotten more items. 159 mDirty = true; 160 notifyChanged(); 161 } 162 163 private void onHierarchyChanged() { 164 onChanged(null); 165 } 166 167 @Override 168 public ItemHierarchy findItemById(int id) { 169 if (id == getId()) { 170 return this; 171 } 172 for (ItemHierarchy child : mChildren) { 173 ItemHierarchy childFindItem = child.findItemById(id); 174 if (childFindItem != null) { 175 return childFindItem; 176 } 177 } 178 return null; 179 } 180 181 /** 182 * If dirty, this method will recalculate the number of items and mHierarchyStart. 183 */ 184 private void updateDataIfNeeded() { 185 if (mDirty) { 186 mCount = 0; 187 mHierarchyStart.clear(); 188 for (int itemIndex = 0; itemIndex < mChildren.size(); itemIndex++) { 189 ItemHierarchy item = mChildren.get(itemIndex); 190 if (item.getCount() > 0) { 191 mHierarchyStart.put(itemIndex, mCount); 192 } 193 mCount += item.getCount(); 194 } 195 mDirty = false; 196 } 197 } 198 199 /** 200 * Use binary search to locate the item hierarchy a position is contained in. 201 * 202 * @return Index of the item hierarchy which is responsible for the item at {@code position}. 203 */ 204 private int getItemIndex(int position) { 205 updateDataIfNeeded(); 206 if (position < 0 || position >= mCount) { 207 throw new IndexOutOfBoundsException("size=" + mCount + "; index=" + position); 208 } 209 int result = binarySearch(mHierarchyStart, position); 210 if (result < 0) { 211 throw new IllegalStateException("Cannot have item start index < 0"); 212 } 213 return result; 214 } 215 } 216