PageRenderTime 51ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/content/src/main/java/androidx/contentpager/content/ContentPager.java

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