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 /** 25 * A general purpose adapter that is composed of multiple cursors. It just 26 * appends them in the order they are added. 27 */ 28 public 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 Partition(boolean showIfEmpty, boolean hasHeader)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 */ getShowIfEmpty()48 public boolean getShowIfEmpty() { 49 return showIfEmpty; 50 } 51 getHasHeader()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 CompositeCursorAdapter(Context context)65 public CompositeCursorAdapter(Context context) { 66 this(context, INITIAL_CAPACITY); 67 } 68 CompositeCursorAdapter(Context context, int initialCapacity)69 public CompositeCursorAdapter(Context context, int initialCapacity) { 70 mContext = context; 71 mPartitions = new Partition[INITIAL_CAPACITY]; 72 } 73 getContext()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 */ addPartition(boolean showIfEmpty, boolean hasHeader)83 public void addPartition(boolean showIfEmpty, boolean hasHeader) { 84 addPartition(new Partition(showIfEmpty, hasHeader)); 85 } 86 addPartition(Partition partition)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 removePartition(int partitionIndex)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 */ clearPartitions()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 */ close()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 setHasHeader(int partitionIndex, boolean flag)139 public void setHasHeader(int partitionIndex, boolean flag) { 140 mPartitions[partitionIndex].hasHeader = flag; 141 invalidate(); 142 } 143 setShowIfEmpty(int partitionIndex, boolean flag)144 public void setShowIfEmpty(int partitionIndex, boolean flag) { 145 mPartitions[partitionIndex].showIfEmpty = flag; 146 invalidate(); 147 } 148 getPartition(int partitionIndex)149 public Partition getPartition(int partitionIndex) { 150 if (partitionIndex >= mSize) { 151 throw new ArrayIndexOutOfBoundsException(partitionIndex); 152 } 153 return mPartitions[partitionIndex]; 154 } 155 invalidate()156 protected void invalidate() { 157 mCacheValid = false; 158 } 159 getPartitionCount()160 public int getPartitionCount() { 161 return mSize; 162 } 163 ensureCacheValid()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 */ hasHeader(int partition)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 */ getCount()195 public int getCount() { 196 ensureCacheValid(); 197 return mCount; 198 } 199 200 /** 201 * Returns the cursor for the given partition 202 */ getCursor(int partition)203 public Cursor getCursor(int partition) { 204 return mPartitions[partition].cursor; 205 } 206 207 /** 208 * Changes the cursor for an individual partition. 209 */ changeCursor(int partition, Cursor cursor)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 */ isPartitionEmpty(int partition)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 */ getPartitionForPosition(int position)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 */ getOffsetInPartition(int position)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 */ getPositionForPartition(int partition)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 getViewTypeCount()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 */ getItemViewTypeCount()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 */ getItemViewType(int partition, int position)300 protected int getItemViewType(int partition, int position) { 301 return 1; 302 } 303 304 @Override getItemViewType(int position)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 && offset == 0) { 313 return IGNORE_ITEM_VIEW_TYPE; 314 } 315 return getItemViewType(i, position); 316 } 317 start = end; 318 } 319 320 throw new ArrayIndexOutOfBoundsException(position); 321 } 322 getView(int position, View convertView, ViewGroup parent)323 public View getView(int position, View convertView, ViewGroup parent) { 324 ensureCacheValid(); 325 int start = 0; 326 for (int i = 0; i < mSize; i++) { 327 int end = start + mPartitions[i].count; 328 if (position >= start && position < end) { 329 int offset = position - start; 330 if (mPartitions[i].hasHeader) { 331 offset--; 332 } 333 View view; 334 if (offset == -1) { 335 view = getHeaderView(i, mPartitions[i].cursor, convertView, parent); 336 } else { 337 if (!mPartitions[i].cursor.moveToPosition(offset)) { 338 throw new IllegalStateException("Couldn't move cursor to position " 339 + offset); 340 } 341 view = getView(i, mPartitions[i].cursor, offset, convertView, parent); 342 } 343 if (view == null) { 344 throw new NullPointerException("View should not be null, partition: " + i 345 + " position: " + offset); 346 } 347 return view; 348 } 349 start = end; 350 } 351 352 throw new ArrayIndexOutOfBoundsException(position); 353 } 354 355 /** 356 * Returns the header view for the specified partition, creating one if needed. 357 */ getHeaderView(int partition, Cursor cursor, View convertView, ViewGroup parent)358 protected View getHeaderView(int partition, Cursor cursor, View convertView, 359 ViewGroup parent) { 360 View view = convertView != null 361 ? convertView 362 : newHeaderView(mContext, partition, cursor, parent); 363 bindHeaderView(view, partition, cursor); 364 return view; 365 } 366 367 /** 368 * Creates the header view for the specified partition. 369 */ newHeaderView(Context context, int partition, Cursor cursor, ViewGroup parent)370 protected View newHeaderView(Context context, int partition, Cursor cursor, 371 ViewGroup parent) { 372 return null; 373 } 374 375 /** 376 * Binds the header view for the specified partition. 377 */ bindHeaderView(View view, int partition, Cursor cursor)378 protected void bindHeaderView(View view, int partition, Cursor cursor) { 379 } 380 381 /** 382 * Returns an item view for the specified partition, creating one if needed. 383 */ getView(int partition, Cursor cursor, int position, View convertView, ViewGroup parent)384 protected View getView(int partition, Cursor cursor, int position, View convertView, 385 ViewGroup parent) { 386 View view; 387 if (convertView != null) { 388 view = convertView; 389 } else { 390 view = newView(mContext, partition, cursor, position, parent); 391 } 392 bindView(view, partition, cursor, position); 393 return view; 394 } 395 396 /** 397 * Creates an item view for the specified partition and position. Position 398 * corresponds directly to the current cursor position. 399 */ newView(Context context, int partition, Cursor cursor, int position, ViewGroup parent)400 protected abstract View newView(Context context, int partition, Cursor cursor, int position, 401 ViewGroup parent); 402 403 /** 404 * Binds an item view for the specified partition and position. Position 405 * corresponds directly to the current cursor position. 406 */ bindView(View v, int partition, Cursor cursor, int position)407 protected abstract void bindView(View v, int partition, Cursor cursor, int position); 408 409 /** 410 * Returns a pre-positioned cursor for the specified list position. 411 */ getItem(int position)412 public Object getItem(int position) { 413 ensureCacheValid(); 414 int start = 0; 415 for (int i = 0; i < mSize; i++) { 416 int end = start + mPartitions[i].count; 417 if (position >= start && position < end) { 418 int offset = position - start; 419 if (mPartitions[i].hasHeader) { 420 offset--; 421 } 422 if (offset == -1) { 423 return null; 424 } 425 Cursor cursor = mPartitions[i].cursor; 426 cursor.moveToPosition(offset); 427 return cursor; 428 } 429 start = end; 430 } 431 432 return null; 433 } 434 435 /** 436 * Returns the item ID for the specified list position. 437 */ getItemId(int position)438 public long getItemId(int position) { 439 ensureCacheValid(); 440 int start = 0; 441 for (int i = 0; i < mSize; i++) { 442 int end = start + mPartitions[i].count; 443 if (position >= start && position < end) { 444 int offset = position - start; 445 if (mPartitions[i].hasHeader) { 446 offset--; 447 } 448 if (offset == -1) { 449 return 0; 450 } 451 if (mPartitions[i].idColumnIndex == -1) { 452 return 0; 453 } 454 455 Cursor cursor = mPartitions[i].cursor; 456 if (cursor == null || cursor.isClosed() || !cursor.moveToPosition(offset)) { 457 return 0; 458 } 459 return cursor.getLong(mPartitions[i].idColumnIndex); 460 } 461 start = end; 462 } 463 464 return 0; 465 } 466 467 /** 468 * Returns false if any partition has a header. 469 */ 470 @Override areAllItemsEnabled()471 public boolean areAllItemsEnabled() { 472 for (int i = 0; i < mSize; i++) { 473 if (mPartitions[i].hasHeader) { 474 return false; 475 } 476 } 477 return true; 478 } 479 480 /** 481 * Returns true for all items except headers. 482 */ 483 @Override isEnabled(int position)484 public boolean isEnabled(int position) { 485 ensureCacheValid(); 486 int start = 0; 487 for (int i = 0; i < mSize; i++) { 488 int end = start + mPartitions[i].count; 489 if (position >= start && position < end) { 490 int offset = position - start; 491 if (mPartitions[i].hasHeader && offset == 0) { 492 return false; 493 } else { 494 return isEnabled(i, offset); 495 } 496 } 497 start = end; 498 } 499 500 return false; 501 } 502 503 /** 504 * Returns true if the item at the specified offset of the specified 505 * partition is selectable and clickable. 506 */ isEnabled(int partition, int position)507 protected boolean isEnabled(int partition, int position) { 508 return true; 509 } 510 511 /** 512 * Enable or disable data change notifications. It may be a good idea to 513 * disable notifications before making changes to several partitions at once. 514 */ setNotificationsEnabled(boolean flag)515 public void setNotificationsEnabled(boolean flag) { 516 mNotificationsEnabled = flag; 517 if (flag && mNotificationNeeded) { 518 notifyDataSetChanged(); 519 } 520 } 521 522 @Override notifyDataSetChanged()523 public void notifyDataSetChanged() { 524 if (mNotificationsEnabled) { 525 mNotificationNeeded = false; 526 super.notifyDataSetChanged(); 527 } else { 528 mNotificationNeeded = true; 529 } 530 } 531 } 532