1 /*
2  * Copyright (C) 2008 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 android.content.cts;
18 
19 import static junit.framework.Assert.assertEquals;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.content.ContentProvider;
24 import android.content.ContentProvider.PipeDataWriter;
25 import android.content.ContentResolver;
26 import android.content.ContentUris;
27 import android.content.ContentValues;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.UriMatcher;
31 import android.content.pm.PackageManager;
32 import android.content.res.AssetFileDescriptor;
33 import android.database.Cursor;
34 import android.database.SQLException;
35 import android.database.sqlite.SQLiteDatabase;
36 import android.database.sqlite.SQLiteOpenHelper;
37 import android.database.sqlite.SQLiteQueryBuilder;
38 import android.net.Uri;
39 import android.os.Bundle;
40 import android.os.CancellationSignal;
41 import android.os.ParcelFileDescriptor;
42 import android.os.SystemClock;
43 import android.text.TextUtils;
44 import android.util.Log;
45 
46 import java.io.File;
47 import java.io.FileNotFoundException;
48 import java.io.FileOutputStream;
49 import java.io.IOException;
50 import java.io.OutputStreamWriter;
51 import java.io.PrintWriter;
52 import java.io.UnsupportedEncodingException;
53 import java.util.HashMap;
54 
55 public class MockContentProvider extends ContentProvider implements PipeDataWriter<String> {
56     private static final String TAG = "MockContentProvider";
57 
58     private static final String DEFAULT_AUTHORITY = "ctstest";
59     private static final String DEFAULT_DBNAME = "ctstest.db";
60     private static final int DBVERSION = 2;
61 
62     private static final int TESTTABLE1 = 1;
63     private static final int TESTTABLE1_ID = 2;
64     private static final int TESTTABLE1_CROSS = 3;
65     private static final int TESTTABLE2 = 4;
66     private static final int TESTTABLE2_ID = 5;
67     private static final int CRASH_ID = 6;
68     private static final int HANG_ID = 7;
69 
70     private static @Nullable Uri sRefreshedUri;
71     private static boolean sRefreshReturnValue;
72 
73     private final String mAuthority;
74     private final String mDbName;
75     private final UriMatcher URL_MATCHER;
76     private HashMap<String, String> CTSDBTABLE1_LIST_PROJECTION_MAP;
77     private HashMap<String, String> CTSDBTABLE2_LIST_PROJECTION_MAP;
78 
79     private SQLiteOpenHelper mOpenHelper;
80 
81     private static class DatabaseHelper extends SQLiteOpenHelper {
82 
DatabaseHelper(Context context, String dbname)83         DatabaseHelper(Context context, String dbname) {
84             super(context, dbname, null, DBVERSION);
85         }
86 
87         @Override
onCreate(SQLiteDatabase db)88         public void onCreate(SQLiteDatabase db) {
89             db.execSQL("CREATE TABLE TestTable1 ("
90                     + "_id INTEGER PRIMARY KEY, " + "key TEXT, " + "value INTEGER"
91                     + ");");
92 
93             db.execSQL("CREATE TABLE TestTable2 ("
94                     + "_id INTEGER PRIMARY KEY, " + "key TEXT, " + "value INTEGER"
95                     + ");");
96         }
97 
98         @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)99         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
100             db.execSQL("DROP TABLE IF EXISTS TestTable1");
101             db.execSQL("DROP TABLE IF EXISTS TestTable2");
102             onCreate(db);
103         }
104     }
105 
MockContentProvider()106     public MockContentProvider() {
107         this(DEFAULT_AUTHORITY, DEFAULT_DBNAME);
108     }
109 
MockContentProvider(String authority, String dbName)110     public MockContentProvider(String authority, String dbName) {
111         mAuthority = authority;
112         mDbName = dbName;
113 
114         URL_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
115         URL_MATCHER.addURI(mAuthority, "testtable1", TESTTABLE1);
116         URL_MATCHER.addURI(mAuthority, "testtable1/#", TESTTABLE1_ID);
117         URL_MATCHER.addURI(mAuthority, "testtable1/cross", TESTTABLE1_CROSS);
118         URL_MATCHER.addURI(mAuthority, "testtable2", TESTTABLE2);
119         URL_MATCHER.addURI(mAuthority, "testtable2/#", TESTTABLE2_ID);
120         URL_MATCHER.addURI(mAuthority, "crash", CRASH_ID);
121         URL_MATCHER.addURI(mAuthority, "hang", HANG_ID);
122 
123         CTSDBTABLE1_LIST_PROJECTION_MAP = new HashMap<>();
124         CTSDBTABLE1_LIST_PROJECTION_MAP.put("_id", "_id");
125         CTSDBTABLE1_LIST_PROJECTION_MAP.put("key", "key");
126         CTSDBTABLE1_LIST_PROJECTION_MAP.put("value", "value");
127 
128         CTSDBTABLE2_LIST_PROJECTION_MAP = new HashMap<>();
129         CTSDBTABLE2_LIST_PROJECTION_MAP.put("_id", "_id");
130         CTSDBTABLE2_LIST_PROJECTION_MAP.put("key", "key");
131         CTSDBTABLE2_LIST_PROJECTION_MAP.put("value", "value");
132     }
133 
134     @Override
onCreate()135     public boolean onCreate() {
136         mOpenHelper = new DatabaseHelper(getContext(), mDbName);
137         crashOnLaunchIfNeeded();
138         return true;
139     }
140 
141     @Override
delete(Uri uri, String selection, String[] selectionArgs)142     public int delete(Uri uri, String selection, String[] selectionArgs) {
143         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
144         String segment;
145         int count;
146 
147         switch (URL_MATCHER.match(uri)) {
148         case TESTTABLE1:
149             if (null == selection) {
150                 // get the count when remove all rows
151                 selection = "1";
152             }
153             count = db.delete("TestTable1", selection, selectionArgs);
154             break;
155         case TESTTABLE1_ID:
156             segment = uri.getPathSegments().get(1);
157             count = db.delete("TestTable1", "_id=" + segment +
158                     (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""),
159                     selectionArgs);
160             break;
161         case TESTTABLE2:
162             count = db.delete("TestTable2", selection, selectionArgs);
163             break;
164         case TESTTABLE2_ID:
165             segment = uri.getPathSegments().get(1);
166             count = db.delete("TestTable2", "_id=" + segment +
167                     (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""),
168                     selectionArgs);
169             break;
170         case CRASH_ID:
171             // Wha...?  Delete ME?!?  O.K.!
172             Log.i(TAG, "Delete self requested!");
173             count = 1;
174             android.os.Process.killProcess(android.os.Process.myPid());
175             break;
176         default:
177             throw new IllegalArgumentException("Unknown URL " + uri);
178         }
179 
180         getContext().getContentResolver().notifyChange(uri, null);
181         return count;
182     }
183 
184     @Override
getType(Uri uri)185     public String getType(Uri uri) {
186         switch (URL_MATCHER.match(uri)) {
187         case TESTTABLE1:
188             return "vnd.android.cursor.dir/com.android.content.testtable1";
189         case TESTTABLE1_ID:
190             return "vnd.android.cursor.item/com.android.content.testtable1";
191         case TESTTABLE1_CROSS:
192             return "vnd.android.cursor.cross/com.android.content.testtable1";
193         case TESTTABLE2:
194             return "vnd.android.cursor.dir/com.android.content.testtable2";
195         case TESTTABLE2_ID:
196             return "vnd.android.cursor.item/com.android.content.testtable2";
197 
198         default:
199             throw new IllegalArgumentException("Unknown URL " + uri);
200         }
201     }
202 
203     @Override
getStreamTypes(@onNull Uri uri, @NonNull String mimeTypeFilter)204     public String[] getStreamTypes(@NonNull Uri uri, @NonNull String mimeTypeFilter) {
205         if (URL_MATCHER.match(uri) == TESTTABLE2_ID) {
206             switch (Integer.parseInt(uri.getPathSegments().get(1)) % 10) {
207                 case 0:
208                     return new String[]{"image/jpeg"};
209                 case 1:
210                     return new String[]{"audio/mpeg"};
211                 case 2:
212                     return new String[]{"video/mpeg", "audio/mpeg"};
213             }
214         }
215         return super.getStreamTypes(uri, mimeTypeFilter);
216     }
217 
218     @Override
insert(Uri uri, ContentValues initialValues)219     public Uri insert(Uri uri, ContentValues initialValues) {
220         long rowID;
221         ContentValues values;
222         String table;
223         Uri testUri;
224 
225         if (initialValues != null)
226             values = new ContentValues(initialValues);
227         else
228             values = new ContentValues();
229 
230         if (values.containsKey("value") == false)
231             values.put("value", -1);
232 
233         switch (URL_MATCHER.match(uri)) {
234         case TESTTABLE1:
235             table = "TestTable1";
236             testUri = Uri.parse("content://" + mAuthority + "/testtable1");
237             break;
238         case TESTTABLE2:
239             table = "TestTable2";
240             testUri = Uri.parse("content://" + mAuthority + "/testtable2");
241             break;
242         default:
243             throw new IllegalArgumentException("Unknown URL " + uri);
244         }
245 
246         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
247         rowID = db.insert(table, "key", values);
248 
249         if (rowID > 0) {
250             Uri url = ContentUris.withAppendedId(testUri, rowID);
251             getContext().getContentResolver().notifyChange(url, null);
252             return url;
253         }
254 
255         throw new SQLException("Failed to insert row into " + uri);
256     }
257 
258     @Override
query(@onNull Uri uri, @Nullable String[] projection, @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal)259     public @Nullable Cursor query(@NonNull Uri uri, @Nullable String[] projection,
260             @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal) {
261         if (queryArgs != null && queryArgs.containsKey(ContentResolver.QUERY_ARG_SORT_LOCALE)) {
262             final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
263             final String locale = queryArgs.getString(ContentResolver.QUERY_ARG_SORT_LOCALE);
264             final String safeLocale = locale.replaceAll("[^a-zA-Z]", "");
265             try (Cursor c = db.rawQuery("SELECT icu_load_collation(?, ?);",
266                     new String[] { locale, safeLocale }, cancellationSignal)) {
267                 while (c.moveToNext()) {
268                 }
269             }
270 
271             final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
272             qb.setTables("TestTable1");
273             qb.setProjectionMap(CTSDBTABLE1_LIST_PROJECTION_MAP);
274 
275             final String sortOrder = TextUtils.join(", ",
276                     queryArgs.getStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS));
277             return qb.query(db, projection, null, null, null, null,
278                     sortOrder + " COLLATE " + safeLocale,
279                     null, cancellationSignal);
280         } else {
281             return super.query(uri, projection, queryArgs, cancellationSignal);
282         }
283     }
284 
285     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)286     public Cursor query(Uri uri, String[] projection, String selection,
287             String[] selectionArgs, String sortOrder) {
288         return query(uri, projection, selection, selectionArgs, sortOrder, null);
289     }
290 
291     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal)292     public Cursor query(Uri uri, String[] projection, String selection,
293             String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal) {
294 
295         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
296 
297         switch (URL_MATCHER.match(uri)) {
298         case TESTTABLE1:
299             qb.setTables("TestTable1");
300             qb.setProjectionMap(CTSDBTABLE1_LIST_PROJECTION_MAP);
301             break;
302 
303         case TESTTABLE1_ID:
304             qb.setTables("TestTable1");
305             qb.appendWhere("_id=" + uri.getPathSegments().get(1));
306             break;
307 
308         case TESTTABLE1_CROSS:
309             // Create a ridiculous cross-product of the test table.  This is done
310             // to create an artificially long-running query to enable us to test
311             // remote query cancellation in ContentResolverTest.
312             qb.setTables("TestTable1 a, TestTable1 b, TestTable1 c, TestTable1 d, TestTable1 e");
313             break;
314 
315         case TESTTABLE2:
316             qb.setTables("TestTable2");
317             qb.setProjectionMap(CTSDBTABLE2_LIST_PROJECTION_MAP);
318             break;
319 
320         case TESTTABLE2_ID:
321             qb.setTables("TestTable2");
322             qb.appendWhere("_id=" + uri.getPathSegments().get(1));
323             break;
324 
325         case CRASH_ID:
326             crashOnLaunchIfNeeded();
327             qb.setTables("TestTable1");
328             qb.setProjectionMap(CTSDBTABLE1_LIST_PROJECTION_MAP);
329             break;
330 
331         case HANG_ID:
332             while (true) {
333                 Log.i(TAG, "Hanging provider by request...");
334                 SystemClock.sleep(1000);
335             }
336 
337         default:
338             throw new IllegalArgumentException("Unknown URL " + uri);
339         }
340 
341         /* If no sort order is specified use the default */
342         String orderBy = TextUtils.isEmpty(sortOrder) ? "_id" : sortOrder;
343 
344         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
345         Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy,
346                 null, cancellationSignal);
347 
348         c.setNotificationUri(getContext().getContentResolver(), uri);
349         return c;
350     }
351 
352     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)353     public int update(Uri uri, ContentValues values, String selection,
354             String[] selectionArgs) {
355         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
356         String segment;
357         int count;
358 
359         switch (URL_MATCHER.match(uri)) {
360         case TESTTABLE1:
361             count = db.update("TestTable1", values, selection, selectionArgs);
362             break;
363 
364         case TESTTABLE1_ID:
365             segment = uri.getPathSegments().get(1);
366             count = db.update("TestTable1", values, "_id=" + segment +
367                     (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""),
368                     selectionArgs);
369             break;
370 
371         case TESTTABLE2:
372             count = db.update("TestTable2", values, selection, selectionArgs);
373             break;
374 
375         case TESTTABLE2_ID:
376             segment = uri.getPathSegments().get(1);
377             count = db.update("TestTable2", values, "_id=" + segment +
378                     (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""),
379                     selectionArgs);
380             break;
381 
382         default:
383             throw new IllegalArgumentException("Unknown URL " + uri);
384         }
385 
386         getContext().getContentResolver().notifyChange(uri, null);
387         return count;
388     }
389 
390     @Override
openAssetFile(Uri uri, String mode)391     public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
392         switch (URL_MATCHER.match(uri)) {
393             case CRASH_ID:
394                 crashOnLaunchIfNeeded();
395                 return new AssetFileDescriptor(
396                         openPipeHelper(uri, null, null,
397                                 "This is the openAssetFile test data!", this), 0,
398                         AssetFileDescriptor.UNKNOWN_LENGTH);
399 
400             default:
401                 return super.openAssetFile(uri, mode);
402         }
403     }
404 
405     @Override
openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)406     public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
407             throws FileNotFoundException {
408         switch (URL_MATCHER.match(uri)) {
409             case CRASH_ID:
410                 crashOnLaunchIfNeeded();
411                 return new AssetFileDescriptor(
412                         openPipeHelper(uri, null, null,
413                                 "This is the openTypedAssetFile test data!", this), 0,
414                         AssetFileDescriptor.UNKNOWN_LENGTH);
415 
416             default:
417                 return super.openTypedAssetFile(uri, mimeTypeFilter, opts);
418         }
419     }
420 
421     @Override
writeDataToPipe(ParcelFileDescriptor output, Uri uri, String mimeType, Bundle opts, String args)422     public void writeDataToPipe(ParcelFileDescriptor output, Uri uri, String mimeType, Bundle opts,
423             String args) {
424         FileOutputStream fout = new FileOutputStream(output.getFileDescriptor());
425         PrintWriter pw = null;
426         try {
427             pw = new PrintWriter(new OutputStreamWriter(fout, "UTF-8"));
428             pw.print(args);
429         } catch (UnsupportedEncodingException e) {
430             Log.w(TAG, "Ooops", e);
431         } finally {
432             if (pw != null) {
433                 pw.flush();
434             }
435             try {
436                 fout.close();
437             } catch (IOException e) {
438             }
439         }
440     }
441 
442     @Override
refresh(Uri uri, @Nullable Bundle args, @Nullable CancellationSignal cancellationSignal)443     public boolean refresh(Uri uri, @Nullable Bundle args,
444             @Nullable CancellationSignal cancellationSignal) {
445         sRefreshedUri = uri;
446         return sRefreshReturnValue;
447     }
448 
449     @Override
checkUriPermission(@onNull Uri uri, int uid, @Intent.AccessUriMode int modeFlags)450     public int checkUriPermission(@NonNull Uri uri, int uid, @Intent.AccessUriMode int modeFlags) {
451         if ((modeFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
452             return PackageManager.PERMISSION_GRANTED;
453         } else {
454             return PackageManager.PERMISSION_DENIED;
455         }
456     }
457 
crashOnLaunchIfNeeded()458     private void crashOnLaunchIfNeeded() {
459         if (getCrashOnLaunch(getContext())) {
460             // The test case wants us to crash our process on first launch.
461             // Well, okay then!
462             Log.i(TAG, "TEST IS CRASHING SELF, CROSS FINGERS!");
463             setCrashOnLaunch(getContext(), false);
464             android.os.Process.killProcess(android.os.Process.myPid());
465         }
466     }
467 
getCrashOnLaunch(Context context)468     public static boolean getCrashOnLaunch(Context context) {
469         File file = getCrashOnLaunchFile(context);
470         return file.exists();
471     }
472 
setCrashOnLaunch(Context context, boolean value)473     public static void setCrashOnLaunch(Context context, boolean value) {
474         File file = getCrashOnLaunchFile(context);
475         if (value) {
476             try {
477                 file.createNewFile();
478             } catch (IOException ex) {
479                 throw new RuntimeException("Could not create crash on launch file.", ex);
480             }
481         } else {
482             file.delete();
483         }
484     }
485 
setRefreshReturnValue(boolean value)486     public static void setRefreshReturnValue(boolean value) {
487         sRefreshReturnValue = value;
488     }
489 
assertRefreshed(Uri expectedUri)490     public static void assertRefreshed(Uri expectedUri) {
491         assertEquals(sRefreshedUri, expectedUri);
492     }
493 
getCrashOnLaunchFile(Context context)494     private static File getCrashOnLaunchFile(Context context) {
495         return context.getFileStreamPath("MockContentProvider.crashonlaunch");
496     }
497 }
498