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
17package androidx.contentpager.content;
18
19import static org.junit.Assert.assertEquals;
20
21import android.content.ContentProvider;
22import android.content.ContentResolver;
23import android.content.ContentValues;
24import android.database.AbstractWindowedCursor;
25import android.database.Cursor;
26import android.database.CursorWindow;
27import android.database.MatrixCursor;
28import android.database.MatrixCursor.RowBuilder;
29import android.net.Uri;
30import android.os.Bundle;
31import android.os.CancellationSignal;
32
33import androidx.annotation.Nullable;
34import androidx.annotation.VisibleForTesting;
35
36/**
37 * A stub data paging provider used for testing of paging support.
38 * Ignores client supplied projections.
39 */
40public 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