CompositeCursorAdapter.java revision 60ae52719e0567fa5b1860df19807716951eca50
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 */
16package com.android.common.widget;
17
18import android.content.Context;
19import android.database.Cursor;
20import android.view.View;
21import android.view.ViewGroup;
22import android.widget.BaseAdapter;
23
24/**
25 * A general purpose adapter that is composed of multiple cursors. It just
26 * appends them in the order they are added.
27 */
28public abstract class CompositeCursorAdapter extends BaseAdapter {
29
30    private static final int INITIAL_CAPACITY = 2;
31
32    public static class Partition {
33        boolean showIfEmpty;
34        boolean hasHeader;
35
36        Cursor cursor;
37        int idColumnIndex;
38        int count;
39
40        public Partition(boolean showIfEmpty, boolean hasHeader) {
41            this.showIfEmpty = showIfEmpty;
42            this.hasHeader = hasHeader;
43        }
44
45        /**
46         * True if the directory should be shown even if no contacts are found.
47         */
48        public boolean getShowIfEmpty() {
49            return showIfEmpty;
50        }
51
52        public boolean getHasHeader() {
53            return hasHeader;
54        }
55    }
56
57    private final Context mContext;
58    private Partition[] mPartitions;
59    private int mSize = 0;
60    private int mCount = 0;
61    private boolean mCacheValid = true;
62
63    public CompositeCursorAdapter(Context context) {
64        this(context, INITIAL_CAPACITY);
65    }
66
67    public CompositeCursorAdapter(Context context, int initialCapacity) {
68        mContext = context;
69        mPartitions = new Partition[INITIAL_CAPACITY];
70    }
71
72    public Context getContext() {
73        return mContext;
74    }
75
76    /**
77     * Registers a partition. The cursor for that partition can be set later.
78     * Partitions should be added in the order they are supposed to appear in the
79     * list.
80     */
81    public void addPartition(boolean showIfEmpty, boolean hasHeader) {
82        addPartition(new Partition(showIfEmpty, hasHeader));
83    }
84
85    public void addPartition(Partition partition) {
86        if (mSize >= mPartitions.length) {
87            int newCapacity = mSize + 2;
88            Partition[] newAdapters = new Partition[newCapacity];
89            System.arraycopy(mPartitions, 0, newAdapters, 0, mSize);
90            mPartitions = newAdapters;
91        }
92        mPartitions[mSize++] = partition;
93        invalidate();
94        notifyDataSetChanged();
95    }
96
97    public void removePartition(int partitionIndex) {
98        Cursor cursor = mPartitions[partitionIndex].cursor;
99        if (cursor != null && !cursor.isClosed()) {
100            cursor.close();
101        }
102
103        System.arraycopy(mPartitions, partitionIndex + 1, mPartitions, partitionIndex,
104                mSize - partitionIndex - 1);
105        mSize--;
106        invalidate();
107        notifyDataSetChanged();
108    }
109
110    /**
111     * Removes cursors for all partitions.
112     */
113    public void clearPartitions() {
114        for (int i = 0; i < mSize; i++) {
115            mPartitions[i].cursor = null;
116        }
117        invalidate();
118        notifyDataSetChanged();
119    }
120
121    public void setHasHeader(int partitionIndex, boolean flag) {
122        mPartitions[partitionIndex].hasHeader = flag;
123        invalidate();
124    }
125
126    public void setShowIfEmpty(int partitionIndex, boolean flag) {
127        mPartitions[partitionIndex].showIfEmpty = flag;
128        invalidate();
129    }
130
131    public Partition getPartition(int partitionIndex) {
132        if (partitionIndex >= mSize) {
133            throw new ArrayIndexOutOfBoundsException(partitionIndex);
134        }
135        return mPartitions[partitionIndex];
136    }
137
138    protected void invalidate() {
139        mCacheValid = false;
140    }
141
142    public int getPartitionCount() {
143        return mSize;
144    }
145
146    protected void ensureCacheValid() {
147        if (mCacheValid) {
148            return;
149        }
150
151        mCount = 0;
152        for (int i = 0; i < mSize; i++) {
153            Cursor cursor = mPartitions[i].cursor;
154            int count = cursor != null ? cursor.getCount() : 0;
155            if (mPartitions[i].hasHeader) {
156                if (count != 0 || mPartitions[i].showIfEmpty) {
157                    count++;
158                }
159            }
160            mPartitions[i].count = count;
161            mCount += count;
162        }
163
164        mCacheValid = true;
165    }
166
167    /**
168     * Returns true if the specified partition was configured to have a header.
169     */
170    public boolean hasHeader(int partition) {
171        return mPartitions[partition].hasHeader;
172    }
173
174    /**
175     * Returns the total number of list items in all partitions.
176     */
177    public int getCount() {
178        ensureCacheValid();
179        return mCount;
180    }
181
182    /**
183     * Returns the cursor for the given partition
184     */
185    public Cursor getCursor(int partition) {
186        return mPartitions[partition].cursor;
187    }
188
189    /**
190     * Changes the cursor for an individual partition.
191     */
192    public void changeCursor(int partition, Cursor cursor) {
193        Cursor prevCursor = mPartitions[partition].cursor;
194        if (prevCursor != cursor) {
195            if (prevCursor != null && !prevCursor.isClosed()) {
196                prevCursor.close();
197            }
198            mPartitions[partition].cursor = cursor;
199            if (cursor != null) {
200                mPartitions[partition].idColumnIndex = cursor.getColumnIndex("_id");
201            }
202            invalidate();
203            notifyDataSetChanged();
204        }
205    }
206
207    /**
208     * Returns true if the specified partition has no cursor or an empty cursor.
209     */
210    public boolean isPartitionEmpty(int partition) {
211        Cursor cursor = mPartitions[partition].cursor;
212        return cursor == null || cursor.getCount() == 0;
213    }
214
215    /**
216     * Given a list position, returns the index of the corresponding partition.
217     */
218    public int getPartitionForPosition(int position) {
219        ensureCacheValid();
220        int start = 0;
221        for (int i = 0; i < mSize; i++) {
222            int end = start + mPartitions[i].count;
223            if (position >= start && position < end) {
224                return i;
225            }
226            start = end;
227        }
228        return -1;
229    }
230
231    /**
232     * Given a list position, return the offset of the corresponding item in its
233     * partition.  The header, if any, will have offset -1.
234     */
235    public int getOffsetInPartition(int position) {
236        ensureCacheValid();
237        int start = 0;
238        for (int i = 0; i < mSize; i++) {
239            int end = start + mPartitions[i].count;
240            if (position >= start && position < end) {
241                int offset = position - start;
242                if (mPartitions[i].hasHeader) {
243                    offset--;
244                }
245                return offset;
246            }
247            start = end;
248        }
249        return -1;
250    }
251
252    /**
253     * Returns the first list position for the specified partition.
254     */
255    public int getPositionForPartition(int partition) {
256        ensureCacheValid();
257        int position = 0;
258        for (int i = 0; i < partition; i++) {
259            position += mPartitions[i].count;
260        }
261        return position;
262    }
263
264    @Override
265    public int getViewTypeCount() {
266        return getItemViewTypeCount() + 1;
267    }
268
269    /**
270     * Returns the overall number of item view types across all partitions. An
271     * implementation of this method needs to ensure that the returned count is
272     * consistent with the values returned by {@link #getItemViewType(int,int)}.
273     */
274    public int getItemViewTypeCount() {
275        return 1;
276    }
277
278    /**
279     * Returns the view type for the list item at the specified position in the
280     * specified partition.
281     */
282    protected int getItemViewType(int partition, int position) {
283        return 1;
284    }
285
286    @Override
287    public int getItemViewType(int position) {
288        ensureCacheValid();
289        int start = 0;
290        for (int i = 0; i < mSize; i++) {
291            int end = start  + mPartitions[i].count;
292            if (position >= start && position < end) {
293                int offset = position - start;
294                if (mPartitions[i].hasHeader && offset == 0) {
295                    return IGNORE_ITEM_VIEW_TYPE;
296                }
297                return getItemViewType(i, position);
298            }
299            start = end;
300        }
301
302        throw new ArrayIndexOutOfBoundsException(position);
303    }
304
305    public View getView(int position, View convertView, ViewGroup parent) {
306        ensureCacheValid();
307        int start = 0;
308        for (int i = 0; i < mSize; i++) {
309            int end = start + mPartitions[i].count;
310            if (position >= start && position < end) {
311                int offset = position - start;
312                if (mPartitions[i].hasHeader) {
313                    offset--;
314                }
315                View view;
316                if (offset == -1) {
317                    view = getHeaderView(i, mPartitions[i].cursor, convertView, parent);
318                } else {
319                    if (!mPartitions[i].cursor.moveToPosition(offset)) {
320                        throw new IllegalStateException("Couldn't move cursor to position "
321                                + offset);
322                    }
323                    view = getView(i, mPartitions[i].cursor, offset, convertView, parent);
324                }
325                if (view == null) {
326                    throw new NullPointerException("View should not be null, partition: " + i
327                            + " position: " + offset);
328                }
329                return view;
330            }
331            start = end;
332        }
333
334        throw new ArrayIndexOutOfBoundsException(position);
335    }
336
337    /**
338     * Returns the header view for the specified partition, creating one if needed.
339     */
340    protected View getHeaderView(int partition, Cursor cursor, View convertView,
341            ViewGroup parent) {
342        View view = convertView != null
343                ? convertView
344                : newHeaderView(mContext, partition, cursor, parent);
345        bindHeaderView(view, partition, cursor);
346        return view;
347    }
348
349    /**
350     * Creates the header view for the specified partition.
351     */
352    protected View newHeaderView(Context context, int partition, Cursor cursor,
353            ViewGroup parent) {
354        return null;
355    }
356
357    /**
358     * Binds the header view for the specified partition.
359     */
360    protected void bindHeaderView(View view, int partition, Cursor cursor) {
361    }
362
363    /**
364     * Returns an item view for the specified partition, creating one if needed.
365     */
366    protected View getView(int partition, Cursor cursor, int position, View convertView,
367            ViewGroup parent) {
368        View view;
369        if (convertView != null) {
370            view = convertView;
371        } else {
372            view = newView(mContext, partition, cursor, position, parent);
373        }
374        bindView(view, partition, cursor, position);
375        return view;
376    }
377
378    /**
379     * Creates an item view for the specified partition and position. Position
380     * corresponds directly to the current cursor position.
381     */
382    protected abstract View newView(Context context, int partition, Cursor cursor, int position,
383            ViewGroup parent);
384
385    /**
386     * Binds an item view for the specified partition and position. Position
387     * corresponds directly to the current cursor position.
388     */
389    protected abstract void bindView(View v, int partition, Cursor cursor, int position);
390
391    /**
392     * Returns a pre-positioned cursor for the specified list position.
393     */
394    public Object getItem(int position) {
395        ensureCacheValid();
396        int start = 0;
397        for (int i = 0; i < mSize; i++) {
398            int end = start + mPartitions[i].count;
399            if (position >= start && position < end) {
400                int offset = position - start;
401                if (mPartitions[i].hasHeader) {
402                    offset--;
403                }
404                if (offset == -1) {
405                    return null;
406                }
407                Cursor cursor = mPartitions[i].cursor;
408                cursor.moveToPosition(offset);
409                return cursor;
410            }
411            start = end;
412        }
413
414        return null;
415    }
416
417    /**
418     * Returns the item ID for the specified list position.
419     */
420    public long getItemId(int position) {
421        ensureCacheValid();
422        int start = 0;
423        for (int i = 0; i < mSize; i++) {
424            int end = start + mPartitions[i].count;
425            if (position >= start && position < end) {
426                int offset = position - start;
427                if (mPartitions[i].hasHeader) {
428                    offset--;
429                }
430                if (offset == -1) {
431                    return 0;
432                }
433                if (mPartitions[i].idColumnIndex == -1) {
434                    return 0;
435                }
436
437                Cursor cursor = mPartitions[i].cursor;
438                if (cursor == null || cursor.isClosed() || !cursor.moveToPosition(offset)) {
439                    return 0;
440                }
441                return cursor.getLong(mPartitions[i].idColumnIndex);
442            }
443            start = end;
444        }
445
446        return 0;
447    }
448
449    /**
450     * Returns false if any partition has a header.
451     */
452    @Override
453    public boolean areAllItemsEnabled() {
454        for (int i = 0; i < mSize; i++) {
455            if (mPartitions[i].hasHeader) {
456                return false;
457            }
458        }
459        return true;
460    }
461
462    /**
463     * Returns true for all items except headers.
464     */
465    @Override
466    public boolean isEnabled(int position) {
467        ensureCacheValid();
468        int start = 0;
469        for (int i = 0; i < mSize; i++) {
470            int end = start + mPartitions[i].count;
471            if (position >= start && position < end) {
472                int offset = position - start;
473                if (mPartitions[i].hasHeader && offset == 0) {
474                    return false;
475                } else {
476                    return isEnabled(i, offset);
477                }
478            }
479            start = end;
480        }
481
482        return false;
483    }
484
485    /**
486     * Returns true if the item at the specified offset of the specified
487     * partition is selectable and clickable.
488     */
489    protected boolean isEnabled(int partition, int position) {
490        return true;
491    }
492}
493