• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 org.junit.Assert.assertEquals;
20 
21 import android.content.ContentProvider;
22 import android.content.ContentResolver;
23 import android.content.ContentValues;
24 import android.database.AbstractWindowedCursor;
25 import android.database.Cursor;
26 import android.database.CursorWindow;
27 import android.database.MatrixCursor;
28 import android.database.MatrixCursor.RowBuilder;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.CancellationSignal;
32 
33 import androidx.annotation.Nullable;
34 import androidx.annotation.VisibleForTesting;
35 
36 /**
37  * A stub data paging provider used for testing of paging support.
38  * Ignores client supplied projections.
39  */
40 public final class TestContentProvider extends ContentProvider {
41 
42     public static final String AUTHORITY = "androidx.contentpager.content.test.testpagingprovider";
43 
44     public static final String UNPAGED_PATH = "/un-paged";
45     public static final String PAGED_PATH = "/paged";
46     public static final String PAGED_WINDOWED_PATH = PAGED_PATH + "/windowed";
47 
48     public static final Uri UNPAGED_URI = new Uri.Builder()
49             .scheme("content")
50             .authority(AUTHORITY)
51             .path(UNPAGED_PATH)
52             .build();
53     public static final Uri PAGED_URI = new Uri.Builder()
54             .scheme("content")
55             .authority(AUTHORITY)
56             .path(PAGED_PATH)
57             .build();
58     public static final Uri PAGED_WINDOWED_URI = new Uri.Builder()
59             .scheme("content")
60             .authority(AUTHORITY)
61             .path(PAGED_WINDOWED_PATH)
62             .build();
63 
64     public static final String COLUMN_POS = "ColumnPos";
65     public static final String COLUMN_A = "ColumnA";
66     public static final String COLUMN_B = "ColumnB";
67     public static final String COLUMN_C = "ColumnC";
68     public static final String COLUMN_D = "ColumnD";
69     public static final String[] PROJECTION = {
70             COLUMN_POS,
71             COLUMN_A,
72             COLUMN_B,
73             COLUMN_C,
74             COLUMN_D
75     };
76 
77     @VisibleForTesting
78     public static final String RECORD_COUNT = "test-record-count";
79 
80     @VisibleForTesting
81     public static final int DEFAULT_RECORD_COUNT = 567;
82 
83     private static final String TAG = "TestPagingProvider";
84 
85     @Override
onCreate()86     public boolean onCreate() {
87         return true;
88     }
89 
90     @Override
query( Uri uri, @Nullable String[] projection, String selection, String[] selectionArgs, String sortOrder)91     public Cursor query(
92             Uri uri, @Nullable String[] projection, String selection, String[] selectionArgs,
93             String sortOrder) {
94         return query(uri, projection, null, null);
95     }
96 
97     @Override
query(Uri uri, String[] ignored, Bundle queryArgs, CancellationSignal cancellationSignal)98     public Cursor query(Uri uri, String[] ignored, Bundle queryArgs,
99             CancellationSignal cancellationSignal) {
100 
101         queryArgs = queryArgs != null ? queryArgs : Bundle.EMPTY;
102 
103         int recordCount = getIntValue(RECORD_COUNT, queryArgs, uri, DEFAULT_RECORD_COUNT);
104         if (recordCount < 0) {
105             throw new RuntimeException("Recordset size must be >= 0");
106         }
107 
108         Cursor cursor = null;
109         switch (uri.getPath()) {
110             case UNPAGED_PATH:
111                 cursor = buildUnpagedResults(recordCount);
112                 break;
113             case PAGED_PATH:
114                 cursor = buildPagedResults(uri, queryArgs, recordCount);
115                 break;
116             case PAGED_WINDOWED_PATH:
117                 cursor = buildPagedWindowedResults(uri, queryArgs, recordCount);
118                 break;
119             default:
120                 throw new IllegalArgumentException("Unrecognized path: " + uri.getPath());
121         }
122 
123         cursor.setNotificationUri(getContext().getContentResolver(), uri);
124 
125         return cursor;
126     }
127 
128     /**
129      * Return a int value specified in Bundle key, Uri query arg, or fallback default value.
130      */
getIntValue(String key, Bundle queryArgs, Uri uri, int defaultValue)131     private static int getIntValue(String key, Bundle queryArgs, Uri uri, int defaultValue) {
132         int value = queryArgs.getInt(key, Integer.MIN_VALUE);
133         if (value != Integer.MIN_VALUE) {
134             return value;
135         }
136 
137         @Nullable String argValue = uri.getQueryParameter(key);
138         if (argValue != null) {
139             try {
140                 return Integer.parseInt(argValue);
141             } catch (NumberFormatException ignored) {
142             }
143         }
144 
145         return defaultValue;
146     }
147 
buildPagedResults(Uri uri, Bundle queryArgs, int recordsetSize)148     private MatrixCursor buildPagedResults(Uri uri, Bundle queryArgs, int recordsetSize) {
149         int offset = getIntValue(ContentResolver.QUERY_ARG_OFFSET, queryArgs, uri, 0);
150         int limit = getIntValue(ContentResolver.QUERY_ARG_LIMIT, queryArgs, uri, recordsetSize);
151 
152         MatrixCursor c = createInMemoryCursor();
153         Bundle extras = c.getExtras();
154 
155         // Calculate the number of items to include in the cursor.
156         int numItems = constrain(recordsetSize - offset, 0, limit);
157 
158         // Build the paged result set.
159         for (int i = offset; i < offset + numItems; i++) {
160             fillRow(c.newRow(), i);
161         }
162 
163         extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, new String[] {
164                 ContentResolver.QUERY_ARG_OFFSET,
165                 ContentResolver.QUERY_ARG_LIMIT
166         });
167         extras.putInt(ContentResolver.EXTRA_TOTAL_COUNT, recordsetSize);
168         return c;
169     }
170 
buildPagedWindowedResults( Uri uri, Bundle queryArgs, int recordsetSize)171     private AbstractWindowedCursor buildPagedWindowedResults(
172             Uri uri, Bundle queryArgs, int recordsetSize) {
173         int offset = getIntValue(ContentResolver.QUERY_ARG_OFFSET, queryArgs, uri, 0);
174         int limit = getIntValue(ContentResolver.QUERY_ARG_LIMIT, queryArgs, uri, recordsetSize);
175 
176         int windowSize = limit - 1;
177 
178         TestWindowedCursor c = new TestWindowedCursor(PROJECTION, recordsetSize);
179         CursorWindow window = c.getWindow();
180         window.setNumColumns(PROJECTION.length);
181 
182         Bundle extras = c.getExtras();
183 
184         // Build the unpaged result set.
185         for (int row = 0; row < windowSize; row++) {
186             if (!window.allocRow()) {
187                 break;
188             }
189             if (!fillRow(window, row)) {
190                 window.freeLastRow();
191                 break;
192             }
193         }
194 
195         extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, new String[] {
196                 ContentResolver.QUERY_ARG_OFFSET,
197                 ContentResolver.QUERY_ARG_LIMIT
198         });
199         extras.putInt(ContentResolver.EXTRA_TOTAL_COUNT, recordsetSize);
200         return c;
201     }
202 
buildUnpagedResults(int recordsetSize)203     private MatrixCursor buildUnpagedResults(int recordsetSize) {
204         MatrixCursor c = createInMemoryCursor();
205 
206         // Build the unpaged result set.
207         for (int i = 0; i < recordsetSize; i++) {
208             fillRow(c.newRow(), i);
209         }
210 
211         return c;
212     }
213 
214     /**
215      * Returns data type of the given object's value.
216      *<p>
217      * Returned values are
218      * <ul>
219      *   <li>{@link Cursor#FIELD_TYPE_NULL}</li>
220      *   <li>{@link Cursor#FIELD_TYPE_INTEGER}</li>
221      *   <li>{@link Cursor#FIELD_TYPE_FLOAT}</li>
222      *   <li>{@link Cursor#FIELD_TYPE_STRING}</li>
223      *   <li>{@link Cursor#FIELD_TYPE_BLOB}</li>
224      *</ul>
225      *</p>
226      */
getTypeOfObject(Object obj)227     public static int getTypeOfObject(Object obj) {
228         if (obj == null) {
229             return Cursor.FIELD_TYPE_NULL;
230         } else if (obj instanceof byte[]) {
231             return Cursor.FIELD_TYPE_BLOB;
232         } else if (obj instanceof Float || obj instanceof Double) {
233             return Cursor.FIELD_TYPE_FLOAT;
234         } else if (obj instanceof Long || obj instanceof Integer
235                 || obj instanceof Short || obj instanceof Byte) {
236             return Cursor.FIELD_TYPE_INTEGER;
237         } else {
238             return Cursor.FIELD_TYPE_STRING;
239         }
240     }
241 
createInMemoryCursor()242     private MatrixCursor createInMemoryCursor() {
243         MatrixCursor c = new MatrixCursor(PROJECTION);
244         Bundle extras = new Bundle();
245         c.setExtras(extras);
246         return c;
247     }
248 
fillRow(RowBuilder row, int rowId)249     private void fillRow(RowBuilder row, int rowId) {
250         row.add(createCellValue(rowId, 0));
251         row.add(createCellValue(rowId, 1));
252         row.add(createCellValue(rowId, 2));
253         row.add(createCellValue(rowId, 3));
254         row.add(createCellValue(rowId, 4));
255     }
256 
257     /**
258      * @return true if the row was successfully populated. If false, caller should freeLastRow.
259      */
fillRow(CursorWindow window, int row)260     private static boolean fillRow(CursorWindow window, int row) {
261         if (!window.putLong((int) createCellValue(row, 0), row, 0)) {
262             return false;
263         }
264         for (int i = 1; i < PROJECTION.length; i++) {
265             if (!window.putString((String) createCellValue(row, i), row, i)) {
266                 return false;
267             }
268         }
269         return true;
270     }
271 
createCellValue(int row, int col)272     private static Object createCellValue(int row, int col) {
273         switch(col) {
274             case 0:
275                 return row;
276             case 1:
277                 return "--aaa--" + row;
278             case 2:
279                 return "**bbb**" + row;
280             case 3:
281                 return ("^^ccc^^" + row);
282             case 4:
283                 return "##ddd##" + row;
284             default:
285                 throw new IllegalArgumentException("Unsupported column: " + col);
286         }
287     }
288 
289     /**
290      * Asserts that the value at the current cursor position x column
291      * is expected test data for the supplied row.
292      *
293      * <p>Cursor must be pre-positioned.
294      *
295      * @param cursor must be prepositioned to the row to be tested.
296      * @param row row value expected to be reflected in cell. This can be different
297      *            than the cursor position due to paging.
298      * @param column
299      */
300     @VisibleForTesting
assertExpectedCellValue(Cursor cursor, int row, int column)301     public static void assertExpectedCellValue(Cursor cursor, int row, int column) {
302         int type = cursor.getType(column);
303         switch(type) {
304             case Cursor.FIELD_TYPE_NULL:
305                 throw new UnsupportedOperationException("Not implemented.");
306             case Cursor.FIELD_TYPE_INTEGER:
307                 assertEquals(createCellValue(row, column), cursor.getInt(column));
308                 break;
309             case Cursor.FIELD_TYPE_FLOAT:
310                 assertEquals(createCellValue(row, column), cursor.getDouble(column));
311                 break;
312             case Cursor.FIELD_TYPE_BLOB:
313                 assertEquals(createCellValue(row, column), cursor.getBlob(column));
314                 break;
315             case Cursor.FIELD_TYPE_STRING:
316                 assertEquals(createCellValue(row, column), cursor.getString(column));
317                 break;
318             default:
319                 throw new UnsupportedOperationException("Unknown column type: " + type);
320         }
321     }
322 
323     @Override
getType(Uri uri)324     public String getType(Uri uri) {
325         throw new UnsupportedOperationException();
326     }
327 
328     @Override
insert(Uri uri, ContentValues values)329     public Uri insert(Uri uri, ContentValues values) {
330         throw new UnsupportedOperationException();
331     }
332 
333     @Override
delete(Uri uri, String selection, String[] selectionArgs)334     public int delete(Uri uri, String selection, String[] selectionArgs) {
335         throw new UnsupportedOperationException();
336     }
337 
338     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)339     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
340         throw new UnsupportedOperationException();
341     }
342 
constrain(int amount, int low, int high)343     private static int constrain(int amount, int low, int high) {
344         return amount < low ? low : (amount > high ? high : amount);
345     }
346 
347     /**
348      * Returns a Uri that includes paging information embedded in the URI.
349      * This allows a test client to force paged results when running on older SDKs...
350      * pre Android O SDKs lacking the ContentResolver#query w/ Bundle override
351      * necessary for paging.
352      */
forcePagingSpec(Uri uri, int offset, int limit)353     public static Uri forcePagingSpec(Uri uri, int offset, int limit) {
354         assert (uri.getPath().equals(TestContentProvider.PAGED_PATH)
355                 || uri.getPath().equals(TestContentProvider.PAGED_WINDOWED_PATH));
356         return uri.buildUpon()
357                 .appendQueryParameter(ContentResolver.QUERY_ARG_OFFSET, String.valueOf(offset))
358                 .appendQueryParameter(ContentResolver.QUERY_ARG_LIMIT, String.valueOf(limit))
359                 .build();
360     }
361 
forceRecordCount(Uri uri, int recordCount)362     public static Uri forceRecordCount(Uri uri, int recordCount) {
363         return uri.buildUpon()
364                 .appendQueryParameter(RECORD_COUNT, String.valueOf(recordCount))
365                 .build();
366     }
367 
368     private static final class TestWindowedCursor extends AbstractWindowedCursor {
369 
370         private final String[] mProjection;
371         private final int mCount;
372         private final Bundle mExtras;
373 
TestWindowedCursor(String[] projection, int count)374         TestWindowedCursor(String[] projection, int count) {
375             mProjection = projection;
376             mCount = count;
377             mExtras = new Bundle();
378 
379             setWindow(new CursorWindow("stevie"));
380         }
381 
382         @Override
getExtras()383         public Bundle getExtras() {
384             return mExtras;
385         }
386 
387         @Override
getCount()388         public int getCount() {
389             return mCount;
390         }
391 
392         @Override
getColumnNames()393         public String[] getColumnNames() {
394             return mProjection;
395         }
396     }
397 }
398