CompositeCursorAdapter.java revision 5cc774535d73c09b6788b63ecc728e60da09cfa9
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    /**
122     * Closes all cursors and removes all partitions.
123     */
124    public void close() {
125        for (int i = 0; i < mSize; i++) {
126            Cursor cursor = mPartitions[i].cursor;
127            if (cursor != null && !cursor.isClosed()) {
128                cursor.close();
129                mPartitions[i].cursor = null;
130            }
131        }
132        mSize = 0;
133        invalidate();
134        notifyDataSetChanged();
135    }
136
137    public void setHasHeader(int partitionIndex, boolean flag) {
138        mPartitions[partitionIndex].hasHeader = flag;
139        invalidate();
140    }
141
142    public void setShowIfEmpty(int partitionIndex, boolean flag) {
143        mPartitions[partitionIndex].showIfEmpty = flag;
144        invalidate();
145    }
146
147    public Partition getPartition(int partitionIndex) {
148        if (partitionIndex >= mSize) {
149            throw new ArrayIndexOutOfBoundsException(partitionIndex);
150        }
151        return mPartitions[partitionIndex];
152    }
153
154    protected void invalidate() {
155        mCacheValid = false;
156    }
157
158    public int getPartitionCount() {
159        return mSize;
160    }
161
162    protected void ensureCacheValid() {
163        if (mCacheValid) {
164            return;
165        }
166
167        mCount = 0;
168        for (int i = 0; i < mSize; i++) {
169            Cursor cursor = mPartitions[i].cursor;
170            int count = cursor != null ? cursor.getCount() : 0;
171            if (mPartitions[i].hasHeader) {
172                if (count != 0 || mPartitions[i].showIfEmpty) {
173                    count++;
174                }
175            }
176            mPartitions[i].count = count;
177            mCount += count;
178        }
179
180        mCacheValid = true;
181    }
182
183    /**
184     * Returns true if the specified partition was configured to have a header.
185     */
186    public boolean hasHeader(int partition) {
187        return mPartitions[partition].hasHeader;
188    }
189
190    /**
191     * Returns the total number of list items in all partitions.
192     */
193    public int getCount() {
194        ensureCacheValid();
195        return mCount;
196    }
197
198    /**
199     * Returns the cursor for the given partition
200     */
201    public Cursor getCursor(int partition) {
202        return mPartitions[partition].cursor;
203    }
204
205    /**
206     * Changes the cursor for an individual partition.
207     */
208    public void changeCursor(int partition, Cursor cursor) {
209        Cursor prevCursor = mPartitions[partition].cursor;
210        if (prevCursor != cursor) {
211            if (prevCursor != null && !prevCursor.isClosed()) {
212                prevCursor.close();
213            }
214            mPartitions[partition].cursor = cursor;
215            if (cursor != null) {
216                mPartitions[partition].idColumnIndex = cursor.getColumnIndex("_id");
217            }
218            invalidate();
219            notifyDataSetChanged();
220        }
221    }
222
223    /**
224     * Returns true if the specified partition has no cursor or an empty cursor.
225     */
226    public boolean isPartitionEmpty(int partition) {
227        Cursor cursor = mPartitions[partition].cursor;
228        return cursor == null || cursor.getCount() == 0;
229    }
230
231    /**
232     * Given a list position, returns the index of the corresponding partition.
233     */
234    public int getPartitionForPosition(int position) {
235        ensureCacheValid();
236        int start = 0;
237        for (int i = 0; i < mSize; i++) {
238            int end = start + mPartitions[i].count;
239            if (position >= start && position < end) {
240                return i;
241            }
242            start = end;
243        }
244        return -1;
245    }
246
247    /**
248     * Given a list position, return the offset of the corresponding item in its
249     * partition.  The header, if any, will have offset -1.
250     */
251    public int getOffsetInPartition(int position) {
252        ensureCacheValid();
253        int start = 0;
254        for (int i = 0; i < mSize; i++) {
255            int end = start + mPartitions[i].count;
256            if (position >= start && position < end) {
257                int offset = position - start;
258                if (mPartitions[i].hasHeader) {
259                    offset--;
260                }
261                return offset;
262            }
263            start = end;
264        }
265        return -1;
266    }
267
268    /**
269     * Returns the first list position for the specified partition.
270     */
271    public int getPositionForPartition(int partition) {
272        ensureCacheValid();
273        int position = 0;
274        for (int i = 0; i < partition; i++) {
275            position += mPartitions[i].count;
276        }
277        return position;
278    }
279
280    @Override
281    public int getViewTypeCount() {
282        return getItemViewTypeCount() + 1;
283    }
284
285    /**
286     * Returns the overall number of item view types across all partitions. An
287     * implementation of this method needs to ensure that the returned count is
288     * consistent with the values returned by {@link #getItemViewType(int,int)}.
289     */
290    public int getItemViewTypeCount() {
291        return 1;
292    }
293
294    /**
295     * Returns the view type for the list item at the specified position in the
296     * specified partition.
297     */
298    protected int getItemViewType(int partition, int position) {
299        return 1;
300    }
301
302    @Override
303    public int getItemViewType(int position) {
304        ensureCacheValid();
305        int start = 0;
306        for (int i = 0; i < mSize; i++) {
307            int end = start  + mPartitions[i].count;
308            if (position >= start && position < end) {
309                int offset = position - start;
310                if (mPartitions[i].hasHeader && offset == 0) {
311                    return IGNORE_ITEM_VIEW_TYPE;
312                }
313                return getItemViewType(i, position);
314            }
315            start = end;
316        }
317
318        throw new ArrayIndexOutOfBoundsException(position);
319    }
320
321    public View getView(int position, View convertView, ViewGroup parent) {
322        ensureCacheValid();
323        int start = 0;
324        for (int i = 0; i < mSize; i++) {
325            int end = start + mPartitions[i].count;
326            if (position >= start && position < end) {
327                int offset = position - start;
328                if (mPartitions[i].hasHeader) {
329                    offset--;
330                }
331                View view;
332                if (offset == -1) {
333                    view = getHeaderView(i, mPartitions[i].cursor, convertView, parent);
334                } else {
335                    if (!mPartitions[i].cursor.moveToPosition(offset)) {
336                        throw new IllegalStateException("Couldn't move cursor to position "
337                                + offset);
338                    }
339                    view = getView(i, mPartitions[i].cursor, offset, convertView, parent);
340                }
341                if (view == null) {
342                    throw new NullPointerException("View should not be null, partition: " + i
343                            + " position: " + offset);
344                }
345                return view;
346            }
347            start = end;
348        }
349
350        throw new ArrayIndexOutOfBoundsException(position);
351    }
352
353    /**
354     * Returns the header view for the specified partition, creating one if needed.
355     */
356    protected View getHeaderView(int partition, Cursor cursor, View convertView,
357            ViewGroup parent) {
358        View view = convertView != null
359                ? convertView
360                : newHeaderView(mContext, partition, cursor, parent);
361        bindHeaderView(view, partition, cursor);
362        return view;
363    }
364
365    /**
366     * Creates the header view for the specified partition.
367     */
368    protected View newHeaderView(Context context, int partition, Cursor cursor,
369            ViewGroup parent) {
370        return null;
371    }
372
373    /**
374     * Binds the header view for the specified partition.
375     */
376    protected void bindHeaderView(View view, int partition, Cursor cursor) {
377    }
378
379    /**
380     * Returns an item view for the specified partition, creating one if needed.
381     */
382    protected View getView(int partition, Cursor cursor, int position, View convertView,
383            ViewGroup parent) {
384        View view;
385        if (convertView != null) {
386            view = convertView;
387        } else {
388            view = newView(mContext, partition, cursor, position, parent);
389        }
390        bindView(view, partition, cursor, position);
391        return view;
392    }
393
394    /**
395     * Creates an item view for the specified partition and position. Position
396     * corresponds directly to the current cursor position.
397     */
398    protected abstract View newView(Context context, int partition, Cursor cursor, int position,
399            ViewGroup parent);
400
401    /**
402     * Binds an item view for the specified partition and position. Position
403     * corresponds directly to the current cursor position.
404     */
405    protected abstract void bindView(View v, int partition, Cursor cursor, int position);
406
407    /**
408     * Returns a pre-positioned cursor for the specified list position.
409     */
410    public Object getItem(int position) {
411        ensureCacheValid();
412        int start = 0;
413        for (int i = 0; i < mSize; i++) {
414            int end = start + mPartitions[i].count;
415            if (position >= start && position < end) {
416                int offset = position - start;
417                if (mPartitions[i].hasHeader) {
418                    offset--;
419                }
420                if (offset == -1) {
421                    return null;
422                }
423                Cursor cursor = mPartitions[i].cursor;
424                cursor.moveToPosition(offset);
425                return cursor;
426            }
427            start = end;
428        }
429
430        return null;
431    }
432
433    /**
434     * Returns the item ID for the specified list position.
435     */
436    public long getItemId(int position) {
437        ensureCacheValid();
438        int start = 0;
439        for (int i = 0; i < mSize; i++) {
440            int end = start + mPartitions[i].count;
441            if (position >= start && position < end) {
442                int offset = position - start;
443                if (mPartitions[i].hasHeader) {
444                    offset--;
445                }
446                if (offset == -1) {
447                    return 0;
448                }
449                if (mPartitions[i].idColumnIndex == -1) {
450                    return 0;
451                }
452
453                Cursor cursor = mPartitions[i].cursor;
454                if (cursor == null || cursor.isClosed() || !cursor.moveToPosition(offset)) {
455                    return 0;
456                }
457                return cursor.getLong(mPartitions[i].idColumnIndex);
458            }
459            start = end;
460        }
461
462        return 0;
463    }
464
465    /**
466     * Returns false if any partition has a header.
467     */
468    @Override
469    public boolean areAllItemsEnabled() {
470        for (int i = 0; i < mSize; i++) {
471            if (mPartitions[i].hasHeader) {
472                return false;
473            }
474        }
475        return true;
476    }
477
478    /**
479     * Returns true for all items except headers.
480     */
481    @Override
482    public boolean isEnabled(int position) {
483        ensureCacheValid();
484        int start = 0;
485        for (int i = 0; i < mSize; i++) {
486            int end = start + mPartitions[i].count;
487            if (position >= start && position < end) {
488                int offset = position - start;
489                if (mPartitions[i].hasHeader && offset == 0) {
490                    return false;
491                } else {
492                    return isEnabled(i, offset);
493                }
494            }
495            start = end;
496        }
497
498        return false;
499    }
500
501    /**
502     * Returns true if the item at the specified offset of the specified
503     * partition is selectable and clickable.
504     */
505    protected boolean isEnabled(int partition, int position) {
506        return true;
507    }
508}
509