Home | History | Annotate | Download | only in widget
      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 /**
     25  * A general purpose adapter that is composed of multiple cursors. It just
     26  * appends them in the order they are added.
     27  */
     28 public 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