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