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 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 
     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