DateSortedExpandableListAdapter.java revision 2483745674f95f60b8c3b8c9e817f2df1776a0b5
1/*
2 * Copyright (C) 2010 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.browser;
18
19import android.content.Context;
20import android.database.ContentObserver;
21import android.database.Cursor;
22import android.database.DataSetObserver;
23import android.os.Handler;
24import android.provider.BaseColumns;
25import android.view.LayoutInflater;
26import android.view.View;
27import android.view.ViewGroup;
28import android.webkit.DateSorter;
29import android.widget.ExpandableListAdapter;
30import android.widget.ExpandableListView;
31import android.widget.TextView;
32
33import java.util.Vector;
34
35/**
36 * ExpandableListAdapter which separates data into categories based on date.
37 * Used for History and Downloads.
38 */
39public class DateSortedExpandableListAdapter implements ExpandableListAdapter {
40    // Array for each of our bins.  Each entry represents how many items are
41    // in that bin.
42    private int mItemMap[];
43    // This is our GroupCount.  We will have at most DateSorter.DAY_COUNT
44    // bins, less if the user has no items in one or more bins.
45    private int mNumberOfBins;
46    private Vector<DataSetObserver> mObservers;
47    private Cursor mCursor;
48    private DateSorter mDateSorter;
49    private int mDateIndex;
50    private int mIdIndex;
51    private Context mContext;
52
53    private class ChangeObserver extends ContentObserver {
54        public ChangeObserver() {
55            super(new Handler());
56        }
57
58        @Override
59        public boolean deliverSelfNotifications() {
60            return true;
61        }
62
63        @Override
64        public void onChange(boolean selfChange) {
65            refreshData();
66        }
67    }
68
69    public DateSortedExpandableListAdapter(Context context, Cursor cursor,
70            int dateIndex) {
71        mContext = context;
72        mDateSorter = new DateSorter(context);
73        mObservers = new Vector<DataSetObserver>();
74        mCursor = cursor;
75        mIdIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID);
76        cursor.registerContentObserver(new ChangeObserver());
77        mDateIndex = dateIndex;
78        buildMap();
79    }
80
81    /**
82     * Set up the bins for determining which items belong to which groups.
83     */
84    private void buildMap() {
85        // The cursor is sorted by date
86        // The ItemMap will store the number of items in each bin.
87        int array[] = new int[DateSorter.DAY_COUNT];
88        // Zero out the array.
89        for (int j = 0; j < DateSorter.DAY_COUNT; j++) {
90            array[j] = 0;
91        }
92        mNumberOfBins = 0;
93        int dateIndex = -1;
94        if (mCursor.moveToFirst() && mCursor.getCount() > 0) {
95            while (!mCursor.isAfterLast()) {
96                long date = getLong(mDateIndex);
97                int index = mDateSorter.getIndex(date);
98                if (index > dateIndex) {
99                    mNumberOfBins++;
100                    if (index == DateSorter.DAY_COUNT - 1) {
101                        // We are already in the last bin, so it will
102                        // include all the remaining items
103                        array[index] = mCursor.getCount()
104                                - mCursor.getPosition();
105                        break;
106                    }
107                    dateIndex = index;
108                }
109                array[dateIndex]++;
110                mCursor.moveToNext();
111            }
112        }
113        mItemMap = array;
114    }
115
116    /**
117     * Get the byte array at cursorIndex from the Cursor.  Assumes the Cursor
118     * has already been moved to the correct position.  Along with
119     * {@link #getInt} and {@link #getString}, these are provided so the client
120     * does not need to access the Cursor directly
121     * @param cursorIndex Index to query the Cursor.
122     * @return corresponding byte array from the Cursor.
123     */
124    /* package */ byte[] getBlob(int cursorIndex) {
125        return mCursor.getBlob(cursorIndex);
126    }
127
128    /* package */ Context getContext() {
129        return mContext;
130    }
131
132    /**
133     * Get the integer at cursorIndex from the Cursor.  Assumes the Cursor has
134     * already been moved to the correct position.  Along with
135     * {@link #getBlob} and {@link #getString}, these are provided so the client
136     * does not need to access the Cursor directly
137     * @param cursorIndex Index to query the Cursor.
138     * @return corresponding integer from the Cursor.
139     */
140    /* package */ int getInt(int cursorIndex) {
141        return mCursor.getInt(cursorIndex);
142    }
143
144    /**
145     * Get the long at cursorIndex from the Cursor.  Assumes the Cursor has
146     * already been moved to the correct position.
147     */
148    /* package */ long getLong(int cursorIndex) {
149        return mCursor.getLong(cursorIndex);
150    }
151
152    /**
153     * Get the String at cursorIndex from the Cursor.  Assumes the Cursor has
154     * already been moved to the correct position.  Along with
155     * {@link #getInt} and {@link #getInt}, these are provided so the client
156     * does not need to access the Cursor directly
157     * @param cursorIndex Index to query the Cursor.
158     * @return corresponding String from the Cursor.
159     */
160    /* package */ String getString(int cursorIndex) {
161        return mCursor.getString(cursorIndex);
162    }
163
164    /**
165     * Determine which group an item belongs to.
166     * @param childId ID of the child view in question.
167     * @return int Group position of the containing group.
168    /* package */ int groupFromChildId(long childId) {
169        int group = -1;
170        for (mCursor.moveToFirst(); !mCursor.isAfterLast();
171                mCursor.moveToNext()) {
172            if (getLong(mIdIndex) == childId) {
173                int bin = mDateSorter.getIndex(getLong(mDateIndex));
174                // bin is the same as the group if the number of bins is the
175                // same as DateSorter
176                if (mDateSorter.DAY_COUNT == mNumberOfBins) return bin;
177                // There are some empty bins.  Find the corresponding group.
178                group = 0;
179                for (int i = 0; i < bin; i++) {
180                    if (mItemMap[i] != 0) group++;
181                }
182                break;
183            }
184        }
185        return group;
186    }
187
188    /**
189     * Translates from a group position in the ExpandableList to a bin.  This is
190     * necessary because some groups have no history items, so we do not include
191     * those in the ExpandableList.
192     * @param groupPosition Position in the ExpandableList's set of groups
193     * @return The corresponding bin that holds that group.
194     */
195    private int groupPositionToBin(int groupPosition) {
196        if (groupPosition < 0 || groupPosition >= DateSorter.DAY_COUNT) {
197            throw new AssertionError("group position out of range");
198        }
199        if (DateSorter.DAY_COUNT == mNumberOfBins || 0 == mNumberOfBins) {
200            // In the first case, we have exactly the same number of bins
201            // as our maximum possible, so there is no need to do a
202            // conversion
203            // The second statement is in case this method gets called when
204            // the array is empty, in which case the provided groupPosition
205            // will do fine.
206            return groupPosition;
207        }
208        int arrayPosition = -1;
209        while (groupPosition > -1) {
210            arrayPosition++;
211            if (mItemMap[arrayPosition] != 0) {
212                groupPosition--;
213            }
214        }
215        return arrayPosition;
216    }
217
218    /**
219     * Move the cursor to the position indicated.
220     * @param packedPosition Position in packed position representation.
221     * @return True on success, false otherwise.
222     */
223    boolean moveCursorToPackedChildPosition(long packedPosition) {
224        if (ExpandableListView.getPackedPositionType(packedPosition) !=
225                ExpandableListView.PACKED_POSITION_TYPE_CHILD) {
226            return false;
227        }
228        int groupPosition = ExpandableListView.getPackedPositionGroup(
229                packedPosition);
230        int childPosition = ExpandableListView.getPackedPositionChild(
231                packedPosition);
232        return moveCursorToChildPosition(groupPosition, childPosition);
233    }
234
235    /**
236     * Move the cursor the the position indicated.
237     * @param groupPosition Index of the group containing the desired item.
238     * @param childPosition Index of the item within the specified group.
239     * @return boolean False if the cursor is closed, so the Cursor was not
240     *      moved.  True on success.
241     */
242    /* package */ boolean moveCursorToChildPosition(int groupPosition,
243            int childPosition) {
244        if (mCursor.isClosed()) return false;
245        groupPosition = groupPositionToBin(groupPosition);
246        int index = childPosition;
247        for (int i = 0; i < groupPosition; i++) {
248            index += mItemMap[i];
249        }
250        return mCursor.moveToPosition(index);
251    }
252
253    /* package */ void refreshData() {
254        if (mCursor.isClosed()) {
255            return;
256        }
257        mCursor.requery();
258        buildMap();
259        for (DataSetObserver o : mObservers) {
260            o.onChanged();
261        }
262    }
263
264    public View getGroupView(int groupPosition, boolean isExpanded,
265            View convertView, ViewGroup parent) {
266        TextView item;
267        if (null == convertView || !(convertView instanceof TextView)) {
268            LayoutInflater factory = LayoutInflater.from(mContext);
269            item = (TextView) factory.inflate(R.layout.history_header, null);
270        } else {
271            item = (TextView) convertView;
272        }
273        String label = mDateSorter.getLabel(groupPositionToBin(groupPosition));
274        item.setText(label);
275        return item;
276    }
277
278    public View getChildView(int groupPosition, int childPosition,
279            boolean isLastChild, View convertView, ViewGroup parent) {
280        return null;
281    }
282
283    public boolean areAllItemsEnabled() {
284        return true;
285    }
286
287    public boolean isChildSelectable(int groupPosition, int childPosition) {
288        return true;
289    }
290
291    public int getGroupCount() {
292        return mNumberOfBins;
293    }
294
295    public int getChildrenCount(int groupPosition) {
296        return mItemMap[groupPositionToBin(groupPosition)];
297    }
298
299    public Object getGroup(int groupPosition) {
300        return null;
301    }
302
303    public Object getChild(int groupPosition, int childPosition) {
304        return null;
305    }
306
307    public long getGroupId(int groupPosition) {
308        return groupPosition;
309    }
310
311    public long getChildId(int groupPosition, int childPosition) {
312        if (moveCursorToChildPosition(groupPosition, childPosition)) {
313            return getLong(mIdIndex);
314        }
315        return 0;
316    }
317
318    public boolean hasStableIds() {
319        return true;
320    }
321
322    public void registerDataSetObserver(DataSetObserver observer) {
323        mObservers.add(observer);
324    }
325
326    public void unregisterDataSetObserver(DataSetObserver observer) {
327        mObservers.remove(observer);
328    }
329
330    public void onGroupExpanded(int groupPosition) {
331    }
332
333    public void onGroupCollapsed(int groupPosition) {
334    }
335
336    public long getCombinedChildId(long groupId, long childId) {
337        return childId;
338    }
339
340    public long getCombinedGroupId(long groupId) {
341        return groupId;
342    }
343
344    public boolean isEmpty() {
345        return mCursor.isClosed() || mCursor.getCount() == 0;
346    }
347}
348