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
17package com.android.setupwizardlib.items;
18
19import android.content.Context;
20import android.util.AttributeSet;
21import android.util.Log;
22import android.util.SparseIntArray;
23
24import java.util.ArrayList;
25import java.util.List;
26
27public 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