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