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