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.Cursor;
21import android.database.DataSetObserver;
22import android.view.LayoutInflater;
23import android.view.View;
24import android.view.ViewGroup;
25import android.webkit.DateSorter;
26import android.widget.BaseExpandableListAdapter;
27import android.widget.ExpandableListView;
28import android.widget.TextView;
29
30/**
31 * ExpandableListAdapter which separates data into categories based on date.
32 * Used for History and Downloads.
33 */
34public class DateSortedExpandableListAdapter extends BaseExpandableListAdapter {
35    // Array for each of our bins.  Each entry represents how many items are
36    // in that bin.
37    private int mItemMap[];
38    // This is our GroupCount.  We will have at most DateSorter.DAY_COUNT
39    // bins, less if the user has no items in one or more bins.
40    private int mNumberOfBins;
41    private Cursor mCursor;
42    private DateSorter mDateSorter;
43    private int mDateIndex;
44    private int mIdIndex;
45    private Context mContext;
46
47    boolean mDataValid;
48
49    DataSetObserver mDataSetObserver = new DataSetObserver() {
50        @Override
51        public void onChanged() {
52            mDataValid = true;
53            notifyDataSetChanged();
54        }
55
56        @Override
57        public void onInvalidated() {
58            mDataValid = false;
59            notifyDataSetInvalidated();
60        }
61    };
62
63    public DateSortedExpandableListAdapter(Context context, int dateIndex) {
64        mContext = context;
65        mDateSorter = new DateSorter(context);
66        mDateIndex = dateIndex;
67        mDataValid = false;
68        mIdIndex = -1;
69    }
70
71    /**
72     * Set up the bins for determining which items belong to which groups.
73     */
74    private void buildMap() {
75        // The cursor is sorted by date
76        // The ItemMap will store the number of items in each bin.
77        int array[] = new int[DateSorter.DAY_COUNT];
78        // Zero out the array.
79        for (int j = 0; j < DateSorter.DAY_COUNT; j++) {
80            array[j] = 0;
81        }
82        mNumberOfBins = 0;
83        int dateIndex = -1;
84        if (mCursor.moveToFirst() && mCursor.getCount() > 0) {
85            while (!mCursor.isAfterLast()) {
86                long date = getLong(mDateIndex);
87                int index = mDateSorter.getIndex(date);
88                if (index > dateIndex) {
89                    mNumberOfBins++;
90                    if (index == DateSorter.DAY_COUNT - 1) {
91                        // We are already in the last bin, so it will
92                        // include all the remaining items
93                        array[index] = mCursor.getCount()
94                                - mCursor.getPosition();
95                        break;
96                    }
97                    dateIndex = index;
98                }
99                array[dateIndex]++;
100                mCursor.moveToNext();
101            }
102        }
103        mItemMap = array;
104    }
105
106    /**
107     * Get the byte array at cursorIndex from the Cursor.  Assumes the Cursor
108     * has already been moved to the correct position.  Along with
109     * {@link #getInt} and {@link #getString}, these are provided so the client
110     * does not need to access the Cursor directly
111     * @param cursorIndex Index to query the Cursor.
112     * @return corresponding byte array from the Cursor.
113     */
114    /* package */ byte[] getBlob(int cursorIndex) {
115        if (!mDataValid) return null;
116        return mCursor.getBlob(cursorIndex);
117    }
118
119    /* package */ Context getContext() {
120        return mContext;
121    }
122
123    /**
124     * Get the integer at cursorIndex from the Cursor.  Assumes the Cursor has
125     * already been moved to the correct position.  Along with
126     * {@link #getBlob} and {@link #getString}, these are provided so the client
127     * does not need to access the Cursor directly
128     * @param cursorIndex Index to query the Cursor.
129     * @return corresponding integer from the Cursor.
130     */
131    /* package */ int getInt(int cursorIndex) {
132        if (!mDataValid) return 0;
133        return mCursor.getInt(cursorIndex);
134    }
135
136    /**
137     * Get the long at cursorIndex from the Cursor.  Assumes the Cursor has
138     * already been moved to the correct position.
139     */
140    /* package */ long getLong(int cursorIndex) {
141        if (!mDataValid) return 0;
142        return mCursor.getLong(cursorIndex);
143    }
144
145    /**
146     * Get the String at cursorIndex from the Cursor.  Assumes the Cursor has
147     * already been moved to the correct position.  Along with
148     * {@link #getInt} and {@link #getInt}, these are provided so the client
149     * does not need to access the Cursor directly
150     * @param cursorIndex Index to query the Cursor.
151     * @return corresponding String from the Cursor.
152     */
153    /* package */ String getString(int cursorIndex) {
154        if (!mDataValid) return null;
155        return mCursor.getString(cursorIndex);
156    }
157
158    /**
159     * Determine which group an item belongs to.
160     * @param childId ID of the child view in question.
161     * @return int Group position of the containing group.
162    /* package */ int groupFromChildId(long childId) {
163        if (!mDataValid) return -1;
164        int group = -1;
165        for (mCursor.moveToFirst(); !mCursor.isAfterLast();
166                mCursor.moveToNext()) {
167            if (getLong(mIdIndex) == childId) {
168                int bin = mDateSorter.getIndex(getLong(mDateIndex));
169                // bin is the same as the group if the number of bins is the
170                // same as DateSorter
171                if (DateSorter.DAY_COUNT == mNumberOfBins) {
172                    return bin;
173                }
174                // There are some empty bins.  Find the corresponding group.
175                group = 0;
176                for (int i = 0; i < bin; i++) {
177                    if (mItemMap[i] != 0) {
178                        group++;
179                    }
180                }
181                break;
182            }
183        }
184        return group;
185    }
186
187    /**
188     * Translates from a group position in the ExpandableList to a bin.  This is
189     * necessary because some groups have no history items, so we do not include
190     * those in the ExpandableList.
191     * @param groupPosition Position in the ExpandableList's set of groups
192     * @return The corresponding bin that holds that group.
193     */
194    private int groupPositionToBin(int groupPosition) {
195        if (!mDataValid) return -1;
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 (!mDataValid || mCursor.isClosed()) {
245            return false;
246        }
247        groupPosition = groupPositionToBin(groupPosition);
248        int index = childPosition;
249        for (int i = 0; i < groupPosition; i++) {
250            index += mItemMap[i];
251        }
252        return mCursor.moveToPosition(index);
253    }
254
255    public void changeCursor(Cursor cursor) {
256        if (cursor == mCursor) {
257            return;
258        }
259        if (mCursor != null) {
260            mCursor.unregisterDataSetObserver(mDataSetObserver);
261            mCursor.close();
262        }
263        mCursor = cursor;
264        if (cursor != null) {
265            cursor.registerDataSetObserver(mDataSetObserver);
266            mIdIndex = cursor.getColumnIndexOrThrow("_id");
267            mDataValid = true;
268            buildMap();
269            // notify the observers about the new cursor
270            notifyDataSetChanged();
271        } else {
272            mIdIndex = -1;
273            mDataValid = false;
274            // notify the observers about the lack of a data set
275            notifyDataSetInvalidated();
276        }
277    }
278
279    @Override
280    public View getGroupView(int groupPosition, boolean isExpanded,
281            View convertView, ViewGroup parent) {
282        if (!mDataValid) throw new IllegalStateException("Data is not valid");
283        TextView item;
284        if (null == convertView || !(convertView instanceof TextView)) {
285            LayoutInflater factory = LayoutInflater.from(mContext);
286            item = (TextView) factory.inflate(R.layout.history_header, null);
287        } else {
288            item = (TextView) convertView;
289        }
290        String label = mDateSorter.getLabel(groupPositionToBin(groupPosition));
291        item.setText(label);
292        return item;
293    }
294
295    @Override
296    public View getChildView(int groupPosition, int childPosition,
297            boolean isLastChild, View convertView, ViewGroup parent) {
298        if (!mDataValid) throw new IllegalStateException("Data is not valid");
299        return null;
300    }
301
302    @Override
303    public boolean areAllItemsEnabled() {
304        return true;
305    }
306
307    @Override
308    public boolean isChildSelectable(int groupPosition, int childPosition) {
309        return true;
310    }
311
312    @Override
313    public int getGroupCount() {
314        if (!mDataValid) return 0;
315        return mNumberOfBins;
316    }
317
318    @Override
319    public int getChildrenCount(int groupPosition) {
320        if (!mDataValid) return 0;
321        return mItemMap[groupPositionToBin(groupPosition)];
322    }
323
324    @Override
325    public Object getGroup(int groupPosition) {
326        return null;
327    }
328
329    @Override
330    public Object getChild(int groupPosition, int childPosition) {
331        return null;
332    }
333
334    @Override
335    public long getGroupId(int groupPosition) {
336        if (!mDataValid) return 0;
337        return groupPosition;
338    }
339
340    @Override
341    public long getChildId(int groupPosition, int childPosition) {
342        if (!mDataValid) return 0;
343        if (moveCursorToChildPosition(groupPosition, childPosition)) {
344            return getLong(mIdIndex);
345        }
346        return 0;
347    }
348
349    @Override
350    public boolean hasStableIds() {
351        return true;
352    }
353
354    @Override
355    public void onGroupExpanded(int groupPosition) {
356    }
357
358    @Override
359    public void onGroupCollapsed(int groupPosition) {
360    }
361
362    @Override
363    public long getCombinedChildId(long groupId, long childId) {
364        if (!mDataValid) return 0;
365        return childId;
366    }
367
368    @Override
369    public long getCombinedGroupId(long groupId) {
370        if (!mDataValid) return 0;
371        return groupId;
372    }
373
374    @Override
375    public boolean isEmpty() {
376        return !mDataValid || mCursor == null || mCursor.isClosed() || mCursor.getCount() == 0;
377    }
378}
379