| /* |
| * 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.android.browser; |
| |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.database.DataSetObserver; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.webkit.DateSorter; |
| import android.widget.BaseExpandableListAdapter; |
| import android.widget.ExpandableListView; |
| import android.widget.TextView; |
| |
| /** |
| * ExpandableListAdapter which separates data into categories based on date. |
| * Used for History and Downloads. |
| */ |
| public class DateSortedExpandableListAdapter extends BaseExpandableListAdapter { |
| // Array for each of our bins. Each entry represents how many items are |
| // in that bin. |
| private int mItemMap[]; |
| // This is our GroupCount. We will have at most DateSorter.DAY_COUNT |
| // bins, less if the user has no items in one or more bins. |
| private int mNumberOfBins; |
| private Cursor mCursor; |
| private DateSorter mDateSorter; |
| private int mDateIndex; |
| private int mIdIndex; |
| private Context mContext; |
| |
| boolean mDataValid; |
| |
| DataSetObserver mDataSetObserver = new DataSetObserver() { |
| @Override |
| public void onChanged() { |
| mDataValid = true; |
| notifyDataSetChanged(); |
| } |
| |
| @Override |
| public void onInvalidated() { |
| mDataValid = false; |
| notifyDataSetInvalidated(); |
| } |
| }; |
| |
| public DateSortedExpandableListAdapter(Context context, int dateIndex) { |
| mContext = context; |
| mDateSorter = new DateSorter(context); |
| mDateIndex = dateIndex; |
| mDataValid = false; |
| mIdIndex = -1; |
| } |
| |
| /** |
| * Set up the bins for determining which items belong to which groups. |
| */ |
| private void buildMap() { |
| // The cursor is sorted by date |
| // The ItemMap will store the number of items in each bin. |
| int array[] = new int[DateSorter.DAY_COUNT]; |
| // Zero out the array. |
| for (int j = 0; j < DateSorter.DAY_COUNT; j++) { |
| array[j] = 0; |
| } |
| mNumberOfBins = 0; |
| int dateIndex = -1; |
| if (mCursor.moveToFirst() && mCursor.getCount() > 0) { |
| while (!mCursor.isAfterLast()) { |
| long date = getLong(mDateIndex); |
| int index = mDateSorter.getIndex(date); |
| if (index > dateIndex) { |
| mNumberOfBins++; |
| if (index == DateSorter.DAY_COUNT - 1) { |
| // We are already in the last bin, so it will |
| // include all the remaining items |
| array[index] = mCursor.getCount() |
| - mCursor.getPosition(); |
| break; |
| } |
| dateIndex = index; |
| } |
| array[dateIndex]++; |
| mCursor.moveToNext(); |
| } |
| } |
| mItemMap = array; |
| } |
| |
| /** |
| * Get the byte array at cursorIndex from the Cursor. Assumes the Cursor |
| * has already been moved to the correct position. Along with |
| * {@link #getInt} and {@link #getString}, these are provided so the client |
| * does not need to access the Cursor directly |
| * @param cursorIndex Index to query the Cursor. |
| * @return corresponding byte array from the Cursor. |
| */ |
| /* package */ byte[] getBlob(int cursorIndex) { |
| if (!mDataValid) return null; |
| return mCursor.getBlob(cursorIndex); |
| } |
| |
| /* package */ Context getContext() { |
| return mContext; |
| } |
| |
| /** |
| * Get the integer at cursorIndex from the Cursor. Assumes the Cursor has |
| * already been moved to the correct position. Along with |
| * {@link #getBlob} and {@link #getString}, these are provided so the client |
| * does not need to access the Cursor directly |
| * @param cursorIndex Index to query the Cursor. |
| * @return corresponding integer from the Cursor. |
| */ |
| /* package */ int getInt(int cursorIndex) { |
| if (!mDataValid) return 0; |
| return mCursor.getInt(cursorIndex); |
| } |
| |
| /** |
| * Get the long at cursorIndex from the Cursor. Assumes the Cursor has |
| * already been moved to the correct position. |
| */ |
| /* package */ long getLong(int cursorIndex) { |
| if (!mDataValid) return 0; |
| return mCursor.getLong(cursorIndex); |
| } |
| |
| /** |
| * Get the String at cursorIndex from the Cursor. Assumes the Cursor has |
| * already been moved to the correct position. Along with |
| * {@link #getInt} and {@link #getInt}, these are provided so the client |
| * does not need to access the Cursor directly |
| * @param cursorIndex Index to query the Cursor. |
| * @return corresponding String from the Cursor. |
| */ |
| /* package */ String getString(int cursorIndex) { |
| if (!mDataValid) return null; |
| return mCursor.getString(cursorIndex); |
| } |
| |
| /** |
| * Determine which group an item belongs to. |
| * @param childId ID of the child view in question. |
| * @return int Group position of the containing group. |
| /* package */ int groupFromChildId(long childId) { |
| if (!mDataValid) return -1; |
| int group = -1; |
| for (mCursor.moveToFirst(); !mCursor.isAfterLast(); |
| mCursor.moveToNext()) { |
| if (getLong(mIdIndex) == childId) { |
| int bin = mDateSorter.getIndex(getLong(mDateIndex)); |
| // bin is the same as the group if the number of bins is the |
| // same as DateSorter |
| if (DateSorter.DAY_COUNT == mNumberOfBins) { |
| return bin; |
| } |
| // There are some empty bins. Find the corresponding group. |
| group = 0; |
| for (int i = 0; i < bin; i++) { |
| if (mItemMap[i] != 0) { |
| group++; |
| } |
| } |
| break; |
| } |
| } |
| return group; |
| } |
| |
| /** |
| * Translates from a group position in the ExpandableList to a bin. This is |
| * necessary because some groups have no history items, so we do not include |
| * those in the ExpandableList. |
| * @param groupPosition Position in the ExpandableList's set of groups |
| * @return The corresponding bin that holds that group. |
| */ |
| private int groupPositionToBin(int groupPosition) { |
| if (!mDataValid) return -1; |
| if (groupPosition < 0 || groupPosition >= DateSorter.DAY_COUNT) { |
| throw new AssertionError("group position out of range"); |
| } |
| if (DateSorter.DAY_COUNT == mNumberOfBins || 0 == mNumberOfBins) { |
| // In the first case, we have exactly the same number of bins |
| // as our maximum possible, so there is no need to do a |
| // conversion |
| // The second statement is in case this method gets called when |
| // the array is empty, in which case the provided groupPosition |
| // will do fine. |
| return groupPosition; |
| } |
| int arrayPosition = -1; |
| while (groupPosition > -1) { |
| arrayPosition++; |
| if (mItemMap[arrayPosition] != 0) { |
| groupPosition--; |
| } |
| } |
| return arrayPosition; |
| } |
| |
| /** |
| * Move the cursor to the position indicated. |
| * @param packedPosition Position in packed position representation. |
| * @return True on success, false otherwise. |
| */ |
| boolean moveCursorToPackedChildPosition(long packedPosition) { |
| if (ExpandableListView.getPackedPositionType(packedPosition) != |
| ExpandableListView.PACKED_POSITION_TYPE_CHILD) { |
| return false; |
| } |
| int groupPosition = ExpandableListView.getPackedPositionGroup( |
| packedPosition); |
| int childPosition = ExpandableListView.getPackedPositionChild( |
| packedPosition); |
| return moveCursorToChildPosition(groupPosition, childPosition); |
| } |
| |
| /** |
| * Move the cursor the the position indicated. |
| * @param groupPosition Index of the group containing the desired item. |
| * @param childPosition Index of the item within the specified group. |
| * @return boolean False if the cursor is closed, so the Cursor was not |
| * moved. True on success. |
| */ |
| /* package */ boolean moveCursorToChildPosition(int groupPosition, |
| int childPosition) { |
| if (!mDataValid || mCursor.isClosed()) { |
| return false; |
| } |
| groupPosition = groupPositionToBin(groupPosition); |
| int index = childPosition; |
| for (int i = 0; i < groupPosition; i++) { |
| index += mItemMap[i]; |
| } |
| return mCursor.moveToPosition(index); |
| } |
| |
| public void changeCursor(Cursor cursor) { |
| if (cursor == mCursor) { |
| return; |
| } |
| if (mCursor != null) { |
| mCursor.unregisterDataSetObserver(mDataSetObserver); |
| mCursor.close(); |
| } |
| mCursor = cursor; |
| if (cursor != null) { |
| cursor.registerDataSetObserver(mDataSetObserver); |
| mIdIndex = cursor.getColumnIndexOrThrow("_id"); |
| mDataValid = true; |
| buildMap(); |
| // notify the observers about the new cursor |
| notifyDataSetChanged(); |
| } else { |
| mIdIndex = -1; |
| mDataValid = false; |
| // notify the observers about the lack of a data set |
| notifyDataSetInvalidated(); |
| } |
| } |
| |
| @Override |
| public View getGroupView(int groupPosition, boolean isExpanded, |
| View convertView, ViewGroup parent) { |
| if (!mDataValid) throw new IllegalStateException("Data is not valid"); |
| TextView item; |
| if (null == convertView || !(convertView instanceof TextView)) { |
| LayoutInflater factory = LayoutInflater.from(mContext); |
| item = (TextView) factory.inflate(R.layout.history_header, null); |
| } else { |
| item = (TextView) convertView; |
| } |
| String label = mDateSorter.getLabel(groupPositionToBin(groupPosition)); |
| item.setText(label); |
| return item; |
| } |
| |
| @Override |
| public View getChildView(int groupPosition, int childPosition, |
| boolean isLastChild, View convertView, ViewGroup parent) { |
| if (!mDataValid) throw new IllegalStateException("Data is not valid"); |
| return null; |
| } |
| |
| @Override |
| public boolean areAllItemsEnabled() { |
| return true; |
| } |
| |
| @Override |
| public boolean isChildSelectable(int groupPosition, int childPosition) { |
| return true; |
| } |
| |
| @Override |
| public int getGroupCount() { |
| if (!mDataValid) return 0; |
| return mNumberOfBins; |
| } |
| |
| @Override |
| public int getChildrenCount(int groupPosition) { |
| if (!mDataValid) return 0; |
| return mItemMap[groupPositionToBin(groupPosition)]; |
| } |
| |
| @Override |
| public Object getGroup(int groupPosition) { |
| return null; |
| } |
| |
| @Override |
| public Object getChild(int groupPosition, int childPosition) { |
| return null; |
| } |
| |
| @Override |
| public long getGroupId(int groupPosition) { |
| if (!mDataValid) return 0; |
| return groupPosition; |
| } |
| |
| @Override |
| public long getChildId(int groupPosition, int childPosition) { |
| if (!mDataValid) return 0; |
| if (moveCursorToChildPosition(groupPosition, childPosition)) { |
| return getLong(mIdIndex); |
| } |
| return 0; |
| } |
| |
| @Override |
| public boolean hasStableIds() { |
| return true; |
| } |
| |
| @Override |
| public void onGroupExpanded(int groupPosition) { |
| } |
| |
| @Override |
| public void onGroupCollapsed(int groupPosition) { |
| } |
| |
| @Override |
| public long getCombinedChildId(long groupId, long childId) { |
| if (!mDataValid) return 0; |
| return childId; |
| } |
| |
| @Override |
| public long getCombinedGroupId(long groupId) { |
| if (!mDataValid) return 0; |
| return groupId; |
| } |
| |
| @Override |
| public boolean isEmpty() { |
| return !mDataValid || mCursor == null || mCursor.isClosed() || mCursor.getCount() == 0; |
| } |
| } |