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