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