1 /*
2  * Copyright (C) 2017 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 androidx.contentpager.content;
18 
19 import static androidx.core.util.Preconditions.checkArgument;
20 import static androidx.core.util.Preconditions.checkState;
21 
22 import android.content.ContentResolver;
23 import android.database.CrossProcessCursor;
24 import android.database.Cursor;
25 import android.database.CursorWindow;
26 import android.database.CursorWrapper;
27 import android.net.Uri;
28 import android.os.Build;
29 import android.os.Bundle;
30 import android.os.CancellationSignal;
31 import android.os.OperationCanceledException;
32 import android.util.Log;
33 
34 import androidx.annotation.GuardedBy;
35 import androidx.annotation.IntDef;
36 import androidx.annotation.MainThread;
37 import androidx.annotation.NonNull;
38 import androidx.annotation.Nullable;
39 import androidx.annotation.RequiresPermission;
40 import androidx.annotation.VisibleForTesting;
41 import androidx.annotation.WorkerThread;
42 import androidx.collection.LruCache;
43 
44 import java.lang.annotation.Retention;
45 import java.lang.annotation.RetentionPolicy;
46 import java.util.HashSet;
47 import java.util.Set;
48 
49 /**
50  * {@link ContentPager} provides support for loading "paged" data on a background thread
51  * using the {@link ContentResolver} framework. This provides an effective compatibility
52  * layer for the ContentResolver "paging" support added in Android O. Those Android O changes,
53  * like this class, help reduce or eliminate the occurrence of expensive inter-process
54  * shared memory operations (aka "CursorWindow swaps") happening on the UI thread when
55  * working with remote providers.
56  *
57  * <p>The list of terms used in this document:
58  *
59  * <ol>"The provider" is a {@link android.content.ContentProvider} supplying data identified
60  * by a specific content {@link Uri}. A provider is the source of data, and for the sake of
61  * this documents, the provider resides in a remote process.
62 
63  * <ol>"supports paging" A provider supports paging when it returns a pre-paged {@link Cursor}
64  * that honors the paging contract. See @link ContentResolver#QUERY_ARG_OFFSET} and
65  * {@link ContentResolver#QUERY_ARG_LIMIT} for details on the contract.
66 
67  * <ol>"CursorWindow swaps" The process by which new data is loaded into a shared memory
68  * via a CursorWindow instance. This is a prominent contributor to UI jank in applications
69  * that use Cursor as backing data for UI elements like {@code RecyclerView}.
70  *
71  * <p><b>Details</b>
72  *
73  * <p>Data will be loaded from a content uri in one of two ways, depending on the runtime
74  * environment and if the provider supports paging.
75  *
76  * <li>If the system is Android O and greater and the provider supports paging, the Cursor
77  * will be returned, effectively unmodified, to a {@link ContentCallback} supplied by
78  * your application.
79  *
80  * <li>If the system is less than Android O or the provider does not support paging, the
81  * loader will fetch an unpaged Cursor from the provider. The unpaged Cursor will be held
82  * by the ContentPager, and data will be copied into a new cursor in a background thread.
83  * The new cursor will be returned to a {@link ContentCallback} supplied by your application.
84  *
85  * <p>In either cases, when an application employs this library it can generally assume
86  * that there will be no CursorWindow swap. But picking the right limit for records can
87  * help reduce or even eliminate some heavy lifting done to guard against swaps.
88  *
89  * <p>How do we avoid that entirely?
90  *
91  * <p><b>Picking a reasonable item limit</b>
92  *
93  * <p>Authors are encouraged to experiment with limits using real data and the widest column
94  * projection they'll use in their app. The total number of records that will fit into shared
95  * memory varies depending on multiple factors.
96  *
97  * <li>The number of columns being requested in the cursor projection. Limit the number
98  * of columns, to reduce the size of each row.
99  * <li>The size of the data in each column.
100  * <li>the Cursor type.
101  *
102  * <p>If the cursor is running in-process, there may be no need for paging. Depending on
103  * the Cursor implementation chosen there may be no shared memory/CursorWindow in use.
104  * NOTE: If the provider is running in your process, you should implement paging support
105  * inorder to make your app run fast and to consume the fewest resources possible.
106  *
107  * <p>In common cases where there is a low volume (in the hundreds) of records in the dataset
108  * being queried, all of the data should easily fit in shared memory. A debugger can be handy
109  * to understand with greater accuracy how many results can fit in shared memory. Inspect
110  * the Cursor object returned from a call to
111  * {@link ContentResolver#query(Uri, String[], String, String[], String)}. If the underlying
112  * type is a {@link android.database.CrossProcessCursor} or
113  * {@link android.database.AbstractWindowedCursor} it'll have a {@link CursorWindow} field.
114  * Check {@link CursorWindow#getNumRows()}. If getNumRows returns less than
115  * {@link Cursor#getCount}, then you've found something close to the max rows that'll
116  * fit in a page. If the data in row is expected to be relatively stable in size, reduce
117  * row count by 15-20% to get a reasonable max page size.
118  *
119  * <p><b>What if the limit I guessed was wrong?</b>
120 
121  * <p>The library includes safeguards that protect against situations where an author
122  * specifies a record limit that exceeds the number of rows accessible without a CursorWindow swap.
123  * In such a circumstance, the Cursor will be adapted to report a count ({Cursor#getCount})
124  * that reflects only records available without CursorWindow swap. But this involves
125  * extra work that can be eliminated with a correct limit.
126  *
127  * <p>In addition to adjusted coujnt, {@link #EXTRA_SUGGESTED_LIMIT} will be included
128  * in cursor extras. When EXTRA_SUGGESTED_LIMIT is present in extras, the client should
129  * strongly consider using this value as the limit for subsequent queries as doing so should
130  * help avoid the ned to wrap pre-paged cursors.
131  *
132  * <p><b>Lifecycle and cleanup</b>
133  *
134  * <p>Cursors resulting from queries are owned by the requesting client. So they must be closed
135  * by the client at the appropriate time.
136  *
137  * <p>However, the library retains an internal cache of content that needs to be cleaned up.
138  * In order to cleanup, call {@link #reset()}.
139  *
140  * <p><b>Projections</b>
141  *
142  * <p>Note that projection is ignored when determining the identity of a query. When
143  * adding or removing projection, clients should call {@link #reset()} to clear
144  * cached data.
145  */
146 public class ContentPager {
147 
148     @VisibleForTesting
149     static final String CURSOR_DISPOSITION = "androidx.appcompat.widget.CURSOR_DISPOSITION";
150 
151     @IntDef(value = {
152             ContentPager.CURSOR_DISPOSITION_COPIED,
153             ContentPager.CURSOR_DISPOSITION_PAGED,
154             ContentPager.CURSOR_DISPOSITION_REPAGED,
155             ContentPager.CURSOR_DISPOSITION_WRAPPED
156     })
157     @Retention(RetentionPolicy.SOURCE)
158     public @interface CursorDisposition {}
159 
160     /** The cursor size exceeded page size. A new cursor with with page data was created. */
161     public static final int CURSOR_DISPOSITION_COPIED = 1;
162 
163     /**
164      * The cursor was provider paged.
165      */
166     public static final int CURSOR_DISPOSITION_PAGED = 2;
167 
168     /** The cursor was pre-paged, but total size was larger than CursorWindow size. */
169     public static final int CURSOR_DISPOSITION_REPAGED = 3;
170 
171     /**
172      * The cursor was not pre-paged, but total size was smaller than page size.
173      * Cursor wrapped to supply data in extras only.
174      */
175     public static final int CURSOR_DISPOSITION_WRAPPED = 4;
176 
177     /** @see ContentResolver#EXTRA_HONORED_ARGS */
178     public static final String EXTRA_HONORED_ARGS = ContentResolver.EXTRA_HONORED_ARGS;
179 
180     /** @see ContentResolver#EXTRA_TOTAL_COUNT */
181     public static final String EXTRA_TOTAL_COUNT = ContentResolver.EXTRA_TOTAL_COUNT;
182 
183     /** @see ContentResolver#QUERY_ARG_OFFSET */
184     public static final String QUERY_ARG_OFFSET = ContentResolver.QUERY_ARG_OFFSET;
185 
186     /** @see ContentResolver#QUERY_ARG_LIMIT */
187     public static final String QUERY_ARG_LIMIT = ContentResolver.QUERY_ARG_LIMIT;
188 
189     /** Denotes the requested limit, if the limit was not-honored. */
190     public static final String EXTRA_REQUESTED_LIMIT = "android-support:extra-ignored-limit";
191 
192     /** Specifies a limit likely to fit in CursorWindow limit. */
193     public static final String EXTRA_SUGGESTED_LIMIT = "android-support:extra-suggested-limit";
194 
195     private static final boolean DEBUG = false;
196     private static final String TAG = "ContentPager";
197     private static final int DEFAULT_CURSOR_CACHE_SIZE = 1;
198 
199     private final QueryRunner mQueryRunner;
200     private final QueryRunner.Callback mQueryCallback;
201     private final ContentResolver mResolver;
202     private final Object mContentLock = new Object();
203     private final @GuardedBy("mContentLock") Set<Query> mActiveQueries = new HashSet<>();
204     private final @GuardedBy("mContentLock") CursorCache mCursorCache;
205 
206     private final Stats mStats = new Stats();
207 
208     /**
209      * Creates a new ContentPager with a default cursor cache size of 1.
210      */
ContentPager(ContentResolver resolver, QueryRunner queryRunner)211     public ContentPager(ContentResolver resolver, QueryRunner queryRunner) {
212         this(resolver, queryRunner, DEFAULT_CURSOR_CACHE_SIZE);
213     }
214 
215     /**
216      * Creates a new ContentPager.
217      *
218      * @param cursorCacheSize Specifies the size of the unpaged cursor cache. If you will
219      *     only be querying a single content Uri, 1 is sufficient. If you wish to use
220      *     a single ContentPager for queries against several independent Uris this number
221      *     should be increased to reflect that. Remember that adding or modifying a
222      *     query argument creates a new Uri.
223      * @param resolver The content resolver to use when performing queries.
224      * @param queryRunner The query running to use. This provides a means of executing
225      *         queries on a background thread.
226      */
ContentPager( @onNull ContentResolver resolver, @NonNull QueryRunner queryRunner, int cursorCacheSize)227     public ContentPager(
228             @NonNull ContentResolver resolver,
229             @NonNull QueryRunner queryRunner,
230             int cursorCacheSize) {
231 
232         checkArgument(resolver != null, "'resolver' argument cannot be null.");
233         checkArgument(queryRunner != null, "'queryRunner' argument cannot be null.");
234         checkArgument(cursorCacheSize > 0, "'cursorCacheSize' argument must be greater than 0.");
235 
236         mResolver = resolver;
237         mQueryRunner = queryRunner;
238         mQueryCallback = new QueryRunner.Callback() {
239 
240             @WorkerThread
241             @Override
242             public @Nullable Cursor runQueryInBackground(Query query) {
243                 return loadContentInBackground(query);
244             }
245 
246             @MainThread
247             @Override
248             public void onQueryFinished(Query query, Cursor cursor) {
249                 ContentPager.this.onCursorReady(query, cursor);
250             }
251         };
252 
253         mCursorCache = new CursorCache(cursorCacheSize);
254     }
255 
256     /**
257      * Initiates loading of content.
258      * For details on all params but callback, see
259      * {@link ContentResolver#query(Uri, String[], Bundle, CancellationSignal)}.
260      *
261      * @param uri The URI, using the content:// scheme, for the content to retrieve.
262      * @param projection A list of which columns to return. Passing null will return
263      *         the default project as determined by the provider. This can be inefficient,
264      *         so it is best to supply a projection.
265      * @param queryArgs A Bundle containing any arguments to the query.
266      * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
267      * If the operation is canceled, then {@link OperationCanceledException} will be thrown
268      * when the query is executed.
269      * @param callback The callback that will receive the query results.
270      *
271      * @return A Query object describing the query.
272      */
273     @MainThread
query( @onNull @equiresPermission.Read Uri uri, @Nullable String[] projection, @NonNull Bundle queryArgs, @Nullable CancellationSignal cancellationSignal, @NonNull ContentCallback callback)274     public @NonNull Query query(
275             @NonNull @RequiresPermission.Read Uri uri,
276             @Nullable String[] projection,
277             @NonNull Bundle queryArgs,
278             @Nullable CancellationSignal cancellationSignal,
279             @NonNull ContentCallback callback) {
280 
281         checkArgument(uri != null, "'uri' argument cannot be null.");
282         checkArgument(queryArgs != null, "'queryArgs' argument cannot be null.");
283         checkArgument(callback != null, "'callback' argument cannot be null.");
284 
285         Query query = new Query(uri, projection, queryArgs, cancellationSignal, callback);
286 
287         if (DEBUG) Log.d(TAG, "Handling query: " + query);
288 
289         if (!mQueryRunner.isRunning(query)) {
290             synchronized (mContentLock) {
291                 mActiveQueries.add(query);
292             }
293             mQueryRunner.query(query, mQueryCallback);
294         }
295 
296         return query;
297     }
298 
299     /**
300      * Clears any cached data. This method must be called in order to cleanup runtime state
301      * (like cursors).
302      */
303     @MainThread
reset()304     public void reset() {
305         synchronized (mContentLock) {
306             if (DEBUG) Log.d(TAG, "Clearing un-paged cursor cache.");
307             mCursorCache.evictAll();
308 
309             for (Query query : mActiveQueries) {
310                 if (DEBUG) Log.d(TAG, "Canceling running query: " + query);
311                 mQueryRunner.cancel(query);
312                 query.cancel();
313             }
314 
315             mActiveQueries.clear();
316         }
317     }
318 
319     @WorkerThread
loadContentInBackground(Query query)320     private Cursor loadContentInBackground(Query query) {
321         if (DEBUG) Log.v(TAG, "Loading cursor for query: " + query);
322         mStats.increment(Stats.EXTRA_TOTAL_QUERIES);
323 
324         synchronized (mContentLock) {
325             // We have a existing unpaged-cursor for this query. Instead of running a new query
326             // via ContentResolver, we'll just copy results from that.
327             // This is the "compat" behavior.
328             if (mCursorCache.hasEntry(query.getUri())) {
329                 if (DEBUG) Log.d(TAG, "Found unpaged results in cache for: " + query);
330                 return createPagedCursor(query);
331             }
332         }
333 
334         // We don't have an unpaged query, so we run the query using ContentResolver.
335         // It may be that no query for this URI has ever been run, so no unpaged
336         // results have been saved. Or, it may be the the provider supports paging
337         // directly, and is returning a pre-paged result set...so no unpaged
338         // cursor will ever be set.
339         Cursor cursor = query.run(mResolver);
340         mStats.increment(Stats.EXTRA_RESOLVED_QUERIES);
341 
342         //       for the window. If so, communicate the overflow back to the client.
343         if (cursor == null) {
344             Log.e(TAG, "Query resulted in null cursor. " + query);
345             return null;
346         }
347 
348         if (isProviderPaged(cursor)) {
349             return processProviderPagedCursor(query, cursor);
350         }
351 
352         // Cache the unpaged results so we can generate pages from them on subsequent queries.
353         synchronized (mContentLock) {
354             mCursorCache.put(query.getUri(), cursor);
355             return createPagedCursor(query);
356         }
357     }
358 
359     @WorkerThread
360     @GuardedBy("mContentLock")
createPagedCursor(Query query)361     private Cursor createPagedCursor(Query query) {
362         Cursor unpaged = mCursorCache.get(query.getUri());
363         checkState(unpaged != null, "No un-paged cursor in cache, or can't retrieve it.");
364 
365         mStats.increment(Stats.EXTRA_COMPAT_PAGED);
366 
367         if (DEBUG) Log.d(TAG, "Synthesizing cursor for page: " + query);
368         int count = Math.min(query.getLimit(), unpaged.getCount());
369 
370         // don't wander off the end of the cursor.
371         if (query.getOffset() + query.getLimit() > unpaged.getCount()) {
372             count = unpaged.getCount() % query.getLimit();
373         }
374 
375         if (DEBUG) Log.d(TAG, "Cursor count: " + count);
376 
377         Cursor result = null;
378         // If the cursor isn't advertising support for paging, but is in-fact smaller
379         // than the page size requested, we just decorate the cursor with paging data,
380         // and wrap it without copy.
381         if (query.getOffset() == 0 && unpaged.getCount() < query.getLimit()) {
382             result = new CursorView(
383                     unpaged, unpaged.getCount(), CURSOR_DISPOSITION_WRAPPED);
384         } else {
385             // This creates an in-memory copy of the data that fits the requested page.
386             // ContentObservers registered on InMemoryCursor are directly registered
387             // on the unpaged cursor.
388             result = new InMemoryCursor(
389                     unpaged, query.getOffset(), count, CURSOR_DISPOSITION_COPIED);
390         }
391 
392         mStats.includeStats(result.getExtras());
393         return result;
394     }
395 
396     @WorkerThread
processProviderPagedCursor(Query query, Cursor cursor)397     private @Nullable Cursor processProviderPagedCursor(Query query, Cursor cursor) {
398 
399         CursorWindow window = getWindow(cursor);
400         int windowSize = cursor.getCount();
401         if (window != null) {
402             if (DEBUG) Log.d(TAG, "Returning provider-paged cursor.");
403             windowSize = window.getNumRows();
404         }
405 
406         // Android O paging APIs are *all* about avoiding CursorWindow swaps,
407         // because the swaps need to happen on the UI thread in jank-inducing ways.
408         // But, the APIs don't *guarantee* that no window-swapping will happen
409         // when traversing a cursor.
410         //
411         // Here in the support lib, we can guarantee there is no window swapping
412         // by detecting mismatches between requested sizes and window sizes.
413         // When a mismatch is detected we can return a cursor that reports
414         // a size bounded by its CursorWindow size, and includes a suggested
415         // size to use for subsequent queries.
416 
417         if (DEBUG) Log.d(TAG, "Cursor window overflow detected. Returning re-paged cursor.");
418 
419         int disposition = (cursor.getCount() <= windowSize)
420                 ? CURSOR_DISPOSITION_PAGED
421                 : CURSOR_DISPOSITION_REPAGED;
422 
423         Cursor result = new CursorView(cursor, windowSize, disposition);
424         Bundle extras = result.getExtras();
425 
426         // If the orig cursor reports a size larger than the window, suggest a better limit.
427         if (cursor.getCount() > windowSize) {
428             extras.putInt(EXTRA_REQUESTED_LIMIT, query.getLimit());
429             extras.putInt(EXTRA_SUGGESTED_LIMIT, (int) (windowSize * .85));
430         }
431 
432         mStats.increment(Stats.EXTRA_PROVIDER_PAGED);
433         mStats.includeStats(extras);
434         return result;
435     }
436 
getWindow(Cursor cursor)437     private CursorWindow getWindow(Cursor cursor) {
438         if (cursor instanceof CursorWrapper) {
439             return getWindow(((CursorWrapper) cursor).getWrappedCursor());
440         }
441         if (cursor instanceof CrossProcessCursor) {
442             return ((CrossProcessCursor) cursor).getWindow();
443         }
444         // TODO: Any other ways we can find/access windows?
445         return null;
446     }
447 
448     // Called in the foreground when the cursor is ready for the client.
449     @MainThread
onCursorReady(Query query, Cursor cursor)450     private void onCursorReady(Query query, Cursor cursor) {
451         synchronized (mContentLock) {
452             mActiveQueries.remove(query);
453         }
454 
455         query.getCallback().onCursorReady(query, cursor);
456     }
457 
458     /**
459      * @return true if the cursor extras contains all of the signs of being paged.
460      *     Technically we could also check SDK version since facilities for paging
461      *     were added in SDK 26, but if it looks like a duck and talks like a duck
462      *     itsa duck (especially if it helps with testing).
463      */
464     @WorkerThread
isProviderPaged(Cursor cursor)465     private boolean isProviderPaged(Cursor cursor) {
466         Bundle extras = cursor.getExtras();
467         extras = extras != null ? extras : Bundle.EMPTY;
468         String[] honoredArgs = extras.getStringArray(EXTRA_HONORED_ARGS);
469 
470         return (extras.containsKey(EXTRA_TOTAL_COUNT)
471                 && honoredArgs != null
472                 && contains(honoredArgs, QUERY_ARG_OFFSET)
473                 && contains(honoredArgs, QUERY_ARG_LIMIT));
474     }
475 
contains(T[] array, T value)476     private static <T> boolean contains(T[] array, T value) {
477         for (T element : array) {
478             if (value.equals(element)) {
479                 return true;
480             }
481         }
482         return false;
483     }
484 
485     /**
486      * @return Bundle populated with existing extras (if any) as well as
487      * all usefule paging related extras.
488      */
buildExtras( @ullable Bundle extras, int recordCount, @CursorDisposition int cursorDisposition)489     static Bundle buildExtras(
490             @Nullable Bundle extras, int recordCount, @CursorDisposition int cursorDisposition) {
491 
492         if (extras == null || extras == Bundle.EMPTY) {
493             extras = new Bundle();
494         } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
495             extras = extras.deepCopy();
496         }
497         // else we modify cursor extras directly, cuz that's our only choice.
498 
499         extras.putInt(CURSOR_DISPOSITION, cursorDisposition);
500         if (!extras.containsKey(EXTRA_TOTAL_COUNT)) {
501             extras.putInt(EXTRA_TOTAL_COUNT, recordCount);
502         }
503 
504         if (!extras.containsKey(EXTRA_HONORED_ARGS)) {
505             extras.putStringArray(EXTRA_HONORED_ARGS, new String[]{
506                     ContentPager.QUERY_ARG_OFFSET,
507                     ContentPager.QUERY_ARG_LIMIT
508             });
509         }
510 
511         return extras;
512     }
513 
514     /**
515      * Builds a Bundle with offset and limit values suitable for with
516      * {@link #query(Uri, String[], Bundle, CancellationSignal, ContentCallback)}.
517      *
518      * @param offset must be greater than or equal to 0.
519      * @param limit can be any value. Only values greater than or equal to 0 are respected.
520      *         If any other value results in no upper limit on results. Note that a well
521      *         behaved client should probably supply a reasonable limit. See class
522      *         documentation on how to select a limit.
523      *
524      * @return Mutable Bundle pre-populated with offset and limits vales.
525      */
createArgs(int offset, int limit)526     public static @NonNull Bundle createArgs(int offset, int limit) {
527         checkArgument(offset >= 0);
528         Bundle args = new Bundle();
529         args.putInt(ContentPager.QUERY_ARG_OFFSET, offset);
530         args.putInt(ContentPager.QUERY_ARG_LIMIT, limit);
531         return args;
532     }
533 
534     /**
535      * Callback by which a client receives results of a query.
536      */
537     public interface ContentCallback {
538         /**
539          * Called when paged cursor is ready. Null, if query failed.
540          * @param query The query having been executed.
541          * @param cursor the query results. Null if query couldn't be executed.
542          */
543         @MainThread
onCursorReady(@onNull Query query, @Nullable Cursor cursor)544         void onCursorReady(@NonNull Query query, @Nullable Cursor cursor);
545     }
546 
547     /**
548      * Provides support for adding extras to a cursor. This is necessary
549      * as a cursor returning an extras Bundle that is either Bundle.EMPTY
550      * or null, cannot have information added to the cursor. On SDKs earlier
551      * than M, there is no facility to replace the Bundle.
552      */
553     private static final class CursorView extends CursorWrapper {
554         private final Bundle mExtras;
555         private final int mSize;
556 
CursorView(Cursor delegate, int size, @CursorDisposition int disposition)557         CursorView(Cursor delegate, int size, @CursorDisposition int disposition) {
558             super(delegate);
559             mSize = size;
560 
561             mExtras = buildExtras(delegate.getExtras(), delegate.getCount(), disposition);
562         }
563 
564         @Override
getCount()565         public int getCount() {
566             return mSize;
567         }
568 
569         @Override
getExtras()570         public Bundle getExtras() {
571             return mExtras;
572         }
573     }
574 
575     /**
576      * LruCache holding at most {@code maxSize} cursors. Once evicted a cursor
577      * is immediately closed. The only cursor's held in this cache are
578      * unpaged results. For this purpose the cache is keyed by the URI,
579      * not the entire query. Cursors that are pre-paged by the provider
580      * are never cached.
581      */
582     private static final class CursorCache extends LruCache<Uri, Cursor> {
CursorCache(int maxSize)583         CursorCache(int maxSize) {
584             super(maxSize);
585         }
586 
587         @WorkerThread
588         @Override
entryRemoved( boolean evicted, Uri uri, Cursor oldCursor, Cursor newCursor)589         protected void entryRemoved(
590                 boolean evicted, Uri uri, Cursor oldCursor, Cursor newCursor) {
591             if (!oldCursor.isClosed()) {
592                 oldCursor.close();
593             }
594         }
595 
596         /** @return true if an entry is present for the Uri. */
597         @WorkerThread
598         @GuardedBy("mContentLock")
hasEntry(Uri uri)599         boolean hasEntry(Uri uri) {
600             return get(uri) != null;
601         }
602     }
603 
604     /**
605      * Implementations of this interface provide the mechanism
606      * for execution of queries off the UI thread.
607      */
608     public interface QueryRunner {
609         /**
610          * Execute a query.
611          * @param query The query that will be run. This value should be handed
612          *         back to the callback when ready to run in the background.
613          * @param callback The callback that should be called to both execute
614          *         the query (in the background) and to receive the results
615          *         (in the foreground).
616          */
query(@onNull Query query, @NonNull Callback callback)617         void query(@NonNull Query query, @NonNull Callback callback);
618 
619         /**
620          * @param query The query in question.
621          * @return true if the query is already running.
622          */
isRunning(@onNull Query query)623         boolean isRunning(@NonNull Query query);
624 
625         /**
626          * Attempt to cancel a (presumably) running query.
627          * @param query The query in question.
628          */
cancel(@onNull Query query)629         void cancel(@NonNull Query query);
630 
631         /**
632          * Callback that receives a cursor once a query as been executed on the Runner.
633          */
634         interface Callback {
635             /**
636              * Method called on background thread where actual query is executed. This is provided
637              * by ContentPager.
638              * @param query The query to be executed.
639              */
runQueryInBackground(@onNull Query query)640             @Nullable Cursor runQueryInBackground(@NonNull Query query);
641 
642             /**
643              * Called on main thread when query has completed.
644              * @param query The completed query.
645              * @param cursor The results in Cursor form. Null if not successfully completed.
646              */
onQueryFinished(@onNull Query query, @Nullable Cursor cursor)647             void onQueryFinished(@NonNull Query query, @Nullable Cursor cursor);
648         }
649     }
650 
651     static final class Stats {
652 
653         /** Identifes the total number of queries handled by ContentPager. */
654         static final String EXTRA_TOTAL_QUERIES = "android-support:extra-total-queries";
655 
656         /** Identifes the number of queries handled by content resolver. */
657         static final String EXTRA_RESOLVED_QUERIES = "android-support:extra-resolved-queries";
658 
659         /** Identifes the number of pages produced by way of copying. */
660         static final String EXTRA_COMPAT_PAGED = "android-support:extra-compat-paged";
661 
662         /** Identifes the number of pages produced directly by a page-supporting provider. */
663         static final String EXTRA_PROVIDER_PAGED = "android-support:extra-provider-paged";
664 
665         // simple stats objects tracking paged result handling.
666         private int mTotalQueries;
667         private int mResolvedQueries;
668         private int mCompatPaged;
669         private int mProviderPaged;
670 
increment(String prop)671         private void increment(String prop) {
672             switch (prop) {
673                 case EXTRA_TOTAL_QUERIES:
674                     ++mTotalQueries;
675                     break;
676 
677                 case EXTRA_RESOLVED_QUERIES:
678                     ++mResolvedQueries;
679                     break;
680 
681                 case EXTRA_COMPAT_PAGED:
682                     ++mCompatPaged;
683                     break;
684 
685                 case EXTRA_PROVIDER_PAGED:
686                     ++mProviderPaged;
687                     break;
688 
689                 default:
690                     throw new IllegalArgumentException("Unknown property: " + prop);
691             }
692         }
693 
reset()694         private void reset() {
695             mTotalQueries = 0;
696             mResolvedQueries = 0;
697             mCompatPaged = 0;
698             mProviderPaged = 0;
699         }
700 
includeStats(Bundle bundle)701         void includeStats(Bundle bundle) {
702             bundle.putInt(EXTRA_TOTAL_QUERIES, mTotalQueries);
703             bundle.putInt(EXTRA_RESOLVED_QUERIES, mResolvedQueries);
704             bundle.putInt(EXTRA_COMPAT_PAGED, mCompatPaged);
705             bundle.putInt(EXTRA_PROVIDER_PAGED, mProviderPaged);
706         }
707     }
708 }
709