ConversationCursor.java revision 995fff5b8367f91da95850b662ddd65a2e08e9ea
1/******************************************************************************* 2 * Copyright (C) 2012 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 *******************************************************************************/ 17 18package com.android.mail.browse; 19 20import android.app.Activity; 21import android.content.ContentProvider; 22import android.content.ContentProviderOperation; 23import android.content.ContentResolver; 24import android.content.ContentValues; 25import android.content.OperationApplicationException; 26import android.database.CharArrayBuffer; 27import android.database.ContentObserver; 28import android.database.Cursor; 29import android.database.DataSetObserver; 30import android.net.Uri; 31import android.os.Bundle; 32import android.os.Handler; 33import android.os.Looper; 34import android.os.RemoteException; 35import android.util.Log; 36 37import com.android.mail.providers.Conversation; 38import com.android.mail.providers.UIProvider; 39 40import java.util.ArrayList; 41import java.util.HashMap; 42import java.util.Iterator; 43import java.util.List; 44 45/** 46 * ConversationCursor is a wrapper around a conversation list cursor that provides update/delete 47 * caching for quick UI response. This is effectively a singleton class, as the cache is 48 * implemented as a static HashMap. 49 */ 50public final class ConversationCursor implements Cursor { 51 private static final String TAG = "ConversationCursor"; 52 private static final boolean DEBUG = true; // STOPSHIP Set to false before shipping 53 54 // The authority of our conversation provider (a forwarding provider) 55 // This string must match the declaration in AndroidManifest.xml 56 private static final String sAuthority = "com.android.mail.conversation.provider"; 57 58 // The cursor instantiator's activity 59 private static Activity sActivity; 60 // The cursor underlying the caching cursor 61 private static Cursor sUnderlyingCursor; 62 // The new cursor obtained via a requery 63 private static Cursor sRequeryCursor; 64 // A mapping from Uri to updated ContentValues 65 private static HashMap<String, ContentValues> sCacheMap = new HashMap<String, ContentValues>(); 66 // Cache map lock (will be used only very briefly - few ms at most) 67 private static Object sCacheMapLock = new Object(); 68 // A deleted row is indicated by the presence of DELETED_COLUMN in the cache map 69 private static final String DELETED_COLUMN = "__deleted__"; 70 // An row cached during a requery is indicated by the presence of REQUERY_COLUMN in the map 71 private static final String REQUERY_COLUMN = "__requery__"; 72 // A sentinel value for the "index" of the deleted column; it's an int that is otherwise invalid 73 private static final int DELETED_COLUMN_INDEX = -1; 74 // The current conversation cursor 75 private static ConversationCursor sConversationCursor; 76 // The index of the Uri whose data is reflected in the cached row 77 // Updates/Deletes to this Uri are cached 78 private static int sUriColumnIndex; 79 // The listener registered for this cursor 80 private static ConversationListener sListener; 81 // The ConversationProvider instance 82 private static ConversationProvider sProvider; 83 // Set when we're in the middle of a refresh of the underlying cursor 84 private static boolean sRefreshInProgress = false; 85 // Set when we've sent refreshReady() to listeners 86 private static boolean sRefreshReady = false; 87 // Set when we've sent refreshRequired() to listeners 88 private static boolean sRefreshRequired = false; 89 // Our sequence count (for changes sent to underlying provider) 90 private static int sSequence = 0; 91 92 // Column names for this cursor 93 private final String[] mColumnNames; 94 // The resolver for the cursor instantiator's context 95 private static ContentResolver mResolver; 96 // An observer on the underlying cursor (so we can detect changes from outside the UI) 97 private final CursorObserver mCursorObserver; 98 // Whether our observer is currently registered with the underlying cursor 99 private boolean mCursorObserverRegistered = false; 100 101 // The current position of the cursor 102 private int mPosition = -1; 103 // The number of cached deletions from this cursor (used to quickly generate an accurate count) 104 private static int sDeletedCount = 0; 105 106 // Parameters passed to the underlying query 107 private static Uri qUri; 108 private static String[] qProjection; 109 private static String qSelection; 110 private static String[] qSelectionArgs; 111 private static String qSortOrder; 112 113 private ConversationCursor(Cursor cursor, Activity activity, String messageListColumn) { 114 sActivity = activity; 115 mResolver = activity.getContentResolver(); 116 sConversationCursor = this; 117 sUnderlyingCursor = cursor; 118 mCursorObserver = new CursorObserver(); 119 resetCursor(null); 120 mColumnNames = cursor.getColumnNames(); 121 sUriColumnIndex = cursor.getColumnIndex(messageListColumn); 122 if (sUriColumnIndex < 0) { 123 throw new IllegalArgumentException("Cursor must include a message list column"); 124 } 125 } 126 127 /** 128 * Create a ConversationCursor; this should be called by the ListActivity using that cursor 129 * @param activity the activity creating the cursor 130 * @param messageListColumn the column used for individual cursor items 131 * @param uri the query uri 132 * @param projection the query projecion 133 * @param selection the query selection 134 * @param selectionArgs the query selection args 135 * @param sortOrder the query sort order 136 * @return a ConversationCursor 137 */ 138 public static ConversationCursor create(Activity activity, String messageListColumn, Uri uri, 139 String[] projection, String selection, String[] selectionArgs, String sortOrder) { 140 qUri = uri; 141 qProjection = projection; 142 qSelection = selection; 143 qSelectionArgs = selectionArgs; 144 qSortOrder = sortOrder; 145 Cursor cursor = activity.getContentResolver().query(uri, projection, selection, 146 selectionArgs, sortOrder); 147 return new ConversationCursor(cursor, activity, messageListColumn); 148 } 149 150 /** 151 * Return whether the uri string (message list uri) is in the underlying cursor 152 * @param uriString the uri string we're looking for 153 * @return true if the uri string is in the cursor; false otherwise 154 */ 155 private boolean isInUnderlyingCursor(String uriString) { 156 sUnderlyingCursor.moveToPosition(-1); 157 while (sUnderlyingCursor.moveToNext()) { 158 if (uriString.equals(sUnderlyingCursor.getString(sUriColumnIndex))) { 159 return true; 160 } 161 } 162 return false; 163 } 164 165 /** 166 * Reset the cursor; this involves clearing out our cache map and resetting our various counts 167 * The cursor should be reset whenever we get fresh data from the underlying cursor. The cache 168 * is locked during the reset, which will block the UI, but for only a very short time 169 * (estimated at a few ms, but we can profile this; remember that the cache will usually 170 * be empty or have a few entries) 171 */ 172 private void resetCursor(Cursor newCursor) { 173 // Temporary, log time for reset 174 long startTime = System.currentTimeMillis(); 175 if (DEBUG) { 176 Log.d(TAG, "[--resetCursor--]"); 177 } 178 synchronized (sCacheMapLock) { 179 // Walk through the cache. Here are the cases: 180 // 1) The entry isn't marked with REQUERY - remove it from the cache. If DELETED is 181 // set, decrement the deleted count 182 // 2) The REQUERY entry is still in the UP 183 // 2a) The REQUERY entry isn't DELETED; we're good, and the client change will remain 184 // (i.e. client wins, it's on its way to the UP) 185 // 2b) The REQUERY entry is DELETED; we're good (client change remains, it's on 186 // its way to the UP) 187 // 3) the REQUERY was deleted on the server (sheesh; this would be bizarre timing!) - 188 // we need to throw the item out of the cache 189 // So ... the only interesting case is #3, we need to look for remaining deleted items 190 // and see if they're still in the UP 191 Iterator<HashMap.Entry<String, ContentValues>> iter = sCacheMap.entrySet().iterator(); 192 while (iter.hasNext()) { 193 HashMap.Entry<String, ContentValues> entry = iter.next(); 194 ContentValues values = entry.getValue(); 195 if (values.containsKey(REQUERY_COLUMN) && isInUnderlyingCursor(entry.getKey())) { 196 // If we're in a requery and we're still around, remove the requery key 197 // We're good here, the cached change (delete/update) is on its way to UP 198 values.remove(REQUERY_COLUMN); 199 } else { 200 // Keep the deleted count up-to-date; remove the cache entry 201 if (values.containsKey(DELETED_COLUMN)) { 202 sDeletedCount--; 203 } 204 // Remove the entry 205 iter.remove(); 206 } 207 } 208 209 // Swap cursor 210 if (newCursor != null) { 211 close(); 212 sUnderlyingCursor = newCursor; 213 } 214 215 mPosition = -1; 216 sUnderlyingCursor.moveToPosition(mPosition); 217 if (!mCursorObserverRegistered) { 218 sUnderlyingCursor.registerContentObserver(mCursorObserver); 219 mCursorObserverRegistered = true; 220 } 221 sRefreshRequired = false; 222 } 223 Log.d(TAG, "resetCache time: " + ((System.currentTimeMillis() - startTime)) + "ms"); 224 } 225 226 /** 227 * Set the listener for this cursor; we'll notify it when our data changes 228 */ 229 public void setListener(ConversationListener listener) { 230 sListener = listener; 231 } 232 233 /** 234 * Generate a forwarding Uri to ConversationProvider from an original Uri. We do this by 235 * changing the authority to ours, but otherwise leaving the Uri intact. 236 * NOTE: This won't handle query parameters, so the functionality will need to be added if 237 * parameters are used in the future 238 * @param uri the uri 239 * @return a forwarding uri to ConversationProvider 240 */ 241 private static String uriToCachingUriString (Uri uri) { 242 String provider = uri.getAuthority(); 243 return uri.getScheme() + "://" + sAuthority + "/" + provider + uri.getPath(); 244 } 245 246 /** 247 * Regenerate the original Uri from a forwarding (ConversationProvider) Uri 248 * NOTE: See note above for uriToCachingUri 249 * @param uri the forwarding Uri 250 * @return the original Uri 251 */ 252 private static Uri uriFromCachingUri(Uri uri) { 253 List<String> path = uri.getPathSegments(); 254 Uri.Builder builder = new Uri.Builder().scheme(uri.getScheme()).authority(path.get(0)); 255 for (int i = 1; i < path.size(); i++) { 256 builder.appendPath(path.get(i)); 257 } 258 return builder.build(); 259 } 260 261 /** 262 * Cache a column name/value pair for a given Uri 263 * @param uriString the Uri for which the column name/value pair applies 264 * @param columnName the column name 265 * @param value the value to be cached 266 */ 267 private static void cacheValue(String uriString, String columnName, Object value) { 268 synchronized (sCacheMapLock) { 269 // Get the map for our uri 270 ContentValues map = sCacheMap.get(uriString); 271 // Create one if necessary 272 if (map == null) { 273 map = new ContentValues(); 274 sCacheMap.put(uriString, map); 275 } 276 // If we're caching a deletion, add to our count 277 if ((columnName == DELETED_COLUMN) && (map.get(columnName) == null)) { 278 sDeletedCount++; 279 if (DEBUG) { 280 Log.d(TAG, "Deleted " + uriString); 281 } 282 } 283 // ContentValues has no generic "put", so we must test. For now, the only classes of 284 // values implemented are Boolean/Integer/String, though others are trivially added 285 if (value instanceof Boolean) { 286 map.put(columnName, ((Boolean) value).booleanValue() ? 1 : 0); 287 } else if (value instanceof Integer) { 288 map.put(columnName, (Integer) value); 289 } else if (value instanceof String) { 290 map.put(columnName, (String) value); 291 } else { 292 String cname = value.getClass().getName(); 293 throw new IllegalArgumentException("Value class not compatible with cache: " 294 + cname); 295 } 296 if (sRefreshInProgress) { 297 map.put(REQUERY_COLUMN, 1); 298 } 299 if (DEBUG && (columnName != DELETED_COLUMN)) { 300 Log.d(TAG, "Caching value for " + uriString + ": " + columnName); 301 } 302 } 303 } 304 305 /** 306 * Get the cached value for the provided column; we special case -1 as the "deleted" column 307 * @param columnIndex the index of the column whose cached value we want to retrieve 308 * @return the cached value for this column, or null if there is none 309 */ 310 private Object getCachedValue(int columnIndex) { 311 String uri = sUnderlyingCursor.getString(sUriColumnIndex); 312 ContentValues uriMap = sCacheMap.get(uri); 313 if (uriMap != null) { 314 String columnName; 315 if (columnIndex == DELETED_COLUMN_INDEX) { 316 columnName = DELETED_COLUMN; 317 } else { 318 columnName = mColumnNames[columnIndex]; 319 } 320 return uriMap.get(columnName); 321 } 322 return null; 323 } 324 325 /** 326 * When the underlying cursor changes, we want to alert the listener 327 */ 328 private void underlyingChanged() { 329 if (sListener != null) { 330 if (mCursorObserverRegistered) { 331 sUnderlyingCursor.unregisterContentObserver(mCursorObserver); 332 mCursorObserverRegistered = false; 333 } 334 if (DEBUG) { 335 Log.d(TAG, "[Notify: onRefreshRequired()]"); 336 } 337 sListener.onRefreshRequired(); 338 sRefreshRequired = true; 339 } 340 } 341 342 /** 343 * Put the refreshed cursor in place (called by the UI) 344 */ 345 // NOTE: We don't like the name (it implies syncing with the server); suggestions gladly 346 // taken - reset? syncToUnderlying? completeRefresh? align? 347 public void sync() { 348 if (DEBUG) { 349 Log.d(TAG, "[sync() called]"); 350 } 351 if (sRequeryCursor == null) { 352 throw new IllegalStateException("Can't swap cursors; no requery done"); 353 } 354 resetCursor(sRequeryCursor); 355 sRequeryCursor = null; 356 sRefreshInProgress = false; 357 sRefreshReady = false; 358 } 359 360 public boolean isRefreshRequired() { 361 return sRefreshRequired; 362 } 363 364 public boolean isRefreshReady() { 365 return sRefreshReady; 366 } 367 368 /** 369 * Cancel a refresh in progress 370 */ 371 public void cancelRefresh() { 372 if (DEBUG) { 373 Log.d(TAG, "[cancelRefresh() called]"); 374 } 375 synchronized(sCacheMapLock) { 376 // Mark the requery closed 377 sRefreshInProgress = false; 378 // If we have the cursor, close it; otherwise, it will get closed when the query 379 // finishes (it checks sRequeryInProgress) 380 if (sRequeryCursor != null) { 381 sRequeryCursor.close(); 382 sRequeryCursor = null; 383 } 384 } 385 } 386 387 /** 388 * Get a list of deletions from ConversationCursor to the refreshed cursor that hasn't yet 389 * been swapped into place; this allows the UI to animate these away if desired 390 * @return a list of positions deleted in ConversationCursor 391 */ 392 public ArrayList<Integer> getRefreshDeletions () { 393 if (DEBUG) { 394 Log.d(TAG, "[getRefreshDeletions() called]"); 395 } 396 Cursor deviceCursor = sConversationCursor; 397 Cursor serverCursor = sRequeryCursor; 398 ArrayList<Integer> deleteList = new ArrayList<Integer>(); 399 int serverCount = serverCursor.getCount(); 400 int deviceCount = deviceCursor.getCount(); 401 deviceCursor.moveToFirst(); 402 serverCursor.moveToFirst(); 403 while (serverCount > 0 || deviceCount > 0) { 404 if (serverCount == 0) { 405 for (; deviceCount > 0; deviceCount--) 406 deleteList.add(deviceCursor.getPosition()); 407 break; 408 } else if (deviceCount == 0) { 409 break; 410 } 411 long deviceMs = deviceCursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN); 412 long serverMs = serverCursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN); 413 String deviceUri = deviceCursor.getString(UIProvider.CONVERSATION_URI_COLUMN); 414 String serverUri = serverCursor.getString(UIProvider.CONVERSATION_URI_COLUMN); 415 deviceCursor.moveToNext(); 416 serverCursor.moveToNext(); 417 serverCount--; 418 deviceCount--; 419 if (serverMs == deviceMs) { 420 // Check for duplicates here; if our identical dates refer to different messages, 421 // we'll just quit here for now (at worst, this will cause a non-animating delete) 422 // My guess is that this happens VERY rarely, if at all 423 if (!deviceUri.equals(serverUri)) { 424 // To do this right, we'd find all of the rows with the same ms (date), etc... 425 //return deleteList; 426 } 427 continue; 428 } else if (deviceMs > serverMs) { 429 deleteList.add(deviceCursor.getPosition() - 1); 430 // Move back because we've already advanced cursor (that's why we subtract 1 above) 431 serverCount++; 432 serverCursor.moveToPrevious(); 433 } else if (serverMs > deviceMs) { 434 // If we wanted to track insertions, we'd so so here 435 // Move back because we've already advanced cursor 436 deviceCount++; 437 deviceCursor.moveToPrevious(); 438 } 439 } 440 Log.d(TAG, "Deletions: " + deleteList); 441 return deleteList; 442 } 443 444 /** 445 * When we get a requery from the UI, we'll do it, but also clear the cache. The listener is 446 * notified when the requery is complete 447 * NOTE: This will have to change, of course, when we start using loaders... 448 */ 449 public boolean refresh() { 450 if (DEBUG) { 451 Log.d(TAG, "[refresh() called]"); 452 } 453 if (sRefreshInProgress) { 454 return false; 455 } 456 // Say we're starting a requery 457 sRefreshInProgress = true; 458 new Thread(new Runnable() { 459 @Override 460 public void run() { 461 // Get new data 462 sRequeryCursor = 463 mResolver.query(qUri, qProjection, qSelection, qSelectionArgs, qSortOrder); 464 // Make sure window is full 465 synchronized(sCacheMapLock) { 466 if (sRefreshInProgress) { 467 sRequeryCursor.getCount(); 468 sActivity.runOnUiThread(new Runnable() { 469 @Override 470 public void run() { 471 if (DEBUG) { 472 Log.d(TAG, "[Notify: onRefreshReady()]"); 473 } 474 sListener.onRefreshReady(); 475 sRefreshReady = true; 476 }}); 477 } else { 478 cancelRefresh(); 479 } 480 } 481 } 482 }).start(); 483 return true; 484 } 485 486 public void close() { 487 // Unregister our observer on the underlying cursor and close as usual 488 if (mCursorObserverRegistered) { 489 sUnderlyingCursor.unregisterContentObserver(mCursorObserver); 490 mCursorObserverRegistered = false; 491 } 492 sUnderlyingCursor.close(); 493 } 494 495 /** 496 * Move to the next not-deleted item in the conversation 497 */ 498 public boolean moveToNext() { 499 while (true) { 500 boolean ret = sUnderlyingCursor.moveToNext(); 501 if (!ret) return false; 502 if (getCachedValue(DELETED_COLUMN_INDEX) instanceof Integer) continue; 503 mPosition++; 504 return true; 505 } 506 } 507 508 /** 509 * Move to the previous not-deleted item in the conversation 510 */ 511 public boolean moveToPrevious() { 512 while (true) { 513 boolean ret = sUnderlyingCursor.moveToPrevious(); 514 if (!ret) return false; 515 if (getCachedValue(-1) instanceof Integer) continue; 516 mPosition--; 517 return true; 518 } 519 } 520 521 public int getPosition() { 522 return mPosition; 523 } 524 525 /** 526 * The actual cursor's count must be decremented by the number we've deleted from the UI 527 */ 528 public int getCount() { 529 return sUnderlyingCursor.getCount() - sDeletedCount; 530 } 531 532 public boolean moveToFirst() { 533 sUnderlyingCursor.moveToPosition(-1); 534 mPosition = -1; 535 return moveToNext(); 536 } 537 538 public boolean moveToPosition(int pos) { 539 if (pos < -1 || pos >= getCount()) return false; 540 if (pos == mPosition) return true; 541 if (pos > mPosition) { 542 while (pos > mPosition) { 543 if (!moveToNext()) { 544 return false; 545 } 546 } 547 return true; 548 } else if (pos == 0) { 549 return moveToFirst(); 550 } else { 551 while (pos < mPosition) { 552 if (!moveToPrevious()) { 553 return false; 554 } 555 } 556 return true; 557 } 558 } 559 560 public boolean moveToLast() { 561 throw new UnsupportedOperationException("moveToLast unsupported!"); 562 } 563 564 public boolean move(int offset) { 565 throw new UnsupportedOperationException("move unsupported!"); 566 } 567 568 /** 569 * We need to override all of the getters to make sure they look at cached values before using 570 * the values in the underlying cursor 571 */ 572 @Override 573 public double getDouble(int columnIndex) { 574 Object obj = getCachedValue(columnIndex); 575 if (obj != null) return (Double)obj; 576 return sUnderlyingCursor.getDouble(columnIndex); 577 } 578 579 @Override 580 public float getFloat(int columnIndex) { 581 Object obj = getCachedValue(columnIndex); 582 if (obj != null) return (Float)obj; 583 return sUnderlyingCursor.getFloat(columnIndex); 584 } 585 586 @Override 587 public int getInt(int columnIndex) { 588 Object obj = getCachedValue(columnIndex); 589 if (obj != null) return (Integer)obj; 590 return sUnderlyingCursor.getInt(columnIndex); 591 } 592 593 @Override 594 public long getLong(int columnIndex) { 595 Object obj = getCachedValue(columnIndex); 596 if (obj != null) return (Long)obj; 597 return sUnderlyingCursor.getLong(columnIndex); 598 } 599 600 @Override 601 public short getShort(int columnIndex) { 602 Object obj = getCachedValue(columnIndex); 603 if (obj != null) return (Short)obj; 604 return sUnderlyingCursor.getShort(columnIndex); 605 } 606 607 @Override 608 public String getString(int columnIndex) { 609 // If we're asking for the Uri for the conversation list, we return a forwarding URI 610 // so that we can intercept update/delete and handle it ourselves 611 if (columnIndex == sUriColumnIndex) { 612 Uri uri = Uri.parse(sUnderlyingCursor.getString(columnIndex)); 613 return uriToCachingUriString(uri); 614 } 615 Object obj = getCachedValue(columnIndex); 616 if (obj != null) return (String)obj; 617 return sUnderlyingCursor.getString(columnIndex); 618 } 619 620 @Override 621 public byte[] getBlob(int columnIndex) { 622 Object obj = getCachedValue(columnIndex); 623 if (obj != null) return (byte[])obj; 624 return sUnderlyingCursor.getBlob(columnIndex); 625 } 626 627 /** 628 * Observer of changes to underlying data 629 */ 630 private class CursorObserver extends ContentObserver { 631 public CursorObserver() { 632 super(new Handler()); 633 } 634 635 @Override 636 public void onChange(boolean selfChange) { 637 // If we're here, then something outside of the UI has changed the data, and we 638 // must query the underlying provider for that data 639 if (DEBUG) { 640 Log.d(TAG, "Underlying conversation cursor changed; requerying"); 641 } 642 // It's not at all obvious to me why we must unregister/re-register after the requery 643 // However, if we don't we'll only get one notification and no more... 644 ConversationCursor.this.underlyingChanged(); 645 } 646 } 647 648 /** 649 * ConversationProvider is the ContentProvider for our forwarding Uri's; it passes queries 650 * and inserts directly, and caches updates/deletes before passing them through. The caching 651 * will cause a redraw of the list with updated values. 652 */ 653 public static class ConversationProvider extends ContentProvider { 654 public static final String AUTHORITY = sAuthority; 655 656 @Override 657 public boolean onCreate() { 658 sProvider = this; 659 return true; 660 } 661 662 @Override 663 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 664 String sortOrder) { 665 return mResolver.query( 666 uriFromCachingUri(uri), projection, selection, selectionArgs, sortOrder); 667 } 668 669 @Override 670 public Uri insert(Uri uri, ContentValues values) { 671 insertLocal(uri, values); 672 return ProviderExecute.opInsert(uri, values); 673 } 674 675 @Override 676 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 677 updateLocal(uri, values); 678 return ProviderExecute.opUpdate(uri, values); 679 } 680 681 @Override 682 public int delete(Uri uri, String selection, String[] selectionArgs) { 683 deleteLocal(uri); 684 return ProviderExecute.opDelete(uri); 685 } 686 687 @Override 688 public String getType(Uri uri) { 689 return null; 690 } 691 692 /** 693 * Quick and dirty class that executes underlying provider CRUD operations on a background 694 * thread. 695 */ 696 static class ProviderExecute implements Runnable { 697 static final int DELETE = 0; 698 static final int INSERT = 1; 699 static final int UPDATE = 2; 700 701 final int mCode; 702 final Uri mUri; 703 final ContentValues mValues; //HEHEH 704 705 ProviderExecute(int code, Uri uri, ContentValues values) { 706 mCode = code; 707 mUri = uriFromCachingUri(uri); 708 mValues = values; 709 } 710 711 ProviderExecute(int code, Uri uri) { 712 this(code, uri, null); 713 } 714 715 static Uri opInsert(Uri uri, ContentValues values) { 716 ProviderExecute e = new ProviderExecute(INSERT, uri, values); 717 if (offUiThread()) return (Uri)e.go(); 718 new Thread(e).start(); 719 return null; 720 } 721 722 static int opDelete(Uri uri) { 723 ProviderExecute e = new ProviderExecute(DELETE, uri); 724 if (offUiThread()) return (Integer)e.go(); 725 new Thread(new ProviderExecute(DELETE, uri)).start(); 726 return 0; 727 } 728 729 static int opUpdate(Uri uri, ContentValues values) { 730 ProviderExecute e = new ProviderExecute(UPDATE, uri, values); 731 if (offUiThread()) return (Integer)e.go(); 732 new Thread(e).start(); 733 return 0; 734 } 735 736 @Override 737 public void run() { 738 go(); 739 } 740 741 public Object go() { 742 switch(mCode) { 743 case DELETE: 744 return mResolver.delete(mUri, null, null); 745 case INSERT: 746 return mResolver.insert(mUri, mValues); 747 case UPDATE: 748 return mResolver.update(mUri, mValues, null, null); 749 default: 750 return null; 751 } 752 } 753 } 754 755 private void insertLocal(Uri uri, ContentValues values) { 756 // Placeholder for now; there's no local insert 757 } 758 759 private void deleteLocal(Uri uri) { 760 Uri underlyingUri = uriFromCachingUri(uri); 761 // Remember to decode the underlying Uri as it might be encoded (as w/ Gmail) 762 String uriString = Uri.decode(underlyingUri.toString()); 763 cacheValue(uriString, DELETED_COLUMN, true); 764 } 765 766 private void updateLocal(Uri uri, ContentValues values) { 767 Uri underlyingUri = uriFromCachingUri(uri); 768 // Remember to decode the underlying Uri as it might be encoded (as w/ Gmail) 769 String uriString = Uri.decode(underlyingUri.toString()); 770 for (String columnName: values.keySet()) { 771 cacheValue(uriString, columnName, values.get(columnName)); 772 } 773 } 774 775 static boolean offUiThread() { 776 return Looper.getMainLooper().getThread() != Thread.currentThread(); 777 } 778 779 public int apply(ArrayList<ConversationOperation> ops) { 780 final HashMap<String, ArrayList<ContentProviderOperation>> batchMap = 781 new HashMap<String, ArrayList<ContentProviderOperation>>(); 782 // Increment sequence count 783 sSequence++; 784 // Execute locally and build CPO's for underlying provider 785 for (ConversationOperation op: ops) { 786 Uri underlyingUri = uriFromCachingUri(op.mUri); 787 String authority = underlyingUri.getAuthority(); 788 ArrayList<ContentProviderOperation> authOps = batchMap.get(authority); 789 if (authOps == null) { 790 authOps = new ArrayList<ContentProviderOperation>(); 791 batchMap.put(authority, authOps); 792 } 793 authOps.add(op.execute(underlyingUri)); 794 } 795 796 // Send changes to underlying provider 797 for (String authority: batchMap.keySet()) { 798 try { 799 if (offUiThread()) { 800 mResolver.applyBatch(authority, batchMap.get(authority)); 801 } else { 802 final String auth = authority; 803 new Thread(new Runnable() { 804 @Override 805 public void run() { 806 try { 807 mResolver.applyBatch(auth, batchMap.get(auth)); 808 } catch (RemoteException e) { 809 } catch (OperationApplicationException e) { 810 } 811 } 812 }).start(); 813 } 814 } catch (RemoteException e) { 815 } catch (OperationApplicationException e) { 816 } 817 } 818 return sSequence; 819 } 820 } 821 822 /** 823 * ConversationOperation is the encapsulation of a ContentProvider operation to be performed 824 * atomically as part of a "batch" operation. 825 */ 826 public static class ConversationOperation { 827 public static final int DELETE = 0; 828 public static final int INSERT = 1; 829 public static final int UPDATE = 2; 830 831 private final int mType; 832 private final Uri mUri; 833 private final ContentValues mValues; 834 // True if an updated item should be removed locally (from ConversationCursor) 835 // This would be the case for a folder/label change in which the conversation is no longer 836 // in the folder represented by the ConversationCursor 837 private final boolean mLocalDeleteOnUpdate; 838 839 public ConversationOperation(int type, Conversation conv) { 840 this(type, conv, null); 841 } 842 843 public ConversationOperation(int type, Conversation conv, ContentValues values) { 844 mType = type; 845 mUri = conv.uri; 846 mValues = values; 847 mLocalDeleteOnUpdate = conv.localDeleteOnUpdate; 848 } 849 850 private ContentProviderOperation execute(Uri underlyingUri) { 851 Uri uri = underlyingUri.buildUpon() 852 .appendQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER, 853 Integer.toString(sSequence)) 854 .build(); 855 switch(mType) { 856 case DELETE: 857 sProvider.deleteLocal(mUri); 858 return ContentProviderOperation.newDelete(uri).build(); 859 case UPDATE: 860 if (mLocalDeleteOnUpdate) { 861 sProvider.deleteLocal(mUri); 862 } else { 863 sProvider.updateLocal(mUri, mValues); 864 } 865 return ContentProviderOperation.newUpdate(uri) 866 .withValues(mValues) 867 .build(); 868 case INSERT: 869 sProvider.insertLocal(mUri, mValues); 870 return ContentProviderOperation.newInsert(uri) 871 .withValues(mValues).build(); 872 default: 873 throw new UnsupportedOperationException( 874 "No such ConversationOperation type: " + mType); 875 } 876 } 877 } 878 879 /** 880 * For now, a single listener can be associated with the cursor, and for now we'll just 881 * notify on deletions 882 */ 883 public interface ConversationListener { 884 // Data in the underlying provider has changed; a refresh is required to sync up 885 public void onRefreshRequired(); 886 // We've completed a requested refresh of the underlying cursor 887 public void onRefreshReady(); 888 } 889 890 @Override 891 public boolean isFirst() { 892 throw new UnsupportedOperationException(); 893 } 894 895 @Override 896 public boolean isLast() { 897 throw new UnsupportedOperationException(); 898 } 899 900 @Override 901 public boolean isBeforeFirst() { 902 throw new UnsupportedOperationException(); 903 } 904 905 @Override 906 public boolean isAfterLast() { 907 throw new UnsupportedOperationException(); 908 } 909 910 @Override 911 public int getColumnIndex(String columnName) { 912 return sUnderlyingCursor.getColumnIndex(columnName); 913 } 914 915 @Override 916 public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { 917 return sUnderlyingCursor.getColumnIndexOrThrow(columnName); 918 } 919 920 @Override 921 public String getColumnName(int columnIndex) { 922 return sUnderlyingCursor.getColumnName(columnIndex); 923 } 924 925 @Override 926 public String[] getColumnNames() { 927 return sUnderlyingCursor.getColumnNames(); 928 } 929 930 @Override 931 public int getColumnCount() { 932 return sUnderlyingCursor.getColumnCount(); 933 } 934 935 @Override 936 public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { 937 throw new UnsupportedOperationException(); 938 } 939 940 @Override 941 public int getType(int columnIndex) { 942 return sUnderlyingCursor.getType(columnIndex); 943 } 944 945 @Override 946 public boolean isNull(int columnIndex) { 947 throw new UnsupportedOperationException(); 948 } 949 950 @Override 951 public void deactivate() { 952 throw new UnsupportedOperationException(); 953 } 954 955 @Override 956 public boolean isClosed() { 957 return sUnderlyingCursor.isClosed(); 958 } 959 960 @Override 961 public void registerContentObserver(ContentObserver observer) { 962 sUnderlyingCursor.registerContentObserver(observer); 963 } 964 965 @Override 966 public void unregisterContentObserver(ContentObserver observer) { 967 sUnderlyingCursor.unregisterContentObserver(observer); 968 } 969 970 @Override 971 public void registerDataSetObserver(DataSetObserver observer) { 972 sUnderlyingCursor.registerDataSetObserver(observer); 973 } 974 975 @Override 976 public void unregisterDataSetObserver(DataSetObserver observer) { 977 sUnderlyingCursor.unregisterDataSetObserver(observer); 978 } 979 980 @Override 981 public void setNotificationUri(ContentResolver cr, Uri uri) { 982 throw new UnsupportedOperationException(); 983 } 984 985 @Override 986 public boolean getWantsAllOnMoveCalls() { 987 throw new UnsupportedOperationException(); 988 } 989 990 @Override 991 public Bundle getExtras() { 992 throw new UnsupportedOperationException(); 993 } 994 995 @Override 996 public Bundle respond(Bundle extras) { 997 throw new UnsupportedOperationException(); 998 } 999 1000 @Override 1001 public boolean requery() { 1002 return true; 1003 } 1004} 1005