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