blob: af9e0ab5cb8208ea60c4a1edc16959fae6d46b71 [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;
Marc Blank48eba7a2012-01-27 16:16:19 -0800109
110 private ConversationCursor(Cursor cursor, Activity activity, String messageListColumn) {
Marc Blank97bca7b2012-01-24 11:17:00 -0800111 sConversationCursor = this;
Marc Blankc16be932012-02-24 12:43:48 -0800112 // If we have an existing underlying cursor, make sure it's closed
113 if (sUnderlyingCursor != null) {
114 sUnderlyingCursor.close();
115 }
Marc Blank48eba7a2012-01-27 16:16:19 -0800116 sUnderlyingCursor = cursor;
Marc Blankb600a832012-02-16 09:20:18 -0800117 sListeners.clear();
Marc Blankc16be932012-02-24 12:43:48 -0800118 sRefreshRequired = false;
119 sRefreshReady = false;
120 sRefreshInProgress = false;
Marc Blankc8a99422012-01-19 14:27:47 -0800121 mCursorObserver = new CursorObserver();
Marc Blank48eba7a2012-01-27 16:16:19 -0800122 resetCursor(null);
Marc Blankc8a99422012-01-19 14:27:47 -0800123 mColumnNames = cursor.getColumnNames();
Marc Blank48eba7a2012-01-27 16:16:19 -0800124 sUriColumnIndex = cursor.getColumnIndex(messageListColumn);
Marc Blank97bca7b2012-01-24 11:17:00 -0800125 if (sUriColumnIndex < 0) {
Marc Blankc8a99422012-01-19 14:27:47 -0800126 throw new IllegalArgumentException("Cursor must include a message list column");
127 }
Marc Blankc8a99422012-01-19 14:27:47 -0800128 }
129
130 /**
Marc Blank48eba7a2012-01-27 16:16:19 -0800131 * Create a ConversationCursor; this should be called by the ListActivity using that cursor
132 * @param activity the activity creating the cursor
133 * @param messageListColumn the column used for individual cursor items
134 * @param uri the query uri
135 * @param projection the query projecion
136 * @param selection the query selection
137 * @param selectionArgs the query selection args
138 * @param sortOrder the query sort order
139 * @return a ConversationCursor
Marc Blankc8a99422012-01-19 14:27:47 -0800140 */
Marc Blank48eba7a2012-01-27 16:16:19 -0800141 public static ConversationCursor create(Activity activity, String messageListColumn, Uri uri,
142 String[] projection, String selection, String[] selectionArgs, String sortOrder) {
Marc Blank948985b2012-02-29 11:26:40 -0800143 sActivity = activity;
144 mResolver = activity.getContentResolver();
145 if (selection != null || sortOrder != null) {
146 throw new IllegalArgumentException(
147 "Selection and sort order aren't allowed in ConversationCursors");
148 }
149 synchronized (sCacheMapLock) {
150 // First, let's see if we already have a cursor
151 if (sConversationCursor != null) {
152 // If it's the same, just clean up
153 if (qUri.equals(uri) && !sRefreshRequired && !sRefreshInProgress) {
154 if (sRefreshReady) {
155 // If we already have a refresh ready, just sync() it
156 Log.d(TAG, "Create: refreshed cursor ready, sync");
157 sConversationCursor.sync();
158 } else {
159 // Position the cursor before the first item (as it would be if new), reset
160 // the cache, and return as new
161 Log.d(TAG, "Create: cursor good, reset position and clear map");
162 sConversationCursor.moveToPosition(-1);
163 sConversationCursor.mPosition = -1;
164 synchronized (sCacheMapLock) {
165 sCacheMap.clear();
166 }
Marc Blankc16be932012-02-24 12:43:48 -0800167 }
Marc Blank948985b2012-02-29 11:26:40 -0800168 } else {
169 // Set qUri/qProjection these in case they changed
170 Log.d(TAG, "Create: new query or refresh needed, query/sync");
171 sRequeryCursor = doQuery(uri, projection);
172 sConversationCursor.sync();
Marc Blankc16be932012-02-24 12:43:48 -0800173 }
174 return sConversationCursor;
Marc Blankc16be932012-02-24 12:43:48 -0800175 }
Marc Blank948985b2012-02-29 11:26:40 -0800176 // Create new ConversationCursor
177 Log.d(TAG, "Create: initial creation");
178 return new ConversationCursor(doQuery(uri, projection), activity, messageListColumn);
Marc Blankc16be932012-02-24 12:43:48 -0800179 }
Marc Blank948985b2012-02-29 11:26:40 -0800180 }
181
182 private static Cursor doQuery(Uri uri, String[] projection) {
Marc Blank48eba7a2012-01-27 16:16:19 -0800183 qUri = uri;
184 qProjection = projection;
Marc Blank948985b2012-02-29 11:26:40 -0800185 return mResolver.query(qUri, qProjection, null, null, null);
Marc Blank48eba7a2012-01-27 16:16:19 -0800186 }
187
188 /**
189 * Return whether the uri string (message list uri) is in the underlying cursor
190 * @param uriString the uri string we're looking for
191 * @return true if the uri string is in the cursor; false otherwise
192 */
193 private boolean isInUnderlyingCursor(String uriString) {
194 sUnderlyingCursor.moveToPosition(-1);
195 while (sUnderlyingCursor.moveToNext()) {
196 if (uriString.equals(sUnderlyingCursor.getString(sUriColumnIndex))) {
197 return true;
198 }
Marc Blank97bca7b2012-01-24 11:17:00 -0800199 }
Marc Blank48eba7a2012-01-27 16:16:19 -0800200 return false;
201 }
202
203 /**
204 * Reset the cursor; this involves clearing out our cache map and resetting our various counts
205 * The cursor should be reset whenever we get fresh data from the underlying cursor. The cache
206 * is locked during the reset, which will block the UI, but for only a very short time
207 * (estimated at a few ms, but we can profile this; remember that the cache will usually
208 * be empty or have a few entries)
209 */
210 private void resetCursor(Cursor newCursor) {
211 // Temporary, log time for reset
212 long startTime = System.currentTimeMillis();
Marc Blank3f1eb852012-02-03 15:38:01 -0800213 if (DEBUG) {
214 Log.d(TAG, "[--resetCursor--]");
215 }
Marc Blank48eba7a2012-01-27 16:16:19 -0800216 synchronized (sCacheMapLock) {
217 // Walk through the cache. Here are the cases:
218 // 1) The entry isn't marked with REQUERY - remove it from the cache. If DELETED is
219 // set, decrement the deleted count
220 // 2) The REQUERY entry is still in the UP
221 // 2a) The REQUERY entry isn't DELETED; we're good, and the client change will remain
222 // (i.e. client wins, it's on its way to the UP)
223 // 2b) The REQUERY entry is DELETED; we're good (client change remains, it's on
224 // its way to the UP)
225 // 3) the REQUERY was deleted on the server (sheesh; this would be bizarre timing!) -
226 // we need to throw the item out of the cache
227 // So ... the only interesting case is #3, we need to look for remaining deleted items
228 // and see if they're still in the UP
229 Iterator<HashMap.Entry<String, ContentValues>> iter = sCacheMap.entrySet().iterator();
230 while (iter.hasNext()) {
231 HashMap.Entry<String, ContentValues> entry = iter.next();
232 ContentValues values = entry.getValue();
233 if (values.containsKey(REQUERY_COLUMN) && isInUnderlyingCursor(entry.getKey())) {
234 // If we're in a requery and we're still around, remove the requery key
235 // We're good here, the cached change (delete/update) is on its way to UP
236 values.remove(REQUERY_COLUMN);
237 } else {
238 // Keep the deleted count up-to-date; remove the cache entry
239 if (values.containsKey(DELETED_COLUMN)) {
240 sDeletedCount--;
241 }
242 // Remove the entry
243 iter.remove();
244 }
245 }
246
247 // Swap cursor
248 if (newCursor != null) {
Marc Blankdd10bc82012-02-01 19:10:46 -0800249 close();
Marc Blank48eba7a2012-01-27 16:16:19 -0800250 sUnderlyingCursor = newCursor;
251 }
252
253 mPosition = -1;
254 sUnderlyingCursor.moveToPosition(mPosition);
255 if (!mCursorObserverRegistered) {
256 sUnderlyingCursor.registerContentObserver(mCursorObserver);
257 mCursorObserverRegistered = true;
258 }
Marc Blank4e25c942012-02-02 19:41:14 -0800259 sRefreshRequired = false;
Marc Blank48eba7a2012-01-27 16:16:19 -0800260 }
261 Log.d(TAG, "resetCache time: " + ((System.currentTimeMillis() - startTime)) + "ms");
Marc Blankc8a99422012-01-19 14:27:47 -0800262 }
263
264 /**
Marc Blankb600a832012-02-16 09:20:18 -0800265 * Add a listener for this cursor; we'll notify it when our data changes
Marc Blankc8a99422012-01-19 14:27:47 -0800266 */
Marc Blankb600a832012-02-16 09:20:18 -0800267 public void addListener(ConversationListener listener) {
Marc Blankb33465d2012-02-24 11:15:00 -0800268 synchronized (sListeners) {
Marc Blankf3626952012-02-28 17:06:05 -0800269 if (!sListeners.contains(listener)) {
270 sListeners.add(listener);
271 } else {
272 Log.d(TAG, "Ignoring duplicate add of listener");
273 }
Marc Blankb33465d2012-02-24 11:15:00 -0800274 }
Marc Blankb600a832012-02-16 09:20:18 -0800275 }
276
277 /**
278 * Remove a listener for this cursor
279 */
280 public void removeListener(ConversationListener listener) {
Marc Blankb33465d2012-02-24 11:15:00 -0800281 synchronized(sListeners) {
282 sListeners.remove(listener);
283 }
Marc Blankc8a99422012-01-19 14:27:47 -0800284 }
285
286 /**
287 * Generate a forwarding Uri to ConversationProvider from an original Uri. We do this by
288 * changing the authority to ours, but otherwise leaving the Uri intact.
289 * NOTE: This won't handle query parameters, so the functionality will need to be added if
290 * parameters are used in the future
291 * @param uri the uri
292 * @return a forwarding uri to ConversationProvider
293 */
294 private static String uriToCachingUriString (Uri uri) {
295 String provider = uri.getAuthority();
Paul Westbrook77177b12012-02-07 15:23:42 -0800296 return uri.getScheme() + "://" + ConversationProvider.AUTHORITY
297 + "/" + provider + uri.getPath();
Marc Blankc8a99422012-01-19 14:27:47 -0800298 }
299
300 /**
301 * Regenerate the original Uri from a forwarding (ConversationProvider) Uri
302 * NOTE: See note above for uriToCachingUri
303 * @param uri the forwarding Uri
304 * @return the original Uri
305 */
306 private static Uri uriFromCachingUri(Uri uri) {
307 List<String> path = uri.getPathSegments();
308 Uri.Builder builder = new Uri.Builder().scheme(uri.getScheme()).authority(path.get(0));
309 for (int i = 1; i < path.size(); i++) {
310 builder.appendPath(path.get(i));
311 }
312 return builder.build();
313 }
314
315 /**
316 * Cache a column name/value pair for a given Uri
317 * @param uriString the Uri for which the column name/value pair applies
318 * @param columnName the column name
319 * @param value the value to be cached
320 */
321 private static void cacheValue(String uriString, String columnName, Object value) {
Marc Blank48eba7a2012-01-27 16:16:19 -0800322 synchronized (sCacheMapLock) {
323 // Get the map for our uri
324 ContentValues map = sCacheMap.get(uriString);
325 // Create one if necessary
326 if (map == null) {
327 map = new ContentValues();
328 sCacheMap.put(uriString, map);
Marc Blankc8a99422012-01-19 14:27:47 -0800329 }
Marc Blank48eba7a2012-01-27 16:16:19 -0800330 // If we're caching a deletion, add to our count
331 if ((columnName == DELETED_COLUMN) && (map.get(columnName) == null)) {
332 sDeletedCount++;
333 if (DEBUG) {
334 Log.d(TAG, "Deleted " + uriString);
335 }
Marc Blank97bca7b2012-01-24 11:17:00 -0800336 }
Marc Blank48eba7a2012-01-27 16:16:19 -0800337 // ContentValues has no generic "put", so we must test. For now, the only classes of
338 // values implemented are Boolean/Integer/String, though others are trivially added
339 if (value instanceof Boolean) {
340 map.put(columnName, ((Boolean) value).booleanValue() ? 1 : 0);
341 } else if (value instanceof Integer) {
342 map.put(columnName, (Integer) value);
343 } else if (value instanceof String) {
344 map.put(columnName, (String) value);
345 } else {
346 String cname = value.getClass().getName();
347 throw new IllegalArgumentException("Value class not compatible with cache: "
348 + cname);
349 }
Marc Blank4e25c942012-02-02 19:41:14 -0800350 if (sRefreshInProgress) {
Marc Blank48eba7a2012-01-27 16:16:19 -0800351 map.put(REQUERY_COLUMN, 1);
352 }
353 if (DEBUG && (columnName != DELETED_COLUMN)) {
354 Log.d(TAG, "Caching value for " + uriString + ": " + columnName);
355 }
Marc Blankc8a99422012-01-19 14:27:47 -0800356 }
357 }
358
359 /**
360 * Get the cached value for the provided column; we special case -1 as the "deleted" column
361 * @param columnIndex the index of the column whose cached value we want to retrieve
362 * @return the cached value for this column, or null if there is none
363 */
364 private Object getCachedValue(int columnIndex) {
Marc Blank48eba7a2012-01-27 16:16:19 -0800365 String uri = sUnderlyingCursor.getString(sUriColumnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800366 ContentValues uriMap = sCacheMap.get(uri);
367 if (uriMap != null) {
368 String columnName;
369 if (columnIndex == DELETED_COLUMN_INDEX) {
370 columnName = DELETED_COLUMN;
371 } else {
372 columnName = mColumnNames[columnIndex];
373 }
374 return uriMap.get(columnName);
375 }
376 return null;
377 }
378
379 /**
Marc Blank97bca7b2012-01-24 11:17:00 -0800380 * When the underlying cursor changes, we want to alert the listener
Marc Blankc8a99422012-01-19 14:27:47 -0800381 */
382 private void underlyingChanged() {
Marc Blankb600a832012-02-16 09:20:18 -0800383 if (mCursorObserverRegistered) {
Marc Blankf9d87192012-02-16 10:50:41 -0800384 try {
385 sUnderlyingCursor.unregisterContentObserver(mCursorObserver);
386 } catch (IllegalStateException e) {
387 // Maybe the cursor was GC'd?
388 }
Marc Blankb600a832012-02-16 09:20:18 -0800389 mCursorObserverRegistered = false;
Marc Blank97bca7b2012-01-24 11:17:00 -0800390 }
Marc Blankb600a832012-02-16 09:20:18 -0800391 if (DEBUG) {
392 Log.d(TAG, "[Notify: onRefreshRequired()]");
393 }
Marc Blankb33465d2012-02-24 11:15:00 -0800394 synchronized(sListeners) {
395 for (ConversationListener listener: sListeners) {
396 listener.onRefreshRequired();
397 }
Marc Blankb600a832012-02-16 09:20:18 -0800398 }
399 sRefreshRequired = true;
Marc Blankc8a99422012-01-19 14:27:47 -0800400 }
401
Marc Blank4015c182012-01-31 12:38:36 -0800402 /**
403 * Put the refreshed cursor in place (called by the UI)
404 */
Marc Blank4e25c942012-02-02 19:41:14 -0800405 // NOTE: We don't like the name (it implies syncing with the server); suggestions gladly
406 // taken - reset? syncToUnderlying? completeRefresh? align?
407 public void sync() {
Marc Blank948985b2012-02-29 11:26:40 -0800408 synchronized (sCacheMapLock) {
409 if (DEBUG) {
410 Log.d(TAG, "[sync() called]");
411 }
412 if (sRequeryCursor == null) {
413 // This can happen during an animated deletion, if the UI isn't keeping track
414 // If we have no new data, this is a noop
415 Log.w(TAG, "UI calling sync() out of sequence");
416 }
417 resetCursor(sRequeryCursor);
418 sRequeryCursor = null;
419 sRefreshInProgress = false;
420 sRefreshReady = false;
Marc Blank3f1eb852012-02-03 15:38:01 -0800421 }
Marc Blank4e25c942012-02-02 19:41:14 -0800422 }
423
424 public boolean isRefreshRequired() {
425 return sRefreshRequired;
426 }
427
428 public boolean isRefreshReady() {
429 return sRefreshReady;
Marc Blank4015c182012-01-31 12:38:36 -0800430 }
431
432 /**
433 * Cancel a refresh in progress
434 */
435 public void cancelRefresh() {
Marc Blank3f1eb852012-02-03 15:38:01 -0800436 if (DEBUG) {
437 Log.d(TAG, "[cancelRefresh() called]");
438 }
Marc Blank4015c182012-01-31 12:38:36 -0800439 synchronized(sCacheMapLock) {
440 // Mark the requery closed
Marc Blank4e25c942012-02-02 19:41:14 -0800441 sRefreshInProgress = false;
Marc Blank948985b2012-02-29 11:26:40 -0800442 sRefreshReady = false;
Marc Blank4015c182012-01-31 12:38:36 -0800443 // If we have the cursor, close it; otherwise, it will get closed when the query
444 // finishes (it checks sRequeryInProgress)
445 if (sRequeryCursor != null) {
446 sRequeryCursor.close();
447 sRequeryCursor = null;
448 }
449 }
450 }
451
452 /**
453 * Get a list of deletions from ConversationCursor to the refreshed cursor that hasn't yet
454 * been swapped into place; this allows the UI to animate these away if desired
455 * @return a list of positions deleted in ConversationCursor
456 */
457 public ArrayList<Integer> getRefreshDeletions () {
Marc Blank3f1eb852012-02-03 15:38:01 -0800458 if (DEBUG) {
459 Log.d(TAG, "[getRefreshDeletions() called]");
460 }
Marc Blank4015c182012-01-31 12:38:36 -0800461 Cursor deviceCursor = sConversationCursor;
462 Cursor serverCursor = sRequeryCursor;
Mindy Pereira8e915722012-02-16 14:42:56 -0800463 // TODO: (mindyp) saw some instability here. Adding an assert to try to
464 // catch it.
465 assert(sRequeryCursor != null);
Marc Blank4015c182012-01-31 12:38:36 -0800466 ArrayList<Integer> deleteList = new ArrayList<Integer>();
467 int serverCount = serverCursor.getCount();
468 int deviceCount = deviceCursor.getCount();
469 deviceCursor.moveToFirst();
470 serverCursor.moveToFirst();
471 while (serverCount > 0 || deviceCount > 0) {
472 if (serverCount == 0) {
473 for (; deviceCount > 0; deviceCount--)
474 deleteList.add(deviceCursor.getPosition());
475 break;
476 } else if (deviceCount == 0) {
477 break;
478 }
479 long deviceMs = deviceCursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN);
480 long serverMs = serverCursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN);
481 String deviceUri = deviceCursor.getString(UIProvider.CONVERSATION_URI_COLUMN);
482 String serverUri = serverCursor.getString(UIProvider.CONVERSATION_URI_COLUMN);
483 deviceCursor.moveToNext();
484 serverCursor.moveToNext();
485 serverCount--;
486 deviceCount--;
487 if (serverMs == deviceMs) {
488 // Check for duplicates here; if our identical dates refer to different messages,
489 // we'll just quit here for now (at worst, this will cause a non-animating delete)
490 // My guess is that this happens VERY rarely, if at all
491 if (!deviceUri.equals(serverUri)) {
492 // To do this right, we'd find all of the rows with the same ms (date), etc...
493 //return deleteList;
494 }
495 continue;
496 } else if (deviceMs > serverMs) {
497 deleteList.add(deviceCursor.getPosition() - 1);
498 // Move back because we've already advanced cursor (that's why we subtract 1 above)
499 serverCount++;
500 serverCursor.moveToPrevious();
501 } else if (serverMs > deviceMs) {
502 // If we wanted to track insertions, we'd so so here
503 // Move back because we've already advanced cursor
504 deviceCount++;
505 deviceCursor.moveToPrevious();
506 }
507 }
508 Log.d(TAG, "Deletions: " + deleteList);
509 return deleteList;
Marc Blank48eba7a2012-01-27 16:16:19 -0800510 }
511
Marc Blank97bca7b2012-01-24 11:17:00 -0800512 /**
Marc Blank48eba7a2012-01-27 16:16:19 -0800513 * When we get a requery from the UI, we'll do it, but also clear the cache. The listener is
514 * notified when the requery is complete
Marc Blank97bca7b2012-01-24 11:17:00 -0800515 * NOTE: This will have to change, of course, when we start using loaders...
516 */
Marc Blank48eba7a2012-01-27 16:16:19 -0800517 public boolean refresh() {
Marc Blank3f1eb852012-02-03 15:38:01 -0800518 if (DEBUG) {
519 Log.d(TAG, "[refresh() called]");
520 }
Marc Blank4e25c942012-02-02 19:41:14 -0800521 if (sRefreshInProgress) {
Marc Blank48eba7a2012-01-27 16:16:19 -0800522 return false;
523 }
524 // Say we're starting a requery
Marc Blank4e25c942012-02-02 19:41:14 -0800525 sRefreshInProgress = true;
Marc Blank48eba7a2012-01-27 16:16:19 -0800526 new Thread(new Runnable() {
527 @Override
528 public void run() {
529 // Get new data
Marc Blank948985b2012-02-29 11:26:40 -0800530 sRequeryCursor = doQuery(qUri, qProjection);
Marc Blank48eba7a2012-01-27 16:16:19 -0800531 // Make sure window is full
Marc Blank4015c182012-01-31 12:38:36 -0800532 synchronized(sCacheMapLock) {
Marc Blank4e25c942012-02-02 19:41:14 -0800533 if (sRefreshInProgress) {
Marc Blank4015c182012-01-31 12:38:36 -0800534 sRequeryCursor.getCount();
Marc Blank948985b2012-02-29 11:26:40 -0800535 sRefreshReady = true;
Marc Blank4015c182012-01-31 12:38:36 -0800536 sActivity.runOnUiThread(new Runnable() {
537 @Override
538 public void run() {
Marc Blank3f1eb852012-02-03 15:38:01 -0800539 if (DEBUG) {
540 Log.d(TAG, "[Notify: onRefreshReady()]");
541 }
Paul Westbrook5449aee2012-02-24 16:19:47 -0800542 if (sRequeryCursor != null && !sRequeryCursor.isClosed()) {
543 synchronized (sListeners) {
544 for (ConversationListener listener : sListeners) {
545 listener.onRefreshReady();
546 }
Marc Blankb33465d2012-02-24 11:15:00 -0800547 }
Marc Blankb600a832012-02-16 09:20:18 -0800548 }
Marc Blank4015c182012-01-31 12:38:36 -0800549 }});
550 } else {
551 cancelRefresh();
552 }
553 }
Marc Blank48eba7a2012-01-27 16:16:19 -0800554 }
555 }).start();
Marc Blankc8a99422012-01-19 14:27:47 -0800556 return true;
557 }
558
Marc Blankb600a832012-02-16 09:20:18 -0800559 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800560 public void close() {
Marc Blankf9d87192012-02-16 10:50:41 -0800561 if (!sUnderlyingCursor.isClosed()) {
562 // Unregister our observer on the underlying cursor and close as usual
563 if (mCursorObserverRegistered) {
564 try {
565 sUnderlyingCursor.unregisterContentObserver(mCursorObserver);
566 } catch (IllegalStateException e) {
567 // Maybe the cursor got GC'd?
568 }
569 mCursorObserverRegistered = false;
570 }
571 sUnderlyingCursor.close();
Marc Blankdd10bc82012-02-01 19:10:46 -0800572 }
Marc Blankc8a99422012-01-19 14:27:47 -0800573 }
574
575 /**
576 * Move to the next not-deleted item in the conversation
577 */
Marc Blankb600a832012-02-16 09:20:18 -0800578 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800579 public boolean moveToNext() {
580 while (true) {
Marc Blank48eba7a2012-01-27 16:16:19 -0800581 boolean ret = sUnderlyingCursor.moveToNext();
Marc Blankc8a99422012-01-19 14:27:47 -0800582 if (!ret) return false;
583 if (getCachedValue(DELETED_COLUMN_INDEX) instanceof Integer) continue;
584 mPosition++;
585 return true;
586 }
587 }
588
589 /**
590 * Move to the previous not-deleted item in the conversation
591 */
Marc Blankb600a832012-02-16 09:20:18 -0800592 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800593 public boolean moveToPrevious() {
594 while (true) {
Marc Blank48eba7a2012-01-27 16:16:19 -0800595 boolean ret = sUnderlyingCursor.moveToPrevious();
Marc Blankc8a99422012-01-19 14:27:47 -0800596 if (!ret) return false;
597 if (getCachedValue(-1) instanceof Integer) continue;
598 mPosition--;
599 return true;
600 }
601 }
602
Marc Blankb600a832012-02-16 09:20:18 -0800603 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800604 public int getPosition() {
605 return mPosition;
606 }
607
608 /**
609 * The actual cursor's count must be decremented by the number we've deleted from the UI
610 */
Marc Blankb600a832012-02-16 09:20:18 -0800611 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800612 public int getCount() {
Marc Blank48eba7a2012-01-27 16:16:19 -0800613 return sUnderlyingCursor.getCount() - sDeletedCount;
Marc Blankc8a99422012-01-19 14:27:47 -0800614 }
615
Marc Blankb600a832012-02-16 09:20:18 -0800616 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800617 public boolean moveToFirst() {
Marc Blank48eba7a2012-01-27 16:16:19 -0800618 sUnderlyingCursor.moveToPosition(-1);
Marc Blankc8a99422012-01-19 14:27:47 -0800619 mPosition = -1;
620 return moveToNext();
621 }
622
Marc Blankb600a832012-02-16 09:20:18 -0800623 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800624 public boolean moveToPosition(int pos) {
Marc Blankdd10bc82012-02-01 19:10:46 -0800625 if (pos < -1 || pos >= getCount()) return false;
Marc Blankc8a99422012-01-19 14:27:47 -0800626 if (pos == mPosition) return true;
627 if (pos > mPosition) {
628 while (pos > mPosition) {
629 if (!moveToNext()) {
630 return false;
631 }
632 }
633 return true;
634 } else if (pos == 0) {
635 return moveToFirst();
636 } else {
637 while (pos < mPosition) {
638 if (!moveToPrevious()) {
639 return false;
640 }
641 }
642 return true;
643 }
644 }
645
Marc Blankb600a832012-02-16 09:20:18 -0800646 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800647 public boolean moveToLast() {
648 throw new UnsupportedOperationException("moveToLast unsupported!");
649 }
650
Marc Blankb600a832012-02-16 09:20:18 -0800651 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800652 public boolean move(int offset) {
653 throw new UnsupportedOperationException("move unsupported!");
654 }
655
656 /**
657 * We need to override all of the getters to make sure they look at cached values before using
658 * the values in the underlying cursor
659 */
660 @Override
661 public double getDouble(int columnIndex) {
662 Object obj = getCachedValue(columnIndex);
663 if (obj != null) return (Double)obj;
Marc Blank48eba7a2012-01-27 16:16:19 -0800664 return sUnderlyingCursor.getDouble(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800665 }
666
667 @Override
668 public float getFloat(int columnIndex) {
669 Object obj = getCachedValue(columnIndex);
670 if (obj != null) return (Float)obj;
Marc Blank48eba7a2012-01-27 16:16:19 -0800671 return sUnderlyingCursor.getFloat(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800672 }
673
674 @Override
675 public int getInt(int columnIndex) {
676 Object obj = getCachedValue(columnIndex);
677 if (obj != null) return (Integer)obj;
Marc Blank48eba7a2012-01-27 16:16:19 -0800678 return sUnderlyingCursor.getInt(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800679 }
680
681 @Override
682 public long getLong(int columnIndex) {
683 Object obj = getCachedValue(columnIndex);
684 if (obj != null) return (Long)obj;
Marc Blank48eba7a2012-01-27 16:16:19 -0800685 return sUnderlyingCursor.getLong(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800686 }
687
688 @Override
689 public short getShort(int columnIndex) {
690 Object obj = getCachedValue(columnIndex);
691 if (obj != null) return (Short)obj;
Marc Blank48eba7a2012-01-27 16:16:19 -0800692 return sUnderlyingCursor.getShort(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800693 }
694
695 @Override
696 public String getString(int columnIndex) {
697 // If we're asking for the Uri for the conversation list, we return a forwarding URI
698 // so that we can intercept update/delete and handle it ourselves
Marc Blank97bca7b2012-01-24 11:17:00 -0800699 if (columnIndex == sUriColumnIndex) {
Marc Blank48eba7a2012-01-27 16:16:19 -0800700 Uri uri = Uri.parse(sUnderlyingCursor.getString(columnIndex));
Marc Blankc8a99422012-01-19 14:27:47 -0800701 return uriToCachingUriString(uri);
702 }
703 Object obj = getCachedValue(columnIndex);
704 if (obj != null) return (String)obj;
Marc Blank48eba7a2012-01-27 16:16:19 -0800705 return sUnderlyingCursor.getString(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800706 }
707
708 @Override
709 public byte[] getBlob(int columnIndex) {
710 Object obj = getCachedValue(columnIndex);
711 if (obj != null) return (byte[])obj;
Marc Blank48eba7a2012-01-27 16:16:19 -0800712 return sUnderlyingCursor.getBlob(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800713 }
714
715 /**
716 * Observer of changes to underlying data
717 */
718 private class CursorObserver extends ContentObserver {
719 public CursorObserver() {
Mindy Pereira609480e2012-02-16 13:54:18 -0800720 super(null);
Marc Blankc8a99422012-01-19 14:27:47 -0800721 }
722
723 @Override
724 public void onChange(boolean selfChange) {
725 // If we're here, then something outside of the UI has changed the data, and we
Marc Blank48eba7a2012-01-27 16:16:19 -0800726 // must query the underlying provider for that data
Marc Blankc8a99422012-01-19 14:27:47 -0800727 if (DEBUG) {
728 Log.d(TAG, "Underlying conversation cursor changed; requerying");
729 }
730 // It's not at all obvious to me why we must unregister/re-register after the requery
731 // However, if we don't we'll only get one notification and no more...
Marc Blankc8a99422012-01-19 14:27:47 -0800732 ConversationCursor.this.underlyingChanged();
733 }
734 }
735
736 /**
737 * ConversationProvider is the ContentProvider for our forwarding Uri's; it passes queries
738 * and inserts directly, and caches updates/deletes before passing them through. The caching
739 * will cause a redraw of the list with updated values.
740 */
Paul Westbrook77177b12012-02-07 15:23:42 -0800741 public abstract static class ConversationProvider extends ContentProvider {
742 public static String AUTHORITY;
743
744 /**
745 * Allows the implmenting provider to specify the authority that should be used.
746 */
747 protected abstract String getAuthority();
Marc Blank8d69d4e2012-01-25 12:04:28 -0800748
Marc Blankc8a99422012-01-19 14:27:47 -0800749 @Override
750 public boolean onCreate() {
Marc Blank8d69d4e2012-01-25 12:04:28 -0800751 sProvider = this;
Paul Westbrook77177b12012-02-07 15:23:42 -0800752 AUTHORITY = getAuthority();
Marc Blank8d69d4e2012-01-25 12:04:28 -0800753 return true;
Marc Blankc8a99422012-01-19 14:27:47 -0800754 }
755
756 @Override
757 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
758 String sortOrder) {
759 return mResolver.query(
760 uriFromCachingUri(uri), projection, selection, selectionArgs, sortOrder);
761 }
762
763 @Override
Marc Blankf892f0a2012-01-30 13:04:10 -0800764 public Uri insert(Uri uri, ContentValues values) {
765 insertLocal(uri, values);
766 return ProviderExecute.opInsert(uri, values);
767 }
768
769 @Override
770 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
Marc Blank03bbaad2012-01-31 11:27:16 -0800771 updateLocal(uri, values);
Marc Blankf892f0a2012-01-30 13:04:10 -0800772 return ProviderExecute.opUpdate(uri, values);
773 }
774
775 @Override
776 public int delete(Uri uri, String selection, String[] selectionArgs) {
Marc Blank03bbaad2012-01-31 11:27:16 -0800777 deleteLocal(uri);
Marc Blankf892f0a2012-01-30 13:04:10 -0800778 return ProviderExecute.opDelete(uri);
779 }
780
781 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800782 public String getType(Uri uri) {
783 return null;
784 }
785
786 /**
787 * Quick and dirty class that executes underlying provider CRUD operations on a background
788 * thread.
789 */
790 static class ProviderExecute implements Runnable {
791 static final int DELETE = 0;
792 static final int INSERT = 1;
793 static final int UPDATE = 2;
794
795 final int mCode;
796 final Uri mUri;
797 final ContentValues mValues; //HEHEH
798
799 ProviderExecute(int code, Uri uri, ContentValues values) {
800 mCode = code;
801 mUri = uriFromCachingUri(uri);
802 mValues = values;
803 }
804
805 ProviderExecute(int code, Uri uri) {
806 this(code, uri, null);
807 }
808
Marc Blank8d69d4e2012-01-25 12:04:28 -0800809 static Uri opInsert(Uri uri, ContentValues values) {
810 ProviderExecute e = new ProviderExecute(INSERT, uri, values);
811 if (offUiThread()) return (Uri)e.go();
812 new Thread(e).start();
813 return null;
Marc Blankc8a99422012-01-19 14:27:47 -0800814 }
815
Marc Blank8d69d4e2012-01-25 12:04:28 -0800816 static int opDelete(Uri uri) {
817 ProviderExecute e = new ProviderExecute(DELETE, uri);
818 if (offUiThread()) return (Integer)e.go();
819 new Thread(new ProviderExecute(DELETE, uri)).start();
820 return 0;
821 }
822
823 static int opUpdate(Uri uri, ContentValues values) {
824 ProviderExecute e = new ProviderExecute(UPDATE, uri, values);
825 if (offUiThread()) return (Integer)e.go();
826 new Thread(e).start();
827 return 0;
Marc Blankc8a99422012-01-19 14:27:47 -0800828 }
829
830 @Override
831 public void run() {
Marc Blank8d69d4e2012-01-25 12:04:28 -0800832 go();
833 }
834
835 public Object go() {
Marc Blankc8a99422012-01-19 14:27:47 -0800836 switch(mCode) {
837 case DELETE:
Marc Blank8d69d4e2012-01-25 12:04:28 -0800838 return mResolver.delete(mUri, null, null);
Marc Blankc8a99422012-01-19 14:27:47 -0800839 case INSERT:
Marc Blank8d69d4e2012-01-25 12:04:28 -0800840 return mResolver.insert(mUri, mValues);
Marc Blankc8a99422012-01-19 14:27:47 -0800841 case UPDATE:
Marc Blank8d69d4e2012-01-25 12:04:28 -0800842 return mResolver.update(mUri, mValues, null, null);
843 default:
844 return null;
Marc Blankc8a99422012-01-19 14:27:47 -0800845 }
846 }
847 }
848
Marc Blank8d69d4e2012-01-25 12:04:28 -0800849 private void insertLocal(Uri uri, ContentValues values) {
850 // Placeholder for now; there's no local insert
851 }
852
Marc Blank248b1b42012-02-07 13:43:02 -0800853 @VisibleForTesting
854 void deleteLocal(Uri uri) {
Marc Blank8d69d4e2012-01-25 12:04:28 -0800855 Uri underlyingUri = uriFromCachingUri(uri);
Mindy Pereira8a77f8b2012-02-02 16:34:42 -0800856 // Remember to decode the underlying Uri as it might be encoded (as w/ Gmail)
857 String uriString = Uri.decode(underlyingUri.toString());
Marc Blank8d69d4e2012-01-25 12:04:28 -0800858 cacheValue(uriString, DELETED_COLUMN, true);
Marc Blankc8a99422012-01-19 14:27:47 -0800859 }
860
Marc Blank248b1b42012-02-07 13:43:02 -0800861 @VisibleForTesting
862 void updateLocal(Uri uri, ContentValues values) {
Mindy Pereiraf98b3182012-02-22 11:07:13 -0800863 if (values == null) {
864 return;
865 }
Marc Blankc8a99422012-01-19 14:27:47 -0800866 Uri underlyingUri = uriFromCachingUri(uri);
867 // Remember to decode the underlying Uri as it might be encoded (as w/ Gmail)
868 String uriString = Uri.decode(underlyingUri.toString());
869 for (String columnName: values.keySet()) {
870 cacheValue(uriString, columnName, values.get(columnName));
871 }
Marc Blank8d69d4e2012-01-25 12:04:28 -0800872 }
873
Marc Blank03bbaad2012-01-31 11:27:16 -0800874 static boolean offUiThread() {
Marc Blank8d69d4e2012-01-25 12:04:28 -0800875 return Looper.getMainLooper().getThread() != Thread.currentThread();
876 }
877
Marc Blank1b9efd92012-02-01 14:27:55 -0800878 public int apply(ArrayList<ConversationOperation> ops) {
Marc Blank8d69d4e2012-01-25 12:04:28 -0800879 final HashMap<String, ArrayList<ContentProviderOperation>> batchMap =
880 new HashMap<String, ArrayList<ContentProviderOperation>>();
Marc Blankb31ab5a2012-02-01 12:28:29 -0800881 // Increment sequence count
882 sSequence++;
Marc Blankf892f0a2012-01-30 13:04:10 -0800883 // Execute locally and build CPO's for underlying provider
Marc Blank8d69d4e2012-01-25 12:04:28 -0800884 for (ConversationOperation op: ops) {
885 Uri underlyingUri = uriFromCachingUri(op.mUri);
886 String authority = underlyingUri.getAuthority();
887 ArrayList<ContentProviderOperation> authOps = batchMap.get(authority);
888 if (authOps == null) {
889 authOps = new ArrayList<ContentProviderOperation>();
890 batchMap.put(authority, authOps);
891 }
892 authOps.add(op.execute(underlyingUri));
Marc Blankf892f0a2012-01-30 13:04:10 -0800893 }
894
895 // Send changes to underlying provider
Marc Blank8d69d4e2012-01-25 12:04:28 -0800896 for (String authority: batchMap.keySet()) {
897 try {
898 if (offUiThread()) {
Marc Blankb31ab5a2012-02-01 12:28:29 -0800899 mResolver.applyBatch(authority, batchMap.get(authority));
Marc Blank8d69d4e2012-01-25 12:04:28 -0800900 } else {
901 final String auth = authority;
902 new Thread(new Runnable() {
903 @Override
904 public void run() {
905 try {
906 mResolver.applyBatch(auth, batchMap.get(auth));
907 } catch (RemoteException e) {
908 } catch (OperationApplicationException e) {
909 }
910 }
911 }).start();
Marc Blank8d69d4e2012-01-25 12:04:28 -0800912 }
913 } catch (RemoteException e) {
914 } catch (OperationApplicationException e) {
915 }
916 }
Marc Blank1b9efd92012-02-01 14:27:55 -0800917 return sSequence;
Marc Blank8d69d4e2012-01-25 12:04:28 -0800918 }
919 }
920
921 /**
922 * ConversationOperation is the encapsulation of a ContentProvider operation to be performed
923 * atomically as part of a "batch" operation.
924 */
925 public static class ConversationOperation {
926 public static final int DELETE = 0;
927 public static final int INSERT = 1;
928 public static final int UPDATE = 2;
Mindy Pereiraf98b3182012-02-22 11:07:13 -0800929 public static final int ARCHIVE = 3;
Mindy Pereira830c00f2012-02-22 11:43:49 -0800930 public static final int MUTE = 4;
931 public static final int REPORT_SPAM = 5;
Marc Blank8d69d4e2012-01-25 12:04:28 -0800932
933 private final int mType;
934 private final Uri mUri;
935 private final ContentValues mValues;
Marc Blankce538182012-02-03 13:04:27 -0800936 // True if an updated item should be removed locally (from ConversationCursor)
937 // This would be the case for a folder/label change in which the conversation is no longer
938 // in the folder represented by the ConversationCursor
939 private final boolean mLocalDeleteOnUpdate;
Marc Blank8d69d4e2012-01-25 12:04:28 -0800940
Marc Blankf892f0a2012-01-30 13:04:10 -0800941 public ConversationOperation(int type, Conversation conv) {
942 this(type, conv, null);
Marc Blank8d69d4e2012-01-25 12:04:28 -0800943 }
944
Marc Blankf892f0a2012-01-30 13:04:10 -0800945 public ConversationOperation(int type, Conversation conv, ContentValues values) {
Marc Blank8d69d4e2012-01-25 12:04:28 -0800946 mType = type;
Marc Blankc43bc0a2012-02-02 11:25:18 -0800947 mUri = conv.uri;
Marc Blank8d69d4e2012-01-25 12:04:28 -0800948 mValues = values;
Marc Blankce538182012-02-03 13:04:27 -0800949 mLocalDeleteOnUpdate = conv.localDeleteOnUpdate;
Marc Blank8d69d4e2012-01-25 12:04:28 -0800950 }
951
952 private ContentProviderOperation execute(Uri underlyingUri) {
Marc Blankb31ab5a2012-02-01 12:28:29 -0800953 Uri uri = underlyingUri.buildUpon()
Marc Blankdd10bc82012-02-01 19:10:46 -0800954 .appendQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER,
955 Integer.toString(sSequence))
Marc Blankb31ab5a2012-02-01 12:28:29 -0800956 .build();
Marc Blank8d69d4e2012-01-25 12:04:28 -0800957 switch(mType) {
958 case DELETE:
Marc Blank03bbaad2012-01-31 11:27:16 -0800959 sProvider.deleteLocal(mUri);
Marc Blankb31ab5a2012-02-01 12:28:29 -0800960 return ContentProviderOperation.newDelete(uri).build();
Marc Blank8d69d4e2012-01-25 12:04:28 -0800961 case UPDATE:
Marc Blankce538182012-02-03 13:04:27 -0800962 if (mLocalDeleteOnUpdate) {
Marc Blank995fff52012-02-06 13:28:20 -0800963 sProvider.deleteLocal(mUri);
Marc Blankce538182012-02-03 13:04:27 -0800964 } else {
965 sProvider.updateLocal(mUri, mValues);
966 }
Marc Blankb31ab5a2012-02-01 12:28:29 -0800967 return ContentProviderOperation.newUpdate(uri)
Marc Blank8d69d4e2012-01-25 12:04:28 -0800968 .withValues(mValues)
969 .build();
970 case INSERT:
971 sProvider.insertLocal(mUri, mValues);
Marc Blankb31ab5a2012-02-01 12:28:29 -0800972 return ContentProviderOperation.newInsert(uri)
Marc Blank8d69d4e2012-01-25 12:04:28 -0800973 .withValues(mValues).build();
Mindy Pereiraf98b3182012-02-22 11:07:13 -0800974 case ARCHIVE:
Mindy Pereiraf98b3182012-02-22 11:07:13 -0800975 sProvider.deleteLocal(mUri);
Paul Westbrook334e64a2012-02-23 13:26:35 -0800976
977 // Create an update operation that represents archive
978 return ContentProviderOperation.newUpdate(uri).withValue(
979 ConversationOperations.OPERATION_KEY, ConversationOperations.ARCHIVE)
980 .build();
Mindy Pereira830c00f2012-02-22 11:43:49 -0800981 case MUTE:
Paul Westbrook334e64a2012-02-23 13:26:35 -0800982 if (mLocalDeleteOnUpdate) {
983 sProvider.deleteLocal(mUri);
984 }
985
986 // Create an update operation that represents mute
987 return ContentProviderOperation.newUpdate(uri).withValue(
988 ConversationOperations.OPERATION_KEY, ConversationOperations.MUTE)
989 .build();
Mindy Pereira830c00f2012-02-22 11:43:49 -0800990 case REPORT_SPAM:
Mindy Pereira830c00f2012-02-22 11:43:49 -0800991 sProvider.deleteLocal(mUri);
Paul Westbrook334e64a2012-02-23 13:26:35 -0800992
993 // Create an update operation that represents report spam
994 return ContentProviderOperation.newUpdate(uri).withValue(
995 ConversationOperations.OPERATION_KEY,
996 ConversationOperations.REPORT_SPAM).build();
Marc Blank8d69d4e2012-01-25 12:04:28 -0800997 default:
998 throw new UnsupportedOperationException(
999 "No such ConversationOperation type: " + mType);
1000 }
Marc Blankc8a99422012-01-19 14:27:47 -08001001 }
1002 }
Marc Blank97bca7b2012-01-24 11:17:00 -08001003
1004 /**
1005 * For now, a single listener can be associated with the cursor, and for now we'll just
1006 * notify on deletions
1007 */
1008 public interface ConversationListener {
Marc Blank48eba7a2012-01-27 16:16:19 -08001009 // Data in the underlying provider has changed; a refresh is required to sync up
1010 public void onRefreshRequired();
1011 // We've completed a requested refresh of the underlying cursor
1012 public void onRefreshReady();
1013 }
1014
1015 @Override
1016 public boolean isFirst() {
1017 throw new UnsupportedOperationException();
1018 }
1019
1020 @Override
1021 public boolean isLast() {
1022 throw new UnsupportedOperationException();
1023 }
1024
1025 @Override
1026 public boolean isBeforeFirst() {
1027 throw new UnsupportedOperationException();
1028 }
1029
1030 @Override
1031 public boolean isAfterLast() {
1032 throw new UnsupportedOperationException();
1033 }
1034
1035 @Override
1036 public int getColumnIndex(String columnName) {
1037 return sUnderlyingCursor.getColumnIndex(columnName);
1038 }
1039
1040 @Override
1041 public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
1042 return sUnderlyingCursor.getColumnIndexOrThrow(columnName);
1043 }
1044
1045 @Override
1046 public String getColumnName(int columnIndex) {
1047 return sUnderlyingCursor.getColumnName(columnIndex);
1048 }
1049
1050 @Override
1051 public String[] getColumnNames() {
1052 return sUnderlyingCursor.getColumnNames();
1053 }
1054
1055 @Override
1056 public int getColumnCount() {
1057 return sUnderlyingCursor.getColumnCount();
1058 }
1059
1060 @Override
1061 public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
1062 throw new UnsupportedOperationException();
1063 }
1064
1065 @Override
1066 public int getType(int columnIndex) {
1067 return sUnderlyingCursor.getType(columnIndex);
1068 }
1069
1070 @Override
1071 public boolean isNull(int columnIndex) {
1072 throw new UnsupportedOperationException();
1073 }
1074
1075 @Override
1076 public void deactivate() {
1077 throw new UnsupportedOperationException();
1078 }
1079
1080 @Override
1081 public boolean isClosed() {
1082 return sUnderlyingCursor.isClosed();
1083 }
1084
1085 @Override
1086 public void registerContentObserver(ContentObserver observer) {
Marc Blankdd10bc82012-02-01 19:10:46 -08001087 sUnderlyingCursor.registerContentObserver(observer);
Marc Blank48eba7a2012-01-27 16:16:19 -08001088 }
1089
1090 @Override
1091 public void unregisterContentObserver(ContentObserver observer) {
Marc Blankdd10bc82012-02-01 19:10:46 -08001092 sUnderlyingCursor.unregisterContentObserver(observer);
Marc Blank48eba7a2012-01-27 16:16:19 -08001093 }
1094
1095 @Override
1096 public void registerDataSetObserver(DataSetObserver observer) {
Marc Blankdd10bc82012-02-01 19:10:46 -08001097 sUnderlyingCursor.registerDataSetObserver(observer);
Marc Blank48eba7a2012-01-27 16:16:19 -08001098 }
1099
1100 @Override
1101 public void unregisterDataSetObserver(DataSetObserver observer) {
Marc Blankdd10bc82012-02-01 19:10:46 -08001102 sUnderlyingCursor.unregisterDataSetObserver(observer);
Marc Blank48eba7a2012-01-27 16:16:19 -08001103 }
1104
1105 @Override
1106 public void setNotificationUri(ContentResolver cr, Uri uri) {
1107 throw new UnsupportedOperationException();
1108 }
1109
1110 @Override
1111 public boolean getWantsAllOnMoveCalls() {
1112 throw new UnsupportedOperationException();
1113 }
1114
1115 @Override
1116 public Bundle getExtras() {
1117 throw new UnsupportedOperationException();
1118 }
1119
1120 @Override
1121 public Bundle respond(Bundle extras) {
1122 throw new UnsupportedOperationException();
1123 }
1124
1125 @Override
1126 public boolean requery() {
1127 return true;
Marc Blank97bca7b2012-01-24 11:17:00 -08001128 }
Marc Blankc8a99422012-01-19 14:27:47 -08001129}