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