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 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
     86     public boolean onCreate() {
     87         return true;
     88     }
     89 
     90     @Override
     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
     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      */
    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 
    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 
    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 
    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      */
    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 
    242     private MatrixCursor createInMemoryCursor() {
    243         MatrixCursor c = new MatrixCursor(PROJECTION);
    244         Bundle extras = new Bundle();
    245         c.setExtras(extras);
    246         return c;
    247     }
    248 
    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      */
    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 
    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
    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
    324     public String getType(Uri uri) {
    325         throw new UnsupportedOperationException();
    326     }
    327 
    328     @Override
    329     public Uri insert(Uri uri, ContentValues values) {
    330         throw new UnsupportedOperationException();
    331     }
    332 
    333     @Override
    334     public int delete(Uri uri, String selection, String[] selectionArgs) {
    335         throw new UnsupportedOperationException();
    336     }
    337 
    338     @Override
    339     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    340         throw new UnsupportedOperationException();
    341     }
    342 
    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      */
    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 
    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 
    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
    383         public Bundle getExtras() {
    384             return mExtras;
    385         }
    386 
    387         @Override
    388         public int getCount() {
    389             return mCount;
    390         }
    391 
    392         @Override
    393         public String[] getColumnNames() {
    394             return mProjection;
    395         }
    396     }
    397 }
    398