blob: e1c3f9ce83a63371d6a88d45762cca2c9e554fa0 [file] [log] [blame]
Marc Blankc8a99422012-01-19 14:27:47 -08001/*******************************************************************************
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
Marc Blank48eba7a2012-01-27 16:16:19 -080020import android.app.Activity;
Marc Blankc8a99422012-01-19 14:27:47 -080021import android.content.ContentProvider;
Marc Blank8d69d4e2012-01-25 12:04:28 -080022import android.content.ContentProviderOperation;
Marc Blankc8a99422012-01-19 14:27:47 -080023import android.content.ContentResolver;
24import android.content.ContentValues;
Marc Blank8d69d4e2012-01-25 12:04:28 -080025import android.content.OperationApplicationException;
Marc Blank48eba7a2012-01-27 16:16:19 -080026import android.database.CharArrayBuffer;
Marc Blankc8a99422012-01-19 14:27:47 -080027import android.database.ContentObserver;
28import android.database.Cursor;
Marc Blank48eba7a2012-01-27 16:16:19 -080029import android.database.DataSetObserver;
Marc Blankc8a99422012-01-19 14:27:47 -080030import android.net.Uri;
Marc Blank48eba7a2012-01-27 16:16:19 -080031import android.os.Bundle;
Marc Blank8d69d4e2012-01-25 12:04:28 -080032import android.os.Looper;
33import android.os.RemoteException;
Marc Blankc8a99422012-01-19 14:27:47 -080034import android.util.Log;
Marc Blankc8a99422012-01-19 14:27:47 -080035
Marc Blankf892f0a2012-01-30 13:04:10 -080036import com.android.mail.providers.Conversation;
Marc Blank4015c182012-01-31 12:38:36 -080037import com.android.mail.providers.UIProvider;
Paul Westbrook334e64a2012-02-23 13:26:35 -080038import com.android.mail.providers.UIProvider.ConversationOperations;
Marc Blank248b1b42012-02-07 13:43:02 -080039import com.google.common.annotations.VisibleForTesting;
Marc Blankf892f0a2012-01-30 13:04:10 -080040
Marc Blank97bca7b2012-01-24 11:17:00 -080041import java.util.ArrayList;
Marc Blankc8a99422012-01-19 14:27:47 -080042import java.util.HashMap;
Marc Blank48eba7a2012-01-27 16:16:19 -080043import java.util.Iterator;
Marc Blankc8a99422012-01-19 14:27:47 -080044import 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 */
Marc Blank48eba7a2012-01-27 16:16:19 -080051public final class ConversationCursor implements Cursor {
Marc Blankc8a99422012-01-19 14:27:47 -080052 private static final String TAG = "ConversationCursor";
53 private static final boolean DEBUG = true; // STOPSHIP Set to false before shipping
54
Marc Blank48eba7a2012-01-27 16:16:19 -080055 // The cursor instantiator's activity
56 private static Activity sActivity;
57 // The cursor underlying the caching cursor
Marc Blank248b1b42012-02-07 13:43:02 -080058 @VisibleForTesting
59 static Cursor sUnderlyingCursor;
Marc Blank48eba7a2012-01-27 16:16:19 -080060 // The new cursor obtained via a requery
61 private static Cursor sRequeryCursor;
Marc Blankc8a99422012-01-19 14:27:47 -080062 // A mapping from Uri to updated ContentValues
63 private static HashMap<String, ContentValues> sCacheMap = new HashMap<String, ContentValues>();
Marc Blank48eba7a2012-01-27 16:16:19 -080064 // Cache map lock (will be used only very briefly - few ms at most)
65 private static Object sCacheMapLock = new Object();
Marc Blankc8a99422012-01-19 14:27:47 -080066 // A deleted row is indicated by the presence of DELETED_COLUMN in the cache map
67 private static final String DELETED_COLUMN = "__deleted__";
Marc Blank48eba7a2012-01-27 16:16:19 -080068 // 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__";
Marc Blankc8a99422012-01-19 14:27:47 -080070 // 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;
Marc Blank97bca7b2012-01-24 11:17:00 -080072 // 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;
Marc Blankb600a832012-02-16 09:20:18 -080077 // The listeners registered for this cursor
78 private static ArrayList<ConversationListener> sListeners =
79 new ArrayList<ConversationListener>();
Marc Blank8d69d4e2012-01-25 12:04:28 -080080 // The ConversationProvider instance
Marc Blank248b1b42012-02-07 13:43:02 -080081 @VisibleForTesting
82 static ConversationProvider sProvider;
Marc Blank4e25c942012-02-02 19:41:14 -080083 // 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;
Marc Blankb31ab5a2012-02-01 12:28:29 -080089 // Our sequence count (for changes sent to underlying provider)
90 private static int sSequence = 0;
Marc Blankc8a99422012-01-19 14:27:47 -080091
Marc Blankc8a99422012-01-19 14:27:47 -080092 // Column names for this cursor
93 private final String[] mColumnNames;
Marc Blankc8a99422012-01-19 14:27:47 -080094 // 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;
Marc Blank97bca7b2012-01-24 11:17:00 -080098 // Whether our observer is currently registered with the underlying cursor
99 private boolean mCursorObserverRegistered = false;
Marc Blankc8a99422012-01-19 14:27:47 -0800100
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
Marc Blank48eba7a2012-01-27 16:16:19 -0800106 // 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();
Marc Blank97bca7b2012-01-24 11:17:00 -0800116 sConversationCursor = this;
Marc Blank48eba7a2012-01-27 16:16:19 -0800117 sUnderlyingCursor = cursor;
Marc Blankb600a832012-02-16 09:20:18 -0800118 sListeners.clear();
Marc Blankc8a99422012-01-19 14:27:47 -0800119 mCursorObserver = new CursorObserver();
Marc Blank48eba7a2012-01-27 16:16:19 -0800120 resetCursor(null);
Marc Blankc8a99422012-01-19 14:27:47 -0800121 mColumnNames = cursor.getColumnNames();
Marc Blank48eba7a2012-01-27 16:16:19 -0800122 sUriColumnIndex = cursor.getColumnIndex(messageListColumn);
Marc Blank97bca7b2012-01-24 11:17:00 -0800123 if (sUriColumnIndex < 0) {
Marc Blankc8a99422012-01-19 14:27:47 -0800124 throw new IllegalArgumentException("Cursor must include a message list column");
125 }
Marc Blankc8a99422012-01-19 14:27:47 -0800126 }
127
128 /**
Marc Blank48eba7a2012-01-27 16:16:19 -0800129 * 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
Marc Blankc8a99422012-01-19 14:27:47 -0800138 */
Marc Blank48eba7a2012-01-27 16:16:19 -0800139 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 }
Marc Blank97bca7b2012-01-24 11:17:00 -0800162 }
Marc Blank48eba7a2012-01-27 16:16:19 -0800163 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();
Marc Blank3f1eb852012-02-03 15:38:01 -0800176 if (DEBUG) {
177 Log.d(TAG, "[--resetCursor--]");
178 }
Marc Blank48eba7a2012-01-27 16:16:19 -0800179 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) {
Marc Blankdd10bc82012-02-01 19:10:46 -0800212 close();
Marc Blank48eba7a2012-01-27 16:16:19 -0800213 sUnderlyingCursor = newCursor;
214 }
215
216 mPosition = -1;
217 sUnderlyingCursor.moveToPosition(mPosition);
218 if (!mCursorObserverRegistered) {
219 sUnderlyingCursor.registerContentObserver(mCursorObserver);
220 mCursorObserverRegistered = true;
221 }
Marc Blank4e25c942012-02-02 19:41:14 -0800222 sRefreshRequired = false;
Marc Blank48eba7a2012-01-27 16:16:19 -0800223 }
224 Log.d(TAG, "resetCache time: " + ((System.currentTimeMillis() - startTime)) + "ms");
Marc Blankc8a99422012-01-19 14:27:47 -0800225 }
226
227 /**
Marc Blankb600a832012-02-16 09:20:18 -0800228 * Add a listener for this cursor; we'll notify it when our data changes
Marc Blankc8a99422012-01-19 14:27:47 -0800229 */
Marc Blankb600a832012-02-16 09:20:18 -0800230 public void addListener(ConversationListener listener) {
Marc Blankb33465d2012-02-24 11:15:00 -0800231 synchronized (sListeners) {
232 sListeners.add(listener);
233 }
Marc Blankb600a832012-02-16 09:20:18 -0800234 }
235
236 /**
237 * Remove a listener for this cursor
238 */
239 public void removeListener(ConversationListener listener) {
Marc Blankb33465d2012-02-24 11:15:00 -0800240 synchronized(sListeners) {
241 sListeners.remove(listener);
242 }
Marc Blankc8a99422012-01-19 14:27:47 -0800243 }
244
245 /**
246 * Generate a forwarding Uri to ConversationProvider from an original Uri. We do this by
247 * changing the authority to ours, but otherwise leaving the Uri intact.
248 * NOTE: This won't handle query parameters, so the functionality will need to be added if
249 * parameters are used in the future
250 * @param uri the uri
251 * @return a forwarding uri to ConversationProvider
252 */
253 private static String uriToCachingUriString (Uri uri) {
254 String provider = uri.getAuthority();
Paul Westbrook77177b12012-02-07 15:23:42 -0800255 return uri.getScheme() + "://" + ConversationProvider.AUTHORITY
256 + "/" + provider + uri.getPath();
Marc Blankc8a99422012-01-19 14:27:47 -0800257 }
258
259 /**
260 * Regenerate the original Uri from a forwarding (ConversationProvider) Uri
261 * NOTE: See note above for uriToCachingUri
262 * @param uri the forwarding Uri
263 * @return the original Uri
264 */
265 private static Uri uriFromCachingUri(Uri uri) {
266 List<String> path = uri.getPathSegments();
267 Uri.Builder builder = new Uri.Builder().scheme(uri.getScheme()).authority(path.get(0));
268 for (int i = 1; i < path.size(); i++) {
269 builder.appendPath(path.get(i));
270 }
271 return builder.build();
272 }
273
274 /**
275 * Cache a column name/value pair for a given Uri
276 * @param uriString the Uri for which the column name/value pair applies
277 * @param columnName the column name
278 * @param value the value to be cached
279 */
280 private static void cacheValue(String uriString, String columnName, Object value) {
Marc Blank48eba7a2012-01-27 16:16:19 -0800281 synchronized (sCacheMapLock) {
282 // Get the map for our uri
283 ContentValues map = sCacheMap.get(uriString);
284 // Create one if necessary
285 if (map == null) {
286 map = new ContentValues();
287 sCacheMap.put(uriString, map);
Marc Blankc8a99422012-01-19 14:27:47 -0800288 }
Marc Blank48eba7a2012-01-27 16:16:19 -0800289 // If we're caching a deletion, add to our count
290 if ((columnName == DELETED_COLUMN) && (map.get(columnName) == null)) {
291 sDeletedCount++;
292 if (DEBUG) {
293 Log.d(TAG, "Deleted " + uriString);
294 }
Marc Blank97bca7b2012-01-24 11:17:00 -0800295 }
Marc Blank48eba7a2012-01-27 16:16:19 -0800296 // ContentValues has no generic "put", so we must test. For now, the only classes of
297 // values implemented are Boolean/Integer/String, though others are trivially added
298 if (value instanceof Boolean) {
299 map.put(columnName, ((Boolean) value).booleanValue() ? 1 : 0);
300 } else if (value instanceof Integer) {
301 map.put(columnName, (Integer) value);
302 } else if (value instanceof String) {
303 map.put(columnName, (String) value);
304 } else {
305 String cname = value.getClass().getName();
306 throw new IllegalArgumentException("Value class not compatible with cache: "
307 + cname);
308 }
Marc Blank4e25c942012-02-02 19:41:14 -0800309 if (sRefreshInProgress) {
Marc Blank48eba7a2012-01-27 16:16:19 -0800310 map.put(REQUERY_COLUMN, 1);
311 }
312 if (DEBUG && (columnName != DELETED_COLUMN)) {
313 Log.d(TAG, "Caching value for " + uriString + ": " + columnName);
314 }
Marc Blankc8a99422012-01-19 14:27:47 -0800315 }
316 }
317
318 /**
319 * Get the cached value for the provided column; we special case -1 as the "deleted" column
320 * @param columnIndex the index of the column whose cached value we want to retrieve
321 * @return the cached value for this column, or null if there is none
322 */
323 private Object getCachedValue(int columnIndex) {
Marc Blank48eba7a2012-01-27 16:16:19 -0800324 String uri = sUnderlyingCursor.getString(sUriColumnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800325 ContentValues uriMap = sCacheMap.get(uri);
326 if (uriMap != null) {
327 String columnName;
328 if (columnIndex == DELETED_COLUMN_INDEX) {
329 columnName = DELETED_COLUMN;
330 } else {
331 columnName = mColumnNames[columnIndex];
332 }
333 return uriMap.get(columnName);
334 }
335 return null;
336 }
337
338 /**
Marc Blank97bca7b2012-01-24 11:17:00 -0800339 * When the underlying cursor changes, we want to alert the listener
Marc Blankc8a99422012-01-19 14:27:47 -0800340 */
341 private void underlyingChanged() {
Marc Blankb600a832012-02-16 09:20:18 -0800342 if (mCursorObserverRegistered) {
Marc Blankf9d87192012-02-16 10:50:41 -0800343 try {
344 sUnderlyingCursor.unregisterContentObserver(mCursorObserver);
345 } catch (IllegalStateException e) {
346 // Maybe the cursor was GC'd?
347 }
Marc Blankb600a832012-02-16 09:20:18 -0800348 mCursorObserverRegistered = false;
Marc Blank97bca7b2012-01-24 11:17:00 -0800349 }
Marc Blankb600a832012-02-16 09:20:18 -0800350 if (DEBUG) {
351 Log.d(TAG, "[Notify: onRefreshRequired()]");
352 }
Marc Blankb33465d2012-02-24 11:15:00 -0800353 synchronized(sListeners) {
354 for (ConversationListener listener: sListeners) {
355 listener.onRefreshRequired();
356 }
Marc Blankb600a832012-02-16 09:20:18 -0800357 }
358 sRefreshRequired = true;
Marc Blankc8a99422012-01-19 14:27:47 -0800359 }
360
Marc Blank4015c182012-01-31 12:38:36 -0800361 /**
362 * Put the refreshed cursor in place (called by the UI)
363 */
Marc Blank4e25c942012-02-02 19:41:14 -0800364 // NOTE: We don't like the name (it implies syncing with the server); suggestions gladly
365 // taken - reset? syncToUnderlying? completeRefresh? align?
366 public void sync() {
Marc Blank3f1eb852012-02-03 15:38:01 -0800367 if (DEBUG) {
368 Log.d(TAG, "[sync() called]");
369 }
Marc Blank48eba7a2012-01-27 16:16:19 -0800370 if (sRequeryCursor == null) {
371 throw new IllegalStateException("Can't swap cursors; no requery done");
372 }
373 resetCursor(sRequeryCursor);
374 sRequeryCursor = null;
Marc Blank4e25c942012-02-02 19:41:14 -0800375 sRefreshInProgress = false;
376 sRefreshReady = false;
377 }
378
379 public boolean isRefreshRequired() {
380 return sRefreshRequired;
381 }
382
383 public boolean isRefreshReady() {
384 return sRefreshReady;
Marc Blank4015c182012-01-31 12:38:36 -0800385 }
386
387 /**
388 * Cancel a refresh in progress
389 */
390 public void cancelRefresh() {
Marc Blank3f1eb852012-02-03 15:38:01 -0800391 if (DEBUG) {
392 Log.d(TAG, "[cancelRefresh() called]");
393 }
Marc Blank4015c182012-01-31 12:38:36 -0800394 synchronized(sCacheMapLock) {
395 // Mark the requery closed
Marc Blank4e25c942012-02-02 19:41:14 -0800396 sRefreshInProgress = false;
Marc Blank4015c182012-01-31 12:38:36 -0800397 // If we have the cursor, close it; otherwise, it will get closed when the query
398 // finishes (it checks sRequeryInProgress)
399 if (sRequeryCursor != null) {
400 sRequeryCursor.close();
401 sRequeryCursor = null;
402 }
403 }
404 }
405
406 /**
407 * Get a list of deletions from ConversationCursor to the refreshed cursor that hasn't yet
408 * been swapped into place; this allows the UI to animate these away if desired
409 * @return a list of positions deleted in ConversationCursor
410 */
411 public ArrayList<Integer> getRefreshDeletions () {
Marc Blank3f1eb852012-02-03 15:38:01 -0800412 if (DEBUG) {
413 Log.d(TAG, "[getRefreshDeletions() called]");
414 }
Marc Blank4015c182012-01-31 12:38:36 -0800415 Cursor deviceCursor = sConversationCursor;
416 Cursor serverCursor = sRequeryCursor;
Mindy Pereira8e915722012-02-16 14:42:56 -0800417 // TODO: (mindyp) saw some instability here. Adding an assert to try to
418 // catch it.
419 assert(sRequeryCursor != null);
Marc Blank4015c182012-01-31 12:38:36 -0800420 ArrayList<Integer> deleteList = new ArrayList<Integer>();
421 int serverCount = serverCursor.getCount();
422 int deviceCount = deviceCursor.getCount();
423 deviceCursor.moveToFirst();
424 serverCursor.moveToFirst();
425 while (serverCount > 0 || deviceCount > 0) {
426 if (serverCount == 0) {
427 for (; deviceCount > 0; deviceCount--)
428 deleteList.add(deviceCursor.getPosition());
429 break;
430 } else if (deviceCount == 0) {
431 break;
432 }
433 long deviceMs = deviceCursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN);
434 long serverMs = serverCursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN);
435 String deviceUri = deviceCursor.getString(UIProvider.CONVERSATION_URI_COLUMN);
436 String serverUri = serverCursor.getString(UIProvider.CONVERSATION_URI_COLUMN);
437 deviceCursor.moveToNext();
438 serverCursor.moveToNext();
439 serverCount--;
440 deviceCount--;
441 if (serverMs == deviceMs) {
442 // Check for duplicates here; if our identical dates refer to different messages,
443 // we'll just quit here for now (at worst, this will cause a non-animating delete)
444 // My guess is that this happens VERY rarely, if at all
445 if (!deviceUri.equals(serverUri)) {
446 // To do this right, we'd find all of the rows with the same ms (date), etc...
447 //return deleteList;
448 }
449 continue;
450 } else if (deviceMs > serverMs) {
451 deleteList.add(deviceCursor.getPosition() - 1);
452 // Move back because we've already advanced cursor (that's why we subtract 1 above)
453 serverCount++;
454 serverCursor.moveToPrevious();
455 } else if (serverMs > deviceMs) {
456 // If we wanted to track insertions, we'd so so here
457 // Move back because we've already advanced cursor
458 deviceCount++;
459 deviceCursor.moveToPrevious();
460 }
461 }
462 Log.d(TAG, "Deletions: " + deleteList);
463 return deleteList;
Marc Blank48eba7a2012-01-27 16:16:19 -0800464 }
465
Marc Blank97bca7b2012-01-24 11:17:00 -0800466 /**
Marc Blank48eba7a2012-01-27 16:16:19 -0800467 * When we get a requery from the UI, we'll do it, but also clear the cache. The listener is
468 * notified when the requery is complete
Marc Blank97bca7b2012-01-24 11:17:00 -0800469 * NOTE: This will have to change, of course, when we start using loaders...
470 */
Marc Blank48eba7a2012-01-27 16:16:19 -0800471 public boolean refresh() {
Marc Blank3f1eb852012-02-03 15:38:01 -0800472 if (DEBUG) {
473 Log.d(TAG, "[refresh() called]");
474 }
Marc Blank4e25c942012-02-02 19:41:14 -0800475 if (sRefreshInProgress) {
Marc Blank48eba7a2012-01-27 16:16:19 -0800476 return false;
477 }
478 // Say we're starting a requery
Marc Blank4e25c942012-02-02 19:41:14 -0800479 sRefreshInProgress = true;
Marc Blank48eba7a2012-01-27 16:16:19 -0800480 new Thread(new Runnable() {
481 @Override
482 public void run() {
483 // Get new data
484 sRequeryCursor =
485 mResolver.query(qUri, qProjection, qSelection, qSelectionArgs, qSortOrder);
486 // Make sure window is full
Marc Blank4015c182012-01-31 12:38:36 -0800487 synchronized(sCacheMapLock) {
Marc Blank4e25c942012-02-02 19:41:14 -0800488 if (sRefreshInProgress) {
Marc Blank4015c182012-01-31 12:38:36 -0800489 sRequeryCursor.getCount();
490 sActivity.runOnUiThread(new Runnable() {
491 @Override
492 public void run() {
Marc Blank3f1eb852012-02-03 15:38:01 -0800493 if (DEBUG) {
494 Log.d(TAG, "[Notify: onRefreshReady()]");
495 }
Marc Blankb33465d2012-02-24 11:15:00 -0800496 synchronized (sListeners) {
497 for (ConversationListener listener : sListeners) {
498 listener.onRefreshReady();
499 }
Marc Blankb600a832012-02-16 09:20:18 -0800500 }
Marc Blank4e25c942012-02-02 19:41:14 -0800501 sRefreshReady = true;
Marc Blank4015c182012-01-31 12:38:36 -0800502 }});
503 } else {
504 cancelRefresh();
505 }
506 }
Marc Blank48eba7a2012-01-27 16:16:19 -0800507 }
508 }).start();
Marc Blankc8a99422012-01-19 14:27:47 -0800509 return true;
510 }
511
Marc Blankb600a832012-02-16 09:20:18 -0800512 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800513 public void close() {
Marc Blankf9d87192012-02-16 10:50:41 -0800514 if (!sUnderlyingCursor.isClosed()) {
515 // Unregister our observer on the underlying cursor and close as usual
516 if (mCursorObserverRegistered) {
517 try {
518 sUnderlyingCursor.unregisterContentObserver(mCursorObserver);
519 } catch (IllegalStateException e) {
520 // Maybe the cursor got GC'd?
521 }
522 mCursorObserverRegistered = false;
523 }
524 sUnderlyingCursor.close();
Marc Blankdd10bc82012-02-01 19:10:46 -0800525 }
Marc Blankc8a99422012-01-19 14:27:47 -0800526 }
527
528 /**
529 * Move to the next not-deleted item in the conversation
530 */
Marc Blankb600a832012-02-16 09:20:18 -0800531 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800532 public boolean moveToNext() {
533 while (true) {
Marc Blank48eba7a2012-01-27 16:16:19 -0800534 boolean ret = sUnderlyingCursor.moveToNext();
Marc Blankc8a99422012-01-19 14:27:47 -0800535 if (!ret) return false;
536 if (getCachedValue(DELETED_COLUMN_INDEX) instanceof Integer) continue;
537 mPosition++;
538 return true;
539 }
540 }
541
542 /**
543 * Move to the previous not-deleted item in the conversation
544 */
Marc Blankb600a832012-02-16 09:20:18 -0800545 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800546 public boolean moveToPrevious() {
547 while (true) {
Marc Blank48eba7a2012-01-27 16:16:19 -0800548 boolean ret = sUnderlyingCursor.moveToPrevious();
Marc Blankc8a99422012-01-19 14:27:47 -0800549 if (!ret) return false;
550 if (getCachedValue(-1) instanceof Integer) continue;
551 mPosition--;
552 return true;
553 }
554 }
555
Marc Blankb600a832012-02-16 09:20:18 -0800556 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800557 public int getPosition() {
558 return mPosition;
559 }
560
561 /**
562 * The actual cursor's count must be decremented by the number we've deleted from the UI
563 */
Marc Blankb600a832012-02-16 09:20:18 -0800564 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800565 public int getCount() {
Marc Blank48eba7a2012-01-27 16:16:19 -0800566 return sUnderlyingCursor.getCount() - sDeletedCount;
Marc Blankc8a99422012-01-19 14:27:47 -0800567 }
568
Marc Blankb600a832012-02-16 09:20:18 -0800569 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800570 public boolean moveToFirst() {
Marc Blank48eba7a2012-01-27 16:16:19 -0800571 sUnderlyingCursor.moveToPosition(-1);
Marc Blankc8a99422012-01-19 14:27:47 -0800572 mPosition = -1;
573 return moveToNext();
574 }
575
Marc Blankb600a832012-02-16 09:20:18 -0800576 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800577 public boolean moveToPosition(int pos) {
Marc Blankdd10bc82012-02-01 19:10:46 -0800578 if (pos < -1 || pos >= getCount()) return false;
Marc Blankc8a99422012-01-19 14:27:47 -0800579 if (pos == mPosition) return true;
580 if (pos > mPosition) {
581 while (pos > mPosition) {
582 if (!moveToNext()) {
583 return false;
584 }
585 }
586 return true;
587 } else if (pos == 0) {
588 return moveToFirst();
589 } else {
590 while (pos < mPosition) {
591 if (!moveToPrevious()) {
592 return false;
593 }
594 }
595 return true;
596 }
597 }
598
Marc Blankb600a832012-02-16 09:20:18 -0800599 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800600 public boolean moveToLast() {
601 throw new UnsupportedOperationException("moveToLast unsupported!");
602 }
603
Marc Blankb600a832012-02-16 09:20:18 -0800604 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800605 public boolean move(int offset) {
606 throw new UnsupportedOperationException("move unsupported!");
607 }
608
609 /**
610 * We need to override all of the getters to make sure they look at cached values before using
611 * the values in the underlying cursor
612 */
613 @Override
614 public double getDouble(int columnIndex) {
615 Object obj = getCachedValue(columnIndex);
616 if (obj != null) return (Double)obj;
Marc Blank48eba7a2012-01-27 16:16:19 -0800617 return sUnderlyingCursor.getDouble(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800618 }
619
620 @Override
621 public float getFloat(int columnIndex) {
622 Object obj = getCachedValue(columnIndex);
623 if (obj != null) return (Float)obj;
Marc Blank48eba7a2012-01-27 16:16:19 -0800624 return sUnderlyingCursor.getFloat(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800625 }
626
627 @Override
628 public int getInt(int columnIndex) {
629 Object obj = getCachedValue(columnIndex);
630 if (obj != null) return (Integer)obj;
Marc Blank48eba7a2012-01-27 16:16:19 -0800631 return sUnderlyingCursor.getInt(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800632 }
633
634 @Override
635 public long getLong(int columnIndex) {
636 Object obj = getCachedValue(columnIndex);
637 if (obj != null) return (Long)obj;
Marc Blank48eba7a2012-01-27 16:16:19 -0800638 return sUnderlyingCursor.getLong(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800639 }
640
641 @Override
642 public short getShort(int columnIndex) {
643 Object obj = getCachedValue(columnIndex);
644 if (obj != null) return (Short)obj;
Marc Blank48eba7a2012-01-27 16:16:19 -0800645 return sUnderlyingCursor.getShort(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800646 }
647
648 @Override
649 public String getString(int columnIndex) {
650 // If we're asking for the Uri for the conversation list, we return a forwarding URI
651 // so that we can intercept update/delete and handle it ourselves
Marc Blank97bca7b2012-01-24 11:17:00 -0800652 if (columnIndex == sUriColumnIndex) {
Marc Blank48eba7a2012-01-27 16:16:19 -0800653 Uri uri = Uri.parse(sUnderlyingCursor.getString(columnIndex));
Marc Blankc8a99422012-01-19 14:27:47 -0800654 return uriToCachingUriString(uri);
655 }
656 Object obj = getCachedValue(columnIndex);
657 if (obj != null) return (String)obj;
Marc Blank48eba7a2012-01-27 16:16:19 -0800658 return sUnderlyingCursor.getString(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800659 }
660
661 @Override
662 public byte[] getBlob(int columnIndex) {
663 Object obj = getCachedValue(columnIndex);
664 if (obj != null) return (byte[])obj;
Marc Blank48eba7a2012-01-27 16:16:19 -0800665 return sUnderlyingCursor.getBlob(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800666 }
667
668 /**
669 * Observer of changes to underlying data
670 */
671 private class CursorObserver extends ContentObserver {
672 public CursorObserver() {
Mindy Pereira609480e2012-02-16 13:54:18 -0800673 super(null);
Marc Blankc8a99422012-01-19 14:27:47 -0800674 }
675
676 @Override
677 public void onChange(boolean selfChange) {
678 // If we're here, then something outside of the UI has changed the data, and we
Marc Blank48eba7a2012-01-27 16:16:19 -0800679 // must query the underlying provider for that data
Marc Blankc8a99422012-01-19 14:27:47 -0800680 if (DEBUG) {
681 Log.d(TAG, "Underlying conversation cursor changed; requerying");
682 }
683 // It's not at all obvious to me why we must unregister/re-register after the requery
684 // However, if we don't we'll only get one notification and no more...
Marc Blankc8a99422012-01-19 14:27:47 -0800685 ConversationCursor.this.underlyingChanged();
686 }
687 }
688
689 /**
690 * ConversationProvider is the ContentProvider for our forwarding Uri's; it passes queries
691 * and inserts directly, and caches updates/deletes before passing them through. The caching
692 * will cause a redraw of the list with updated values.
693 */
Paul Westbrook77177b12012-02-07 15:23:42 -0800694 public abstract static class ConversationProvider extends ContentProvider {
695 public static String AUTHORITY;
696
697 /**
698 * Allows the implmenting provider to specify the authority that should be used.
699 */
700 protected abstract String getAuthority();
Marc Blank8d69d4e2012-01-25 12:04:28 -0800701
Marc Blankc8a99422012-01-19 14:27:47 -0800702 @Override
703 public boolean onCreate() {
Marc Blank8d69d4e2012-01-25 12:04:28 -0800704 sProvider = this;
Paul Westbrook77177b12012-02-07 15:23:42 -0800705 AUTHORITY = getAuthority();
Marc Blank8d69d4e2012-01-25 12:04:28 -0800706 return true;
Marc Blankc8a99422012-01-19 14:27:47 -0800707 }
708
709 @Override
710 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
711 String sortOrder) {
712 return mResolver.query(
713 uriFromCachingUri(uri), projection, selection, selectionArgs, sortOrder);
714 }
715
716 @Override
Marc Blankf892f0a2012-01-30 13:04:10 -0800717 public Uri insert(Uri uri, ContentValues values) {
718 insertLocal(uri, values);
719 return ProviderExecute.opInsert(uri, values);
720 }
721
722 @Override
723 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
Marc Blank03bbaad2012-01-31 11:27:16 -0800724 updateLocal(uri, values);
Marc Blankf892f0a2012-01-30 13:04:10 -0800725 return ProviderExecute.opUpdate(uri, values);
726 }
727
728 @Override
729 public int delete(Uri uri, String selection, String[] selectionArgs) {
Marc Blank03bbaad2012-01-31 11:27:16 -0800730 deleteLocal(uri);
Marc Blankf892f0a2012-01-30 13:04:10 -0800731 return ProviderExecute.opDelete(uri);
732 }
733
734 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800735 public String getType(Uri uri) {
736 return null;
737 }
738
739 /**
740 * Quick and dirty class that executes underlying provider CRUD operations on a background
741 * thread.
742 */
743 static class ProviderExecute implements Runnable {
744 static final int DELETE = 0;
745 static final int INSERT = 1;
746 static final int UPDATE = 2;
747
748 final int mCode;
749 final Uri mUri;
750 final ContentValues mValues; //HEHEH
751
752 ProviderExecute(int code, Uri uri, ContentValues values) {
753 mCode = code;
754 mUri = uriFromCachingUri(uri);
755 mValues = values;
756 }
757
758 ProviderExecute(int code, Uri uri) {
759 this(code, uri, null);
760 }
761
Marc Blank8d69d4e2012-01-25 12:04:28 -0800762 static Uri opInsert(Uri uri, ContentValues values) {
763 ProviderExecute e = new ProviderExecute(INSERT, uri, values);
764 if (offUiThread()) return (Uri)e.go();
765 new Thread(e).start();
766 return null;
Marc Blankc8a99422012-01-19 14:27:47 -0800767 }
768
Marc Blank8d69d4e2012-01-25 12:04:28 -0800769 static int opDelete(Uri uri) {
770 ProviderExecute e = new ProviderExecute(DELETE, uri);
771 if (offUiThread()) return (Integer)e.go();
772 new Thread(new ProviderExecute(DELETE, uri)).start();
773 return 0;
774 }
775
776 static int opUpdate(Uri uri, ContentValues values) {
777 ProviderExecute e = new ProviderExecute(UPDATE, uri, values);
778 if (offUiThread()) return (Integer)e.go();
779 new Thread(e).start();
780 return 0;
Marc Blankc8a99422012-01-19 14:27:47 -0800781 }
782
783 @Override
784 public void run() {
Marc Blank8d69d4e2012-01-25 12:04:28 -0800785 go();
786 }
787
788 public Object go() {
Marc Blankc8a99422012-01-19 14:27:47 -0800789 switch(mCode) {
790 case DELETE:
Marc Blank8d69d4e2012-01-25 12:04:28 -0800791 return mResolver.delete(mUri, null, null);
Marc Blankc8a99422012-01-19 14:27:47 -0800792 case INSERT:
Marc Blank8d69d4e2012-01-25 12:04:28 -0800793 return mResolver.insert(mUri, mValues);
Marc Blankc8a99422012-01-19 14:27:47 -0800794 case UPDATE:
Marc Blank8d69d4e2012-01-25 12:04:28 -0800795 return mResolver.update(mUri, mValues, null, null);
796 default:
797 return null;
Marc Blankc8a99422012-01-19 14:27:47 -0800798 }
799 }
800 }
801
Marc Blank8d69d4e2012-01-25 12:04:28 -0800802 private void insertLocal(Uri uri, ContentValues values) {
803 // Placeholder for now; there's no local insert
804 }
805
Marc Blank248b1b42012-02-07 13:43:02 -0800806 @VisibleForTesting
807 void deleteLocal(Uri uri) {
Marc Blank8d69d4e2012-01-25 12:04:28 -0800808 Uri underlyingUri = uriFromCachingUri(uri);
Mindy Pereira8a77f8b2012-02-02 16:34:42 -0800809 // Remember to decode the underlying Uri as it might be encoded (as w/ Gmail)
810 String uriString = Uri.decode(underlyingUri.toString());
Marc Blank8d69d4e2012-01-25 12:04:28 -0800811 cacheValue(uriString, DELETED_COLUMN, true);
Marc Blankc8a99422012-01-19 14:27:47 -0800812 }
813
Marc Blank248b1b42012-02-07 13:43:02 -0800814 @VisibleForTesting
815 void updateLocal(Uri uri, ContentValues values) {
Mindy Pereiraf98b3182012-02-22 11:07:13 -0800816 if (values == null) {
817 return;
818 }
Marc Blankc8a99422012-01-19 14:27:47 -0800819 Uri underlyingUri = uriFromCachingUri(uri);
820 // Remember to decode the underlying Uri as it might be encoded (as w/ Gmail)
821 String uriString = Uri.decode(underlyingUri.toString());
822 for (String columnName: values.keySet()) {
823 cacheValue(uriString, columnName, values.get(columnName));
824 }
Marc Blank8d69d4e2012-01-25 12:04:28 -0800825 }
826
Marc Blank03bbaad2012-01-31 11:27:16 -0800827 static boolean offUiThread() {
Marc Blank8d69d4e2012-01-25 12:04:28 -0800828 return Looper.getMainLooper().getThread() != Thread.currentThread();
829 }
830
Marc Blank1b9efd92012-02-01 14:27:55 -0800831 public int apply(ArrayList<ConversationOperation> ops) {
Marc Blank8d69d4e2012-01-25 12:04:28 -0800832 final HashMap<String, ArrayList<ContentProviderOperation>> batchMap =
833 new HashMap<String, ArrayList<ContentProviderOperation>>();
Marc Blankb31ab5a2012-02-01 12:28:29 -0800834 // Increment sequence count
835 sSequence++;
Marc Blankf892f0a2012-01-30 13:04:10 -0800836 // Execute locally and build CPO's for underlying provider
Marc Blank8d69d4e2012-01-25 12:04:28 -0800837 for (ConversationOperation op: ops) {
838 Uri underlyingUri = uriFromCachingUri(op.mUri);
839 String authority = underlyingUri.getAuthority();
840 ArrayList<ContentProviderOperation> authOps = batchMap.get(authority);
841 if (authOps == null) {
842 authOps = new ArrayList<ContentProviderOperation>();
843 batchMap.put(authority, authOps);
844 }
845 authOps.add(op.execute(underlyingUri));
Marc Blankf892f0a2012-01-30 13:04:10 -0800846 }
847
848 // Send changes to underlying provider
Marc Blank8d69d4e2012-01-25 12:04:28 -0800849 for (String authority: batchMap.keySet()) {
850 try {
851 if (offUiThread()) {
Marc Blankb31ab5a2012-02-01 12:28:29 -0800852 mResolver.applyBatch(authority, batchMap.get(authority));
Marc Blank8d69d4e2012-01-25 12:04:28 -0800853 } else {
854 final String auth = authority;
855 new Thread(new Runnable() {
856 @Override
857 public void run() {
858 try {
859 mResolver.applyBatch(auth, batchMap.get(auth));
860 } catch (RemoteException e) {
861 } catch (OperationApplicationException e) {
862 }
863 }
864 }).start();
Marc Blank8d69d4e2012-01-25 12:04:28 -0800865 }
866 } catch (RemoteException e) {
867 } catch (OperationApplicationException e) {
868 }
869 }
Marc Blank1b9efd92012-02-01 14:27:55 -0800870 return sSequence;
Marc Blank8d69d4e2012-01-25 12:04:28 -0800871 }
872 }
873
874 /**
875 * ConversationOperation is the encapsulation of a ContentProvider operation to be performed
876 * atomically as part of a "batch" operation.
877 */
878 public static class ConversationOperation {
879 public static final int DELETE = 0;
880 public static final int INSERT = 1;
881 public static final int UPDATE = 2;
Mindy Pereiraf98b3182012-02-22 11:07:13 -0800882 public static final int ARCHIVE = 3;
Mindy Pereira830c00f2012-02-22 11:43:49 -0800883 public static final int MUTE = 4;
884 public static final int REPORT_SPAM = 5;
Marc Blank8d69d4e2012-01-25 12:04:28 -0800885
886 private final int mType;
887 private final Uri mUri;
888 private final ContentValues mValues;
Marc Blankce538182012-02-03 13:04:27 -0800889 // True if an updated item should be removed locally (from ConversationCursor)
890 // This would be the case for a folder/label change in which the conversation is no longer
891 // in the folder represented by the ConversationCursor
892 private final boolean mLocalDeleteOnUpdate;
Marc Blank8d69d4e2012-01-25 12:04:28 -0800893
Marc Blankf892f0a2012-01-30 13:04:10 -0800894 public ConversationOperation(int type, Conversation conv) {
895 this(type, conv, null);
Marc Blank8d69d4e2012-01-25 12:04:28 -0800896 }
897
Marc Blankf892f0a2012-01-30 13:04:10 -0800898 public ConversationOperation(int type, Conversation conv, ContentValues values) {
Marc Blank8d69d4e2012-01-25 12:04:28 -0800899 mType = type;
Marc Blankc43bc0a2012-02-02 11:25:18 -0800900 mUri = conv.uri;
Marc Blank8d69d4e2012-01-25 12:04:28 -0800901 mValues = values;
Marc Blankce538182012-02-03 13:04:27 -0800902 mLocalDeleteOnUpdate = conv.localDeleteOnUpdate;
Marc Blank8d69d4e2012-01-25 12:04:28 -0800903 }
904
905 private ContentProviderOperation execute(Uri underlyingUri) {
Marc Blankb31ab5a2012-02-01 12:28:29 -0800906 Uri uri = underlyingUri.buildUpon()
Marc Blankdd10bc82012-02-01 19:10:46 -0800907 .appendQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER,
908 Integer.toString(sSequence))
Marc Blankb31ab5a2012-02-01 12:28:29 -0800909 .build();
Marc Blank8d69d4e2012-01-25 12:04:28 -0800910 switch(mType) {
911 case DELETE:
Marc Blank03bbaad2012-01-31 11:27:16 -0800912 sProvider.deleteLocal(mUri);
Marc Blankb31ab5a2012-02-01 12:28:29 -0800913 return ContentProviderOperation.newDelete(uri).build();
Marc Blank8d69d4e2012-01-25 12:04:28 -0800914 case UPDATE:
Marc Blankce538182012-02-03 13:04:27 -0800915 if (mLocalDeleteOnUpdate) {
Marc Blank995fff52012-02-06 13:28:20 -0800916 sProvider.deleteLocal(mUri);
Marc Blankce538182012-02-03 13:04:27 -0800917 } else {
918 sProvider.updateLocal(mUri, mValues);
919 }
Marc Blankb31ab5a2012-02-01 12:28:29 -0800920 return ContentProviderOperation.newUpdate(uri)
Marc Blank8d69d4e2012-01-25 12:04:28 -0800921 .withValues(mValues)
922 .build();
923 case INSERT:
924 sProvider.insertLocal(mUri, mValues);
Marc Blankb31ab5a2012-02-01 12:28:29 -0800925 return ContentProviderOperation.newInsert(uri)
Marc Blank8d69d4e2012-01-25 12:04:28 -0800926 .withValues(mValues).build();
Mindy Pereiraf98b3182012-02-22 11:07:13 -0800927 case ARCHIVE:
Mindy Pereiraf98b3182012-02-22 11:07:13 -0800928 sProvider.deleteLocal(mUri);
Paul Westbrook334e64a2012-02-23 13:26:35 -0800929
930 // Create an update operation that represents archive
931 return ContentProviderOperation.newUpdate(uri).withValue(
932 ConversationOperations.OPERATION_KEY, ConversationOperations.ARCHIVE)
933 .build();
Mindy Pereira830c00f2012-02-22 11:43:49 -0800934 case MUTE:
Paul Westbrook334e64a2012-02-23 13:26:35 -0800935 if (mLocalDeleteOnUpdate) {
936 sProvider.deleteLocal(mUri);
937 }
938
939 // Create an update operation that represents mute
940 return ContentProviderOperation.newUpdate(uri).withValue(
941 ConversationOperations.OPERATION_KEY, ConversationOperations.MUTE)
942 .build();
Mindy Pereira830c00f2012-02-22 11:43:49 -0800943 case REPORT_SPAM:
Mindy Pereira830c00f2012-02-22 11:43:49 -0800944 sProvider.deleteLocal(mUri);
Paul Westbrook334e64a2012-02-23 13:26:35 -0800945
946 // Create an update operation that represents report spam
947 return ContentProviderOperation.newUpdate(uri).withValue(
948 ConversationOperations.OPERATION_KEY,
949 ConversationOperations.REPORT_SPAM).build();
Marc Blank8d69d4e2012-01-25 12:04:28 -0800950 default:
951 throw new UnsupportedOperationException(
952 "No such ConversationOperation type: " + mType);
953 }
Marc Blankc8a99422012-01-19 14:27:47 -0800954 }
955 }
Marc Blank97bca7b2012-01-24 11:17:00 -0800956
957 /**
958 * For now, a single listener can be associated with the cursor, and for now we'll just
959 * notify on deletions
960 */
961 public interface ConversationListener {
Marc Blank48eba7a2012-01-27 16:16:19 -0800962 // Data in the underlying provider has changed; a refresh is required to sync up
963 public void onRefreshRequired();
964 // We've completed a requested refresh of the underlying cursor
965 public void onRefreshReady();
966 }
967
968 @Override
969 public boolean isFirst() {
970 throw new UnsupportedOperationException();
971 }
972
973 @Override
974 public boolean isLast() {
975 throw new UnsupportedOperationException();
976 }
977
978 @Override
979 public boolean isBeforeFirst() {
980 throw new UnsupportedOperationException();
981 }
982
983 @Override
984 public boolean isAfterLast() {
985 throw new UnsupportedOperationException();
986 }
987
988 @Override
989 public int getColumnIndex(String columnName) {
990 return sUnderlyingCursor.getColumnIndex(columnName);
991 }
992
993 @Override
994 public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
995 return sUnderlyingCursor.getColumnIndexOrThrow(columnName);
996 }
997
998 @Override
999 public String getColumnName(int columnIndex) {
1000 return sUnderlyingCursor.getColumnName(columnIndex);
1001 }
1002
1003 @Override
1004 public String[] getColumnNames() {
1005 return sUnderlyingCursor.getColumnNames();
1006 }
1007
1008 @Override
1009 public int getColumnCount() {
1010 return sUnderlyingCursor.getColumnCount();
1011 }
1012
1013 @Override
1014 public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
1015 throw new UnsupportedOperationException();
1016 }
1017
1018 @Override
1019 public int getType(int columnIndex) {
1020 return sUnderlyingCursor.getType(columnIndex);
1021 }
1022
1023 @Override
1024 public boolean isNull(int columnIndex) {
1025 throw new UnsupportedOperationException();
1026 }
1027
1028 @Override
1029 public void deactivate() {
1030 throw new UnsupportedOperationException();
1031 }
1032
1033 @Override
1034 public boolean isClosed() {
1035 return sUnderlyingCursor.isClosed();
1036 }
1037
1038 @Override
1039 public void registerContentObserver(ContentObserver observer) {
Marc Blankdd10bc82012-02-01 19:10:46 -08001040 sUnderlyingCursor.registerContentObserver(observer);
Marc Blank48eba7a2012-01-27 16:16:19 -08001041 }
1042
1043 @Override
1044 public void unregisterContentObserver(ContentObserver observer) {
Marc Blankdd10bc82012-02-01 19:10:46 -08001045 sUnderlyingCursor.unregisterContentObserver(observer);
Marc Blank48eba7a2012-01-27 16:16:19 -08001046 }
1047
1048 @Override
1049 public void registerDataSetObserver(DataSetObserver observer) {
Marc Blankdd10bc82012-02-01 19:10:46 -08001050 sUnderlyingCursor.registerDataSetObserver(observer);
Marc Blank48eba7a2012-01-27 16:16:19 -08001051 }
1052
1053 @Override
1054 public void unregisterDataSetObserver(DataSetObserver observer) {
Marc Blankdd10bc82012-02-01 19:10:46 -08001055 sUnderlyingCursor.unregisterDataSetObserver(observer);
Marc Blank48eba7a2012-01-27 16:16:19 -08001056 }
1057
1058 @Override
1059 public void setNotificationUri(ContentResolver cr, Uri uri) {
1060 throw new UnsupportedOperationException();
1061 }
1062
1063 @Override
1064 public boolean getWantsAllOnMoveCalls() {
1065 throw new UnsupportedOperationException();
1066 }
1067
1068 @Override
1069 public Bundle getExtras() {
1070 throw new UnsupportedOperationException();
1071 }
1072
1073 @Override
1074 public Bundle respond(Bundle extras) {
1075 throw new UnsupportedOperationException();
1076 }
1077
1078 @Override
1079 public boolean requery() {
1080 return true;
Marc Blank97bca7b2012-01-24 11:17:00 -08001081 }
Paul Westbrookf83bde42012-02-23 15:42:26 -08001082
1083 @Override
1084 protected void finalize() {
1085 close();
1086 }
Marc Blankc8a99422012-01-19 14:27:47 -08001087}