blob: 6d0daecd289ef8927046cf2bf56ed1f69aabb070 [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;
Paul Westbrook0a22d442012-11-08 23:32:43 -080030import android.database.CursorWrapper;
Marc Blank48eba7a2012-01-27 16:16:19 -080031import android.database.DataSetObserver;
Marc Blankc8a99422012-01-19 14:27:47 -080032import android.net.Uri;
Marc Blanke3d36792012-04-02 09:30:14 -070033import android.os.AsyncTask;
Marc Blank48eba7a2012-01-27 16:16:19 -080034import android.os.Bundle;
Marc Blanke77d5262012-08-19 17:58:24 -070035import android.os.Handler;
Marc Blank8d69d4e2012-01-25 12:04:28 -080036import android.os.Looper;
37import android.os.RemoteException;
mindypcb0b30e2012-11-30 10:16:35 -080038import android.text.TextUtils;
Marc Blank44dc4492012-08-03 10:23:33 -070039import android.util.Log;
Marc Blankc8a99422012-01-19 14:27:47 -080040
Marc Blankf892f0a2012-01-30 13:04:10 -080041import com.android.mail.providers.Conversation;
Paul Westbrook26746eb2012-12-06 14:44:01 -080042import com.android.mail.providers.Folder;
Andy Huangb2033d82012-12-07 19:30:57 -080043import com.android.mail.providers.FolderList;
Marc Blank4015c182012-01-31 12:38:36 -080044import com.android.mail.providers.UIProvider;
Marc Blank51144942012-03-20 13:59:32 -070045import com.android.mail.providers.UIProvider.ConversationListQueryParameters;
Paul Westbrook334e64a2012-02-23 13:26:35 -080046import com.android.mail.providers.UIProvider.ConversationOperations;
Vikram Aggarwal81a4f082012-09-28 09:19:04 -070047import com.android.mail.ui.ConversationListFragment;
mindyp5c1d8352012-11-05 10:12:44 -080048import com.android.mail.utils.LogTag;
Marc Blank03fa19a2012-02-29 13:16:27 -080049import com.android.mail.utils.LogUtils;
mindypf74147f2012-11-01 11:19:15 -070050import com.android.mail.utils.Utils;
Marc Blank248b1b42012-02-07 13:43:02 -080051import com.google.common.annotations.VisibleForTesting;
Paul Westbrookc8f2a3c2013-01-08 13:57:24 -080052import com.google.common.collect.ImmutableMap;
Paul Westbrookbf232c32012-04-18 03:17:41 -070053import com.google.common.collect.Lists;
Paul Westbrookfe7f5bb2012-09-09 17:59:35 -070054import com.google.common.collect.Sets;
Marc Blankf892f0a2012-01-30 13:04:10 -080055
Marc Blank97bca7b2012-01-24 11:17:00 -080056import java.util.ArrayList;
Paul Westbrookbf232c32012-04-18 03:17:41 -070057import java.util.Arrays;
58import java.util.Collection;
Marc Blankc8a99422012-01-19 14:27:47 -080059import java.util.HashMap;
Marc Blank48eba7a2012-01-27 16:16:19 -080060import java.util.Iterator;
Marc Blankc8a99422012-01-19 14:27:47 -080061import java.util.List;
Paul Westbrookc8f2a3c2013-01-08 13:57:24 -080062import java.util.Map;
Paul Westbrookfe7f5bb2012-09-09 17:59:35 -070063import java.util.Set;
Marc Blankc8a99422012-01-19 14:27:47 -080064
65/**
66 * ConversationCursor is a wrapper around a conversation list cursor that provides update/delete
67 * caching for quick UI response. This is effectively a singleton class, as the cache is
68 * implemented as a static HashMap.
69 */
Marc Blank48eba7a2012-01-27 16:16:19 -080070public final class ConversationCursor implements Cursor {
mindyp5c1d8352012-11-05 10:12:44 -080071 private static final String LOG_TAG = LogTag.getLogTag();
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -080072 /** Turn to true for debugging. */
73 private static final boolean DEBUG = false;
74 /** A deleted row is indicated by the presence of DELETED_COLUMN in the cache map */
Marc Blankc8a99422012-01-19 14:27:47 -080075 private static final String DELETED_COLUMN = "__deleted__";
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -080076 /** An row cached during a requery is indicated by the presence of REQUERY_COLUMN in the map */
mindyp7f55c682012-10-04 11:38:27 -070077 private static final String UPDATE_TIME_COLUMN = "__updatetime__";
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -080078 /**
79 * A sentinel value for the "index" of the deleted column; it's an int that is otherwise invalid
80 */
Marc Blankc8a99422012-01-19 14:27:47 -080081 private static final int DELETED_COLUMN_INDEX = -1;
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -080082 /**
83 * If a cached value within 10 seconds of a refresh(), preserve it. This time has been
84 * chosen empirically (long enough for UI changes to propagate in any reasonable case)
85 */
mindyp7f55c682012-10-04 11:38:27 -070086 private static final long REQUERY_ALLOWANCE_TIME = 10000L;
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -080087
88 /**
89 * The index of the Uri whose data is reflected in the cached row. Updates/Deletes to this Uri
90 * are cached
91 */
92 private static final int URI_COLUMN_INDEX = UIProvider.CONVERSATION_URI_COLUMN;
93
Vikram Aggarwal7460a1c2012-12-28 15:58:58 -080094 /** The resolver for the cursor instantiator's context */
95 private final ContentResolver mResolver;
96
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -080097 /** Our sequence count (for changes sent to underlying provider) */
98 private static int sSequence = 0;
Paul Westbrookbf232c32012-04-18 03:17:41 -070099 @VisibleForTesting
100 static ConversationProvider sProvider;
101
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -0800102 /** The cursor underlying the caching cursor */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700103 @VisibleForTesting
Paul Westbrook0a22d442012-11-08 23:32:43 -0800104 UnderlyingCursorWrapper mUnderlyingCursor;
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -0800105 /** The new cursor obtained via a requery */
Paul Westbrook0a22d442012-11-08 23:32:43 -0800106 private volatile UnderlyingCursorWrapper mRequeryCursor;
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -0800107 /** A mapping from Uri to updated ContentValues */
108 private final HashMap<String, ContentValues> mCacheMap = new HashMap<String, ContentValues>();
109 /** Cache map lock (will be used only very briefly - few ms at most) */
110 private final Object mCacheMapLock = new Object();
111 /** The listeners registered for this cursor */
112 private final List<ConversationListener> mListeners = Lists.newArrayList();
113 /**
114 * The ConversationProvider instance // The runnable executing a refresh (query of underlying
115 * provider)
116 */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700117 private RefreshTask mRefreshTask;
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -0800118 /** Set when we've sent refreshReady() to listeners */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700119 private boolean mRefreshReady = false;
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -0800120 /** Set when we've sent refreshRequired() to listeners */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700121 private boolean mRefreshRequired = false;
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -0800122 /** Whether our first query on this cursor should include a limit */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700123 private boolean mInitialConversationLimit = false;
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -0800124 /** A list of mostly-dead items */
125 private final List<Conversation> mMostlyDead = Lists.newArrayList();
126 /** The name of the loader */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700127 private final String mName;
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -0800128 /** Column names for this cursor */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700129 private String[] mColumnNames;
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -0800130 /** An observer on the underlying cursor (so we can detect changes from outside the UI) */
Marc Blankc8a99422012-01-19 14:27:47 -0800131 private final CursorObserver mCursorObserver;
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -0800132 /** Whether our observer is currently registered with the underlying cursor */
Marc Blank97bca7b2012-01-24 11:17:00 -0800133 private boolean mCursorObserverRegistered = false;
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -0800134 /** Whether our loader is paused */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700135 private boolean mPaused = false;
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -0800136 /** Whether or not sync from underlying provider should be deferred */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700137 private boolean mDeferSync = false;
Marc Blankc8a99422012-01-19 14:27:47 -0800138
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -0800139 /** The current position of the cursor */
Marc Blankc8a99422012-01-19 14:27:47 -0800140 private int mPosition = -1;
Andy Huang397621b2012-03-14 20:52:39 -0700141
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -0800142 /**
143 * The number of cached deletions from this cursor (used to quickly generate an accurate count)
144 */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700145 private int mDeletedCount = 0;
Marc Blankc8a99422012-01-19 14:27:47 -0800146
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -0800147 /** Parameters passed to the underlying query */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700148 private Uri qUri;
149 private String[] qProjection;
Marc Blank48eba7a2012-01-27 16:16:19 -0800150
Paul Westbrook0a22d442012-11-08 23:32:43 -0800151 private void setCursor(UnderlyingCursorWrapper cursor) {
Marc Blankc16be932012-02-24 12:43:48 -0800152 // If we have an existing underlying cursor, make sure it's closed
Paul Westbrookbf232c32012-04-18 03:17:41 -0700153 if (mUnderlyingCursor != null) {
Paul Westbrook04e0dc72012-08-01 17:34:48 -0700154 close();
Marc Blankc16be932012-02-24 12:43:48 -0800155 }
Marc Blankc8a99422012-01-19 14:27:47 -0800156 mColumnNames = cursor.getColumnNames();
Paul Westbrookbf232c32012-04-18 03:17:41 -0700157 mRefreshRequired = false;
158 mRefreshReady = false;
159 mRefreshTask = null;
160 resetCursor(cursor);
Marc Blankc8a99422012-01-19 14:27:47 -0800161 }
162
Paul Westbrookbf232c32012-04-18 03:17:41 -0700163 public ConversationCursor(Activity activity, Uri uri, boolean initialConversationLimit,
164 String name) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700165 mInitialConversationLimit = initialConversationLimit;
Vikram Aggarwal7460a1c2012-12-28 15:58:58 -0800166 mResolver = activity.getApplicationContext().getContentResolver();
Paul Westbrookbf232c32012-04-18 03:17:41 -0700167 qUri = uri;
168 mName = name;
169 qProjection = UIProvider.CONVERSATION_PROJECTION;
Marc Blanke77d5262012-08-19 17:58:24 -0700170 mCursorObserver = new CursorObserver(new Handler(Looper.getMainLooper()));
Paul Westbrookff5c7572012-03-16 13:43:18 -0700171 }
172
173 /**
Marc Blank48eba7a2012-01-27 16:16:19 -0800174 * Create a ConversationCursor; this should be called by the ListActivity using that cursor
Marc Blankc8a99422012-01-19 14:27:47 -0800175 */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700176 public void load() {
177 synchronized (mCacheMapLock) {
Marc Blank51144942012-03-20 13:59:32 -0700178 try {
Marc Blank51144942012-03-20 13:59:32 -0700179 // Create new ConversationCursor
mindyp5c1d8352012-11-05 10:12:44 -0800180 LogUtils.i(LOG_TAG, "Create: initial creation");
mindyp7f55c682012-10-04 11:38:27 -0700181 setCursor(doQuery(mInitialConversationLimit));
Marc Blank51144942012-03-20 13:59:32 -0700182 } finally {
183 // If we used a limit, queue up a query without limit
Paul Westbrookbf232c32012-04-18 03:17:41 -0700184 if (mInitialConversationLimit) {
185 mInitialConversationLimit = false;
186 refresh();
Marc Blank51144942012-03-20 13:59:32 -0700187 }
Marc Blankc16be932012-02-24 12:43:48 -0800188 }
189 }
Marc Blank948985b2012-02-29 11:26:40 -0800190 }
191
Marc Blank6ca57e82012-03-20 19:09:12 -0700192 /**
Marc Blanke1d1b072012-04-13 17:29:16 -0700193 * Pause notifications to UI
194 */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700195 public void pause() {
Marc Blanke1d1b072012-04-13 17:29:16 -0700196 if (DEBUG) {
mindyp5c1d8352012-11-05 10:12:44 -0800197 LogUtils.i(LOG_TAG, "[Paused: %s]", mName);
Marc Blanke1d1b072012-04-13 17:29:16 -0700198 }
Marc Blank1c391172012-04-23 09:23:17 -0700199 mPaused = true;
Marc Blanke1d1b072012-04-13 17:29:16 -0700200 }
201
202 /**
203 * Resume notifications to UI; if any are pending, send them
204 */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700205 public void resume() {
Marc Blanke1d1b072012-04-13 17:29:16 -0700206 if (DEBUG) {
mindyp5c1d8352012-11-05 10:12:44 -0800207 LogUtils.i(LOG_TAG, "[Resumed: %s]", mName);
Marc Blanke1d1b072012-04-13 17:29:16 -0700208 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700209 mPaused = false;
Marc Blanke1d1b072012-04-13 17:29:16 -0700210 checkNotifyUI();
211 }
212
Paul Westbrookbf232c32012-04-18 03:17:41 -0700213 private void checkNotifyUI() {
mindyp5c1d8352012-11-05 10:12:44 -0800214 LogUtils.d(
215 LOG_TAG,
216 "Received notify ui callback and sending a notification is enabled?" +
217 " %s and refresh ready ? %s",
218 (!mPaused && !mDeferSync),
219 (mRefreshReady || (mRefreshRequired && mRefreshTask == null)));
Paul Westbrookbf232c32012-04-18 03:17:41 -0700220 if (!mPaused && !mDeferSync) {
221 if (mRefreshRequired && (mRefreshTask == null)) {
222 notifyRefreshRequired();
223 } else if (mRefreshReady) {
224 notifyRefreshReady();
Marc Blanke1d1b072012-04-13 17:29:16 -0700225 }
226 } else {
mindyp5c1d8352012-11-05 10:12:44 -0800227 LogUtils.i(LOG_TAG, "[checkNotifyUI: %s%s",
Paul Westbrooke2bde3a2012-07-31 17:35:43 -0700228 (mPaused ? "Paused " : ""), (mDeferSync ? "Defer" : ""));
Marc Blanke1d1b072012-04-13 17:29:16 -0700229 }
230 }
231
Paul Westbrook0a22d442012-11-08 23:32:43 -0800232 public Set<Long> getConversationIds() {
233 return mUnderlyingCursor != null ? mUnderlyingCursor.conversationIds() : null;
234 }
235
Marc Blanke1d1b072012-04-13 17:29:16 -0700236 /**
mindypf74147f2012-11-01 11:19:15 -0700237 * Simple wrapper for a cursor that provides methods for quickly determining
238 * the existence of a row.
239 */
Paul Westbrook0a22d442012-11-08 23:32:43 -0800240 private class UnderlyingCursorWrapper extends CursorWrapper {
241 // Ideally these two objects could be combined into a Map from
242 // conversationId -> position, but the cached values uses the conversation
243 // uri as a key.
Paul Westbrookc8f2a3c2013-01-08 13:57:24 -0800244 private final Map<String, Integer> mConversationUriPositionMap;
245 private final Map<Long, Integer> mConversationIdPositionMap;
mindypf74147f2012-11-01 11:19:15 -0700246
Paul Westbrook0a22d442012-11-08 23:32:43 -0800247 public UnderlyingCursorWrapper(Cursor result) {
248 super(result);
Paul Westbrookc8f2a3c2013-01-08 13:57:24 -0800249 final ImmutableMap.Builder<String, Integer> conversationUriPositionMapBuilder =
250 new ImmutableMap.Builder<String, Integer>();
251 final ImmutableMap.Builder<Long, Integer> conversationIdPositionMapBuilder =
252 new ImmutableMap.Builder<Long, Integer>();
Paul Westbrook0a22d442012-11-08 23:32:43 -0800253 if (result != null && result.moveToFirst()) {
254 // We don't want iterating over this cursor to trigger a network
mindypf74147f2012-11-01 11:19:15 -0700255 // request
Paul Westbrook0a22d442012-11-08 23:32:43 -0800256 final boolean networkWasEnabled =
257 Utils.disableConversationCursorNetworkAccess(result);
mindypf74147f2012-11-01 11:19:15 -0700258 do {
Paul Westbrookc8f2a3c2013-01-08 13:57:24 -0800259 final int position = result.getPosition();
260 conversationUriPositionMapBuilder.put(
261 result.getString(URI_COLUMN_INDEX), position);
262 conversationIdPositionMapBuilder.put(
263 result.getLong(UIProvider.CONVERSATION_ID_COLUMN), position);
Paul Westbrook0a22d442012-11-08 23:32:43 -0800264 } while (result.moveToNext());
mindypf74147f2012-11-01 11:19:15 -0700265
266 if (networkWasEnabled) {
Paul Westbrook0a22d442012-11-08 23:32:43 -0800267 Utils.enableConversationCursorNetworkAccess(result);
mindypf74147f2012-11-01 11:19:15 -0700268 }
269 }
Paul Westbrookc8f2a3c2013-01-08 13:57:24 -0800270 mConversationUriPositionMap = conversationUriPositionMapBuilder.build();
271 mConversationIdPositionMap = conversationIdPositionMapBuilder.build();
mindypf74147f2012-11-01 11:19:15 -0700272 }
273
274 public boolean contains(String uri) {
Paul Westbrookc8f2a3c2013-01-08 13:57:24 -0800275 return mConversationUriPositionMap.containsKey(uri);
mindypf74147f2012-11-01 11:19:15 -0700276 }
277
Paul Westbrook0a22d442012-11-08 23:32:43 -0800278 public Set<Long> conversationIds() {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800279 return mConversationIdPositionMap.keySet();
Paul Westbrookc8f2a3c2013-01-08 13:57:24 -0800280 }
281
282 public int getPosition(long conversationId) {
283 final Integer position = mConversationIdPositionMap.get(conversationId);
284 return position != null ? position.intValue() : -1;
285 }
286
287 public int getPosition(String conversationUri) {
288 final Integer position = mConversationUriPositionMap.get(conversationUri);
289 return position != null ? position.intValue() : -1;
mindypf74147f2012-11-01 11:19:15 -0700290 }
291 }
292
293 /**
Marc Blanke3d36792012-04-02 09:30:14 -0700294 * Runnable that performs the query on the underlying provider
295 */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700296 private class RefreshTask extends AsyncTask<Void, Void, Void> {
Paul Westbrook0a22d442012-11-08 23:32:43 -0800297 private UnderlyingCursorWrapper mCursor = null;
Marc Blanke3d36792012-04-02 09:30:14 -0700298
Paul Westbrookbf232c32012-04-18 03:17:41 -0700299 private RefreshTask() {
Marc Blanke3d36792012-04-02 09:30:14 -0700300 }
301
302 @Override
303 protected Void doInBackground(Void... params) {
304 if (DEBUG) {
mindyp5c1d8352012-11-05 10:12:44 -0800305 LogUtils.i(LOG_TAG, "[Start refresh of %s: %d]", mName, hashCode());
Marc Blanke3d36792012-04-02 09:30:14 -0700306 }
307 // Get new data
Paul Westbrookbf232c32012-04-18 03:17:41 -0700308 mCursor = doQuery(false);
Marc Blank5ef5e0f2012-09-16 10:31:51 -0700309 // Make sure window is full
310 mCursor.getCount();
Marc Blanke3d36792012-04-02 09:30:14 -0700311 return null;
312 }
313
314 @Override
315 protected void onPostExecute(Void param) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700316 synchronized(mCacheMapLock) {
mindyp5c1d8352012-11-05 10:12:44 -0800317 LogUtils.d(
318 LOG_TAG,
319 "Received notify ui callback and sending a notification is enabled? %s",
320 (!mPaused && !mDeferSync));
Marc Blanke602ae12012-08-22 10:02:40 -0700321 // If cursor got closed (e.g. reset loader) in the meantime, cancel the refresh
322 if (isClosed()) {
323 onCancelled();
324 return;
325 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700326 mRequeryCursor = mCursor;
Paul Westbrookbf232c32012-04-18 03:17:41 -0700327 mRefreshReady = true;
Marc Blanke3d36792012-04-02 09:30:14 -0700328 if (DEBUG) {
mindyp5c1d8352012-11-05 10:12:44 -0800329 LogUtils.i(LOG_TAG, "[Query done %s: %d]", mName, hashCode());
Marc Blanke3d36792012-04-02 09:30:14 -0700330 }
Marc Blank1c391172012-04-23 09:23:17 -0700331 if (!mDeferSync && !mPaused) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700332 notifyRefreshReady();
Marc Blank1c391172012-04-23 09:23:17 -0700333 }
Marc Blanke3d36792012-04-02 09:30:14 -0700334 }
335 }
336
337 @Override
338 protected void onCancelled() {
339 if (DEBUG) {
mindyp5c1d8352012-11-05 10:12:44 -0800340 LogUtils.i(LOG_TAG, "[Ignoring refresh result: %d]", hashCode());
Marc Blanke3d36792012-04-02 09:30:14 -0700341 }
342 if (mCursor != null) {
343 mCursor.close();
344 }
345 }
346 }
347
Paul Westbrook0a22d442012-11-08 23:32:43 -0800348 private UnderlyingCursorWrapper doQuery(boolean withLimit) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700349 Uri uri = qUri;
Marc Blank51144942012-03-20 13:59:32 -0700350 if (withLimit) {
351 uri = uri.buildUpon().appendQueryParameter(ConversationListQueryParameters.LIMIT,
352 ConversationListQueryParameters.DEFAULT_LIMIT).build();
353 }
354 long time = System.currentTimeMillis();
Paul Westbrookdac65802012-03-23 16:59:15 -0700355
Vikram Aggarwal7460a1c2012-12-28 15:58:58 -0800356 final Cursor result = mResolver.query(uri, qProjection, null, null, null);
mindyp7f55c682012-10-04 11:38:27 -0700357 if (result == null) {
mindyp5c1d8352012-11-05 10:12:44 -0800358 Log.w(LOG_TAG, "doQuery returning null cursor, uri: " + uri);
Marc Blank44dc4492012-08-03 10:23:33 -0700359 } else if (DEBUG) {
Marc Blank51144942012-03-20 13:59:32 -0700360 time = System.currentTimeMillis() - time;
mindyp5c1d8352012-11-05 10:12:44 -0800361 LogUtils.i(LOG_TAG, "ConversationCursor query: %s, %dms, %d results",
Paul Westbrookdac65802012-03-23 16:59:15 -0700362 uri, time, result.getCount());
Marc Blank51144942012-03-20 13:59:32 -0700363 }
Paul Westbrook0a22d442012-11-08 23:32:43 -0800364 return new UnderlyingCursorWrapper(result);
Marc Blank48eba7a2012-01-27 16:16:19 -0800365 }
366
Marc Blank3232a962012-03-08 15:32:37 -0800367 static boolean offUiThread() {
368 return Looper.getMainLooper().getThread() != Thread.currentThread();
369 }
370
Marc Blank48eba7a2012-01-27 16:16:19 -0800371 /**
372 * Reset the cursor; this involves clearing out our cache map and resetting our various counts
373 * The cursor should be reset whenever we get fresh data from the underlying cursor. The cache
374 * is locked during the reset, which will block the UI, but for only a very short time
375 * (estimated at a few ms, but we can profile this; remember that the cache will usually
376 * be empty or have a few entries)
377 */
Paul Westbrook0a22d442012-11-08 23:32:43 -0800378 private void resetCursor(UnderlyingCursorWrapper newCursorWrapper) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700379 synchronized (mCacheMapLock) {
mindyp7f55c682012-10-04 11:38:27 -0700380 // Walk through the cache
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800381 final Iterator<Map.Entry<String, ContentValues>> iter =
Paul Westbrook4de145b2012-12-19 16:43:25 -0800382 mCacheMap.entrySet().iterator();
Paul Westbrookd9e49da2012-10-26 19:17:10 -0700383 final long now = System.currentTimeMillis();
Marc Blank48eba7a2012-01-27 16:16:19 -0800384 while (iter.hasNext()) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800385 Map.Entry<String, ContentValues> entry = iter.next();
Paul Westbrookd9e49da2012-10-26 19:17:10 -0700386 final ContentValues values = entry.getValue();
387 final String key = entry.getKey();
mindypf74147f2012-11-01 11:19:15 -0700388 boolean withinTimeWindow = false;
389 boolean removed = false;
Paul Westbrookd9e49da2012-10-26 19:17:10 -0700390 if (values != null) {
391 Long updateTime = values.getAsLong(UPDATE_TIME_COLUMN);
392 if (updateTime != null && ((now - updateTime) < REQUERY_ALLOWANCE_TIME)) {
mindyp5c1d8352012-11-05 10:12:44 -0800393 LogUtils.i(LOG_TAG, "IN resetCursor, keep recent changes to %s", key);
mindypf74147f2012-11-01 11:19:15 -0700394 withinTimeWindow = true;
Paul Westbrookd9e49da2012-10-26 19:17:10 -0700395 } else if (updateTime == null) {
mindyp5c1d8352012-11-05 10:12:44 -0800396 LogUtils.e(LOG_TAG, "null updateTime from mCacheMap for key: %s", key);
Paul Westbrookd9e49da2012-10-26 19:17:10 -0700397 }
398 if (values.containsKey(DELETED_COLUMN)) {
mindypf74147f2012-11-01 11:19:15 -0700399 // Item is deleted locally AND deleted in the new cursor.
400 if (!newCursorWrapper.contains(key)) {
401 // Keep the deleted count up-to-date; remove the
402 // cache entry
403 mDeletedCount--;
404 removed = true;
mindyp5c1d8352012-11-05 10:12:44 -0800405 LogUtils.i(LOG_TAG,
mindypf74147f2012-11-01 11:19:15 -0700406 "IN resetCursor, sDeletedCount decremented to: %d by %s",
407 mDeletedCount, key);
408 }
Paul Westbrookd9e49da2012-10-26 19:17:10 -0700409 }
410 } else {
mindyp5c1d8352012-11-05 10:12:44 -0800411 LogUtils.e(LOG_TAG, "null ContentValues from mCacheMap for key: %s", key);
mindyp7f55c682012-10-04 11:38:27 -0700412 }
mindypf74147f2012-11-01 11:19:15 -0700413 // Remove the entry if it was time for an update or the item was deleted by the user.
414 if (!withinTimeWindow || removed) {
415 iter.remove();
416 }
Marc Blank48eba7a2012-01-27 16:16:19 -0800417 }
Paul Westbrookc33ec4c2012-08-09 01:47:25 -0700418
Marc Blank28cb53d2012-08-19 17:58:24 -0700419 // Swap cursor
420 if (mUnderlyingCursor != null) {
421 close();
422 }
Paul Westbrook0a22d442012-11-08 23:32:43 -0800423 mUnderlyingCursor = newCursorWrapper;
Paul Westbrookc33ec4c2012-08-09 01:47:25 -0700424
Marc Blank28cb53d2012-08-19 17:58:24 -0700425 mPosition = -1;
426 mUnderlyingCursor.moveToPosition(mPosition);
427 if (!mCursorObserverRegistered) {
428 mUnderlyingCursor.registerContentObserver(mCursorObserver);
429 mCursorObserverRegistered = true;
430 }
431 mRefreshRequired = false;
Paul Westbrookc33ec4c2012-08-09 01:47:25 -0700432 }
Marc Blankc8a99422012-01-19 14:27:47 -0800433 }
434
435 /**
Paul Westbrookfe7f5bb2012-09-09 17:59:35 -0700436 * Returns the conversation uris for the Conversations that the ConversationCursor is treating
437 * as deleted. This is an optimization to allow clients to determine if an item has been
438 * removed, without having to iterate through the whole cursor
439 */
440 public Set<String> getDeletedItems() {
441 synchronized (mCacheMapLock) {
442 // Walk through the cache and return the list of uris that have been deleted
443 final Set<String> deletedItems = Sets.newHashSet();
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800444 final Iterator<Map.Entry<String, ContentValues>> iter =
Paul Westbrookfe7f5bb2012-09-09 17:59:35 -0700445 mCacheMap.entrySet().iterator();
446 while (iter.hasNext()) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800447 final Map.Entry<String, ContentValues> entry = iter.next();
Paul Westbrookfe7f5bb2012-09-09 17:59:35 -0700448 final ContentValues values = entry.getValue();
449 if (values.containsKey(DELETED_COLUMN)) {
450 // Since clients of the conversation cursor see conversation ConversationCursor
451 // provider uris, we need to make sure that this also returns these uris
452 final Uri conversationUri = Uri.parse(entry.getKey());
453 deletedItems.add(uriToCachingUriString(conversationUri)) ;
454 }
455 }
456 return deletedItems;
457 }
458 }
459
460 /**
Paul Westbrookc8f2a3c2013-01-08 13:57:24 -0800461 * Returns the position, in the ConversationCursor, of the Conversation with the specified id.
462 * The returned posision will take into account any items that have been deleted.
463 */
464 public int getConversationPosition(long conversationId) {
465 final int underlyingPosition = mUnderlyingCursor.getPosition(conversationId);
466 if (underlyingPosition < 0) {
467 // The conversation wasn't found in the underlying cursor, return the underlying result.
468 return underlyingPosition;
469 }
470
471 // Walk through each of the deleted items. If the deleted item is before the underlying
472 // position, decrement the position
473 synchronized (mCacheMapLock) {
474 int updatedPosition = underlyingPosition;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800475 final Iterator<Map.Entry<String, ContentValues>> iter =
Paul Westbrookc8f2a3c2013-01-08 13:57:24 -0800476 mCacheMap.entrySet().iterator();
477 while (iter.hasNext()) {
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800478 final Map.Entry<String, ContentValues> entry = iter.next();
Paul Westbrookc8f2a3c2013-01-08 13:57:24 -0800479 final ContentValues values = entry.getValue();
480 if (values.containsKey(DELETED_COLUMN)) {
481 // Since clients of the conversation cursor see conversation ConversationCursor
482 // provider uris, we need to make sure that this also returns these uris
483 final String conversationUri = entry.getKey();
484 final int deletedItemPosition = mUnderlyingCursor.getPosition(conversationUri);
485 if (deletedItemPosition == underlyingPosition) {
486 // The requested items has been deleted.
487 return -1;
488 }
489
490 if (deletedItemPosition >= 0 && deletedItemPosition < underlyingPosition) {
491 // This item has been deleted, but is still in the underlying cursor, at
492 // a position before the requested item. Decrement the position of the
493 // requested item.
494 updatedPosition--;
495 }
496 }
497 }
498 return updatedPosition;
499 }
500 }
501
502 /**
Marc Blankbf128eb2012-04-18 15:58:45 -0700503 * Add a listener for this cursor; we'll notify it when our data changes
504 */
505 public void addListener(ConversationListener listener) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700506 synchronized (mListeners) {
507 if (!mListeners.contains(listener)) {
508 mListeners.add(listener);
Marc Blankbf128eb2012-04-18 15:58:45 -0700509 } else {
mindyp5c1d8352012-11-05 10:12:44 -0800510 LogUtils.i(LOG_TAG, "Ignoring duplicate add of listener");
Marc Blankbf128eb2012-04-18 15:58:45 -0700511 }
512 }
513 }
514
515 /**
516 * Remove a listener for this cursor
517 */
518 public void removeListener(ConversationListener listener) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700519 synchronized(mListeners) {
520 mListeners.remove(listener);
Marc Blankbf128eb2012-04-18 15:58:45 -0700521 }
522 }
523
524 /**
Marc Blankc8a99422012-01-19 14:27:47 -0800525 * Generate a forwarding Uri to ConversationProvider from an original Uri. We do this by
526 * changing the authority to ours, but otherwise leaving the Uri intact.
527 * NOTE: This won't handle query parameters, so the functionality will need to be added if
528 * parameters are used in the future
529 * @param uri the uri
530 * @return a forwarding uri to ConversationProvider
531 */
532 private static String uriToCachingUriString (Uri uri) {
Paul Westbrookfe7f5bb2012-09-09 17:59:35 -0700533 final String provider = uri.getAuthority();
Paul Westbrook77177b12012-02-07 15:23:42 -0800534 return uri.getScheme() + "://" + ConversationProvider.AUTHORITY
535 + "/" + provider + uri.getPath();
Marc Blankc8a99422012-01-19 14:27:47 -0800536 }
537
538 /**
539 * Regenerate the original Uri from a forwarding (ConversationProvider) Uri
540 * NOTE: See note above for uriToCachingUri
541 * @param uri the forwarding Uri
542 * @return the original Uri
543 */
544 private static Uri uriFromCachingUri(Uri uri) {
Marc Blankd9787152012-03-15 09:43:12 -0700545 String authority = uri.getAuthority();
546 // Don't modify uri's that aren't ours
547 if (!authority.equals(ConversationProvider.AUTHORITY)) {
548 return uri;
549 }
Marc Blankc8a99422012-01-19 14:27:47 -0800550 List<String> path = uri.getPathSegments();
551 Uri.Builder builder = new Uri.Builder().scheme(uri.getScheme()).authority(path.get(0));
552 for (int i = 1; i < path.size(); i++) {
553 builder.appendPath(path.get(i));
554 }
555 return builder.build();
556 }
557
Marc Blanke1d1b072012-04-13 17:29:16 -0700558 private static String uriStringFromCachingUri(Uri uri) {
559 Uri underlyingUri = uriFromCachingUri(uri);
560 // Remember to decode the underlying Uri as it might be encoded (as w/ Gmail)
561 return Uri.decode(underlyingUri.toString());
562 }
563
Andy Huangdaa06ab2012-07-24 10:46:44 -0700564 public void setConversationColumn(Uri conversationUri, String columnName, Object value) {
565 final String uriStr = uriStringFromCachingUri(conversationUri);
Paul Westbrookbf232c32012-04-18 03:17:41 -0700566 synchronized (mCacheMapLock) {
Andy Huangdaa06ab2012-07-24 10:46:44 -0700567 cacheValue(uriStr, columnName, value);
Marc Blankbec51152012-03-22 19:27:34 -0700568 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700569 notifyDataChanged();
Marc Blank958bf4d2012-04-05 18:10:55 -0700570 }
571
572 /**
Marc Blankc8a99422012-01-19 14:27:47 -0800573 * Cache a column name/value pair for a given Uri
574 * @param uriString the Uri for which the column name/value pair applies
575 * @param columnName the column name
576 * @param value the value to be cached
577 */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700578 private void cacheValue(String uriString, String columnName, Object value) {
Andy Huang489dd222012-03-29 14:52:55 -0700579 // Calling this method off the UI thread will mess with ListView's reading of the cursor's
580 // count
581 if (offUiThread()) {
mindyp5c1d8352012-11-05 10:12:44 -0800582 LogUtils.e(LOG_TAG, new Error(),
583 "cacheValue incorrectly being called from non-UI thread");
Andy Huang489dd222012-03-29 14:52:55 -0700584 }
585
Paul Westbrookbf232c32012-04-18 03:17:41 -0700586 synchronized (mCacheMapLock) {
Marc Blank958bf4d2012-04-05 18:10:55 -0700587 // Get the map for our uri
Paul Westbrookbf232c32012-04-18 03:17:41 -0700588 ContentValues map = mCacheMap.get(uriString);
Marc Blank958bf4d2012-04-05 18:10:55 -0700589 // Create one if necessary
590 if (map == null) {
591 map = new ContentValues();
Paul Westbrookbf232c32012-04-18 03:17:41 -0700592 mCacheMap.put(uriString, map);
Marc Blank958bf4d2012-04-05 18:10:55 -0700593 }
594 // If we're caching a deletion, add to our count
595 if (columnName == DELETED_COLUMN) {
596 final boolean state = (Boolean)value;
597 final boolean hasValue = map.get(columnName) != null;
598 if (state && !hasValue) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700599 mDeletedCount++;
Marc Blank958bf4d2012-04-05 18:10:55 -0700600 if (DEBUG) {
mindyp5c1d8352012-11-05 10:12:44 -0800601 LogUtils.i(LOG_TAG, "Deleted %s, incremented deleted count=%d", uriString,
Paul Westbrookbf232c32012-04-18 03:17:41 -0700602 mDeletedCount);
Paul Westbrook9c87fe32012-03-28 22:14:42 -0700603 }
Marc Blank958bf4d2012-04-05 18:10:55 -0700604 } else if (!state && hasValue) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700605 mDeletedCount--;
Marc Blank958bf4d2012-04-05 18:10:55 -0700606 map.remove(columnName);
607 if (DEBUG) {
mindyp5c1d8352012-11-05 10:12:44 -0800608 LogUtils.i(LOG_TAG, "Undeleted %s, decremented deleted count=%d", uriString,
Paul Westbrookbf232c32012-04-18 03:17:41 -0700609 mDeletedCount);
Paul Westbrook9c87fe32012-03-28 22:14:42 -0700610 }
Marc Blank958bf4d2012-04-05 18:10:55 -0700611 return;
612 } else if (!state) {
613 // Trying to undelete, but it's not deleted; just return
614 if (DEBUG) {
mindyp5c1d8352012-11-05 10:12:44 -0800615 LogUtils.i(LOG_TAG, "Undeleted %s, IGNORING, deleted count=%d", uriString,
Paul Westbrookbf232c32012-04-18 03:17:41 -0700616 mDeletedCount);
Marc Blank958bf4d2012-04-05 18:10:55 -0700617 }
618 return;
Paul Westbrook9c87fe32012-03-28 22:14:42 -0700619 }
Marc Blank48eba7a2012-01-27 16:16:19 -0800620 }
Marc Blank958bf4d2012-04-05 18:10:55 -0700621 // ContentValues has no generic "put", so we must test. For now, the only classes
Andy Huang351ad4e2012-12-06 16:04:58 -0800622 // of values implemented are Boolean/Integer/String/Blob, though others are trivially
Marc Blank958bf4d2012-04-05 18:10:55 -0700623 // added
624 if (value instanceof Boolean) {
625 map.put(columnName, ((Boolean) value).booleanValue() ? 1 : 0);
626 } else if (value instanceof Integer) {
627 map.put(columnName, (Integer) value);
628 } else if (value instanceof String) {
629 map.put(columnName, (String) value);
Andy Huang351ad4e2012-12-06 16:04:58 -0800630 } else if (value instanceof byte[]) {
631 map.put(columnName, (byte[])value);
Marc Blank958bf4d2012-04-05 18:10:55 -0700632 } else {
633 final String cname = value.getClass().getName();
634 throw new IllegalArgumentException("Value class not compatible with cache: "
635 + cname);
636 }
mindyp7f55c682012-10-04 11:38:27 -0700637 map.put(UPDATE_TIME_COLUMN, System.currentTimeMillis());
Marc Blank958bf4d2012-04-05 18:10:55 -0700638 if (DEBUG && (columnName != DELETED_COLUMN)) {
mindyp5c1d8352012-11-05 10:12:44 -0800639 LogUtils.i(LOG_TAG, "Caching value for %s: %s", uriString, columnName);
Marc Blank958bf4d2012-04-05 18:10:55 -0700640 }
Marc Blankc8a99422012-01-19 14:27:47 -0800641 }
642 }
643
644 /**
645 * Get the cached value for the provided column; we special case -1 as the "deleted" column
646 * @param columnIndex the index of the column whose cached value we want to retrieve
647 * @return the cached value for this column, or null if there is none
648 */
649 private Object getCachedValue(int columnIndex) {
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -0800650 String uri = mUnderlyingCursor.getString(URI_COLUMN_INDEX);
Marc Blanke1d1b072012-04-13 17:29:16 -0700651 return getCachedValue(uri, columnIndex);
652 }
653
654 private Object getCachedValue(String uri, int columnIndex) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700655 ContentValues uriMap = mCacheMap.get(uri);
Marc Blankc8a99422012-01-19 14:27:47 -0800656 if (uriMap != null) {
657 String columnName;
658 if (columnIndex == DELETED_COLUMN_INDEX) {
659 columnName = DELETED_COLUMN;
660 } else {
661 columnName = mColumnNames[columnIndex];
662 }
663 return uriMap.get(columnName);
664 }
665 return null;
666 }
667
668 /**
Marc Blank97bca7b2012-01-24 11:17:00 -0800669 * When the underlying cursor changes, we want to alert the listener
Marc Blankc8a99422012-01-19 14:27:47 -0800670 */
671 private void underlyingChanged() {
Paul Westbrookc33ec4c2012-08-09 01:47:25 -0700672 synchronized(mCacheMapLock) {
Marc Blank28cb53d2012-08-19 17:58:24 -0700673 if (mCursorObserverRegistered) {
674 try {
675 mUnderlyingCursor.unregisterContentObserver(mCursorObserver);
676 } catch (IllegalStateException e) {
677 // Maybe the cursor was GC'd?
678 }
679 mCursorObserverRegistered = false;
680 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700681 mRefreshRequired = true;
682 if (!mPaused) {
Marc Blanke1d1b072012-04-13 17:29:16 -0700683 notifyRefreshRequired();
684 }
Marc Blank97bca7b2012-01-24 11:17:00 -0800685 }
Marc Blanke1d1b072012-04-13 17:29:16 -0700686 }
687
688 /**
689 * Must be called on UI thread; notify listeners that a refresh is required
690 */
691 private void notifyRefreshRequired() {
Marc Blankb600a832012-02-16 09:20:18 -0800692 if (DEBUG) {
mindyp5c1d8352012-11-05 10:12:44 -0800693 LogUtils.i(LOG_TAG, "[Notify %s: onRefreshRequired()]", mName);
Marc Blankb600a832012-02-16 09:20:18 -0800694 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700695 if (!mDeferSync) {
696 synchronized(mListeners) {
697 for (ConversationListener listener: mListeners) {
Marc Blankbf128eb2012-04-18 15:58:45 -0700698 listener.onRefreshRequired();
699 }
700 }
Marc Blankb600a832012-02-16 09:20:18 -0800701 }
Marc Blanke1d1b072012-04-13 17:29:16 -0700702 }
703
704 /**
705 * Must be called on UI thread; notify listeners that a new cursor is ready
706 */
707 private void notifyRefreshReady() {
708 if (DEBUG) {
mindyp5c1d8352012-11-05 10:12:44 -0800709 LogUtils.i(LOG_TAG, "[Notify %s: onRefreshReady(), %d listeners]",
Paul Westbrooke2bde3a2012-07-31 17:35:43 -0700710 mName, mListeners.size());
Marc Blanke1d1b072012-04-13 17:29:16 -0700711 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700712 synchronized(mListeners) {
713 for (ConversationListener listener: mListeners) {
Marc Blankbf128eb2012-04-18 15:58:45 -0700714 listener.onRefreshReady();
715 }
716 }
Marc Blanke1d1b072012-04-13 17:29:16 -0700717 }
718
719 /**
720 * Must be called on UI thread; notify listeners that data has changed
721 */
722 private void notifyDataChanged() {
723 if (DEBUG) {
mindyp5c1d8352012-11-05 10:12:44 -0800724 LogUtils.i(LOG_TAG, "[Notify %s: onDataSetChanged()]", mName);
Marc Blanke1d1b072012-04-13 17:29:16 -0700725 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700726 synchronized(mListeners) {
727 for (ConversationListener listener: mListeners) {
Marc Blankbf128eb2012-04-18 15:58:45 -0700728 listener.onDataSetChanged();
729 }
730 }
Marc Blankc8a99422012-01-19 14:27:47 -0800731 }
732
Marc Blank4015c182012-01-31 12:38:36 -0800733 /**
734 * Put the refreshed cursor in place (called by the UI)
735 */
Marc Blank4e25c942012-02-02 19:41:14 -0800736 public void sync() {
Marc Blank28cb53d2012-08-19 17:58:24 -0700737 if (mRequeryCursor == null) {
Marc Blank09b32382012-03-20 12:12:17 -0700738 // This can happen during an animated deletion, if the UI isn't keeping track, or
739 // if a new query intervened (i.e. user changed folders)
Marc Blank948985b2012-02-29 11:26:40 -0800740 if (DEBUG) {
mindyp5c1d8352012-11-05 10:12:44 -0800741 LogUtils.i(LOG_TAG, "[sync() %s; no requery cursor]", mName);
Marc Blank948985b2012-02-29 11:26:40 -0800742 }
Marc Blank09b32382012-03-20 12:12:17 -0700743 return;
744 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700745 synchronized(mCacheMapLock) {
Marc Blank28cb53d2012-08-19 17:58:24 -0700746 if (DEBUG) {
mindyp5c1d8352012-11-05 10:12:44 -0800747 LogUtils.i(LOG_TAG, "[sync() %s]", mName);
Marc Blank28cb53d2012-08-19 17:58:24 -0700748 }
749 resetCursor(mRequeryCursor);
Paul Westbrookbf232c32012-04-18 03:17:41 -0700750 mRequeryCursor = null;
751 mRefreshTask = null;
752 mRefreshReady = false;
Marc Blank3f1eb852012-02-03 15:38:01 -0800753 }
Paul Westbrook66150d72012-04-18 04:45:16 -0700754 notifyDataChanged();
Marc Blank4e25c942012-02-02 19:41:14 -0800755 }
756
757 public boolean isRefreshRequired() {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700758 return mRefreshRequired;
Marc Blank4e25c942012-02-02 19:41:14 -0800759 }
760
761 public boolean isRefreshReady() {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700762 return mRefreshReady;
Marc Blank4015c182012-01-31 12:38:36 -0800763 }
764
765 /**
766 * Cancel a refresh in progress
767 */
Paul Westbrookbf232c32012-04-18 03:17:41 -0700768 public void cancelRefresh() {
Marc Blank3f1eb852012-02-03 15:38:01 -0800769 if (DEBUG) {
mindyp5c1d8352012-11-05 10:12:44 -0800770 LogUtils.i(LOG_TAG, "[cancelRefresh() %s]", mName);
Marc Blank3f1eb852012-02-03 15:38:01 -0800771 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700772 synchronized(mCacheMapLock) {
773 if (mRefreshTask != null) {
774 mRefreshTask.cancel(true);
775 mRefreshTask = null;
Marc Blanke3d36792012-04-02 09:30:14 -0700776 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700777 mRefreshReady = false;
Marc Blank4015c182012-01-31 12:38:36 -0800778 // If we have the cursor, close it; otherwise, it will get closed when the query
Marc Blank09b32382012-03-20 12:12:17 -0700779 // finishes (it checks sRefreshInProgress)
Paul Westbrookbf232c32012-04-18 03:17:41 -0700780 if (mRequeryCursor != null) {
781 mRequeryCursor.close();
782 mRequeryCursor = null;
Marc Blank4015c182012-01-31 12:38:36 -0800783 }
784 }
785 }
786
787 /**
Marc Blank48eba7a2012-01-27 16:16:19 -0800788 * When we get a requery from the UI, we'll do it, but also clear the cache. The listener is
789 * notified when the requery is complete
Marc Blank97bca7b2012-01-24 11:17:00 -0800790 * NOTE: This will have to change, of course, when we start using loaders...
791 */
Marc Blank48eba7a2012-01-27 16:16:19 -0800792 public boolean refresh() {
Marc Blank3f1eb852012-02-03 15:38:01 -0800793 if (DEBUG) {
mindyp5c1d8352012-11-05 10:12:44 -0800794 LogUtils.i(LOG_TAG, "[refresh() %s]", mName);
Marc Blank3f1eb852012-02-03 15:38:01 -0800795 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700796 synchronized(mCacheMapLock) {
797 if (mRefreshTask != null) {
Marc Blanke3d36792012-04-02 09:30:14 -0700798 if (DEBUG) {
mindyp5c1d8352012-11-05 10:12:44 -0800799 LogUtils.i(LOG_TAG, "[refresh() %s returning; already running %d]",
Paul Westbrooke2bde3a2012-07-31 17:35:43 -0700800 mName, mRefreshTask.hashCode());
Marc Blank4015c182012-01-31 12:38:36 -0800801 }
Marc Blanke3d36792012-04-02 09:30:14 -0700802 return false;
Marc Blank48eba7a2012-01-27 16:16:19 -0800803 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700804 mRefreshTask = new RefreshTask();
805 mRefreshTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
Marc Blanke3d36792012-04-02 09:30:14 -0700806 }
Marc Blankc8a99422012-01-19 14:27:47 -0800807 return true;
808 }
809
Paul Westbrookbf232c32012-04-18 03:17:41 -0700810 public void disable() {
811 close();
812 mCacheMap.clear();
813 mListeners.clear();
814 mUnderlyingCursor = null;
815 }
816
Marc Blankb600a832012-02-16 09:20:18 -0800817 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800818 public void close() {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700819 if (mUnderlyingCursor != null && !mUnderlyingCursor.isClosed()) {
Marc Blankf9d87192012-02-16 10:50:41 -0800820 // Unregister our observer on the underlying cursor and close as usual
821 if (mCursorObserverRegistered) {
822 try {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700823 mUnderlyingCursor.unregisterContentObserver(mCursorObserver);
Marc Blankf9d87192012-02-16 10:50:41 -0800824 } catch (IllegalStateException e) {
825 // Maybe the cursor got GC'd?
826 }
827 mCursorObserverRegistered = false;
828 }
Paul Westbrookbf232c32012-04-18 03:17:41 -0700829 mUnderlyingCursor.close();
Marc Blankdd10bc82012-02-01 19:10:46 -0800830 }
Marc Blankc8a99422012-01-19 14:27:47 -0800831 }
832
833 /**
834 * Move to the next not-deleted item in the conversation
835 */
Marc Blankb600a832012-02-16 09:20:18 -0800836 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800837 public boolean moveToNext() {
838 while (true) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700839 boolean ret = mUnderlyingCursor.moveToNext();
Marc Blank5d8b1fb2012-07-20 17:12:36 -0700840 if (!ret) {
Marc Blank44d5f0a2012-09-21 14:20:55 -0700841 mPosition = getCount();
Marc Blanke5884c72012-08-18 10:03:59 -0700842 // STOPSHIP
mindyp5c1d8352012-11-05 10:12:44 -0800843 LogUtils.i(LOG_TAG, "*** moveToNext returns false; pos = %d, und = %d, del = %d",
Marc Blanke5884c72012-08-18 10:03:59 -0700844 mPosition, mUnderlyingCursor.getPosition(), mDeletedCount);
Marc Blank5d8b1fb2012-07-20 17:12:36 -0700845 return false;
846 }
Marc Blankc8a99422012-01-19 14:27:47 -0800847 if (getCachedValue(DELETED_COLUMN_INDEX) instanceof Integer) continue;
848 mPosition++;
849 return true;
850 }
851 }
852
853 /**
854 * Move to the previous not-deleted item in the conversation
855 */
Marc Blankb600a832012-02-16 09:20:18 -0800856 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800857 public boolean moveToPrevious() {
858 while (true) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700859 boolean ret = mUnderlyingCursor.moveToPrevious();
Marc Blank5d8b1fb2012-07-20 17:12:36 -0700860 if (!ret) {
861 // Make sure we're before the first position
862 mPosition = -1;
863 return false;
864 }
Marc Blankec7c4da2012-03-22 20:28:55 -0700865 if (getCachedValue(DELETED_COLUMN_INDEX) instanceof Integer) continue;
Marc Blankc8a99422012-01-19 14:27:47 -0800866 mPosition--;
867 return true;
868 }
869 }
870
Marc Blankb600a832012-02-16 09:20:18 -0800871 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800872 public int getPosition() {
873 return mPosition;
874 }
875
876 /**
877 * The actual cursor's count must be decremented by the number we've deleted from the UI
878 */
Marc Blankb600a832012-02-16 09:20:18 -0800879 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800880 public int getCount() {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700881 if (mUnderlyingCursor == null) {
882 throw new IllegalStateException(
883 "getCount() on disabled cursor: " + mName + "(" + qUri + ")");
884 }
885 return mUnderlyingCursor.getCount() - mDeletedCount;
Marc Blankc8a99422012-01-19 14:27:47 -0800886 }
887
Marc Blankb600a832012-02-16 09:20:18 -0800888 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800889 public boolean moveToFirst() {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700890 if (mUnderlyingCursor == null) {
891 throw new IllegalStateException(
892 "moveToFirst() on disabled cursor: " + mName + "(" + qUri + ")");
893 }
894 mUnderlyingCursor.moveToPosition(-1);
Marc Blankc8a99422012-01-19 14:27:47 -0800895 mPosition = -1;
896 return moveToNext();
897 }
898
Marc Blankb600a832012-02-16 09:20:18 -0800899 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800900 public boolean moveToPosition(int pos) {
Paul Westbrookbf232c32012-04-18 03:17:41 -0700901 if (mUnderlyingCursor == null) {
902 throw new IllegalStateException(
903 "moveToPosition() on disabled cursor: " + mName + "(" + qUri + ")");
904 }
Marc Blank7ffbaaa2012-07-30 16:19:19 -0700905 // Handle the "move to first" case before anything else; moveToPosition(0) in an empty
906 // SQLiteCursor moves the position to 0 when returning false, which we will mirror.
907 // But we don't want to return true on a subsequent "move to first", which we would if we
908 // check pos vs mPosition first
mindyp35f6bba2012-09-25 17:39:04 -0700909 if (mUnderlyingCursor.getPosition() == -1) {
mindyp5c1d8352012-11-05 10:12:44 -0800910 LogUtils.i(LOG_TAG, "*** Underlying cursor position is -1 asking to move from %d to %d",
mindyp35f6bba2012-09-25 17:39:04 -0700911 mPosition, pos);
912 }
Marc Blank7ffbaaa2012-07-30 16:19:19 -0700913 if (pos == 0) {
914 return moveToFirst();
Paul Westbrookc84d2512012-09-26 00:18:28 -0700915 } else if (pos < 0) {
916 mPosition = -1;
917 mUnderlyingCursor.moveToPosition(mPosition);
918 return false;
Marc Blank7ffbaaa2012-07-30 16:19:19 -0700919 } else if (pos == mPosition) {
Marc Blank9735cdc2012-09-22 17:17:57 -0700920 // Return false if we're past the end of the cursor
921 return pos < getCount();
Marc Blank7ffbaaa2012-07-30 16:19:19 -0700922 } else if (pos > mPosition) {
Marc Blankc8a99422012-01-19 14:27:47 -0800923 while (pos > mPosition) {
924 if (!moveToNext()) {
925 return false;
926 }
927 }
928 return true;
Marc Blank18a97172012-08-22 14:59:10 -0700929 } else if ((pos >= 0) && (mPosition - pos) > pos) {
Marc Blanke5884c72012-08-18 10:03:59 -0700930 // Optimization if it's easier to move forward to position instead of backward
931 // STOPSHIP (Remove logging)
mindyp5c1d8352012-11-05 10:12:44 -0800932 LogUtils.i(LOG_TAG, "*** Move from %d to %d, starting from first", mPosition, pos);
Marc Blanke5884c72012-08-18 10:03:59 -0700933 moveToFirst();
934 return moveToPosition(pos);
Marc Blankc8a99422012-01-19 14:27:47 -0800935 } else {
936 while (pos < mPosition) {
937 if (!moveToPrevious()) {
938 return false;
939 }
940 }
941 return true;
942 }
943 }
944
Marc Blank93b3a152012-04-11 15:53:19 -0700945 /**
946 * Make sure mPosition is correct after locally deleting/undeleting items
947 */
948 private void recalibratePosition() {
Paul Westbrookc84d2512012-09-26 00:18:28 -0700949 final int pos = mPosition;
Marc Blank93b3a152012-04-11 15:53:19 -0700950 moveToFirst();
951 moveToPosition(pos);
952 }
953
Marc Blankb600a832012-02-16 09:20:18 -0800954 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800955 public boolean moveToLast() {
956 throw new UnsupportedOperationException("moveToLast unsupported!");
957 }
958
Marc Blankb600a832012-02-16 09:20:18 -0800959 @Override
Marc Blankc8a99422012-01-19 14:27:47 -0800960 public boolean move(int offset) {
961 throw new UnsupportedOperationException("move unsupported!");
962 }
963
964 /**
965 * We need to override all of the getters to make sure they look at cached values before using
966 * the values in the underlying cursor
967 */
968 @Override
969 public double getDouble(int columnIndex) {
970 Object obj = getCachedValue(columnIndex);
971 if (obj != null) return (Double)obj;
Paul Westbrookbf232c32012-04-18 03:17:41 -0700972 return mUnderlyingCursor.getDouble(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800973 }
974
975 @Override
976 public float getFloat(int columnIndex) {
977 Object obj = getCachedValue(columnIndex);
978 if (obj != null) return (Float)obj;
Paul Westbrookbf232c32012-04-18 03:17:41 -0700979 return mUnderlyingCursor.getFloat(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800980 }
981
982 @Override
983 public int getInt(int columnIndex) {
984 Object obj = getCachedValue(columnIndex);
985 if (obj != null) return (Integer)obj;
Paul Westbrookbf232c32012-04-18 03:17:41 -0700986 return mUnderlyingCursor.getInt(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800987 }
988
989 @Override
990 public long getLong(int columnIndex) {
Marc Blanke1d1b072012-04-13 17:29:16 -0700991 Object obj = getCachedValue(columnIndex);
992 if (obj != null) return (Long)obj;
Paul Westbrookbf232c32012-04-18 03:17:41 -0700993 return mUnderlyingCursor.getLong(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -0800994 }
995
996 @Override
997 public short getShort(int columnIndex) {
998 Object obj = getCachedValue(columnIndex);
999 if (obj != null) return (Short)obj;
Paul Westbrookbf232c32012-04-18 03:17:41 -07001000 return mUnderlyingCursor.getShort(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -08001001 }
1002
1003 @Override
1004 public String getString(int columnIndex) {
1005 // If we're asking for the Uri for the conversation list, we return a forwarding URI
1006 // so that we can intercept update/delete and handle it ourselves
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -08001007 if (columnIndex == URI_COLUMN_INDEX) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001008 Uri uri = Uri.parse(mUnderlyingCursor.getString(columnIndex));
Marc Blankc8a99422012-01-19 14:27:47 -08001009 return uriToCachingUriString(uri);
1010 }
1011 Object obj = getCachedValue(columnIndex);
1012 if (obj != null) return (String)obj;
Paul Westbrookbf232c32012-04-18 03:17:41 -07001013 return mUnderlyingCursor.getString(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -08001014 }
1015
1016 @Override
1017 public byte[] getBlob(int columnIndex) {
1018 Object obj = getCachedValue(columnIndex);
1019 if (obj != null) return (byte[])obj;
Paul Westbrookbf232c32012-04-18 03:17:41 -07001020 return mUnderlyingCursor.getBlob(columnIndex);
Marc Blankc8a99422012-01-19 14:27:47 -08001021 }
1022
1023 /**
1024 * Observer of changes to underlying data
1025 */
1026 private class CursorObserver extends ContentObserver {
Marc Blanke77d5262012-08-19 17:58:24 -07001027 public CursorObserver(Handler handler) {
1028 super(handler);
Marc Blankc8a99422012-01-19 14:27:47 -08001029 }
1030
1031 @Override
1032 public void onChange(boolean selfChange) {
1033 // If we're here, then something outside of the UI has changed the data, and we
Marc Blanke77d5262012-08-19 17:58:24 -07001034 // must query the underlying provider for that data;
Marc Blankc8a99422012-01-19 14:27:47 -08001035 ConversationCursor.this.underlyingChanged();
1036 }
1037 }
1038
1039 /**
1040 * ConversationProvider is the ContentProvider for our forwarding Uri's; it passes queries
1041 * and inserts directly, and caches updates/deletes before passing them through. The caching
1042 * will cause a redraw of the list with updated values.
1043 */
Paul Westbrook77177b12012-02-07 15:23:42 -08001044 public abstract static class ConversationProvider extends ContentProvider {
1045 public static String AUTHORITY;
Vikram Aggarwal7460a1c2012-12-28 15:58:58 -08001046 private ContentResolver mResolver;
Paul Westbrook77177b12012-02-07 15:23:42 -08001047
1048 /**
Vikram Aggarwal6a621462012-04-02 14:42:40 -07001049 * Allows the implementing provider to specify the authority that should be used.
Paul Westbrook77177b12012-02-07 15:23:42 -08001050 */
1051 protected abstract String getAuthority();
Marc Blank8d69d4e2012-01-25 12:04:28 -08001052
Marc Blankc8a99422012-01-19 14:27:47 -08001053 @Override
1054 public boolean onCreate() {
Marc Blank8d69d4e2012-01-25 12:04:28 -08001055 sProvider = this;
Paul Westbrook77177b12012-02-07 15:23:42 -08001056 AUTHORITY = getAuthority();
Vikram Aggarwal7460a1c2012-12-28 15:58:58 -08001057 mResolver = getContext().getContentResolver();
Marc Blank8d69d4e2012-01-25 12:04:28 -08001058 return true;
Marc Blankc8a99422012-01-19 14:27:47 -08001059 }
1060
1061 @Override
1062 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
1063 String sortOrder) {
Vikram Aggarwal7460a1c2012-12-28 15:58:58 -08001064 return mResolver.query(
Marc Blankc8a99422012-01-19 14:27:47 -08001065 uriFromCachingUri(uri), projection, selection, selectionArgs, sortOrder);
1066 }
1067
1068 @Override
Marc Blankf892f0a2012-01-30 13:04:10 -08001069 public Uri insert(Uri uri, ContentValues values) {
1070 insertLocal(uri, values);
Vikram Aggarwal7460a1c2012-12-28 15:58:58 -08001071 return ProviderExecute.opInsert(mResolver, uri, values);
Marc Blankf892f0a2012-01-30 13:04:10 -08001072 }
1073
1074 @Override
1075 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001076 throw new IllegalStateException("Unexpected call to ConversationProvider.delete");
Marc Blankf892f0a2012-01-30 13:04:10 -08001077 }
1078
1079 @Override
1080 public int delete(Uri uri, String selection, String[] selectionArgs) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001081 throw new IllegalStateException("Unexpected call to ConversationProvider.delete");
Marc Blankf892f0a2012-01-30 13:04:10 -08001082 }
1083
1084 @Override
Marc Blankc8a99422012-01-19 14:27:47 -08001085 public String getType(Uri uri) {
1086 return null;
1087 }
1088
1089 /**
1090 * Quick and dirty class that executes underlying provider CRUD operations on a background
1091 * thread.
1092 */
1093 static class ProviderExecute implements Runnable {
1094 static final int DELETE = 0;
1095 static final int INSERT = 1;
1096 static final int UPDATE = 2;
1097
1098 final int mCode;
1099 final Uri mUri;
1100 final ContentValues mValues; //HEHEH
Vikram Aggarwal7460a1c2012-12-28 15:58:58 -08001101 final ContentResolver mResolver;
Marc Blankc8a99422012-01-19 14:27:47 -08001102
Vikram Aggarwal7460a1c2012-12-28 15:58:58 -08001103 ProviderExecute(int code, ContentResolver resolver, Uri uri, ContentValues values) {
Marc Blankc8a99422012-01-19 14:27:47 -08001104 mCode = code;
1105 mUri = uriFromCachingUri(uri);
1106 mValues = values;
Vikram Aggarwal7460a1c2012-12-28 15:58:58 -08001107 mResolver = resolver;
Marc Blankc8a99422012-01-19 14:27:47 -08001108 }
1109
Vikram Aggarwal7460a1c2012-12-28 15:58:58 -08001110 static Uri opInsert(ContentResolver resolver, Uri uri, ContentValues values) {
1111 ProviderExecute e = new ProviderExecute(INSERT, resolver, uri, values);
Marc Blank8d69d4e2012-01-25 12:04:28 -08001112 if (offUiThread()) return (Uri)e.go();
1113 new Thread(e).start();
1114 return null;
Marc Blankc8a99422012-01-19 14:27:47 -08001115 }
1116
Marc Blankc8a99422012-01-19 14:27:47 -08001117 @Override
1118 public void run() {
Marc Blank8d69d4e2012-01-25 12:04:28 -08001119 go();
1120 }
1121
1122 public Object go() {
Marc Blankc8a99422012-01-19 14:27:47 -08001123 switch(mCode) {
1124 case DELETE:
Vikram Aggarwal7460a1c2012-12-28 15:58:58 -08001125 return mResolver.delete(mUri, null, null);
Marc Blankc8a99422012-01-19 14:27:47 -08001126 case INSERT:
Vikram Aggarwal7460a1c2012-12-28 15:58:58 -08001127 return mResolver.insert(mUri, mValues);
Marc Blankc8a99422012-01-19 14:27:47 -08001128 case UPDATE:
Vikram Aggarwal7460a1c2012-12-28 15:58:58 -08001129 return mResolver.update(mUri, mValues, null, null);
Marc Blank8d69d4e2012-01-25 12:04:28 -08001130 default:
1131 return null;
Marc Blankc8a99422012-01-19 14:27:47 -08001132 }
1133 }
1134 }
1135
Marc Blank8d69d4e2012-01-25 12:04:28 -08001136 private void insertLocal(Uri uri, ContentValues values) {
1137 // Placeholder for now; there's no local insert
1138 }
1139
Marc Blank2596f002012-03-22 10:26:26 -07001140 private int mUndoSequence = 0;
1141 private ArrayList<Uri> mUndoDeleteUris = new ArrayList<Uri>();
1142
Marc Blanke1d1b072012-04-13 17:29:16 -07001143 void addToUndoSequence(Uri uri) {
Marc Blank2596f002012-03-22 10:26:26 -07001144 if (sSequence != mUndoSequence) {
1145 mUndoSequence = sSequence;
1146 mUndoDeleteUris.clear();
1147 }
1148 mUndoDeleteUris.add(uri);
1149 }
1150
1151 @VisibleForTesting
Paul Westbrookbf232c32012-04-18 03:17:41 -07001152 void deleteLocal(Uri uri, ConversationCursor conversationCursor) {
Marc Blanke1d1b072012-04-13 17:29:16 -07001153 String uriString = uriStringFromCachingUri(uri);
Paul Westbrookbf232c32012-04-18 03:17:41 -07001154 conversationCursor.cacheValue(uriString, DELETED_COLUMN, true);
Marc Blanke1d1b072012-04-13 17:29:16 -07001155 addToUndoSequence(uri);
1156 }
1157
1158 @VisibleForTesting
Paul Westbrookbf232c32012-04-18 03:17:41 -07001159 void undeleteLocal(Uri uri, ConversationCursor conversationCursor) {
Marc Blanke1d1b072012-04-13 17:29:16 -07001160 String uriString = uriStringFromCachingUri(uri);
Paul Westbrookbf232c32012-04-18 03:17:41 -07001161 conversationCursor.cacheValue(uriString, DELETED_COLUMN, false);
Marc Blank2596f002012-03-22 10:26:26 -07001162 }
1163
Paul Westbrookbf232c32012-04-18 03:17:41 -07001164 void setMostlyDead(Conversation conv, ConversationCursor conversationCursor) {
Marc Blanke1d1b072012-04-13 17:29:16 -07001165 Uri uri = conv.uri;
1166 String uriString = uriStringFromCachingUri(uri);
Paul Westbrookbf232c32012-04-18 03:17:41 -07001167 conversationCursor.setMostlyDead(uriString, conv);
Marc Blanke1d1b072012-04-13 17:29:16 -07001168 addToUndoSequence(uri);
Marc Blanke1d1b072012-04-13 17:29:16 -07001169 }
1170
Paul Westbrookbf232c32012-04-18 03:17:41 -07001171 void commitMostlyDead(Conversation conv, ConversationCursor conversationCursor) {
1172 conversationCursor.commitMostlyDead(conv);
Marc Blanke1d1b072012-04-13 17:29:16 -07001173 }
1174
Paul Westbrookbf232c32012-04-18 03:17:41 -07001175 boolean clearMostlyDead(Uri uri, ConversationCursor conversationCursor) {
Marc Blanke1d1b072012-04-13 17:29:16 -07001176 String uriString = uriStringFromCachingUri(uri);
Paul Westbrookbf232c32012-04-18 03:17:41 -07001177 return conversationCursor.clearMostlyDead(uriString);
Marc Blanke1d1b072012-04-13 17:29:16 -07001178 }
1179
Paul Westbrookbf232c32012-04-18 03:17:41 -07001180 public void undo(ConversationCursor conversationCursor) {
Marc Blank2596f002012-03-22 10:26:26 -07001181 if (sSequence == mUndoSequence) {
1182 for (Uri uri: mUndoDeleteUris) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001183 if (!clearMostlyDead(uri, conversationCursor)) {
1184 undeleteLocal(uri, conversationCursor);
Marc Blanke1d1b072012-04-13 17:29:16 -07001185 }
Marc Blank2596f002012-03-22 10:26:26 -07001186 }
1187 mUndoSequence = 0;
Paul Westbrookbf232c32012-04-18 03:17:41 -07001188 conversationCursor.recalibratePosition();
mindyp6b5b2992012-09-10 10:12:02 -07001189 // Notify listeners that there was a change to the underlying
1190 // cursor to add back in some items.
1191 conversationCursor.notifyDataChanged();
Marc Blank2596f002012-03-22 10:26:26 -07001192 }
Marc Blankc8a99422012-01-19 14:27:47 -08001193 }
1194
Marc Blank248b1b42012-02-07 13:43:02 -08001195 @VisibleForTesting
Paul Westbrookbf232c32012-04-18 03:17:41 -07001196 void updateLocal(Uri uri, ContentValues values, ConversationCursor conversationCursor) {
Mindy Pereiraf98b3182012-02-22 11:07:13 -08001197 if (values == null) {
1198 return;
1199 }
Marc Blanke1d1b072012-04-13 17:29:16 -07001200 String uriString = uriStringFromCachingUri(uri);
Marc Blankc8a99422012-01-19 14:27:47 -08001201 for (String columnName: values.keySet()) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001202 conversationCursor.cacheValue(uriString, columnName, values.get(columnName));
Marc Blankc8a99422012-01-19 14:27:47 -08001203 }
Marc Blank8d69d4e2012-01-25 12:04:28 -08001204 }
1205
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001206 public int apply(Collection<ConversationOperation> ops,
Paul Westbrookbf232c32012-04-18 03:17:41 -07001207 ConversationCursor conversationCursor) {
Marc Blank8d69d4e2012-01-25 12:04:28 -08001208 final HashMap<String, ArrayList<ContentProviderOperation>> batchMap =
1209 new HashMap<String, ArrayList<ContentProviderOperation>>();
Marc Blankb31ab5a2012-02-01 12:28:29 -08001210 // Increment sequence count
1211 sSequence++;
Marc Blank93b3a152012-04-11 15:53:19 -07001212
Marc Blankf892f0a2012-01-30 13:04:10 -08001213 // Execute locally and build CPO's for underlying provider
Marc Blank93b3a152012-04-11 15:53:19 -07001214 boolean recalibrateRequired = false;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001215 for (ConversationOperation op: ops) {
1216 Uri underlyingUri = uriFromCachingUri(op.mUri);
1217 String authority = underlyingUri.getAuthority();
1218 ArrayList<ContentProviderOperation> authOps = batchMap.get(authority);
1219 if (authOps == null) {
1220 authOps = new ArrayList<ContentProviderOperation>();
1221 batchMap.put(authority, authOps);
1222 }
Marc Blanke1d1b072012-04-13 17:29:16 -07001223 ContentProviderOperation cpo = op.execute(underlyingUri);
1224 if (cpo != null) {
1225 authOps.add(cpo);
1226 }
Marc Blank93b3a152012-04-11 15:53:19 -07001227 // Keep track of whether our operations require recalibrating the cursor position
1228 if (op.mRecalibrateRequired) {
1229 recalibrateRequired = true;
1230 }
Marc Blankf892f0a2012-01-30 13:04:10 -08001231 }
1232
Marc Blank93b3a152012-04-11 15:53:19 -07001233 // Recalibrate cursor position if required
1234 if (recalibrateRequired) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001235 conversationCursor.recalibratePosition();
Marc Blank93b3a152012-04-11 15:53:19 -07001236 }
Marc Blank958bf4d2012-04-05 18:10:55 -07001237
Marc Blankfde56a72012-07-19 17:18:21 -07001238 // Notify listeners that data has changed
1239 conversationCursor.notifyDataChanged();
1240
Marc Blankf892f0a2012-01-30 13:04:10 -08001241 // Send changes to underlying provider
Vikram Aggarwal08235422012-11-13 10:00:11 -08001242 final boolean notUiThread = offUiThread();
1243 for (final String authority: batchMap.keySet()) {
1244 final ArrayList<ContentProviderOperation> opList = batchMap.get(authority);
1245 if (notUiThread) {
1246 try {
Vikram Aggarwal7460a1c2012-12-28 15:58:58 -08001247 mResolver.applyBatch(authority, opList);
Vikram Aggarwal08235422012-11-13 10:00:11 -08001248 } catch (RemoteException e) {
1249 } catch (OperationApplicationException e) {
Marc Blank8d69d4e2012-01-25 12:04:28 -08001250 }
Vikram Aggarwal08235422012-11-13 10:00:11 -08001251 } else {
1252 new Thread(new Runnable() {
1253 @Override
1254 public void run() {
1255 try {
Vikram Aggarwal7460a1c2012-12-28 15:58:58 -08001256 mResolver.applyBatch(authority, opList);
Vikram Aggarwal08235422012-11-13 10:00:11 -08001257 } catch (RemoteException e) {
1258 } catch (OperationApplicationException e) {
1259 }
1260 }
1261 }).start();
Marc Blank8d69d4e2012-01-25 12:04:28 -08001262 }
1263 }
Marc Blank1b9efd92012-02-01 14:27:55 -08001264 return sSequence;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001265 }
1266 }
1267
Paul Westbrookbf232c32012-04-18 03:17:41 -07001268 void setMostlyDead(String uriString, Conversation conv) {
mindyp5c1d8352012-11-05 10:12:44 -08001269 LogUtils.i(LOG_TAG, "[Mostly dead, deferring: %s] ", uriString);
Paul Westbrookbf232c32012-04-18 03:17:41 -07001270 cacheValue(uriString,
1271 UIProvider.ConversationColumns.FLAGS, Conversation.FLAG_MOSTLY_DEAD);
1272 conv.convFlags |= Conversation.FLAG_MOSTLY_DEAD;
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -08001273 mMostlyDead.add(conv);
Paul Westbrookbf232c32012-04-18 03:17:41 -07001274 mDeferSync = true;
1275 }
1276
1277 void commitMostlyDead(Conversation conv) {
1278 conv.convFlags &= ~Conversation.FLAG_MOSTLY_DEAD;
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -08001279 mMostlyDead.remove(conv);
mindyp5c1d8352012-11-05 10:12:44 -08001280 LogUtils.i(LOG_TAG, "[All dead: %s]", conv.uri);
Vikram Aggarwal8d9313b2013-01-03 16:15:10 -08001281 if (mMostlyDead.isEmpty()) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001282 mDeferSync = false;
1283 checkNotifyUI();
1284 }
1285 }
1286
1287 boolean clearMostlyDead(String uriString) {
1288 Object val = getCachedValue(uriString,
1289 UIProvider.CONVERSATION_FLAGS_COLUMN);
1290 if (val != null) {
1291 int flags = ((Integer)val).intValue();
1292 if ((flags & Conversation.FLAG_MOSTLY_DEAD) != 0) {
1293 cacheValue(uriString, UIProvider.ConversationColumns.FLAGS,
1294 flags &= ~Conversation.FLAG_MOSTLY_DEAD);
1295 return true;
1296 }
1297 }
1298 return false;
1299 }
1300
1301
1302
1303
Marc Blank8d69d4e2012-01-25 12:04:28 -08001304 /**
1305 * ConversationOperation is the encapsulation of a ContentProvider operation to be performed
1306 * atomically as part of a "batch" operation.
1307 */
Paul Westbrookbf232c32012-04-18 03:17:41 -07001308 public class ConversationOperation {
Marc Blanke1d1b072012-04-13 17:29:16 -07001309 private static final int MOSTLY = 0x80;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001310 public static final int DELETE = 0;
1311 public static final int INSERT = 1;
1312 public static final int UPDATE = 2;
Mindy Pereiraf98b3182012-02-22 11:07:13 -08001313 public static final int ARCHIVE = 3;
Mindy Pereira830c00f2012-02-22 11:43:49 -08001314 public static final int MUTE = 4;
1315 public static final int REPORT_SPAM = 5;
Paul Westbrook77eee622012-07-10 13:41:57 -07001316 public static final int REPORT_NOT_SPAM = 6;
Paul Westbrook76b20622012-07-12 11:45:43 -07001317 public static final int REPORT_PHISHING = 7;
Paul Westbrookef362542012-08-27 14:53:32 -07001318 public static final int DISCARD_DRAFTS = 8;
Marc Blanke1d1b072012-04-13 17:29:16 -07001319 public static final int MOSTLY_ARCHIVE = MOSTLY | ARCHIVE;
1320 public static final int MOSTLY_DELETE = MOSTLY | DELETE;
Mindy Pereira06642fa2012-07-12 16:23:27 -07001321 public static final int MOSTLY_DESTRUCTIVE_UPDATE = MOSTLY | UPDATE;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001322
1323 private final int mType;
1324 private final Uri mUri;
Marc Blanke1d1b072012-04-13 17:29:16 -07001325 private final Conversation mConversation;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001326 private final ContentValues mValues;
Marc Blankce538182012-02-03 13:04:27 -08001327 // True if an updated item should be removed locally (from ConversationCursor)
Mindy Pereira30fd47b2012-03-09 09:24:00 -08001328 // This would be the case for a folder change in which the conversation is no longer
Marc Blankce538182012-02-03 13:04:27 -08001329 // in the folder represented by the ConversationCursor
1330 private final boolean mLocalDeleteOnUpdate;
Marc Blank93b3a152012-04-11 15:53:19 -07001331 // After execution, this indicates whether or not the operation requires recalibration of
1332 // the current cursor position (i.e. it removed or added items locally)
1333 private boolean mRecalibrateRequired = true;
Marc Blanke1d1b072012-04-13 17:29:16 -07001334 // Whether this item is already mostly dead
1335 private final boolean mMostlyDead;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001336
Marc Blankf892f0a2012-01-30 13:04:10 -08001337 public ConversationOperation(int type, Conversation conv) {
Andy Huang2c4e6dc2012-07-25 18:02:59 -07001338 this(type, conv, null);
Marc Blank8d69d4e2012-01-25 12:04:28 -08001339 }
1340
Andy Huang2c4e6dc2012-07-25 18:02:59 -07001341 public ConversationOperation(int type, Conversation conv, ContentValues values) {
Marc Blank8d69d4e2012-01-25 12:04:28 -08001342 mType = type;
Marc Blankc43bc0a2012-02-02 11:25:18 -08001343 mUri = conv.uri;
Marc Blanke1d1b072012-04-13 17:29:16 -07001344 mConversation = conv;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001345 mValues = values;
Marc Blankce538182012-02-03 13:04:27 -08001346 mLocalDeleteOnUpdate = conv.localDeleteOnUpdate;
Marc Blanke1d1b072012-04-13 17:29:16 -07001347 mMostlyDead = conv.isMostlyDead();
Marc Blank8d69d4e2012-01-25 12:04:28 -08001348 }
1349
1350 private ContentProviderOperation execute(Uri underlyingUri) {
Marc Blankb31ab5a2012-02-01 12:28:29 -08001351 Uri uri = underlyingUri.buildUpon()
Marc Blankdd10bc82012-02-01 19:10:46 -08001352 .appendQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER,
1353 Integer.toString(sSequence))
Marc Blankb31ab5a2012-02-01 12:28:29 -08001354 .build();
Marc Blanke1d1b072012-04-13 17:29:16 -07001355 ContentProviderOperation op = null;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001356 switch(mType) {
Marc Blank8d69d4e2012-01-25 12:04:28 -08001357 case UPDATE:
Marc Blankce538182012-02-03 13:04:27 -08001358 if (mLocalDeleteOnUpdate) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001359 sProvider.deleteLocal(mUri, ConversationCursor.this);
Marc Blankce538182012-02-03 13:04:27 -08001360 } else {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001361 sProvider.updateLocal(mUri, mValues, ConversationCursor.this);
Marc Blank93b3a152012-04-11 15:53:19 -07001362 mRecalibrateRequired = false;
Marc Blankce538182012-02-03 13:04:27 -08001363 }
Mindy Pereira06642fa2012-07-12 16:23:27 -07001364 if (!mMostlyDead) {
1365 op = ContentProviderOperation.newUpdate(uri)
1366 .withValues(mValues)
1367 .build();
1368 } else {
1369 sProvider.commitMostlyDead(mConversation, ConversationCursor.this);
1370 }
1371 break;
1372 case MOSTLY_DESTRUCTIVE_UPDATE:
1373 sProvider.setMostlyDead(mConversation, ConversationCursor.this);
1374 op = ContentProviderOperation.newUpdate(uri).withValues(mValues).build();
Andy Huang397621b2012-03-14 20:52:39 -07001375 break;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001376 case INSERT:
1377 sProvider.insertLocal(mUri, mValues);
Andy Huang397621b2012-03-14 20:52:39 -07001378 op = ContentProviderOperation.newInsert(uri)
Marc Blank8d69d4e2012-01-25 12:04:28 -08001379 .withValues(mValues).build();
Andy Huang397621b2012-03-14 20:52:39 -07001380 break;
Marc Blanke1d1b072012-04-13 17:29:16 -07001381 // Destructive actions below!
1382 // "Mostly" operations are reflected globally, but not locally, except to set
1383 // FLAG_MOSTLY_DEAD in the conversation itself
1384 case DELETE:
Paul Westbrookbf232c32012-04-18 03:17:41 -07001385 sProvider.deleteLocal(mUri, ConversationCursor.this);
Marc Blanke1d1b072012-04-13 17:29:16 -07001386 if (!mMostlyDead) {
1387 op = ContentProviderOperation.newDelete(uri).build();
1388 } else {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001389 sProvider.commitMostlyDead(mConversation, ConversationCursor.this);
Marc Blanke1d1b072012-04-13 17:29:16 -07001390 }
1391 break;
1392 case MOSTLY_DELETE:
Paul Westbrookbf232c32012-04-18 03:17:41 -07001393 sProvider.setMostlyDead(mConversation,ConversationCursor.this);
Marc Blanke1d1b072012-04-13 17:29:16 -07001394 op = ContentProviderOperation.newDelete(uri).build();
1395 break;
Mindy Pereiraf98b3182012-02-22 11:07:13 -08001396 case ARCHIVE:
Paul Westbrookbf232c32012-04-18 03:17:41 -07001397 sProvider.deleteLocal(mUri, ConversationCursor.this);
Marc Blanke1d1b072012-04-13 17:29:16 -07001398 if (!mMostlyDead) {
1399 // Create an update operation that represents archive
1400 op = ContentProviderOperation.newUpdate(uri).withValue(
1401 ConversationOperations.OPERATION_KEY,
1402 ConversationOperations.ARCHIVE)
1403 .build();
1404 } else {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001405 sProvider.commitMostlyDead(mConversation, ConversationCursor.this);
Marc Blanke1d1b072012-04-13 17:29:16 -07001406 }
1407 break;
1408 case MOSTLY_ARCHIVE:
Paul Westbrookbf232c32012-04-18 03:17:41 -07001409 sProvider.setMostlyDead(mConversation, ConversationCursor.this);
Paul Westbrook334e64a2012-02-23 13:26:35 -08001410 // Create an update operation that represents archive
Andy Huang397621b2012-03-14 20:52:39 -07001411 op = ContentProviderOperation.newUpdate(uri).withValue(
Paul Westbrook334e64a2012-02-23 13:26:35 -08001412 ConversationOperations.OPERATION_KEY, ConversationOperations.ARCHIVE)
1413 .build();
Andy Huang397621b2012-03-14 20:52:39 -07001414 break;
Mindy Pereira830c00f2012-02-22 11:43:49 -08001415 case MUTE:
Paul Westbrook334e64a2012-02-23 13:26:35 -08001416 if (mLocalDeleteOnUpdate) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001417 sProvider.deleteLocal(mUri, ConversationCursor.this);
Paul Westbrook334e64a2012-02-23 13:26:35 -08001418 }
1419
1420 // Create an update operation that represents mute
Andy Huang397621b2012-03-14 20:52:39 -07001421 op = ContentProviderOperation.newUpdate(uri).withValue(
Paul Westbrook334e64a2012-02-23 13:26:35 -08001422 ConversationOperations.OPERATION_KEY, ConversationOperations.MUTE)
1423 .build();
Andy Huang397621b2012-03-14 20:52:39 -07001424 break;
Mindy Pereira830c00f2012-02-22 11:43:49 -08001425 case REPORT_SPAM:
Paul Westbrook77eee622012-07-10 13:41:57 -07001426 case REPORT_NOT_SPAM:
Paul Westbrookbf232c32012-04-18 03:17:41 -07001427 sProvider.deleteLocal(mUri, ConversationCursor.this);
Paul Westbrook334e64a2012-02-23 13:26:35 -08001428
Paul Westbrook77eee622012-07-10 13:41:57 -07001429 final String operation = mType == REPORT_SPAM ?
1430 ConversationOperations.REPORT_SPAM :
1431 ConversationOperations.REPORT_NOT_SPAM;
1432
Paul Westbrook334e64a2012-02-23 13:26:35 -08001433 // Create an update operation that represents report spam
Andy Huang397621b2012-03-14 20:52:39 -07001434 op = ContentProviderOperation.newUpdate(uri).withValue(
Paul Westbrook77eee622012-07-10 13:41:57 -07001435 ConversationOperations.OPERATION_KEY, operation).build();
Andy Huang397621b2012-03-14 20:52:39 -07001436 break;
Paul Westbrook76b20622012-07-12 11:45:43 -07001437 case REPORT_PHISHING:
1438 sProvider.deleteLocal(mUri, ConversationCursor.this);
1439
Paul Westbrookef362542012-08-27 14:53:32 -07001440 // Create an update operation that represents report phishing
Paul Westbrook76b20622012-07-12 11:45:43 -07001441 op = ContentProviderOperation.newUpdate(uri).withValue(
1442 ConversationOperations.OPERATION_KEY,
1443 ConversationOperations.REPORT_PHISHING).build();
1444 break;
Paul Westbrookef362542012-08-27 14:53:32 -07001445 case DISCARD_DRAFTS:
1446 sProvider.deleteLocal(mUri, ConversationCursor.this);
1447
1448 // Create an update operation that represents discarding drafts
1449 op = ContentProviderOperation.newUpdate(uri).withValue(
1450 ConversationOperations.OPERATION_KEY,
1451 ConversationOperations.DISCARD_DRAFTS).build();
1452 break;
Marc Blank8d69d4e2012-01-25 12:04:28 -08001453 default:
1454 throw new UnsupportedOperationException(
1455 "No such ConversationOperation type: " + mType);
1456 }
Andy Huang397621b2012-03-14 20:52:39 -07001457
Andy Huang397621b2012-03-14 20:52:39 -07001458 return op;
Marc Blankc8a99422012-01-19 14:27:47 -08001459 }
1460 }
Marc Blank97bca7b2012-01-24 11:17:00 -08001461
1462 /**
1463 * For now, a single listener can be associated with the cursor, and for now we'll just
1464 * notify on deletions
1465 */
1466 public interface ConversationListener {
Marc Blankbec51152012-03-22 19:27:34 -07001467 /**
1468 * Data in the underlying provider has changed; a refresh is required to sync up
1469 */
Marc Blank48eba7a2012-01-27 16:16:19 -08001470 public void onRefreshRequired();
Marc Blankbec51152012-03-22 19:27:34 -07001471 /**
1472 * We've completed a requested refresh of the underlying cursor
1473 */
Marc Blank48eba7a2012-01-27 16:16:19 -08001474 public void onRefreshReady();
Marc Blankbec51152012-03-22 19:27:34 -07001475 /**
1476 * The data underlying the cursor has changed; the UI should redraw the list
1477 */
1478 public void onDataSetChanged();
Marc Blank48eba7a2012-01-27 16:16:19 -08001479 }
1480
1481 @Override
1482 public boolean isFirst() {
1483 throw new UnsupportedOperationException();
1484 }
1485
1486 @Override
1487 public boolean isLast() {
1488 throw new UnsupportedOperationException();
1489 }
1490
1491 @Override
1492 public boolean isBeforeFirst() {
1493 throw new UnsupportedOperationException();
1494 }
1495
1496 @Override
1497 public boolean isAfterLast() {
1498 throw new UnsupportedOperationException();
1499 }
1500
1501 @Override
1502 public int getColumnIndex(String columnName) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001503 return mUnderlyingCursor.getColumnIndex(columnName);
Marc Blank48eba7a2012-01-27 16:16:19 -08001504 }
1505
1506 @Override
1507 public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001508 return mUnderlyingCursor.getColumnIndexOrThrow(columnName);
Marc Blank48eba7a2012-01-27 16:16:19 -08001509 }
1510
1511 @Override
1512 public String getColumnName(int columnIndex) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001513 return mUnderlyingCursor.getColumnName(columnIndex);
Marc Blank48eba7a2012-01-27 16:16:19 -08001514 }
1515
1516 @Override
1517 public String[] getColumnNames() {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001518 return mUnderlyingCursor.getColumnNames();
Marc Blank48eba7a2012-01-27 16:16:19 -08001519 }
1520
1521 @Override
1522 public int getColumnCount() {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001523 return mUnderlyingCursor.getColumnCount();
Marc Blank48eba7a2012-01-27 16:16:19 -08001524 }
1525
1526 @Override
1527 public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
1528 throw new UnsupportedOperationException();
1529 }
1530
1531 @Override
1532 public int getType(int columnIndex) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001533 return mUnderlyingCursor.getType(columnIndex);
Marc Blank48eba7a2012-01-27 16:16:19 -08001534 }
1535
1536 @Override
1537 public boolean isNull(int columnIndex) {
1538 throw new UnsupportedOperationException();
1539 }
1540
1541 @Override
1542 public void deactivate() {
1543 throw new UnsupportedOperationException();
1544 }
1545
1546 @Override
1547 public boolean isClosed() {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001548 return mUnderlyingCursor == null || mUnderlyingCursor.isClosed();
Marc Blank48eba7a2012-01-27 16:16:19 -08001549 }
1550
1551 @Override
1552 public void registerContentObserver(ContentObserver observer) {
Andy Huang397621b2012-03-14 20:52:39 -07001553 // Nope. We never notify of underlying changes on this channel, since the cursor watches
1554 // internally and offers onRefreshRequired/onRefreshReady to accomplish the same thing.
Marc Blank48eba7a2012-01-27 16:16:19 -08001555 }
1556
1557 @Override
1558 public void unregisterContentObserver(ContentObserver observer) {
Andy Huang397621b2012-03-14 20:52:39 -07001559 // See above.
Marc Blank48eba7a2012-01-27 16:16:19 -08001560 }
1561
1562 @Override
1563 public void registerDataSetObserver(DataSetObserver observer) {
Andy Huang2c4e6dc2012-07-25 18:02:59 -07001564 // Nope. We use ConversationListener to accomplish this.
Marc Blank48eba7a2012-01-27 16:16:19 -08001565 }
1566
1567 @Override
1568 public void unregisterDataSetObserver(DataSetObserver observer) {
Andy Huang2c4e6dc2012-07-25 18:02:59 -07001569 // See above.
Marc Blank48eba7a2012-01-27 16:16:19 -08001570 }
1571
1572 @Override
1573 public void setNotificationUri(ContentResolver cr, Uri uri) {
1574 throw new UnsupportedOperationException();
1575 }
1576
1577 @Override
1578 public boolean getWantsAllOnMoveCalls() {
1579 throw new UnsupportedOperationException();
1580 }
1581
1582 @Override
1583 public Bundle getExtras() {
Mindy Pereira70a70c92012-08-02 08:39:45 -07001584 return mUnderlyingCursor != null ? mUnderlyingCursor.getExtras() : Bundle.EMPTY;
Marc Blank48eba7a2012-01-27 16:16:19 -08001585 }
1586
1587 @Override
1588 public Bundle respond(Bundle extras) {
Paul Westbrook606a6a12012-04-24 21:40:31 -07001589 if (mUnderlyingCursor != null) {
1590 return mUnderlyingCursor.respond(extras);
1591 }
1592 return Bundle.EMPTY;
Marc Blank48eba7a2012-01-27 16:16:19 -08001593 }
1594
1595 @Override
1596 public boolean requery() {
1597 return true;
Marc Blank97bca7b2012-01-24 11:17:00 -08001598 }
Paul Westbrookbf232c32012-04-18 03:17:41 -07001599
1600 // Below are methods that update Conversation data (update/delete)
1601
1602 public int updateBoolean(Context context, Conversation conversation, String columnName,
1603 boolean value) {
1604 return updateBoolean(context, Arrays.asList(conversation), columnName, value);
1605 }
1606
1607 /**
1608 * Update an integer column for a group of conversations (see updateValues below)
1609 */
1610 public int updateInt(Context context, Collection<Conversation> conversations,
1611 String columnName, int value) {
1612 ContentValues cv = new ContentValues();
1613 cv.put(columnName, value);
1614 return updateValues(context, conversations, cv);
1615 }
1616
1617 /**
1618 * Update a string column for a group of conversations (see updateValues below)
1619 */
1620 public int updateBoolean(Context context, Collection<Conversation> conversations,
1621 String columnName, boolean value) {
1622 ContentValues cv = new ContentValues();
1623 cv.put(columnName, value);
1624 return updateValues(context, conversations, cv);
1625 }
1626
1627 /**
1628 * Update a string column for a group of conversations (see updateValues below)
1629 */
1630 public int updateString(Context context, Collection<Conversation> conversations,
1631 String columnName, String value) {
Paul Westbrook26746eb2012-12-06 14:44:01 -08001632 return updateStrings(context, conversations, new String[]{
1633 columnName
1634 }, new String[]{
1635 value
Mindy Pereiraebdfd982012-07-13 09:22:01 -07001636 });
1637 }
1638
1639 /**
1640 * Update a string columns for a group of conversations (see updateValues below)
1641 */
1642 public int updateStrings(Context context, Collection<Conversation> conversations,
mindyp389f0b22012-08-29 11:12:54 -07001643 String columnName, ArrayList<String> values) {
1644 ArrayList<ConversationOperation> operations = new ArrayList<ConversationOperation>();
1645 int i = 0;
1646 ContentValues cv = new ContentValues();
1647 for (Conversation c : conversations) {
1648 cv.put(columnName, values.get(i));
1649 operations.add(getOperationForConversation(c, ConversationOperation.UPDATE, cv));
1650 }
1651 return apply(context, operations);
1652 }
1653
1654 /**
1655 * Update a string columns for a group of conversations (see updateValues below)
1656 */
1657 public int updateStrings(Context context, Collection<Conversation> conversations,
Mindy Pereiraebdfd982012-07-13 09:22:01 -07001658 String[] columnNames, String[] values) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001659 ContentValues cv = new ContentValues();
Mindy Pereiraebdfd982012-07-13 09:22:01 -07001660 for (int i = 0; i < columnNames.length; i++) {
1661 cv.put(columnNames[i], values[i]);
1662 }
Paul Westbrookbf232c32012-04-18 03:17:41 -07001663 return updateValues(context, conversations, cv);
1664 }
1665
Paul Westbrookbf232c32012-04-18 03:17:41 -07001666 /**
1667 * Update a boolean column for a group of conversations, immediately in the UI and in a single
1668 * transaction in the underlying provider
Paul Westbrookbf232c32012-04-18 03:17:41 -07001669 * @param context the caller's context
Andy Huang2c4e6dc2012-07-25 18:02:59 -07001670 * @param conversations a collection of conversations
1671 * @param values the data to update
Paul Westbrookbf232c32012-04-18 03:17:41 -07001672 * @return the sequence number of the operation (for undo)
1673 */
Mindy Pereira6c2663d2012-07-20 15:37:29 -07001674 public int updateValues(Context context, Collection<Conversation> conversations,
Paul Westbrookbf232c32012-04-18 03:17:41 -07001675 ContentValues values) {
1676 return apply(context,
1677 getOperationsForConversations(conversations, ConversationOperation.UPDATE, values));
1678 }
1679
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001680 /**
1681 * Apply many operations in a single batch transaction.
1682 * @param context the caller's context
1683 * @param op the collection of operations obtained through successive calls to
1684 * {@link #getOperationForConversation(Conversation, int, ContentValues)}.
1685 * @return the sequence number of the operation (for undo)
1686 */
1687 public int updateBulkValues(Context context, Collection<ConversationOperation> op) {
1688 return apply(context, op);
1689 }
1690
Paul Westbrookbf232c32012-04-18 03:17:41 -07001691 private ArrayList<ConversationOperation> getOperationsForConversations(
Andy Huang2c4e6dc2012-07-25 18:02:59 -07001692 Collection<Conversation> conversations, int type, ContentValues values) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001693 final ArrayList<ConversationOperation> ops = Lists.newArrayList();
1694 for (Conversation conv: conversations) {
mindyp389f0b22012-08-29 11:12:54 -07001695 ops.add(getOperationForConversation(conv, type, values));
Paul Westbrookbf232c32012-04-18 03:17:41 -07001696 }
1697 return ops;
1698 }
1699
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001700 public ConversationOperation getOperationForConversation(Conversation conv, int type,
mindyp389f0b22012-08-29 11:12:54 -07001701 ContentValues values) {
1702 return new ConversationOperation(type, conv, values);
1703 }
1704
mindyp5cc0ab22012-12-11 08:47:35 -08001705 public void addFolderUpdates(ArrayList<Uri> folderUris, ArrayList<Boolean> add,
Paul Westbrook26746eb2012-12-06 14:44:01 -08001706 ContentValues values) {
mindypcb0b30e2012-11-30 10:16:35 -08001707 ArrayList<String> folders = new ArrayList<String>();
1708 for (int i = 0; i < folderUris.size(); i++) {
1709 folders.add(folderUris.get(i).buildUpon().appendPath(add.get(i) + "").toString());
1710 }
1711 values.put(ConversationOperations.FOLDERS_UPDATED,
1712 TextUtils.join(ConversationOperations.FOLDERS_UPDATED_SPLIT_PATTERN, folders));
Paul Westbrook26746eb2012-12-06 14:44:01 -08001713 }
1714
mindyp5cc0ab22012-12-11 08:47:35 -08001715 public void addTargetFolders(Collection<Folder> targetFolders, ContentValues values) {
Andy Huangb2033d82012-12-07 19:30:57 -08001716 values.put(Conversation.UPDATE_FOLDER_COLUMN, FolderList.copyOf(targetFolders).toBlob());
Paul Westbrook26746eb2012-12-06 14:44:01 -08001717 }
1718
1719 public ConversationOperation getConversationFolderOperation(Conversation conv,
1720 ArrayList<Uri> folderUris, ArrayList<Boolean> add, Collection<Folder> targetFolders) {
mindyp5cc0ab22012-12-11 08:47:35 -08001721 return getConversationFolderOperation(conv, folderUris, add, targetFolders,
1722 new ContentValues());
1723 }
1724
1725 public ConversationOperation getConversationFolderOperation(Conversation conv,
1726 ArrayList<Uri> folderUris, ArrayList<Boolean> add, Collection<Folder> targetFolders,
1727 ContentValues values) {
Paul Westbrook26746eb2012-12-06 14:44:01 -08001728 addFolderUpdates(folderUris, add, values);
Paul Westbrook26746eb2012-12-06 14:44:01 -08001729 addTargetFolders(targetFolders, values);
1730 return getOperationForConversation(conv, ConversationOperation.UPDATE, values);
mindypcb0b30e2012-11-30 10:16:35 -08001731 }
1732
Paul Westbrookbf232c32012-04-18 03:17:41 -07001733 /**
1734 * Delete a single conversation
1735 * @param context the caller's context
1736 * @return the sequence number of the operation (for undo)
1737 */
1738 public int delete(Context context, Conversation conversation) {
1739 ArrayList<Conversation> conversations = new ArrayList<Conversation>();
1740 conversations.add(conversation);
1741 return delete(context, conversations);
1742 }
1743
1744 /**
1745 * Delete a single conversation
1746 * @param context the caller's context
1747 * @return the sequence number of the operation (for undo)
1748 */
1749 public int mostlyArchive(Context context, Conversation conversation) {
1750 ArrayList<Conversation> conversations = new ArrayList<Conversation>();
1751 conversations.add(conversation);
1752 return archive(context, conversations);
1753 }
1754
1755 /**
1756 * Delete a single conversation
1757 * @param context the caller's context
1758 * @return the sequence number of the operation (for undo)
1759 */
1760 public int mostlyDelete(Context context, Conversation conversation) {
1761 ArrayList<Conversation> conversations = new ArrayList<Conversation>();
1762 conversations.add(conversation);
1763 return delete(context, conversations);
1764 }
1765
Paul Westbrookbf232c32012-04-18 03:17:41 -07001766 // Convenience methods
Vikram Aggarwalcc754b12012-08-30 14:04:21 -07001767 private int apply(Context context, Collection<ConversationOperation> operations) {
Paul Westbrookbf232c32012-04-18 03:17:41 -07001768 return sProvider.apply(operations, this);
1769 }
1770
1771 private void undoLocal() {
1772 sProvider.undo(this);
1773 }
1774
1775 public void undo(final Context context, final Uri undoUri) {
1776 new Thread(new Runnable() {
1777 @Override
1778 public void run() {
1779 Cursor c = context.getContentResolver().query(undoUri, UIProvider.UNDO_PROJECTION,
1780 null, null, null);
1781 if (c != null) {
1782 c.close();
1783 }
1784 }
1785 }).start();
1786 undoLocal();
1787 }
1788
1789 /**
1790 * Delete a group of conversations immediately in the UI and in a single transaction in the
1791 * underlying provider. See applyAction for argument descriptions
1792 */
1793 public int delete(Context context, Collection<Conversation> conversations) {
1794 return applyAction(context, conversations, ConversationOperation.DELETE);
1795 }
1796
1797 /**
1798 * As above, for archive
1799 */
1800 public int archive(Context context, Collection<Conversation> conversations) {
1801 return applyAction(context, conversations, ConversationOperation.ARCHIVE);
1802 }
1803
1804 /**
1805 * As above, for mute
1806 */
1807 public int mute(Context context, Collection<Conversation> conversations) {
1808 return applyAction(context, conversations, ConversationOperation.MUTE);
1809 }
1810
1811 /**
1812 * As above, for report spam
1813 */
1814 public int reportSpam(Context context, Collection<Conversation> conversations) {
1815 return applyAction(context, conversations, ConversationOperation.REPORT_SPAM);
1816 }
1817
1818 /**
Paul Westbrook77eee622012-07-10 13:41:57 -07001819 * As above, for report not spam
1820 */
1821 public int reportNotSpam(Context context, Collection<Conversation> conversations) {
1822 return applyAction(context, conversations, ConversationOperation.REPORT_NOT_SPAM);
1823 }
1824
1825 /**
Paul Westbrook76b20622012-07-12 11:45:43 -07001826 * As above, for report phishing
1827 */
1828 public int reportPhishing(Context context, Collection<Conversation> conversations) {
1829 return applyAction(context, conversations, ConversationOperation.REPORT_PHISHING);
1830 }
1831
1832 /**
Paul Westbrookef362542012-08-27 14:53:32 -07001833 * Discard the drafts in the specified conversations
1834 */
1835 public int discardDrafts(Context context, Collection<Conversation> conversations) {
1836 return applyAction(context, conversations, ConversationOperation.DISCARD_DRAFTS);
1837 }
1838
1839 /**
Paul Westbrookbf232c32012-04-18 03:17:41 -07001840 * As above, for mostly archive
1841 */
1842 public int mostlyArchive(Context context, Collection<Conversation> conversations) {
1843 return applyAction(context, conversations, ConversationOperation.MOSTLY_ARCHIVE);
1844 }
1845
1846 /**
1847 * As above, for mostly delete
1848 */
1849 public int mostlyDelete(Context context, Collection<Conversation> conversations) {
1850 return applyAction(context, conversations, ConversationOperation.MOSTLY_DELETE);
1851 }
1852
1853 /**
Mindy Pereiraebdfd982012-07-13 09:22:01 -07001854 * As above, for mostly destructive updates.
1855 */
1856 public int mostlyDestructiveUpdate(Context context, Collection<Conversation> conversations,
Andy Huangb2033d82012-12-07 19:30:57 -08001857 ContentValues values) {
Mindy Pereira06642fa2012-07-12 16:23:27 -07001858 return apply(
1859 context,
1860 getOperationsForConversations(conversations,
Andy Huangb2033d82012-12-07 19:30:57 -08001861 ConversationOperation.MOSTLY_DESTRUCTIVE_UPDATE, values));
Mindy Pereira06642fa2012-07-12 16:23:27 -07001862 }
1863
1864 /**
Paul Westbrookbf232c32012-04-18 03:17:41 -07001865 * Convenience method for performing an operation on a group of conversations
1866 * @param context the caller's context
1867 * @param conversations the conversations to be affected
1868 * @param opAction the action to take
1869 * @return the sequence number of the operation applied in CC
1870 */
1871 private int applyAction(Context context, Collection<Conversation> conversations,
1872 int opAction) {
1873 ArrayList<ConversationOperation> ops = Lists.newArrayList();
1874 for (Conversation conv: conversations) {
1875 ConversationOperation op =
1876 new ConversationOperation(opAction, conv);
1877 ops.add(op);
1878 }
1879 return apply(context, ops);
1880 }
1881
Vikram Aggarwal81a4f082012-09-28 09:19:04 -07001882 /**
1883 * Do not make this method dependent on the internal mechanism of the cursor.
1884 * Currently just calls the parent implementation. If this is ever overriden, take care to
1885 * ensure that two references map to the same hashcode. If
1886 * ConversationCursor first == ConversationCursor second,
1887 * then
1888 * first.hashCode() == second.hashCode().
1889 * The {@link ConversationListFragment} relies on this behavior of
1890 * {@link ConversationCursor#hashCode()} to avoid storing dangerous references to the cursor.
1891 * {@inheritDoc}
1892 */
1893 @Override
1894 public int hashCode() {
1895 return super.hashCode();
1896 }
Vikram Aggarwal55d664a2012-11-13 10:25:56 -08001897}