Data will be loaded from a content uri in one of two ways, depending on the runtime
environment and if the provider supports paging.
the Cursor type.
If the cursor is running in-process, there may be no need for paging. Depending on
the Cursor implementation chosen there may be no shared memory/CursorWindow in use.
NOTE: If the provider is running in your process, you should implement paging support
inorder to make your app run fast and to consume the fewest resources possible.
In common cases where there is a low volume (in the hundreds) of records in the dataset
being queried, all of the data should easily fit in shared memory. A debugger can be handy
to understand with greater accuracy how many results can fit in shared memory. Inspect
the Cursor object returned from a call to
. If the underlying
type is a or
it'll have a field.
Check . If getNumRows returns less than
, then you've found something close to the max rows that'll
fit in a page. If the data in row is expected to be relatively stable in size, reduce
row count by 15-20% to get a reasonable max page size.
What if the limit I guessed was wrong?
The library includes safeguards that protect against situations where an author
specifies a record limit that exceeds the number of rows accessible without a CursorWindow swap.
In such a circumstance, the Cursor will be adapted to report a count ({Cursor#getCount})
that reflects only records available without CursorWindow swap. But this involves
extra work that can be eliminated with a correct limit.
In addition to adjusted coujnt, ContentPager.EXTRA_SUGGESTED_LIMIT will be included
in cursor extras. When EXTRA_SUGGESTED_LIMIT is present in extras, the client should
strongly consider using this value as the limit for subsequent queries as doing so should
help avoid the ned to wrap pre-paged cursors.
Lifecycle and cleanup
Cursors resulting from queries are owned by the requesting client. So they must be closed
by the client at the appropriate time.
However, the library retains an internal cache of content that needs to be cleaned up.
In order to cleanup, call ContentPager.reset().
Projections
Note that projection is ignored when determining the identity of a query. When
adding or removing projection, clients should call ContentPager.reset() to clear
cached data.
Summary
Methods |
---|
public static Bundle | createArgs(int offset, int limit)
Builds a Bundle with offset and limit values suitable for with
ContentPager.query(Uri, String[], Bundle, CancellationSignal, ContentPager.ContentCallback). |
public Query | query(Uri uri, java.lang.String projection[], Bundle queryArgs, CancellationSignal cancellationSignal, ContentPager.ContentCallback callback)
Initiates loading of content. |
public void | reset()
Clears any cached data. |
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Fields
public static final int
CURSOR_DISPOSITION_COPIEDThe cursor size exceeded page size. A new cursor with with page data was created.
public static final int
CURSOR_DISPOSITION_PAGEDThe cursor was provider paged.
public static final int
CURSOR_DISPOSITION_REPAGEDThe cursor was pre-paged, but total size was larger than CursorWindow size.
public static final int
CURSOR_DISPOSITION_WRAPPEDThe cursor was not pre-paged, but total size was smaller than page size.
Cursor wrapped to supply data in extras only.
public static final java.lang.String
EXTRA_HONORED_ARGSSee also:
public static final java.lang.String
EXTRA_TOTAL_COUNTSee also:
public static final java.lang.String
QUERY_ARG_OFFSETSee also:
public static final java.lang.String
QUERY_ARG_LIMITSee also:
public static final java.lang.String
EXTRA_REQUESTED_LIMITDenotes the requested limit, if the limit was not-honored.
public static final java.lang.String
EXTRA_SUGGESTED_LIMITSpecifies a limit likely to fit in CursorWindow limit.
Constructors
Creates a new ContentPager with a default cursor cache size of 1.
Creates a new ContentPager.
Parameters:
cursorCacheSize: Specifies the size of the unpaged cursor cache. If you will
only be querying a single content Uri, 1 is sufficient. If you wish to use
a single ContentPager for queries against several independent Uris this number
should be increased to reflect that. Remember that adding or modifying a
query argument creates a new Uri.
resolver: The content resolver to use when performing queries.
queryRunner: The query running to use. This provides a means of executing
queries on a background thread.
Methods
Initiates loading of content.
For details on all params but callback, see
.
Parameters:
uri: The URI, using the content:// scheme, for the content to retrieve.
projection: A list of which columns to return. Passing null will return
the default project as determined by the provider. This can be inefficient,
so it is best to supply a projection.
queryArgs: A Bundle containing any arguments to the query.
cancellationSignal: A signal to cancel the operation in progress, or null if none.
If the operation is canceled, then will be thrown
when the query is executed.
callback: The callback that will receive the query results.
Returns:
A Query object describing the query.
Clears any cached data. This method must be called in order to cleanup runtime state
(like cursors).
public static Bundle
createArgs(int offset, int limit)
Builds a Bundle with offset and limit values suitable for with
ContentPager.query(Uri, String[], Bundle, CancellationSignal, ContentPager.ContentCallback).
Parameters:
offset: must be greater than or equal to 0.
limit: can be any value. Only values greater than or equal to 0 are respected.
If any other value results in no upper limit on results. Note that a well
behaved client should probably supply a reasonable limit. See class
documentation on how to select a limit.
Returns:
Mutable Bundle pre-populated with offset and limits vales.
Source
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.contentpager.content;
import static androidx.core.util.Preconditions.checkArgument;
import static androidx.core.util.Preconditions.checkState;
import android.content.ContentResolver;
import android.database.CrossProcessCursor;
import android.database.Cursor;
import android.database.CursorWindow;
import android.database.CursorWrapper;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.OperationCanceledException;
import android.util.Log;
import androidx.annotation.GuardedBy;
import androidx.annotation.IntDef;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresPermission;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.collection.LruCache;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashSet;
import java.util.Set;
/**
* {@link ContentPager} provides support for loading "paged" data on a background thread
* using the {@link ContentResolver} framework. This provides an effective compatibility
* layer for the ContentResolver "paging" support added in Android O. Those Android O changes,
* like this class, help reduce or eliminate the occurrence of expensive inter-process
* shared memory operations (aka "CursorWindow swaps") happening on the UI thread when
* working with remote providers.
*
* <p>The list of terms used in this document:
*
* <ol>"The provider" is a {@link android.content.ContentProvider} supplying data identified
* by a specific content {@link Uri}. A provider is the source of data, and for the sake of
* this documents, the provider resides in a remote process.
* <ol>"supports paging" A provider supports paging when it returns a pre-paged {@link Cursor}
* that honors the paging contract. See @link ContentResolver#QUERY_ARG_OFFSET} and
* {@link ContentResolver#QUERY_ARG_LIMIT} for details on the contract.
* <ol>"CursorWindow swaps" The process by which new data is loaded into a shared memory
* via a CursorWindow instance. This is a prominent contributor to UI jank in applications
* that use Cursor as backing data for UI elements like {@code RecyclerView}.
*
* <p><b>Details</b>
*
* <p>Data will be loaded from a content uri in one of two ways, depending on the runtime
* environment and if the provider supports paging.
*
* <li>If the system is Android O and greater and the provider supports paging, the Cursor
* will be returned, effectively unmodified, to a {@link ContentCallback} supplied by
* your application.
*
* <li>If the system is less than Android O or the provider does not support paging, the
* loader will fetch an unpaged Cursor from the provider. The unpaged Cursor will be held
* by the ContentPager, and data will be copied into a new cursor in a background thread.
* The new cursor will be returned to a {@link ContentCallback} supplied by your application.
*
* <p>In either cases, when an application employs this library it can generally assume
* that there will be no CursorWindow swap. But picking the right limit for records can
* help reduce or even eliminate some heavy lifting done to guard against swaps.
*
* <p>How do we avoid that entirely?
*
* <p><b>Picking a reasonable item limit</b>
*
* <p>Authors are encouraged to experiment with limits using real data and the widest column
* projection they'll use in their app. The total number of records that will fit into shared
* memory varies depending on multiple factors.
*
* <li>The number of columns being requested in the cursor projection. Limit the number
* of columns, to reduce the size of each row.
* <li>The size of the data in each column.
* <li>the Cursor type.
*
* <p>If the cursor is running in-process, there may be no need for paging. Depending on
* the Cursor implementation chosen there may be no shared memory/CursorWindow in use.
* NOTE: If the provider is running in your process, you should implement paging support
* inorder to make your app run fast and to consume the fewest resources possible.
*
* <p>In common cases where there is a low volume (in the hundreds) of records in the dataset
* being queried, all of the data should easily fit in shared memory. A debugger can be handy
* to understand with greater accuracy how many results can fit in shared memory. Inspect
* the Cursor object returned from a call to
* {@link ContentResolver#query(Uri, String[], String, String[], String)}. If the underlying
* type is a {@link android.database.CrossProcessCursor} or
* {@link android.database.AbstractWindowedCursor} it'll have a {@link CursorWindow} field.
* Check {@link CursorWindow#getNumRows()}. If getNumRows returns less than
* {@link Cursor#getCount}, then you've found something close to the max rows that'll
* fit in a page. If the data in row is expected to be relatively stable in size, reduce
* row count by 15-20% to get a reasonable max page size.
*
* <p><b>What if the limit I guessed was wrong?</b>
* <p>The library includes safeguards that protect against situations where an author
* specifies a record limit that exceeds the number of rows accessible without a CursorWindow swap.
* In such a circumstance, the Cursor will be adapted to report a count ({Cursor#getCount})
* that reflects only records available without CursorWindow swap. But this involves
* extra work that can be eliminated with a correct limit.
*
* <p>In addition to adjusted coujnt, {@link #EXTRA_SUGGESTED_LIMIT} will be included
* in cursor extras. When EXTRA_SUGGESTED_LIMIT is present in extras, the client should
* strongly consider using this value as the limit for subsequent queries as doing so should
* help avoid the ned to wrap pre-paged cursors.
*
* <p><b>Lifecycle and cleanup</b>
*
* <p>Cursors resulting from queries are owned by the requesting client. So they must be closed
* by the client at the appropriate time.
*
* <p>However, the library retains an internal cache of content that needs to be cleaned up.
* In order to cleanup, call {@link #reset()}.
*
* <p><b>Projections</b>
*
* <p>Note that projection is ignored when determining the identity of a query. When
* adding or removing projection, clients should call {@link #reset()} to clear
* cached data.
*/
public class ContentPager {
@VisibleForTesting
static final String CURSOR_DISPOSITION = "androidx.appcompat.widget.CURSOR_DISPOSITION";
@IntDef(value = {
ContentPager.CURSOR_DISPOSITION_COPIED,
ContentPager.CURSOR_DISPOSITION_PAGED,
ContentPager.CURSOR_DISPOSITION_REPAGED,
ContentPager.CURSOR_DISPOSITION_WRAPPED
})
@Retention(RetentionPolicy.SOURCE)
public @interface CursorDisposition {}
/** The cursor size exceeded page size. A new cursor with with page data was created. */
public static final int CURSOR_DISPOSITION_COPIED = 1;
/**
* The cursor was provider paged.
*/
public static final int CURSOR_DISPOSITION_PAGED = 2;
/** The cursor was pre-paged, but total size was larger than CursorWindow size. */
public static final int CURSOR_DISPOSITION_REPAGED = 3;
/**
* The cursor was not pre-paged, but total size was smaller than page size.
* Cursor wrapped to supply data in extras only.
*/
public static final int CURSOR_DISPOSITION_WRAPPED = 4;
/** @see ContentResolver#EXTRA_HONORED_ARGS */
public static final String EXTRA_HONORED_ARGS = ContentResolver.EXTRA_HONORED_ARGS;
/** @see ContentResolver#EXTRA_TOTAL_COUNT */
public static final String EXTRA_TOTAL_COUNT = ContentResolver.EXTRA_TOTAL_COUNT;
/** @see ContentResolver#QUERY_ARG_OFFSET */
public static final String QUERY_ARG_OFFSET = ContentResolver.QUERY_ARG_OFFSET;
/** @see ContentResolver#QUERY_ARG_LIMIT */
public static final String QUERY_ARG_LIMIT = ContentResolver.QUERY_ARG_LIMIT;
/** Denotes the requested limit, if the limit was not-honored. */
public static final String EXTRA_REQUESTED_LIMIT = "android-support:extra-ignored-limit";
/** Specifies a limit likely to fit in CursorWindow limit. */
public static final String EXTRA_SUGGESTED_LIMIT = "android-support:extra-suggested-limit";
private static final boolean DEBUG = false;
private static final String TAG = "ContentPager";
private static final int DEFAULT_CURSOR_CACHE_SIZE = 1;
private final QueryRunner mQueryRunner;
private final QueryRunner.Callback mQueryCallback;
private final ContentResolver mResolver;
private final Object mContentLock = new Object();
private final @GuardedBy("mContentLock") Set<Query> mActiveQueries = new HashSet<>();
private final @GuardedBy("mContentLock") CursorCache mCursorCache;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final Stats mStats = new Stats();
/**
* Creates a new ContentPager with a default cursor cache size of 1.
*/
public ContentPager(ContentResolver resolver, QueryRunner queryRunner) {
this(resolver, queryRunner, DEFAULT_CURSOR_CACHE_SIZE);
}
/**
* Creates a new ContentPager.
*
* @param cursorCacheSize Specifies the size of the unpaged cursor cache. If you will
* only be querying a single content Uri, 1 is sufficient. If you wish to use
* a single ContentPager for queries against several independent Uris this number
* should be increased to reflect that. Remember that adding or modifying a
* query argument creates a new Uri.
* @param resolver The content resolver to use when performing queries.
* @param queryRunner The query running to use. This provides a means of executing
* queries on a background thread.
*/
public ContentPager(
@NonNull ContentResolver resolver,
@NonNull QueryRunner queryRunner,
int cursorCacheSize) {
checkArgument(resolver != null, "'resolver' argument cannot be null.");
checkArgument(queryRunner != null, "'queryRunner' argument cannot be null.");
checkArgument(cursorCacheSize > 0, "'cursorCacheSize' argument must be greater than 0.");
mResolver = resolver;
mQueryRunner = queryRunner;
mQueryCallback = new QueryRunner.Callback() {
@WorkerThread
@Override
public @Nullable Cursor runQueryInBackground(Query query) {
return loadContentInBackground(query);
}
@MainThread
@Override
public void onQueryFinished(Query query, Cursor cursor) {
ContentPager.this.onCursorReady(query, cursor);
}
};
mCursorCache = new CursorCache(cursorCacheSize);
}
/**
* Initiates loading of content.
* For details on all params but callback, see
* {@link ContentResolver#query(Uri, String[], Bundle, CancellationSignal)}.
*
* @param uri The URI, using the content:// scheme, for the content to retrieve.
* @param projection A list of which columns to return. Passing null will return
* the default project as determined by the provider. This can be inefficient,
* so it is best to supply a projection.
* @param queryArgs A Bundle containing any arguments to the query.
* @param cancellationSignal A signal to cancel the operation in progress, or null if none.
* If the operation is canceled, then {@link OperationCanceledException} will be thrown
* when the query is executed.
* @param callback The callback that will receive the query results.
*
* @return A Query object describing the query.
*/
@MainThread
public @NonNull Query query(
@NonNull @RequiresPermission.Read Uri uri,
@Nullable String[] projection,
@NonNull Bundle queryArgs,
@Nullable CancellationSignal cancellationSignal,
@NonNull ContentCallback callback) {
checkArgument(uri != null, "'uri' argument cannot be null.");
checkArgument(queryArgs != null, "'queryArgs' argument cannot be null.");
checkArgument(callback != null, "'callback' argument cannot be null.");
Query query = new Query(uri, projection, queryArgs, cancellationSignal, callback);
if (DEBUG) Log.d(TAG, "Handling query: " + query);
if (!mQueryRunner.isRunning(query)) {
synchronized (mContentLock) {
mActiveQueries.add(query);
}
mQueryRunner.query(query, mQueryCallback);
}
return query;
}
/**
* Clears any cached data. This method must be called in order to cleanup runtime state
* (like cursors).
*/
@MainThread
public void reset() {
synchronized (mContentLock) {
if (DEBUG) Log.d(TAG, "Clearing un-paged cursor cache.");
mCursorCache.evictAll();
for (Query query : mActiveQueries) {
if (DEBUG) Log.d(TAG, "Canceling running query: " + query);
mQueryRunner.cancel(query);
query.cancel();
}
mActiveQueries.clear();
}
}
@WorkerThread
@SuppressWarnings("WeakerAccess") /* synthetic access */
Cursor loadContentInBackground(Query query) {
if (DEBUG) Log.v(TAG, "Loading cursor for query: " + query);
mStats.increment(Stats.EXTRA_TOTAL_QUERIES);
synchronized (mContentLock) {
// We have a existing unpaged-cursor for this query. Instead of running a new query
// via ContentResolver, we'll just copy results from that.
// This is the "compat" behavior.
if (mCursorCache.hasEntry(query.getUri())) {
if (DEBUG) Log.d(TAG, "Found unpaged results in cache for: " + query);
return createPagedCursor(query);
}
}
// We don't have an unpaged query, so we run the query using ContentResolver.
// It may be that no query for this URI has ever been run, so no unpaged
// results have been saved. Or, it may be the the provider supports paging
// directly, and is returning a pre-paged result set...so no unpaged
// cursor will ever be set.
Cursor cursor = query.run(mResolver);
mStats.increment(Stats.EXTRA_RESOLVED_QUERIES);
// for the window. If so, communicate the overflow back to the client.
if (cursor == null) {
Log.e(TAG, "Query resulted in null cursor. " + query);
return null;
}
if (isProviderPaged(cursor)) {
return processProviderPagedCursor(query, cursor);
}
// Cache the unpaged results so we can generate pages from them on subsequent queries.
synchronized (mContentLock) {
mCursorCache.put(query.getUri(), cursor);
return createPagedCursor(query);
}
}
@WorkerThread
@GuardedBy("mContentLock")
private Cursor createPagedCursor(Query query) {
Cursor unpaged = mCursorCache.get(query.getUri());
checkState(unpaged != null, "No un-paged cursor in cache, or can't retrieve it.");
mStats.increment(Stats.EXTRA_COMPAT_PAGED);
if (DEBUG) Log.d(TAG, "Synthesizing cursor for page: " + query);
int count = Math.min(query.getLimit(), unpaged.getCount());
// don't wander off the end of the cursor.
if (query.getOffset() + query.getLimit() > unpaged.getCount()) {
count = unpaged.getCount() % query.getLimit();
}
if (DEBUG) Log.d(TAG, "Cursor count: " + count);
Cursor result = null;
// If the cursor isn't advertising support for paging, but is in-fact smaller
// than the page size requested, we just decorate the cursor with paging data,
// and wrap it without copy.
if (query.getOffset() == 0 && unpaged.getCount() < query.getLimit()) {
result = new CursorView(
unpaged, unpaged.getCount(), CURSOR_DISPOSITION_WRAPPED);
} else {
// This creates an in-memory copy of the data that fits the requested page.
// ContentObservers registered on InMemoryCursor are directly registered
// on the unpaged cursor.
result = new InMemoryCursor(
unpaged, query.getOffset(), count, CURSOR_DISPOSITION_COPIED);
}
mStats.includeStats(result.getExtras());
return result;
}
@WorkerThread
private @Nullable Cursor processProviderPagedCursor(Query query, Cursor cursor) {
CursorWindow window = getWindow(cursor);
int windowSize = cursor.getCount();
if (window != null) {
if (DEBUG) Log.d(TAG, "Returning provider-paged cursor.");
windowSize = window.getNumRows();
}
// Android O paging APIs are *all* about avoiding CursorWindow swaps,
// because the swaps need to happen on the UI thread in jank-inducing ways.
// But, the APIs don't *guarantee* that no window-swapping will happen
// when traversing a cursor.
//
// Here in the support lib, we can guarantee there is no window swapping
// by detecting mismatches between requested sizes and window sizes.
// When a mismatch is detected we can return a cursor that reports
// a size bounded by its CursorWindow size, and includes a suggested
// size to use for subsequent queries.
if (DEBUG) Log.d(TAG, "Cursor window overflow detected. Returning re-paged cursor.");
int disposition = (cursor.getCount() <= windowSize)
? CURSOR_DISPOSITION_PAGED
: CURSOR_DISPOSITION_REPAGED;
Cursor result = new CursorView(cursor, windowSize, disposition);
Bundle extras = result.getExtras();
// If the orig cursor reports a size larger than the window, suggest a better limit.
if (cursor.getCount() > windowSize) {
extras.putInt(EXTRA_REQUESTED_LIMIT, query.getLimit());
extras.putInt(EXTRA_SUGGESTED_LIMIT, (int) (windowSize * .85));
}
mStats.increment(Stats.EXTRA_PROVIDER_PAGED);
mStats.includeStats(extras);
return result;
}
private CursorWindow getWindow(Cursor cursor) {
if (cursor instanceof CursorWrapper) {
return getWindow(((CursorWrapper) cursor).getWrappedCursor());
}
if (cursor instanceof CrossProcessCursor) {
return ((CrossProcessCursor) cursor).getWindow();
}
// TODO: Any other ways we can find/access windows?
return null;
}
// Called in the foreground when the cursor is ready for the client.
@MainThread
@SuppressWarnings("WeakerAccess") /* synthetic access */
void onCursorReady(Query query, Cursor cursor) {
synchronized (mContentLock) {
mActiveQueries.remove(query);
}
query.getCallback().onCursorReady(query, cursor);
}
/**
* @return true if the cursor extras contains all of the signs of being paged.
* Technically we could also check SDK version since facilities for paging
* were added in SDK 26, but if it looks like a duck and talks like a duck
* itsa duck (especially if it helps with testing).
*/
@WorkerThread
private boolean isProviderPaged(Cursor cursor) {
Bundle extras = cursor.getExtras();
extras = extras != null ? extras : Bundle.EMPTY;
String[] honoredArgs = extras.getStringArray(EXTRA_HONORED_ARGS);
return (extras.containsKey(EXTRA_TOTAL_COUNT)
&& honoredArgs != null
&& contains(honoredArgs, QUERY_ARG_OFFSET)
&& contains(honoredArgs, QUERY_ARG_LIMIT));
}
private static <T> boolean contains(T[] array, T value) {
for (T element : array) {
if (value.equals(element)) {
return true;
}
}
return false;
}
/**
* @return Bundle populated with existing extras (if any) as well as
* all usefule paging related extras.
*/
static Bundle buildExtras(
@Nullable Bundle extras, int recordCount, @CursorDisposition int cursorDisposition) {
if (extras == null || extras == Bundle.EMPTY) {
extras = new Bundle();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
extras = extras.deepCopy();
}
// else we modify cursor extras directly, cuz that's our only choice.
extras.putInt(CURSOR_DISPOSITION, cursorDisposition);
if (!extras.containsKey(EXTRA_TOTAL_COUNT)) {
extras.putInt(EXTRA_TOTAL_COUNT, recordCount);
}
if (!extras.containsKey(EXTRA_HONORED_ARGS)) {
extras.putStringArray(EXTRA_HONORED_ARGS, new String[]{
ContentPager.QUERY_ARG_OFFSET,
ContentPager.QUERY_ARG_LIMIT
});
}
return extras;
}
/**
* Builds a Bundle with offset and limit values suitable for with
* {@link #query(Uri, String[], Bundle, CancellationSignal, ContentCallback)}.
*
* @param offset must be greater than or equal to 0.
* @param limit can be any value. Only values greater than or equal to 0 are respected.
* If any other value results in no upper limit on results. Note that a well
* behaved client should probably supply a reasonable limit. See class
* documentation on how to select a limit.
*
* @return Mutable Bundle pre-populated with offset and limits vales.
*/
public static @NonNull Bundle createArgs(int offset, int limit) {
checkArgument(offset >= 0);
Bundle args = new Bundle();
args.putInt(ContentPager.QUERY_ARG_OFFSET, offset);
args.putInt(ContentPager.QUERY_ARG_LIMIT, limit);
return args;
}
/**
* Callback by which a client receives results of a query.
*/
public interface ContentCallback {
/**
* Called when paged cursor is ready. Null, if query failed.
* @param query The query having been executed.
* @param cursor the query results. Null if query couldn't be executed.
*/
@MainThread
void onCursorReady(@NonNull Query query, @Nullable Cursor cursor);
}
/**
* Provides support for adding extras to a cursor. This is necessary
* as a cursor returning an extras Bundle that is either Bundle.EMPTY
* or null, cannot have information added to the cursor. On SDKs earlier
* than M, there is no facility to replace the Bundle.
*/
private static final class CursorView extends CursorWrapper {
private final Bundle mExtras;
private final int mSize;
CursorView(Cursor delegate, int size, @CursorDisposition int disposition) {
super(delegate);
mSize = size;
mExtras = buildExtras(delegate.getExtras(), delegate.getCount(), disposition);
}
@Override
public int getCount() {
return mSize;
}
@Override
public Bundle getExtras() {
return mExtras;
}
}
/**
* LruCache holding at most {@code maxSize} cursors. Once evicted a cursor
* is immediately closed. The only cursor's held in this cache are
* unpaged results. For this purpose the cache is keyed by the URI,
* not the entire query. Cursors that are pre-paged by the provider
* are never cached.
*/
private static final class CursorCache extends LruCache<Uri, Cursor> {
CursorCache(int maxSize) {
super(maxSize);
}
@WorkerThread
@Override
protected void entryRemoved(
boolean evicted, Uri uri, Cursor oldCursor, Cursor newCursor) {
if (!oldCursor.isClosed()) {
oldCursor.close();
}
}
/** @return true if an entry is present for the Uri. */
@WorkerThread
boolean hasEntry(Uri uri) {
return get(uri) != null;
}
}
/**
* Implementations of this interface provide the mechanism
* for execution of queries off the UI thread.
*/
public interface QueryRunner {
/**
* Execute a query.
* @param query The query that will be run. This value should be handed
* back to the callback when ready to run in the background.
* @param callback The callback that should be called to both execute
* the query (in the background) and to receive the results
* (in the foreground).
*/
void query(@NonNull Query query, @NonNull Callback callback);
/**
* @param query The query in question.
* @return true if the query is already running.
*/
boolean isRunning(@NonNull Query query);
/**
* Attempt to cancel a (presumably) running query.
* @param query The query in question.
*/
void cancel(@NonNull Query query);
/**
* Callback that receives a cursor once a query as been executed on the Runner.
*/
interface Callback {
/**
* Method called on background thread where actual query is executed. This is provided
* by ContentPager.
* @param query The query to be executed.
*/
@Nullable Cursor runQueryInBackground(@NonNull Query query);
/**
* Called on main thread when query has completed.
* @param query The completed query.
* @param cursor The results in Cursor form. Null if not successfully completed.
*/
void onQueryFinished(@NonNull Query query, @Nullable Cursor cursor);
}
}
static final class Stats {
/** Identifes the total number of queries handled by ContentPager. */
static final String EXTRA_TOTAL_QUERIES = "android-support:extra-total-queries";
/** Identifes the number of queries handled by content resolver. */
static final String EXTRA_RESOLVED_QUERIES = "android-support:extra-resolved-queries";
/** Identifes the number of pages produced by way of copying. */
static final String EXTRA_COMPAT_PAGED = "android-support:extra-compat-paged";
/** Identifes the number of pages produced directly by a page-supporting provider. */
static final String EXTRA_PROVIDER_PAGED = "android-support:extra-provider-paged";
// simple stats objects tracking paged result handling.
private int mTotalQueries;
private int mResolvedQueries;
private int mCompatPaged;
private int mProviderPaged;
void increment(String prop) {
switch (prop) {
case EXTRA_TOTAL_QUERIES:
++mTotalQueries;
break;
case EXTRA_RESOLVED_QUERIES:
++mResolvedQueries;
break;
case EXTRA_COMPAT_PAGED:
++mCompatPaged;
break;
case EXTRA_PROVIDER_PAGED:
++mProviderPaged;
break;
default:
throw new IllegalArgumentException("Unknown property: " + prop);
}
}
void includeStats(Bundle bundle) {
bundle.putInt(EXTRA_TOTAL_QUERIES, mTotalQueries);
bundle.putInt(EXTRA_RESOLVED_QUERIES, mResolvedQueries);
bundle.putInt(EXTRA_COMPAT_PAGED, mCompatPaged);
bundle.putInt(EXTRA_PROVIDER_PAGED, mProviderPaged);
}
}
}