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