CompositeCursorAdapter.java revision 5cc774535d73c09b6788b63ecc728e60da09cfa9
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 */ 16package com.android.common.widget; 17 18import android.content.Context; 19import android.database.Cursor; 20import android.view.View; 21import android.view.ViewGroup; 22import 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 */ 28public 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 63 public CompositeCursorAdapter(Context context) { 64 this(context, INITIAL_CAPACITY); 65 } 66 67 public CompositeCursorAdapter(Context context, int initialCapacity) { 68 mContext = context; 69 mPartitions = new Partition[INITIAL_CAPACITY]; 70 } 71 72 public Context getContext() { 73 return mContext; 74 } 75 76 /** 77 * Registers a partition. The cursor for that partition can be set later. 78 * Partitions should be added in the order they are supposed to appear in the 79 * list. 80 */ 81 public void addPartition(boolean showIfEmpty, boolean hasHeader) { 82 addPartition(new Partition(showIfEmpty, hasHeader)); 83 } 84 85 public void addPartition(Partition partition) { 86 if (mSize >= mPartitions.length) { 87 int newCapacity = mSize + 2; 88 Partition[] newAdapters = new Partition[newCapacity]; 89 System.arraycopy(mPartitions, 0, newAdapters, 0, mSize); 90 mPartitions = newAdapters; 91 } 92 mPartitions[mSize++] = partition; 93 invalidate(); 94 notifyDataSetChanged(); 95 } 96 97 public void removePartition(int partitionIndex) { 98 Cursor cursor = mPartitions[partitionIndex].cursor; 99 if (cursor != null && !cursor.isClosed()) { 100 cursor.close(); 101 } 102 103 System.arraycopy(mPartitions, partitionIndex + 1, mPartitions, partitionIndex, 104 mSize - partitionIndex - 1); 105 mSize--; 106 invalidate(); 107 notifyDataSetChanged(); 108 } 109 110 /** 111 * Removes cursors for all partitions. 112 */ 113 public void clearPartitions() { 114 for (int i = 0; i < mSize; i++) { 115 mPartitions[i].cursor = null; 116 } 117 invalidate(); 118 notifyDataSetChanged(); 119 } 120 121 /** 122 * Closes all cursors and removes all partitions. 123 */ 124 public void close() { 125 for (int i = 0; i < mSize; i++) { 126 Cursor cursor = mPartitions[i].cursor; 127 if (cursor != null && !cursor.isClosed()) { 128 cursor.close(); 129 mPartitions[i].cursor = null; 130 } 131 } 132 mSize = 0; 133 invalidate(); 134 notifyDataSetChanged(); 135 } 136 137 public void setHasHeader(int partitionIndex, boolean flag) { 138 mPartitions[partitionIndex].hasHeader = flag; 139 invalidate(); 140 } 141 142 public void setShowIfEmpty(int partitionIndex, boolean flag) { 143 mPartitions[partitionIndex].showIfEmpty = flag; 144 invalidate(); 145 } 146 147 public Partition getPartition(int partitionIndex) { 148 if (partitionIndex >= mSize) { 149 throw new ArrayIndexOutOfBoundsException(partitionIndex); 150 } 151 return mPartitions[partitionIndex]; 152 } 153 154 protected void invalidate() { 155 mCacheValid = false; 156 } 157 158 public int getPartitionCount() { 159 return mSize; 160 } 161 162 protected void ensureCacheValid() { 163 if (mCacheValid) { 164 return; 165 } 166 167 mCount = 0; 168 for (int i = 0; i < mSize; i++) { 169 Cursor cursor = mPartitions[i].cursor; 170 int count = cursor != null ? cursor.getCount() : 0; 171 if (mPartitions[i].hasHeader) { 172 if (count != 0 || mPartitions[i].showIfEmpty) { 173 count++; 174 } 175 } 176 mPartitions[i].count = count; 177 mCount += count; 178 } 179 180 mCacheValid = true; 181 } 182 183 /** 184 * Returns true if the specified partition was configured to have a header. 185 */ 186 public boolean hasHeader(int partition) { 187 return mPartitions[partition].hasHeader; 188 } 189 190 /** 191 * Returns the total number of list items in all partitions. 192 */ 193 public int getCount() { 194 ensureCacheValid(); 195 return mCount; 196 } 197 198 /** 199 * Returns the cursor for the given partition 200 */ 201 public Cursor getCursor(int partition) { 202 return mPartitions[partition].cursor; 203 } 204 205 /** 206 * Changes the cursor for an individual partition. 207 */ 208 public void changeCursor(int partition, Cursor cursor) { 209 Cursor prevCursor = mPartitions[partition].cursor; 210 if (prevCursor != cursor) { 211 if (prevCursor != null && !prevCursor.isClosed()) { 212 prevCursor.close(); 213 } 214 mPartitions[partition].cursor = cursor; 215 if (cursor != null) { 216 mPartitions[partition].idColumnIndex = cursor.getColumnIndex("_id"); 217 } 218 invalidate(); 219 notifyDataSetChanged(); 220 } 221 } 222 223 /** 224 * Returns true if the specified partition has no cursor or an empty cursor. 225 */ 226 public boolean isPartitionEmpty(int partition) { 227 Cursor cursor = mPartitions[partition].cursor; 228 return cursor == null || cursor.getCount() == 0; 229 } 230 231 /** 232 * Given a list position, returns the index of the corresponding partition. 233 */ 234 public int getPartitionForPosition(int position) { 235 ensureCacheValid(); 236 int start = 0; 237 for (int i = 0; i < mSize; i++) { 238 int end = start + mPartitions[i].count; 239 if (position >= start && position < end) { 240 return i; 241 } 242 start = end; 243 } 244 return -1; 245 } 246 247 /** 248 * Given a list position, return the offset of the corresponding item in its 249 * partition. The header, if any, will have offset -1. 250 */ 251 public int getOffsetInPartition(int position) { 252 ensureCacheValid(); 253 int start = 0; 254 for (int i = 0; i < mSize; i++) { 255 int end = start + mPartitions[i].count; 256 if (position >= start && position < end) { 257 int offset = position - start; 258 if (mPartitions[i].hasHeader) { 259 offset--; 260 } 261 return offset; 262 } 263 start = end; 264 } 265 return -1; 266 } 267 268 /** 269 * Returns the first list position for the specified partition. 270 */ 271 public int getPositionForPartition(int partition) { 272 ensureCacheValid(); 273 int position = 0; 274 for (int i = 0; i < partition; i++) { 275 position += mPartitions[i].count; 276 } 277 return position; 278 } 279 280 @Override 281 public int getViewTypeCount() { 282 return getItemViewTypeCount() + 1; 283 } 284 285 /** 286 * Returns the overall number of item view types across all partitions. An 287 * implementation of this method needs to ensure that the returned count is 288 * consistent with the values returned by {@link #getItemViewType(int,int)}. 289 */ 290 public int getItemViewTypeCount() { 291 return 1; 292 } 293 294 /** 295 * Returns the view type for the list item at the specified position in the 296 * specified partition. 297 */ 298 protected int getItemViewType(int partition, int position) { 299 return 1; 300 } 301 302 @Override 303 public int getItemViewType(int position) { 304 ensureCacheValid(); 305 int start = 0; 306 for (int i = 0; i < mSize; i++) { 307 int end = start + mPartitions[i].count; 308 if (position >= start && position < end) { 309 int offset = position - start; 310 if (mPartitions[i].hasHeader && offset == 0) { 311 return IGNORE_ITEM_VIEW_TYPE; 312 } 313 return getItemViewType(i, position); 314 } 315 start = end; 316 } 317 318 throw new ArrayIndexOutOfBoundsException(position); 319 } 320 321 public View getView(int position, View convertView, ViewGroup parent) { 322 ensureCacheValid(); 323 int start = 0; 324 for (int i = 0; i < mSize; i++) { 325 int end = start + mPartitions[i].count; 326 if (position >= start && position < end) { 327 int offset = position - start; 328 if (mPartitions[i].hasHeader) { 329 offset--; 330 } 331 View view; 332 if (offset == -1) { 333 view = getHeaderView(i, mPartitions[i].cursor, convertView, parent); 334 } else { 335 if (!mPartitions[i].cursor.moveToPosition(offset)) { 336 throw new IllegalStateException("Couldn't move cursor to position " 337 + offset); 338 } 339 view = getView(i, mPartitions[i].cursor, offset, convertView, parent); 340 } 341 if (view == null) { 342 throw new NullPointerException("View should not be null, partition: " + i 343 + " position: " + offset); 344 } 345 return view; 346 } 347 start = end; 348 } 349 350 throw new ArrayIndexOutOfBoundsException(position); 351 } 352 353 /** 354 * Returns the header view for the specified partition, creating one if needed. 355 */ 356 protected View getHeaderView(int partition, Cursor cursor, View convertView, 357 ViewGroup parent) { 358 View view = convertView != null 359 ? convertView 360 : newHeaderView(mContext, partition, cursor, parent); 361 bindHeaderView(view, partition, cursor); 362 return view; 363 } 364 365 /** 366 * Creates the header view for the specified partition. 367 */ 368 protected View newHeaderView(Context context, int partition, Cursor cursor, 369 ViewGroup parent) { 370 return null; 371 } 372 373 /** 374 * Binds the header view for the specified partition. 375 */ 376 protected void bindHeaderView(View view, int partition, Cursor cursor) { 377 } 378 379 /** 380 * Returns an item view for the specified partition, creating one if needed. 381 */ 382 protected View getView(int partition, Cursor cursor, int position, View convertView, 383 ViewGroup parent) { 384 View view; 385 if (convertView != null) { 386 view = convertView; 387 } else { 388 view = newView(mContext, partition, cursor, position, parent); 389 } 390 bindView(view, partition, cursor, position); 391 return view; 392 } 393 394 /** 395 * Creates an item view for the specified partition and position. Position 396 * corresponds directly to the current cursor position. 397 */ 398 protected abstract View newView(Context context, int partition, Cursor cursor, int position, 399 ViewGroup parent); 400 401 /** 402 * Binds an item view for the specified partition and position. Position 403 * corresponds directly to the current cursor position. 404 */ 405 protected abstract void bindView(View v, int partition, Cursor cursor, int position); 406 407 /** 408 * Returns a pre-positioned cursor for the specified list position. 409 */ 410 public Object getItem(int position) { 411 ensureCacheValid(); 412 int start = 0; 413 for (int i = 0; i < mSize; i++) { 414 int end = start + mPartitions[i].count; 415 if (position >= start && position < end) { 416 int offset = position - start; 417 if (mPartitions[i].hasHeader) { 418 offset--; 419 } 420 if (offset == -1) { 421 return null; 422 } 423 Cursor cursor = mPartitions[i].cursor; 424 cursor.moveToPosition(offset); 425 return cursor; 426 } 427 start = end; 428 } 429 430 return null; 431 } 432 433 /** 434 * Returns the item ID for the specified list position. 435 */ 436 public long getItemId(int position) { 437 ensureCacheValid(); 438 int start = 0; 439 for (int i = 0; i < mSize; i++) { 440 int end = start + mPartitions[i].count; 441 if (position >= start && position < end) { 442 int offset = position - start; 443 if (mPartitions[i].hasHeader) { 444 offset--; 445 } 446 if (offset == -1) { 447 return 0; 448 } 449 if (mPartitions[i].idColumnIndex == -1) { 450 return 0; 451 } 452 453 Cursor cursor = mPartitions[i].cursor; 454 if (cursor == null || cursor.isClosed() || !cursor.moveToPosition(offset)) { 455 return 0; 456 } 457 return cursor.getLong(mPartitions[i].idColumnIndex); 458 } 459 start = end; 460 } 461 462 return 0; 463 } 464 465 /** 466 * Returns false if any partition has a header. 467 */ 468 @Override 469 public boolean areAllItemsEnabled() { 470 for (int i = 0; i < mSize; i++) { 471 if (mPartitions[i].hasHeader) { 472 return false; 473 } 474 } 475 return true; 476 } 477 478 /** 479 * Returns true for all items except headers. 480 */ 481 @Override 482 public boolean isEnabled(int position) { 483 ensureCacheValid(); 484 int start = 0; 485 for (int i = 0; i < mSize; i++) { 486 int end = start + mPartitions[i].count; 487 if (position >= start && position < end) { 488 int offset = position - start; 489 if (mPartitions[i].hasHeader && offset == 0) { 490 return false; 491 } else { 492 return isEnabled(i, offset); 493 } 494 } 495 start = end; 496 } 497 498 return false; 499 } 500 501 /** 502 * Returns true if the item at the specified offset of the specified 503 * partition is selectable and clickable. 504 */ 505 protected boolean isEnabled(int partition, int position) { 506 return true; 507 } 508} 509