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  */
16 package com.android.common.widget;
17 
18 import android.content.Context;
19 import android.database.Cursor;
20 import android.view.View;
21 import android.view.ViewGroup;
22 import android.widget.BaseAdapter;
23 
24 import 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  */
30 public 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 
Partition(boolean showIfEmpty, boolean hasHeader)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          */
getShowIfEmpty()50         public boolean getShowIfEmpty() {
51             return showIfEmpty;
52         }
53 
getHasHeader()54         public boolean getHasHeader() {
55             return hasHeader;
56         }
57 
isEmpty()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 
CompositeCursorAdapter(Context context)70     public CompositeCursorAdapter(Context context) {
71         this(context, INITIAL_CAPACITY);
72     }
73 
CompositeCursorAdapter(Context context, int initialCapacity)74     public CompositeCursorAdapter(Context context, int initialCapacity) {
75         mContext = context;
76         mPartitions = new ArrayList<Partition>();
77     }
78 
getContext()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      */
addPartition(boolean showIfEmpty, boolean hasHeader)88     public void addPartition(boolean showIfEmpty, boolean hasHeader) {
89         addPartition(new Partition(showIfEmpty, hasHeader));
90     }
91 
addPartition(Partition partition)92     public void addPartition(Partition partition) {
93         mPartitions.add(partition);
94         invalidate();
95         notifyDataSetChanged();
96     }
97 
addPartition(int location, Partition partition)98     public void addPartition(int location, Partition partition) {
99         mPartitions.add(location, partition);
100         invalidate();
101         notifyDataSetChanged();
102     }
103 
removePartition(int partitionIndex)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 
clearPartitions()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      */
close()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 
setHasHeader(int partitionIndex, boolean flag)143     public void setHasHeader(int partitionIndex, boolean flag) {
144         mPartitions.get(partitionIndex).hasHeader = flag;
145         invalidate();
146     }
147 
setShowIfEmpty(int partitionIndex, boolean flag)148     public void setShowIfEmpty(int partitionIndex, boolean flag) {
149         mPartitions.get(partitionIndex).showIfEmpty = flag;
150         invalidate();
151     }
152 
getPartition(int partitionIndex)153     public Partition getPartition(int partitionIndex) {
154         return mPartitions.get(partitionIndex);
155     }
156 
invalidate()157     protected void invalidate() {
158         mCacheValid = false;
159     }
160 
getPartitionCount()161     public int getPartitionCount() {
162         return mPartitions.size();
163     }
164 
ensureCacheValid()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      */
hasHeader(int partition)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      */
getCount()201     public int getCount() {
202         ensureCacheValid();
203         return mCount;
204     }
205 
206     /**
207      * Returns the cursor for the given partition
208      */
getCursor(int partition)209     public Cursor getCursor(int partition) {
210         return mPartitions.get(partition).cursor;
211     }
212 
213     /**
214      * Changes the cursor for an individual partition.
215      */
changeCursor(int partition, Cursor cursor)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      */
isPartitionEmpty(int partition)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      */
getPartitionForPosition(int position)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      */
getOffsetInPartition(int position)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      */
getPositionForPartition(int partition)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
getViewTypeCount()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      */
getItemViewTypeCount()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      */
getItemViewType(int partition, int position)306     protected int getItemViewType(int partition, int position) {
307         return 1;
308     }
309 
310     @Override
getItemViewType(int position)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 
getView(int position, View convertView, ViewGroup parent)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      */
getHeaderView(int partition, Cursor cursor, View convertView, ViewGroup parent)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      */
newHeaderView(Context context, int partition, Cursor cursor, ViewGroup parent)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      */
bindHeaderView(View view, int partition, Cursor cursor)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      */
getView(int partition, Cursor cursor, int position, View convertView, ViewGroup parent)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      */
newView(Context context, int partition, Cursor cursor, int position, ViewGroup parent)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      */
bindView(View v, int partition, Cursor cursor, int position)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      */
getItem(int position)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      */
getItemId(int position)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
areAllItemsEnabled()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
isEnabled(int position)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      */
isEnabled(int partition, int position)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      */
setNotificationsEnabled(boolean flag)527     public void setNotificationsEnabled(boolean flag) {
528         mNotificationsEnabled = flag;
529         if (flag && mNotificationNeeded) {
530             notifyDataSetChanged();
531         }
532     }
533 
534     @Override
notifyDataSetChanged()535     public void notifyDataSetChanged() {
536         if (mNotificationsEnabled) {
537             mNotificationNeeded = false;
538             super.notifyDataSetChanged();
539         } else {
540             mNotificationNeeded = true;
541         }
542     }
543 }
544