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