CompositeCursorAdapter.java revision 53f35c7c31f31db40b106cc0f7a51f5daf40a7fd
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 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) { 313 offset--; 314 } 315 if (offset == -1) { 316 return IGNORE_ITEM_VIEW_TYPE; 317 } else { 318 return getItemViewType(i, offset); 319 } 320 } 321 start = end; 322 } 323 324 throw new ArrayIndexOutOfBoundsException(position); 325 } 326 327 public View getView(int position, View convertView, ViewGroup parent) { 328 ensureCacheValid(); 329 int start = 0; 330 for (int i = 0; i < mSize; i++) { 331 int end = start + mPartitions[i].count; 332 if (position >= start && position < end) { 333 int offset = position - start; 334 if (mPartitions[i].hasHeader) { 335 offset--; 336 } 337 View view; 338 if (offset == -1) { 339 view = getHeaderView(i, mPartitions[i].cursor, convertView, parent); 340 } else { 341 if (!mPartitions[i].cursor.moveToPosition(offset)) { 342 throw new IllegalStateException("Couldn't move cursor to position " 343 + offset); 344 } 345 view = getView(i, mPartitions[i].cursor, offset, convertView, parent); 346 } 347 if (view == null) { 348 throw new NullPointerException("View should not be null, partition: " + i 349 + " position: " + offset); 350 } 351 return view; 352 } 353 start = end; 354 } 355 356 throw new ArrayIndexOutOfBoundsException(position); 357 } 358 359 /** 360 * Returns the header view for the specified partition, creating one if needed. 361 */ 362 protected View getHeaderView(int partition, Cursor cursor, View convertView, 363 ViewGroup parent) { 364 View view = convertView != null 365 ? convertView 366 : newHeaderView(mContext, partition, cursor, parent); 367 bindHeaderView(view, partition, cursor); 368 return view; 369 } 370 371 /** 372 * Creates the header view for the specified partition. 373 */ 374 protected View newHeaderView(Context context, int partition, Cursor cursor, 375 ViewGroup parent) { 376 return null; 377 } 378 379 /** 380 * Binds the header view for the specified partition. 381 */ 382 protected void bindHeaderView(View view, int partition, Cursor cursor) { 383 } 384 385 /** 386 * Returns an item view for the specified partition, creating one if needed. 387 */ 388 protected View getView(int partition, Cursor cursor, int position, View convertView, 389 ViewGroup parent) { 390 View view; 391 if (convertView != null) { 392 view = convertView; 393 } else { 394 view = newView(mContext, partition, cursor, position, parent); 395 } 396 bindView(view, partition, cursor, position); 397 return view; 398 } 399 400 /** 401 * Creates an item view for the specified partition and position. Position 402 * corresponds directly to the current cursor position. 403 */ 404 protected abstract View newView(Context context, int partition, Cursor cursor, int position, 405 ViewGroup parent); 406 407 /** 408 * Binds an item view for the specified partition and position. Position 409 * corresponds directly to the current cursor position. 410 */ 411 protected abstract void bindView(View v, int partition, Cursor cursor, int position); 412 413 /** 414 * Returns a pre-positioned cursor for the specified list position. 415 */ 416 public Object getItem(int position) { 417 ensureCacheValid(); 418 int start = 0; 419 for (int i = 0; i < mSize; i++) { 420 int end = start + mPartitions[i].count; 421 if (position >= start && position < end) { 422 int offset = position - start; 423 if (mPartitions[i].hasHeader) { 424 offset--; 425 } 426 if (offset == -1) { 427 return null; 428 } 429 Cursor cursor = mPartitions[i].cursor; 430 cursor.moveToPosition(offset); 431 return cursor; 432 } 433 start = end; 434 } 435 436 return null; 437 } 438 439 /** 440 * Returns the item ID for the specified list position. 441 */ 442 public long getItemId(int position) { 443 ensureCacheValid(); 444 int start = 0; 445 for (int i = 0; i < mSize; i++) { 446 int end = start + mPartitions[i].count; 447 if (position >= start && position < end) { 448 int offset = position - start; 449 if (mPartitions[i].hasHeader) { 450 offset--; 451 } 452 if (offset == -1) { 453 return 0; 454 } 455 if (mPartitions[i].idColumnIndex == -1) { 456 return 0; 457 } 458 459 Cursor cursor = mPartitions[i].cursor; 460 if (cursor == null || cursor.isClosed() || !cursor.moveToPosition(offset)) { 461 return 0; 462 } 463 return cursor.getLong(mPartitions[i].idColumnIndex); 464 } 465 start = end; 466 } 467 468 return 0; 469 } 470 471 /** 472 * Returns false if any partition has a header. 473 */ 474 @Override 475 public boolean areAllItemsEnabled() { 476 for (int i = 0; i < mSize; i++) { 477 if (mPartitions[i].hasHeader) { 478 return false; 479 } 480 } 481 return true; 482 } 483 484 /** 485 * Returns true for all items except headers. 486 */ 487 @Override 488 public boolean isEnabled(int position) { 489 ensureCacheValid(); 490 int start = 0; 491 for (int i = 0; i < mSize; i++) { 492 int end = start + mPartitions[i].count; 493 if (position >= start && position < end) { 494 int offset = position - start; 495 if (mPartitions[i].hasHeader && offset == 0) { 496 return false; 497 } else { 498 return isEnabled(i, offset); 499 } 500 } 501 start = end; 502 } 503 504 return false; 505 } 506 507 /** 508 * Returns true if the item at the specified offset of the specified 509 * partition is selectable and clickable. 510 */ 511 protected boolean isEnabled(int partition, int position) { 512 return true; 513 } 514 515 /** 516 * Enable or disable data change notifications. It may be a good idea to 517 * disable notifications before making changes to several partitions at once. 518 */ 519 public void setNotificationsEnabled(boolean flag) { 520 mNotificationsEnabled = flag; 521 if (flag && mNotificationNeeded) { 522 notifyDataSetChanged(); 523 } 524 } 525 526 @Override 527 public void notifyDataSetChanged() { 528 if (mNotificationsEnabled) { 529 mNotificationNeeded = false; 530 super.notifyDataSetChanged(); 531 } else { 532 mNotificationNeeded = true; 533 } 534 } 535} 536