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