diff --git a/src/com/android/email/LegacyConversions.java b/src/com/android/email/LegacyConversions.java index eebc0b06d..c6acece7e 100644 --- a/src/com/android/email/LegacyConversions.java +++ b/src/com/android/email/LegacyConversions.java @@ -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; diff --git a/src/com/android/email/provider/ContentCache.java b/src/com/android/email/provider/ContentCache.java new file mode 100644 index 000000000..28e3bdf48 --- /dev/null +++ b/src/com/android/email/provider/ContentCache.java @@ -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 { + 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 sNotCacheableMap = new CounterMap(); + + // All defined caches + private static final ArrayList sContentCaches = new ArrayList(); + // 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 sActiveCursors; + + // A set of locked content id's + private final CounterMap mLockMap = new CounterMap(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 extends HashMap { + 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 { + 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 removeList = new ArrayList(); + 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(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 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 { + 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 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()); + } +} diff --git a/src/com/android/email/provider/EmailContent.java b/src/com/android/email/provider/EmailContent.java index cb9795929..3ffbe822b 100644 --- a/src/com/android/email/provider/EmailContent.java +++ b/src/com/android/email/provider/EmailContent.java @@ -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; } diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java index ae98d1249..79559b358 100644 --- a/src/com/android/email/provider/EmailProvider.java +++ b/src/com/android/email/provider/EmailProvider.java @@ -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; diff --git a/tests/src/com/android/email/LegacyConversionsTests.java b/tests/src/com/android/email/LegacyConversionsTests.java index 467b796d1..fd4be31f5 100644 --- a/tests/src/com/android/email/LegacyConversionsTests.java +++ b/tests/src/com/android/email/LegacyConversionsTests.java @@ -758,7 +758,6 @@ public class LegacyConversionsTests extends ProviderTestCase2 { 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); diff --git a/tests/src/com/android/email/provider/ContentCacheTests.java b/tests/src/com/android/email/provider/ContentCacheTests.java new file mode 100644 index 000000000..666b92bd4 --- /dev/null +++ b/tests/src/com/android/email/provider/ContentCacheTests.java @@ -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 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 map = new ContentCache.CounterMap(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()); + } +} diff --git a/tests/src/com/android/email/provider/ProviderTestUtils.java b/tests/src/com/android/email/provider/ProviderTestUtils.java index a5a3339d5..7c56b5338 100644 --- a/tests/src/com/android/email/provider/ProviderTestUtils.java +++ b/tests/src/com/android/email/provider/ProviderTestUtils.java @@ -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); diff --git a/tests/src/com/android/email/provider/ProviderTests.java b/tests/src/com/android/email/provider/ProviderTests.java index 2e1229b10..f1863668c 100644 --- a/tests/src/com/android/email/provider/ProviderTests.java +++ b/tests/src/com/android/email/provider/ProviderTests.java @@ -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 { 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 { * @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. */