ConversationCursor.java revision e602ae10b24ed61b5fdd651f82b330b7e700d746
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.Context; 26import android.content.OperationApplicationException; 27import android.database.CharArrayBuffer; 28import android.database.ContentObserver; 29import android.database.Cursor; 30import android.database.CursorWrapper; 31import android.database.DataSetObserver; 32import android.net.Uri; 33import android.os.AsyncTask; 34import android.os.Bundle; 35import android.os.Handler; 36import android.os.Looper; 37import android.os.RemoteException; 38import android.util.Log; 39 40import com.android.mail.providers.Conversation; 41import com.android.mail.providers.UIProvider; 42import com.android.mail.providers.UIProvider.ConversationListQueryParameters; 43import com.android.mail.providers.UIProvider.ConversationOperations; 44import com.android.mail.utils.LogUtils; 45import com.google.common.annotations.VisibleForTesting; 46import com.google.common.collect.Lists; 47 48import java.util.ArrayList; 49import java.util.Arrays; 50import java.util.Collection; 51import java.util.HashMap; 52import java.util.Iterator; 53import java.util.List; 54 55/** 56 * ConversationCursor is a wrapper around a conversation list cursor that provides update/delete 57 * caching for quick UI response. This is effectively a singleton class, as the cache is 58 * implemented as a static HashMap. 59 */ 60public final class ConversationCursor implements Cursor { 61 private static final String TAG = "ConversationCursor"; 62 63 private static final boolean DEBUG = true; // STOPSHIP Set to false before shipping 64 // A deleted row is indicated by the presence of DELETED_COLUMN in the cache map 65 private static final String DELETED_COLUMN = "__deleted__"; 66 // An row cached during a requery is indicated by the presence of REQUERY_COLUMN in the map 67 private static final String REQUERY_COLUMN = "__requery__"; 68 // A sentinel value for the "index" of the deleted column; it's an int that is otherwise invalid 69 private static final int DELETED_COLUMN_INDEX = -1; 70 // Empty deletion list 71 private static final Collection<Conversation> EMPTY_DELETION_LIST = Lists.newArrayList(); 72 // The index of the Uri whose data is reflected in the cached row 73 // Updates/Deletes to this Uri are cached 74 private static int sUriColumnIndex; 75 // Our sequence count (for changes sent to underlying provider) 76 private static int sSequence = 0; 77 // The resolver for the cursor instantiator's context 78 private static ContentResolver sResolver; 79 @VisibleForTesting 80 static ConversationProvider sProvider; 81 82 // The cursor underlying the caching cursor 83 @VisibleForTesting 84 Wrapper mUnderlyingCursor; 85 // The new cursor obtained via a requery 86 private volatile Wrapper mRequeryCursor; 87 // A mapping from Uri to updated ContentValues 88 private HashMap<String, ContentValues> mCacheMap = new HashMap<String, ContentValues>(); 89 // Cache map lock (will be used only very briefly - few ms at most) 90 private Object mCacheMapLock = new Object(); 91 // The listeners registered for this cursor 92 private List<ConversationListener> mListeners = Lists.newArrayList(); 93 // The ConversationProvider instance 94 // The runnable executing a refresh (query of underlying provider) 95 private RefreshTask mRefreshTask; 96 // Set when we've sent refreshReady() to listeners 97 private boolean mRefreshReady = false; 98 // Set when we've sent refreshRequired() to listeners 99 private boolean mRefreshRequired = false; 100 // Whether our first query on this cursor should include a limit 101 private boolean mInitialConversationLimit = false; 102 // A list of mostly-dead items 103 private List<Conversation> sMostlyDead = Lists.newArrayList(); 104 // The name of the loader 105 private final String mName; 106 // Column names for this cursor 107 private String[] mColumnNames; 108 // An observer on the underlying cursor (so we can detect changes from outside the UI) 109 private final CursorObserver mCursorObserver; 110 // Whether our observer is currently registered with the underlying cursor 111 private boolean mCursorObserverRegistered = false; 112 // Whether our loader is paused 113 private boolean mPaused = false; 114 // Whether or not sync from underlying provider should be deferred 115 private boolean mDeferSync = false; 116 117 // The current position of the cursor 118 private int mPosition = -1; 119 120 // The number of cached deletions from this cursor (used to quickly generate an accurate count) 121 private int mDeletedCount = 0; 122 123 // Parameters passed to the underlying query 124 private Uri qUri; 125 private String[] qProjection; 126 127 private void setCursor(Wrapper cursor) { 128 // If we have an existing underlying cursor, make sure it's closed 129 if (mUnderlyingCursor != null) { 130 close(); 131 } 132 mColumnNames = cursor.getColumnNames(); 133 mRefreshRequired = false; 134 mRefreshReady = false; 135 mRefreshTask = null; 136 resetCursor(cursor); 137 } 138 139 public ConversationCursor(Activity activity, Uri uri, boolean initialConversationLimit, 140 String name) { 141 mInitialConversationLimit = initialConversationLimit; 142 sResolver = activity.getContentResolver(); 143 sUriColumnIndex = UIProvider.CONVERSATION_URI_COLUMN; 144 qUri = uri; 145 mName = name; 146 qProjection = UIProvider.CONVERSATION_PROJECTION; 147 mCursorObserver = new CursorObserver(new Handler(Looper.getMainLooper())); 148 } 149 150 /** 151 * Create a ConversationCursor; this should be called by the ListActivity using that cursor 152 * @param activity the activity creating the cursor 153 * @param messageListColumn the column used for individual cursor items 154 * @param uri the query uri 155 * @param projection the query projecion 156 * @param selection the query selection 157 * @param selectionArgs the query selection args 158 * @param sortOrder the query sort order 159 * @return a ConversationCursor 160 */ 161 public void load() { 162 synchronized (mCacheMapLock) { 163 try { 164 // Create new ConversationCursor 165 LogUtils.i(TAG, "Create: initial creation"); 166 Wrapper c = doQuery(mInitialConversationLimit); 167 setCursor(c); 168 } finally { 169 // If we used a limit, queue up a query without limit 170 if (mInitialConversationLimit) { 171 mInitialConversationLimit = false; 172 refresh(); 173 } 174 } 175 } 176 } 177 178 /** 179 * Pause notifications to UI 180 */ 181 public void pause() { 182 if (DEBUG) { 183 LogUtils.i(TAG, "[Paused: %s]", mName); 184 } 185 mPaused = true; 186 } 187 188 /** 189 * Resume notifications to UI; if any are pending, send them 190 */ 191 public void resume() { 192 if (DEBUG) { 193 LogUtils.i(TAG, "[Resumed: %s]", mName); 194 } 195 mPaused = false; 196 checkNotifyUI(); 197 } 198 199 private void checkNotifyUI() { 200 if (!mPaused && !mDeferSync) { 201 if (mRefreshRequired && (mRefreshTask == null)) { 202 notifyRefreshRequired(); 203 } else if (mRefreshReady) { 204 notifyRefreshReady(); 205 } 206 } else { 207 LogUtils.i(TAG, "[checkNotifyUI: %s%s", 208 (mPaused ? "Paused " : ""), (mDeferSync ? "Defer" : "")); 209 } 210 } 211 212 /** 213 * Runnable that performs the query on the underlying provider 214 */ 215 private class RefreshTask extends AsyncTask<Void, Void, Void> { 216 private Wrapper mCursor = null; 217 218 private RefreshTask() { 219 } 220 221 @Override 222 protected Void doInBackground(Void... params) { 223 if (DEBUG) { 224 LogUtils.i(TAG, "[Start refresh of %s: %d]", mName, hashCode()); 225 } 226 // Get new data 227 mCursor = doQuery(false); 228 return null; 229 } 230 231 @Override 232 protected void onPostExecute(Void param) { 233 synchronized(mCacheMapLock) { 234 // If cursor got closed (e.g. reset loader) in the meantime, cancel the refresh 235 if (isClosed()) { 236 onCancelled(); 237 return; 238 } 239 mRequeryCursor = mCursor; 240 // Make sure window is full 241 mRequeryCursor.getCount(); 242 mRefreshReady = true; 243 if (DEBUG) { 244 LogUtils.i(TAG, "[Query done %s: %d]", mName, hashCode()); 245 } 246 if (!mDeferSync && !mPaused) { 247 notifyRefreshReady(); 248 } 249 } 250 } 251 252 @Override 253 protected void onCancelled() { 254 if (DEBUG) { 255 LogUtils.i(TAG, "[Ignoring refresh result: %d]", hashCode()); 256 } 257 if (mCursor != null) { 258 mCursor.close(); 259 } 260 } 261 } 262 263 /** 264 * Wrapper that includes the Uri used to create the cursor 265 */ 266 private static class Wrapper extends CursorWrapper { 267 private final Uri mUri; 268 269 Wrapper(Cursor cursor, Uri uri) { 270 super(cursor); 271 mUri = uri; 272 } 273 } 274 275 private Wrapper doQuery(boolean withLimit) { 276 Uri uri = qUri; 277 if (withLimit) { 278 uri = uri.buildUpon().appendQueryParameter(ConversationListQueryParameters.LIMIT, 279 ConversationListQueryParameters.DEFAULT_LIMIT).build(); 280 } 281 long time = System.currentTimeMillis(); 282 283 Wrapper result = new Wrapper(sResolver.query(uri, qProjection, null, null, null), uri); 284 if (result.getWrappedCursor() == null) { 285 Log.w(TAG, "doQuery returning null cursor, uri: " + uri); 286 } else if (DEBUG) { 287 time = System.currentTimeMillis() - time; 288 LogUtils.i(TAG, "ConversationCursor query: %s, %dms, %d results", 289 uri, time, result.getCount()); 290 } 291 return result; 292 } 293 294 /** 295 * Return whether the uri string (message list uri) is in the underlying cursor 296 * @param uriString the uri string we're looking for 297 * @return true if the uri string is in the cursor; false otherwise 298 */ 299 private boolean isInUnderlyingCursor(String uriString) { 300 mUnderlyingCursor.moveToPosition(-1); 301 while (mUnderlyingCursor.moveToNext()) { 302 if (uriString.equals(mUnderlyingCursor.getString(sUriColumnIndex))) { 303 return true; 304 } 305 } 306 return false; 307 } 308 309 static boolean offUiThread() { 310 return Looper.getMainLooper().getThread() != Thread.currentThread(); 311 } 312 313 /** 314 * Reset the cursor; this involves clearing out our cache map and resetting our various counts 315 * The cursor should be reset whenever we get fresh data from the underlying cursor. The cache 316 * is locked during the reset, which will block the UI, but for only a very short time 317 * (estimated at a few ms, but we can profile this; remember that the cache will usually 318 * be empty or have a few entries) 319 */ 320 private void resetCursor(Wrapper newCursor) { 321 synchronized (mCacheMapLock) { 322 // Walk through the cache. Here are the cases: 323 // 1) The entry isn't marked with REQUERY - remove it from the cache. If DELETED is 324 // set, decrement the deleted count 325 // 2) The REQUERY entry is still in the UP 326 // 2a) The REQUERY entry isn't DELETED; we're good, and the client change will remain 327 // (i.e. client wins, it's on its way to the UP) 328 // 2b) The REQUERY entry is DELETED; we're good (client change remains, it's on 329 // its way to the UP) 330 // 3) the REQUERY was deleted on the server (sheesh; this would be bizarre timing!) - 331 // we need to throw the item out of the cache 332 // So ... the only interesting case is #3, we need to look for remaining deleted items 333 // and see if they're still in the UP 334 Iterator<HashMap.Entry<String, ContentValues>> iter = mCacheMap.entrySet().iterator(); 335 while (iter.hasNext()) { 336 HashMap.Entry<String, ContentValues> entry = iter.next(); 337 ContentValues values = entry.getValue(); 338 if (values.containsKey(REQUERY_COLUMN) && isInUnderlyingCursor(entry.getKey())) { 339 // If we're in a requery and we're still around, remove the requery key 340 // We're good here, the cached change (delete/update) is on its way to UP 341 values.remove(REQUERY_COLUMN); 342 LogUtils.i(TAG, 343 "IN resetCursor, remove requery column from %s", entry.getKey()); 344 } else { 345 // Keep the deleted count up-to-date; remove the cache entry 346 if (values.containsKey(DELETED_COLUMN)) { 347 mDeletedCount--; 348 LogUtils.i(TAG, "IN resetCursor, sDeletedCount decremented to: %d by %s", 349 mDeletedCount, entry.getKey()); 350 } 351 // Remove the entry 352 iter.remove(); 353 } 354 } 355 356 // Swap cursor 357 if (mUnderlyingCursor != null) { 358 close(); 359 } 360 mUnderlyingCursor = newCursor; 361 362 mPosition = -1; 363 mUnderlyingCursor.moveToPosition(mPosition); 364 if (!mCursorObserverRegistered) { 365 mUnderlyingCursor.registerContentObserver(mCursorObserver); 366 mCursorObserverRegistered = true; 367 } 368 mRefreshRequired = false; 369 } 370 } 371 372 /** 373 * Add a listener for this cursor; we'll notify it when our data changes 374 */ 375 public void addListener(ConversationListener listener) { 376 synchronized (mListeners) { 377 if (!mListeners.contains(listener)) { 378 mListeners.add(listener); 379 } else { 380 LogUtils.i(TAG, "Ignoring duplicate add of listener"); 381 } 382 } 383 } 384 385 /** 386 * Remove a listener for this cursor 387 */ 388 public void removeListener(ConversationListener listener) { 389 synchronized(mListeners) { 390 mListeners.remove(listener); 391 } 392 } 393 394 /** 395 * Generate a forwarding Uri to ConversationProvider from an original Uri. We do this by 396 * changing the authority to ours, but otherwise leaving the Uri intact. 397 * NOTE: This won't handle query parameters, so the functionality will need to be added if 398 * parameters are used in the future 399 * @param uri the uri 400 * @return a forwarding uri to ConversationProvider 401 */ 402 private static String uriToCachingUriString (Uri uri) { 403 String provider = uri.getAuthority(); 404 return uri.getScheme() + "://" + ConversationProvider.AUTHORITY 405 + "/" + provider + uri.getPath(); 406 } 407 408 /** 409 * Regenerate the original Uri from a forwarding (ConversationProvider) Uri 410 * NOTE: See note above for uriToCachingUri 411 * @param uri the forwarding Uri 412 * @return the original Uri 413 */ 414 private static Uri uriFromCachingUri(Uri uri) { 415 String authority = uri.getAuthority(); 416 // Don't modify uri's that aren't ours 417 if (!authority.equals(ConversationProvider.AUTHORITY)) { 418 return uri; 419 } 420 List<String> path = uri.getPathSegments(); 421 Uri.Builder builder = new Uri.Builder().scheme(uri.getScheme()).authority(path.get(0)); 422 for (int i = 1; i < path.size(); i++) { 423 builder.appendPath(path.get(i)); 424 } 425 return builder.build(); 426 } 427 428 private static String uriStringFromCachingUri(Uri uri) { 429 Uri underlyingUri = uriFromCachingUri(uri); 430 // Remember to decode the underlying Uri as it might be encoded (as w/ Gmail) 431 return Uri.decode(underlyingUri.toString()); 432 } 433 434 public void setConversationColumn(Uri conversationUri, String columnName, Object value) { 435 final String uriStr = uriStringFromCachingUri(conversationUri); 436 synchronized (mCacheMapLock) { 437 cacheValue(uriStr, columnName, value); 438 } 439 notifyDataChanged(); 440 } 441 442 /** 443 * Cache a column name/value pair for a given Uri 444 * @param uriString the Uri for which the column name/value pair applies 445 * @param columnName the column name 446 * @param value the value to be cached 447 */ 448 private void cacheValue(String uriString, String columnName, Object value) { 449 // Calling this method off the UI thread will mess with ListView's reading of the cursor's 450 // count 451 if (offUiThread()) { 452 LogUtils.e(TAG, new Error(), "cacheValue incorrectly being called from non-UI thread"); 453 } 454 455 synchronized (mCacheMapLock) { 456 // Get the map for our uri 457 ContentValues map = mCacheMap.get(uriString); 458 // Create one if necessary 459 if (map == null) { 460 map = new ContentValues(); 461 mCacheMap.put(uriString, map); 462 } 463 // If we're caching a deletion, add to our count 464 if (columnName == DELETED_COLUMN) { 465 final boolean state = (Boolean)value; 466 final boolean hasValue = map.get(columnName) != null; 467 if (state && !hasValue) { 468 mDeletedCount++; 469 if (DEBUG) { 470 LogUtils.i(TAG, "Deleted %s, incremented deleted count=%d", uriString, 471 mDeletedCount); 472 } 473 } else if (!state && hasValue) { 474 mDeletedCount--; 475 map.remove(columnName); 476 if (DEBUG) { 477 LogUtils.i(TAG, "Undeleted %s, decremented deleted count=%d", uriString, 478 mDeletedCount); 479 } 480 return; 481 } else if (!state) { 482 // Trying to undelete, but it's not deleted; just return 483 if (DEBUG) { 484 LogUtils.i(TAG, "Undeleted %s, IGNORING, deleted count=%d", uriString, 485 mDeletedCount); 486 } 487 return; 488 } 489 } 490 // ContentValues has no generic "put", so we must test. For now, the only classes 491 // of values implemented are Boolean/Integer/String, though others are trivially 492 // added 493 if (value instanceof Boolean) { 494 map.put(columnName, ((Boolean) value).booleanValue() ? 1 : 0); 495 } else if (value instanceof Integer) { 496 map.put(columnName, (Integer) value); 497 } else if (value instanceof String) { 498 map.put(columnName, (String) value); 499 } else { 500 final String cname = value.getClass().getName(); 501 throw new IllegalArgumentException("Value class not compatible with cache: " 502 + cname); 503 } 504 if (mRefreshTask != null) { 505 map.put(REQUERY_COLUMN, 1); 506 } 507 if (DEBUG && (columnName != DELETED_COLUMN)) { 508 LogUtils.i(TAG, "Caching value for %s: %s", uriString, columnName); 509 } 510 } 511 } 512 513 /** 514 * Get the cached value for the provided column; we special case -1 as the "deleted" column 515 * @param columnIndex the index of the column whose cached value we want to retrieve 516 * @return the cached value for this column, or null if there is none 517 */ 518 private Object getCachedValue(int columnIndex) { 519 String uri = mUnderlyingCursor.getString(sUriColumnIndex); 520 return getCachedValue(uri, columnIndex); 521 } 522 523 private Object getCachedValue(String uri, int columnIndex) { 524 ContentValues uriMap = mCacheMap.get(uri); 525 if (uriMap != null) { 526 String columnName; 527 if (columnIndex == DELETED_COLUMN_INDEX) { 528 columnName = DELETED_COLUMN; 529 } else { 530 columnName = mColumnNames[columnIndex]; 531 } 532 return uriMap.get(columnName); 533 } 534 return null; 535 } 536 537 /** 538 * When the underlying cursor changes, we want to alert the listener 539 */ 540 private void underlyingChanged() { 541 synchronized(mCacheMapLock) { 542 if (mCursorObserverRegistered) { 543 try { 544 mUnderlyingCursor.unregisterContentObserver(mCursorObserver); 545 } catch (IllegalStateException e) { 546 // Maybe the cursor was GC'd? 547 } 548 mCursorObserverRegistered = false; 549 } 550 mRefreshRequired = true; 551 if (!mPaused) { 552 notifyRefreshRequired(); 553 } 554 } 555 } 556 557 /** 558 * Must be called on UI thread; notify listeners that a refresh is required 559 */ 560 private void notifyRefreshRequired() { 561 if (DEBUG) { 562 LogUtils.i(TAG, "[Notify %s: onRefreshRequired()]", mName); 563 } 564 if (!mDeferSync) { 565 synchronized(mListeners) { 566 for (ConversationListener listener: mListeners) { 567 listener.onRefreshRequired(); 568 } 569 } 570 } 571 } 572 573 /** 574 * Must be called on UI thread; notify listeners that a new cursor is ready 575 */ 576 private void notifyRefreshReady() { 577 if (DEBUG) { 578 LogUtils.i(TAG, "[Notify %s: onRefreshReady(), %d listeners]", 579 mName, mListeners.size()); 580 } 581 synchronized(mListeners) { 582 for (ConversationListener listener: mListeners) { 583 listener.onRefreshReady(); 584 } 585 } 586 } 587 588 /** 589 * Must be called on UI thread; notify listeners that data has changed 590 */ 591 private void notifyDataChanged() { 592 if (DEBUG) { 593 LogUtils.i(TAG, "[Notify %s: onDataSetChanged()]", mName); 594 } 595 synchronized(mListeners) { 596 for (ConversationListener listener: mListeners) { 597 listener.onDataSetChanged(); 598 } 599 } 600 } 601 602 /** 603 * Put the refreshed cursor in place (called by the UI) 604 */ 605 public void sync() { 606 if (mRequeryCursor == null) { 607 // This can happen during an animated deletion, if the UI isn't keeping track, or 608 // if a new query intervened (i.e. user changed folders) 609 if (DEBUG) { 610 LogUtils.i(TAG, "[sync() %s; no requery cursor]", mName); 611 } 612 return; 613 } 614 synchronized(mCacheMapLock) { 615 if (DEBUG) { 616 LogUtils.i(TAG, "[sync() %s]", mName); 617 } 618 resetCursor(mRequeryCursor); 619 mRequeryCursor = null; 620 mRefreshTask = null; 621 mRefreshReady = false; 622 } 623 notifyDataChanged(); 624 } 625 626 public boolean isRefreshRequired() { 627 return mRefreshRequired; 628 } 629 630 public boolean isRefreshReady() { 631 return mRefreshReady; 632 } 633 634 /** 635 * Cancel a refresh in progress 636 */ 637 public void cancelRefresh() { 638 if (DEBUG) { 639 LogUtils.i(TAG, "[cancelRefresh() %s]", mName); 640 } 641 synchronized(mCacheMapLock) { 642 if (mRefreshTask != null) { 643 mRefreshTask.cancel(true); 644 mRefreshTask = null; 645 } 646 mRefreshReady = false; 647 // If we have the cursor, close it; otherwise, it will get closed when the query 648 // finishes (it checks sRefreshInProgress) 649 if (mRequeryCursor != null) { 650 mRequeryCursor.close(); 651 mRequeryCursor = null; 652 } 653 } 654 } 655 656 /** 657 * Get a list of deletions from ConversationCursor to the refreshed cursor that hasn't yet 658 * been swapped into place; this allows the UI to animate these away if desired 659 * @return a list of positions deleted in ConversationCursor 660 */ 661 public Collection<Conversation> getRefreshDeletions () { 662 return EMPTY_DELETION_LIST; 663 } 664 665 /** 666 * When we get a requery from the UI, we'll do it, but also clear the cache. The listener is 667 * notified when the requery is complete 668 * NOTE: This will have to change, of course, when we start using loaders... 669 */ 670 public boolean refresh() { 671 if (DEBUG) { 672 LogUtils.i(TAG, "[refresh() %s]", mName); 673 } 674 synchronized(mCacheMapLock) { 675 if (mRefreshTask != null) { 676 if (DEBUG) { 677 LogUtils.i(TAG, "[refresh() %s returning; already running %d]", 678 mName, mRefreshTask.hashCode()); 679 } 680 return false; 681 } 682 mRefreshTask = new RefreshTask(); 683 mRefreshTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 684 } 685 return true; 686 } 687 688 public void disable() { 689 close(); 690 mCacheMap.clear(); 691 mListeners.clear(); 692 mUnderlyingCursor = null; 693 } 694 695 @Override 696 public void close() { 697 if (mUnderlyingCursor != null && !mUnderlyingCursor.isClosed()) { 698 // Unregister our observer on the underlying cursor and close as usual 699 if (mCursorObserverRegistered) { 700 try { 701 mUnderlyingCursor.unregisterContentObserver(mCursorObserver); 702 } catch (IllegalStateException e) { 703 // Maybe the cursor got GC'd? 704 } 705 mCursorObserverRegistered = false; 706 } 707 mUnderlyingCursor.close(); 708 } 709 } 710 711 /** 712 * Move to the next not-deleted item in the conversation 713 */ 714 @Override 715 public boolean moveToNext() { 716 while (true) { 717 boolean ret = mUnderlyingCursor.moveToNext(); 718 if (!ret) { 719 mPosition = getCount(); 720 // STOPSHIP 721 LogUtils.i(TAG, "*** moveToNext returns false; pos = %d, und = %d, del = %d", 722 mPosition, mUnderlyingCursor.getPosition(), mDeletedCount); 723 return false; 724 } 725 if (getCachedValue(DELETED_COLUMN_INDEX) instanceof Integer) continue; 726 mPosition++; 727 return true; 728 } 729 } 730 731 /** 732 * Move to the previous not-deleted item in the conversation 733 */ 734 @Override 735 public boolean moveToPrevious() { 736 while (true) { 737 boolean ret = mUnderlyingCursor.moveToPrevious(); 738 if (!ret) { 739 // Make sure we're before the first position 740 mPosition = -1; 741 return false; 742 } 743 if (getCachedValue(DELETED_COLUMN_INDEX) instanceof Integer) continue; 744 mPosition--; 745 return true; 746 } 747 } 748 749 @Override 750 public int getPosition() { 751 return mPosition; 752 } 753 754 /** 755 * The actual cursor's count must be decremented by the number we've deleted from the UI 756 */ 757 @Override 758 public int getCount() { 759 if (mUnderlyingCursor == null) { 760 throw new IllegalStateException( 761 "getCount() on disabled cursor: " + mName + "(" + qUri + ")"); 762 } 763 return mUnderlyingCursor.getCount() - mDeletedCount; 764 } 765 766 @Override 767 public boolean moveToFirst() { 768 if (mUnderlyingCursor == null) { 769 throw new IllegalStateException( 770 "moveToFirst() on disabled cursor: " + mName + "(" + qUri + ")"); 771 } 772 mUnderlyingCursor.moveToPosition(-1); 773 mPosition = -1; 774 return moveToNext(); 775 } 776 777 @Override 778 public boolean moveToPosition(int pos) { 779 if (mUnderlyingCursor == null) { 780 throw new IllegalStateException( 781 "moveToPosition() on disabled cursor: " + mName + "(" + qUri + ")"); 782 } 783 // Handle the "move to first" case before anything else; moveToPosition(0) in an empty 784 // SQLiteCursor moves the position to 0 when returning false, which we will mirror. 785 // But we don't want to return true on a subsequent "move to first", which we would if we 786 // check pos vs mPosition first 787 if (pos == 0) { 788 return moveToFirst(); 789 } else if (pos == mPosition) { 790 return true; 791 } else if (pos > mPosition) { 792 while (pos > mPosition) { 793 if (!moveToNext()) { 794 return false; 795 } 796 } 797 return true; 798 } else if ((mPosition - pos) > pos) { 799 // Optimization if it's easier to move forward to position instead of backward 800 // STOPSHIP (Remove logging) 801 LogUtils.i(TAG, "*** Move from %d to %d, starting from first", mPosition, pos); 802 moveToFirst(); 803 return moveToPosition(pos); 804 } else { 805 while (pos < mPosition) { 806 if (!moveToPrevious()) { 807 return false; 808 } 809 } 810 return true; 811 } 812 } 813 814 /** 815 * Make sure mPosition is correct after locally deleting/undeleting items 816 */ 817 private void recalibratePosition() { 818 int pos = mPosition; 819 moveToFirst(); 820 moveToPosition(pos); 821 } 822 823 @Override 824 public boolean moveToLast() { 825 throw new UnsupportedOperationException("moveToLast unsupported!"); 826 } 827 828 @Override 829 public boolean move(int offset) { 830 throw new UnsupportedOperationException("move unsupported!"); 831 } 832 833 /** 834 * We need to override all of the getters to make sure they look at cached values before using 835 * the values in the underlying cursor 836 */ 837 @Override 838 public double getDouble(int columnIndex) { 839 Object obj = getCachedValue(columnIndex); 840 if (obj != null) return (Double)obj; 841 return mUnderlyingCursor.getDouble(columnIndex); 842 } 843 844 @Override 845 public float getFloat(int columnIndex) { 846 Object obj = getCachedValue(columnIndex); 847 if (obj != null) return (Float)obj; 848 return mUnderlyingCursor.getFloat(columnIndex); 849 } 850 851 @Override 852 public int getInt(int columnIndex) { 853 Object obj = getCachedValue(columnIndex); 854 if (obj != null) return (Integer)obj; 855 return mUnderlyingCursor.getInt(columnIndex); 856 } 857 858 @Override 859 public long getLong(int columnIndex) { 860 Object obj = getCachedValue(columnIndex); 861 if (obj != null) return (Long)obj; 862 return mUnderlyingCursor.getLong(columnIndex); 863 } 864 865 @Override 866 public short getShort(int columnIndex) { 867 Object obj = getCachedValue(columnIndex); 868 if (obj != null) return (Short)obj; 869 return mUnderlyingCursor.getShort(columnIndex); 870 } 871 872 @Override 873 public String getString(int columnIndex) { 874 // If we're asking for the Uri for the conversation list, we return a forwarding URI 875 // so that we can intercept update/delete and handle it ourselves 876 if (columnIndex == sUriColumnIndex) { 877 Uri uri = Uri.parse(mUnderlyingCursor.getString(columnIndex)); 878 return uriToCachingUriString(uri); 879 } 880 Object obj = getCachedValue(columnIndex); 881 if (obj != null) return (String)obj; 882 return mUnderlyingCursor.getString(columnIndex); 883 } 884 885 @Override 886 public byte[] getBlob(int columnIndex) { 887 Object obj = getCachedValue(columnIndex); 888 if (obj != null) return (byte[])obj; 889 return mUnderlyingCursor.getBlob(columnIndex); 890 } 891 892 /** 893 * Observer of changes to underlying data 894 */ 895 private class CursorObserver extends ContentObserver { 896 public CursorObserver(Handler handler) { 897 super(handler); 898 } 899 900 @Override 901 public void onChange(boolean selfChange) { 902 // If we're here, then something outside of the UI has changed the data, and we 903 // must query the underlying provider for that data; 904 ConversationCursor.this.underlyingChanged(); 905 } 906 } 907 908 /** 909 * ConversationProvider is the ContentProvider for our forwarding Uri's; it passes queries 910 * and inserts directly, and caches updates/deletes before passing them through. The caching 911 * will cause a redraw of the list with updated values. 912 */ 913 public abstract static class ConversationProvider extends ContentProvider { 914 public static String AUTHORITY; 915 916 /** 917 * Allows the implementing provider to specify the authority that should be used. 918 */ 919 protected abstract String getAuthority(); 920 921 @Override 922 public boolean onCreate() { 923 sProvider = this; 924 AUTHORITY = getAuthority(); 925 return true; 926 } 927 928 @Override 929 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 930 String sortOrder) { 931 return sResolver.query( 932 uriFromCachingUri(uri), projection, selection, selectionArgs, sortOrder); 933 } 934 935 @Override 936 public Uri insert(Uri uri, ContentValues values) { 937 insertLocal(uri, values); 938 return ProviderExecute.opInsert(uri, values); 939 } 940 941 @Override 942 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 943 throw new IllegalStateException("Unexpected call to ConversationProvider.delete"); 944// updateLocal(uri, values); 945 // return ProviderExecute.opUpdate(uri, values); 946 } 947 948 @Override 949 public int delete(Uri uri, String selection, String[] selectionArgs) { 950 throw new IllegalStateException("Unexpected call to ConversationProvider.delete"); 951 //deleteLocal(uri); 952 // return ProviderExecute.opDelete(uri); 953 } 954 955 @Override 956 public String getType(Uri uri) { 957 return null; 958 } 959 960 /** 961 * Quick and dirty class that executes underlying provider CRUD operations on a background 962 * thread. 963 */ 964 static class ProviderExecute implements Runnable { 965 static final int DELETE = 0; 966 static final int INSERT = 1; 967 static final int UPDATE = 2; 968 969 final int mCode; 970 final Uri mUri; 971 final ContentValues mValues; //HEHEH 972 973 ProviderExecute(int code, Uri uri, ContentValues values) { 974 mCode = code; 975 mUri = uriFromCachingUri(uri); 976 mValues = values; 977 } 978 979 ProviderExecute(int code, Uri uri) { 980 this(code, uri, null); 981 } 982 983 static Uri opInsert(Uri uri, ContentValues values) { 984 ProviderExecute e = new ProviderExecute(INSERT, uri, values); 985 if (offUiThread()) return (Uri)e.go(); 986 new Thread(e).start(); 987 return null; 988 } 989 990 static int opDelete(Uri uri) { 991 ProviderExecute e = new ProviderExecute(DELETE, uri); 992 if (offUiThread()) return (Integer)e.go(); 993 new Thread(new ProviderExecute(DELETE, uri)).start(); 994 return 0; 995 } 996 997 static int opUpdate(Uri uri, ContentValues values) { 998 ProviderExecute e = new ProviderExecute(UPDATE, uri, values); 999 if (offUiThread()) return (Integer)e.go(); 1000 new Thread(e).start(); 1001 return 0; 1002 } 1003 1004 @Override 1005 public void run() { 1006 go(); 1007 } 1008 1009 public Object go() { 1010 switch(mCode) { 1011 case DELETE: 1012 return sResolver.delete(mUri, null, null); 1013 case INSERT: 1014 return sResolver.insert(mUri, mValues); 1015 case UPDATE: 1016 return sResolver.update(mUri, mValues, null, null); 1017 default: 1018 return null; 1019 } 1020 } 1021 } 1022 1023 private void insertLocal(Uri uri, ContentValues values) { 1024 // Placeholder for now; there's no local insert 1025 } 1026 1027 private int mUndoSequence = 0; 1028 private ArrayList<Uri> mUndoDeleteUris = new ArrayList<Uri>(); 1029 1030 void addToUndoSequence(Uri uri) { 1031 if (sSequence != mUndoSequence) { 1032 mUndoSequence = sSequence; 1033 mUndoDeleteUris.clear(); 1034 } 1035 mUndoDeleteUris.add(uri); 1036 } 1037 1038 @VisibleForTesting 1039 void deleteLocal(Uri uri, ConversationCursor conversationCursor) { 1040 String uriString = uriStringFromCachingUri(uri); 1041 conversationCursor.cacheValue(uriString, DELETED_COLUMN, true); 1042 addToUndoSequence(uri); 1043 } 1044 1045 @VisibleForTesting 1046 void undeleteLocal(Uri uri, ConversationCursor conversationCursor) { 1047 String uriString = uriStringFromCachingUri(uri); 1048 conversationCursor.cacheValue(uriString, DELETED_COLUMN, false); 1049 } 1050 1051 void setMostlyDead(Conversation conv, ConversationCursor conversationCursor) { 1052 Uri uri = conv.uri; 1053 String uriString = uriStringFromCachingUri(uri); 1054 conversationCursor.setMostlyDead(uriString, conv); 1055 addToUndoSequence(uri); 1056 } 1057 1058 void commitMostlyDead(Conversation conv, ConversationCursor conversationCursor) { 1059 conversationCursor.commitMostlyDead(conv); 1060 } 1061 1062 boolean clearMostlyDead(Uri uri, ConversationCursor conversationCursor) { 1063 String uriString = uriStringFromCachingUri(uri); 1064 return conversationCursor.clearMostlyDead(uriString); 1065 } 1066 1067 public void undo(ConversationCursor conversationCursor) { 1068 if (sSequence == mUndoSequence) { 1069 for (Uri uri: mUndoDeleteUris) { 1070 if (!clearMostlyDead(uri, conversationCursor)) { 1071 undeleteLocal(uri, conversationCursor); 1072 } 1073 } 1074 mUndoSequence = 0; 1075 conversationCursor.recalibratePosition(); 1076 } 1077 } 1078 1079 @VisibleForTesting 1080 void updateLocal(Uri uri, ContentValues values, ConversationCursor conversationCursor) { 1081 if (values == null) { 1082 return; 1083 } 1084 String uriString = uriStringFromCachingUri(uri); 1085 for (String columnName: values.keySet()) { 1086 conversationCursor.cacheValue(uriString, columnName, values.get(columnName)); 1087 } 1088 } 1089 1090 public int apply(ArrayList<ConversationOperation> ops, 1091 ConversationCursor conversationCursor) { 1092 final HashMap<String, ArrayList<ContentProviderOperation>> batchMap = 1093 new HashMap<String, ArrayList<ContentProviderOperation>>(); 1094 // Increment sequence count 1095 sSequence++; 1096 1097 // Execute locally and build CPO's for underlying provider 1098 boolean recalibrateRequired = false; 1099 for (ConversationOperation op: ops) { 1100 Uri underlyingUri = uriFromCachingUri(op.mUri); 1101 String authority = underlyingUri.getAuthority(); 1102 ArrayList<ContentProviderOperation> authOps = batchMap.get(authority); 1103 if (authOps == null) { 1104 authOps = new ArrayList<ContentProviderOperation>(); 1105 batchMap.put(authority, authOps); 1106 } 1107 ContentProviderOperation cpo = op.execute(underlyingUri); 1108 if (cpo != null) { 1109 authOps.add(cpo); 1110 } 1111 // Keep track of whether our operations require recalibrating the cursor position 1112 if (op.mRecalibrateRequired) { 1113 recalibrateRequired = true; 1114 } 1115 } 1116 1117 // Recalibrate cursor position if required 1118 if (recalibrateRequired) { 1119 conversationCursor.recalibratePosition(); 1120 } 1121 1122 // Notify listeners that data has changed 1123 conversationCursor.notifyDataChanged(); 1124 1125 // Send changes to underlying provider 1126 for (String authority: batchMap.keySet()) { 1127 try { 1128 if (offUiThread()) { 1129 sResolver.applyBatch(authority, batchMap.get(authority)); 1130 } else { 1131 final String auth = authority; 1132 new Thread(new Runnable() { 1133 @Override 1134 public void run() { 1135 try { 1136 sResolver.applyBatch(auth, batchMap.get(auth)); 1137 } catch (RemoteException e) { 1138 } catch (OperationApplicationException e) { 1139 } 1140 } 1141 }).start(); 1142 } 1143 } catch (RemoteException e) { 1144 } catch (OperationApplicationException e) { 1145 } 1146 } 1147 return sSequence; 1148 } 1149 } 1150 1151 void setMostlyDead(String uriString, Conversation conv) { 1152 LogUtils.i(TAG, "[Mostly dead, deferring: %s] ", uriString); 1153 cacheValue(uriString, 1154 UIProvider.ConversationColumns.FLAGS, Conversation.FLAG_MOSTLY_DEAD); 1155 conv.convFlags |= Conversation.FLAG_MOSTLY_DEAD; 1156 sMostlyDead.add(conv); 1157 mDeferSync = true; 1158 } 1159 1160 void commitMostlyDead(Conversation conv) { 1161 conv.convFlags &= ~Conversation.FLAG_MOSTLY_DEAD; 1162 sMostlyDead.remove(conv); 1163 LogUtils.i(TAG, "[All dead: %s]", conv.uri); 1164 if (sMostlyDead.isEmpty()) { 1165 mDeferSync = false; 1166 checkNotifyUI(); 1167 } 1168 } 1169 1170 boolean clearMostlyDead(String uriString) { 1171 Object val = getCachedValue(uriString, 1172 UIProvider.CONVERSATION_FLAGS_COLUMN); 1173 if (val != null) { 1174 int flags = ((Integer)val).intValue(); 1175 if ((flags & Conversation.FLAG_MOSTLY_DEAD) != 0) { 1176 cacheValue(uriString, UIProvider.ConversationColumns.FLAGS, 1177 flags &= ~Conversation.FLAG_MOSTLY_DEAD); 1178 return true; 1179 } 1180 } 1181 return false; 1182 } 1183 1184 1185 1186 1187 /** 1188 * ConversationOperation is the encapsulation of a ContentProvider operation to be performed 1189 * atomically as part of a "batch" operation. 1190 */ 1191 public class ConversationOperation { 1192 private static final int MOSTLY = 0x80; 1193 public static final int DELETE = 0; 1194 public static final int INSERT = 1; 1195 public static final int UPDATE = 2; 1196 public static final int ARCHIVE = 3; 1197 public static final int MUTE = 4; 1198 public static final int REPORT_SPAM = 5; 1199 public static final int REPORT_NOT_SPAM = 6; 1200 public static final int REPORT_PHISHING = 7; 1201 public static final int MOSTLY_ARCHIVE = MOSTLY | ARCHIVE; 1202 public static final int MOSTLY_DELETE = MOSTLY | DELETE; 1203 public static final int MOSTLY_DESTRUCTIVE_UPDATE = MOSTLY | UPDATE; 1204 1205 private final int mType; 1206 private final Uri mUri; 1207 private final Conversation mConversation; 1208 private final ContentValues mValues; 1209 // True if an updated item should be removed locally (from ConversationCursor) 1210 // This would be the case for a folder change in which the conversation is no longer 1211 // in the folder represented by the ConversationCursor 1212 private final boolean mLocalDeleteOnUpdate; 1213 // After execution, this indicates whether or not the operation requires recalibration of 1214 // the current cursor position (i.e. it removed or added items locally) 1215 private boolean mRecalibrateRequired = true; 1216 // Whether this item is already mostly dead 1217 private final boolean mMostlyDead; 1218 1219 public ConversationOperation(int type, Conversation conv) { 1220 this(type, conv, null); 1221 } 1222 1223 public ConversationOperation(int type, Conversation conv, ContentValues values) { 1224 mType = type; 1225 mUri = conv.uri; 1226 mConversation = conv; 1227 mValues = values; 1228 mLocalDeleteOnUpdate = conv.localDeleteOnUpdate; 1229 mMostlyDead = conv.isMostlyDead(); 1230 } 1231 1232 private ContentProviderOperation execute(Uri underlyingUri) { 1233 Uri uri = underlyingUri.buildUpon() 1234 .appendQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER, 1235 Integer.toString(sSequence)) 1236 .build(); 1237 ContentProviderOperation op = null; 1238 switch(mType) { 1239 case UPDATE: 1240 if (mLocalDeleteOnUpdate) { 1241 sProvider.deleteLocal(mUri, ConversationCursor.this); 1242 } else { 1243 sProvider.updateLocal(mUri, mValues, ConversationCursor.this); 1244 mRecalibrateRequired = false; 1245 } 1246 if (!mMostlyDead) { 1247 op = ContentProviderOperation.newUpdate(uri) 1248 .withValues(mValues) 1249 .build(); 1250 } else { 1251 sProvider.commitMostlyDead(mConversation, ConversationCursor.this); 1252 } 1253 break; 1254 case MOSTLY_DESTRUCTIVE_UPDATE: 1255 sProvider.setMostlyDead(mConversation, ConversationCursor.this); 1256 op = ContentProviderOperation.newUpdate(uri).withValues(mValues).build(); 1257 break; 1258 case INSERT: 1259 sProvider.insertLocal(mUri, mValues); 1260 op = ContentProviderOperation.newInsert(uri) 1261 .withValues(mValues).build(); 1262 break; 1263 // Destructive actions below! 1264 // "Mostly" operations are reflected globally, but not locally, except to set 1265 // FLAG_MOSTLY_DEAD in the conversation itself 1266 case DELETE: 1267 sProvider.deleteLocal(mUri, ConversationCursor.this); 1268 if (!mMostlyDead) { 1269 op = ContentProviderOperation.newDelete(uri).build(); 1270 } else { 1271 sProvider.commitMostlyDead(mConversation, ConversationCursor.this); 1272 } 1273 break; 1274 case MOSTLY_DELETE: 1275 sProvider.setMostlyDead(mConversation,ConversationCursor.this); 1276 op = ContentProviderOperation.newDelete(uri).build(); 1277 break; 1278 case ARCHIVE: 1279 sProvider.deleteLocal(mUri, ConversationCursor.this); 1280 if (!mMostlyDead) { 1281 // Create an update operation that represents archive 1282 op = ContentProviderOperation.newUpdate(uri).withValue( 1283 ConversationOperations.OPERATION_KEY, 1284 ConversationOperations.ARCHIVE) 1285 .build(); 1286 } else { 1287 sProvider.commitMostlyDead(mConversation, ConversationCursor.this); 1288 } 1289 break; 1290 case MOSTLY_ARCHIVE: 1291 sProvider.setMostlyDead(mConversation, ConversationCursor.this); 1292 // Create an update operation that represents archive 1293 op = ContentProviderOperation.newUpdate(uri).withValue( 1294 ConversationOperations.OPERATION_KEY, ConversationOperations.ARCHIVE) 1295 .build(); 1296 break; 1297 case MUTE: 1298 if (mLocalDeleteOnUpdate) { 1299 sProvider.deleteLocal(mUri, ConversationCursor.this); 1300 } 1301 1302 // Create an update operation that represents mute 1303 op = ContentProviderOperation.newUpdate(uri).withValue( 1304 ConversationOperations.OPERATION_KEY, ConversationOperations.MUTE) 1305 .build(); 1306 break; 1307 case REPORT_SPAM: 1308 case REPORT_NOT_SPAM: 1309 sProvider.deleteLocal(mUri, ConversationCursor.this); 1310 1311 final String operation = mType == REPORT_SPAM ? 1312 ConversationOperations.REPORT_SPAM : 1313 ConversationOperations.REPORT_NOT_SPAM; 1314 1315 // Create an update operation that represents report spam 1316 op = ContentProviderOperation.newUpdate(uri).withValue( 1317 ConversationOperations.OPERATION_KEY, operation).build(); 1318 break; 1319 case REPORT_PHISHING: 1320 sProvider.deleteLocal(mUri, ConversationCursor.this); 1321 1322 // Create an update operation that represents report spam 1323 op = ContentProviderOperation.newUpdate(uri).withValue( 1324 ConversationOperations.OPERATION_KEY, 1325 ConversationOperations.REPORT_PHISHING).build(); 1326 break; 1327 default: 1328 throw new UnsupportedOperationException( 1329 "No such ConversationOperation type: " + mType); 1330 } 1331 1332 return op; 1333 } 1334 } 1335 1336 /** 1337 * For now, a single listener can be associated with the cursor, and for now we'll just 1338 * notify on deletions 1339 */ 1340 public interface ConversationListener { 1341 /** 1342 * Data in the underlying provider has changed; a refresh is required to sync up 1343 */ 1344 public void onRefreshRequired(); 1345 /** 1346 * We've completed a requested refresh of the underlying cursor 1347 */ 1348 public void onRefreshReady(); 1349 /** 1350 * The data underlying the cursor has changed; the UI should redraw the list 1351 */ 1352 public void onDataSetChanged(); 1353 } 1354 1355 @Override 1356 public boolean isFirst() { 1357 throw new UnsupportedOperationException(); 1358 } 1359 1360 @Override 1361 public boolean isLast() { 1362 throw new UnsupportedOperationException(); 1363 } 1364 1365 @Override 1366 public boolean isBeforeFirst() { 1367 throw new UnsupportedOperationException(); 1368 } 1369 1370 @Override 1371 public boolean isAfterLast() { 1372 throw new UnsupportedOperationException(); 1373 } 1374 1375 @Override 1376 public int getColumnIndex(String columnName) { 1377 return mUnderlyingCursor.getColumnIndex(columnName); 1378 } 1379 1380 @Override 1381 public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { 1382 return mUnderlyingCursor.getColumnIndexOrThrow(columnName); 1383 } 1384 1385 @Override 1386 public String getColumnName(int columnIndex) { 1387 return mUnderlyingCursor.getColumnName(columnIndex); 1388 } 1389 1390 @Override 1391 public String[] getColumnNames() { 1392 return mUnderlyingCursor.getColumnNames(); 1393 } 1394 1395 @Override 1396 public int getColumnCount() { 1397 return mUnderlyingCursor.getColumnCount(); 1398 } 1399 1400 @Override 1401 public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { 1402 throw new UnsupportedOperationException(); 1403 } 1404 1405 @Override 1406 public int getType(int columnIndex) { 1407 return mUnderlyingCursor.getType(columnIndex); 1408 } 1409 1410 @Override 1411 public boolean isNull(int columnIndex) { 1412 throw new UnsupportedOperationException(); 1413 } 1414 1415 @Override 1416 public void deactivate() { 1417 throw new UnsupportedOperationException(); 1418 } 1419 1420 @Override 1421 public boolean isClosed() { 1422 return mUnderlyingCursor == null || mUnderlyingCursor.isClosed(); 1423 } 1424 1425 @Override 1426 public void registerContentObserver(ContentObserver observer) { 1427 // Nope. We never notify of underlying changes on this channel, since the cursor watches 1428 // internally and offers onRefreshRequired/onRefreshReady to accomplish the same thing. 1429 } 1430 1431 @Override 1432 public void unregisterContentObserver(ContentObserver observer) { 1433 // See above. 1434 } 1435 1436 @Override 1437 public void registerDataSetObserver(DataSetObserver observer) { 1438 // Nope. We use ConversationListener to accomplish this. 1439 } 1440 1441 @Override 1442 public void unregisterDataSetObserver(DataSetObserver observer) { 1443 // See above. 1444 } 1445 1446 @Override 1447 public void setNotificationUri(ContentResolver cr, Uri uri) { 1448 throw new UnsupportedOperationException(); 1449 } 1450 1451 @Override 1452 public boolean getWantsAllOnMoveCalls() { 1453 throw new UnsupportedOperationException(); 1454 } 1455 1456 @Override 1457 public Bundle getExtras() { 1458 return mUnderlyingCursor != null ? mUnderlyingCursor.getExtras() : Bundle.EMPTY; 1459 } 1460 1461 @Override 1462 public Bundle respond(Bundle extras) { 1463 if (mUnderlyingCursor != null) { 1464 return mUnderlyingCursor.respond(extras); 1465 } 1466 return Bundle.EMPTY; 1467 } 1468 1469 @Override 1470 public boolean requery() { 1471 return true; 1472 } 1473 1474 // Below are methods that update Conversation data (update/delete) 1475 1476 public int updateBoolean(Context context, Conversation conversation, String columnName, 1477 boolean value) { 1478 return updateBoolean(context, Arrays.asList(conversation), columnName, value); 1479 } 1480 1481 /** 1482 * Update an integer column for a group of conversations (see updateValues below) 1483 */ 1484 public int updateInt(Context context, Collection<Conversation> conversations, 1485 String columnName, int value) { 1486 ContentValues cv = new ContentValues(); 1487 cv.put(columnName, value); 1488 return updateValues(context, conversations, cv); 1489 } 1490 1491 /** 1492 * Update a string column for a group of conversations (see updateValues below) 1493 */ 1494 public int updateBoolean(Context context, Collection<Conversation> conversations, 1495 String columnName, boolean value) { 1496 ContentValues cv = new ContentValues(); 1497 cv.put(columnName, value); 1498 return updateValues(context, conversations, cv); 1499 } 1500 1501 /** 1502 * Update a string column for a group of conversations (see updateValues below) 1503 */ 1504 public int updateString(Context context, Collection<Conversation> conversations, 1505 String columnName, String value) { 1506 return updateStrings(context, conversations, new String[] { 1507 columnName 1508 }, new String[] { 1509 value 1510 }); 1511 } 1512 1513 /** 1514 * Update a string columns for a group of conversations (see updateValues below) 1515 */ 1516 public int updateStrings(Context context, Collection<Conversation> conversations, 1517 String[] columnNames, String[] values) { 1518 ContentValues cv = new ContentValues(); 1519 for (int i = 0; i < columnNames.length; i++) { 1520 cv.put(columnNames[i], values[i]); 1521 } 1522 return updateValues(context, conversations, cv); 1523 } 1524 1525 public int updateBoolean(Context context, String conversationUri, String columnName, 1526 boolean value) { 1527 Conversation conv = new Conversation(); 1528 conv.uri = Uri.parse(conversationUri); 1529 return updateBoolean(context, conv, columnName, value); 1530 } 1531 1532 /** 1533 * Update a boolean column for a group of conversations, immediately in the UI and in a single 1534 * transaction in the underlying provider 1535 * @param context the caller's context 1536 * @param conversations a collection of conversations 1537 * @param values the data to update 1538 * @return the sequence number of the operation (for undo) 1539 */ 1540 public int updateValues(Context context, Collection<Conversation> conversations, 1541 ContentValues values) { 1542 return apply(context, 1543 getOperationsForConversations(conversations, ConversationOperation.UPDATE, values)); 1544 } 1545 1546 private ArrayList<ConversationOperation> getOperationsForConversations( 1547 Collection<Conversation> conversations, int type, ContentValues values) { 1548 final ArrayList<ConversationOperation> ops = Lists.newArrayList(); 1549 for (Conversation conv: conversations) { 1550 ConversationOperation op = new ConversationOperation(type, conv, values); 1551 ops.add(op); 1552 } 1553 return ops; 1554 } 1555 1556 /** 1557 * Delete a single conversation 1558 * @param context the caller's context 1559 * @return the sequence number of the operation (for undo) 1560 */ 1561 public int delete(Context context, Conversation conversation) { 1562 ArrayList<Conversation> conversations = new ArrayList<Conversation>(); 1563 conversations.add(conversation); 1564 return delete(context, conversations); 1565 } 1566 1567 /** 1568 * Delete a single conversation 1569 * @param context the caller's context 1570 * @return the sequence number of the operation (for undo) 1571 */ 1572 public int mostlyArchive(Context context, Conversation conversation) { 1573 ArrayList<Conversation> conversations = new ArrayList<Conversation>(); 1574 conversations.add(conversation); 1575 return archive(context, conversations); 1576 } 1577 1578 /** 1579 * Delete a single conversation 1580 * @param context the caller's context 1581 * @return the sequence number of the operation (for undo) 1582 */ 1583 public int mostlyDelete(Context context, Conversation conversation) { 1584 ArrayList<Conversation> conversations = new ArrayList<Conversation>(); 1585 conversations.add(conversation); 1586 return delete(context, conversations); 1587 } 1588 1589 // Convenience methods 1590 private int apply(Context context, ArrayList<ConversationOperation> operations) { 1591 return sProvider.apply(operations, this); 1592 } 1593 1594 private void undoLocal() { 1595 sProvider.undo(this); 1596 } 1597 1598 public void undo(final Context context, final Uri undoUri) { 1599 new Thread(new Runnable() { 1600 @Override 1601 public void run() { 1602 Cursor c = context.getContentResolver().query(undoUri, UIProvider.UNDO_PROJECTION, 1603 null, null, null); 1604 if (c != null) { 1605 c.close(); 1606 } 1607 } 1608 }).start(); 1609 undoLocal(); 1610 } 1611 1612 /** 1613 * Delete a group of conversations immediately in the UI and in a single transaction in the 1614 * underlying provider. See applyAction for argument descriptions 1615 */ 1616 public int delete(Context context, Collection<Conversation> conversations) { 1617 return applyAction(context, conversations, ConversationOperation.DELETE); 1618 } 1619 1620 /** 1621 * As above, for archive 1622 */ 1623 public int archive(Context context, Collection<Conversation> conversations) { 1624 return applyAction(context, conversations, ConversationOperation.ARCHIVE); 1625 } 1626 1627 /** 1628 * As above, for mute 1629 */ 1630 public int mute(Context context, Collection<Conversation> conversations) { 1631 return applyAction(context, conversations, ConversationOperation.MUTE); 1632 } 1633 1634 /** 1635 * As above, for report spam 1636 */ 1637 public int reportSpam(Context context, Collection<Conversation> conversations) { 1638 return applyAction(context, conversations, ConversationOperation.REPORT_SPAM); 1639 } 1640 1641 /** 1642 * As above, for report not spam 1643 */ 1644 public int reportNotSpam(Context context, Collection<Conversation> conversations) { 1645 return applyAction(context, conversations, ConversationOperation.REPORT_NOT_SPAM); 1646 } 1647 1648 /** 1649 * As above, for report phishing 1650 */ 1651 public int reportPhishing(Context context, Collection<Conversation> conversations) { 1652 return applyAction(context, conversations, ConversationOperation.REPORT_PHISHING); 1653 } 1654 1655 /** 1656 * As above, for mostly archive 1657 */ 1658 public int mostlyArchive(Context context, Collection<Conversation> conversations) { 1659 return applyAction(context, conversations, ConversationOperation.MOSTLY_ARCHIVE); 1660 } 1661 1662 /** 1663 * As above, for mostly delete 1664 */ 1665 public int mostlyDelete(Context context, Collection<Conversation> conversations) { 1666 return applyAction(context, conversations, ConversationOperation.MOSTLY_DELETE); 1667 } 1668 1669 /** 1670 * As above, for mostly destructive updates 1671 */ 1672 public int mostlyDestructiveUpdate(Context context, Collection<Conversation> conversations, 1673 String column, String value) { 1674 return mostlyDestructiveUpdate(context, conversations, new String[] { 1675 column 1676 }, new String[] { 1677 value 1678 }); 1679 } 1680 1681 /** 1682 * As above, for mostly destructive updates. 1683 */ 1684 public int mostlyDestructiveUpdate(Context context, Collection<Conversation> conversations, 1685 String[] columnNames, String[] values) { 1686 ContentValues cv = new ContentValues(); 1687 for (int i = 0; i < columnNames.length; i++) { 1688 cv.put(columnNames[i], values[i]); 1689 } 1690 return apply( 1691 context, 1692 getOperationsForConversations(conversations, 1693 ConversationOperation.MOSTLY_DESTRUCTIVE_UPDATE, cv)); 1694 } 1695 1696 /** 1697 * Convenience method for performing an operation on a group of conversations 1698 * @param context the caller's context 1699 * @param conversations the conversations to be affected 1700 * @param opAction the action to take 1701 * @return the sequence number of the operation applied in CC 1702 */ 1703 private int applyAction(Context context, Collection<Conversation> conversations, 1704 int opAction) { 1705 ArrayList<ConversationOperation> ops = Lists.newArrayList(); 1706 for (Conversation conv: conversations) { 1707 ConversationOperation op = 1708 new ConversationOperation(opAction, conv); 1709 ops.add(op); 1710 } 1711 return apply(context, ops); 1712 } 1713 1714} 1715