Home | History | Annotate | Download | only in content
      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      */
    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      */
    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
    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
    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
    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")
    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
    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 
    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
    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
    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 
    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      */
    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      */
    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
    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 
    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
    565         public int getCount() {
    566             return mSize;
    567         }
    568 
    569         @Override
    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> {
    583         CursorCache(int maxSize) {
    584             super(maxSize);
    585         }
    586 
    587         @WorkerThread
    588         @Override
    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")
    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          */
    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          */
    623         boolean isRunning(@NonNull Query query);
    624 
    625         /**
    626          * Attempt to cancel a (presumably) running query.
    627          * @param query The query in question.
    628          */
    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              */
    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              */
    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 
    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 
    694         private void reset() {
    695             mTotalQueries = 0;
    696             mResolvedQueries = 0;
    697             mCompatPaged = 0;
    698             mProviderPaged = 0;
    699         }
    700 
    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