blob: 246469f10cbcbf1d58ac4a40e7fdaff3e883cefb [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.setupwizardlib.items;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseIntArray;
import java.util.ArrayList;
import java.util.List;
public class ItemGroup extends AbstractItemHierarchy
implements ItemInflater.ItemParent, ItemHierarchy.Observer {
/* static section */
private static final String TAG = "ItemGroup";
/**
* Binary search for the closest value that's smaller than or equal to {@code value}, and return
* the corresponding key.
*/
private static int binarySearch(SparseIntArray array, int value) {
final int size = array.size();
int lo = 0;
int hi = size - 1;
while (lo <= hi) {
final int mid = (lo + hi) >>> 1;
final int midVal = array.valueAt(mid);
if (midVal < value) {
lo = mid + 1;
} else if (midVal > value) {
hi = mid - 1;
} else {
return array.keyAt(mid); // value found
}
}
// Value not found. Return the last item before our search range, which is the closest
// value smaller than the value we are looking for.
return array.keyAt(lo - 1);
}
/**
* Same as {@link List#indexOf(Object)}, but using identity comparison rather than {@link
* Object#equals(Object)}.
*/
private static <T> int identityIndexOf(List<T> list, T object) {
final int count = list.size();
for (int i = 0; i < count; i++) {
if (list.get(i) == object) {
return i;
}
}
return -1;
}
/* non-static section */
private final List<ItemHierarchy> children = new ArrayList<>();
/**
* A mapping from the index of an item hierarchy in children, to the first position in which the
* corresponding child hierarchy represents. For example:
*
* <p>ItemHierarchy Item Item Position Index
*
* <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
*
* <p>1 [ <Empty Item Hierarchy> ]
*
* <p>2 [ Use cellular data ] 5
*
* <p>3 [ Don't connect ] 6
*
* <p>For this example of Wi-Fi screen, the following mapping will be produced: [ 0 -> 0 | 2 -> 5
* | 3 -> 6 ]
*
* <p>Also note how ItemHierarchy index 1 is not present in the map, because it is empty.
*
* <p>ItemGroup uses this map to look for which ItemHierarchy an item at a given position belongs
* to.
*/
private final SparseIntArray hierarchyStart = new SparseIntArray();
private int count = 0;
private boolean dirty = false;
public ItemGroup() {
super();
}
public ItemGroup(Context context, AttributeSet attrs) {
// Constructor for XML inflation
super(context, attrs);
}
/** Add a child hierarchy to this item group. */
@Override
public void addChild(ItemHierarchy child) {
dirty = true;
children.add(child);
child.registerObserver(this);
final int count = child.getCount();
if (count > 0) {
notifyItemRangeInserted(getChildPosition(child), count);
}
}
/**
* Remove a previously added child from this item group.
*
* @return True if there is a match for the child and it is removed. False if the child could not
* be found in our list of child hierarchies.
*/
public boolean removeChild(ItemHierarchy child) {
final int childIndex = identityIndexOf(children, child);
final int childPosition = getChildPosition(childIndex);
dirty = true;
if (childIndex != -1) {
final int childCount = child.getCount();
children.remove(childIndex);
child.unregisterObserver(this);
if (childCount > 0) {
notifyItemRangeRemoved(childPosition, childCount);
}
return true;
}
return false;
}
/** Remove all children from this hierarchy. */
public void clear() {
if (children.isEmpty()) {
return;
}
final int numRemoved = getCount();
for (ItemHierarchy item : children) {
item.unregisterObserver(this);
}
dirty = true;
children.clear();
notifyItemRangeRemoved(0, numRemoved);
}
@Override
public int getCount() {
updateDataIfNeeded();
return count;
}
@Override
public IItem getItemAt(int position) {
int itemIndex = getItemIndex(position);
ItemHierarchy item = children.get(itemIndex);
int subpos = position - hierarchyStart.get(itemIndex);
return item.getItemAt(subpos);
}
@Override
public void onChanged(ItemHierarchy hierarchy) {
// Need to set dirty, because our children may have gotten more items.
dirty = true;
notifyChanged();
}
/**
* @return The "Item Position" of the given child, or -1 if the child is not found. If the given
* child is empty, position of the next visible item is returned.
*/
private int getChildPosition(ItemHierarchy child) {
// Check the identity of the child rather than using .equals(), because here we want
// to find the index of the instance itself rather than something that equals to it.
return getChildPosition(identityIndexOf(children, child));
}
private int getChildPosition(int childIndex) {
updateDataIfNeeded();
if (childIndex != -1) {
int childPos = -1;
int childCount = children.size();
for (int i = childIndex; childPos < 0 && i < childCount; i++) {
// Find the position of the first visible child after childIndex. This is required
// when removing the last item from a nested ItemGroup.
childPos = hierarchyStart.get(i, -1);
}
if (childPos < 0) {
// If the last item in a group is being removed, there will be no visible item.
// In that case return the count instead, since that is where the item would have
// been if the child is not empty.
childPos = getCount();
}
return childPos;
}
return -1;
}
@Override
public void onItemRangeChanged(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
// No need to set dirty because onItemRangeChanged does not include any structural changes.
final int childPosition = getChildPosition(itemHierarchy);
if (childPosition >= 0) {
notifyItemRangeChanged(childPosition + positionStart, itemCount);
} else {
Log.e(TAG, "Unexpected child change " + itemHierarchy);
}
}
@Override
public void onItemRangeInserted(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
dirty = true;
final int childPosition = getChildPosition(itemHierarchy);
if (childPosition >= 0) {
notifyItemRangeInserted(childPosition + positionStart, itemCount);
} else {
Log.e(TAG, "Unexpected child insert " + itemHierarchy);
}
}
@Override
public void onItemRangeMoved(
ItemHierarchy itemHierarchy, int fromPosition, int toPosition, int itemCount) {
dirty = true;
final int childPosition = getChildPosition(itemHierarchy);
if (childPosition >= 0) {
notifyItemRangeMoved(childPosition + fromPosition, childPosition + toPosition, itemCount);
} else {
Log.e(TAG, "Unexpected child move " + itemHierarchy);
}
}
@Override
public void onItemRangeRemoved(ItemHierarchy itemHierarchy, int positionStart, int itemCount) {
dirty = true;
final int childPosition = getChildPosition(itemHierarchy);
if (childPosition >= 0) {
notifyItemRangeRemoved(childPosition + positionStart, itemCount);
} else {
Log.e(TAG, "Unexpected child remove " + itemHierarchy);
}
}
@Override
public ItemHierarchy findItemById(int id) {
if (id == getId()) {
return this;
}
for (ItemHierarchy child : children) {
ItemHierarchy childFindItem = child.findItemById(id);
if (childFindItem != null) {
return childFindItem;
}
}
return null;
}
/** If dirty, this method will recalculate the number of items and hierarchyStart. */
private void updateDataIfNeeded() {
if (dirty) {
count = 0;
hierarchyStart.clear();
for (int itemIndex = 0; itemIndex < children.size(); itemIndex++) {
ItemHierarchy item = children.get(itemIndex);
if (item.getCount() > 0) {
hierarchyStart.put(itemIndex, count);
}
count += item.getCount();
}
dirty = false;
}
}
/**
* Use binary search to locate the item hierarchy a position is contained in.
*
* @return Index of the item hierarchy which is responsible for the item at {@code position}.
*/
private int getItemIndex(int position) {
updateDataIfNeeded();
if (position < 0 || position >= count) {
throw new IndexOutOfBoundsException("size=" + count + "; index=" + position);
}
int result = binarySearch(hierarchyStart, position);
if (result < 0) {
throw new IllegalStateException("Cannot have item start index < 0");
}
return result;
}
}