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 = cursor != null ? cursor.getCount() : 0;
    174             if (partition.hasHeader) {
    175                 if (count != 0 || partition.showIfEmpty) {
    176                     count++;
    177                 }
    178             }
    179             partition.count = count;
    180             mCount += count;
    181         }
    182 
    183         mCacheValid = true;
    184     }
    185 
    186     /**
    187      * Returns true if the specified partition was configured to have a header.
    188      */
    189     public boolean hasHeader(int partition) {
    190         return mPartitions.get(partition).hasHeader;
    191     }
    192 
    193     /**
    194      * Returns the total number of list items in all partitions.
    195      */
    196     public int getCount() {
    197         ensureCacheValid();
    198         return mCount;
    199     }
    200 
    201     /**
    202      * Returns the cursor for the given partition
    203      */
    204     public Cursor getCursor(int partition) {
    205         return mPartitions.get(partition).cursor;
    206     }
    207 
    208     /**
    209      * Changes the cursor for an individual partition.
    210      */
    211     public void changeCursor(int partition, Cursor cursor) {
    212         Cursor prevCursor = mPartitions.get(partition).cursor;
    213         if (prevCursor != cursor) {
    214             if (prevCursor != null && !prevCursor.isClosed()) {
    215                 prevCursor.close();
    216             }
    217             mPartitions.get(partition).cursor = cursor;
    218             if (cursor != null) {
    219                 mPartitions.get(partition).idColumnIndex = cursor.getColumnIndex("_id");
    220             }
    221             invalidate();
    222             notifyDataSetChanged();
    223         }
    224     }
    225 
    226     /**
    227      * Returns true if the specified partition has no cursor or an empty cursor.
    228      */
    229     public boolean isPartitionEmpty(int partition) {
    230         Cursor cursor = mPartitions.get(partition).cursor;
    231         return cursor == null || cursor.getCount() == 0;
    232     }
    233 
    234     /**
    235      * Given a list position, returns the index of the corresponding partition.
    236      */
    237     public int getPartitionForPosition(int position) {
    238         ensureCacheValid();
    239         int start = 0;
    240         for (int i = 0, n = mPartitions.size(); i < n; i++) {
    241             int end = start + mPartitions.get(i).count;
    242             if (position >= start && position < end) {
    243                 return i;
    244             }
    245             start = end;
    246         }
    247         return -1;
    248     }
    249 
    250     /**
    251      * Given a list position, return the offset of the corresponding item in its
    252      * partition.  The header, if any, will have offset -1.
    253      */
    254     public int getOffsetInPartition(int position) {
    255         ensureCacheValid();
    256         int start = 0;
    257         for (Partition partition : mPartitions) {
    258             int end = start + partition.count;
    259             if (position >= start && position < end) {
    260                 int offset = position - start;
    261                 if (partition.hasHeader) {
    262                     offset--;
    263                 }
    264                 return offset;
    265             }
    266             start = end;
    267         }
    268         return -1;
    269     }
    270 
    271     /**
    272      * Returns the first list position for the specified partition.
    273      */
    274     public int getPositionForPartition(int partition) {
    275         ensureCacheValid();
    276         int position = 0;
    277         for (int i = 0; i < partition; i++) {
    278             position += mPartitions.get(i).count;
    279         }
    280         return position;
    281     }
    282 
    283     @Override
    284     public int getViewTypeCount() {
    285         return getItemViewTypeCount() + 1;
    286     }
    287 
    288     /**
    289      * Returns the overall number of item view types across all partitions. An
    290      * implementation of this method needs to ensure that the returned count is
    291      * consistent with the values returned by {@link #getItemViewType(int,int)}.
    292      */
    293     public int getItemViewTypeCount() {
    294         return 1;
    295     }
    296 
    297     /**
    298      * Returns the view type for the list item at the specified position in the
    299      * specified partition.
    300      */
    301     protected int getItemViewType(int partition, int position) {
    302         return 1;
    303     }
    304 
    305     @Override
    306     public int getItemViewType(int position) {
    307         ensureCacheValid();
    308         int start = 0;
    309         for (int i = 0, n = mPartitions.size(); i < n; i++) {
    310             int end = start  + mPartitions.get(i).count;
    311             if (position >= start && position < end) {
    312                 int offset = position - start;
    313                 if (mPartitions.get(i).hasHeader) {
    314                     offset--;
    315                 }
    316                 if (offset == -1) {
    317                     return IGNORE_ITEM_VIEW_TYPE;
    318                 } else {
    319                     return getItemViewType(i, offset);
    320                 }
    321             }
    322             start = end;
    323         }
    324 
    325         throw new ArrayIndexOutOfBoundsException(position);
    326     }
    327 
    328     public View getView(int position, View convertView, ViewGroup parent) {
    329         ensureCacheValid();
    330         int start = 0;
    331         for (int i = 0, n = mPartitions.size(); i < n; i++) {
    332             int end = start + mPartitions.get(i).count;
    333             if (position >= start && position < end) {
    334                 int offset = position - start;
    335                 if (mPartitions.get(i).hasHeader) {
    336                     offset--;
    337                 }
    338                 View view;
    339                 if (offset == -1) {
    340                     view = getHeaderView(i, mPartitions.get(i).cursor, convertView, parent);
    341                 } else {
    342                     if (!mPartitions.get(i).cursor.moveToPosition(offset)) {
    343                         throw new IllegalStateException("Couldn't move cursor to position "
    344                                 + offset);
    345                     }
    346                     view = getView(i, mPartitions.get(i).cursor, offset, convertView, parent);
    347                 }
    348                 if (view == null) {
    349                     throw new NullPointerException("View should not be null, partition: " + i
    350                             + " position: " + offset);
    351                 }
    352                 return view;
    353             }
    354             start = end;
    355         }
    356 
    357         throw new ArrayIndexOutOfBoundsException(position);
    358     }
    359 
    360     /**
    361      * Returns the header view for the specified partition, creating one if needed.
    362      */
    363     protected View getHeaderView(int partition, Cursor cursor, View convertView,
    364             ViewGroup parent) {
    365         View view = convertView != null
    366                 ? convertView
    367                 : newHeaderView(mContext, partition, cursor, parent);
    368         bindHeaderView(view, partition, cursor);
    369         return view;
    370     }
    371 
    372     /**
    373      * Creates the header view for the specified partition.
    374      */
    375     protected View newHeaderView(Context context, int partition, Cursor cursor,
    376             ViewGroup parent) {
    377         return null;
    378     }
    379 
    380     /**
    381      * Binds the header view for the specified partition.
    382      */
    383     protected void bindHeaderView(View view, int partition, Cursor cursor) {
    384     }
    385 
    386     /**
    387      * Returns an item view for the specified partition, creating one if needed.
    388      */
    389     protected View getView(int partition, Cursor cursor, int position, View convertView,
    390             ViewGroup parent) {
    391         View view;
    392         if (convertView != null) {
    393             view = convertView;
    394         } else {
    395             view = newView(mContext, partition, cursor, position, parent);
    396         }
    397         bindView(view, partition, cursor, position);
    398         return view;
    399     }
    400 
    401     /**
    402      * Creates an item view for the specified partition and position. Position
    403      * corresponds directly to the current cursor position.
    404      */
    405     protected abstract View newView(Context context, int partition, Cursor cursor, int position,
    406             ViewGroup parent);
    407 
    408     /**
    409      * Binds an item view for the specified partition and position. Position
    410      * corresponds directly to the current cursor position.
    411      */
    412     protected abstract void bindView(View v, int partition, Cursor cursor, int position);
    413 
    414     /**
    415      * Returns a pre-positioned cursor for the specified list position.
    416      */
    417     public Object getItem(int position) {
    418         ensureCacheValid();
    419         int start = 0;
    420         for (Partition mPartition : mPartitions) {
    421             int end = start + mPartition.count;
    422             if (position >= start && position < end) {
    423                 int offset = position - start;
    424                 if (mPartition.hasHeader) {
    425                     offset--;
    426                 }
    427                 if (offset == -1) {
    428                     return null;
    429                 }
    430                 Cursor cursor = mPartition.cursor;
    431                 cursor.moveToPosition(offset);
    432                 return cursor;
    433             }
    434             start = end;
    435         }
    436 
    437         return null;
    438     }
    439 
    440     /**
    441      * Returns the item ID for the specified list position.
    442      */
    443     public long getItemId(int position) {
    444         ensureCacheValid();
    445         int start = 0;
    446         for (Partition mPartition : mPartitions) {
    447             int end = start + mPartition.count;
    448             if (position >= start && position < end) {
    449                 int offset = position - start;
    450                 if (mPartition.hasHeader) {
    451                     offset--;
    452                 }
    453                 if (offset == -1) {
    454                     return 0;
    455                 }
    456                 if (mPartition.idColumnIndex == -1) {
    457                     return 0;
    458                 }
    459 
    460                 Cursor cursor = mPartition.cursor;
    461                 if (cursor == null || cursor.isClosed() || !cursor.moveToPosition(offset)) {
    462                     return 0;
    463                 }
    464                 return cursor.getLong(mPartition.idColumnIndex);
    465             }
    466             start = end;
    467         }
    468 
    469         return 0;
    470     }
    471 
    472     /**
    473      * Returns false if any partition has a header.
    474      */
    475     @Override
    476     public boolean areAllItemsEnabled() {
    477         for (Partition mPartition : mPartitions) {
    478             if (mPartition.hasHeader) {
    479                 return false;
    480             }
    481         }
    482         return true;
    483     }
    484 
    485     /**
    486      * Returns true for all items except headers.
    487      */
    488     @Override
    489     public boolean isEnabled(int position) {
    490         ensureCacheValid();
    491         int start = 0;
    492         for (int i = 0, n = mPartitions.size(); i < n; i++) {
    493             int end = start + mPartitions.get(i).count;
    494             if (position >= start && position < end) {
    495                 int offset = position - start;
    496                 if (mPartitions.get(i).hasHeader && offset == 0) {
    497                     return false;
    498                 } else {
    499                     return isEnabled(i, offset);
    500                 }
    501             }
    502             start = end;
    503         }
    504 
    505         return false;
    506     }
    507 
    508     /**
    509      * Returns true if the item at the specified offset of the specified
    510      * partition is selectable and clickable.
    511      */
    512     protected boolean isEnabled(int partition, int position) {
    513         return true;
    514     }
    515 
    516     /**
    517      * Enable or disable data change notifications.  It may be a good idea to
    518      * disable notifications before making changes to several partitions at once.
    519      */
    520     public void setNotificationsEnabled(boolean flag) {
    521         mNotificationsEnabled = flag;
    522         if (flag && mNotificationNeeded) {
    523             notifyDataSetChanged();
    524         }
    525     }
    526 
    527     @Override
    528     public void notifyDataSetChanged() {
    529         if (mNotificationsEnabled) {
    530             mNotificationNeeded = false;
    531             super.notifyDataSetChanged();
    532         } else {
    533             mNotificationNeeded = true;
    534         }
    535     }
    536 }
    537