ConversationCursor.java revision c8a994227b9c686d88ee05840544162711a85712
1/******************************************************************************* 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 20import android.content.ContentProvider; 21import android.content.ContentResolver; 22import android.content.ContentValues; 23import android.content.Context; 24import android.database.ContentObserver; 25import android.database.Cursor; 26import android.database.CursorWrapper; 27import android.net.Uri; 28import android.os.Handler; 29import android.util.Log; 30import android.widget.CursorAdapter; 31 32import java.util.HashMap; 33import java.util.List; 34 35/** 36 * ConversationCursor is a wrapper around a conversation list cursor that provides update/delete 37 * caching for quick UI response. This is effectively a singleton class, as the cache is 38 * implemented as a static HashMap. 39 */ 40public class ConversationCursor extends CursorWrapper { 41 private static final String TAG = "ConversationCursor"; 42 private static final boolean DEBUG = true; // STOPSHIP Set to false before shipping 43 44 // The authority of our conversation provider (a forwarding provider) 45 // This string must match the declaration in AndroidManifest.xml 46 private static final String sAuthority = "com.android.mail.conversation.provider"; 47 48 // A mapping from Uri to updated ContentValues 49 private static HashMap<String, ContentValues> sCacheMap = new HashMap<String, ContentValues>(); 50 // A deleted row is indicated by the presence of DELETED_COLUMN in the cache map 51 private static final String DELETED_COLUMN = "__deleted__"; 52 // A sentinel value for the "index" of the deleted column; it's an int that is otherwise invalid 53 private static final int DELETED_COLUMN_INDEX = -1; 54 55 // The cursor underlying the caching cursor 56 private final Cursor mUnderlying; 57 // Column names for this cursor 58 private final String[] mColumnNames; 59 // The index of the Uri whose data is reflected in the cached row 60 // Updates/Deletes to this Uri are cached 61 private final int mUriColumnIndex; 62 // The resolver for the cursor instantiator's context 63 private static ContentResolver mResolver; 64 // An observer on the underlying cursor (so we can detect changes from outside the UI) 65 private final CursorObserver mCursorObserver; 66 // The adapter using this cursor (which needs to refresh when data changes) 67 private static CursorAdapter mAdapter; 68 69 // The current position of the cursor 70 private int mPosition = -1; 71 // The number of cached deletions from this cursor (used to quickly generate an accurate count) 72 private static int sDeletedCount = 0; 73 74 public ConversationCursor(Cursor cursor, Context context, String messageListColumn) { 75 super(cursor); 76 mUnderlying = cursor; 77 mCursorObserver = new CursorObserver(); 78 // New cursor -> clear the cache 79 resetCache(); 80 mColumnNames = cursor.getColumnNames(); 81 mUriColumnIndex = getColumnIndex(messageListColumn); 82 if (mUriColumnIndex < 0) { 83 throw new IllegalArgumentException("Cursor must include a message list column"); 84 } 85 mResolver = context.getContentResolver(); 86 // We'll observe the underlying cursor and act when it changes 87 //cursor.registerContentObserver(mCursorObserver); 88 } 89 90 /** 91 * Reset the cache; this involves clearing out our cache map and resetting our various counts 92 * The cache should be reset whenever we get fresh data from the underlying cursor 93 */ 94 private void resetCache() { 95 sCacheMap.clear(); 96 sDeletedCount = 0; 97 mPosition = -1; 98 mUnderlying.registerContentObserver(mCursorObserver); 99 } 100 101 /** 102 * Set the adapter for this cursor; we'll notify it when our data changes 103 */ 104 public void setAdapter(CursorAdapter adapter) { 105 mAdapter = adapter; 106 } 107 108 /** 109 * Generate a forwarding Uri to ConversationProvider from an original Uri. We do this by 110 * changing the authority to ours, but otherwise leaving the Uri intact. 111 * NOTE: This won't handle query parameters, so the functionality will need to be added if 112 * parameters are used in the future 113 * @param uri the uri 114 * @return a forwarding uri to ConversationProvider 115 */ 116 private static String uriToCachingUriString (Uri uri) { 117 String provider = uri.getAuthority(); 118 return uri.getScheme() + "://" + sAuthority + "/" + provider + uri.getPath(); 119 } 120 121 /** 122 * Regenerate the original Uri from a forwarding (ConversationProvider) Uri 123 * NOTE: See note above for uriToCachingUri 124 * @param uri the forwarding Uri 125 * @return the original Uri 126 */ 127 private static Uri uriFromCachingUri(Uri uri) { 128 List<String> path = uri.getPathSegments(); 129 Uri.Builder builder = new Uri.Builder().scheme(uri.getScheme()).authority(path.get(0)); 130 for (int i = 1; i < path.size(); i++) { 131 builder.appendPath(path.get(i)); 132 } 133 return builder.build(); 134 } 135 136 /** 137 * Cache a column name/value pair for a given Uri 138 * @param uriString the Uri for which the column name/value pair applies 139 * @param columnName the column name 140 * @param value the value to be cached 141 */ 142 private static void cacheValue(String uriString, String columnName, Object value) { 143 // Get the map for our uri 144 ContentValues map = sCacheMap.get(uriString); 145 // Create one if necessary 146 if (map == null) { 147 map = new ContentValues(); 148 sCacheMap.put(uriString, map); 149 } 150 // If we're caching a deletion, add to our count 151 if ((columnName == DELETED_COLUMN) && (map.get(columnName) == null)) { 152 sDeletedCount++; 153 if (DEBUG) { 154 Log.d(TAG, "Deleted " + uriString); 155 } 156 } 157 // ContentValues has no generic "put", so we must test. For now, the only classes of 158 // values implemented are Boolean/Integer/String, though others are trivially added 159 if (value instanceof Boolean) { 160 map.put(columnName, ((Boolean)value).booleanValue() ? 1 : 0); 161 } else if (value instanceof Integer) { 162 map.put(columnName, (Integer)value); 163 } else if (value instanceof String) { 164 map.put(columnName, (String)value); 165 } else { 166 String cname = value.getClass().getName(); 167 throw new IllegalArgumentException("Value class not compatible with cache: " + cname); 168 } 169 170 // Since we've changed the data, alert the adapter to redraw 171 mAdapter.notifyDataSetChanged(); 172 if (DEBUG && (columnName != DELETED_COLUMN)) { 173 Log.d(TAG, "Caching value for " + uriString + ": " + columnName); 174 } 175 } 176 177 /** 178 * Get the cached value for the provided column; we special case -1 as the "deleted" column 179 * @param columnIndex the index of the column whose cached value we want to retrieve 180 * @return the cached value for this column, or null if there is none 181 */ 182 private Object getCachedValue(int columnIndex) { 183 String uri = super.getString(mUriColumnIndex); 184 ContentValues uriMap = sCacheMap.get(uri); 185 if (uriMap != null) { 186 String columnName; 187 if (columnIndex == DELETED_COLUMN_INDEX) { 188 columnName = DELETED_COLUMN; 189 } else { 190 columnName = mColumnNames[columnIndex]; 191 } 192 return uriMap.get(columnName); 193 } 194 return null; 195 } 196 197 /** 198 * When the underlying cursor changes, we want to force a requery to get the new provider data; 199 * the cache must also be reset here since it's no longer fresh 200 */ 201 private void underlyingChanged() { 202 super.requery(); 203 resetCache(); 204 } 205 206 // We don't want to do anything when we get a requery, as our data is updated immediately from 207 // the UI and we detect changes on the underlying provider above 208 public boolean requery() { 209 return true; 210 } 211 212 public void close() { 213 // Unregister our observer on the underlying cursor and close as usual 214 mUnderlying.unregisterContentObserver(mCursorObserver); 215 super.close(); 216 } 217 218 /** 219 * Move to the next not-deleted item in the conversation 220 */ 221 public boolean moveToNext() { 222 while (true) { 223 boolean ret = super.moveToNext(); 224 if (!ret) return false; 225 if (getCachedValue(DELETED_COLUMN_INDEX) instanceof Integer) continue; 226 mPosition++; 227 return true; 228 } 229 } 230 231 /** 232 * Move to the previous not-deleted item in the conversation 233 */ 234 public boolean moveToPrevious() { 235 while (true) { 236 boolean ret = super.moveToPrevious(); 237 if (!ret) return false; 238 if (getCachedValue(-1) instanceof Integer) continue; 239 mPosition--; 240 return true; 241 } 242 } 243 244 public int getPosition() { 245 return mPosition; 246 } 247 248 /** 249 * The actual cursor's count must be decremented by the number we've deleted from the UI 250 */ 251 public int getCount() { 252 return super.getCount() - sDeletedCount; 253 } 254 255 public boolean moveToFirst() { 256 super.moveToPosition(-1); 257 mPosition = -1; 258 return moveToNext(); 259 } 260 261 public boolean moveToPosition(int pos) { 262 if (pos == mPosition) return true; 263 if (pos > mPosition) { 264 while (pos > mPosition) { 265 if (!moveToNext()) { 266 return false; 267 } 268 } 269 return true; 270 } else if (pos == 0) { 271 return moveToFirst(); 272 } else { 273 while (pos < mPosition) { 274 if (!moveToPrevious()) { 275 return false; 276 } 277 } 278 return true; 279 } 280 } 281 282 public boolean moveToLast() { 283 throw new UnsupportedOperationException("moveToLast unsupported!"); 284 } 285 286 public boolean move(int offset) { 287 throw new UnsupportedOperationException("move unsupported!"); 288 } 289 290 /** 291 * We need to override all of the getters to make sure they look at cached values before using 292 * the values in the underlying cursor 293 */ 294 @Override 295 public double getDouble(int columnIndex) { 296 Object obj = getCachedValue(columnIndex); 297 if (obj != null) return (Double)obj; 298 return super.getDouble(columnIndex); 299 } 300 301 @Override 302 public float getFloat(int columnIndex) { 303 Object obj = getCachedValue(columnIndex); 304 if (obj != null) return (Float)obj; 305 return super.getFloat(columnIndex); 306 } 307 308 @Override 309 public int getInt(int columnIndex) { 310 Object obj = getCachedValue(columnIndex); 311 if (obj != null) return (Integer)obj; 312 return super.getInt(columnIndex); 313 } 314 315 @Override 316 public long getLong(int columnIndex) { 317 Object obj = getCachedValue(columnIndex); 318 if (obj != null) return (Long)obj; 319 return super.getLong(columnIndex); 320 } 321 322 @Override 323 public short getShort(int columnIndex) { 324 Object obj = getCachedValue(columnIndex); 325 if (obj != null) return (Short)obj; 326 return super.getShort(columnIndex); 327 } 328 329 @Override 330 public String getString(int columnIndex) { 331 // If we're asking for the Uri for the conversation list, we return a forwarding URI 332 // so that we can intercept update/delete and handle it ourselves 333 if (columnIndex == mUriColumnIndex) { 334 Uri uri = Uri.parse(super.getString(columnIndex)); 335 return uriToCachingUriString(uri); 336 } 337 Object obj = getCachedValue(columnIndex); 338 if (obj != null) return (String)obj; 339 return super.getString(columnIndex); 340 } 341 342 @Override 343 public byte[] getBlob(int columnIndex) { 344 Object obj = getCachedValue(columnIndex); 345 if (obj != null) return (byte[])obj; 346 return super.getBlob(columnIndex); 347 } 348 349 /** 350 * Observer of changes to underlying data 351 */ 352 private class CursorObserver extends ContentObserver { 353 public CursorObserver() { 354 super(new Handler()); 355 } 356 357 @Override 358 public void onChange(boolean selfChange) { 359 // If we're here, then something outside of the UI has changed the data, and we 360 // must requery to get that data from the underlying provider 361 if (DEBUG) { 362 Log.d(TAG, "Underlying conversation cursor changed; requerying"); 363 } 364 // It's not at all obvious to me why we must unregister/re-register after the requery 365 // However, if we don't we'll only get one notification and no more... 366 mUnderlying.unregisterContentObserver(mCursorObserver); 367 ConversationCursor.this.underlyingChanged(); 368 } 369 } 370 371 /** 372 * ConversationProvider is the ContentProvider for our forwarding Uri's; it passes queries 373 * and inserts directly, and caches updates/deletes before passing them through. The caching 374 * will cause a redraw of the list with updated values. 375 */ 376 public static class ConversationProvider extends ContentProvider { 377 @Override 378 public boolean onCreate() { 379 return false; 380 } 381 382 @Override 383 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 384 String sortOrder) { 385 return mResolver.query( 386 uriFromCachingUri(uri), projection, selection, selectionArgs, sortOrder); 387 } 388 389 @Override 390 public String getType(Uri uri) { 391 return null; 392 } 393 394 /** 395 * Quick and dirty class that executes underlying provider CRUD operations on a background 396 * thread. 397 */ 398 static class ProviderExecute implements Runnable { 399 static final int DELETE = 0; 400 static final int INSERT = 1; 401 static final int UPDATE = 2; 402 403 final int mCode; 404 final Uri mUri; 405 final ContentValues mValues; //HEHEH 406 407 ProviderExecute(int code, Uri uri, ContentValues values) { 408 mCode = code; 409 mUri = uriFromCachingUri(uri); 410 mValues = values; 411 } 412 413 ProviderExecute(int code, Uri uri) { 414 this(code, uri, null); 415 } 416 417 static void opDelete(Uri uri) { 418 new Thread(new ProviderExecute(DELETE, uri)).start(); 419 } 420 421 static void opInsert(Uri uri, ContentValues values) { 422 new Thread(new ProviderExecute(INSERT, uri, values)).start(); 423 } 424 425 static void opUpdate(Uri uri, ContentValues values) { 426 new Thread(new ProviderExecute(UPDATE, uri, values)).start(); 427 } 428 429 @Override 430 public void run() { 431 switch(mCode) { 432 case DELETE: 433 mResolver.delete(mUri, null, null); 434 break; 435 case INSERT: 436 mResolver.insert(mUri, mValues); 437 break; 438 case UPDATE: 439 mResolver.update(mUri, mValues, null, null); 440 break; 441 } 442 } 443 } 444 445 @Override 446 public Uri insert(Uri uri, ContentValues values) { 447 ProviderExecute.opInsert(uri, values); 448 return null; 449 } 450 451 @Override 452 public int delete(Uri uri, String selection, String[] selectionArgs) { 453 Uri underlyingUri = uriFromCachingUri(uri); 454 String uriString = underlyingUri.toString(); 455 cacheValue(uriString, DELETED_COLUMN, true); 456 ProviderExecute.opDelete(uri); 457 return 0; 458 } 459 460 @Override 461 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 462 Uri underlyingUri = uriFromCachingUri(uri); 463 // Remember to decode the underlying Uri as it might be encoded (as w/ Gmail) 464 String uriString = Uri.decode(underlyingUri.toString()); 465 for (String columnName: values.keySet()) { 466 cacheValue(uriString, columnName, values.get(columnName)); 467 } 468 ProviderExecute.opUpdate(uri, values); 469 return 0; 470 } 471 } 472} 473