/* Copyright © 2013-2014, Silent Circle, LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Any redistribution, use, or modification is done solely for personal benefit and not for any commercial purpose or for monetary gain * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Silent Circle nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SILENT CIRCLE, LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * This implementation is edited version of original Android sources. */ /* * Copyright (C) 2010 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.silentcircle.contacts.utils; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.database.DataSetObserver; import android.os.Handler; import android.util.Log; import android.util.SparseIntArray; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; /** * Maintains a list that groups adjacent items sharing the same value of * a "group-by" field. The list has three types of elements: stand-alone, group header and group * child. Groups are collapsible and collapsed by default. */ public abstract class GroupingListAdapter extends BaseAdapter { private static final String TAG ="GroupingListAdapter"; private static final int GROUP_METADATA_ARRAY_INITIAL_SIZE = 16; private static final int GROUP_METADATA_ARRAY_INCREMENT = 128; private static final long GROUP_OFFSET_MASK = 0x00000000FFFFFFFFL; private static final long GROUP_SIZE_MASK = 0x7FFFFFFF00000000L; private static final long EXPANDED_GROUP_MASK = 0x8000000000000000L; public static final int ITEM_TYPE_STANDALONE = 0; public static final int ITEM_TYPE_GROUP_HEADER = 1; public static final int ITEM_TYPE_IN_GROUP = 2; /** * Information about a specific list item: is it a group, if so is it expanded. * Otherwise, is it a stand-alone item or a group member. */ protected static class PositionMetadata { int itemType; boolean isExpanded; int cursorPosition; int childCount; private int groupPosition; private int listPosition = -1; } private Context mContext; private Cursor mCursor; /** * Count of list items. */ private int mCount; private int mRowIdColumnIndex; /** * Count of groups in the list. */ private int mGroupCount; /** * Information about where these groups are located in the list, how large they are * and whether they are expanded. */ private long[] mGroupMetadata; private SparseIntArray mPositionCache = new SparseIntArray(); private int mLastCachedListPosition; private int mLastCachedCursorPosition; private int mLastCachedGroup; /** * A reusable temporary instance of PositionMetadata */ private PositionMetadata mPositionMetadata = new PositionMetadata(); protected ContentObserver mChangeObserver = new ContentObserver(new Handler()) { @Override public boolean deliverSelfNotifications() { return true; } @Override public void onChange(boolean selfChange) { onContentChanged(); } }; protected DataSetObserver mDataSetObserver = new DataSetObserver() { @Override public void onChanged() { notifyDataSetChanged(); } @Override public void onInvalidated() { notifyDataSetInvalidated(); } }; public GroupingListAdapter(Context context) { mContext = context; resetCache(); } /** * Finds all groups of adjacent items in the cursor and calls {@link #addGroup} for * each of them. */ protected abstract void addGroups(Cursor cursor); protected abstract View newStandAloneView(Context context, ViewGroup parent); protected abstract void bindStandAloneView(View view, Context context, Cursor cursor, int listPos); protected abstract View newGroupView(Context context, ViewGroup parent); protected abstract void bindGroupView(View view, Context context, Cursor cursor, int groupSize, boolean expanded, int listPos); protected abstract View newChildView(Context context, ViewGroup parent); protected abstract void bindChildView(View view, Context context, Cursor cursor, int listPos); /** * Cache should be reset whenever the cursor changes or groups are expanded or collapsed. */ private void resetCache() { mCount = -1; mLastCachedListPosition = -1; mLastCachedCursorPosition = -1; mLastCachedGroup = -1; mPositionMetadata.listPosition = -1; mPositionCache.clear(); } protected void onContentChanged() { } public void changeCursor(Cursor cursor) { if (cursor == mCursor) { return; } if (mCursor != null) { mCursor.unregisterContentObserver(mChangeObserver); mCursor.unregisterDataSetObserver(mDataSetObserver); mCursor.close(); } mCursor = cursor; resetCache(); findGroups(); if (cursor != null) { cursor.registerContentObserver(mChangeObserver); cursor.registerDataSetObserver(mDataSetObserver); mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id"); notifyDataSetChanged(); } else { // notify the observers about the lack of a data set notifyDataSetInvalidated(); } } public Cursor getCursor() { return mCursor; } /** * Scans over the entire cursor looking for duplicate phone numbers that need * to be collapsed. */ private void findGroups() { mGroupCount = 0; mGroupMetadata = new long[GROUP_METADATA_ARRAY_INITIAL_SIZE]; if (mCursor == null) { return; } addGroups(mCursor); } /** * Records information about grouping in the list. Should be called by the overridden * {@link #addGroups} method. */ protected void addGroup(int cursorPosition, int size, boolean expanded) { if (mGroupCount >= mGroupMetadata.length) { int newSize = idealLongArraySize(mGroupMetadata.length + GROUP_METADATA_ARRAY_INCREMENT); long[] array = new long[newSize]; System.arraycopy(mGroupMetadata, 0, array, 0, mGroupCount); mGroupMetadata = array; } long metadata = ((long)size << 32) | cursorPosition; if (expanded) { metadata |= EXPANDED_GROUP_MASK; } mGroupMetadata[mGroupCount++] = metadata; } // Copy/paste from ArrayUtils private int idealLongArraySize(int need) { return idealByteArraySize(need * 8) / 8; } // Copy/paste from ArrayUtils private int idealByteArraySize(int need) { for (int i = 4; i < 32; i++) if (need <= (1 << i) - 12) return (1 << i) - 12; return need; } public int getCount() { if (mCursor == null) { return 0; } if (mCount != -1) { return mCount; } int cursorPosition = 0; int count = 0; for (int i = 0; i < mGroupCount; i++) { long metadata = mGroupMetadata[i]; int offset = (int)(metadata & GROUP_OFFSET_MASK); boolean expanded = (metadata & EXPANDED_GROUP_MASK) != 0; int size = (int)((metadata & GROUP_SIZE_MASK) >> 32); count += (offset - cursorPosition); if (expanded) { count += size + 1; } else { count++; } cursorPosition = offset + size; } mCount = count + mCursor.getCount() - cursorPosition; return mCount; } /** * Figures out whether the item at the specified position represents a * stand-alone element, a group or a group child. Also computes the * corresponding cursor position. */ public void obtainPositionMetadata(PositionMetadata metadata, int position) { // If the description object already contains requested information, just return if (metadata.listPosition == position) { return; } int listPosition = 0; int cursorPosition = 0; int firstGroupToCheck = 0; // Check cache for the supplied position. What we are looking for is // the group descriptor immediately preceding the supplied position. // Once we have that, we will be able to tell whether the position // is the header of the group, a member of the group or a standalone item. if (mLastCachedListPosition != -1) { if (position <= mLastCachedListPosition) { // Have SparceIntArray do a binary search for us. int index = mPositionCache.indexOfKey(position); // If we get back a positive number, the position corresponds to // a group header. if (index < 0) { // We had a cache miss, but we did obtain valuable information anyway. // The negative number will allow us to compute the location of // the group header immediately preceding the supplied position. index = ~index - 1; if (index >= mPositionCache.size()) { index--; } } // A non-negative index gives us the position of the group header // corresponding or preceding the position, so we can // search for the group information at the supplied position // starting with the cached group we just found if (index >= 0) { listPosition = mPositionCache.keyAt(index); firstGroupToCheck = mPositionCache.valueAt(index); long descriptor = mGroupMetadata[firstGroupToCheck]; cursorPosition = (int)(descriptor & GROUP_OFFSET_MASK); } } else { // If we haven't examined groups beyond the supplied position, // we will start where we left off previously firstGroupToCheck = mLastCachedGroup; listPosition = mLastCachedListPosition; cursorPosition = mLastCachedCursorPosition; } } for (int i = firstGroupToCheck; i < mGroupCount; i++) { long group = mGroupMetadata[i]; int offset = (int)(group & GROUP_OFFSET_MASK); // Move pointers to the beginning of the group listPosition += (offset - cursorPosition); cursorPosition = offset; if (i > mLastCachedGroup) { mPositionCache.append(listPosition, i); mLastCachedListPosition = listPosition; mLastCachedCursorPosition = cursorPosition; mLastCachedGroup = i; } // Now we have several possibilities: // A) The requested position precedes the group if (position < listPosition) { metadata.itemType = ITEM_TYPE_STANDALONE; metadata.cursorPosition = cursorPosition - (listPosition - position); return; } boolean expanded = (group & EXPANDED_GROUP_MASK) != 0; int size = (int) ((group & GROUP_SIZE_MASK) >> 32); // B) The requested position is a group header if (position == listPosition) { metadata.itemType = ITEM_TYPE_GROUP_HEADER; metadata.groupPosition = i; metadata.isExpanded = expanded; metadata.childCount = size; metadata.cursorPosition = offset; return; } if (expanded) { // C) The requested position is an element in the expanded group if (position < listPosition + size + 1) { metadata.itemType = ITEM_TYPE_IN_GROUP; metadata.cursorPosition = cursorPosition + (position - listPosition) - 1; return; } // D) The element is past the expanded group listPosition += size + 1; } else { // E) The element is past the collapsed group listPosition++; } // Move cursor past the group cursorPosition += size; } // The required item is past the last group metadata.itemType = ITEM_TYPE_STANDALONE; metadata.cursorPosition = cursorPosition + (position - listPosition); } /** * Returns true if the specified position in the list corresponds to a * group header. */ public boolean isGroupHeader(int position) { obtainPositionMetadata(mPositionMetadata, position); return mPositionMetadata.itemType == ITEM_TYPE_GROUP_HEADER; } /** * Given a position of a groups header in the list, returns the size of * the corresponding group. */ public int getGroupSize(int position) { obtainPositionMetadata(mPositionMetadata, position); return mPositionMetadata.childCount; } /** * Mark group as expanded if it is collapsed and vice versa. */ public void toggleGroup(int position) { obtainPositionMetadata(mPositionMetadata, position); if (mPositionMetadata.itemType != ITEM_TYPE_GROUP_HEADER) { throw new IllegalArgumentException("Not a group at position " + position); } if (mPositionMetadata.isExpanded) { mGroupMetadata[mPositionMetadata.groupPosition] &= ~EXPANDED_GROUP_MASK; } else { mGroupMetadata[mPositionMetadata.groupPosition] |= EXPANDED_GROUP_MASK; } resetCache(); notifyDataSetChanged(); } @Override public int getViewTypeCount() { return 3; } @Override public int getItemViewType(int position) { obtainPositionMetadata(mPositionMetadata, position); return mPositionMetadata.itemType; } public Object getItem(int position) { if (mCursor == null) { return null; } obtainPositionMetadata(mPositionMetadata, position); if (mCursor.moveToPosition(mPositionMetadata.cursorPosition)) { return mCursor; } else { return null; } } public long getItemId(int position) { Object item = getItem(position); if (item != null) { return mCursor.getLong(mRowIdColumnIndex); } else { return -1; } } public View getView(int position, View convertView, ViewGroup parent) { obtainPositionMetadata(mPositionMetadata, position); View view = convertView; if (view == null) { switch (mPositionMetadata.itemType) { case ITEM_TYPE_STANDALONE: view = newStandAloneView(mContext, parent); break; case ITEM_TYPE_GROUP_HEADER: view = newGroupView(mContext, parent); break; case ITEM_TYPE_IN_GROUP: view = newChildView(mContext, parent); break; } } mCursor.moveToPosition(mPositionMetadata.cursorPosition); switch (mPositionMetadata.itemType) { case ITEM_TYPE_STANDALONE: bindStandAloneView(view, mContext, mCursor, position); break; case ITEM_TYPE_GROUP_HEADER: bindGroupView(view, mContext, mCursor, mPositionMetadata.childCount, mPositionMetadata.isExpanded, position); break; case ITEM_TYPE_IN_GROUP: bindChildView(view, mContext, mCursor, position); break; } return view; } }