blob: b006f4ccc95deef4d26d70079b135c4279a0f9d0 [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;
Paul Westbrookbf232c32012-04-18 03:17:41 -070025import android.content.Context;
Marc Blank8d69d4e2012-01-25 12:04:28 -080026import android.content.OperationApplicationException;
Marc Blank48eba7a2012-01-27 16:16:19 -080027import android.database.CharArrayBuffer;
Marc Blankc8a99422012-01-19 14:27:47 -080028import android.database.ContentObserver;
29import android.database.Cursor;
Marc Blank6ca57e82012-03-20 19:09:12 -070030import android.database.CursorWrapper;
Andy Huang397621b2012-03-14 20:52:39 -070031import android.database.DataSetObservable;
Marc Blank48eba7a2012-01-27 16:16:19 -080032import android.database.DataSetObserver;
Marc Blankc8a99422012-01-19 14:27:47 -080033import android.net.Uri;
Marc Blanke3d36792012-04-02 09:30:14 -070034import android.os.AsyncTask;
Marc Blank48eba7a2012-01-27 16:16:19 -080035import android.os.Bundle;
Marc Blank8d69d4e2012-01-25 12:04:28 -080036import android.os.Looper;
37import android.os.RemoteException;
Marc Blankc8a99422012-01-19 14:27:47 -080038
Marc Blankf892f0a2012-01-30 13:04:10 -080039import com.android.mail.providers.Conversation;
Marc Blank4015c182012-01-31 12:38:36 -080040import com.android.mail.providers.UIProvider;
Marc Blank51144942012-03-20 13:59:32 -070041import com.android.mail.providers.UIProvider.ConversationListQueryParameters;
Paul Westbrook334e64a2012-02-23 13:26:35 -080042import com.android.mail.providers.UIProvider.ConversationOperations;
Marc Blank03fa19a2012-02-29 13:16:27 -080043import com.android.mail.utils.LogUtils;
Marc Blank248b1b42012-02-07 13:43:02 -080044import com.google.common.annotations.VisibleForTesting;
Paul Westbrookbf232c32012-04-18 03:17:41 -070045import com.google.common.collect.Lists;
Marc Blankf892f0a2012-01-30 13:04:10 -080046
Marc Blank97bca7b2012-01-24 11:17:00 -080047import java.util.ArrayList;
Paul Westbrookbf232c32012-04-18 03:17:41 -070048import java.util.Arrays;
49import java.util.Collection;
Marc Blankc8a99422012-01-19 14:27:47 -080050import java.util.HashMap;
Marc Blank48eba7a2012-01-27 16:16:19 -080051import java.util.Iterator;
Marc Blankc8a99422012-01-19 14:27:47 -080052import java.util.List;
53
54/**
55 * ConversationCursor is a wrapper around a conversation list cursor that provides update/delete
56 * caching for quick UI response. This is effectively a singleton class, as the cache is
57 * implemented as a static HashMap.
58 */
Marc Blank48eba7a2012-01-27 16:16:19 -080059public final class ConversationCursor implements Cursor {
Marc Blankc8a99422012-01-19 14:27:47 -080060 private static final String TAG = "ConversationCursor";
Marc Blankcf164d62012-04-20 08:56:17 -070061
Marc Blankfc883312012-07-26 09:55:11 -070062 private static final boolean DEBUG = true; // STOPSHIP Set to false before shipping
Marc Blankc8a99422012-01-19 14:27:47 -080063 // A deleted row is indicated by the presence of DELETED_COLUMN in the cache map
64 private static final String DELETED_COLUMN = "__deleted__";
Marc Blank48eba7a2012-01-27 16:16:19 -080065 // An row cached during a requery is indicated by the presence of REQUERY_COLUMN in the map
66 private static final String REQUERY_COLUMN = "__requery__";
Marc Blankc8a99422012-01-19 14:27:47 -080067 // A sentinel value for the "index" of the deleted column; it's an int that is otherwise invalid
68 private static final int DELETED_COLUMN_INDEX = -1;
Marc Blank8e074ab2012-03-07 09:21:40 -080069 // Empty deletion list
Vikram Aggarwal7f602f72012-04-30 16:04:06 -070070 private static final Collection<Conversation> EMPTY_DELETION_LIST = Lists.newArrayList();
Marc Blank97bca7b2012-01-24 11:17:00 -080071 // The index of the Uri whose data is reflected in the cached row
72 // Updates/Deletes to this Uri are cached
73 private static int sUriColumnIndex;
Marc Blankcf164d62012-04-20 08:56:17 -070074 // Our sequence count (for changes sent to underlying provider)
75 private static int sSequence = 0;
Marc Blankcf164d62012-04-20 08:56:17 -070076 // The resolver for the cursor instantiator's context
Paul Westbrookbf232c32012-04-18 03:17:41 -070077 private static ContentResolver sResolver;
78 @VisibleForTesting
79 static ConversationProvider sProvider;
80
81 // The cursor instantiator's activity
82 private Activity mActivity;
83 // The cursor underlying the caching cursor
84 @VisibleForTesting
85 Wrapper mUnderlyingCursor;
86 // The new cursor obtained via a requery
87 private volatile Wrapper mRequeryCursor;
88 // A mapping from Uri to updated ContentValues
89 private HashMap<String, ContentValues> mCacheMap = new HashMap<String, ContentValues>();
90 // Cache map lock (will be used only very briefly - few ms at most)
91 private Object mCacheMapLock = new Object();
92 // The listeners registered for this cursor
93 private List<ConversationListener> mListeners = Lists.newArrayList();
94 // The ConversationProvider instance
95 // The runnable executing a refresh (query of underlying provider)
96 private RefreshTask mRefreshTask;
97 // Set when we've sent refreshReady() to listeners
98 private boolean mRefreshReady = false;
99 // Set when we've sent refreshRequired() to listeners
100 private boolean mRefreshRequired = false;
101 // Whether our first query on this cursor should include a limit
102 private boolean mInitialConversationLimit = false;
103 // A list of mostly-dead items
104 private List<Conversation> sMostlyDead = Lists.newArrayList();
105 // The name of the loader
106 private final String mName;
107 // Column names for this cursor
108 private String[] mColumnNames;
Marc Blankc8a99422012-01-19 14:27:47 -0800109 // An observer on the underlying cursor (so we can detect changes from outside the UI)
110 private final CursorObserver mCursorObserver;
Marc Blank97bca7b2012-01-24 11:17:00 -0800111 // Whether our observer is currently registered with the underlying cursor
112 private boolean mCursorObserverRegistered = false;
Paul Westbrookbf232c32012-04-18 03:17:41 -0700113 // Whether our loader is paused
114 private boolean mPaused = false;
Marc Blanke1d1b072012-04-13 17:29:16 -0700115 // Whether or not sync from underlying provider should be deferred
Paul Westbrookbf232c32012-04-18 03:17:41 -0700116 private boolean mDeferSync = false;
Marc Blankc8a99422012-01-19 14:27:47 -0800117
118 // The current position of the cursor
119 private int mPosition = -1;
Andy Huang397621b2012-03-14 20:52:39 -0700120
Marc Blankc8a99422012-01-19 14:27:47 -0800121 // The number of cached deletions from this cursor (used to quickly generate an accurate count)
Paul Westbrookbf232c32012-04-18 03:17:41 -0700122 private int mDeletedCount = 0;
Marc Blankc8a99422012-01-19 14:27:47 -0800123
Marc Blank48eba7a2012-01-27 16:16:19 -0800124 // Parameters passed to the underlying query
Paul Westbrookbf232c32012-04-18 03:17:41 -0700125 private Uri qUri;
126 private String[] qProjection;
Marc Blank48eba7a2012-01-27 16:16:19 -0800127
Paul Westbrookbf232c32012-04-18 03:17:41 -0700128 private void setCursor(Wrapper cursor) {
Marc Blankc16be932012-02-24 12:43:48 -0800129 // If we have an existing underlying cursor, make sure it's closed
Paul Westbrookbf232c32012-04-18 03:17:41 -0700130 if (mUnderlyingCursor != null) {
131 mUnderlyingCursor.close();
Marc Blankc16be932012-02-24 12:43:48 -0800132 }
Marc Blankc8a99422012-01-19 14:27:47 -0800133 mColumnNames = cursor.getColumnNames();
Paul Westbrookbf232c32012-04-18 03:17:41 -0700134 mRefreshRequired = false;
135 mRefreshReady = false;
136 mRefreshTask = null;
137 resetCursor(cursor);
Marc Blankc8a99422012-01-19 14:27:47 -0800138 }
139
Paul Westbrookbf232c32012-04-18 03:17:41 -0700140 public ConversationCursor(Activity activity, Uri uri, boolean initialConversationLimit,
141 String name) {
142 //sActivity = activity;
143 mInitialConversationLimit = initialConversationLimit;
144 sResolver = activity.getContentResolver();
145 sUriColumnIndex = UIProvider.CONVERSATION_URI_COLUMN;
146 qUri = uri;
147 mName = name;
148 qProjection = UIProvider.CONVERSATION_PROJECTION;
149 mCursorObserver = new CursorObserver();
Paul Westbrookff5c7572012-03-16 13:43:18 -0700150 }
151
152 /**
Marc Blank48eba7a2012-01-27 16:16:19 -0800153 * Create a ConversationCursor; this should be called by the ListActivity using that cursor
154 * @param activity the activity creating the cursor
155 * @param messageListColumn the column used for individual cursor items
156 * @param uri the query uri
157 * @param projection the query projecion
158 * @param selection the query selection
159 * @param selectionArgs the query selection args
160 * @param sortOrder the query sort order
161 * @return a ConversationCursor
Marc Blankc8a99422012-01-19 14:27:47 -0800162 */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700163 public void load() {
164 synchronized (mCacheMapLock) {
Marc Blank51144942012-03-20 13:59:32 -0700165 try {
Marc Blank51144942012-03-20 13:59:32 -0700166 // Create new ConversationCursor
167 LogUtils.i(TAG, "Create: initial creation");
Paul Westbrookbf232c32012-04-18 03:17:41 -0700168 Wrapper c = doQuery(mInitialConversationLimit);
169 setCursor(c);
Marc Blank51144942012-03-20 13:59:32 -0700170 } finally {
171 // If we used a limit, queue up a query without limit
Paul Westbrookbf232c32012-04-18 03:17:41 -0700172 if (mInitialConversationLimit) {
173 mInitialConversationLimit = false;
174 refresh();
Marc Blank51144942012-03-20 13:59:32 -0700175 }
Marc Blankc16be932012-02-24 12:43:48 -0800176 }
177 }
Marc Blank948985b2012-02-29 11:26:40 -0800178 }
179
Marc Blank6ca57e82012-03-20 19:09:12 -0700180 /**
Marc Blanke1d1b072012-04-13 17:29:16 -0700181 * Pause notifications to UI
182 */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700183 public void pause() {
Marc Blanke1d1b072012-04-13 17:29:16 -0700184 if (DEBUG) {
Paul Westbrooke2bde3a2012-07-31 17:35:43 -0700185 LogUtils.i(TAG, "[Paused: %s]", mName);
Marc Blanke1d1b072012-04-13 17:29:16 -0700186 }
Marc Blank1c391172012-04-23 09:23:17 -0700187 mPaused = true;
Marc Blanke1d1b072012-04-13 17:29:16 -0700188 }
189
190 /**
191 * Resume notifications to UI; if any are pending, send them
192 */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700193 public void resume() {
Marc Blanke1d1b072012-04-13 17:29:16 -0700194 if (DEBUG) {
Paul Westbrooke2bde3a2012-07-31 17:35:43 -0700195 LogUtils.i(TAG, "[Resumed: %s]", mName);
Marc Blanke1d1b072012-04-13 17:29:16 -0700196 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700197 mPaused = false;
Marc Blanke1d1b072012-04-13 17:29:16 -0700198 checkNotifyUI();
199 }
200
Paul Westbrookbf232c32012-04-18 03:17:41 -0700201 private void checkNotifyUI() {
202 if (!mPaused && !mDeferSync) {
203 if (mRefreshRequired && (mRefreshTask == null)) {
204 notifyRefreshRequired();
205 } else if (mRefreshReady) {
206 notifyRefreshReady();
Marc Blanke1d1b072012-04-13 17:29:16 -0700207 }
208 } else {
Paul Westbrooke2bde3a2012-07-31 17:35:43 -0700209 LogUtils.i(TAG, "[checkNotifyUI: %s%s",
210 (mPaused ? "Paused " : ""), (mDeferSync ? "Defer" : ""));
Marc Blanke1d1b072012-04-13 17:29:16 -0700211 }
212 }
213
214 /**
Marc Blanke3d36792012-04-02 09:30:14 -0700215 * Runnable that performs the query on the underlying provider
216 */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700217 private class RefreshTask extends AsyncTask<Void, Void, Void> {
Marc Blanke3d36792012-04-02 09:30:14 -0700218 private Wrapper mCursor = null;
Marc Blanke3d36792012-04-02 09:30:14 -0700219
Paul Westbrookbf232c32012-04-18 03:17:41 -0700220 private RefreshTask() {
Marc Blanke3d36792012-04-02 09:30:14 -0700221 }
222
223 @Override
224 protected Void doInBackground(Void... params) {
225 if (DEBUG) {
Paul Westbrooke2bde3a2012-07-31 17:35:43 -0700226 LogUtils.i(TAG, "[Start refresh of %s: %d]", mName, hashCode());
Marc Blanke3d36792012-04-02 09:30:14 -0700227 }
228 // Get new data
Paul Westbrookbf232c32012-04-18 03:17:41 -0700229 mCursor = doQuery(false);
Marc Blanke3d36792012-04-02 09:30:14 -0700230 return null;
231 }
232
233 @Override
234 protected void onPostExecute(Void param) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700235 synchronized(mCacheMapLock) {
236 mRequeryCursor = mCursor;
Marc Blanke3d36792012-04-02 09:30:14 -0700237 // Make sure window is full
Paul Westbrookbf232c32012-04-18 03:17:41 -0700238 mRequeryCursor.getCount();
239 mRefreshReady = true;
Marc Blanke3d36792012-04-02 09:30:14 -0700240 if (DEBUG) {
Paul Westbrooke2bde3a2012-07-31 17:35:43 -0700241 LogUtils.i(TAG, "[Query done %s: %d]", mName, hashCode());
Marc Blanke3d36792012-04-02 09:30:14 -0700242 }
Marc Blank1c391172012-04-23 09:23:17 -0700243 if (!mDeferSync && !mPaused) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700244 notifyRefreshReady();
Marc Blank1c391172012-04-23 09:23:17 -0700245 }
Marc Blanke3d36792012-04-02 09:30:14 -0700246 }
247 }
248
249 @Override
250 protected void onCancelled() {
251 if (DEBUG) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700252 LogUtils.i(TAG, "[Ignoring refresh result: %d]", hashCode());
Marc Blanke3d36792012-04-02 09:30:14 -0700253 }
254 if (mCursor != null) {
255 mCursor.close();
256 }
257 }
258 }
259
260 /**
Marc Blank6ca57e82012-03-20 19:09:12 -0700261 * Wrapper that includes the Uri used to create the cursor
262 */
263 private static class Wrapper extends CursorWrapper {
264 private final Uri mUri;
265
266 Wrapper(Cursor cursor, Uri uri) {
267 super(cursor);
268 mUri = uri;
269 }
Marc Blank6ca57e82012-03-20 19:09:12 -0700270 }
271
Paul Westbrookbf232c32012-04-18 03:17:41 -0700272 private Wrapper doQuery(boolean withLimit) {
273 if (sResolver == null) {
274 sResolver = mActivity.getContentResolver();
Mindy Pereira3982e232012-02-29 15:00:34 -0800275 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700276 Uri uri = qUri;
Marc Blank51144942012-03-20 13:59:32 -0700277 if (withLimit) {
278 uri = uri.buildUpon().appendQueryParameter(ConversationListQueryParameters.LIMIT,
279 ConversationListQueryParameters.DEFAULT_LIMIT).build();
280 }
281 long time = System.currentTimeMillis();
Paul Westbrookdac65802012-03-23 16:59:15 -0700282
Paul Westbrookbf232c32012-04-18 03:17:41 -0700283 Wrapper result = new Wrapper(sResolver.query(uri, qProjection, null, null, null), uri);
Marc Blank51144942012-03-20 13:59:32 -0700284 if (DEBUG) {
285 time = System.currentTimeMillis() - time;
Paul Westbrookdac65802012-03-23 16:59:15 -0700286 LogUtils.i(TAG, "ConversationCursor query: %s, %dms, %d results",
287 uri, time, result.getCount());
Marc Blank51144942012-03-20 13:59:32 -0700288 }
289 return result;
Marc Blank48eba7a2012-01-27 16:16:19 -0800290 }
291
292 /**
293 * Return whether the uri string (message list uri) is in the underlying cursor
294 * @param uriString the uri string we're looking for
295 * @return true if the uri string is in the cursor; false otherwise
296 */
297 private boolean isInUnderlyingCursor(String uriString) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700298 mUnderlyingCursor.moveToPosition(-1);
299 while (mUnderlyingCursor.moveToNext()) {
300 if (uriString.equals(mUnderlyingCursor.getString(sUriColumnIndex))) {
Marc Blank48eba7a2012-01-27 16:16:19 -0800301 return true;
302 }
Marc Blank97bca7b2012-01-24 11:17:00 -0800303 }
Marc Blank48eba7a2012-01-27 16:16:19 -0800304 return false;
305 }
306
Marc Blank3232a962012-03-08 15:32:37 -0800307 static boolean offUiThread() {
308 return Looper.getMainLooper().getThread() != Thread.currentThread();
309 }
310
Marc Blank48eba7a2012-01-27 16:16:19 -0800311 /**
312 * Reset the cursor; this involves clearing out our cache map and resetting our various counts
313 * The cursor should be reset whenever we get fresh data from the underlying cursor. The cache
314 * is locked during the reset, which will block the UI, but for only a very short time
315 * (estimated at a few ms, but we can profile this; remember that the cache will usually
316 * be empty or have a few entries)
317 */
Marc Blank6ca57e82012-03-20 19:09:12 -0700318 private void resetCursor(Wrapper newCursor) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700319 synchronized (mCacheMapLock) {
Marc Blank48eba7a2012-01-27 16:16:19 -0800320 // Walk through the cache. Here are the cases:
321 // 1) The entry isn't marked with REQUERY - remove it from the cache. If DELETED is
322 // set, decrement the deleted count
323 // 2) The REQUERY entry is still in the UP
324 // 2a) The REQUERY entry isn't DELETED; we're good, and the client change will remain
325 // (i.e. client wins, it's on its way to the UP)
326 // 2b) The REQUERY entry is DELETED; we're good (client change remains, it's on
327 // its way to the UP)
328 // 3) the REQUERY was deleted on the server (sheesh; this would be bizarre timing!) -
329 // we need to throw the item out of the cache
330 // So ... the only interesting case is #3, we need to look for remaining deleted items
331 // and see if they're still in the UP
Paul Westbrookbf232c32012-04-18 03:17:41 -0700332 Iterator<HashMap.Entry<String, ContentValues>> iter = mCacheMap.entrySet().iterator();
Marc Blank48eba7a2012-01-27 16:16:19 -0800333 while (iter.hasNext()) {
334 HashMap.Entry<String, ContentValues> entry = iter.next();
335 ContentValues values = entry.getValue();
336 if (values.containsKey(REQUERY_COLUMN) && isInUnderlyingCursor(entry.getKey())) {
337 // If we're in a requery and we're still around, remove the requery key
338 // We're good here, the cached change (delete/update) is on its way to UP
339 values.remove(REQUERY_COLUMN);
Marc Blank805ff202012-04-05 13:45:09 -0700340 LogUtils.i(TAG, new Error(),
341 "IN resetCursor, remove requery column from %s", entry.getKey());
Marc Blank48eba7a2012-01-27 16:16:19 -0800342 } else {
343 // Keep the deleted count up-to-date; remove the cache entry
344 if (values.containsKey(DELETED_COLUMN)) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700345 mDeletedCount--;
Andy Huang489dd222012-03-29 14:52:55 -0700346 LogUtils.i(TAG, new Error(),
Marc Blank805ff202012-04-05 13:45:09 -0700347 "IN resetCursor, sDeletedCount decremented to: %d by %s",
Paul Westbrookbf232c32012-04-18 03:17:41 -0700348 mDeletedCount, entry.getKey());
Marc Blank48eba7a2012-01-27 16:16:19 -0800349 }
350 // Remove the entry
351 iter.remove();
352 }
353 }
354
355 // Swap cursor
Paul Westbrookbf232c32012-04-18 03:17:41 -0700356 if (mUnderlyingCursor != null) {
Marc Blankdd10bc82012-02-01 19:10:46 -0800357 close();
Marc Blank48eba7a2012-01-27 16:16:19 -0800358 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700359 mUnderlyingCursor = newCursor;
Marc Blank48eba7a2012-01-27 16:16:19 -0800360
361 mPosition = -1;
Paul Westbrookbf232c32012-04-18 03:17:41 -0700362 mUnderlyingCursor.moveToPosition(mPosition);
Marc Blank48eba7a2012-01-27 16:16:19 -0800363 if (!mCursorObserverRegistered) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700364 mUnderlyingCursor.registerContentObserver(mCursorObserver);
Marc Blank48eba7a2012-01-27 16:16:19 -0800365 mCursorObserverRegistered = true;
366 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700367 mRefreshRequired = false;
Marc Blank48eba7a2012-01-27 16:16:19 -0800368 }
Marc Blankc8a99422012-01-19 14:27:47 -0800369 }
370
371 /**
Marc Blankbf128eb2012-04-18 15:58:45 -0700372 * Add a listener for this cursor; we'll notify it when our data changes
373 */
374 public void addListener(ConversationListener listener) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700375 synchronized (mListeners) {
376 if (!mListeners.contains(listener)) {
377 mListeners.add(listener);
Marc Blankbf128eb2012-04-18 15:58:45 -0700378 } else {
379 LogUtils.i(TAG, "Ignoring duplicate add of listener");
380 }
381 }
382 }
383
384 /**
385 * Remove a listener for this cursor
386 */
387 public void removeListener(ConversationListener listener) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700388 synchronized(mListeners) {
389 mListeners.remove(listener);
Marc Blankbf128eb2012-04-18 15:58:45 -0700390 }
391 }
392
393 /**
Marc Blankc8a99422012-01-19 14:27:47 -0800394 * Generate a forwarding Uri to ConversationProvider from an original Uri. We do this by
395 * changing the authority to ours, but otherwise leaving the Uri intact.
396 * NOTE: This won't handle query parameters, so the functionality will need to be added if
397 * parameters are used in the future
398 * @param uri the uri
399 * @return a forwarding uri to ConversationProvider
400 */
401 private static String uriToCachingUriString (Uri uri) {
402 String provider = uri.getAuthority();
Paul Westbrook77177b12012-02-07 15:23:42 -0800403 return uri.getScheme() + "://" + ConversationProvider.AUTHORITY
404 + "/" + provider + uri.getPath();
Marc Blankc8a99422012-01-19 14:27:47 -0800405 }
406
407 /**
408 * Regenerate the original Uri from a forwarding (ConversationProvider) Uri
409 * NOTE: See note above for uriToCachingUri
410 * @param uri the forwarding Uri
411 * @return the original Uri
412 */
413 private static Uri uriFromCachingUri(Uri uri) {
Marc Blankd9787152012-03-15 09:43:12 -0700414 String authority = uri.getAuthority();
415 // Don't modify uri's that aren't ours
416 if (!authority.equals(ConversationProvider.AUTHORITY)) {
417 return uri;
418 }
Marc Blankc8a99422012-01-19 14:27:47 -0800419 List<String> path = uri.getPathSegments();
420 Uri.Builder builder = new Uri.Builder().scheme(uri.getScheme()).authority(path.get(0));
421 for (int i = 1; i < path.size(); i++) {
422 builder.appendPath(path.get(i));
423 }
424 return builder.build();
425 }
426
Marc Blanke1d1b072012-04-13 17:29:16 -0700427 private static String uriStringFromCachingUri(Uri uri) {
428 Uri underlyingUri = uriFromCachingUri(uri);
429 // Remember to decode the underlying Uri as it might be encoded (as w/ Gmail)
430 return Uri.decode(underlyingUri.toString());
431 }
432
Andy Huangdaa06ab2012-07-24 10:46:44 -0700433 public void setConversationColumn(Uri conversationUri, String columnName, Object value) {
434 final String uriStr = uriStringFromCachingUri(conversationUri);
Paul Westbrookbf232c32012-04-18 03:17:41 -0700435 synchronized (mCacheMapLock) {
Andy Huangdaa06ab2012-07-24 10:46:44 -0700436 cacheValue(uriStr, columnName, value);
Marc Blankbec51152012-03-22 19:27:34 -0700437 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700438 notifyDataChanged();
Marc Blank958bf4d2012-04-05 18:10:55 -0700439 }
440
441 /**
Marc Blankc8a99422012-01-19 14:27:47 -0800442 * Cache a column name/value pair for a given Uri
443 * @param uriString the Uri for which the column name/value pair applies
444 * @param columnName the column name
445 * @param value the value to be cached
446 */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700447 private void cacheValue(String uriString, String columnName, Object value) {
Andy Huang489dd222012-03-29 14:52:55 -0700448 // Calling this method off the UI thread will mess with ListView's reading of the cursor's
449 // count
450 if (offUiThread()) {
451 LogUtils.e(TAG, new Error(), "cacheValue incorrectly being called from non-UI thread");
452 }
453
Paul Westbrookbf232c32012-04-18 03:17:41 -0700454 synchronized (mCacheMapLock) {
Marc Blank958bf4d2012-04-05 18:10:55 -0700455 // Get the map for our uri
Paul Westbrookbf232c32012-04-18 03:17:41 -0700456 ContentValues map = mCacheMap.get(uriString);
Marc Blank958bf4d2012-04-05 18:10:55 -0700457 // Create one if necessary
458 if (map == null) {
459 map = new ContentValues();
Paul Westbrookbf232c32012-04-18 03:17:41 -0700460 mCacheMap.put(uriString, map);
Marc Blank958bf4d2012-04-05 18:10:55 -0700461 }
462 // If we're caching a deletion, add to our count
463 if (columnName == DELETED_COLUMN) {
464 final boolean state = (Boolean)value;
465 final boolean hasValue = map.get(columnName) != null;
466 if (state && !hasValue) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700467 mDeletedCount++;
Marc Blank958bf4d2012-04-05 18:10:55 -0700468 if (DEBUG) {
469 LogUtils.i(TAG, "Deleted %s, incremented deleted count=%d", uriString,
Paul Westbrookbf232c32012-04-18 03:17:41 -0700470 mDeletedCount);
Paul Westbrook9c87fe32012-03-28 22:14:42 -0700471 }
Marc Blank958bf4d2012-04-05 18:10:55 -0700472 } else if (!state && hasValue) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700473 mDeletedCount--;
Marc Blank958bf4d2012-04-05 18:10:55 -0700474 map.remove(columnName);
475 if (DEBUG) {
476 LogUtils.i(TAG, "Undeleted %s, decremented deleted count=%d", uriString,
Paul Westbrookbf232c32012-04-18 03:17:41 -0700477 mDeletedCount);
Paul Westbrook9c87fe32012-03-28 22:14:42 -0700478 }
Marc Blank958bf4d2012-04-05 18:10:55 -0700479 return;
480 } else if (!state) {
481 // Trying to undelete, but it's not deleted; just return
482 if (DEBUG) {
483 LogUtils.i(TAG, "Undeleted %s, IGNORING, deleted count=%d", uriString,
Paul Westbrookbf232c32012-04-18 03:17:41 -0700484 mDeletedCount);
Marc Blank958bf4d2012-04-05 18:10:55 -0700485 }
486 return;
Paul Westbrook9c87fe32012-03-28 22:14:42 -0700487 }
Marc Blank48eba7a2012-01-27 16:16:19 -0800488 }
Marc Blank958bf4d2012-04-05 18:10:55 -0700489 // ContentValues has no generic "put", so we must test. For now, the only classes
490 // of values implemented are Boolean/Integer/String, though others are trivially
491 // added
492 if (value instanceof Boolean) {
493 map.put(columnName, ((Boolean) value).booleanValue() ? 1 : 0);
494 } else if (value instanceof Integer) {
495 map.put(columnName, (Integer) value);
496 } else if (value instanceof String) {
497 map.put(columnName, (String) value);
498 } else {
499 final String cname = value.getClass().getName();
500 throw new IllegalArgumentException("Value class not compatible with cache: "
501 + cname);
502 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700503 if (mRefreshTask != null) {
Marc Blank958bf4d2012-04-05 18:10:55 -0700504 map.put(REQUERY_COLUMN, 1);
505 }
506 if (DEBUG && (columnName != DELETED_COLUMN)) {
507 LogUtils.i(TAG, "Caching value for %s: %s", uriString, columnName);
508 }
Marc Blankc8a99422012-01-19 14:27:47 -0800509 }
510 }
511
512 /**
513 * Get the cached value for the provided column; we special case -1 as the "deleted" column
514 * @param columnIndex the index of the column whose cached value we want to retrieve
515 * @return the cached value for this column, or null if there is none
516 */
517 private Object getCachedValue(int columnIndex) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700518 String uri = mUnderlyingCursor.getString(sUriColumnIndex);
Marc Blanke1d1b072012-04-13 17:29:16 -0700519 return getCachedValue(uri, columnIndex);
520 }
521
522 private Object getCachedValue(String uri, int columnIndex) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700523 ContentValues uriMap = mCacheMap.get(uri);
Marc Blankc8a99422012-01-19 14:27:47 -0800524 if (uriMap != null) {
525 String columnName;
526 if (columnIndex == DELETED_COLUMN_INDEX) {
527 columnName = DELETED_COLUMN;
528 } else {
529 columnName = mColumnNames[columnIndex];
530 }
531 return uriMap.get(columnName);
532 }
533 return null;
534 }
535
536 /**
Marc Blank97bca7b2012-01-24 11:17:00 -0800537 * When the underlying cursor changes, we want to alert the listener
Marc Blankc8a99422012-01-19 14:27:47 -0800538 */
539 private void underlyingChanged() {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700540 synchronized(mCacheMapLock) {
Marc Blanke1d1b072012-04-13 17:29:16 -0700541 if (mCursorObserverRegistered) {
542 try {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700543 mUnderlyingCursor.unregisterContentObserver(mCursorObserver);
Marc Blanke1d1b072012-04-13 17:29:16 -0700544 } catch (IllegalStateException e) {
545 // Maybe the cursor was GC'd?
546 }
547 mCursorObserverRegistered = false;
Marc Blankf9d87192012-02-16 10:50:41 -0800548 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700549 mRefreshRequired = true;
550 if (!mPaused) {
Marc Blanke1d1b072012-04-13 17:29:16 -0700551 notifyRefreshRequired();
552 }
Marc Blank97bca7b2012-01-24 11:17:00 -0800553 }
Marc Blanke1d1b072012-04-13 17:29:16 -0700554 }
555
556 /**
557 * Must be called on UI thread; notify listeners that a refresh is required
558 */
559 private void notifyRefreshRequired() {
Marc Blankb600a832012-02-16 09:20:18 -0800560 if (DEBUG) {
Paul Westbrooke2bde3a2012-07-31 17:35:43 -0700561 LogUtils.i(TAG, "[Notify %s: onRefreshRequired()]", mName);
Marc Blankb600a832012-02-16 09:20:18 -0800562 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700563 if (!mDeferSync) {
564 synchronized(mListeners) {
565 for (ConversationListener listener: mListeners) {
Marc Blankbf128eb2012-04-18 15:58:45 -0700566 listener.onRefreshRequired();
567 }
568 }
Marc Blankb600a832012-02-16 09:20:18 -0800569 }
Marc Blanke1d1b072012-04-13 17:29:16 -0700570 }
571
572 /**
573 * Must be called on UI thread; notify listeners that a new cursor is ready
574 */
575 private void notifyRefreshReady() {
576 if (DEBUG) {
Paul Westbrooke2bde3a2012-07-31 17:35:43 -0700577 LogUtils.i(TAG, "[Notify %s: onRefreshReady(), %d listeners]",
578 mName, mListeners.size());
Marc Blanke1d1b072012-04-13 17:29:16 -0700579 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700580 synchronized(mListeners) {
581 for (ConversationListener listener: mListeners) {
Marc Blankbf128eb2012-04-18 15:58:45 -0700582 listener.onRefreshReady();
583 }
584 }
Marc Blanke1d1b072012-04-13 17:29:16 -0700585 }
586
587 /**
588 * Must be called on UI thread; notify listeners that data has changed
589 */
590 private void notifyDataChanged() {
591 if (DEBUG) {
Paul Westbrooke2bde3a2012-07-31 17:35:43 -0700592 LogUtils.i(TAG, "[Notify %s: onDataSetChanged()]", mName);
Marc Blanke1d1b072012-04-13 17:29:16 -0700593 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700594 synchronized(mListeners) {
595 for (ConversationListener listener: mListeners) {
Marc Blankbf128eb2012-04-18 15:58:45 -0700596 listener.onDataSetChanged();
597 }
598 }
Marc Blankc8a99422012-01-19 14:27:47 -0800599 }
600
Marc Blank4015c182012-01-31 12:38:36 -0800601 /**
602 * Put the refreshed cursor in place (called by the UI)
603 */
Marc Blank4e25c942012-02-02 19:41:14 -0800604 public void sync() {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700605 if (mRequeryCursor == null) {
Marc Blank09b32382012-03-20 12:12:17 -0700606 // This can happen during an animated deletion, if the UI isn't keeping track, or
607 // if a new query intervened (i.e. user changed folders)
Marc Blank948985b2012-02-29 11:26:40 -0800608 if (DEBUG) {
Paul Westbrooke2bde3a2012-07-31 17:35:43 -0700609 LogUtils.i(TAG, "[sync() %s; no requery cursor]", mName);
Marc Blank948985b2012-02-29 11:26:40 -0800610 }
Marc Blank09b32382012-03-20 12:12:17 -0700611 return;
612 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700613 synchronized(mCacheMapLock) {
Marc Blank09b32382012-03-20 12:12:17 -0700614 if (DEBUG) {
Paul Westbrooke2bde3a2012-07-31 17:35:43 -0700615 LogUtils.i(TAG, "[sync() %s]", mName);
Marc Blank948985b2012-02-29 11:26:40 -0800616 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700617 resetCursor(mRequeryCursor);
618 mRequeryCursor = null;
619 mRefreshTask = null;
620 mRefreshReady = false;
Marc Blank3f1eb852012-02-03 15:38:01 -0800621 }
Paul Westbrook66150d72012-04-18 04:45:16 -0700622 notifyDataChanged();
Marc Blank4e25c942012-02-02 19:41:14 -0800623 }
624
625 public boolean isRefreshRequired() {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700626 return mRefreshRequired;
Marc Blank4e25c942012-02-02 19:41:14 -0800627 }
628
629 public boolean isRefreshReady() {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700630 return mRefreshReady;
Marc Blank4015c182012-01-31 12:38:36 -0800631 }
632
633 /**
634 * Cancel a refresh in progress
635 */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700636 public void cancelRefresh() {
Marc Blank3f1eb852012-02-03 15:38:01 -0800637 if (DEBUG) {
Paul Westbrooke2bde3a2012-07-31 17:35:43 -0700638 LogUtils.i(TAG, "[cancelRefresh() %s]", mName);
Marc Blank3f1eb852012-02-03 15:38:01 -0800639 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700640 synchronized(mCacheMapLock) {
641 if (mRefreshTask != null) {
642 mRefreshTask.cancel(true);
643 mRefreshTask = null;
Marc Blanke3d36792012-04-02 09:30:14 -0700644 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700645 mRefreshReady = false;
Marc Blank4015c182012-01-31 12:38:36 -0800646 // If we have the cursor, close it; otherwise, it will get closed when the query
Marc Blank09b32382012-03-20 12:12:17 -0700647 // finishes (it checks sRefreshInProgress)
Paul Westbrookbf232c32012-04-18 03:17:41 -0700648 if (mRequeryCursor != null) {
649 mRequeryCursor.close();
650 mRequeryCursor = null;
Marc Blank4015c182012-01-31 12:38:36 -0800651 }
652 }
653 }
654
655 /**
656 * Get a list of deletions from ConversationCursor to the refreshed cursor that hasn't yet
657 * been swapped into place; this allows the UI to animate these away if desired
658 * @return a list of positions deleted in ConversationCursor
659 */
Vikram Aggarwal7f602f72012-04-30 16:04:06 -0700660 public Collection<Conversation> getRefreshDeletions () {
Marc Blank435fbb52012-03-20 23:08:50 -0700661 return EMPTY_DELETION_LIST;
Marc Blank48eba7a2012-01-27 16:16:19 -0800662 }
663
Marc Blank97bca7b2012-01-24 11:17:00 -0800664 /**
Marc Blank48eba7a2012-01-27 16:16:19 -0800665 * When we get a requery from the UI, we'll do it, but also clear the cache. The listener is
666 * notified when the requery is complete
Marc Blank97bca7b2012-01-24 11:17:00 -0800667 * NOTE: This will have to change, of course, when we start using loaders...
668 */
Marc Blank48eba7a2012-01-27 16:16:19 -0800669 public boolean refresh() {
Marc Blank3f1eb852012-02-03 15:38:01 -0800670 if (DEBUG) {
Paul Westbrooke2bde3a2012-07-31 17:35:43 -0700671 LogUtils.i(TAG, "[refresh() %s]", mName);
Marc Blank3f1eb852012-02-03 15:38:01 -0800672 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700673 synchronized(mCacheMapLock) {
674 if (mRefreshTask != null) {
Marc Blanke3d36792012-04-02 09:30:14 -0700675 if (DEBUG) {
Paul Westbrooke2bde3a2012-07-31 17:35:43 -0700676 LogUtils.i(TAG, "[refresh() %s returning; already running %d]",
677 mName, mRefreshTask.hashCode());
Marc Blank4015c182012-01-31 12:38:36 -0800678 }
Marc Blanke3d36792012-04-02 09:30:14 -0700679 return false;
Marc Blank48eba7a2012-01-27 16:16:19 -0800680 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700681 mRefreshTask = new RefreshTask();
682 mRefreshTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
Marc Blanke3d36792012-04-02 09:30:14 -0700683 }
Marc Blankc8a99422012-01-19 14:27:47 -0800684 return true;
685 }
686
Paul Westbrookbf232c32012-04-18 03:17:41 -0700687 public void disable() {
688 close();
689 mCacheMap.clear();
690 mListeners.clear();
691 mUnderlyingCursor = null;
692 }
693
Marc Blankb600a832012-02-16 09:20:18 -0800694 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800695 public void close() {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700696 if (mUnderlyingCursor != null && !mUnderlyingCursor.isClosed()) {
Marc Blankf9d87192012-02-16 10:50:41 -0800697 // Unregister our observer on the underlying cursor and close as usual
698 if (mCursorObserverRegistered) {
699 try {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700700 mUnderlyingCursor.unregisterContentObserver(mCursorObserver);
Marc Blankf9d87192012-02-16 10:50:41 -0800701 } catch (IllegalStateException e) {
702 // Maybe the cursor got GC'd?
703 }
704 mCursorObserverRegistered = false;
705 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700706 mUnderlyingCursor.close();
Marc Blankdd10bc82012-02-01 19:10:46 -0800707 }
Marc Blankc8a99422012-01-19 14:27:47 -0800708 }
709
710 /**
711 * Move to the next not-deleted item in the conversation
712 */
Marc Blankb600a832012-02-16 09:20:18 -0800713 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800714 public boolean moveToNext() {
715 while (true) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700716 boolean ret = mUnderlyingCursor.moveToNext();
Marc Blank5d8b1fb2012-07-20 17:12:36 -0700717 if (!ret) {
718 // Make sure we're still in sync (mPosition++ should also work)
719 mPosition = mUnderlyingCursor.getPosition();
720 return false;
721 }
Marc Blankc8a99422012-01-19 14:27:47 -0800722 if (getCachedValue(DELETED_COLUMN_INDEX) instanceof Integer) continue;
723 mPosition++;
724 return true;
725 }
726 }
727
728 /**
729 * Move to the previous not-deleted item in the conversation
730 */
Marc Blankb600a832012-02-16 09:20:18 -0800731 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800732 public boolean moveToPrevious() {
733 while (true) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700734 boolean ret = mUnderlyingCursor.moveToPrevious();
Marc Blank5d8b1fb2012-07-20 17:12:36 -0700735 if (!ret) {
736 // Make sure we're before the first position
737 mPosition = -1;
738 return false;
739 }
Marc Blankec7c4da2012-03-22 20:28:55 -0700740 if (getCachedValue(DELETED_COLUMN_INDEX) instanceof Integer) continue;
Marc Blankc8a99422012-01-19 14:27:47 -0800741 mPosition--;
742 return true;
743 }
744 }
745
Marc Blankb600a832012-02-16 09:20:18 -0800746 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800747 public int getPosition() {
748 return mPosition;
749 }
750
751 /**
752 * The actual cursor's count must be decremented by the number we've deleted from the UI
753 */
Marc Blankb600a832012-02-16 09:20:18 -0800754 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800755 public int getCount() {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700756 if (mUnderlyingCursor == null) {
757 throw new IllegalStateException(
758 "getCount() on disabled cursor: " + mName + "(" + qUri + ")");
759 }
760 return mUnderlyingCursor.getCount() - mDeletedCount;
Marc Blankc8a99422012-01-19 14:27:47 -0800761 }
762
Marc Blankb600a832012-02-16 09:20:18 -0800763 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800764 public boolean moveToFirst() {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700765 if (mUnderlyingCursor == null) {
766 throw new IllegalStateException(
767 "moveToFirst() on disabled cursor: " + mName + "(" + qUri + ")");
768 }
769 mUnderlyingCursor.moveToPosition(-1);
Marc Blankc8a99422012-01-19 14:27:47 -0800770 mPosition = -1;
771 return moveToNext();
772 }
773
Marc Blankb600a832012-02-16 09:20:18 -0800774 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800775 public boolean moveToPosition(int pos) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700776 if (mUnderlyingCursor == null) {
777 throw new IllegalStateException(
778 "moveToPosition() on disabled cursor: " + mName + "(" + qUri + ")");
779 }
Marc Blank7ffbaaa2012-07-30 16:19:19 -0700780 // Handle the "move to first" case before anything else; moveToPosition(0) in an empty
781 // SQLiteCursor moves the position to 0 when returning false, which we will mirror.
782 // But we don't want to return true on a subsequent "move to first", which we would if we
783 // check pos vs mPosition first
784 if (pos == 0) {
785 return moveToFirst();
786 } else if (pos == mPosition) {
787 return true;
788 } else if (pos > mPosition) {
Marc Blankc8a99422012-01-19 14:27:47 -0800789 while (pos > mPosition) {
790 if (!moveToNext()) {
791 return false;
792 }
793 }
794 return true;
Marc Blankc8a99422012-01-19 14:27:47 -0800795 } else {
796 while (pos < mPosition) {
797 if (!moveToPrevious()) {
798 return false;
799 }
800 }
801 return true;
802 }
803 }
804
Marc Blank93b3a152012-04-11 15:53:19 -0700805 /**
806 * Make sure mPosition is correct after locally deleting/undeleting items
807 */
808 private void recalibratePosition() {
809 int pos = mPosition;
810 moveToFirst();
811 moveToPosition(pos);
812 }
813
Marc Blankb600a832012-02-16 09:20:18 -0800814 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800815 public boolean moveToLast() {
816 throw new UnsupportedOperationException("moveToLast unsupported!");
817 }
818
Marc Blankb600a832012-02-16 09:20:18 -0800819 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800820 public boolean move(int offset) {
821 throw new UnsupportedOperationException("move unsupported!");
822 }
823
824 /**
825 * We need to override all of the getters to make sure they look at cached values before using
826 * the values in the underlying cursor
827 */
828 @Override
829 public double getDouble(int columnIndex) {
830 Object obj = getCachedValue(columnIndex);
831 if (obj != null) return (Double)obj;
Paul Westbrookbf232c32012-04-18 03:17:41 -0700832 return mUnderlyingCursor.getDouble(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800833 }
834
835 @Override
836 public float getFloat(int columnIndex) {
837 Object obj = getCachedValue(columnIndex);
838 if (obj != null) return (Float)obj;
Paul Westbrookbf232c32012-04-18 03:17:41 -0700839 return mUnderlyingCursor.getFloat(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800840 }
841
842 @Override
843 public int getInt(int columnIndex) {
844 Object obj = getCachedValue(columnIndex);
845 if (obj != null) return (Integer)obj;
Paul Westbrookbf232c32012-04-18 03:17:41 -0700846 return mUnderlyingCursor.getInt(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800847 }
848
849 @Override
850 public long getLong(int columnIndex) {
Marc Blanke1d1b072012-04-13 17:29:16 -0700851 Object obj = getCachedValue(columnIndex);
852 if (obj != null) return (Long)obj;
Paul Westbrookbf232c32012-04-18 03:17:41 -0700853 return mUnderlyingCursor.getLong(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800854 }
855
856 @Override
857 public short getShort(int columnIndex) {
858 Object obj = getCachedValue(columnIndex);
859 if (obj != null) return (Short)obj;
Paul Westbrookbf232c32012-04-18 03:17:41 -0700860 return mUnderlyingCursor.getShort(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800861 }
862
863 @Override
864 public String getString(int columnIndex) {
865 // If we're asking for the Uri for the conversation list, we return a forwarding URI
866 // so that we can intercept update/delete and handle it ourselves
Marc Blank97bca7b2012-01-24 11:17:00 -0800867 if (columnIndex == sUriColumnIndex) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700868 Uri uri = Uri.parse(mUnderlyingCursor.getString(columnIndex));
Marc Blankc8a99422012-01-19 14:27:47 -0800869 return uriToCachingUriString(uri);
870 }
871 Object obj = getCachedValue(columnIndex);
872 if (obj != null) return (String)obj;
Paul Westbrookbf232c32012-04-18 03:17:41 -0700873 return mUnderlyingCursor.getString(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800874 }
875
876 @Override
877 public byte[] getBlob(int columnIndex) {
878 Object obj = getCachedValue(columnIndex);
879 if (obj != null) return (byte[])obj;
Paul Westbrookbf232c32012-04-18 03:17:41 -0700880 return mUnderlyingCursor.getBlob(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800881 }
882
883 /**
884 * Observer of changes to underlying data
885 */
886 private class CursorObserver extends ContentObserver {
887 public CursorObserver() {
Mindy Pereira609480e2012-02-16 13:54:18 -0800888 super(null);
Marc Blankc8a99422012-01-19 14:27:47 -0800889 }
890
891 @Override
892 public void onChange(boolean selfChange) {
893 // If we're here, then something outside of the UI has changed the data, and we
Marc Blank48eba7a2012-01-27 16:16:19 -0800894 // must query the underlying provider for that data
Marc Blankc8a99422012-01-19 14:27:47 -0800895 ConversationCursor.this.underlyingChanged();
896 }
897 }
898
899 /**
900 * ConversationProvider is the ContentProvider for our forwarding Uri's; it passes queries
901 * and inserts directly, and caches updates/deletes before passing them through. The caching
902 * will cause a redraw of the list with updated values.
903 */
Paul Westbrook77177b12012-02-07 15:23:42 -0800904 public abstract static class ConversationProvider extends ContentProvider {
905 public static String AUTHORITY;
906
907 /**
Vikram Aggarwal6a621462012-04-02 14:42:40 -0700908 * Allows the implementing provider to specify the authority that should be used.
Paul Westbrook77177b12012-02-07 15:23:42 -0800909 */
910 protected abstract String getAuthority();
Marc Blank8d69d4e2012-01-25 12:04:28 -0800911
Marc Blankc8a99422012-01-19 14:27:47 -0800912 @Override
913 public boolean onCreate() {
Marc Blank8d69d4e2012-01-25 12:04:28 -0800914 sProvider = this;
Paul Westbrook77177b12012-02-07 15:23:42 -0800915 AUTHORITY = getAuthority();
Marc Blank8d69d4e2012-01-25 12:04:28 -0800916 return true;
Marc Blankc8a99422012-01-19 14:27:47 -0800917 }
918
919 @Override
920 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
921 String sortOrder) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700922 return sResolver.query(
Marc Blankc8a99422012-01-19 14:27:47 -0800923 uriFromCachingUri(uri), projection, selection, selectionArgs, sortOrder);
924 }
925
926 @Override
Marc Blankf892f0a2012-01-30 13:04:10 -0800927 public Uri insert(Uri uri, ContentValues values) {
928 insertLocal(uri, values);
929 return ProviderExecute.opInsert(uri, values);
930 }
931
932 @Override
933 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700934 throw new IllegalStateException("Unexpected call to ConversationProvider.delete");
935// updateLocal(uri, values);
936 // return ProviderExecute.opUpdate(uri, values);
Marc Blankf892f0a2012-01-30 13:04:10 -0800937 }
938
939 @Override
940 public int delete(Uri uri, String selection, String[] selectionArgs) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700941 throw new IllegalStateException("Unexpected call to ConversationProvider.delete");
942 //deleteLocal(uri);
943 // return ProviderExecute.opDelete(uri);
Marc Blankf892f0a2012-01-30 13:04:10 -0800944 }
945
946 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800947 public String getType(Uri uri) {
948 return null;
949 }
950
951 /**
952 * Quick and dirty class that executes underlying provider CRUD operations on a background
953 * thread.
954 */
955 static class ProviderExecute implements Runnable {
956 static final int DELETE = 0;
957 static final int INSERT = 1;
958 static final int UPDATE = 2;
959
960 final int mCode;
961 final Uri mUri;
962 final ContentValues mValues; //HEHEH
963
964 ProviderExecute(int code, Uri uri, ContentValues values) {
965 mCode = code;
966 mUri = uriFromCachingUri(uri);
967 mValues = values;
968 }
969
970 ProviderExecute(int code, Uri uri) {
971 this(code, uri, null);
972 }
973
Marc Blank8d69d4e2012-01-25 12:04:28 -0800974 static Uri opInsert(Uri uri, ContentValues values) {
975 ProviderExecute e = new ProviderExecute(INSERT, uri, values);
976 if (offUiThread()) return (Uri)e.go();
977 new Thread(e).start();
978 return null;
Marc Blankc8a99422012-01-19 14:27:47 -0800979 }
980
Marc Blank8d69d4e2012-01-25 12:04:28 -0800981 static int opDelete(Uri uri) {
982 ProviderExecute e = new ProviderExecute(DELETE, uri);
983 if (offUiThread()) return (Integer)e.go();
984 new Thread(new ProviderExecute(DELETE, uri)).start();
985 return 0;
986 }
987
988 static int opUpdate(Uri uri, ContentValues values) {
989 ProviderExecute e = new ProviderExecute(UPDATE, uri, values);
990 if (offUiThread()) return (Integer)e.go();
991 new Thread(e).start();
992 return 0;
Marc Blankc8a99422012-01-19 14:27:47 -0800993 }
994
995 @Override
996 public void run() {
Marc Blank8d69d4e2012-01-25 12:04:28 -0800997 go();
998 }
999
1000 public Object go() {
Marc Blankc8a99422012-01-19 14:27:47 -08001001 switch(mCode) {
1002 case DELETE:
Paul Westbrookbf232c32012-04-18 03:17:41 -07001003 return sResolver.delete(mUri, null, null);
Marc Blankc8a99422012-01-19 14:27:47 -08001004 case INSERT:
Paul Westbrookbf232c32012-04-18 03:17:41 -07001005 return sResolver.insert(mUri, mValues);
Marc Blankc8a99422012-01-19 14:27:47 -08001006 case UPDATE:
Paul Westbrookbf232c32012-04-18 03:17:41 -07001007 return sResolver.update(mUri, mValues, null, null);
Marc Blank8d69d4e2012-01-25 12:04:28 -08001008 default:
1009 return null;
Marc Blankc8a99422012-01-19 14:27:47 -08001010 }
1011 }
1012 }
1013
Marc Blank8d69d4e2012-01-25 12:04:28 -08001014 private void insertLocal(Uri uri, ContentValues values) {
1015 // Placeholder for now; there's no local insert
1016 }
1017
Marc Blank2596f002012-03-22 10:26:26 -07001018 private int mUndoSequence = 0;
1019 private ArrayList<Uri> mUndoDeleteUris = new ArrayList<Uri>();
1020
Marc Blanke1d1b072012-04-13 17:29:16 -07001021 void addToUndoSequence(Uri uri) {
Marc Blank2596f002012-03-22 10:26:26 -07001022 if (sSequence != mUndoSequence) {
1023 mUndoSequence = sSequence;
1024 mUndoDeleteUris.clear();
1025 }
1026 mUndoDeleteUris.add(uri);
1027 }
1028
1029 @VisibleForTesting
Paul Westbrookbf232c32012-04-18 03:17:41 -07001030 void deleteLocal(Uri uri, ConversationCursor conversationCursor) {
Marc Blanke1d1b072012-04-13 17:29:16 -07001031 String uriString = uriStringFromCachingUri(uri);
Paul Westbrookbf232c32012-04-18 03:17:41 -07001032 conversationCursor.cacheValue(uriString, DELETED_COLUMN, true);
Marc Blanke1d1b072012-04-13 17:29:16 -07001033 addToUndoSequence(uri);
1034 }
1035
1036 @VisibleForTesting
Paul Westbrookbf232c32012-04-18 03:17:41 -07001037 void undeleteLocal(Uri uri, ConversationCursor conversationCursor) {
Marc Blanke1d1b072012-04-13 17:29:16 -07001038 String uriString = uriStringFromCachingUri(uri);
Paul Westbrookbf232c32012-04-18 03:17:41 -07001039 conversationCursor.cacheValue(uriString, DELETED_COLUMN, false);
Marc Blank2596f002012-03-22 10:26:26 -07001040 }
1041
Paul Westbrookbf232c32012-04-18 03:17:41 -07001042 void setMostlyDead(Conversation conv, ConversationCursor conversationCursor) {
Marc Blanke1d1b072012-04-13 17:29:16 -07001043 Uri uri = conv.uri;
1044 String uriString = uriStringFromCachingUri(uri);
Paul Westbrookbf232c32012-04-18 03:17:41 -07001045 conversationCursor.setMostlyDead(uriString, conv);
Marc Blanke1d1b072012-04-13 17:29:16 -07001046 addToUndoSequence(uri);
Marc Blanke1d1b072012-04-13 17:29:16 -07001047 }
1048
Paul Westbrookbf232c32012-04-18 03:17:41 -07001049 void commitMostlyDead(Conversation conv, ConversationCursor conversationCursor) {
1050 conversationCursor.commitMostlyDead(conv);
Marc Blanke1d1b072012-04-13 17:29:16 -07001051 }
1052
Paul Westbrookbf232c32012-04-18 03:17:41 -07001053 boolean clearMostlyDead(Uri uri, ConversationCursor conversationCursor) {
Marc Blanke1d1b072012-04-13 17:29:16 -07001054 String uriString = uriStringFromCachingUri(uri);
Paul Westbrookbf232c32012-04-18 03:17:41 -07001055 return conversationCursor.clearMostlyDead(uriString);
Marc Blanke1d1b072012-04-13 17:29:16 -07001056 }
1057
Paul Westbrookbf232c32012-04-18 03:17:41 -07001058 public void undo(ConversationCursor conversationCursor) {
Marc Blank2596f002012-03-22 10:26:26 -07001059 if (sSequence == mUndoSequence) {
1060 for (Uri uri: mUndoDeleteUris) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001061 if (!clearMostlyDead(uri, conversationCursor)) {
1062 undeleteLocal(uri, conversationCursor);
Marc Blanke1d1b072012-04-13 17:29:16 -07001063 }
Marc Blank2596f002012-03-22 10:26:26 -07001064 }
1065 mUndoSequence = 0;
Paul Westbrookbf232c32012-04-18 03:17:41 -07001066 conversationCursor.recalibratePosition();
Marc Blank2596f002012-03-22 10:26:26 -07001067 }
Marc Blankc8a99422012-01-19 14:27:47 -08001068 }
1069
Marc Blank248b1b42012-02-07 13:43:02 -08001070 @VisibleForTesting
Paul Westbrookbf232c32012-04-18 03:17:41 -07001071 void updateLocal(Uri uri, ContentValues values, ConversationCursor conversationCursor) {
Mindy Pereiraf98b3182012-02-22 11:07:13 -08001072 if (values == null) {
1073 return;
1074 }
Marc Blanke1d1b072012-04-13 17:29:16 -07001075 String uriString = uriStringFromCachingUri(uri);
Marc Blankc8a99422012-01-19 14:27:47 -08001076 for (String columnName: values.keySet()) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001077 conversationCursor.cacheValue(uriString, columnName, values.get(columnName));
Marc Blankc8a99422012-01-19 14:27:47 -08001078 }
Marc Blank8d69d4e2012-01-25 12:04:28 -08001079 }
1080
Paul Westbrookbf232c32012-04-18 03:17:41 -07001081 public int apply(ArrayList<ConversationOperation> ops,
1082 ConversationCursor conversationCursor) {
Marc Blank8d69d4e2012-01-25 12:04:28 -08001083 final HashMap<String, ArrayList<ContentProviderOperation>> batchMap =
1084 new HashMap<String, ArrayList<ContentProviderOperation>>();
Marc Blankb31ab5a2012-02-01 12:28:29 -08001085 // Increment sequence count
1086 sSequence++;
Marc Blank93b3a152012-04-11 15:53:19 -07001087
Marc Blankf892f0a2012-01-30 13:04:10 -08001088 // Execute locally and build CPO's for underlying provider
Marc Blank93b3a152012-04-11 15:53:19 -07001089 boolean recalibrateRequired = false;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001090 for (ConversationOperation op: ops) {
1091 Uri underlyingUri = uriFromCachingUri(op.mUri);
1092 String authority = underlyingUri.getAuthority();
1093 ArrayList<ContentProviderOperation> authOps = batchMap.get(authority);
1094 if (authOps == null) {
1095 authOps = new ArrayList<ContentProviderOperation>();
1096 batchMap.put(authority, authOps);
1097 }
Marc Blanke1d1b072012-04-13 17:29:16 -07001098 ContentProviderOperation cpo = op.execute(underlyingUri);
1099 if (cpo != null) {
1100 authOps.add(cpo);
1101 }
Marc Blank93b3a152012-04-11 15:53:19 -07001102 // Keep track of whether our operations require recalibrating the cursor position
1103 if (op.mRecalibrateRequired) {
1104 recalibrateRequired = true;
1105 }
Marc Blankf892f0a2012-01-30 13:04:10 -08001106 }
1107
Marc Blank93b3a152012-04-11 15:53:19 -07001108 // Recalibrate cursor position if required
1109 if (recalibrateRequired) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001110 conversationCursor.recalibratePosition();
Marc Blank93b3a152012-04-11 15:53:19 -07001111 }
Marc Blank958bf4d2012-04-05 18:10:55 -07001112
Marc Blankfde56a72012-07-19 17:18:21 -07001113 // Notify listeners that data has changed
1114 conversationCursor.notifyDataChanged();
1115
Marc Blankf892f0a2012-01-30 13:04:10 -08001116 // Send changes to underlying provider
Marc Blank8d69d4e2012-01-25 12:04:28 -08001117 for (String authority: batchMap.keySet()) {
1118 try {
1119 if (offUiThread()) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001120 sResolver.applyBatch(authority, batchMap.get(authority));
Marc Blank8d69d4e2012-01-25 12:04:28 -08001121 } else {
1122 final String auth = authority;
1123 new Thread(new Runnable() {
1124 @Override
1125 public void run() {
1126 try {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001127 sResolver.applyBatch(auth, batchMap.get(auth));
Marc Blank8d69d4e2012-01-25 12:04:28 -08001128 } catch (RemoteException e) {
1129 } catch (OperationApplicationException e) {
1130 }
1131 }
1132 }).start();
Marc Blank8d69d4e2012-01-25 12:04:28 -08001133 }
1134 } catch (RemoteException e) {
1135 } catch (OperationApplicationException e) {
1136 }
1137 }
Marc Blank1b9efd92012-02-01 14:27:55 -08001138 return sSequence;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001139 }
1140 }
1141
Paul Westbrookbf232c32012-04-18 03:17:41 -07001142 void setMostlyDead(String uriString, Conversation conv) {
Marc Blank3c9bcef2012-04-29 14:39:54 -07001143 LogUtils.i(TAG, "[Mostly dead, deferring: %s] ", uriString);
Paul Westbrookbf232c32012-04-18 03:17:41 -07001144 cacheValue(uriString,
1145 UIProvider.ConversationColumns.FLAGS, Conversation.FLAG_MOSTLY_DEAD);
1146 conv.convFlags |= Conversation.FLAG_MOSTLY_DEAD;
1147 sMostlyDead.add(conv);
1148 mDeferSync = true;
1149 }
1150
1151 void commitMostlyDead(Conversation conv) {
1152 conv.convFlags &= ~Conversation.FLAG_MOSTLY_DEAD;
1153 sMostlyDead.remove(conv);
1154 LogUtils.i(TAG, "[All dead: %s]", conv.uri);
1155 if (sMostlyDead.isEmpty()) {
1156 mDeferSync = false;
1157 checkNotifyUI();
1158 }
1159 }
1160
1161 boolean clearMostlyDead(String uriString) {
1162 Object val = getCachedValue(uriString,
1163 UIProvider.CONVERSATION_FLAGS_COLUMN);
1164 if (val != null) {
1165 int flags = ((Integer)val).intValue();
1166 if ((flags & Conversation.FLAG_MOSTLY_DEAD) != 0) {
1167 cacheValue(uriString, UIProvider.ConversationColumns.FLAGS,
1168 flags &= ~Conversation.FLAG_MOSTLY_DEAD);
1169 return true;
1170 }
1171 }
1172 return false;
1173 }
1174
1175
1176
1177
Marc Blank8d69d4e2012-01-25 12:04:28 -08001178 /**
1179 * ConversationOperation is the encapsulation of a ContentProvider operation to be performed
1180 * atomically as part of a "batch" operation.
1181 */
Paul Westbrookbf232c32012-04-18 03:17:41 -07001182 public class ConversationOperation {
Marc Blanke1d1b072012-04-13 17:29:16 -07001183 private static final int MOSTLY = 0x80;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001184 public static final int DELETE = 0;
1185 public static final int INSERT = 1;
1186 public static final int UPDATE = 2;
Mindy Pereiraf98b3182012-02-22 11:07:13 -08001187 public static final int ARCHIVE = 3;
Mindy Pereira830c00f2012-02-22 11:43:49 -08001188 public static final int MUTE = 4;
1189 public static final int REPORT_SPAM = 5;
Paul Westbrook77eee622012-07-10 13:41:57 -07001190 public static final int REPORT_NOT_SPAM = 6;
Paul Westbrook76b20622012-07-12 11:45:43 -07001191 public static final int REPORT_PHISHING = 7;
Marc Blanke1d1b072012-04-13 17:29:16 -07001192 public static final int MOSTLY_ARCHIVE = MOSTLY | ARCHIVE;
1193 public static final int MOSTLY_DELETE = MOSTLY | DELETE;
Mindy Pereira06642fa2012-07-12 16:23:27 -07001194 public static final int MOSTLY_DESTRUCTIVE_UPDATE = MOSTLY | UPDATE;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001195
1196 private final int mType;
1197 private final Uri mUri;
Marc Blanke1d1b072012-04-13 17:29:16 -07001198 private final Conversation mConversation;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001199 private final ContentValues mValues;
Marc Blankce538182012-02-03 13:04:27 -08001200 // True if an updated item should be removed locally (from ConversationCursor)
Mindy Pereira30fd47b2012-03-09 09:24:00 -08001201 // This would be the case for a folder change in which the conversation is no longer
Marc Blankce538182012-02-03 13:04:27 -08001202 // in the folder represented by the ConversationCursor
1203 private final boolean mLocalDeleteOnUpdate;
Marc Blank93b3a152012-04-11 15:53:19 -07001204 // After execution, this indicates whether or not the operation requires recalibration of
1205 // the current cursor position (i.e. it removed or added items locally)
1206 private boolean mRecalibrateRequired = true;
Marc Blanke1d1b072012-04-13 17:29:16 -07001207 // Whether this item is already mostly dead
1208 private final boolean mMostlyDead;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001209
Marc Blankf892f0a2012-01-30 13:04:10 -08001210 public ConversationOperation(int type, Conversation conv) {
Andy Huang2c4e6dc2012-07-25 18:02:59 -07001211 this(type, conv, null);
Marc Blank8d69d4e2012-01-25 12:04:28 -08001212 }
1213
Andy Huang2c4e6dc2012-07-25 18:02:59 -07001214 public ConversationOperation(int type, Conversation conv, ContentValues values) {
Marc Blank8d69d4e2012-01-25 12:04:28 -08001215 mType = type;
Marc Blankc43bc0a2012-02-02 11:25:18 -08001216 mUri = conv.uri;
Marc Blanke1d1b072012-04-13 17:29:16 -07001217 mConversation = conv;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001218 mValues = values;
Marc Blankce538182012-02-03 13:04:27 -08001219 mLocalDeleteOnUpdate = conv.localDeleteOnUpdate;
Marc Blanke1d1b072012-04-13 17:29:16 -07001220 mMostlyDead = conv.isMostlyDead();
Marc Blank8d69d4e2012-01-25 12:04:28 -08001221 }
1222
1223 private ContentProviderOperation execute(Uri underlyingUri) {
Marc Blankb31ab5a2012-02-01 12:28:29 -08001224 Uri uri = underlyingUri.buildUpon()
Marc Blankdd10bc82012-02-01 19:10:46 -08001225 .appendQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER,
1226 Integer.toString(sSequence))
Marc Blankb31ab5a2012-02-01 12:28:29 -08001227 .build();
Marc Blanke1d1b072012-04-13 17:29:16 -07001228 ContentProviderOperation op = null;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001229 switch(mType) {
Marc Blank8d69d4e2012-01-25 12:04:28 -08001230 case UPDATE:
Marc Blankce538182012-02-03 13:04:27 -08001231 if (mLocalDeleteOnUpdate) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001232 sProvider.deleteLocal(mUri, ConversationCursor.this);
Marc Blankce538182012-02-03 13:04:27 -08001233 } else {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001234 sProvider.updateLocal(mUri, mValues, ConversationCursor.this);
Marc Blank93b3a152012-04-11 15:53:19 -07001235 mRecalibrateRequired = false;
Marc Blankce538182012-02-03 13:04:27 -08001236 }
Mindy Pereira06642fa2012-07-12 16:23:27 -07001237 if (!mMostlyDead) {
1238 op = ContentProviderOperation.newUpdate(uri)
1239 .withValues(mValues)
1240 .build();
1241 } else {
1242 sProvider.commitMostlyDead(mConversation, ConversationCursor.this);
1243 }
1244 break;
1245 case MOSTLY_DESTRUCTIVE_UPDATE:
1246 sProvider.setMostlyDead(mConversation, ConversationCursor.this);
1247 op = ContentProviderOperation.newUpdate(uri).withValues(mValues).build();
Andy Huang397621b2012-03-14 20:52:39 -07001248 break;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001249 case INSERT:
1250 sProvider.insertLocal(mUri, mValues);
Andy Huang397621b2012-03-14 20:52:39 -07001251 op = ContentProviderOperation.newInsert(uri)
Marc Blank8d69d4e2012-01-25 12:04:28 -08001252 .withValues(mValues).build();
Andy Huang397621b2012-03-14 20:52:39 -07001253 break;
Marc Blanke1d1b072012-04-13 17:29:16 -07001254 // Destructive actions below!
1255 // "Mostly" operations are reflected globally, but not locally, except to set
1256 // FLAG_MOSTLY_DEAD in the conversation itself
1257 case DELETE:
Paul Westbrookbf232c32012-04-18 03:17:41 -07001258 sProvider.deleteLocal(mUri, ConversationCursor.this);
Marc Blanke1d1b072012-04-13 17:29:16 -07001259 if (!mMostlyDead) {
1260 op = ContentProviderOperation.newDelete(uri).build();
1261 } else {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001262 sProvider.commitMostlyDead(mConversation, ConversationCursor.this);
Marc Blanke1d1b072012-04-13 17:29:16 -07001263 }
1264 break;
1265 case MOSTLY_DELETE:
Paul Westbrookbf232c32012-04-18 03:17:41 -07001266 sProvider.setMostlyDead(mConversation,ConversationCursor.this);
Marc Blanke1d1b072012-04-13 17:29:16 -07001267 op = ContentProviderOperation.newDelete(uri).build();
1268 break;
Mindy Pereiraf98b3182012-02-22 11:07:13 -08001269 case ARCHIVE:
Paul Westbrookbf232c32012-04-18 03:17:41 -07001270 sProvider.deleteLocal(mUri, ConversationCursor.this);
Marc Blanke1d1b072012-04-13 17:29:16 -07001271 if (!mMostlyDead) {
1272 // Create an update operation that represents archive
1273 op = ContentProviderOperation.newUpdate(uri).withValue(
1274 ConversationOperations.OPERATION_KEY,
1275 ConversationOperations.ARCHIVE)
1276 .build();
1277 } else {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001278 sProvider.commitMostlyDead(mConversation, ConversationCursor.this);
Marc Blanke1d1b072012-04-13 17:29:16 -07001279 }
1280 break;
1281 case MOSTLY_ARCHIVE:
Paul Westbrookbf232c32012-04-18 03:17:41 -07001282 sProvider.setMostlyDead(mConversation, ConversationCursor.this);
Paul Westbrook334e64a2012-02-23 13:26:35 -08001283 // Create an update operation that represents archive
Andy Huang397621b2012-03-14 20:52:39 -07001284 op = ContentProviderOperation.newUpdate(uri).withValue(
Paul Westbrook334e64a2012-02-23 13:26:35 -08001285 ConversationOperations.OPERATION_KEY, ConversationOperations.ARCHIVE)
1286 .build();
Andy Huang397621b2012-03-14 20:52:39 -07001287 break;
Mindy Pereira830c00f2012-02-22 11:43:49 -08001288 case MUTE:
Paul Westbrook334e64a2012-02-23 13:26:35 -08001289 if (mLocalDeleteOnUpdate) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001290 sProvider.deleteLocal(mUri, ConversationCursor.this);
Paul Westbrook334e64a2012-02-23 13:26:35 -08001291 }
1292
1293 // Create an update operation that represents mute
Andy Huang397621b2012-03-14 20:52:39 -07001294 op = ContentProviderOperation.newUpdate(uri).withValue(
Paul Westbrook334e64a2012-02-23 13:26:35 -08001295 ConversationOperations.OPERATION_KEY, ConversationOperations.MUTE)
1296 .build();
Andy Huang397621b2012-03-14 20:52:39 -07001297 break;
Mindy Pereira830c00f2012-02-22 11:43:49 -08001298 case REPORT_SPAM:
Paul Westbrook77eee622012-07-10 13:41:57 -07001299 case REPORT_NOT_SPAM:
Paul Westbrookbf232c32012-04-18 03:17:41 -07001300 sProvider.deleteLocal(mUri, ConversationCursor.this);
Paul Westbrook334e64a2012-02-23 13:26:35 -08001301
Paul Westbrook77eee622012-07-10 13:41:57 -07001302 final String operation = mType == REPORT_SPAM ?
1303 ConversationOperations.REPORT_SPAM :
1304 ConversationOperations.REPORT_NOT_SPAM;
1305
Paul Westbrook334e64a2012-02-23 13:26:35 -08001306 // Create an update operation that represents report spam
Andy Huang397621b2012-03-14 20:52:39 -07001307 op = ContentProviderOperation.newUpdate(uri).withValue(
Paul Westbrook77eee622012-07-10 13:41:57 -07001308 ConversationOperations.OPERATION_KEY, operation).build();
Andy Huang397621b2012-03-14 20:52:39 -07001309 break;
Paul Westbrook76b20622012-07-12 11:45:43 -07001310 case REPORT_PHISHING:
1311 sProvider.deleteLocal(mUri, ConversationCursor.this);
1312
1313 // Create an update operation that represents report spam
1314 op = ContentProviderOperation.newUpdate(uri).withValue(
1315 ConversationOperations.OPERATION_KEY,
1316 ConversationOperations.REPORT_PHISHING).build();
1317 break;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001318 default:
1319 throw new UnsupportedOperationException(
1320 "No such ConversationOperation type: " + mType);
1321 }
Andy Huang397621b2012-03-14 20:52:39 -07001322
Andy Huang397621b2012-03-14 20:52:39 -07001323 return op;
Marc Blankc8a99422012-01-19 14:27:47 -08001324 }
1325 }
Marc Blank97bca7b2012-01-24 11:17:00 -08001326
1327 /**
1328 * For now, a single listener can be associated with the cursor, and for now we'll just
1329 * notify on deletions
1330 */
1331 public interface ConversationListener {
Marc Blankbec51152012-03-22 19:27:34 -07001332 /**
1333 * Data in the underlying provider has changed; a refresh is required to sync up
1334 */
Marc Blank48eba7a2012-01-27 16:16:19 -08001335 public void onRefreshRequired();
Marc Blankbec51152012-03-22 19:27:34 -07001336 /**
1337 * We've completed a requested refresh of the underlying cursor
1338 */
Marc Blank48eba7a2012-01-27 16:16:19 -08001339 public void onRefreshReady();
Marc Blankbec51152012-03-22 19:27:34 -07001340 /**
1341 * The data underlying the cursor has changed; the UI should redraw the list
1342 */
1343 public void onDataSetChanged();
Marc Blank48eba7a2012-01-27 16:16:19 -08001344 }
1345
1346 @Override
1347 public boolean isFirst() {
1348 throw new UnsupportedOperationException();
1349 }
1350
1351 @Override
1352 public boolean isLast() {
1353 throw new UnsupportedOperationException();
1354 }
1355
1356 @Override
1357 public boolean isBeforeFirst() {
1358 throw new UnsupportedOperationException();
1359 }
1360
1361 @Override
1362 public boolean isAfterLast() {
1363 throw new UnsupportedOperationException();
1364 }
1365
1366 @Override
1367 public int getColumnIndex(String columnName) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001368 return mUnderlyingCursor.getColumnIndex(columnName);
Marc Blank48eba7a2012-01-27 16:16:19 -08001369 }
1370
1371 @Override
1372 public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001373 return mUnderlyingCursor.getColumnIndexOrThrow(columnName);
Marc Blank48eba7a2012-01-27 16:16:19 -08001374 }
1375
1376 @Override
1377 public String getColumnName(int columnIndex) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001378 return mUnderlyingCursor.getColumnName(columnIndex);
Marc Blank48eba7a2012-01-27 16:16:19 -08001379 }
1380
1381 @Override
1382 public String[] getColumnNames() {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001383 return mUnderlyingCursor.getColumnNames();
Marc Blank48eba7a2012-01-27 16:16:19 -08001384 }
1385
1386 @Override
1387 public int getColumnCount() {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001388 return mUnderlyingCursor.getColumnCount();
Marc Blank48eba7a2012-01-27 16:16:19 -08001389 }
1390
1391 @Override
1392 public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
1393 throw new UnsupportedOperationException();
1394 }
1395
1396 @Override
1397 public int getType(int columnIndex) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001398 return mUnderlyingCursor.getType(columnIndex);
Marc Blank48eba7a2012-01-27 16:16:19 -08001399 }
1400
1401 @Override
1402 public boolean isNull(int columnIndex) {
1403 throw new UnsupportedOperationException();
1404 }
1405
1406 @Override
1407 public void deactivate() {
1408 throw new UnsupportedOperationException();
1409 }
1410
1411 @Override
1412 public boolean isClosed() {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001413 return mUnderlyingCursor == null || mUnderlyingCursor.isClosed();
Marc Blank48eba7a2012-01-27 16:16:19 -08001414 }
1415
1416 @Override
1417 public void registerContentObserver(ContentObserver observer) {
Andy Huang397621b2012-03-14 20:52:39 -07001418 // Nope. We never notify of underlying changes on this channel, since the cursor watches
1419 // internally and offers onRefreshRequired/onRefreshReady to accomplish the same thing.
Marc Blank48eba7a2012-01-27 16:16:19 -08001420 }
1421
1422 @Override
1423 public void unregisterContentObserver(ContentObserver observer) {
Andy Huang397621b2012-03-14 20:52:39 -07001424 // See above.
Marc Blank48eba7a2012-01-27 16:16:19 -08001425 }
1426
1427 @Override
1428 public void registerDataSetObserver(DataSetObserver observer) {
Andy Huang2c4e6dc2012-07-25 18:02:59 -07001429 // Nope. We use ConversationListener to accomplish this.
Marc Blank48eba7a2012-01-27 16:16:19 -08001430 }
1431
1432 @Override
1433 public void unregisterDataSetObserver(DataSetObserver observer) {
Andy Huang2c4e6dc2012-07-25 18:02:59 -07001434 // See above.
Marc Blank48eba7a2012-01-27 16:16:19 -08001435 }
1436
1437 @Override
1438 public void setNotificationUri(ContentResolver cr, Uri uri) {
1439 throw new UnsupportedOperationException();
1440 }
1441
1442 @Override
1443 public boolean getWantsAllOnMoveCalls() {
1444 throw new UnsupportedOperationException();
1445 }
1446
1447 @Override
1448 public Bundle getExtras() {
1449 throw new UnsupportedOperationException();
1450 }
1451
1452 @Override
1453 public Bundle respond(Bundle extras) {
Paul Westbrook606a6a12012-04-24 21:40:31 -07001454 if (mUnderlyingCursor != null) {
1455 return mUnderlyingCursor.respond(extras);
1456 }
1457 return Bundle.EMPTY;
Marc Blank48eba7a2012-01-27 16:16:19 -08001458 }
1459
1460 @Override
1461 public boolean requery() {
1462 return true;
Marc Blank97bca7b2012-01-24 11:17:00 -08001463 }
Paul Westbrookbf232c32012-04-18 03:17:41 -07001464
1465 // Below are methods that update Conversation data (update/delete)
1466
1467 public int updateBoolean(Context context, Conversation conversation, String columnName,
1468 boolean value) {
1469 return updateBoolean(context, Arrays.asList(conversation), columnName, value);
1470 }
1471
1472 /**
1473 * Update an integer column for a group of conversations (see updateValues below)
1474 */
1475 public int updateInt(Context context, Collection<Conversation> conversations,
1476 String columnName, int value) {
1477 ContentValues cv = new ContentValues();
1478 cv.put(columnName, value);
1479 return updateValues(context, conversations, cv);
1480 }
1481
1482 /**
1483 * Update a string column for a group of conversations (see updateValues below)
1484 */
1485 public int updateBoolean(Context context, Collection<Conversation> conversations,
1486 String columnName, boolean value) {
1487 ContentValues cv = new ContentValues();
1488 cv.put(columnName, value);
1489 return updateValues(context, conversations, cv);
1490 }
1491
1492 /**
1493 * Update a string column for a group of conversations (see updateValues below)
1494 */
1495 public int updateString(Context context, Collection<Conversation> conversations,
1496 String columnName, String value) {
Mindy Pereiraebdfd982012-07-13 09:22:01 -07001497 return updateStrings(context, conversations, new String[] {
1498 columnName
1499 }, new String[] {
1500 value
1501 });
1502 }
1503
1504 /**
1505 * Update a string columns for a group of conversations (see updateValues below)
1506 */
1507 public int updateStrings(Context context, Collection<Conversation> conversations,
1508 String[] columnNames, String[] values) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001509 ContentValues cv = new ContentValues();
Mindy Pereiraebdfd982012-07-13 09:22:01 -07001510 for (int i = 0; i < columnNames.length; i++) {
1511 cv.put(columnNames[i], values[i]);
1512 }
Paul Westbrookbf232c32012-04-18 03:17:41 -07001513 return updateValues(context, conversations, cv);
1514 }
1515
1516 public int updateBoolean(Context context, String conversationUri, String columnName,
1517 boolean value) {
1518 Conversation conv = new Conversation();
1519 conv.uri = Uri.parse(conversationUri);
1520 return updateBoolean(context, conv, columnName, value);
1521 }
1522
1523 /**
1524 * Update a boolean column for a group of conversations, immediately in the UI and in a single
1525 * transaction in the underlying provider
Paul Westbrookbf232c32012-04-18 03:17:41 -07001526 * @param context the caller's context
Andy Huang2c4e6dc2012-07-25 18:02:59 -07001527 * @param conversations a collection of conversations
1528 * @param values the data to update
Paul Westbrookbf232c32012-04-18 03:17:41 -07001529 * @return the sequence number of the operation (for undo)
1530 */
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001531 public int updateValues(Context context, Collection<Conversation> conversations,
Paul Westbrookbf232c32012-04-18 03:17:41 -07001532 ContentValues values) {
1533 return apply(context,
1534 getOperationsForConversations(conversations, ConversationOperation.UPDATE, values));
1535 }
1536
1537 private ArrayList<ConversationOperation> getOperationsForConversations(
Andy Huang2c4e6dc2012-07-25 18:02:59 -07001538 Collection<Conversation> conversations, int type, ContentValues values) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001539 final ArrayList<ConversationOperation> ops = Lists.newArrayList();
1540 for (Conversation conv: conversations) {
Andy Huang2c4e6dc2012-07-25 18:02:59 -07001541 ConversationOperation op = new ConversationOperation(type, conv, values);
Paul Westbrookbf232c32012-04-18 03:17:41 -07001542 ops.add(op);
1543 }
1544 return ops;
1545 }
1546
1547 /**
1548 * Delete a single conversation
1549 * @param context the caller's context
1550 * @return the sequence number of the operation (for undo)
1551 */
1552 public int delete(Context context, Conversation conversation) {
1553 ArrayList<Conversation> conversations = new ArrayList<Conversation>();
1554 conversations.add(conversation);
1555 return delete(context, conversations);
1556 }
1557
1558 /**
1559 * Delete a single conversation
1560 * @param context the caller's context
1561 * @return the sequence number of the operation (for undo)
1562 */
1563 public int mostlyArchive(Context context, Conversation conversation) {
1564 ArrayList<Conversation> conversations = new ArrayList<Conversation>();
1565 conversations.add(conversation);
1566 return archive(context, conversations);
1567 }
1568
1569 /**
1570 * Delete a single conversation
1571 * @param context the caller's context
1572 * @return the sequence number of the operation (for undo)
1573 */
1574 public int mostlyDelete(Context context, Conversation conversation) {
1575 ArrayList<Conversation> conversations = new ArrayList<Conversation>();
1576 conversations.add(conversation);
1577 return delete(context, conversations);
1578 }
1579
Paul Westbrookbf232c32012-04-18 03:17:41 -07001580 // Convenience methods
1581 private int apply(Context context, ArrayList<ConversationOperation> operations) {
1582 return sProvider.apply(operations, this);
1583 }
1584
1585 private void undoLocal() {
1586 sProvider.undo(this);
1587 }
1588
1589 public void undo(final Context context, final Uri undoUri) {
1590 new Thread(new Runnable() {
1591 @Override
1592 public void run() {
1593 Cursor c = context.getContentResolver().query(undoUri, UIProvider.UNDO_PROJECTION,
1594 null, null, null);
1595 if (c != null) {
1596 c.close();
1597 }
1598 }
1599 }).start();
1600 undoLocal();
1601 }
1602
1603 /**
1604 * Delete a group of conversations immediately in the UI and in a single transaction in the
1605 * underlying provider. See applyAction for argument descriptions
1606 */
1607 public int delete(Context context, Collection<Conversation> conversations) {
1608 return applyAction(context, conversations, ConversationOperation.DELETE);
1609 }
1610
1611 /**
1612 * As above, for archive
1613 */
1614 public int archive(Context context, Collection<Conversation> conversations) {
1615 return applyAction(context, conversations, ConversationOperation.ARCHIVE);
1616 }
1617
1618 /**
1619 * As above, for mute
1620 */
1621 public int mute(Context context, Collection<Conversation> conversations) {
1622 return applyAction(context, conversations, ConversationOperation.MUTE);
1623 }
1624
1625 /**
1626 * As above, for report spam
1627 */
1628 public int reportSpam(Context context, Collection<Conversation> conversations) {
1629 return applyAction(context, conversations, ConversationOperation.REPORT_SPAM);
1630 }
1631
1632 /**
Paul Westbrook77eee622012-07-10 13:41:57 -07001633 * As above, for report not spam
1634 */
1635 public int reportNotSpam(Context context, Collection<Conversation> conversations) {
1636 return applyAction(context, conversations, ConversationOperation.REPORT_NOT_SPAM);
1637 }
1638
1639 /**
Paul Westbrook76b20622012-07-12 11:45:43 -07001640 * As above, for report phishing
1641 */
1642 public int reportPhishing(Context context, Collection<Conversation> conversations) {
1643 return applyAction(context, conversations, ConversationOperation.REPORT_PHISHING);
1644 }
1645
1646 /**
Paul Westbrookbf232c32012-04-18 03:17:41 -07001647 * As above, for mostly archive
1648 */
1649 public int mostlyArchive(Context context, Collection<Conversation> conversations) {
1650 return applyAction(context, conversations, ConversationOperation.MOSTLY_ARCHIVE);
1651 }
1652
1653 /**
1654 * As above, for mostly delete
1655 */
1656 public int mostlyDelete(Context context, Collection<Conversation> conversations) {
1657 return applyAction(context, conversations, ConversationOperation.MOSTLY_DELETE);
1658 }
1659
1660 /**
Mindy Pereira06642fa2012-07-12 16:23:27 -07001661 * As above, for mostly destructive updates
1662 */
1663 public int mostlyDestructiveUpdate(Context context, Collection<Conversation> conversations,
Mindy Pereiraebdfd982012-07-13 09:22:01 -07001664 String column, String value) {
1665 return mostlyDestructiveUpdate(context, conversations, new String[] {
1666 column
1667 }, new String[] {
1668 value
1669 });
1670 }
1671
1672 /**
1673 * As above, for mostly destructive updates.
1674 */
1675 public int mostlyDestructiveUpdate(Context context, Collection<Conversation> conversations,
1676 String[] columnNames, String[] values) {
Mindy Pereira06642fa2012-07-12 16:23:27 -07001677 ContentValues cv = new ContentValues();
Mindy Pereiraebdfd982012-07-13 09:22:01 -07001678 for (int i = 0; i < columnNames.length; i++) {
1679 cv.put(columnNames[i], values[i]);
1680 }
Mindy Pereira06642fa2012-07-12 16:23:27 -07001681 return apply(
1682 context,
1683 getOperationsForConversations(conversations,
1684 ConversationOperation.MOSTLY_DESTRUCTIVE_UPDATE, cv));
1685 }
1686
1687 /**
Paul Westbrookbf232c32012-04-18 03:17:41 -07001688 * Convenience method for performing an operation on a group of conversations
1689 * @param context the caller's context
1690 * @param conversations the conversations to be affected
1691 * @param opAction the action to take
1692 * @return the sequence number of the operation applied in CC
1693 */
1694 private int applyAction(Context context, Collection<Conversation> conversations,
1695 int opAction) {
1696 ArrayList<ConversationOperation> ops = Lists.newArrayList();
1697 for (Conversation conv: conversations) {
1698 ConversationOperation op =
1699 new ConversationOperation(opAction, conv);
1700 ops.add(op);
1701 }
1702 return apply(context, ops);
1703 }
1704
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001705}