EmailProvider content caching
* In this CL, we cache individual rows, based on the CONTENT_PROJECTION defined for the most common queries (Account, HostAuth, Mailbox, and Message) * Queries on individual rows (most often Class.restoreClassById()) will look to the cache first, rather than querying the database * Queries on smaller projections will build MatrixCursor's from cached data * Write-through caching updates the cache with changed columns * Experiments with live data indicate that > 95% of queries that are cacheable (single row, no selection) can be retrieved from the cache, thereby saving a great deal of disk access. * Timing experiments show that cache hits are > 40x faster than cache misses * Unit tests for the various classes exist, with more coming TODO ---- * More unit tests Change-Id: I386a948a2f4cc02b6548d07d9b2fefd1e018a262
This commit is contained in:
parent
6926cf85ef
commit
fab77f147f
|
@ -705,7 +705,6 @@ public class LegacyConversions {
|
|||
// result.mSyncLookback
|
||||
// result.mSyncInterval
|
||||
result.mSyncTime = 0;
|
||||
result.mUnreadCount = fromFolder.getUnreadMessageCount();
|
||||
result.mFlagVisible = true;
|
||||
result.mFlags = 0;
|
||||
result.mVisibleLimit = Email.VISIBLE_LIMIT_DEFAULT;
|
||||
|
|
|
@ -0,0 +1,799 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.email.provider;
|
||||
import com.android.email.Email;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.CursorWrapper;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An LRU cache for EmailContent (Account, HostAuth, Mailbox, and Message, thus far). The intended
|
||||
* user of this cache is EmailProvider itself; caching is entirely transparent to users of the
|
||||
* provider.
|
||||
*
|
||||
* Usage examples; id is a String representation of a row id (_id), as it might be retrieved from
|
||||
* a uri via getPathSegment
|
||||
*
|
||||
* To create a cache:
|
||||
* ContentCache cache = new ContentCache(name, projection, max);
|
||||
*
|
||||
* To (try to) get a cursor from a cache:
|
||||
* Cursor cursor = cache.getCursor(id, projection);
|
||||
*
|
||||
* To read from a table and cache the resulting cursor:
|
||||
* 1. Get a CacheToken: CacheToken token = cache.getToken(id);
|
||||
* 2. Get a cursor from the database: Cursor cursor = db.query(....);
|
||||
* 3. Put the cursor in the cache: cache.putCursor(cursor, id, token);
|
||||
* Only cursors with the projection given in the definition of the cache can be cached
|
||||
*
|
||||
* To delete one or more rows or update multiple rows from a table that uses cached data:
|
||||
* 1. Lock the row in the cache: cache.lock(id);
|
||||
* 2. Delete/update the row(s): db.delete(...);
|
||||
* 3. Invalidate any other caches that might be affected by the delete/update:
|
||||
* The entire cache: affectedCache.invalidate()*
|
||||
* A specific row in a cache: affectedCache.invalidate(rowId)
|
||||
* 4. Unlock the row in the cache: cache.unlock(id);
|
||||
*
|
||||
* To update a single row from a table that uses cached data:
|
||||
* 1. Lock the row in the cache: cache.lock(id);
|
||||
* 2. Update the row: db.update(...);
|
||||
* 3. Unlock the row in the cache, passing in the new values: cache.unlock(id, values);
|
||||
*/
|
||||
public final class ContentCache extends LinkedHashMap<String, Cursor> {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final boolean DEBUG_CACHE = false;
|
||||
private static final boolean DEBUG_TOKENS = false;
|
||||
private static final boolean DEBUG_NOT_CACHEABLE = false;
|
||||
|
||||
// Count of non-cacheable queries (debug only)
|
||||
private static int sNotCacheable = 0;
|
||||
// A map of queries that aren't cacheable (debug only)
|
||||
private static final CounterMap<String> sNotCacheableMap = new CounterMap<String>();
|
||||
|
||||
// All defined caches
|
||||
private static final ArrayList<ContentCache> sContentCaches = new ArrayList<ContentCache>();
|
||||
// A set of all unclosed, cached cursors; this will typically be a very small set, as cursors
|
||||
// tend to be closed quickly after use. The value, for each cursor, is its reference count
|
||||
/*package*/ static CounterMap<Cursor> sActiveCursors;
|
||||
|
||||
// A set of locked content id's
|
||||
private final CounterMap<String> mLockMap = new CounterMap<String>(4);
|
||||
// A set of active tokens
|
||||
/*package*/ TokenList mTokenList;
|
||||
|
||||
// The name of the cache (used for logging)
|
||||
private final String mName;
|
||||
// The base projection (only queries in which all columns exist in this projection will be
|
||||
// able to avoid a cache miss)
|
||||
private final String[] mBaseProjection;
|
||||
// The number of items (cursors) to cache
|
||||
private final int mMaxSize;
|
||||
// The tag used for logging
|
||||
private final String mLogTag;
|
||||
// Cache statistics
|
||||
private final Statistics mStats;
|
||||
|
||||
/**
|
||||
* A synchronized map used as a counter for arbitrary objects (e.g. a reference count)
|
||||
*/
|
||||
/*package*/ static class CounterMap<T> extends HashMap<T, Integer> {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/*package*/ CounterMap(int maxSize) {
|
||||
super(maxSize);
|
||||
}
|
||||
|
||||
/*package*/ CounterMap() {
|
||||
super();
|
||||
}
|
||||
|
||||
/*package*/ void remove(T object) {
|
||||
synchronized(this) {
|
||||
Integer refCount = get(object);
|
||||
if (refCount == null || refCount.intValue() == 0) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (refCount > 1) {
|
||||
put(object, refCount - 1);
|
||||
} else {
|
||||
super.remove(object);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*package*/ void add(T object) {
|
||||
synchronized(this) {
|
||||
Integer refCount = get(object);
|
||||
if (refCount == null) {
|
||||
put(object, 1);
|
||||
} else {
|
||||
put(object, refCount + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*package*/ boolean contains(T object) {
|
||||
synchronized(this) {
|
||||
Integer refCount = get(object);
|
||||
return (refCount != null && refCount.intValue() > 0);
|
||||
}
|
||||
}
|
||||
|
||||
/*package*/ int getCount(T object) {
|
||||
synchronized(this) {
|
||||
Integer refCount = get(object);
|
||||
return (refCount == null) ? 0 : refCount.intValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of tokens that are in use at any moment; there can be more than one token for an id
|
||||
*/
|
||||
/*package*/ static class TokenList extends ArrayList<CacheToken> {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final String mLogTag;
|
||||
|
||||
/*package*/ TokenList(String name) {
|
||||
mLogTag = "TokenList-" + name;
|
||||
}
|
||||
|
||||
/*package*/ int invalidateTokens(String id) {
|
||||
if (DEBUG_TOKENS) {
|
||||
Log.d(mLogTag, "============ Invalidate tokens for: " + id);
|
||||
}
|
||||
ArrayList<CacheToken> removeList = new ArrayList<CacheToken>();
|
||||
int count = 0;
|
||||
for (CacheToken token: this) {
|
||||
if (token.getId().equals(id)) {
|
||||
token.invalidate();
|
||||
removeList.add(token);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
for (CacheToken token: removeList) {
|
||||
remove(token);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/*package*/ void invalidate() {
|
||||
if (DEBUG_TOKENS) {
|
||||
Log.d(mLogTag, "============ List invalidated");
|
||||
}
|
||||
for (CacheToken token: this) {
|
||||
token.invalidate();
|
||||
}
|
||||
clear();
|
||||
}
|
||||
|
||||
/*package*/ boolean remove(CacheToken token) {
|
||||
boolean result = super.remove(token);
|
||||
if (DEBUG_TOKENS) {
|
||||
if (result) {
|
||||
Log.d(mLogTag, "============ Removing token for: " + token.mId);
|
||||
} else {
|
||||
Log.d(mLogTag, "============ No token found for: " + token.mId);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public CacheToken add(String id) {
|
||||
CacheToken token = new CacheToken(id);
|
||||
super.add(token);
|
||||
if (DEBUG_TOKENS) {
|
||||
Log.d(mLogTag, "============ Taking token for: " + token.mId);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A CacheToken is an opaque object that must be passed into putCursor in order to attempt to
|
||||
* write into the cache. The token becomes invalidated by any intervening write to the cached
|
||||
* record.
|
||||
*/
|
||||
public static final class CacheToken {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final String mId;
|
||||
private boolean mIsValid = true;
|
||||
|
||||
/*package*/ CacheToken(String id) {
|
||||
mId = id;
|
||||
}
|
||||
|
||||
/*package*/ String getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
/*package*/ boolean isValid() {
|
||||
return mIsValid;
|
||||
}
|
||||
|
||||
/*package*/ void invalidate() {
|
||||
mIsValid = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object token) {
|
||||
return ((token instanceof CacheToken) && ((CacheToken)token).mId.equals(mId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return mId.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The cached cursor is simply a CursorWrapper whose underlying cursor contains zero or one
|
||||
* rows. We handle simple movement (moveToFirst(), moveToNext(), etc.), and override close()
|
||||
* to keep the underlying cursor alive (unless it's no longer cached due to an invalidation)
|
||||
*/
|
||||
public static final class CachedCursor extends CursorWrapper {
|
||||
// The cursor we're wrapping
|
||||
private final Cursor mCursor;
|
||||
// The cache which generated this cursor
|
||||
private final ContentCache mCache;
|
||||
// The current position of the cursor (can only be 0 or 1)
|
||||
private int mPosition = -1;
|
||||
// The number of rows in this cursor (-1 = not determined)
|
||||
private int mCount = -1;
|
||||
private boolean isClosed = false;
|
||||
|
||||
public CachedCursor(Cursor cursor, ContentCache cache, String name) {
|
||||
super(cursor);
|
||||
// The underlying cursor must always be at position 0
|
||||
cursor.moveToPosition(0);
|
||||
mCursor = cursor;
|
||||
mCache = cache;
|
||||
// Add this to our set of active cursors
|
||||
sActiveCursors.add(cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close this cursor; if the cursor's cache no longer contains the cursor, we'll close the
|
||||
* underlying cursor. In any event we'll remove the cursor from our set of active cursors
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
if (!mCache.containsValue(mCursor)) {
|
||||
super.close();
|
||||
}
|
||||
sActiveCursors.remove(mCursor);
|
||||
isClosed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return isClosed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
if (mCount < 0) {
|
||||
mCount = super.getCount();
|
||||
}
|
||||
return mCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* We'll be happy to move to position 0 or -1; others are illegal
|
||||
*/
|
||||
@Override
|
||||
public boolean moveToPosition(int pos) {
|
||||
if (pos > 0) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
if (pos >= getCount()) {
|
||||
return false;
|
||||
}
|
||||
mPosition = pos;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToFirst() {
|
||||
return moveToPosition(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToNext() {
|
||||
return moveToPosition(mPosition + 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToPrevious() {
|
||||
if (mPosition == 0) {
|
||||
mPosition--;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPosition() {
|
||||
return mPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean move(int offset) {
|
||||
return moveToPosition(mPosition + offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean moveToLast() {
|
||||
return moveToPosition(getCount() - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean isLast() {
|
||||
return mPosition == (getCount() - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean isBeforeFirst() {
|
||||
return mPosition == -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean isAfterLast() {
|
||||
return mPosition == 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public constructor
|
||||
* @param name the name of the cache (used for logging)
|
||||
* @param baseProjection the projection used for cached cursors; queries whose columns are not
|
||||
* included in baseProjection will always generate a cache miss
|
||||
* @param maxSize the maximum number of content cursors to cache
|
||||
*/
|
||||
public ContentCache(String name, String[] baseProjection, int maxSize) {
|
||||
super();
|
||||
mName = name;
|
||||
mMaxSize = maxSize;
|
||||
mBaseProjection = baseProjection;
|
||||
mLogTag = "ContentCache-" + name;
|
||||
sContentCaches.add(this);
|
||||
mTokenList = new TokenList(mName);
|
||||
sActiveCursors = new CounterMap<Cursor>(maxSize);
|
||||
mStats = new Statistics(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the base projection for cached rows
|
||||
* Get the projection used for cached rows (typically, the largest possible projection)
|
||||
* @return
|
||||
*/
|
||||
public String[] getProjection() {
|
||||
return mBaseProjection;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a CacheToken for a row as specified by its id (_id column)
|
||||
* @param id the id of the record
|
||||
* @return a CacheToken needed in order to write data for the record back to the cache
|
||||
*/
|
||||
public synchronized CacheToken getCacheToken(String id) {
|
||||
// If another thread is already writing the data, return an invalid token
|
||||
CacheToken token = mTokenList.add(id);
|
||||
if (mLockMap.contains(id)) {
|
||||
token.invalidate();
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.util.LinkedHashMap#removeEldestEntry(java.util.Map.Entry)
|
||||
*/
|
||||
@Override
|
||||
public synchronized boolean removeEldestEntry(Map.Entry<String,Cursor> entry) {
|
||||
// If we're above the maximum size for this cache, remove the LRU cache entry
|
||||
if (size() > mMaxSize) {
|
||||
Cursor cursor = entry.getValue();
|
||||
// Close this cursor if it's no longer being used
|
||||
if (!sActiveCursors.contains(cursor)) {
|
||||
cursor.close();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to cache a cursor for the given id and projection; returns a valid cursor, either a
|
||||
* cached cursor (if caching was successful) or the original cursor
|
||||
*
|
||||
* @param c the cursor to be cached
|
||||
* @param id the record id (_id) of the content
|
||||
* @param projection the projection represented by the cursor
|
||||
* @return whether or not the cursor was cached
|
||||
*/
|
||||
public synchronized Cursor putCursor(Cursor c, String id, String[] projection,
|
||||
CacheToken token) {
|
||||
try {
|
||||
if (!token.isValid()) {
|
||||
if (DEBUG_CACHE) {
|
||||
Log.d(mLogTag, "============ Stale token for " + id);
|
||||
}
|
||||
mStats.mStaleCount++;
|
||||
return c;
|
||||
}
|
||||
if (c != null && projection == mBaseProjection) {
|
||||
if (DEBUG_CACHE) {
|
||||
Log.d(mLogTag, "============ Caching cursor for: " + id);
|
||||
}
|
||||
// If we've already cached this cursor, invalidate the older one
|
||||
Cursor existingCursor = get(id);
|
||||
if (existingCursor != null) {
|
||||
unlockImpl(id, null, false);
|
||||
}
|
||||
put(id, c);
|
||||
c.moveToFirst();
|
||||
return new CachedCursor(c, this, id);
|
||||
}
|
||||
return c;
|
||||
} finally {
|
||||
mTokenList.remove(token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and, if found, return a cursor, based on cached values, for the supplied id
|
||||
* @param id the _id column of the desired row
|
||||
* @param projection the requested projection for a query
|
||||
* @return a cursor based on cached values, or null if the row is not cached
|
||||
*/
|
||||
public synchronized Cursor getCachedCursor(String id, String[] projection) {
|
||||
if (Email.DEBUG) {
|
||||
// Every 200 calls to getCursor, report cache statistics
|
||||
dumpOnCount(200);
|
||||
}
|
||||
if (projection == mBaseProjection) {
|
||||
return getCachedCursorImpl(id);
|
||||
} else {
|
||||
return getMatrixCursor(id, projection);
|
||||
}
|
||||
}
|
||||
|
||||
private CachedCursor getCachedCursorImpl(String id) {
|
||||
Cursor c = get(id);
|
||||
if (c != null) {
|
||||
mStats.mHitCount++;
|
||||
return new CachedCursor(c, this, id);
|
||||
}
|
||||
mStats.mMissCount++;
|
||||
return null;
|
||||
}
|
||||
|
||||
private MatrixCursor getMatrixCursor(String id, String[] projection) {
|
||||
return getMatrixCursor(id, projection, null);
|
||||
}
|
||||
|
||||
private MatrixCursor getMatrixCursor(String id, String[] projection,
|
||||
ContentValues values) {
|
||||
Cursor c = get(id);
|
||||
if (c != null) {
|
||||
// Make a new MatrixCursor with the requested columns
|
||||
MatrixCursor mc = new MatrixCursor(projection, 1);
|
||||
Object[] row = new Object[projection.length];
|
||||
if (values != null) {
|
||||
// Make a copy; we don't want to change the original
|
||||
values = new ContentValues(values);
|
||||
}
|
||||
int i = 0;
|
||||
for (String column: projection) {
|
||||
int columnIndex = c.getColumnIndex(column);
|
||||
if (columnIndex < 0) {
|
||||
mStats.mProjectionMissCount++;
|
||||
return null;
|
||||
} else {
|
||||
String value;
|
||||
if (values != null && values.containsKey(column)) {
|
||||
Object val = values.get(column);
|
||||
if (val instanceof Boolean) {
|
||||
value = (val == Boolean.TRUE) ? "1" : "0";
|
||||
} else {
|
||||
value = values.getAsString(column);
|
||||
}
|
||||
values.remove(column);
|
||||
} else {
|
||||
value = c.getString(columnIndex);
|
||||
}
|
||||
row[i++] = value;
|
||||
}
|
||||
}
|
||||
if (values != null && values.size() != 0) {
|
||||
return null;
|
||||
}
|
||||
mc.addRow(row);
|
||||
mStats.mHitCount++;
|
||||
return mc;
|
||||
}
|
||||
mStats.mMissCount++;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock a given row, such that no new valid CacheTokens can be created for the passed-in id.
|
||||
* @param id the id of the row to lock
|
||||
*/
|
||||
public synchronized void lock(String id) {
|
||||
// Prevent new valid tokens from being created
|
||||
mLockMap.add(id);
|
||||
// Invalidate current tokens
|
||||
int count = mTokenList.invalidateTokens(id);
|
||||
if (DEBUG_TOKENS) {
|
||||
Log.d(mTokenList.mLogTag, "============ Lock invalidated " + count +
|
||||
" tokens for: " + id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock a given row, allowing new valid CacheTokens to be created for the passed-in id.
|
||||
* @param id the id of the item whose cursor is cached
|
||||
*/
|
||||
public synchronized void unlock(String id) {
|
||||
unlockImpl(id, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the row with id is currently cached, replaces the cached values with the supplied
|
||||
* ContentValues. Then, unlock the row, so that new valid CacheTokens can be created.
|
||||
*
|
||||
* @param id the id of the item whose cursor is cached
|
||||
* @param values updated values for this row
|
||||
*/
|
||||
public synchronized void unlock(String id, ContentValues values) {
|
||||
unlockImpl(id, values, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* If values are passed in, replaces any cached cursor with one containing new values, and
|
||||
* then closes the previously cached one (if any, and if not in use)
|
||||
* If values are not passed in, removes the row from cache
|
||||
* If the row was locked, unlock it
|
||||
* @param id the id of the row
|
||||
* @param values new ContentValues for the row (or null if row should simply be removed)
|
||||
* @param wasLocked whether or not the row was locked; if so, the lock will be removed
|
||||
*/
|
||||
public void unlockImpl(String id, ContentValues values, boolean wasLocked) {
|
||||
Cursor c = get(id);
|
||||
if (c != null) {
|
||||
if (DEBUG_CACHE) {
|
||||
Log.d(mLogTag, "=========== Unlocking cache for: " + id);
|
||||
}
|
||||
if (values != null) {
|
||||
MatrixCursor cursor = getMatrixCursor(id, mBaseProjection, values);
|
||||
if (cursor != null) {
|
||||
if (DEBUG_CACHE) {
|
||||
Log.d(mLogTag, "=========== Recaching with new values: " + id);
|
||||
}
|
||||
cursor.moveToFirst();
|
||||
put(id, cursor);
|
||||
} else {
|
||||
remove(id);
|
||||
}
|
||||
} else {
|
||||
remove(id);
|
||||
}
|
||||
// If there are no cursors using the old cached cursor, close it
|
||||
if (!sActiveCursors.contains(c)) {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
if (wasLocked) {
|
||||
mLockMap.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the entire cache, without logging
|
||||
*/
|
||||
public synchronized void invalidate() {
|
||||
invalidate(null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the entire cache; the arguments are used for logging only, and indicate the
|
||||
* write operation that caused the invalidation
|
||||
*
|
||||
* @param operation a string describing the operation causing the invalidate (or null)
|
||||
* @param uri the uri causing the invalidate (or null)
|
||||
* @param selection the selection used with the uri (or null)
|
||||
*/
|
||||
public synchronized void invalidate(String operation, Uri uri, String selection) {
|
||||
if (DEBUG_CACHE && (operation != null)) {
|
||||
Log.d(mLogTag, "============ INVALIDATED BY " + operation + ": " + uri +
|
||||
", SELECTION: " + selection);
|
||||
}
|
||||
mStats.mInvalidateCount++;
|
||||
// Close all cached cursors that are no longer in use
|
||||
for (String id: keySet()) {
|
||||
Cursor c = get(id);
|
||||
if (!sActiveCursors.contains(c)) {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
clear();
|
||||
// Invalidate all current tokens
|
||||
mTokenList.invalidate();
|
||||
}
|
||||
|
||||
// Debugging code below
|
||||
|
||||
private void dumpOnCount(int num) {
|
||||
mStats.mOpCount++;
|
||||
if ((mStats.mOpCount % num) == 0) {
|
||||
dumpStats();
|
||||
}
|
||||
}
|
||||
|
||||
/*package*/ void recordQueryTime(Cursor c, long nanoTime) {
|
||||
if (c instanceof CachedCursor) {
|
||||
mStats.hitTimes += nanoTime;
|
||||
mStats.hits++;
|
||||
} else {
|
||||
if (c.getCount() == 1) {
|
||||
mStats.missTimes += nanoTime;
|
||||
mStats.miss++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void notCacheable(Uri uri, String selection) {
|
||||
if (DEBUG_NOT_CACHEABLE) {
|
||||
sNotCacheable++;
|
||||
String str = uri.toString() + "$" + selection;
|
||||
sNotCacheableMap.add(str);
|
||||
}
|
||||
}
|
||||
|
||||
private static class CacheCounter implements Comparable<Object> {
|
||||
String uri;
|
||||
Integer count;
|
||||
|
||||
CacheCounter(String _uri, Integer _count) {
|
||||
uri = _uri;
|
||||
count = _count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Object another) {
|
||||
CacheCounter x = (CacheCounter)another;
|
||||
return x.count > count ? 1 : x.count == count ? 0 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static void dumpNotCacheableQueries() {
|
||||
int size = sNotCacheableMap.size();
|
||||
CacheCounter[] array = new CacheCounter[size];
|
||||
|
||||
int i = 0;
|
||||
for (Entry<String, Integer> entry: sNotCacheableMap.entrySet()) {
|
||||
array[i++] = new CacheCounter(entry.getKey(), entry.getValue());
|
||||
}
|
||||
Arrays.sort(array);
|
||||
for (CacheCounter cc: array) {
|
||||
Log.d("NotCacheable", cc.count + ": " + cc.uri);
|
||||
}
|
||||
}
|
||||
|
||||
static class Statistics {
|
||||
private final ContentCache mCache;
|
||||
private final String mName;
|
||||
|
||||
// Cache statistics
|
||||
// The item is in the cache AND is used to create a cursor
|
||||
private int mHitCount = 0;
|
||||
// Basic cache miss (the item is not cached)
|
||||
private int mMissCount = 0;
|
||||
// Incremented when a cachePut is invalid due to an intervening write
|
||||
private int mStaleCount = 0;
|
||||
// A projection miss occurs when the item is cached, but not all requested columns are
|
||||
// available in the base projection
|
||||
private int mProjectionMissCount = 0;
|
||||
// Incremented whenever the entire cache is invalidated
|
||||
private int mInvalidateCount = 0;
|
||||
// Count of operations put/get
|
||||
private int mOpCount = 0;
|
||||
// The following are for timing statistics
|
||||
private long hits = 0;
|
||||
private long hitTimes = 0;
|
||||
private long miss = 0;
|
||||
private long missTimes = 0;
|
||||
|
||||
// Used in toString() and addCacheStatistics()
|
||||
private int mCursorCount = 0;
|
||||
private int mTokenCount = 0;
|
||||
|
||||
Statistics(ContentCache cache) {
|
||||
mCache = cache;
|
||||
mName = mCache.mName;
|
||||
}
|
||||
|
||||
Statistics(String name) {
|
||||
mCache = null;
|
||||
mName = name;
|
||||
}
|
||||
|
||||
private void addCacheStatistics(ContentCache cache) {
|
||||
if (cache != null) {
|
||||
mHitCount += cache.mStats.mHitCount;
|
||||
mMissCount += cache.mStats.mMissCount;
|
||||
mProjectionMissCount += cache.mStats.mProjectionMissCount;
|
||||
mStaleCount += cache.mStats.mStaleCount;
|
||||
hitTimes += cache.mStats.hitTimes;
|
||||
missTimes += cache.mStats.missTimes;
|
||||
hits += cache.mStats.hits;
|
||||
miss += cache.mStats.miss;
|
||||
mCursorCount += cache.size();
|
||||
mTokenCount += cache.mTokenList.size();
|
||||
}
|
||||
}
|
||||
|
||||
private void append(StringBuilder sb, String name, Object value) {
|
||||
sb.append(", ");
|
||||
sb.append(name);
|
||||
sb.append(": ");
|
||||
sb.append(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (mHitCount + mMissCount == 0) return "No cache";
|
||||
int totalTries = mMissCount + mProjectionMissCount + mHitCount;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Cache " + mName);
|
||||
append(sb, "Cursors", mCache == null ? mCursorCount : mCache.size());
|
||||
append(sb, "Hits", mHitCount);
|
||||
append(sb, "Misses", mMissCount + mProjectionMissCount);
|
||||
append(sb, "Inval", mInvalidateCount);
|
||||
append(sb, "Tokens", mCache == null ? mTokenCount : mCache.mTokenList.size());
|
||||
append(sb, "Hit%", mHitCount * 100 / totalTries);
|
||||
append(sb, "\nHit time", hitTimes / 1000000.0 / hits);
|
||||
append(sb, "Miss time", missTimes / 1000000.0 / miss);
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static void dumpStats() {
|
||||
Statistics totals = new Statistics("Totals");
|
||||
|
||||
for (ContentCache cache: sContentCaches) {
|
||||
if (cache != null) {
|
||||
Log.d(cache.mName, cache.mStats.toString());
|
||||
totals.addCacheStatistics(cache);
|
||||
}
|
||||
}
|
||||
Log.d(totals.mName, totals.toString());
|
||||
}
|
||||
}
|
|
@ -2151,12 +2151,10 @@ public abstract class EmailContent {
|
|||
public int mSyncLookback;
|
||||
public int mSyncInterval;
|
||||
public long mSyncTime;
|
||||
public int mUnreadCount;
|
||||
public boolean mFlagVisible = true;
|
||||
public int mFlags;
|
||||
public int mVisibleLimit;
|
||||
public String mSyncStatus;
|
||||
public int mMessageCount;
|
||||
|
||||
public static final int CONTENT_ID_COLUMN = 0;
|
||||
public static final int CONTENT_DISPLAY_NAME_COLUMN = 1;
|
||||
|
@ -2169,19 +2167,17 @@ public abstract class EmailContent {
|
|||
public static final int CONTENT_SYNC_LOOKBACK_COLUMN = 8;
|
||||
public static final int CONTENT_SYNC_INTERVAL_COLUMN = 9;
|
||||
public static final int CONTENT_SYNC_TIME_COLUMN = 10;
|
||||
public static final int CONTENT_UNREAD_COUNT_COLUMN = 11;
|
||||
public static final int CONTENT_FLAG_VISIBLE_COLUMN = 12;
|
||||
public static final int CONTENT_FLAGS_COLUMN = 13;
|
||||
public static final int CONTENT_VISIBLE_LIMIT_COLUMN = 14;
|
||||
public static final int CONTENT_SYNC_STATUS_COLUMN = 15;
|
||||
public static final int CONTENT_MESSAGE_COUNT_COLUMN = 16;
|
||||
public static final int CONTENT_FLAG_VISIBLE_COLUMN = 11;
|
||||
public static final int CONTENT_FLAGS_COLUMN = 12;
|
||||
public static final int CONTENT_VISIBLE_LIMIT_COLUMN = 13;
|
||||
public static final int CONTENT_SYNC_STATUS_COLUMN = 14;
|
||||
public static final String[] CONTENT_PROJECTION = new String[] {
|
||||
RECORD_ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.SERVER_ID,
|
||||
MailboxColumns.PARENT_SERVER_ID, MailboxColumns.ACCOUNT_KEY, MailboxColumns.TYPE,
|
||||
MailboxColumns.DELIMITER, MailboxColumns.SYNC_KEY, MailboxColumns.SYNC_LOOKBACK,
|
||||
MailboxColumns.SYNC_INTERVAL, MailboxColumns.SYNC_TIME,MailboxColumns.UNREAD_COUNT,
|
||||
MailboxColumns.SYNC_INTERVAL, MailboxColumns.SYNC_TIME,
|
||||
MailboxColumns.FLAG_VISIBLE, MailboxColumns.FLAGS, MailboxColumns.VISIBLE_LIMIT,
|
||||
MailboxColumns.SYNC_STATUS, MailboxColumns.MESSAGE_COUNT
|
||||
MailboxColumns.SYNC_STATUS
|
||||
};
|
||||
|
||||
private static final String ACCOUNT_AND_MAILBOX_TYPE_SELECTION =
|
||||
|
@ -2321,12 +2317,10 @@ public abstract class EmailContent {
|
|||
mSyncLookback = cursor.getInt(CONTENT_SYNC_LOOKBACK_COLUMN);
|
||||
mSyncInterval = cursor.getInt(CONTENT_SYNC_INTERVAL_COLUMN);
|
||||
mSyncTime = cursor.getLong(CONTENT_SYNC_TIME_COLUMN);
|
||||
mUnreadCount = cursor.getInt(CONTENT_UNREAD_COUNT_COLUMN);
|
||||
mFlagVisible = cursor.getInt(CONTENT_FLAG_VISIBLE_COLUMN) == 1;
|
||||
mFlags = cursor.getInt(CONTENT_FLAGS_COLUMN);
|
||||
mVisibleLimit = cursor.getInt(CONTENT_VISIBLE_LIMIT_COLUMN);
|
||||
mSyncStatus = cursor.getString(CONTENT_SYNC_STATUS_COLUMN);
|
||||
mMessageCount = cursor.getInt(CONTENT_MESSAGE_COUNT_COLUMN);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -2343,12 +2337,10 @@ public abstract class EmailContent {
|
|||
values.put(MailboxColumns.SYNC_LOOKBACK, mSyncLookback);
|
||||
values.put(MailboxColumns.SYNC_INTERVAL, mSyncInterval);
|
||||
values.put(MailboxColumns.SYNC_TIME, mSyncTime);
|
||||
values.put(MailboxColumns.UNREAD_COUNT, mUnreadCount);
|
||||
values.put(MailboxColumns.FLAG_VISIBLE, mFlagVisible);
|
||||
values.put(MailboxColumns.FLAGS, mFlags);
|
||||
values.put(MailboxColumns.VISIBLE_LIMIT, mVisibleLimit);
|
||||
values.put(MailboxColumns.SYNC_STATUS, mSyncStatus);
|
||||
values.put(MailboxColumns.MESSAGE_COUNT, mMessageCount);
|
||||
return values;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package com.android.email.provider;
|
||||
|
||||
import com.android.email.Email;
|
||||
import com.android.email.provider.ContentCache.CacheToken;
|
||||
import com.android.email.provider.EmailContent.Account;
|
||||
import com.android.email.provider.EmailContent.AccountColumns;
|
||||
import com.android.email.provider.EmailContent.Attachment;
|
||||
|
@ -79,6 +80,16 @@ public class EmailProvider extends ContentProvider {
|
|||
|
||||
private static final String WHERE_ID = EmailContent.RECORD_ID + "=?";
|
||||
|
||||
// We'll cache the following four tables; sizes are best estimates of effective values
|
||||
private static final ContentCache sCacheAccount =
|
||||
new ContentCache("Account", Account.CONTENT_PROJECTION, 4);
|
||||
private static final ContentCache sCacheHostAuth =
|
||||
new ContentCache("HostAuth", HostAuth.CONTENT_PROJECTION, 8);
|
||||
/*package*/ static final ContentCache sCacheMailbox =
|
||||
new ContentCache("Mailbox", Mailbox.CONTENT_PROJECTION, 8);
|
||||
private static final ContentCache sCacheMessage =
|
||||
new ContentCache("Message", Message.CONTENT_PROJECTION, 3);
|
||||
|
||||
// Any changes to the database format *must* include update-in-place code.
|
||||
// Original version: 3
|
||||
// Version 4: Database wipe required; changing AccountManager interface w/Exchange
|
||||
|
@ -150,6 +161,8 @@ public class EmailProvider extends ContentProvider {
|
|||
|
||||
private static final int BASE_SHIFT = 12; // 12 bits to the base type: 0, 0x1000, 0x2000, etc.
|
||||
|
||||
// TABLE_NAMES MUST remain in the order of the BASE constants above (e.g. ACCOUNT_BASE = 0x0000,
|
||||
// MESSAGE_BASE = 0x1000, etc.)
|
||||
private static final String[] TABLE_NAMES = {
|
||||
EmailContent.Account.TABLE_NAME,
|
||||
EmailContent.Mailbox.TABLE_NAME,
|
||||
|
@ -161,6 +174,17 @@ public class EmailProvider extends ContentProvider {
|
|||
EmailContent.Body.TABLE_NAME
|
||||
};
|
||||
|
||||
// CONTENT_CACHES MUST remain in the order of the BASE constants above
|
||||
private static final ContentCache[] CONTENT_CACHES = {
|
||||
sCacheAccount,
|
||||
sCacheMailbox,
|
||||
sCacheMessage,
|
||||
null,
|
||||
sCacheHostAuth,
|
||||
null,
|
||||
null,
|
||||
null};
|
||||
|
||||
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
|
||||
/**
|
||||
|
@ -892,6 +916,8 @@ public class EmailProvider extends ContentProvider {
|
|||
Log.v(TAG, "EmailProvider.delete: uri=" + uri + ", match is " + match);
|
||||
}
|
||||
|
||||
ContentCache cache = CONTENT_CACHES[table];
|
||||
String tableName = TABLE_NAMES[table];
|
||||
int result = -1;
|
||||
|
||||
try {
|
||||
|
@ -912,7 +938,6 @@ public class EmailProvider extends ContentProvider {
|
|||
// 3) End the transaction, committing all changes atomically
|
||||
//
|
||||
// Bodies are auto-deleted here; Attachments are auto-deleted via trigger
|
||||
|
||||
messageDeletion = true;
|
||||
db.beginTransaction();
|
||||
break;
|
||||
|
@ -935,13 +960,40 @@ public class EmailProvider extends ContentProvider {
|
|||
db.execSQL(DELETED_MESSAGE_INSERT + id);
|
||||
db.execSQL(UPDATED_MESSAGE_DELETE + id);
|
||||
}
|
||||
result = db.delete(TABLE_NAMES[table], whereWithId(id, selection),
|
||||
selectionArgs);
|
||||
if (cache != null) {
|
||||
cache.lock(id);
|
||||
}
|
||||
try {
|
||||
result = db.delete(tableName, whereWithId(id, selection), selectionArgs);
|
||||
if (cache != null) {
|
||||
switch(match) {
|
||||
case ACCOUNT_ID:
|
||||
// Account deletion will clear all of the caches, as HostAuth's,
|
||||
// Mailboxes, and Messages will be deleted in the process
|
||||
sCacheMailbox.invalidate("Delete", uri, selection);
|
||||
sCacheHostAuth.invalidate("Delete", uri, selection);
|
||||
//$FALL-THROUGH$
|
||||
case MAILBOX_ID:
|
||||
// Mailbox deletion will clear the Message cache
|
||||
sCacheMessage.invalidate("Delete", uri, selection);
|
||||
//$FALL-THROUGH$
|
||||
case SYNCED_MESSAGE_ID:
|
||||
case MESSAGE_ID:
|
||||
case HOSTAUTH_ID:
|
||||
cache.invalidate("Delete", uri, selection);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cache != null) {
|
||||
cache.unlock(id);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ATTACHMENTS_MESSAGE_ID:
|
||||
// All attachments for the given message
|
||||
id = uri.getPathSegments().get(2);
|
||||
result = db.delete(TABLE_NAMES[table],
|
||||
result = db.delete(tableName,
|
||||
whereWith(Attachment.MESSAGE_KEY + "=" + id, selection), selectionArgs);
|
||||
break;
|
||||
|
||||
|
@ -953,7 +1005,21 @@ public class EmailProvider extends ContentProvider {
|
|||
case MAILBOX:
|
||||
case ACCOUNT:
|
||||
case HOSTAUTH:
|
||||
result = db.delete(TABLE_NAMES[table], selection, selectionArgs);
|
||||
switch(match) {
|
||||
// See the comments above for deletion of ACCOUNT_ID, etc
|
||||
case ACCOUNT:
|
||||
sCacheMailbox.invalidate("Delete", uri, selection);
|
||||
sCacheHostAuth.invalidate("Delete", uri, selection);
|
||||
//$FALL-THROUGH$
|
||||
case MAILBOX:
|
||||
sCacheMessage.invalidate("Delete", uri, selection);
|
||||
//$FALL-THROUGH$
|
||||
case MESSAGE:
|
||||
case HOSTAUTH:
|
||||
cache.invalidate("Delete", uri, selection);
|
||||
break;
|
||||
}
|
||||
result = db.delete(tableName, selection, selectionArgs);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -1150,6 +1216,10 @@ public class EmailProvider extends ContentProvider {
|
|||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
|
||||
String sortOrder) {
|
||||
long time = 0L;
|
||||
if (Email.DEBUG) {
|
||||
time = System.nanoTime();
|
||||
}
|
||||
if (Email.DEBUG_THREAD_CHECK) Email.warnIfUiThread();
|
||||
Cursor c = null;
|
||||
int match = sURIMatcher.match(uri);
|
||||
|
@ -1164,6 +1234,17 @@ public class EmailProvider extends ContentProvider {
|
|||
Log.v(TAG, "EmailProvider.query: uri=" + uri + ", match is " + match);
|
||||
}
|
||||
|
||||
// Find the cache for this query's table (if any)
|
||||
ContentCache cache = null;
|
||||
String tableName = TABLE_NAMES[table];
|
||||
// We can only use the cache if there's no selection
|
||||
if (selection == null) {
|
||||
cache = CONTENT_CACHES[table];
|
||||
}
|
||||
if (cache == null) {
|
||||
ContentCache.notCacheable(uri, selection);
|
||||
}
|
||||
|
||||
try {
|
||||
switch (match) {
|
||||
case BODY:
|
||||
|
@ -1174,7 +1255,7 @@ public class EmailProvider extends ContentProvider {
|
|||
case MAILBOX:
|
||||
case ACCOUNT:
|
||||
case HOSTAUTH:
|
||||
c = db.query(TABLE_NAMES[table], projection,
|
||||
c = db.query(tableName, projection,
|
||||
selection, selectionArgs, null, null, sortOrder, limit);
|
||||
break;
|
||||
case BODY_ID:
|
||||
|
@ -1186,9 +1267,20 @@ public class EmailProvider extends ContentProvider {
|
|||
case ACCOUNT_ID:
|
||||
case HOSTAUTH_ID:
|
||||
id = uri.getPathSegments().get(1);
|
||||
c = db.query(TABLE_NAMES[table], projection,
|
||||
whereWithId(id, selection), selectionArgs, null, null, sortOrder,
|
||||
limit);
|
||||
if (cache != null) {
|
||||
c = cache.getCachedCursor(id, projection);
|
||||
}
|
||||
if (c == null) {
|
||||
CacheToken token = null;
|
||||
if (cache != null) {
|
||||
token = cache.getCacheToken(id);
|
||||
}
|
||||
c = db.query(tableName, projection, whereWithId(id, selection),
|
||||
selectionArgs, null, null, sortOrder, limit);
|
||||
if (cache != null) {
|
||||
c = cache.putCursor(c, id, projection, token);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ATTACHMENTS_MESSAGE_ID:
|
||||
// All attachments for the given message
|
||||
|
@ -1203,6 +1295,14 @@ public class EmailProvider extends ContentProvider {
|
|||
} catch (SQLiteException e) {
|
||||
checkDatabases();
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
checkDatabases();
|
||||
e.printStackTrace();
|
||||
throw e;
|
||||
} finally {
|
||||
if (cache != null && Email.DEBUG) {
|
||||
cache.recordQueryTime(c, System.nanoTime() - time);
|
||||
}
|
||||
}
|
||||
|
||||
if ((c != null) && !isTemporary()) {
|
||||
|
@ -1276,7 +1376,10 @@ public class EmailProvider extends ContentProvider {
|
|||
values.remove(MailboxColumns.MESSAGE_COUNT);
|
||||
}
|
||||
|
||||
ContentCache cache = CONTENT_CACHES[table];
|
||||
String tableName = TABLE_NAMES[table];
|
||||
String id;
|
||||
|
||||
try {
|
||||
switch (match) {
|
||||
case MAILBOX_ID_ADD_TO_FIELD:
|
||||
|
@ -1288,7 +1391,7 @@ public class EmailProvider extends ContentProvider {
|
|||
if (field == null || add == null) {
|
||||
throw new IllegalArgumentException("No field/add specified " + uri);
|
||||
}
|
||||
Cursor c = db.query(TABLE_NAMES[table],
|
||||
Cursor c = db.query(tableName,
|
||||
new String[] {EmailContent.RECORD_ID, field},
|
||||
whereWithId(id, selection),
|
||||
selectionArgs, null, null, null);
|
||||
|
@ -1300,7 +1403,7 @@ public class EmailProvider extends ContentProvider {
|
|||
bind[0] = c.getString(0);
|
||||
long value = c.getLong(1) + add;
|
||||
cv.put(field, value);
|
||||
result = db.update(TABLE_NAMES[table], cv, ID_EQUALS, bind);
|
||||
result = db.update(tableName, cv, ID_EQUALS, bind);
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
|
@ -1317,17 +1420,30 @@ public class EmailProvider extends ContentProvider {
|
|||
case ACCOUNT_ID:
|
||||
case HOSTAUTH_ID:
|
||||
id = uri.getPathSegments().get(1);
|
||||
if (match == SYNCED_MESSAGE_ID) {
|
||||
// For synced messages, first copy the old message to the updated table
|
||||
// Note the insert or ignore semantics, guaranteeing that only the first
|
||||
// update will be reflected in the updated message table; therefore this row
|
||||
// will always have the "original" data
|
||||
db.execSQL(UPDATED_MESSAGE_INSERT + id);
|
||||
} else if (match == MESSAGE_ID) {
|
||||
db.execSQL(UPDATED_MESSAGE_DELETE + id);
|
||||
if (cache != null) {
|
||||
cache.lock(id);
|
||||
}
|
||||
try {
|
||||
if (match == SYNCED_MESSAGE_ID) {
|
||||
// For synced messages, first copy the old message to the updated table
|
||||
// Note the insert or ignore semantics, guaranteeing that only the first
|
||||
// update will be reflected in the updated message table; therefore this
|
||||
// row will always have the "original" data
|
||||
db.execSQL(UPDATED_MESSAGE_INSERT + id);
|
||||
} else if (match == MESSAGE_ID) {
|
||||
db.execSQL(UPDATED_MESSAGE_DELETE + id);
|
||||
}
|
||||
result = db.update(tableName, values, whereWithId(id, selection),
|
||||
selectionArgs);
|
||||
} catch (SQLiteException e) {
|
||||
// Null out values (so they aren't cached) and re-throw
|
||||
values = null;
|
||||
throw e;
|
||||
} finally {
|
||||
if (cache != null) {
|
||||
cache.unlock(id, values);
|
||||
}
|
||||
}
|
||||
result = db.update(TABLE_NAMES[table], values, whereWithId(id, selection),
|
||||
selectionArgs);
|
||||
if (match == ATTACHMENT_ID) {
|
||||
if (values.containsKey(Attachment.FLAGS)) {
|
||||
int flags = values.getAsInteger(Attachment.FLAGS);
|
||||
|
@ -1343,16 +1459,26 @@ public class EmailProvider extends ContentProvider {
|
|||
case MAILBOX:
|
||||
case ACCOUNT:
|
||||
case HOSTAUTH:
|
||||
result = db.update(TABLE_NAMES[table], values, selection, selectionArgs);
|
||||
switch(match) {
|
||||
case MESSAGE:
|
||||
case ACCOUNT:
|
||||
case MAILBOX:
|
||||
case HOSTAUTH:
|
||||
// If we're doing some generic update, the whole cache needs to be
|
||||
// invalidated. This case should be quite rare
|
||||
cache.invalidate("Update", uri, selection);
|
||||
break;
|
||||
}
|
||||
result = db.update(tableName, values, selection, selectionArgs);
|
||||
break;
|
||||
case ACCOUNT_RESET_NEW_COUNT_ID:
|
||||
id = uri.getPathSegments().get(1);
|
||||
result = db.update(TABLE_NAMES[table], CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT,
|
||||
result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT,
|
||||
whereWithId(id, selection), selectionArgs);
|
||||
notificationUri = Account.CONTENT_URI; // Only notify account cursors.
|
||||
break;
|
||||
case ACCOUNT_RESET_NEW_COUNT:
|
||||
result = db.update(TABLE_NAMES[table], CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT,
|
||||
result = db.update(tableName, CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT,
|
||||
selection, selectionArgs);
|
||||
notificationUri = Account.CONTENT_URI; // Only notify account cursors.
|
||||
break;
|
||||
|
|
|
@ -758,7 +758,6 @@ public class LegacyConversionsTests extends ProviderTestCase2<EmailProvider> {
|
|||
assertEquals(0, toMailbox.mSyncLookback);
|
||||
assertEquals(0, toMailbox.mSyncInterval);
|
||||
assertEquals(0, toMailbox.mSyncTime);
|
||||
assertEquals(100, toMailbox.mUnreadCount);
|
||||
assertTrue(toMailbox.mFlagVisible);
|
||||
assertEquals(0, toMailbox.mFlags);
|
||||
assertEquals(Email.VISIBLE_LIMIT_DEFAULT, toMailbox.mVisibleLimit);
|
||||
|
|
|
@ -0,0 +1,264 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.email.provider;
|
||||
|
||||
import com.android.email.provider.ContentCache.CacheToken;
|
||||
import com.android.email.provider.ContentCache.CachedCursor;
|
||||
import com.android.email.provider.ContentCache.TokenList;
|
||||
import com.android.email.provider.EmailContent.Account;
|
||||
import com.android.email.provider.EmailContent.Mailbox;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.CursorWrapper;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.test.ProviderTestCase2;
|
||||
|
||||
/**
|
||||
* Tests of ContentCache
|
||||
*
|
||||
* You can run this entire test case with:
|
||||
* runtest -c com.android.email.provider.ContentCacheTests email
|
||||
*/
|
||||
public class ContentCacheTests extends ProviderTestCase2<EmailProvider> {
|
||||
|
||||
EmailProvider mProvider;
|
||||
Context mMockContext;
|
||||
|
||||
public ContentCacheTests() {
|
||||
super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
mMockContext = getMockContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tearDown() throws Exception {
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
public void testCounterMap() {
|
||||
ContentCache.CounterMap<String> map = new ContentCache.CounterMap<String>(4);
|
||||
// Make sure we can find added items
|
||||
map.add("1");
|
||||
assertTrue(map.contains("1"));
|
||||
map.add("2");
|
||||
map.add("2");
|
||||
// Make sure we can remove once for each add
|
||||
map.remove("2");
|
||||
assertTrue(map.contains("2"));
|
||||
map.remove("2");
|
||||
// Make sure that over-removing throws an exception
|
||||
try {
|
||||
map.remove("2");
|
||||
fail("Removing a third time should throw an exception");
|
||||
} catch (IllegalStateException e) {
|
||||
}
|
||||
try {
|
||||
map.remove("3");
|
||||
fail("Removing object never added should throw an exception");
|
||||
} catch (IllegalStateException e) {
|
||||
}
|
||||
// There should only be one item in the map ("1")
|
||||
assertEquals(1, map.size());
|
||||
assertTrue(map.contains("1"));
|
||||
}
|
||||
|
||||
public void testTokenList() {
|
||||
TokenList list = new TokenList("Name");
|
||||
|
||||
// Add two tokens for "1"
|
||||
CacheToken token1a = list.add("1");
|
||||
assertTrue(token1a.isValid());
|
||||
assertEquals("1", token1a.getId());
|
||||
assertEquals(1, list.size());
|
||||
CacheToken token1b = list.add("1");
|
||||
assertTrue(token1b.isValid());
|
||||
assertEquals("1", token1b.getId());
|
||||
assertTrue(token1a.equals(token1b));
|
||||
assertEquals(2, list.size());
|
||||
|
||||
// Add a token for "2"
|
||||
CacheToken token2 = list.add("2");
|
||||
assertFalse(token1a.equals(token2));
|
||||
assertEquals(3, list.size());
|
||||
|
||||
// Invalidate "1"; there should be two tokens invalidated
|
||||
assertEquals(2, list.invalidateTokens("1"));
|
||||
assertFalse(token1a.isValid());
|
||||
assertFalse(token1b.isValid());
|
||||
// Token2 should still be valid
|
||||
assertTrue(token2.isValid());
|
||||
// Only token2 should be in the list now (invalidation removes tokens)
|
||||
assertEquals(1, list.size());
|
||||
assertEquals(token2, list.get(0));
|
||||
|
||||
// Add 3 tokens for "3"
|
||||
CacheToken token3a = list.add("3");
|
||||
CacheToken token3b = list.add("3");
|
||||
CacheToken token3c = list.add("3");
|
||||
// Remove two of them
|
||||
assertTrue(list.remove(token3a));
|
||||
assertTrue(list.remove(token3b));
|
||||
// Removing tokens doesn't invalidate them
|
||||
assertTrue(token3a.isValid());
|
||||
assertTrue(token3b.isValid());
|
||||
assertTrue(token3c.isValid());
|
||||
// There should be two items left "3" and "2"
|
||||
assertEquals(2, list.size());
|
||||
}
|
||||
|
||||
public void testCachedCursors() {
|
||||
final ContentResolver resolver = mMockContext.getContentResolver();
|
||||
final Context context = mMockContext;
|
||||
|
||||
// Create account and two mailboxes
|
||||
Account acct = ProviderTestUtils.setupAccount("account", true, context);
|
||||
ProviderTestUtils.setupMailbox("box1", acct.mId, true, context);
|
||||
Mailbox box = ProviderTestUtils.setupMailbox("box2", acct.mId, true, context);
|
||||
|
||||
// We need to test with a query that only returns one row (others can't be put in a
|
||||
// CachedCursor)
|
||||
Uri uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, box.mId);
|
||||
Cursor cursor =
|
||||
resolver.query(uri, Mailbox.CONTENT_PROJECTION, null, null, null);
|
||||
// ContentResolver gives us back a wrapper
|
||||
assertTrue(cursor instanceof CursorWrapper);
|
||||
// The wrappedCursor should be a CachedCursor
|
||||
Cursor wrappedCursor = ((CursorWrapper)cursor).getWrappedCursor();
|
||||
assertTrue(wrappedCursor instanceof CachedCursor);
|
||||
CachedCursor cachedCursor = (CachedCursor)wrappedCursor;
|
||||
// The cursor wrapped in cachedCursor is the underlying cursor
|
||||
Cursor activeCursor = cachedCursor.getWrappedCursor();
|
||||
|
||||
// The cursor should be in active cursors
|
||||
Integer activeCount = ContentCache.sActiveCursors.get(activeCursor);
|
||||
assertNotNull(activeCount);
|
||||
assertEquals(1, activeCount.intValue());
|
||||
|
||||
// Some basic functionality that shouldn't throw exceptions and should otherwise act as the
|
||||
// underlying cursor would
|
||||
String[] columnNames = cursor.getColumnNames();
|
||||
assertEquals(Mailbox.CONTENT_PROJECTION.length, columnNames.length);
|
||||
for (int i = 0; i < Mailbox.CONTENT_PROJECTION.length; i++) {
|
||||
assertEquals(Mailbox.CONTENT_PROJECTION[i], columnNames[i]);
|
||||
}
|
||||
|
||||
assertEquals(1, cursor.getCount());
|
||||
cursor.moveToNext();
|
||||
assertEquals(0, cursor.getPosition());
|
||||
cursor.moveToPosition(0);
|
||||
assertEquals(0, cursor.getPosition());
|
||||
// And something that should
|
||||
try {
|
||||
cursor.moveToPosition(1);
|
||||
fail("Shouldn't be able to move to position > 0");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Correct
|
||||
}
|
||||
|
||||
cursor.close();
|
||||
// We've closed the cached cursor; make sure
|
||||
assertTrue(cachedCursor.isClosed());
|
||||
// The underlying cursor shouldn't be closed because it's in a cache (we'll test
|
||||
// that in testContentCache)
|
||||
assertFalse(activeCursor.isClosed());
|
||||
// Our cursor should no longer be in the active cursors map
|
||||
activeCount = ContentCache.sActiveCursors.get(activeCursor);
|
||||
assertNull(activeCount);
|
||||
|
||||
// Make sure that we won't accept cursors with multiple rows
|
||||
cursor = resolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, null, null, null);
|
||||
try {
|
||||
cursor = new CachedCursor(cursor, null, "Foo");
|
||||
fail("Mustn't accept cursor with more than one row");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Correct
|
||||
}
|
||||
}
|
||||
|
||||
private static final String[] SIMPLE_PROJECTION = new String[] {"Foo"};
|
||||
private static final Object[] SIMPLE_ROW = new Object[] {"Bar"};
|
||||
private Cursor getOneRowCursor() {
|
||||
MatrixCursor cursor = new MatrixCursor(SIMPLE_PROJECTION, 1);
|
||||
cursor.addRow(SIMPLE_ROW);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public void testContentCacheRemoveEldestEntry() {
|
||||
// Create a cache of size 2
|
||||
ContentCache cache = new ContentCache("Name", SIMPLE_PROJECTION, 2);
|
||||
// Random cursor; what's in it doesn't matter
|
||||
Cursor cursor1 = getOneRowCursor();
|
||||
// Get a token for arbitrary object named "1"
|
||||
CacheToken token = cache.getCacheToken("1");
|
||||
// Put the cursor in the cache
|
||||
cache.putCursor(cursor1, "1", SIMPLE_PROJECTION, token);
|
||||
assertEquals(1, cache.size());
|
||||
|
||||
// Add another random cursor; what's in it doesn't matter
|
||||
Cursor cursor2 = getOneRowCursor();
|
||||
// Get a token for arbitrary object named "2"
|
||||
token = cache.getCacheToken("2");
|
||||
// Put the cursor in the cache
|
||||
cache.putCursor(cursor1, "2", SIMPLE_PROJECTION, token);
|
||||
assertEquals(2, cache.size());
|
||||
|
||||
// We should be able to find both now in the cache
|
||||
Cursor cachedCursor = cache.getCachedCursor("1", SIMPLE_PROJECTION);
|
||||
assertNotNull(cachedCursor);
|
||||
assertTrue(cachedCursor instanceof CachedCursor);
|
||||
cachedCursor = cache.getCachedCursor("2", SIMPLE_PROJECTION);
|
||||
assertNotNull(cachedCursor);
|
||||
assertTrue(cachedCursor instanceof CachedCursor);
|
||||
|
||||
// Both cursors should be open
|
||||
assertFalse(cursor1.isClosed());
|
||||
assertFalse(cursor2.isClosed());
|
||||
|
||||
// Add another random cursor; what's in it doesn't matter
|
||||
Cursor cursor3 = getOneRowCursor();
|
||||
// Get a token for arbitrary object named "3"
|
||||
token = cache.getCacheToken("3");
|
||||
// Put the cursor in the cache
|
||||
cache.putCursor(cursor1, "3", SIMPLE_PROJECTION, token);
|
||||
// We should never have more than 2 entries in the cache
|
||||
assertEquals(2, cache.size());
|
||||
|
||||
// The first cursor we added should no longer be in the cache (it's the eldest)
|
||||
cachedCursor = cache.getCachedCursor("1", SIMPLE_PROJECTION);
|
||||
assertNull(cachedCursor);
|
||||
// The cursors for 2 and 3 should be cached
|
||||
cachedCursor = cache.getCachedCursor("2", SIMPLE_PROJECTION);
|
||||
assertNotNull(cachedCursor);
|
||||
assertTrue(cachedCursor instanceof CachedCursor);
|
||||
cachedCursor = cache.getCachedCursor("3", SIMPLE_PROJECTION);
|
||||
assertNotNull(cachedCursor);
|
||||
assertTrue(cachedCursor instanceof CachedCursor);
|
||||
|
||||
// Even cursor1 should be open, since all cached cursors are in mActiveCursors until closed
|
||||
assertFalse(cursor1.isClosed());
|
||||
assertFalse(cursor2.isClosed());
|
||||
assertFalse(cursor3.isClosed());
|
||||
}
|
||||
}
|
|
@ -123,8 +123,6 @@ public class ProviderTestUtils extends Assert {
|
|||
box.mSyncLookback = 2;
|
||||
box.mSyncInterval = EmailContent.Account.CHECK_INTERVAL_NEVER;
|
||||
box.mSyncTime = 3;
|
||||
// Should always be saved as zero
|
||||
box.mUnreadCount = 0;
|
||||
box.mFlagVisible = true;
|
||||
box.mFlags = 5;
|
||||
box.mVisibleLimit = 6;
|
||||
|
@ -347,7 +345,6 @@ public class ProviderTestUtils extends Assert {
|
|||
assertEquals(caller + " mSyncLookback", expect.mSyncLookback, actual.mSyncLookback);
|
||||
assertEquals(caller + " mSyncInterval", expect.mSyncInterval, actual.mSyncInterval);
|
||||
assertEquals(caller + " mSyncTime", expect.mSyncTime, actual.mSyncTime);
|
||||
assertEquals(caller + " mUnreadCount", expect.mUnreadCount, actual.mUnreadCount);
|
||||
assertEquals(caller + " mFlagVisible", expect.mFlagVisible, actual.mFlagVisible);
|
||||
assertEquals(caller + " mFlags", expect.mFlags, actual.mFlags);
|
||||
assertEquals(caller + " mVisibleLimit", expect.mVisibleLimit, actual.mVisibleLimit);
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package com.android.email.provider;
|
||||
|
||||
import com.android.email.Snippet;
|
||||
import com.android.email.Utility;
|
||||
import com.android.email.provider.EmailContent.Account;
|
||||
import com.android.email.provider.EmailContent.AccountColumns;
|
||||
import com.android.email.provider.EmailContent.Attachment;
|
||||
|
@ -1647,36 +1648,6 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
|
|||
assertEquals(newStr, oldStr);
|
||||
}
|
||||
|
||||
public void testIdAddToField() {
|
||||
ContentResolver cr = mMockContext.getContentResolver();
|
||||
ContentValues cv = new ContentValues();
|
||||
|
||||
// Try changing the newMessageCount of an account
|
||||
Account account = ProviderTestUtils.setupAccount("field-add", true, mMockContext);
|
||||
int startCount = account.mNewMessageCount;
|
||||
// "field" and "add" are the two required elements
|
||||
cv.put(EmailContent.FIELD_COLUMN_NAME, AccountColumns.NEW_MESSAGE_COUNT);
|
||||
cv.put(EmailContent.ADD_COLUMN_NAME, 17);
|
||||
cr.update(ContentUris.withAppendedId(Account.ADD_TO_FIELD_URI, account.mId),
|
||||
cv, null, null);
|
||||
Account restoredAccount = Account.restoreAccountWithId(mMockContext, account.mId);
|
||||
assertEquals(17 + startCount, restoredAccount.mNewMessageCount);
|
||||
cv.put(EmailContent.ADD_COLUMN_NAME, -11);
|
||||
cr.update(ContentUris.withAppendedId(Account.ADD_TO_FIELD_URI, account.mId),
|
||||
cv, null, null);
|
||||
restoredAccount = Account.restoreAccountWithId(mMockContext, account.mId);
|
||||
assertEquals(17 - 11 + startCount, restoredAccount.mNewMessageCount);
|
||||
|
||||
// Now try with a mailbox
|
||||
Mailbox boxA = ProviderTestUtils.setupMailbox("boxA", account.mId, true, mMockContext);
|
||||
assertEquals(0, boxA.mUnreadCount);
|
||||
cv.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.UNREAD_COUNT);
|
||||
cv.put(EmailContent.ADD_COLUMN_NAME, 11);
|
||||
cr.update(ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, boxA.mId), cv, null, null);
|
||||
Mailbox restoredBoxA = Mailbox.restoreMailboxWithId(mMockContext, boxA.mId);
|
||||
assertEquals(11, restoredBoxA.mUnreadCount);
|
||||
}
|
||||
|
||||
public void testDatabaseCorruptionRecovery() {
|
||||
final ContentResolver resolver = mMockContext.getContentResolver();
|
||||
final Context context = mMockContext;
|
||||
|
@ -1900,8 +1871,9 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
|
|||
* @return the number of messages in a mailbox.
|
||||
*/
|
||||
private int getMessageCount(long mailboxId) {
|
||||
Mailbox b = Mailbox.restoreMailboxWithId(mMockContext, mailboxId);
|
||||
return b.mMessageCount;
|
||||
return Utility.getFirstRowInt(mMockContext,
|
||||
ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
|
||||
new String[] {MailboxColumns.MESSAGE_COUNT}, null, null, null, 0);
|
||||
}
|
||||
|
||||
/** Set -1 to the message count of all mailboxes for the recalculateMessageCount test. */
|
||||
|
|
Loading…
Reference in New Issue