ConversationCursor.java revision 09b32383b951afe1dee7845f062fcf8050601f61
12c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent/******************************************************************************* 22c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * Copyright (C) 2012 Google Inc. 32c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * Licensed to The Android Open Source Project. 42c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * 52c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * Licensed under the Apache License, Version 2.0 (the "License"); 62c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * you may not use this file except in compliance with the License. 72c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * You may obtain a copy of the License at 82c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * 92c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * http://www.apache.org/licenses/LICENSE-2.0 102c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * 112c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * Unless required by applicable law or agreed to in writing, software 122c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * distributed under the License is distributed on an "AS IS" BASIS, 132c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 142c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * See the License for the specific language governing permissions and 152c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * limitations under the License. 162c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent *******************************************************************************/ 172c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent 182c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentpackage com.android.mail.browse; 192c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent 202c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport android.app.Activity; 212c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport android.content.ContentProvider; 222c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport android.content.ContentProviderOperation; 232c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport android.content.ContentResolver; 242c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport android.content.ContentValues; 252c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport android.content.OperationApplicationException; 262c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport android.database.CharArrayBuffer; 272c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport android.database.ContentObserver; 282c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport android.database.Cursor; 292c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport android.database.DataSetObservable; 302c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport android.database.DataSetObserver; 312c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport android.net.Uri; 322c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport android.os.Bundle; 332c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport android.os.Looper; 342c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport android.os.RemoteException; 352c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent 362c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport com.android.mail.providers.Conversation; 372c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport com.android.mail.providers.UIProvider; 382c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport com.android.mail.providers.UIProvider.ConversationOperations; 392c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport com.android.mail.utils.LogUtils; 402c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport com.google.common.annotations.VisibleForTesting; 412c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent 422c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport java.util.ArrayList; 432c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport java.util.HashMap; 442c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport java.util.Iterator; 452c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentimport java.util.List; 462c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent 472c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent/** 482c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * ConversationCursor is a wrapper around a conversation list cursor that provides update/delete 492c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * caching for quick UI response. This is effectively a singleton class, as the cache is 502c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * implemented as a static HashMap. 512c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent */ 522c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurentpublic final class ConversationCursor implements Cursor { 532c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static final String TAG = "ConversationCursor"; 542c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static final boolean DEBUG = true; // STOPSHIP Set to false before shipping 552c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent 562c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // The cursor instantiator's activity 572c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static Activity sActivity; 582c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // The cursor underlying the caching cursor 592c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent @VisibleForTesting 602c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent static Cursor sUnderlyingCursor; 612c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // The new cursor obtained via a requery 622c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static volatile Cursor sRequeryCursor; 632c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // A mapping from Uri to updated ContentValues 642c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static HashMap<String, ContentValues> sCacheMap = new HashMap<String, ContentValues>(); 652c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // Cache map lock (will be used only very briefly - few ms at most) 662c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static Object sCacheMapLock = new Object(); 672c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // A deleted row is indicated by the presence of DELETED_COLUMN in the cache map 682c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static final String DELETED_COLUMN = "__deleted__"; 692c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // An row cached during a requery is indicated by the presence of REQUERY_COLUMN in the map 702c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static final String REQUERY_COLUMN = "__requery__"; 712c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // A sentinel value for the "index" of the deleted column; it's an int that is otherwise invalid 722c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static final int DELETED_COLUMN_INDEX = -1; 732c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // Empty deletion list 742c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static final ArrayList<Integer> EMPTY_DELETION_LIST = new ArrayList<Integer>(); 752c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // The current conversation cursor 762c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static ConversationCursor sConversationCursor; 772c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // The index of the Uri whose data is reflected in the cached row 782c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // Updates/Deletes to this Uri are cached 792c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static int sUriColumnIndex; 802c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // The listeners registered for this cursor 812c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static ArrayList<ConversationListener> sListeners = 822c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent new ArrayList<ConversationListener>(); 832c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // The ConversationProvider instance 842c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent @VisibleForTesting 852c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent static ConversationProvider sProvider; 862c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // Set when we're in the middle of a refresh of the underlying cursor 872c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static boolean sRefreshInProgress = false; 882c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // Set when we've sent refreshReady() to listeners 892c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static boolean sRefreshReady = false; 902c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // Set when we've sent refreshRequired() to listeners 912c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static boolean sRefreshRequired = false; 922c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // Our sequence count (for changes sent to underlying provider) 932c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static int sSequence = 0; 942c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent 952c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // Column names for this cursor 962c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private final String[] mColumnNames; 972c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // The resolver for the cursor instantiator's context 982c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static ContentResolver mResolver; 992c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // An observer on the underlying cursor (so we can detect changes from outside the UI) 1002c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private final CursorObserver mCursorObserver; 1012c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // Whether our observer is currently registered with the underlying cursor 1022c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private boolean mCursorObserverRegistered = false; 1032c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent 1042c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // The current position of the cursor 1052c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private int mPosition = -1; 1062c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent 1072c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent /** 1082c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * Allow UI elements to subscribe to changes that other UI elements might make to this data. 1092c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * This short circuits the usual DB round-trip needed for data to propagate across disparate 1102c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * UI elements. 1112c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * <p> 1122c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * A UI element that receives a notification on this channel should just update its existing 1132c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent * view, and should not trigger a full refresh. 1142c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent */ 1152c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private final DataSetObservable mDataSetObservable = new DataSetObservable(); 1162c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent 1172c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // The number of cached deletions from this cursor (used to quickly generate an accurate count) 1182c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static int sDeletedCount = 0; 1192c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent 1202c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // Parameters passed to the underlying query 1212c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static Uri qUri; 1222c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private static String[] qProjection; 1232c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent 1242c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent private ConversationCursor(Cursor cursor, Activity activity, String messageListColumn) { 1252c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent sConversationCursor = this; 1262c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent // If we have an existing underlying cursor, make sure it's closed 1272c8e5cab3faa6d360e222b7a6c40a80083d021acEric Laurent if (sUnderlyingCursor != null) { 128 sUnderlyingCursor.close(); 129 } 130 sUnderlyingCursor = cursor; 131 sListeners.clear(); 132 sRefreshRequired = false; 133 sRefreshReady = false; 134 sRefreshInProgress = false; 135 mCursorObserver = new CursorObserver(); 136 resetCursor(null); 137 mColumnNames = cursor.getColumnNames(); 138 sUriColumnIndex = cursor.getColumnIndex(messageListColumn); 139 if (sUriColumnIndex < 0) { 140 throw new IllegalArgumentException("Cursor must include a message list column"); 141 } 142 } 143 144 /** 145 * Method to initiaze the ConversationCursor state before an instance is created 146 * This is needed to workaround the crash reported in bug 6185304 147 */ 148 public static void initialize(Activity activity) { 149 sActivity = activity; 150 mResolver = activity.getContentResolver(); 151 } 152 153 /** 154 * Create a ConversationCursor; this should be called by the ListActivity using that cursor 155 * @param activity the activity creating the cursor 156 * @param messageListColumn the column used for individual cursor items 157 * @param uri the query uri 158 * @param projection the query projecion 159 * @param selection the query selection 160 * @param selectionArgs the query selection args 161 * @param sortOrder the query sort order 162 * @return a ConversationCursor 163 */ 164 public static ConversationCursor create(Activity activity, String messageListColumn, Uri uri, 165 String[] projection, String selection, String[] selectionArgs, String sortOrder) { 166 sActivity = activity; 167 mResolver = activity.getContentResolver(); 168 if (selection != null || sortOrder != null) { 169 throw new IllegalArgumentException( 170 "Selection and sort order aren't allowed in ConversationCursors"); 171 } 172 synchronized (sCacheMapLock) { 173 // First, let's see if we already have a cursor 174 if (sConversationCursor != null) { 175 // If it's the same, just clean up 176 if (qUri.equals(uri) && !sRefreshRequired && !sRefreshInProgress) { 177 if (sRefreshReady) { 178 // If we already have a refresh ready, just sync() it 179 LogUtils.i(TAG, "Create: refreshed cursor ready, sync"); 180 } else { 181 // Position the cursor before the first item (as it would be if new), reset 182 // the cache, and return as new 183 LogUtils.i(TAG, "Create: cursor good, reset position and clear map"); 184 sConversationCursor.moveToPosition(-1); 185 sConversationCursor.mPosition = -1; 186 } 187 } else { 188 // We need a new query here; cancel any existing one, ensuring that a sync 189 // from another thread won't be stalled on the query 190 cancelRefresh(); 191 LogUtils.i(TAG, "Create: new query or refresh needed, query/sync"); 192 sRequeryCursor = doQuery(uri, projection); 193 sRefreshReady = true; 194 } 195 return sConversationCursor; 196 } 197 // Create new ConversationCursor 198 LogUtils.i(TAG, "Create: initial creation"); 199 return new ConversationCursor(doQuery(uri, projection), activity, messageListColumn); 200 } 201 } 202 203 private static Cursor doQuery(Uri uri, String[] projection) { 204 qUri = uri; 205 qProjection = projection; 206 if (mResolver == null) { 207 mResolver = sActivity.getContentResolver(); 208 } 209 return mResolver.query(qUri, qProjection, null, null, null); 210 } 211 212 /** 213 * Return whether the uri string (message list uri) is in the underlying cursor 214 * @param uriString the uri string we're looking for 215 * @return true if the uri string is in the cursor; false otherwise 216 */ 217 private boolean isInUnderlyingCursor(String uriString) { 218 sUnderlyingCursor.moveToPosition(-1); 219 while (sUnderlyingCursor.moveToNext()) { 220 if (uriString.equals(sUnderlyingCursor.getString(sUriColumnIndex))) { 221 return true; 222 } 223 } 224 return false; 225 } 226 227 static boolean offUiThread() { 228 return Looper.getMainLooper().getThread() != Thread.currentThread(); 229 } 230 231 /** 232 * Reset the cursor; this involves clearing out our cache map and resetting our various counts 233 * The cursor should be reset whenever we get fresh data from the underlying cursor. The cache 234 * is locked during the reset, which will block the UI, but for only a very short time 235 * (estimated at a few ms, but we can profile this; remember that the cache will usually 236 * be empty or have a few entries) 237 */ 238 private void resetCursor(Cursor newCursor) { 239 // Temporary, log time for reset 240 long startTime = System.currentTimeMillis(); 241 if (DEBUG) { 242 LogUtils.i(TAG, "[--resetCursor--]"); 243 } 244 synchronized (sCacheMapLock) { 245 // Walk through the cache. Here are the cases: 246 // 1) The entry isn't marked with REQUERY - remove it from the cache. If DELETED is 247 // set, decrement the deleted count 248 // 2) The REQUERY entry is still in the UP 249 // 2a) The REQUERY entry isn't DELETED; we're good, and the client change will remain 250 // (i.e. client wins, it's on its way to the UP) 251 // 2b) The REQUERY entry is DELETED; we're good (client change remains, it's on 252 // its way to the UP) 253 // 3) the REQUERY was deleted on the server (sheesh; this would be bizarre timing!) - 254 // we need to throw the item out of the cache 255 // So ... the only interesting case is #3, we need to look for remaining deleted items 256 // and see if they're still in the UP 257 Iterator<HashMap.Entry<String, ContentValues>> iter = sCacheMap.entrySet().iterator(); 258 while (iter.hasNext()) { 259 HashMap.Entry<String, ContentValues> entry = iter.next(); 260 ContentValues values = entry.getValue(); 261 if (values.containsKey(REQUERY_COLUMN) && isInUnderlyingCursor(entry.getKey())) { 262 // If we're in a requery and we're still around, remove the requery key 263 // We're good here, the cached change (delete/update) is on its way to UP 264 values.remove(REQUERY_COLUMN); 265 } else { 266 // Keep the deleted count up-to-date; remove the cache entry 267 if (values.containsKey(DELETED_COLUMN)) { 268 sDeletedCount--; 269 } 270 // Remove the entry 271 iter.remove(); 272 } 273 } 274 275 // Swap cursor 276 if (newCursor != null) { 277 close(); 278 sUnderlyingCursor = newCursor; 279 } 280 281 mPosition = -1; 282 sUnderlyingCursor.moveToPosition(mPosition); 283 if (!mCursorObserverRegistered) { 284 sUnderlyingCursor.registerContentObserver(mCursorObserver); 285 mCursorObserverRegistered = true; 286 } 287 sRefreshRequired = false; 288 } 289 LogUtils.i(TAG, "resetCache time: " + ((System.currentTimeMillis() - startTime)) + "ms"); 290 } 291 292 /** 293 * Add a listener for this cursor; we'll notify it when our data changes 294 */ 295 public void addListener(ConversationListener listener) { 296 synchronized (sListeners) { 297 if (!sListeners.contains(listener)) { 298 sListeners.add(listener); 299 } else { 300 LogUtils.i(TAG, "Ignoring duplicate add of listener"); 301 } 302 } 303 } 304 305 /** 306 * Remove a listener for this cursor 307 */ 308 public void removeListener(ConversationListener listener) { 309 synchronized(sListeners) { 310 sListeners.remove(listener); 311 } 312 } 313 314 /** 315 * Generate a forwarding Uri to ConversationProvider from an original Uri. We do this by 316 * changing the authority to ours, but otherwise leaving the Uri intact. 317 * NOTE: This won't handle query parameters, so the functionality will need to be added if 318 * parameters are used in the future 319 * @param uri the uri 320 * @return a forwarding uri to ConversationProvider 321 */ 322 private static String uriToCachingUriString (Uri uri) { 323 String provider = uri.getAuthority(); 324 return uri.getScheme() + "://" + ConversationProvider.AUTHORITY 325 + "/" + provider + uri.getPath(); 326 } 327 328 /** 329 * Regenerate the original Uri from a forwarding (ConversationProvider) Uri 330 * NOTE: See note above for uriToCachingUri 331 * @param uri the forwarding Uri 332 * @return the original Uri 333 */ 334 private static Uri uriFromCachingUri(Uri uri) { 335 String authority = uri.getAuthority(); 336 // Don't modify uri's that aren't ours 337 if (!authority.equals(ConversationProvider.AUTHORITY)) { 338 return uri; 339 } 340 List<String> path = uri.getPathSegments(); 341 Uri.Builder builder = new Uri.Builder().scheme(uri.getScheme()).authority(path.get(0)); 342 for (int i = 1; i < path.size(); i++) { 343 builder.appendPath(path.get(i)); 344 } 345 return builder.build(); 346 } 347 348 /** 349 * Cache a column name/value pair for a given Uri 350 * @param uriString the Uri for which the column name/value pair applies 351 * @param columnName the column name 352 * @param value the value to be cached 353 */ 354 private static void cacheValue(String uriString, String columnName, Object value) { 355 synchronized (sCacheMapLock) { 356 // Get the map for our uri 357 ContentValues map = sCacheMap.get(uriString); 358 // Create one if necessary 359 if (map == null) { 360 map = new ContentValues(); 361 sCacheMap.put(uriString, map); 362 } 363 // If we're caching a deletion, add to our count 364 if ((columnName == DELETED_COLUMN) && (map.get(columnName) == null)) { 365 sDeletedCount++; 366 if (DEBUG) { 367 LogUtils.i(TAG, "Deleted " + uriString); 368 } 369 } 370 // ContentValues has no generic "put", so we must test. For now, the only classes of 371 // values implemented are Boolean/Integer/String, though others are trivially added 372 if (value instanceof Boolean) { 373 map.put(columnName, ((Boolean) value).booleanValue() ? 1 : 0); 374 } else if (value instanceof Integer) { 375 map.put(columnName, (Integer) value); 376 } else if (value instanceof String) { 377 map.put(columnName, (String) value); 378 } else { 379 String cname = value.getClass().getName(); 380 throw new IllegalArgumentException("Value class not compatible with cache: " 381 + cname); 382 } 383 if (sRefreshInProgress) { 384 map.put(REQUERY_COLUMN, 1); 385 } 386 if (DEBUG && (columnName != DELETED_COLUMN)) { 387 LogUtils.i(TAG, "Caching value for " + uriString + ": " + columnName); 388 } 389 } 390 } 391 392 /** 393 * Get the cached value for the provided column; we special case -1 as the "deleted" column 394 * @param columnIndex the index of the column whose cached value we want to retrieve 395 * @return the cached value for this column, or null if there is none 396 */ 397 private Object getCachedValue(int columnIndex) { 398 String uri = sUnderlyingCursor.getString(sUriColumnIndex); 399 ContentValues uriMap = sCacheMap.get(uri); 400 if (uriMap != null) { 401 String columnName; 402 if (columnIndex == DELETED_COLUMN_INDEX) { 403 columnName = DELETED_COLUMN; 404 } else { 405 columnName = mColumnNames[columnIndex]; 406 } 407 return uriMap.get(columnName); 408 } 409 return null; 410 } 411 412 /** 413 * When the underlying cursor changes, we want to alert the listener 414 */ 415 private void underlyingChanged() { 416 if (mCursorObserverRegistered) { 417 try { 418 sUnderlyingCursor.unregisterContentObserver(mCursorObserver); 419 } catch (IllegalStateException e) { 420 // Maybe the cursor was GC'd? 421 } 422 mCursorObserverRegistered = false; 423 } 424 if (DEBUG) { 425 LogUtils.i(TAG, "[Notify: onRefreshRequired()]"); 426 } 427 synchronized(sListeners) { 428 for (ConversationListener listener: sListeners) { 429 listener.onRefreshRequired(); 430 } 431 } 432 sRefreshRequired = true; 433 } 434 435 /** 436 * Put the refreshed cursor in place (called by the UI) 437 */ 438 public void sync() { 439 if (sRequeryCursor == null) { 440 // This can happen during an animated deletion, if the UI isn't keeping track, or 441 // if a new query intervened (i.e. user changed folders) 442 if (DEBUG) { 443 LogUtils.i(TAG, "[sync() called; no requery cursor]"); 444 } 445 return; 446 } 447 synchronized(sCacheMapLock) { 448 if (DEBUG) { 449 LogUtils.i(TAG, "[sync()]"); 450 } 451 resetCursor(sRequeryCursor); 452 sRequeryCursor = null; 453 sRefreshInProgress = false; 454 sRefreshReady = false; 455 } 456 } 457 458 public boolean isRefreshRequired() { 459 return sRefreshRequired; 460 } 461 462 public boolean isRefreshReady() { 463 return sRefreshReady; 464 } 465 466 /** 467 * Cancel a refresh in progress 468 */ 469 public static void cancelRefresh() { 470 if (DEBUG) { 471 LogUtils.i(TAG, "[cancelRefresh() called]"); 472 } 473 synchronized(sCacheMapLock) { 474 // Mark the requery closed 475 sRefreshInProgress = false; 476 sRefreshReady = false; 477 // If we have the cursor, close it; otherwise, it will get closed when the query 478 // finishes (it checks sRefreshInProgress) 479 if (sRequeryCursor != null) { 480 sRequeryCursor.close(); 481 sRequeryCursor = null; 482 } 483 } 484 } 485 486 /** 487 * Get a list of deletions from ConversationCursor to the refreshed cursor that hasn't yet 488 * been swapped into place; this allows the UI to animate these away if desired 489 * @return a list of positions deleted in ConversationCursor 490 */ 491 public ArrayList<Integer> getRefreshDeletions () { 492 if (DEBUG) { 493 LogUtils.i(TAG, "[getRefreshDeletions() called]"); 494 } 495 // It's possible that the requery cursor is null in the case that loadInBackground() causes 496 // ConversationCursor.create to do a sync() between the time that refreshReady() is called 497 // and the subsequent call to getRefreshDeletions(). This is harmless, and an empty 498 // result list is correct. 499 if (sRequeryCursor == null) { 500 return EMPTY_DELETION_LIST; 501 } 502 Cursor deviceCursor = sConversationCursor; 503 Cursor serverCursor = sRequeryCursor; 504 ArrayList<Integer> deleteList = new ArrayList<Integer>(); 505 int serverCount = serverCursor.getCount(); 506 int deviceCount = deviceCursor.getCount(); 507 deviceCursor.moveToFirst(); 508 serverCursor.moveToFirst(); 509 while (serverCount > 0 || deviceCount > 0) { 510 if (serverCount == 0) { 511 for (; deviceCount > 0; deviceCount--) 512 deleteList.add(deviceCursor.getPosition()); 513 break; 514 } else if (deviceCount == 0) { 515 break; 516 } 517 long deviceMs = deviceCursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN); 518 long serverMs = serverCursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN); 519 String deviceUri = deviceCursor.getString(UIProvider.CONVERSATION_URI_COLUMN); 520 String serverUri = serverCursor.getString(UIProvider.CONVERSATION_URI_COLUMN); 521 deviceCursor.moveToNext(); 522 serverCursor.moveToNext(); 523 serverCount--; 524 deviceCount--; 525 if (serverMs == deviceMs) { 526 // Check for duplicates here; if our identical dates refer to different messages, 527 // we'll just quit here for now (at worst, this will cause a non-animating delete) 528 // My guess is that this happens VERY rarely, if at all 529 if (!deviceUri.equals(serverUri)) { 530 // To do this right, we'd find all of the rows with the same ms (date), etc... 531 //return deleteList; 532 } 533 continue; 534 } else if (deviceMs > serverMs) { 535 deleteList.add(deviceCursor.getPosition() - 1); 536 // Move back because we've already advanced cursor (that's why we subtract 1 above) 537 serverCount++; 538 serverCursor.moveToPrevious(); 539 } else if (serverMs > deviceMs) { 540 // If we wanted to track insertions, we'd so so here 541 // Move back because we've already advanced cursor 542 deviceCount++; 543 deviceCursor.moveToPrevious(); 544 } 545 } 546 LogUtils.i(TAG, "Deletions: " + deleteList); 547 return deleteList; 548 } 549 550 /** 551 * When we get a requery from the UI, we'll do it, but also clear the cache. The listener is 552 * notified when the requery is complete 553 * NOTE: This will have to change, of course, when we start using loaders... 554 */ 555 public boolean refresh() { 556 if (DEBUG) { 557 LogUtils.i(TAG, "[refresh() called]"); 558 } 559 if (sRefreshInProgress) { 560 return false; 561 } 562 // Say we're starting a requery 563 sRefreshInProgress = true; 564 new Thread(new Runnable() { 565 @Override 566 public void run() { 567 // Get new data 568 sRequeryCursor = doQuery(qUri, qProjection); 569 // Make sure window is full 570 synchronized(sCacheMapLock) { 571 if (sRefreshInProgress) { 572 sRequeryCursor.getCount(); 573 sRefreshReady = true; 574 sActivity.runOnUiThread(new Runnable() { 575 @Override 576 public void run() { 577 if (DEBUG) { 578 LogUtils.i(TAG, "[Notify: onRefreshReady()]"); 579 } 580 if (sRequeryCursor != null && !sRequeryCursor.isClosed()) { 581 synchronized (sListeners) { 582 for (ConversationListener listener : sListeners) { 583 listener.onRefreshReady(); 584 } 585 } 586 } 587 }}); 588 } else { 589 cancelRefresh(); 590 } 591 } 592 } 593 }).start(); 594 return true; 595 } 596 597 @Override 598 public void close() { 599 if (!sUnderlyingCursor.isClosed()) { 600 // Unregister our observer on the underlying cursor and close as usual 601 if (mCursorObserverRegistered) { 602 try { 603 sUnderlyingCursor.unregisterContentObserver(mCursorObserver); 604 } catch (IllegalStateException e) { 605 // Maybe the cursor got GC'd? 606 } 607 mCursorObserverRegistered = false; 608 } 609 sUnderlyingCursor.close(); 610 } 611 } 612 613 /** 614 * Move to the next not-deleted item in the conversation 615 */ 616 @Override 617 public boolean moveToNext() { 618 while (true) { 619 boolean ret = sUnderlyingCursor.moveToNext(); 620 if (!ret) return false; 621 if (getCachedValue(DELETED_COLUMN_INDEX) instanceof Integer) continue; 622 mPosition++; 623 return true; 624 } 625 } 626 627 /** 628 * Move to the previous not-deleted item in the conversation 629 */ 630 @Override 631 public boolean moveToPrevious() { 632 while (true) { 633 boolean ret = sUnderlyingCursor.moveToPrevious(); 634 if (!ret) return false; 635 if (getCachedValue(-1) instanceof Integer) continue; 636 mPosition--; 637 return true; 638 } 639 } 640 641 @Override 642 public int getPosition() { 643 return mPosition; 644 } 645 646 /** 647 * The actual cursor's count must be decremented by the number we've deleted from the UI 648 */ 649 @Override 650 public int getCount() { 651 return sUnderlyingCursor.getCount() - sDeletedCount; 652 } 653 654 @Override 655 public boolean moveToFirst() { 656 sUnderlyingCursor.moveToPosition(-1); 657 mPosition = -1; 658 return moveToNext(); 659 } 660 661 @Override 662 public boolean moveToPosition(int pos) { 663 if (pos < -1 || pos >= getCount()) return false; 664 if (pos == mPosition) return true; 665 if (pos > mPosition) { 666 while (pos > mPosition) { 667 if (!moveToNext()) { 668 return false; 669 } 670 } 671 return true; 672 } else if (pos == 0) { 673 return moveToFirst(); 674 } else { 675 while (pos < mPosition) { 676 if (!moveToPrevious()) { 677 return false; 678 } 679 } 680 return true; 681 } 682 } 683 684 @Override 685 public boolean moveToLast() { 686 throw new UnsupportedOperationException("moveToLast unsupported!"); 687 } 688 689 @Override 690 public boolean move(int offset) { 691 throw new UnsupportedOperationException("move unsupported!"); 692 } 693 694 /** 695 * We need to override all of the getters to make sure they look at cached values before using 696 * the values in the underlying cursor 697 */ 698 @Override 699 public double getDouble(int columnIndex) { 700 Object obj = getCachedValue(columnIndex); 701 if (obj != null) return (Double)obj; 702 return sUnderlyingCursor.getDouble(columnIndex); 703 } 704 705 @Override 706 public float getFloat(int columnIndex) { 707 Object obj = getCachedValue(columnIndex); 708 if (obj != null) return (Float)obj; 709 return sUnderlyingCursor.getFloat(columnIndex); 710 } 711 712 @Override 713 public int getInt(int columnIndex) { 714 Object obj = getCachedValue(columnIndex); 715 if (obj != null) return (Integer)obj; 716 return sUnderlyingCursor.getInt(columnIndex); 717 } 718 719 @Override 720 public long getLong(int columnIndex) { 721 Object obj = getCachedValue(columnIndex); 722 if (obj != null) return (Long)obj; 723 return sUnderlyingCursor.getLong(columnIndex); 724 } 725 726 @Override 727 public short getShort(int columnIndex) { 728 Object obj = getCachedValue(columnIndex); 729 if (obj != null) return (Short)obj; 730 return sUnderlyingCursor.getShort(columnIndex); 731 } 732 733 @Override 734 public String getString(int columnIndex) { 735 // If we're asking for the Uri for the conversation list, we return a forwarding URI 736 // so that we can intercept update/delete and handle it ourselves 737 if (columnIndex == sUriColumnIndex) { 738 Uri uri = Uri.parse(sUnderlyingCursor.getString(columnIndex)); 739 return uriToCachingUriString(uri); 740 } 741 Object obj = getCachedValue(columnIndex); 742 if (obj != null) return (String)obj; 743 return sUnderlyingCursor.getString(columnIndex); 744 } 745 746 @Override 747 public byte[] getBlob(int columnIndex) { 748 Object obj = getCachedValue(columnIndex); 749 if (obj != null) return (byte[])obj; 750 return sUnderlyingCursor.getBlob(columnIndex); 751 } 752 753 /** 754 * Observer of changes to underlying data 755 */ 756 private class CursorObserver extends ContentObserver { 757 public CursorObserver() { 758 super(null); 759 } 760 761 @Override 762 public void onChange(boolean selfChange) { 763 // If we're here, then something outside of the UI has changed the data, and we 764 // must query the underlying provider for that data 765 if (DEBUG) { 766 LogUtils.i(TAG, "Underlying conversation cursor changed; requerying"); 767 } 768 // It's not at all obvious to me why we must unregister/re-register after the requery 769 // However, if we don't we'll only get one notification and no more... 770 ConversationCursor.this.underlyingChanged(); 771 } 772 } 773 774 /** 775 * ConversationProvider is the ContentProvider for our forwarding Uri's; it passes queries 776 * and inserts directly, and caches updates/deletes before passing them through. The caching 777 * will cause a redraw of the list with updated values. 778 */ 779 public abstract static class ConversationProvider extends ContentProvider { 780 public static String AUTHORITY; 781 782 /** 783 * Allows the implmenting provider to specify the authority that should be used. 784 */ 785 protected abstract String getAuthority(); 786 787 @Override 788 public boolean onCreate() { 789 sProvider = this; 790 AUTHORITY = getAuthority(); 791 return true; 792 } 793 794 @Override 795 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 796 String sortOrder) { 797 return mResolver.query( 798 uriFromCachingUri(uri), projection, selection, selectionArgs, sortOrder); 799 } 800 801 @Override 802 public Uri insert(Uri uri, ContentValues values) { 803 insertLocal(uri, values); 804 return ProviderExecute.opInsert(uri, values); 805 } 806 807 @Override 808 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 809 updateLocal(uri, values); 810 return ProviderExecute.opUpdate(uri, values); 811 } 812 813 @Override 814 public int delete(Uri uri, String selection, String[] selectionArgs) { 815 deleteLocal(uri); 816 return ProviderExecute.opDelete(uri); 817 } 818 819 @Override 820 public String getType(Uri uri) { 821 return null; 822 } 823 824 /** 825 * Quick and dirty class that executes underlying provider CRUD operations on a background 826 * thread. 827 */ 828 static class ProviderExecute implements Runnable { 829 static final int DELETE = 0; 830 static final int INSERT = 1; 831 static final int UPDATE = 2; 832 833 final int mCode; 834 final Uri mUri; 835 final ContentValues mValues; //HEHEH 836 837 ProviderExecute(int code, Uri uri, ContentValues values) { 838 mCode = code; 839 mUri = uriFromCachingUri(uri); 840 mValues = values; 841 } 842 843 ProviderExecute(int code, Uri uri) { 844 this(code, uri, null); 845 } 846 847 static Uri opInsert(Uri uri, ContentValues values) { 848 ProviderExecute e = new ProviderExecute(INSERT, uri, values); 849 if (offUiThread()) return (Uri)e.go(); 850 new Thread(e).start(); 851 return null; 852 } 853 854 static int opDelete(Uri uri) { 855 ProviderExecute e = new ProviderExecute(DELETE, uri); 856 if (offUiThread()) return (Integer)e.go(); 857 new Thread(new ProviderExecute(DELETE, uri)).start(); 858 return 0; 859 } 860 861 static int opUpdate(Uri uri, ContentValues values) { 862 ProviderExecute e = new ProviderExecute(UPDATE, uri, values); 863 if (offUiThread()) return (Integer)e.go(); 864 new Thread(e).start(); 865 return 0; 866 } 867 868 @Override 869 public void run() { 870 go(); 871 } 872 873 public Object go() { 874 switch(mCode) { 875 case DELETE: 876 return mResolver.delete(mUri, null, null); 877 case INSERT: 878 return mResolver.insert(mUri, mValues); 879 case UPDATE: 880 return mResolver.update(mUri, mValues, null, null); 881 default: 882 return null; 883 } 884 } 885 } 886 887 private void insertLocal(Uri uri, ContentValues values) { 888 // Placeholder for now; there's no local insert 889 } 890 891 @VisibleForTesting 892 void deleteLocal(Uri uri) { 893 Uri underlyingUri = uriFromCachingUri(uri); 894 // Remember to decode the underlying Uri as it might be encoded (as w/ Gmail) 895 String uriString = Uri.decode(underlyingUri.toString()); 896 cacheValue(uriString, DELETED_COLUMN, true); 897 } 898 899 @VisibleForTesting 900 void updateLocal(Uri uri, ContentValues values) { 901 if (values == null) { 902 return; 903 } 904 Uri underlyingUri = uriFromCachingUri(uri); 905 // Remember to decode the underlying Uri as it might be encoded (as w/ Gmail) 906 String uriString = Uri.decode(underlyingUri.toString()); 907 for (String columnName: values.keySet()) { 908 cacheValue(uriString, columnName, values.get(columnName)); 909 } 910 } 911 912 public int apply(ArrayList<ConversationOperation> ops) { 913 final HashMap<String, ArrayList<ContentProviderOperation>> batchMap = 914 new HashMap<String, ArrayList<ContentProviderOperation>>(); 915 // Increment sequence count 916 sSequence++; 917 // Execute locally and build CPO's for underlying provider 918 for (ConversationOperation op: ops) { 919 Uri underlyingUri = uriFromCachingUri(op.mUri); 920 String authority = underlyingUri.getAuthority(); 921 ArrayList<ContentProviderOperation> authOps = batchMap.get(authority); 922 if (authOps == null) { 923 authOps = new ArrayList<ContentProviderOperation>(); 924 batchMap.put(authority, authOps); 925 } 926 authOps.add(op.execute(underlyingUri)); 927 } 928 929 // Send changes to underlying provider 930 for (String authority: batchMap.keySet()) { 931 try { 932 if (offUiThread()) { 933 mResolver.applyBatch(authority, batchMap.get(authority)); 934 } else { 935 final String auth = authority; 936 new Thread(new Runnable() { 937 @Override 938 public void run() { 939 try { 940 mResolver.applyBatch(auth, batchMap.get(auth)); 941 } catch (RemoteException e) { 942 } catch (OperationApplicationException e) { 943 } 944 } 945 }).start(); 946 } 947 } catch (RemoteException e) { 948 } catch (OperationApplicationException e) { 949 } 950 } 951 return sSequence; 952 } 953 } 954 955 /** 956 * ConversationOperation is the encapsulation of a ContentProvider operation to be performed 957 * atomically as part of a "batch" operation. 958 */ 959 public static class ConversationOperation { 960 public static final int DELETE = 0; 961 public static final int INSERT = 1; 962 public static final int UPDATE = 2; 963 public static final int ARCHIVE = 3; 964 public static final int MUTE = 4; 965 public static final int REPORT_SPAM = 5; 966 967 private final int mType; 968 private final Uri mUri; 969 private final ContentValues mValues; 970 // True if an updated item should be removed locally (from ConversationCursor) 971 // This would be the case for a folder change in which the conversation is no longer 972 // in the folder represented by the ConversationCursor 973 private final boolean mLocalDeleteOnUpdate; 974 975 /** 976 * Set to true to immediately notify any {@link DataSetObserver}s watching the global 977 * {@link ConversationCursor} upon applying the change to the data cache. You would not 978 * want to do this if a change you make is being handled specially, like an animated delete. 979 * 980 * TODO: move this to the application Controller, or whoever has a canonical reference 981 * to a {@link ConversationCursor} to notify on. 982 */ 983 private final boolean mAutoNotify; 984 985 public ConversationOperation(int type, Conversation conv) { 986 this(type, conv, null, false /* autoNotify */); 987 } 988 989 public ConversationOperation(int type, Conversation conv, ContentValues values, 990 boolean autoNotify) { 991 mType = type; 992 mUri = conv.uri; 993 mValues = values; 994 mLocalDeleteOnUpdate = conv.localDeleteOnUpdate; 995 mAutoNotify = autoNotify; 996 } 997 998 private ContentProviderOperation execute(Uri underlyingUri) { 999 Uri uri = underlyingUri.buildUpon() 1000 .appendQueryParameter(UIProvider.SEQUENCE_QUERY_PARAMETER, 1001 Integer.toString(sSequence)) 1002 .build(); 1003 ContentProviderOperation op; 1004 switch(mType) { 1005 case DELETE: 1006 sProvider.deleteLocal(mUri); 1007 op = ContentProviderOperation.newDelete(uri).build(); 1008 break; 1009 case UPDATE: 1010 if (mLocalDeleteOnUpdate) { 1011 sProvider.deleteLocal(mUri); 1012 } else { 1013 sProvider.updateLocal(mUri, mValues); 1014 } 1015 op = ContentProviderOperation.newUpdate(uri) 1016 .withValues(mValues) 1017 .build(); 1018 break; 1019 case INSERT: 1020 sProvider.insertLocal(mUri, mValues); 1021 op = ContentProviderOperation.newInsert(uri) 1022 .withValues(mValues).build(); 1023 break; 1024 case ARCHIVE: 1025 sProvider.deleteLocal(mUri); 1026 1027 // Create an update operation that represents archive 1028 op = ContentProviderOperation.newUpdate(uri).withValue( 1029 ConversationOperations.OPERATION_KEY, ConversationOperations.ARCHIVE) 1030 .build(); 1031 break; 1032 case MUTE: 1033 if (mLocalDeleteOnUpdate) { 1034 sProvider.deleteLocal(mUri); 1035 } 1036 1037 // Create an update operation that represents mute 1038 op = ContentProviderOperation.newUpdate(uri).withValue( 1039 ConversationOperations.OPERATION_KEY, ConversationOperations.MUTE) 1040 .build(); 1041 break; 1042 case REPORT_SPAM: 1043 sProvider.deleteLocal(mUri); 1044 1045 // Create an update operation that represents report spam 1046 op = ContentProviderOperation.newUpdate(uri).withValue( 1047 ConversationOperations.OPERATION_KEY, 1048 ConversationOperations.REPORT_SPAM).build(); 1049 break; 1050 default: 1051 throw new UnsupportedOperationException( 1052 "No such ConversationOperation type: " + mType); 1053 } 1054 1055 // FIXME: this is a hack to notify conversation list of changes from conversation view. 1056 // The proper way to do this is to have the Controller handle the 'mark read' action. 1057 // It has a reference to this ConversationCursor so it can notify without using global 1058 // magic. 1059 if (mAutoNotify) { 1060 if (sConversationCursor != null) { 1061 sConversationCursor.notifyDataSetChanged(); 1062 } else { 1063 LogUtils.i(TAG, "Unable to auto-notify because there is no existing" + 1064 " conversation cursor"); 1065 } 1066 } 1067 1068 return op; 1069 } 1070 } 1071 1072 /** 1073 * For now, a single listener can be associated with the cursor, and for now we'll just 1074 * notify on deletions 1075 */ 1076 public interface ConversationListener { 1077 // Data in the underlying provider has changed; a refresh is required to sync up 1078 public void onRefreshRequired(); 1079 // We've completed a requested refresh of the underlying cursor 1080 public void onRefreshReady(); 1081 } 1082 1083 @Override 1084 public boolean isFirst() { 1085 throw new UnsupportedOperationException(); 1086 } 1087 1088 @Override 1089 public boolean isLast() { 1090 throw new UnsupportedOperationException(); 1091 } 1092 1093 @Override 1094 public boolean isBeforeFirst() { 1095 throw new UnsupportedOperationException(); 1096 } 1097 1098 @Override 1099 public boolean isAfterLast() { 1100 throw new UnsupportedOperationException(); 1101 } 1102 1103 @Override 1104 public int getColumnIndex(String columnName) { 1105 return sUnderlyingCursor.getColumnIndex(columnName); 1106 } 1107 1108 @Override 1109 public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { 1110 return sUnderlyingCursor.getColumnIndexOrThrow(columnName); 1111 } 1112 1113 @Override 1114 public String getColumnName(int columnIndex) { 1115 return sUnderlyingCursor.getColumnName(columnIndex); 1116 } 1117 1118 @Override 1119 public String[] getColumnNames() { 1120 return sUnderlyingCursor.getColumnNames(); 1121 } 1122 1123 @Override 1124 public int getColumnCount() { 1125 return sUnderlyingCursor.getColumnCount(); 1126 } 1127 1128 @Override 1129 public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { 1130 throw new UnsupportedOperationException(); 1131 } 1132 1133 @Override 1134 public int getType(int columnIndex) { 1135 return sUnderlyingCursor.getType(columnIndex); 1136 } 1137 1138 @Override 1139 public boolean isNull(int columnIndex) { 1140 throw new UnsupportedOperationException(); 1141 } 1142 1143 @Override 1144 public void deactivate() { 1145 throw new UnsupportedOperationException(); 1146 } 1147 1148 @Override 1149 public boolean isClosed() { 1150 return sUnderlyingCursor.isClosed(); 1151 } 1152 1153 @Override 1154 public void registerContentObserver(ContentObserver observer) { 1155 // Nope. We never notify of underlying changes on this channel, since the cursor watches 1156 // internally and offers onRefreshRequired/onRefreshReady to accomplish the same thing. 1157 } 1158 1159 @Override 1160 public void unregisterContentObserver(ContentObserver observer) { 1161 // See above. 1162 } 1163 1164 @Override 1165 public void registerDataSetObserver(DataSetObserver observer) { 1166 mDataSetObservable.registerObserver(observer); 1167 } 1168 1169 @Override 1170 public void unregisterDataSetObserver(DataSetObserver observer) { 1171 mDataSetObservable.unregisterObserver(observer); 1172 } 1173 1174 public void notifyDataSetChanged() { 1175 mDataSetObservable.notifyChanged(); 1176 } 1177 1178 @Override 1179 public void setNotificationUri(ContentResolver cr, Uri uri) { 1180 throw new UnsupportedOperationException(); 1181 } 1182 1183 @Override 1184 public boolean getWantsAllOnMoveCalls() { 1185 throw new UnsupportedOperationException(); 1186 } 1187 1188 @Override 1189 public Bundle getExtras() { 1190 throw new UnsupportedOperationException(); 1191 } 1192 1193 @Override 1194 public Bundle respond(Bundle extras) { 1195 throw new UnsupportedOperationException(); 1196 } 1197 1198 @Override 1199 public boolean requery() { 1200 return true; 1201 } 1202} 1203