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 && !cursor.isClosed()) { 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