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