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