1 /* 2 * Copyright (C) 2007 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 17 package android.widget; 18 19 import android.app.Activity; 20 import android.content.Context; 21 import android.database.ContentObserver; 22 import android.database.Cursor; 23 import android.database.DataSetObserver; 24 import android.os.Handler; 25 import android.util.Log; 26 import android.util.SparseArray; 27 import android.view.View; 28 import android.view.ViewGroup; 29 30 /** 31 * An adapter that exposes data from a series of {@link Cursor}s to an 32 * {@link ExpandableListView} widget. The top-level {@link Cursor} (that is 33 * given in the constructor) exposes the groups, while subsequent {@link Cursor}s 34 * returned from {@link #getChildrenCursor(Cursor)} expose children within a 35 * particular group. The Cursors must include a column named "_id" or this class 36 * will not work. 37 */ 38 public abstract class CursorTreeAdapter extends BaseExpandableListAdapter implements Filterable, 39 CursorFilter.CursorFilterClient { 40 private Context mContext; 41 private Handler mHandler; 42 private boolean mAutoRequery; 43 44 /** The cursor helper that is used to get the groups */ 45 MyCursorHelper mGroupCursorHelper; 46 47 /** 48 * The map of a group position to the group's children cursor helper (the 49 * cursor helper that is used to get the children for that group) 50 */ 51 SparseArray<MyCursorHelper> mChildrenCursorHelpers; 52 53 // Filter related 54 CursorFilter mCursorFilter; 55 FilterQueryProvider mFilterQueryProvider; 56 57 /** 58 * Constructor. The adapter will call {@link Cursor#requery()} on the cursor whenever 59 * it changes so that the most recent data is always displayed. 60 * 61 * @param cursor The cursor from which to get the data for the groups. 62 */ 63 public CursorTreeAdapter(Cursor cursor, Context context) { 64 init(cursor, context, true); 65 } 66 67 /** 68 * Constructor. 69 * 70 * @param cursor The cursor from which to get the data for the groups. 71 * @param context The context 72 * @param autoRequery If true the adapter will call {@link Cursor#requery()} 73 * on the cursor whenever it changes so the most recent data is 74 * always displayed. 75 */ 76 public CursorTreeAdapter(Cursor cursor, Context context, boolean autoRequery) { 77 init(cursor, context, autoRequery); 78 } 79 80 private void init(Cursor cursor, Context context, boolean autoRequery) { 81 mContext = context; 82 mHandler = new Handler(); 83 mAutoRequery = autoRequery; 84 85 mGroupCursorHelper = new MyCursorHelper(cursor); 86 mChildrenCursorHelpers = new SparseArray<MyCursorHelper>(); 87 } 88 89 /** 90 * Gets the cursor helper for the children in the given group. 91 * 92 * @param groupPosition The group whose children will be returned 93 * @param requestCursor Whether to request a Cursor via 94 * {@link #getChildrenCursor(Cursor)} (true), or to assume a call 95 * to {@link #setChildrenCursor(int, Cursor)} will happen shortly 96 * (false). 97 * @return The cursor helper for the children of the given group 98 */ 99 synchronized MyCursorHelper getChildrenCursorHelper(int groupPosition, boolean requestCursor) { 100 MyCursorHelper cursorHelper = mChildrenCursorHelpers.get(groupPosition); 101 102 if (cursorHelper == null) { 103 if (mGroupCursorHelper.moveTo(groupPosition) == null) return null; 104 105 final Cursor cursor = getChildrenCursor(mGroupCursorHelper.getCursor()); 106 cursorHelper = new MyCursorHelper(cursor); 107 mChildrenCursorHelpers.put(groupPosition, cursorHelper); 108 } 109 110 return cursorHelper; 111 } 112 113 /** 114 * Gets the Cursor for the children at the given group. Subclasses must 115 * implement this method to return the children data for a particular group. 116 * <p> 117 * If you want to asynchronously query a provider to prevent blocking the 118 * UI, it is possible to return null and at a later time call 119 * {@link #setChildrenCursor(int, Cursor)}. 120 * <p> 121 * It is your responsibility to manage this Cursor through the Activity 122 * lifecycle. It is a good idea to use {@link Activity#managedQuery} which 123 * will handle this for you. In some situations, the adapter will deactivate 124 * the Cursor on its own, but this will not always be the case, so please 125 * ensure the Cursor is properly managed. 126 * 127 * @param groupCursor The cursor pointing to the group whose children cursor 128 * should be returned 129 * @return The cursor for the children of a particular group, or null. 130 */ 131 abstract protected Cursor getChildrenCursor(Cursor groupCursor); 132 133 /** 134 * Sets the group Cursor. 135 * 136 * @param cursor The Cursor to set for the group. If there is an existing cursor 137 * it will be closed. 138 */ 139 public void setGroupCursor(Cursor cursor) { 140 mGroupCursorHelper.changeCursor(cursor, false); 141 } 142 143 /** 144 * Sets the children Cursor for a particular group. If there is an existing cursor 145 * it will be closed. 146 * <p> 147 * This is useful when asynchronously querying to prevent blocking the UI. 148 * 149 * @param groupPosition The group whose children are being set via this Cursor. 150 * @param childrenCursor The Cursor that contains the children of the group. 151 */ 152 public void setChildrenCursor(int groupPosition, Cursor childrenCursor) { 153 154 /* 155 * Don't request a cursor from the subclass, instead we will be setting 156 * the cursor ourselves. 157 */ 158 MyCursorHelper childrenCursorHelper = getChildrenCursorHelper(groupPosition, false); 159 160 /* 161 * Don't release any cursor since we know exactly what data is changing 162 * (this cursor, which is still valid). 163 */ 164 childrenCursorHelper.changeCursor(childrenCursor, false); 165 } 166 167 public Cursor getChild(int groupPosition, int childPosition) { 168 // Return this group's children Cursor pointing to the particular child 169 return getChildrenCursorHelper(groupPosition, true).moveTo(childPosition); 170 } 171 172 public long getChildId(int groupPosition, int childPosition) { 173 return getChildrenCursorHelper(groupPosition, true).getId(childPosition); 174 } 175 176 public int getChildrenCount(int groupPosition) { 177 MyCursorHelper helper = getChildrenCursorHelper(groupPosition, true); 178 return (mGroupCursorHelper.isValid() && helper != null) ? helper.getCount() : 0; 179 } 180 181 public Cursor getGroup(int groupPosition) { 182 // Return the group Cursor pointing to the given group 183 return mGroupCursorHelper.moveTo(groupPosition); 184 } 185 186 public int getGroupCount() { 187 return mGroupCursorHelper.getCount(); 188 } 189 190 public long getGroupId(int groupPosition) { 191 return mGroupCursorHelper.getId(groupPosition); 192 } 193 194 public View getGroupView(int groupPosition, boolean isExpanded, View convertView, 195 ViewGroup parent) { 196 Cursor cursor = mGroupCursorHelper.moveTo(groupPosition); 197 if (cursor == null) { 198 throw new IllegalStateException("this should only be called when the cursor is valid"); 199 } 200 201 View v; 202 if (convertView == null) { 203 v = newGroupView(mContext, cursor, isExpanded, parent); 204 } else { 205 v = convertView; 206 } 207 bindGroupView(v, mContext, cursor, isExpanded); 208 return v; 209 } 210 211 /** 212 * Makes a new group view to hold the group data pointed to by cursor. 213 * 214 * @param context Interface to application's global information 215 * @param cursor The group cursor from which to get the data. The cursor is 216 * already moved to the correct position. 217 * @param isExpanded Whether the group is expanded. 218 * @param parent The parent to which the new view is attached to 219 * @return The newly created view. 220 */ 221 protected abstract View newGroupView(Context context, Cursor cursor, boolean isExpanded, 222 ViewGroup parent); 223 224 /** 225 * Bind an existing view to the group data pointed to by cursor. 226 * 227 * @param view Existing view, returned earlier by newGroupView. 228 * @param context Interface to application's global information 229 * @param cursor The cursor from which to get the data. The cursor is 230 * already moved to the correct position. 231 * @param isExpanded Whether the group is expanded. 232 */ 233 protected abstract void bindGroupView(View view, Context context, Cursor cursor, 234 boolean isExpanded); 235 236 public View getChildView(int groupPosition, int childPosition, boolean isLastChild, 237 View convertView, ViewGroup parent) { 238 MyCursorHelper cursorHelper = getChildrenCursorHelper(groupPosition, true); 239 240 Cursor cursor = cursorHelper.moveTo(childPosition); 241 if (cursor == null) { 242 throw new IllegalStateException("this should only be called when the cursor is valid"); 243 } 244 245 View v; 246 if (convertView == null) { 247 v = newChildView(mContext, cursor, isLastChild, parent); 248 } else { 249 v = convertView; 250 } 251 bindChildView(v, mContext, cursor, isLastChild); 252 return v; 253 } 254 255 /** 256 * Makes a new child view to hold the data pointed to by cursor. 257 * 258 * @param context Interface to application's global information 259 * @param cursor The cursor from which to get the data. The cursor is 260 * already moved to the correct position. 261 * @param isLastChild Whether the child is the last child within its group. 262 * @param parent The parent to which the new view is attached to 263 * @return the newly created view. 264 */ 265 protected abstract View newChildView(Context context, Cursor cursor, boolean isLastChild, 266 ViewGroup parent); 267 268 /** 269 * Bind an existing view to the child data pointed to by cursor 270 * 271 * @param view Existing view, returned earlier by newChildView 272 * @param context Interface to application's global information 273 * @param cursor The cursor from which to get the data. The cursor is 274 * already moved to the correct position. 275 * @param isLastChild Whether the child is the last child within its group. 276 */ 277 protected abstract void bindChildView(View view, Context context, Cursor cursor, 278 boolean isLastChild); 279 280 public boolean isChildSelectable(int groupPosition, int childPosition) { 281 return true; 282 } 283 284 public boolean hasStableIds() { 285 return true; 286 } 287 288 private synchronized void releaseCursorHelpers() { 289 for (int pos = mChildrenCursorHelpers.size() - 1; pos >= 0; pos--) { 290 mChildrenCursorHelpers.valueAt(pos).deactivate(); 291 } 292 293 mChildrenCursorHelpers.clear(); 294 } 295 296 @Override 297 public void notifyDataSetChanged() { 298 notifyDataSetChanged(true); 299 } 300 301 /** 302 * Notifies a data set change, but with the option of not releasing any 303 * cached cursors. 304 * 305 * @param releaseCursors Whether to release and deactivate any cached 306 * cursors. 307 */ 308 public void notifyDataSetChanged(boolean releaseCursors) { 309 310 if (releaseCursors) { 311 releaseCursorHelpers(); 312 } 313 314 super.notifyDataSetChanged(); 315 } 316 317 @Override 318 public void notifyDataSetInvalidated() { 319 releaseCursorHelpers(); 320 super.notifyDataSetInvalidated(); 321 } 322 323 @Override 324 public void onGroupCollapsed(int groupPosition) { 325 deactivateChildrenCursorHelper(groupPosition); 326 } 327 328 /** 329 * Deactivates the Cursor and removes the helper from cache. 330 * 331 * @param groupPosition The group whose children Cursor and helper should be 332 * deactivated. 333 */ 334 synchronized void deactivateChildrenCursorHelper(int groupPosition) { 335 MyCursorHelper cursorHelper = getChildrenCursorHelper(groupPosition, true); 336 mChildrenCursorHelpers.remove(groupPosition); 337 cursorHelper.deactivate(); 338 } 339 340 /** 341 * @see CursorAdapter#convertToString(Cursor) 342 */ 343 public String convertToString(Cursor cursor) { 344 return cursor == null ? "" : cursor.toString(); 345 } 346 347 /** 348 * @see CursorAdapter#runQueryOnBackgroundThread(CharSequence) 349 */ 350 public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 351 if (mFilterQueryProvider != null) { 352 return mFilterQueryProvider.runQuery(constraint); 353 } 354 355 return mGroupCursorHelper.getCursor(); 356 } 357 358 public Filter getFilter() { 359 if (mCursorFilter == null) { 360 mCursorFilter = new CursorFilter(this); 361 } 362 return mCursorFilter; 363 } 364 365 /** 366 * @see CursorAdapter#getFilterQueryProvider() 367 */ 368 public FilterQueryProvider getFilterQueryProvider() { 369 return mFilterQueryProvider; 370 } 371 372 /** 373 * @see CursorAdapter#setFilterQueryProvider(FilterQueryProvider) 374 */ 375 public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) { 376 mFilterQueryProvider = filterQueryProvider; 377 } 378 379 /** 380 * @see CursorAdapter#changeCursor(Cursor) 381 */ 382 public void changeCursor(Cursor cursor) { 383 mGroupCursorHelper.changeCursor(cursor, true); 384 } 385 386 /** 387 * @see CursorAdapter#getCursor() 388 */ 389 public Cursor getCursor() { 390 return mGroupCursorHelper.getCursor(); 391 } 392 393 /** 394 * Helper class for Cursor management: 395 * <li> Data validity 396 * <li> Funneling the content and data set observers from a Cursor to a 397 * single data set observer for widgets 398 * <li> ID from the Cursor for use in adapter IDs 399 * <li> Swapping cursors but maintaining other metadata 400 */ 401 class MyCursorHelper { 402 private Cursor mCursor; 403 private boolean mDataValid; 404 private int mRowIDColumn; 405 private MyContentObserver mContentObserver; 406 private MyDataSetObserver mDataSetObserver; 407 408 MyCursorHelper(Cursor cursor) { 409 final boolean cursorPresent = cursor != null; 410 mCursor = cursor; 411 mDataValid = cursorPresent; 412 mRowIDColumn = cursorPresent ? cursor.getColumnIndex("_id") : -1; 413 mContentObserver = new MyContentObserver(); 414 mDataSetObserver = new MyDataSetObserver(); 415 if (cursorPresent) { 416 cursor.registerContentObserver(mContentObserver); 417 cursor.registerDataSetObserver(mDataSetObserver); 418 } 419 } 420 421 Cursor getCursor() { 422 return mCursor; 423 } 424 425 int getCount() { 426 if (mDataValid && mCursor != null) { 427 return mCursor.getCount(); 428 } else { 429 return 0; 430 } 431 } 432 433 long getId(int position) { 434 if (mDataValid && mCursor != null) { 435 if (mCursor.moveToPosition(position)) { 436 return mCursor.getLong(mRowIDColumn); 437 } else { 438 return 0; 439 } 440 } else { 441 return 0; 442 } 443 } 444 445 Cursor moveTo(int position) { 446 if (mDataValid && (mCursor != null) && mCursor.moveToPosition(position)) { 447 return mCursor; 448 } else { 449 return null; 450 } 451 } 452 453 void changeCursor(Cursor cursor, boolean releaseCursors) { 454 if (cursor == mCursor) return; 455 456 deactivate(); 457 mCursor = cursor; 458 if (cursor != null) { 459 cursor.registerContentObserver(mContentObserver); 460 cursor.registerDataSetObserver(mDataSetObserver); 461 mRowIDColumn = cursor.getColumnIndex("_id"); 462 mDataValid = true; 463 // notify the observers about the new cursor 464 notifyDataSetChanged(releaseCursors); 465 } else { 466 mRowIDColumn = -1; 467 mDataValid = false; 468 // notify the observers about the lack of a data set 469 notifyDataSetInvalidated(); 470 } 471 } 472 473 void deactivate() { 474 if (mCursor == null) { 475 return; 476 } 477 478 mCursor.unregisterContentObserver(mContentObserver); 479 mCursor.unregisterDataSetObserver(mDataSetObserver); 480 mCursor.close(); 481 mCursor = null; 482 } 483 484 boolean isValid() { 485 return mDataValid && mCursor != null; 486 } 487 488 private class MyContentObserver extends ContentObserver { 489 public MyContentObserver() { 490 super(mHandler); 491 } 492 493 @Override 494 public boolean deliverSelfNotifications() { 495 return true; 496 } 497 498 @Override 499 public void onChange(boolean selfChange) { 500 if (mAutoRequery && mCursor != null) { 501 if (false) Log.v("Cursor", "Auto requerying " + mCursor + 502 " due to update"); 503 mDataValid = mCursor.requery(); 504 } 505 } 506 } 507 508 private class MyDataSetObserver extends DataSetObserver { 509 @Override 510 public void onChanged() { 511 mDataValid = true; 512 notifyDataSetChanged(); 513 } 514 515 @Override 516 public void onInvalidated() { 517 mDataValid = false; 518 notifyDataSetInvalidated(); 519 } 520 } 521 } 522 } 523