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 && offset == 0) {
313                    return IGNORE_ITEM_VIEW_TYPE;
314                }
315                return getItemViewType(i, position);
316            }
317            start = end;
318        }
319
320        throw new ArrayIndexOutOfBoundsException(position);
321    }
322
323    public View getView(int position, View convertView, ViewGroup parent) {
324        ensureCacheValid();
325        int start = 0;
326        for (int i = 0; i < mSize; i++) {
327            int end = start + mPartitions[i].count;
328            if (position >= start && position < end) {
329                int offset = position - start;
330                if (mPartitions[i].hasHeader) {
331                    offset--;
332                }
333                View view;
334                if (offset == -1) {
335                    view = getHeaderView(i, mPartitions[i].cursor, convertView, parent);
336                } else {
337                    if (!mPartitions[i].cursor.moveToPosition(offset)) {
338                        throw new IllegalStateException("Couldn't move cursor to position "
339                                + offset);
340                    }
341                    view = getView(i, mPartitions[i].cursor, offset, convertView, parent);
342                }
343                if (view == null) {
344                    throw new NullPointerException("View should not be null, partition: " + i
345                            + " position: " + offset);
346                }
347                return view;
348            }
349            start = end;
350        }
351
352        throw new ArrayIndexOutOfBoundsException(position);
353    }
354
355    /**
356     * Returns the header view for the specified partition, creating one if needed.
357     */
358    protected View getHeaderView(int partition, Cursor cursor, View convertView,
359            ViewGroup parent) {
360        View view = convertView != null
361                ? convertView
362                : newHeaderView(mContext, partition, cursor, parent);
363        bindHeaderView(view, partition, cursor);
364        return view;
365    }
366
367    /**
368     * Creates the header view for the specified partition.
369     */
370    protected View newHeaderView(Context context, int partition, Cursor cursor,
371            ViewGroup parent) {
372        return null;
373    }
374
375    /**
376     * Binds the header view for the specified partition.
377     */
378    protected void bindHeaderView(View view, int partition, Cursor cursor) {
379    }
380
381    /**
382     * Returns an item view for the specified partition, creating one if needed.
383     */
384    protected View getView(int partition, Cursor cursor, int position, View convertView,
385            ViewGroup parent) {
386        View view;
387        if (convertView != null) {
388            view = convertView;
389        } else {
390            view = newView(mContext, partition, cursor, position, parent);
391        }
392        bindView(view, partition, cursor, position);
393        return view;
394    }
395
396    /**
397     * Creates an item view for the specified partition and position. Position
398     * corresponds directly to the current cursor position.
399     */
400    protected abstract View newView(Context context, int partition, Cursor cursor, int position,
401            ViewGroup parent);
402
403    /**
404     * Binds an item view for the specified partition and position. Position
405     * corresponds directly to the current cursor position.
406     */
407    protected abstract void bindView(View v, int partition, Cursor cursor, int position);
408
409    /**
410     * Returns a pre-positioned cursor for the specified list position.
411     */
412    public Object getItem(int position) {
413        ensureCacheValid();
414        int start = 0;
415        for (int i = 0; i < mSize; i++) {
416            int end = start + mPartitions[i].count;
417            if (position >= start && position < end) {
418                int offset = position - start;
419                if (mPartitions[i].hasHeader) {
420                    offset--;
421                }
422                if (offset == -1) {
423                    return null;
424                }
425                Cursor cursor = mPartitions[i].cursor;
426                cursor.moveToPosition(offset);
427                return cursor;
428            }
429            start = end;
430        }
431
432        return null;
433    }
434
435    /**
436     * Returns the item ID for the specified list position.
437     */
438    public long getItemId(int position) {
439        ensureCacheValid();
440        int start = 0;
441        for (int i = 0; i < mSize; i++) {
442            int end = start + mPartitions[i].count;
443            if (position >= start && position < end) {
444                int offset = position - start;
445                if (mPartitions[i].hasHeader) {
446                    offset--;
447                }
448                if (offset == -1) {
449                    return 0;
450                }
451                if (mPartitions[i].idColumnIndex == -1) {
452                    return 0;
453                }
454
455                Cursor cursor = mPartitions[i].cursor;
456                if (cursor == null || cursor.isClosed() || !cursor.moveToPosition(offset)) {
457                    return 0;
458                }
459                return cursor.getLong(mPartitions[i].idColumnIndex);
460            }
461            start = end;
462        }
463
464        return 0;
465    }
466
467    /**
468     * Returns false if any partition has a header.
469     */
470    @Override
471    public boolean areAllItemsEnabled() {
472        for (int i = 0; i < mSize; i++) {
473            if (mPartitions[i].hasHeader) {
474                return false;
475            }
476        }
477        return true;
478    }
479
480    /**
481     * Returns true for all items except headers.
482     */
483    @Override
484    public boolean isEnabled(int position) {
485        ensureCacheValid();
486        int start = 0;
487        for (int i = 0; i < mSize; i++) {
488            int end = start + mPartitions[i].count;
489            if (position >= start && position < end) {
490                int offset = position - start;
491                if (mPartitions[i].hasHeader && offset == 0) {
492                    return false;
493                } else {
494                    return isEnabled(i, offset);
495                }
496            }
497            start = end;
498        }
499
500        return false;
501    }
502
503    /**
504     * Returns true if the item at the specified offset of the specified
505     * partition is selectable and clickable.
506     */
507    protected boolean isEnabled(int partition, int position) {
508        return true;
509    }
510
511    /**
512     * Enable or disable data change notifications.  It may be a good idea to
513     * disable notifications before making changes to several partitions at once.
514     */
515    public void setNotificationsEnabled(boolean flag) {
516        mNotificationsEnabled = flag;
517        if (flag && mNotificationNeeded) {
518            notifyDataSetChanged();
519        }
520    }
521
522    @Override
523    public void notifyDataSetChanged() {
524        if (mNotificationsEnabled) {
525            mNotificationNeeded = false;
526            super.notifyDataSetChanged();
527        } else {
528            mNotificationNeeded = true;
529        }
530    }
531}
532