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