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