/* * 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.providers.downloads.ui; import android.app.DownloadManager; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.database.DataSetObserver; import android.os.Handler; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.webkit.DateSorter; import android.widget.ExpandableListAdapter; import android.widget.ExpandableListView; import android.widget.TextView; import java.util.Vector; /** * ExpandableListAdapter which separates data into categories based on date. Copied from * packages/apps/Browser. */ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { // 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 Vector mObservers; private Cursor mCursor; private DateSorter mDateSorter; private int mDateIndex; private int mIdIndex; private Context mContext; private class ChangeObserver extends ContentObserver { public ChangeObserver() { super(new Handler()); } @Override public boolean deliverSelfNotifications() { return true; } @Override public void onChange(boolean selfChange) { refreshData(); } } private class MyDataSetObserver extends DataSetObserver { @Override public void onChanged() { buildMap(); for (DataSetObserver o : mObservers) { o.onChanged(); } } } public DateSortedExpandableListAdapter(Context context, Cursor cursor, int dateIndex) { mContext = context; mDateSorter = new DateSorter(context); mObservers = new Vector(); mCursor = cursor; mIdIndex = cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID); cursor.registerContentObserver(new ChangeObserver()); cursor.registerDataSetObserver(new MyDataSetObserver()); mDateIndex = dateIndex; buildMap(); } /** * 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) { 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) { 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) { 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) { 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) { 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 (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 (mCursor.isClosed()) return false; groupPosition = groupPositionToBin(groupPosition); int index = childPosition; for (int i = 0; i < groupPosition; i++) { index += mItemMap[i]; } return mCursor.moveToPosition(index); } /* package */ void refreshData() { if (mCursor.isClosed()) { return; } mCursor.requery(); } public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { TextView item; if (null == convertView || !(convertView instanceof TextView)) { LayoutInflater factory = LayoutInflater.from(mContext); item = (TextView) factory.inflate(R.layout.list_group_header, parent, false); } else { item = (TextView) convertView; } String label = mDateSorter.getLabel(groupPositionToBin(groupPosition)); item.setText(label); return item; } public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { return null; } public boolean areAllItemsEnabled() { return true; } public boolean isChildSelectable(int groupPosition, int childPosition) { return true; } public int getGroupCount() { return mNumberOfBins; } public int getChildrenCount(int groupPosition) { return mItemMap[groupPositionToBin(groupPosition)]; } public Object getGroup(int groupPosition) { return null; } public Object getChild(int groupPosition, int childPosition) { return null; } public long getGroupId(int groupPosition) { return groupPosition; } public long getChildId(int groupPosition, int childPosition) { if (moveCursorToChildPosition(groupPosition, childPosition)) { return getLong(mIdIndex); } return 0; } public boolean hasStableIds() { return true; } public void registerDataSetObserver(DataSetObserver observer) { mObservers.add(observer); } public void unregisterDataSetObserver(DataSetObserver observer) { mObservers.remove(observer); } public void onGroupExpanded(int groupPosition) { } public void onGroupCollapsed(int groupPosition) { } public long getCombinedChildId(long groupId, long childId) { return childId; } public long getCombinedGroupId(long groupId) { return groupId; } public boolean isEmpty() { return mCursor.isClosed() || mCursor.getCount() == 0; } }