Home | History | Annotate | Download | only in provider
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.email.provider;
     18 
     19 import android.content.ContentValues;
     20 import android.database.CrossProcessCursor;
     21 import android.database.Cursor;
     22 import android.database.CursorWindow;
     23 import android.database.CursorWrapper;
     24 import android.database.MatrixCursor;
     25 import android.net.Uri;
     26 import android.util.Log;
     27 import android.util.LruCache;
     28 
     29 import com.android.email.Email;
     30 import com.google.common.annotations.VisibleForTesting;
     31 
     32 import java.util.ArrayList;
     33 import java.util.Arrays;
     34 import java.util.HashMap;
     35 import java.util.Map;
     36 import java.util.Set;
     37 
     38 /**
     39  * An LRU cache for EmailContent (Account, HostAuth, Mailbox, and Message, thus far).  The intended
     40  * user of this cache is EmailProvider itself; caching is entirely transparent to users of the
     41  * provider.
     42  *
     43  * Usage examples; id is a String representation of a row id (_id), as it might be retrieved from
     44  * a uri via getPathSegment
     45  *
     46  * To create a cache:
     47  *    ContentCache cache = new ContentCache(name, projection, max);
     48  *
     49  * To (try to) get a cursor from a cache:
     50  *    Cursor cursor = cache.getCursor(id, projection);
     51  *
     52  * To read from a table and cache the resulting cursor:
     53  * 1. Get a CacheToken: CacheToken token = cache.getToken(id);
     54  * 2. Get a cursor from the database: Cursor cursor = db.query(....);
     55  * 3. Put the cursor in the cache: cache.putCursor(cursor, id, token);
     56  * Only cursors with the projection given in the definition of the cache can be cached
     57  *
     58  * To delete one or more rows or update multiple rows from a table that uses cached data:
     59  * 1. Lock the row in the cache: cache.lock(id);
     60  * 2. Delete/update the row(s): db.delete(...);
     61  * 3. Invalidate any other caches that might be affected by the delete/update:
     62  *      The entire cache: affectedCache.invalidate()*
     63  *      A specific row in a cache: affectedCache.invalidate(rowId)
     64  * 4. Unlock the row in the cache: cache.unlock(id);
     65  *
     66  * To update a single row from a table that uses cached data:
     67  * 1. Lock the row in the cache: cache.lock(id);
     68  * 2. Update the row: db.update(...);
     69  * 3. Unlock the row in the cache, passing in the new values: cache.unlock(id, values);
     70  *
     71  * Synchronization note: All of the public methods in ContentCache are synchronized (i.e. on the
     72  * cache itself) except for methods that are solely used for debugging and do not modify the cache.
     73  * All references to ContentCache that are external to the ContentCache class MUST synchronize on
     74  * the ContentCache instance (e.g. CachedCursor.close())
     75  */
     76 public final class ContentCache {
     77     private static final boolean DEBUG_CACHE = false;  // DO NOT CHECK IN TRUE
     78     private static final boolean DEBUG_TOKENS = false;  // DO NOT CHECK IN TRUE
     79     private static final boolean DEBUG_NOT_CACHEABLE = false;  // DO NOT CHECK IN TRUE
     80     private static final boolean DEBUG_STATISTICS = false; // DO NOT CHECK THIS IN TRUE
     81 
     82     // If false, reads will not use the cache; this is intended for debugging only
     83     private static final boolean READ_CACHE_ENABLED = true;  // DO NOT CHECK IN FALSE
     84 
     85     // Count of non-cacheable queries (debug only)
     86     private static int sNotCacheable = 0;
     87     // A map of queries that aren't cacheable (debug only)
     88     private static final CounterMap<String> sNotCacheableMap = new CounterMap<String>();
     89 
     90     private final LruCache<String, Cursor> mLruCache;
     91 
     92     // All defined caches
     93     private static final ArrayList<ContentCache> sContentCaches = new ArrayList<ContentCache>();
     94     // A set of all unclosed, cached cursors; this will typically be a very small set, as cursors
     95     // tend to be closed quickly after use.  The value, for each cursor, is its reference count
     96     /*package*/ static final CounterMap<Cursor> sActiveCursors = new CounterMap<Cursor>(24);
     97 
     98     // A set of locked content id's
     99     private final CounterMap<String> mLockMap = new CounterMap<String>(4);
    100     // A set of active tokens
    101     /*package*/ TokenList mTokenList;
    102 
    103     // The name of the cache (used for logging)
    104     private final String mName;
    105     // The base projection (only queries in which all columns exist in this projection will be
    106     // able to avoid a cache miss)
    107     private final String[] mBaseProjection;
    108     // The tag used for logging
    109     private final String mLogTag;
    110     // Cache statistics
    111     private final Statistics mStats;
    112     /** If {@code true}, lock the cache for all writes */
    113     private static boolean sLockCache;
    114 
    115     /**
    116      * A synchronized reference counter for arbitrary objects
    117      */
    118     /*package*/ static class CounterMap<T> {
    119         private HashMap<T, Integer> mMap;
    120 
    121         /*package*/ CounterMap(int maxSize) {
    122             mMap = new HashMap<T, Integer>(maxSize);
    123         }
    124 
    125         /*package*/ CounterMap() {
    126             mMap = new HashMap<T, Integer>();
    127         }
    128 
    129         /*package*/ synchronized int subtract(T object) {
    130             Integer refCount = mMap.get(object);
    131             int newCount;
    132             if (refCount == null || refCount.intValue() == 0) {
    133                 throw new IllegalStateException();
    134             }
    135             if (refCount > 1) {
    136                 newCount = refCount - 1;
    137                 mMap.put(object, newCount);
    138             } else {
    139                 newCount = 0;
    140                 mMap.remove(object);
    141             }
    142             return newCount;
    143         }
    144 
    145         /*package*/ synchronized void add(T object) {
    146             Integer refCount = mMap.get(object);
    147             if (refCount == null) {
    148                 mMap.put(object, 1);
    149             } else {
    150                 mMap.put(object, refCount + 1);
    151             }
    152         }
    153 
    154         /*package*/ synchronized boolean contains(T object) {
    155             return mMap.containsKey(object);
    156         }
    157 
    158         /*package*/ synchronized int getCount(T object) {
    159             Integer refCount = mMap.get(object);
    160             return (refCount == null) ? 0 : refCount.intValue();
    161         }
    162 
    163         synchronized int size() {
    164             return mMap.size();
    165         }
    166 
    167         /**
    168          * For Debugging Only - not efficient
    169          */
    170         synchronized Set<HashMap.Entry<T, Integer>> entrySet() {
    171             return mMap.entrySet();
    172         }
    173     }
    174 
    175     /**
    176      * A list of tokens that are in use at any moment; there can be more than one token for an id
    177      */
    178     /*package*/ static class TokenList extends ArrayList<CacheToken> {
    179         private static final long serialVersionUID = 1L;
    180         private final String mLogTag;
    181 
    182         /*package*/ TokenList(String name) {
    183             mLogTag = "TokenList-" + name;
    184         }
    185 
    186         /*package*/ int invalidateTokens(String id) {
    187             if (Email.DEBUG && DEBUG_TOKENS) {
    188                 Log.d(mLogTag, "============ Invalidate tokens for: " + id);
    189             }
    190             ArrayList<CacheToken> removeList = new ArrayList<CacheToken>();
    191             int count = 0;
    192             for (CacheToken token: this) {
    193                 if (token.getId().equals(id)) {
    194                     token.invalidate();
    195                     removeList.add(token);
    196                     count++;
    197                 }
    198             }
    199             for (CacheToken token: removeList) {
    200                 remove(token);
    201             }
    202             return count;
    203         }
    204 
    205         /*package*/ void invalidate() {
    206             if (Email.DEBUG && DEBUG_TOKENS) {
    207                 Log.d(mLogTag, "============ List invalidated");
    208             }
    209             for (CacheToken token: this) {
    210                 token.invalidate();
    211             }
    212             clear();
    213         }
    214 
    215         /*package*/ boolean remove(CacheToken token) {
    216             boolean result = super.remove(token);
    217             if (Email.DEBUG && DEBUG_TOKENS) {
    218                 if (result) {
    219                     Log.d(mLogTag, "============ Removing token for: " + token.mId);
    220                 } else {
    221                     Log.d(mLogTag, "============ No token found for: " + token.mId);
    222                 }
    223             }
    224             return result;
    225         }
    226 
    227         public CacheToken add(String id) {
    228             CacheToken token = new CacheToken(id);
    229             super.add(token);
    230             if (Email.DEBUG && DEBUG_TOKENS) {
    231                 Log.d(mLogTag, "============ Taking token for: " + token.mId);
    232             }
    233             return token;
    234         }
    235     }
    236 
    237     /**
    238      * A CacheToken is an opaque object that must be passed into putCursor in order to attempt to
    239      * write into the cache.  The token becomes invalidated by any intervening write to the cached
    240      * record.
    241      */
    242     public static final class CacheToken {
    243         private final String mId;
    244         private boolean mIsValid = READ_CACHE_ENABLED;
    245 
    246         /*package*/ CacheToken(String id) {
    247             mId = id;
    248         }
    249 
    250         /*package*/ String getId() {
    251             return mId;
    252         }
    253 
    254         /*package*/ boolean isValid() {
    255             return mIsValid;
    256         }
    257 
    258         /*package*/ void invalidate() {
    259             mIsValid = false;
    260         }
    261 
    262         @Override
    263         public boolean equals(Object token) {
    264             return ((token instanceof CacheToken) && ((CacheToken)token).mId.equals(mId));
    265         }
    266 
    267         @Override
    268         public int hashCode() {
    269             return mId.hashCode();
    270         }
    271     }
    272 
    273     /**
    274      * The cached cursor is simply a CursorWrapper whose underlying cursor contains zero or one
    275      * rows.  We handle simple movement (moveToFirst(), moveToNext(), etc.), and override close()
    276      * to keep the underlying cursor alive (unless it's no longer cached due to an invalidation).
    277      * Multiple CachedCursor's can use the same underlying cursor, so we override the various
    278      * moveX methods such that each CachedCursor can have its own position information
    279      */
    280     public static final class CachedCursor extends CursorWrapper implements CrossProcessCursor {
    281         // The cursor we're wrapping
    282         private final Cursor mCursor;
    283         // The cache which generated this cursor
    284         private final ContentCache mCache;
    285         private final String mId;
    286         // The current position of the cursor (can only be 0 or 1)
    287         private int mPosition = -1;
    288         // The number of rows in this cursor (-1 = not determined)
    289         private int mCount = -1;
    290         private boolean isClosed = false;
    291 
    292         public CachedCursor(Cursor cursor, ContentCache cache, String id) {
    293             super(cursor);
    294             mCursor = cursor;
    295             mCache = cache;
    296             mId = id;
    297             // Add this to our set of active cursors
    298             sActiveCursors.add(cursor);
    299         }
    300 
    301         /**
    302          * Close this cursor; if the cursor's cache no longer contains the underlying cursor, and
    303          * there are no other users of that cursor, we'll close it here. In any event,
    304          * we'll remove the cursor from our set of active cursors.
    305          */
    306         @Override
    307         public void close() {
    308             synchronized(mCache) {
    309                 int count = sActiveCursors.subtract(mCursor);
    310                 if ((count == 0) && mCache.mLruCache.get(mId) != (mCursor)) {
    311                     super.close();
    312                 }
    313             }
    314             isClosed = true;
    315         }
    316 
    317         @Override
    318         public boolean isClosed() {
    319             return isClosed;
    320         }
    321 
    322         @Override
    323         public int getCount() {
    324             if (mCount < 0) {
    325                 mCount = super.getCount();
    326             }
    327             return mCount;
    328         }
    329 
    330         /**
    331          * We'll be happy to move to position 0 or -1
    332          */
    333         @Override
    334         public boolean moveToPosition(int pos) {
    335             if (pos >= getCount() || pos < -1) {
    336                 return false;
    337             }
    338             mPosition = pos;
    339             return true;
    340         }
    341 
    342         @Override
    343         public boolean moveToFirst() {
    344             return moveToPosition(0);
    345         }
    346 
    347         @Override
    348         public boolean moveToNext() {
    349             return moveToPosition(mPosition + 1);
    350         }
    351 
    352         @Override
    353         public boolean moveToPrevious() {
    354             return moveToPosition(mPosition - 1);
    355         }
    356 
    357         @Override
    358         public int getPosition() {
    359             return mPosition;
    360         }
    361 
    362         @Override
    363         public final boolean move(int offset) {
    364             return moveToPosition(mPosition + offset);
    365         }
    366 
    367         @Override
    368         public final boolean moveToLast() {
    369             return moveToPosition(getCount() - 1);
    370         }
    371 
    372         @Override
    373         public final boolean isLast() {
    374             return mPosition == (getCount() - 1);
    375         }
    376 
    377         @Override
    378         public final boolean isBeforeFirst() {
    379             return mPosition == -1;
    380         }
    381 
    382         @Override
    383         public final boolean isAfterLast() {
    384             return mPosition == 1;
    385         }
    386 
    387         @Override
    388         public CursorWindow getWindow() {
    389            return ((CrossProcessCursor)mCursor).getWindow();
    390         }
    391 
    392         @Override
    393         public void fillWindow(int pos, CursorWindow window) {
    394             ((CrossProcessCursor)mCursor).fillWindow(pos, window);
    395         }
    396 
    397         @Override
    398         public boolean onMove(int oldPosition, int newPosition) {
    399             return true;
    400         }
    401     }
    402 
    403     /**
    404      * Public constructor
    405      * @param name the name of the cache (used for logging)
    406      * @param baseProjection the projection used for cached cursors; queries whose columns are not
    407      *  included in baseProjection will always generate a cache miss
    408      * @param maxSize the maximum number of content cursors to cache
    409      */
    410     public ContentCache(String name, String[] baseProjection, int maxSize) {
    411         mName = name;
    412         mLruCache = new LruCache<String, Cursor>(maxSize) {
    413             @Override
    414             protected void entryRemoved(
    415                     boolean evicted, String key, Cursor oldValue, Cursor newValue) {
    416                 // Close this cursor if it's no longer being used
    417                 if (evicted && !sActiveCursors.contains(oldValue)) {
    418                     oldValue.close();
    419                 }
    420             }
    421         };
    422         mBaseProjection = baseProjection;
    423         mLogTag = "ContentCache-" + name;
    424         sContentCaches.add(this);
    425         mTokenList = new TokenList(mName);
    426         mStats = new Statistics(this);
    427     }
    428 
    429     /**
    430      * Return the base projection for cached rows
    431      * Get the projection used for cached rows (typically, the largest possible projection)
    432      * @return
    433      */
    434     public String[] getProjection() {
    435         return mBaseProjection;
    436     }
    437 
    438 
    439     /**
    440      * Get a CacheToken for a row as specified by its id (_id column)
    441      * @param id the id of the record
    442      * @return a CacheToken needed in order to write data for the record back to the cache
    443      */
    444     public synchronized CacheToken getCacheToken(String id) {
    445         // If another thread is already writing the data, return an invalid token
    446         CacheToken token = mTokenList.add(id);
    447         if (mLockMap.contains(id)) {
    448             token.invalidate();
    449         }
    450         return token;
    451     }
    452 
    453     public int size() {
    454         return mLruCache.size();
    455     }
    456 
    457     @VisibleForTesting
    458     Cursor get(String id) {
    459         return mLruCache.get(id);
    460     }
    461 
    462     protected Map<String, Cursor> getSnapshot() {
    463         return mLruCache.snapshot();
    464     }
    465     /**
    466      * Try to cache a cursor for the given id and projection; returns a valid cursor, either a
    467      * cached cursor (if caching was successful) or the original cursor
    468      *
    469      * @param c the cursor to be cached
    470      * @param id the record id (_id) of the content
    471      * @param projection the projection represented by the cursor
    472      * @return whether or not the cursor was cached
    473      */
    474     public Cursor putCursor(Cursor c, String id, String[] projection, CacheToken token) {
    475         // Make sure the underlying cursor is at the first row, and do this without synchronizing,
    476         // to prevent deadlock with a writing thread (which might, for example, be calling into
    477         // CachedCursor.invalidate)
    478         c.moveToPosition(0);
    479         return putCursorImpl(c, id, projection, token);
    480     }
    481     public synchronized Cursor putCursorImpl(Cursor c, String id, String[] projection,
    482             CacheToken token) {
    483         try {
    484             if (!token.isValid()) {
    485                 if (Email.DEBUG && DEBUG_CACHE) {
    486                     Log.d(mLogTag, "============ Stale token for " + id);
    487                 }
    488                 mStats.mStaleCount++;
    489                 return c;
    490             }
    491             if (c != null && Arrays.equals(projection, mBaseProjection) && !sLockCache) {
    492                 if (Email.DEBUG && DEBUG_CACHE) {
    493                     Log.d(mLogTag, "============ Caching cursor for: " + id);
    494                 }
    495                 // If we've already cached this cursor, invalidate the older one
    496                 Cursor existingCursor = get(id);
    497                 if (existingCursor != null) {
    498                    unlockImpl(id, null, false);
    499                 }
    500                 mLruCache.put(id, c);
    501                 return new CachedCursor(c, this, id);
    502             }
    503             return c;
    504         } finally {
    505             mTokenList.remove(token);
    506         }
    507     }
    508 
    509     /**
    510      * Find and, if found, return a cursor, based on cached values, for the supplied id
    511      * @param id the _id column of the desired row
    512      * @param projection the requested projection for a query
    513      * @return a cursor based on cached values, or null if the row is not cached
    514      */
    515     public synchronized Cursor getCachedCursor(String id, String[] projection) {
    516         if (Email.DEBUG && DEBUG_STATISTICS) {
    517             // Every 200 calls to getCursor, report cache statistics
    518             dumpOnCount(200);
    519         }
    520         if (projection == mBaseProjection) {
    521             return getCachedCursorImpl(id);
    522         } else {
    523             return getMatrixCursor(id, projection);
    524         }
    525     }
    526 
    527     private CachedCursor getCachedCursorImpl(String id) {
    528         Cursor c = get(id);
    529         if (c != null) {
    530             mStats.mHitCount++;
    531             return new CachedCursor(c, this, id);
    532         }
    533         mStats.mMissCount++;
    534         return null;
    535     }
    536 
    537     private MatrixCursor getMatrixCursor(String id, String[] projection) {
    538         return getMatrixCursor(id, projection, null);
    539     }
    540 
    541     private MatrixCursor getMatrixCursor(String id, String[] projection,
    542             ContentValues values) {
    543         Cursor c = get(id);
    544         if (c != null) {
    545             // Make a new MatrixCursor with the requested columns
    546             MatrixCursor mc = new MatrixCursor(projection, 1);
    547             if (c.getCount() == 0) {
    548                 return mc;
    549             }
    550             Object[] row = new Object[projection.length];
    551             if (values != null) {
    552                 // Make a copy; we don't want to change the original
    553                 values = new ContentValues(values);
    554             }
    555             int i = 0;
    556             for (String column: projection) {
    557                 int columnIndex = c.getColumnIndex(column);
    558                 if (columnIndex < 0) {
    559                     mStats.mProjectionMissCount++;
    560                     return null;
    561                 } else {
    562                     String value;
    563                     if (values != null && values.containsKey(column)) {
    564                         Object val = values.get(column);
    565                         if (val instanceof Boolean) {
    566                             value = (val == Boolean.TRUE) ? "1" : "0";
    567                         } else {
    568                             value = values.getAsString(column);
    569                         }
    570                         values.remove(column);
    571                     } else {
    572                         value = c.getString(columnIndex);
    573                     }
    574                     row[i++] = value;
    575                 }
    576             }
    577             if (values != null && values.size() != 0) {
    578                 return null;
    579             }
    580             mc.addRow(row);
    581             mStats.mHitCount++;
    582             return mc;
    583         }
    584         mStats.mMissCount++;
    585         return null;
    586     }
    587 
    588     /**
    589      * Lock a given row, such that no new valid CacheTokens can be created for the passed-in id.
    590      * @param id the id of the row to lock
    591      */
    592     public synchronized void lock(String id) {
    593         // Prevent new valid tokens from being created
    594         mLockMap.add(id);
    595         // Invalidate current tokens
    596         int count = mTokenList.invalidateTokens(id);
    597         if (Email.DEBUG && DEBUG_TOKENS) {
    598             Log.d(mTokenList.mLogTag, "============ Lock invalidated " + count +
    599                     " tokens for: " + id);
    600         }
    601     }
    602 
    603     /**
    604      * Unlock a given row, allowing new valid CacheTokens to be created for the passed-in id.
    605      * @param id the id of the item whose cursor is cached
    606      */
    607     public synchronized void unlock(String id) {
    608         unlockImpl(id, null, true);
    609     }
    610 
    611     /**
    612      * If the row with id is currently cached, replaces the cached values with the supplied
    613      * ContentValues.  Then, unlock the row, so that new valid CacheTokens can be created.
    614      *
    615      * @param id the id of the item whose cursor is cached
    616      * @param values updated values for this row
    617      */
    618     public synchronized void unlock(String id, ContentValues values) {
    619         unlockImpl(id, values, true);
    620     }
    621 
    622     /**
    623      * If values are passed in, replaces any cached cursor with one containing new values, and
    624      * then closes the previously cached one (if any, and if not in use)
    625      * If values are not passed in, removes the row from cache
    626      * If the row was locked, unlock it
    627      * @param id the id of the row
    628      * @param values new ContentValues for the row (or null if row should simply be removed)
    629      * @param wasLocked whether or not the row was locked; if so, the lock will be removed
    630      */
    631     private void unlockImpl(String id, ContentValues values, boolean wasLocked) {
    632         Cursor c = get(id);
    633         if (c != null) {
    634             if (Email.DEBUG && DEBUG_CACHE) {
    635                 Log.d(mLogTag, "=========== Unlocking cache for: " + id);
    636             }
    637             if (values != null && !sLockCache) {
    638                 MatrixCursor cursor = getMatrixCursor(id, mBaseProjection, values);
    639                 if (cursor != null) {
    640                     if (Email.DEBUG && DEBUG_CACHE) {
    641                         Log.d(mLogTag, "=========== Recaching with new values: " + id);
    642                     }
    643                     cursor.moveToFirst();
    644                     mLruCache.put(id, cursor);
    645                 } else {
    646                     mLruCache.remove(id);
    647                 }
    648             } else {
    649                 mLruCache.remove(id);
    650             }
    651             // If there are no cursors using the old cached cursor, close it
    652             if (!sActiveCursors.contains(c)) {
    653                 c.close();
    654             }
    655         }
    656         if (wasLocked) {
    657             mLockMap.subtract(id);
    658         }
    659     }
    660 
    661     /**
    662      * Invalidate the entire cache, without logging
    663      */
    664     public synchronized void invalidate() {
    665         invalidate(null, null, null);
    666     }
    667 
    668     /**
    669      * Invalidate the entire cache; the arguments are used for logging only, and indicate the
    670      * write operation that caused the invalidation
    671      *
    672      * @param operation a string describing the operation causing the invalidate (or null)
    673      * @param uri the uri causing the invalidate (or null)
    674      * @param selection the selection used with the uri (or null)
    675      */
    676     public synchronized void invalidate(String operation, Uri uri, String selection) {
    677         if (DEBUG_CACHE && (operation != null)) {
    678             Log.d(mLogTag, "============ INVALIDATED BY " + operation + ": " + uri +
    679                     ", SELECTION: " + selection);
    680         }
    681         mStats.mInvalidateCount++;
    682         // Close all cached cursors that are no longer in use
    683         mLruCache.evictAll();
    684         // Invalidate all current tokens
    685         mTokenList.invalidate();
    686     }
    687 
    688     // Debugging code below
    689 
    690     private void dumpOnCount(int num) {
    691         mStats.mOpCount++;
    692         if ((mStats.mOpCount % num) == 0) {
    693             dumpStats();
    694         }
    695     }
    696 
    697     /*package*/ void recordQueryTime(Cursor c, long nanoTime) {
    698         if (c instanceof CachedCursor) {
    699             mStats.hitTimes += nanoTime;
    700             mStats.hits++;
    701         } else {
    702             if (c.getCount() == 1) {
    703                 mStats.missTimes += nanoTime;
    704                 mStats.miss++;
    705             }
    706         }
    707     }
    708 
    709     public static synchronized void notCacheable(Uri uri, String selection) {
    710         if (DEBUG_NOT_CACHEABLE) {
    711             sNotCacheable++;
    712             String str = uri.toString() + "$" + selection;
    713             sNotCacheableMap.add(str);
    714         }
    715     }
    716 
    717     private static class CacheCounter implements Comparable<CacheCounter> {
    718         String uri;
    719         Integer count;
    720 
    721         CacheCounter(String _uri, Integer _count) {
    722             uri = _uri;
    723             count = _count;
    724         }
    725 
    726         @Override
    727         public int compareTo(CacheCounter another) {
    728             return another.count > count ? 1 : another.count == count ? 0 : -1;
    729         }
    730     }
    731 
    732     private static void dumpNotCacheableQueries() {
    733         int size = sNotCacheableMap.size();
    734         CacheCounter[] array = new CacheCounter[size];
    735 
    736         int i = 0;
    737         for (Map.Entry<String, Integer> entry: sNotCacheableMap.entrySet()) {
    738             array[i++] = new CacheCounter(entry.getKey(), entry.getValue());
    739         }
    740         Arrays.sort(array);
    741         for (CacheCounter cc: array) {
    742             Log.d("NotCacheable", cc.count + ": " + cc.uri);
    743         }
    744     }
    745 
    746     // For use with unit tests
    747     public static void invalidateAllCaches() {
    748         for (ContentCache cache: sContentCaches) {
    749             cache.invalidate();
    750         }
    751     }
    752 
    753     /** Sets the cache lock. If the lock is {@code true}, also invalidates all cached items. */
    754     public static void setLockCacheForTest(boolean lock) {
    755         sLockCache = lock;
    756         if (sLockCache) {
    757             invalidateAllCaches();
    758         }
    759     }
    760 
    761     static class Statistics {
    762         private final ContentCache mCache;
    763         private final String mName;
    764 
    765         // Cache statistics
    766         // The item is in the cache AND is used to create a cursor
    767         private int mHitCount = 0;
    768         // Basic cache miss (the item is not cached)
    769         private int mMissCount = 0;
    770         // Incremented when a cachePut is invalid due to an intervening write
    771         private int mStaleCount = 0;
    772         // A projection miss occurs when the item is cached, but not all requested columns are
    773         // available in the base projection
    774         private int mProjectionMissCount = 0;
    775         // Incremented whenever the entire cache is invalidated
    776         private int mInvalidateCount = 0;
    777         // Count of operations put/get
    778         private int mOpCount = 0;
    779         // The following are for timing statistics
    780         private long hits = 0;
    781         private long hitTimes = 0;
    782         private long miss = 0;
    783         private long missTimes = 0;
    784 
    785         // Used in toString() and addCacheStatistics()
    786         private int mCursorCount = 0;
    787         private int mTokenCount = 0;
    788 
    789         Statistics(ContentCache cache) {
    790             mCache = cache;
    791             mName = mCache.mName;
    792         }
    793 
    794         Statistics(String name) {
    795             mCache = null;
    796             mName = name;
    797         }
    798 
    799         private void addCacheStatistics(ContentCache cache) {
    800             if (cache != null) {
    801                 mHitCount += cache.mStats.mHitCount;
    802                 mMissCount += cache.mStats.mMissCount;
    803                 mProjectionMissCount += cache.mStats.mProjectionMissCount;
    804                 mStaleCount += cache.mStats.mStaleCount;
    805                 hitTimes += cache.mStats.hitTimes;
    806                 missTimes += cache.mStats.missTimes;
    807                 hits += cache.mStats.hits;
    808                 miss += cache.mStats.miss;
    809                 mCursorCount += cache.size();
    810                 mTokenCount += cache.mTokenList.size();
    811             }
    812         }
    813 
    814         private void append(StringBuilder sb, String name, Object value) {
    815             sb.append(", ");
    816             sb.append(name);
    817             sb.append(": ");
    818             sb.append(value);
    819         }
    820 
    821         @Override
    822         public String toString() {
    823             if (mHitCount + mMissCount == 0) return "No cache";
    824             int totalTries = mMissCount + mProjectionMissCount + mHitCount;
    825             StringBuilder sb = new StringBuilder();
    826             sb.append("Cache " + mName);
    827             append(sb, "Cursors", mCache == null ? mCursorCount : mCache.size());
    828             append(sb, "Hits", mHitCount);
    829             append(sb, "Misses", mMissCount + mProjectionMissCount);
    830             append(sb, "Inval", mInvalidateCount);
    831             append(sb, "Tokens", mCache == null ? mTokenCount : mCache.mTokenList.size());
    832             append(sb, "Hit%", mHitCount * 100 / totalTries);
    833             append(sb, "\nHit time", hitTimes / 1000000.0 / hits);
    834             append(sb, "Miss time", missTimes / 1000000.0 / miss);
    835             return sb.toString();
    836         }
    837     }
    838 
    839     public static void dumpStats() {
    840         Statistics totals = new Statistics("Totals");
    841 
    842         for (ContentCache cache: sContentCaches) {
    843             if (cache != null) {
    844                 Log.d(cache.mName, cache.mStats.toString());
    845                 totals.addCacheStatistics(cache);
    846             }
    847         }
    848         Log.d(totals.mName, totals.toString());
    849     }
    850 }
    851