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