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