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