1 package org.robolectric.shadows; 2 3 import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION; 4 import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS; 5 import static android.content.ContentResolver.QUERY_ARG_SQL_SORT_ORDER; 6 import static android.content.ContentResolver.SCHEME_ANDROID_RESOURCE; 7 import static android.content.ContentResolver.SCHEME_CONTENT; 8 import static android.content.ContentResolver.SCHEME_FILE; 9 import static android.os.Build.VERSION_CODES.N; 10 import static android.os.Build.VERSION_CODES.O; 11 import static android.os.Build.VERSION_CODES.Q; 12 import static org.robolectric.util.reflector.Reflector.reflector; 13 14 import android.accounts.Account; 15 import android.annotation.NonNull; 16 import android.annotation.SuppressLint; 17 import android.content.ContentProvider; 18 import android.content.ContentProviderClient; 19 import android.content.ContentProviderOperation; 20 import android.content.ContentProviderResult; 21 import android.content.ContentResolver; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.content.IContentProvider; 25 import android.content.Intent; 26 import android.content.OperationApplicationException; 27 import android.content.PeriodicSync; 28 import android.content.SyncAdapterType; 29 import android.content.SyncInfo; 30 import android.content.UriPermission; 31 import android.content.pm.ProviderInfo; 32 import android.database.ContentObserver; 33 import android.database.Cursor; 34 import android.net.Uri; 35 import android.os.Bundle; 36 import android.os.CancellationSignal; 37 import com.google.common.base.Splitter; 38 import java.io.FileNotFoundException; 39 import java.io.IOException; 40 import java.io.InputStream; 41 import java.io.OutputStream; 42 import java.lang.reflect.InvocationTargetException; 43 import java.util.ArrayList; 44 import java.util.Collection; 45 import java.util.Collections; 46 import java.util.HashMap; 47 import java.util.Iterator; 48 import java.util.List; 49 import java.util.Map; 50 import java.util.Objects; 51 import java.util.concurrent.CopyOnWriteArrayList; 52 import java.util.function.Supplier; 53 import org.robolectric.RuntimeEnvironment; 54 import org.robolectric.annotation.Implementation; 55 import org.robolectric.annotation.Implements; 56 import org.robolectric.annotation.RealObject; 57 import org.robolectric.annotation.Resetter; 58 import org.robolectric.fakes.BaseCursor; 59 import org.robolectric.shadow.api.Shadow; 60 import org.robolectric.util.NamedStream; 61 import org.robolectric.util.ReflectionHelpers; 62 import org.robolectric.util.ReflectionHelpers.ClassParameter; 63 import org.robolectric.util.reflector.Accessor; 64 import org.robolectric.util.reflector.Direct; 65 import org.robolectric.util.reflector.ForType; 66 67 @Implements(ContentResolver.class) 68 @SuppressLint("NewApi") 69 public class ShadowContentResolver { 70 private int nextDatabaseIdForInserts; 71 72 @RealObject ContentResolver realContentResolver; 73 74 private BaseCursor cursor; 75 private static final List<Statement> statements = new CopyOnWriteArrayList<>(); 76 private static final List<InsertStatement> insertStatements = new CopyOnWriteArrayList<>(); 77 private static final List<UpdateStatement> updateStatements = new CopyOnWriteArrayList<>(); 78 private static final List<DeleteStatement> deleteStatements = new CopyOnWriteArrayList<>(); 79 private static final List<NotifiedUri> notifiedUris = new ArrayList<>(); 80 private static final Map<Uri, BaseCursor> uriCursorMap = new HashMap<>(); 81 private static final Map<Uri, Supplier<InputStream>> inputStreamMap = new HashMap<>(); 82 private static final Map<Uri, Supplier<OutputStream>> outputStreamMap = new HashMap<>(); 83 private static final Map<String, List<ContentProviderOperation>> contentProviderOperations = 84 new HashMap<>(); 85 private static final List<UriPermission> uriPermissions = new ArrayList<>(); 86 87 private static final CopyOnWriteArrayList<ContentObserverEntry> contentObservers = 88 new CopyOnWriteArrayList<>(); 89 90 private static final Map<String, Map<Account, Status>> syncableAccounts = new HashMap<>(); 91 private static final Map<String, ContentProvider> providers = 92 Collections.synchronizedMap(new HashMap<>()); 93 private static boolean masterSyncAutomatically; 94 95 private static SyncAdapterType[] syncAdapterTypes; 96 97 @Resetter reset()98 public static void reset() { 99 statements.clear(); 100 insertStatements.clear(); 101 updateStatements.clear(); 102 deleteStatements.clear(); 103 notifiedUris.clear(); 104 uriCursorMap.clear(); 105 inputStreamMap.clear(); 106 outputStreamMap.clear(); 107 contentProviderOperations.clear(); 108 uriPermissions.clear(); 109 contentObservers.clear(); 110 syncableAccounts.clear(); 111 providers.clear(); 112 masterSyncAutomatically = false; 113 } 114 115 private static class ContentObserverEntry { 116 public final Uri uri; 117 public final boolean notifyForDescendents; 118 public final ContentObserver observer; 119 ContentObserverEntry(Uri uri, boolean notifyForDescendents, ContentObserver observer)120 private ContentObserverEntry(Uri uri, boolean notifyForDescendents, ContentObserver observer) { 121 this.uri = uri; 122 this.notifyForDescendents = notifyForDescendents; 123 this.observer = observer; 124 125 if (uri == null || observer == null) { 126 throw new NullPointerException(); 127 } 128 } 129 matches(Uri test)130 public boolean matches(Uri test) { 131 if (!Objects.equals(uri.getScheme(), test.getScheme())) { 132 return false; 133 } 134 if (!Objects.equals(uri.getAuthority(), test.getAuthority())) { 135 return false; 136 } 137 138 String uriPath = uri.getPath(); 139 String testPath = test.getPath(); 140 141 return Objects.equals(uriPath, testPath) 142 || (notifyForDescendents && testPath != null && testPath.startsWith(uriPath)); 143 } 144 } 145 146 public static class NotifiedUri { 147 public final Uri uri; 148 public final boolean syncToNetwork; 149 public final ContentObserver observer; 150 public final int flags; 151 NotifiedUri(Uri uri, ContentObserver observer, int flags)152 public NotifiedUri(Uri uri, ContentObserver observer, int flags) { 153 this.uri = uri; 154 this.syncToNetwork = flags == ContentResolver.NOTIFY_SYNC_TO_NETWORK; 155 this.observer = observer; 156 this.flags = flags; 157 } 158 } 159 160 public static class Status { 161 public int syncRequests; 162 public int state = -1; 163 public boolean syncAutomatically; 164 public Bundle syncExtras; 165 public List<PeriodicSync> syncs = new ArrayList<>(); 166 } 167 registerInputStream(Uri uri, InputStream inputStream)168 public void registerInputStream(Uri uri, InputStream inputStream) { 169 inputStreamMap.put(uri, () -> inputStream); 170 } 171 registerInputStreamSupplier(Uri uri, Supplier<InputStream> supplier)172 public void registerInputStreamSupplier(Uri uri, Supplier<InputStream> supplier) { 173 inputStreamMap.put(uri, supplier); 174 } 175 registerOutputStream(Uri uri, OutputStream outputStream)176 public void registerOutputStream(Uri uri, OutputStream outputStream) { 177 outputStreamMap.put(uri, () -> outputStream); 178 } 179 registerOutputStreamSupplier(Uri uri, Supplier<OutputStream> supplier)180 public void registerOutputStreamSupplier(Uri uri, Supplier<OutputStream> supplier) { 181 outputStreamMap.put(uri, supplier); 182 } 183 184 @Implementation openInputStream(final Uri uri)185 protected InputStream openInputStream(final Uri uri) throws FileNotFoundException { 186 Supplier<InputStream> supplier = inputStreamMap.get(uri); 187 if (supplier != null) { 188 InputStream inputStream = supplier.get(); 189 if (inputStream != null) { 190 return inputStream; 191 } 192 } 193 String scheme = uri.getScheme(); 194 if (SCHEME_ANDROID_RESOURCE.equals(scheme) 195 || SCHEME_FILE.equals(scheme) 196 || (SCHEME_CONTENT.equals(scheme) && getProvider(uri, getContext()) != null)) { 197 return reflector(ContentResolverReflector.class, realContentResolver).openInputStream(uri); 198 } 199 return new UnregisteredInputStream(uri); 200 } 201 202 @Implementation openOutputStream(final Uri uri)203 protected OutputStream openOutputStream(final Uri uri) throws FileNotFoundException { 204 try { 205 return openOutputStream(uri, "w"); 206 } catch (SecurityException | FileNotFoundException e) { 207 // This is legacy behavior is only supported because existing users require it. 208 return new OutputStream() { 209 @Override 210 public void write(int arg0) throws IOException {} 211 212 @Override 213 public String toString() { 214 return "outputstream for " + uri; 215 } 216 }; 217 } 218 } 219 220 @Implementation openOutputStream(Uri uri, String mode)221 protected OutputStream openOutputStream(Uri uri, String mode) throws FileNotFoundException { 222 Supplier<OutputStream> supplier = outputStreamMap.get(uri); 223 if (supplier != null) { 224 OutputStream outputStream = supplier.get(); 225 if (outputStream != null) { 226 return outputStream; 227 } 228 } 229 return reflector(ContentResolverReflector.class, realContentResolver) 230 .openOutputStream(uri, mode); 231 } 232 233 /** 234 * If a {@link ContentProvider} is registered for the given {@link Uri}, its {@link 235 * ContentProvider#insert(Uri, ContentValues)} method will be invoked. 236 * 237 * <p>Tests can verify that this method was called using {@link #getStatements()} or {@link 238 * #getInsertStatements()}. 239 * 240 * <p>If no appropriate {@link ContentProvider} is found, no action will be taken and a {@link 241 * Uri} including the incremented value set with {@link #setNextDatabaseIdForInserts(int)} will 242 * returned. 243 */ 244 @Implementation insert(Uri url, ContentValues values)245 protected Uri insert(Uri url, ContentValues values) { 246 ContentProvider provider = getProvider(url, getContext()); 247 ContentValues valuesCopy = (values == null) ? null : new ContentValues(values); 248 InsertStatement insertStatement = new InsertStatement(url, provider, valuesCopy); 249 statements.add(insertStatement); 250 insertStatements.add(insertStatement); 251 252 if (provider != null) { 253 return provider.insert(url, values); 254 } else { 255 return Uri.parse(url.toString() + "/" + ++nextDatabaseIdForInserts); 256 } 257 } 258 getContext()259 private Context getContext() { 260 return reflector(ContentResolverReflector.class, realContentResolver).getContext(); 261 } 262 263 /** 264 * If a {@link ContentProvider} is registered for the given {@link Uri}, its {@link 265 * ContentProvider#update(Uri, ContentValues, String, String[])} method will be invoked. 266 * 267 * <p>Tests can verify that this method was called using {@link #getStatements()} or {@link 268 * #getUpdateStatements()}. 269 * 270 * @return If no appropriate {@link ContentProvider} is found, no action will be taken and 1 will 271 * be returned. 272 */ 273 @Implementation update(Uri uri, ContentValues values, String where, String[] selectionArgs)274 protected int update(Uri uri, ContentValues values, String where, String[] selectionArgs) { 275 ContentProvider provider = getProvider(uri, getContext()); 276 ContentValues valuesCopy = (values == null) ? null : new ContentValues(values); 277 UpdateStatement updateStatement = 278 new UpdateStatement(uri, provider, valuesCopy, where, selectionArgs); 279 statements.add(updateStatement); 280 updateStatements.add(updateStatement); 281 282 if (provider != null) { 283 return provider.update(uri, values, where, selectionArgs); 284 } else { 285 return 1; 286 } 287 } 288 289 @Implementation(minSdk = O) query( Uri uri, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal)290 protected final Cursor query( 291 Uri uri, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal) { 292 ContentProvider provider = getProvider(uri, getContext()); 293 if (provider != null) { 294 return provider.query(uri, projection, queryArgs, cancellationSignal); 295 } else { 296 BaseCursor returnCursor = getCursor(uri); 297 if (returnCursor == null) { 298 return null; 299 } 300 String selection = queryArgs.getString(QUERY_ARG_SQL_SELECTION); 301 String[] selectionArgs = queryArgs.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS); 302 String sortOrder = queryArgs.getString(QUERY_ARG_SQL_SORT_ORDER); 303 304 returnCursor.setQuery(uri, projection, selection, selectionArgs, sortOrder); 305 return returnCursor; 306 } 307 } 308 309 @Implementation query( Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)310 protected Cursor query( 311 Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { 312 ContentProvider provider = getProvider(uri, getContext()); 313 if (provider != null) { 314 return provider.query(uri, projection, selection, selectionArgs, sortOrder); 315 } else { 316 BaseCursor returnCursor = getCursor(uri); 317 if (returnCursor == null) { 318 return null; 319 } 320 321 returnCursor.setQuery(uri, projection, selection, selectionArgs, sortOrder); 322 return returnCursor; 323 } 324 } 325 326 @Implementation query( Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal)327 protected Cursor query( 328 Uri uri, 329 String[] projection, 330 String selection, 331 String[] selectionArgs, 332 String sortOrder, 333 CancellationSignal cancellationSignal) { 334 ContentProvider provider = getProvider(uri, getContext()); 335 if (provider != null) { 336 return provider.query( 337 uri, projection, selection, selectionArgs, sortOrder, cancellationSignal); 338 } else { 339 BaseCursor returnCursor = getCursor(uri); 340 if (returnCursor == null) { 341 return null; 342 } 343 344 returnCursor.setQuery(uri, projection, selection, selectionArgs, sortOrder); 345 return returnCursor; 346 } 347 } 348 349 @Implementation getType(Uri uri)350 protected String getType(Uri uri) { 351 ContentProvider provider = getProvider(uri, getContext()); 352 if (provider != null) { 353 return provider.getType(uri); 354 } else { 355 return null; 356 } 357 } 358 359 @Implementation call(Uri uri, String method, String arg, Bundle extras)360 protected Bundle call(Uri uri, String method, String arg, Bundle extras) { 361 ContentProvider cp = getProvider(uri, getContext()); 362 if (cp != null) { 363 return cp.call(method, arg, extras); 364 } else { 365 return null; 366 } 367 } 368 369 @Implementation acquireContentProviderClient(String name)370 protected ContentProviderClient acquireContentProviderClient(String name) { 371 ContentProvider provider = getProvider(name, getContext()); 372 if (provider == null) { 373 return null; 374 } 375 return getContentProviderClient(provider, true); 376 } 377 378 @Implementation acquireContentProviderClient(Uri uri)379 protected ContentProviderClient acquireContentProviderClient(Uri uri) { 380 ContentProvider provider = getProvider(uri, getContext()); 381 if (provider == null) { 382 return null; 383 } 384 return getContentProviderClient(provider, true); 385 } 386 387 @Implementation acquireUnstableContentProviderClient(String name)388 protected ContentProviderClient acquireUnstableContentProviderClient(String name) { 389 ContentProvider provider = getProvider(name, getContext()); 390 if (provider == null) { 391 return null; 392 } 393 return getContentProviderClient(provider, false); 394 } 395 396 @Implementation acquireUnstableContentProviderClient(Uri uri)397 protected ContentProviderClient acquireUnstableContentProviderClient(Uri uri) { 398 ContentProvider provider = getProvider(uri, getContext()); 399 if (provider == null) { 400 return null; 401 } 402 return getContentProviderClient(provider, false); 403 } 404 getContentProviderClient(ContentProvider provider, boolean stable)405 private ContentProviderClient getContentProviderClient(ContentProvider provider, boolean stable) { 406 ContentProviderClient client = 407 ReflectionHelpers.callConstructor( 408 ContentProviderClient.class, 409 ClassParameter.from(ContentResolver.class, realContentResolver), 410 ClassParameter.from(IContentProvider.class, provider.getIContentProvider()), 411 ClassParameter.from(boolean.class, stable)); 412 ShadowContentProviderClient shadowContentProviderClient = Shadow.extract(client); 413 shadowContentProviderClient.setContentProvider(provider); 414 return client; 415 } 416 417 @Implementation acquireProvider(String name)418 protected IContentProvider acquireProvider(String name) { 419 return acquireUnstableProvider(name); 420 } 421 422 @Implementation acquireProvider(Uri uri)423 protected IContentProvider acquireProvider(Uri uri) { 424 return acquireUnstableProvider(uri); 425 } 426 427 @Implementation acquireUnstableProvider(String name)428 protected IContentProvider acquireUnstableProvider(String name) { 429 ContentProvider cp = getProvider(name, getContext()); 430 if (cp != null) { 431 return cp.getIContentProvider(); 432 } 433 return null; 434 } 435 436 @Implementation acquireUnstableProvider(Uri uri)437 protected final IContentProvider acquireUnstableProvider(Uri uri) { 438 ContentProvider cp = getProvider(uri, getContext()); 439 if (cp != null) { 440 return cp.getIContentProvider(); 441 } 442 return null; 443 } 444 445 /** 446 * If a {@link ContentProvider} is registered for the given {@link Uri}, its {@link 447 * ContentProvider#delete(Uri, String, String[])} method will be invoked. 448 * 449 * <p>Tests can verify that this method was called using {@link #getDeleteStatements()} or {@link 450 * #getDeletedUris()}. 451 * 452 * <p>If no appropriate {@link ContentProvider} is found, no action will be taken and {@code 1} 453 * will be returned. 454 */ 455 @Implementation delete(Uri url, String where, String[] selectionArgs)456 protected int delete(Uri url, String where, String[] selectionArgs) { 457 ContentProvider provider = getProvider(url, getContext()); 458 459 DeleteStatement deleteStatement = new DeleteStatement(url, provider, where, selectionArgs); 460 statements.add(deleteStatement); 461 deleteStatements.add(deleteStatement); 462 463 if (provider != null) { 464 return provider.delete(url, where, selectionArgs); 465 } else { 466 return 1; 467 } 468 } 469 470 /** 471 * If a {@link ContentProvider} is registered for the given {@link Uri}, its {@link 472 * ContentProvider#bulkInsert(Uri, ContentValues[])} method will be invoked. 473 * 474 * <p>Tests can verify that this method was called using {@link #getStatements()} or {@link 475 * #getInsertStatements()}. 476 * 477 * <p>If no appropriate {@link ContentProvider} is found, no action will be taken and the number 478 * of rows in {@code values} will be returned. 479 */ 480 @Implementation bulkInsert(Uri url, ContentValues[] values)481 protected int bulkInsert(Uri url, ContentValues[] values) { 482 ContentProvider provider = getProvider(url, getContext()); 483 484 InsertStatement insertStatement = new InsertStatement(url, provider, values); 485 statements.add(insertStatement); 486 insertStatements.add(insertStatement); 487 488 if (provider != null) { 489 return provider.bulkInsert(url, values); 490 } else { 491 return values.length; 492 } 493 } 494 495 @Implementation(minSdk = N) notifyChange(Uri uri, ContentObserver observer, int flags)496 protected void notifyChange(Uri uri, ContentObserver observer, int flags) { 497 notifiedUris.add(new NotifiedUri(uri, observer, flags)); 498 499 for (ContentObserverEntry entry : contentObservers) { 500 if (entry.matches(uri) && entry.observer != observer) { 501 entry.observer.dispatchChange(false, uri); 502 } 503 } 504 if (observer != null && observer.deliverSelfNotifications()) { 505 observer.dispatchChange(true, uri); 506 } 507 } 508 509 @Implementation notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork)510 protected void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) { 511 notifyChange(uri, observer, syncToNetwork ? ContentResolver.NOTIFY_SYNC_TO_NETWORK : 0); 512 } 513 514 @Implementation notifyChange(Uri uri, ContentObserver observer)515 protected void notifyChange(Uri uri, ContentObserver observer) { 516 notifyChange(uri, observer, false); 517 } 518 519 @Implementation applyBatch( String authority, ArrayList<ContentProviderOperation> operations)520 protected @NonNull ContentProviderResult[] applyBatch( 521 String authority, ArrayList<ContentProviderOperation> operations) 522 throws OperationApplicationException { 523 ContentProvider provider = getProvider(authority, getContext()); 524 if (provider != null) { 525 return provider.applyBatch(operations); 526 } else { 527 contentProviderOperations.put(authority, operations); 528 return new ContentProviderResult[0]; 529 } 530 } 531 532 @Implementation requestSync(Account account, String authority, Bundle extras)533 protected static void requestSync(Account account, String authority, Bundle extras) { 534 validateSyncExtrasBundle(extras); 535 Status status = getStatus(account, authority, true); 536 status.syncRequests++; 537 status.syncExtras = extras; 538 } 539 540 @Implementation cancelSync(Account account, String authority)541 protected static void cancelSync(Account account, String authority) { 542 Status status = getStatus(account, authority); 543 if (status != null) { 544 status.syncRequests = 0; 545 if (status.syncExtras != null) { 546 status.syncExtras.clear(); 547 } 548 // This may be too much, as the above should be sufficient. 549 if (status.syncs != null) { 550 status.syncs.clear(); 551 } 552 } 553 } 554 555 @Implementation isSyncActive(Account account, String authority)556 protected static boolean isSyncActive(Account account, String authority) { 557 ShadowContentResolver.Status status = getStatus(account, authority); 558 // TODO: this means a sync is *perpetually* active after one request 559 return status != null && status.syncRequests > 0; 560 } 561 562 @Implementation getCurrentSyncs()563 protected static List<SyncInfo> getCurrentSyncs() { 564 List<SyncInfo> list = new ArrayList<>(); 565 for (Map.Entry<String, Map<Account, Status>> map : syncableAccounts.entrySet()) { 566 if (map.getValue() == null) { 567 continue; 568 } 569 for (Map.Entry<Account, Status> mp : map.getValue().entrySet()) { 570 if (isSyncActive(mp.getKey(), map.getKey())) { 571 SyncInfo si = new SyncInfo(0, mp.getKey(), map.getKey(), 0); 572 list.add(si); 573 } 574 } 575 } 576 return list; 577 } 578 579 @Implementation setIsSyncable(Account account, String authority, int syncable)580 protected static void setIsSyncable(Account account, String authority, int syncable) { 581 getStatus(account, authority, true).state = syncable; 582 } 583 584 @Implementation getIsSyncable(Account account, String authority)585 protected static int getIsSyncable(Account account, String authority) { 586 return getStatus(account, authority, true).state; 587 } 588 589 @Implementation getSyncAutomatically(Account account, String authority)590 protected static boolean getSyncAutomatically(Account account, String authority) { 591 return getStatus(account, authority, true).syncAutomatically; 592 } 593 594 @Implementation setSyncAutomatically(Account account, String authority, boolean sync)595 protected static void setSyncAutomatically(Account account, String authority, boolean sync) { 596 getStatus(account, authority, true).syncAutomatically = sync; 597 } 598 599 @Implementation addPeriodicSync( Account account, String authority, Bundle extras, long pollFrequency)600 protected static void addPeriodicSync( 601 Account account, String authority, Bundle extras, long pollFrequency) { 602 validateSyncExtrasBundle(extras); 603 removePeriodicSync(account, authority, extras); 604 getStatus(account, authority, true) 605 .syncs 606 .add(new PeriodicSync(account, authority, extras, pollFrequency)); 607 } 608 609 @Implementation removePeriodicSync(Account account, String authority, Bundle extras)610 protected static void removePeriodicSync(Account account, String authority, Bundle extras) { 611 validateSyncExtrasBundle(extras); 612 Status status = getStatus(account, authority); 613 if (status != null) { 614 for (int i = 0; i < status.syncs.size(); ++i) { 615 if (isBundleEqual(extras, status.syncs.get(i).extras)) { 616 status.syncs.remove(i); 617 break; 618 } 619 } 620 } 621 } 622 623 @Implementation getPeriodicSyncs(Account account, String authority)624 protected static List<PeriodicSync> getPeriodicSyncs(Account account, String authority) { 625 return getStatus(account, authority, true).syncs; 626 } 627 628 @Implementation validateSyncExtrasBundle(Bundle extras)629 protected static void validateSyncExtrasBundle(Bundle extras) { 630 for (String key : extras.keySet()) { 631 Object value = extras.get(key); 632 if (value == null 633 || value instanceof Long 634 || value instanceof Integer 635 || value instanceof Boolean 636 || value instanceof Float 637 || value instanceof Double 638 || value instanceof String 639 || value instanceof Account) { 640 continue; 641 } 642 643 throw new IllegalArgumentException("unexpected value type: " + value.getClass().getName()); 644 } 645 } 646 647 @Implementation setMasterSyncAutomatically(boolean sync)648 protected static void setMasterSyncAutomatically(boolean sync) { 649 masterSyncAutomatically = sync; 650 } 651 652 @Implementation getMasterSyncAutomatically()653 protected static boolean getMasterSyncAutomatically() { 654 return masterSyncAutomatically; 655 } 656 657 @Implementation takePersistableUriPermission(@onNull Uri uri, int modeFlags)658 protected void takePersistableUriPermission(@NonNull Uri uri, int modeFlags) { 659 Objects.requireNonNull(uri, "uri may not be null"); 660 modeFlags &= (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 661 662 // If neither read nor write permission is specified there is nothing to do. 663 if (modeFlags == 0) { 664 return; 665 } 666 667 // Attempt to locate an existing record for the uri. 668 for (Iterator<UriPermission> i = uriPermissions.iterator(); i.hasNext(); ) { 669 UriPermission perm = i.next(); 670 if (uri.equals(perm.getUri())) { 671 if (perm.isReadPermission()) { 672 modeFlags |= Intent.FLAG_GRANT_READ_URI_PERMISSION; 673 } 674 if (perm.isWritePermission()) { 675 modeFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; 676 } 677 i.remove(); 678 break; 679 } 680 } 681 682 addUriPermission(uri, modeFlags); 683 } 684 685 @Implementation releasePersistableUriPermission(@onNull Uri uri, int modeFlags)686 protected void releasePersistableUriPermission(@NonNull Uri uri, int modeFlags) { 687 Objects.requireNonNull(uri, "uri may not be null"); 688 modeFlags &= (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 689 690 // If neither read nor write permission is specified there is nothing to do. 691 if (modeFlags == 0) { 692 return; 693 } 694 695 // Attempt to locate an existing record for the uri. 696 for (Iterator<UriPermission> i = uriPermissions.iterator(); i.hasNext(); ) { 697 UriPermission perm = i.next(); 698 if (uri.equals(perm.getUri())) { 699 // Reconstruct the current mode flags. 700 int oldModeFlags = 701 (perm.isReadPermission() ? Intent.FLAG_GRANT_READ_URI_PERMISSION : 0) 702 | (perm.isWritePermission() ? Intent.FLAG_GRANT_WRITE_URI_PERMISSION : 0); 703 704 // Apply the requested permission change. 705 int newModeFlags = oldModeFlags & ~modeFlags; 706 707 // Update the permission record if a change occurred. 708 if (newModeFlags != oldModeFlags) { 709 i.remove(); 710 if (newModeFlags != 0) { 711 addUriPermission(uri, newModeFlags); 712 } 713 } 714 break; 715 } 716 } 717 } 718 719 @Implementation 720 @NonNull getPersistedUriPermissions()721 protected List<UriPermission> getPersistedUriPermissions() { 722 return uriPermissions; 723 } 724 addUriPermission(@onNull Uri uri, int modeFlags)725 private void addUriPermission(@NonNull Uri uri, int modeFlags) { 726 UriPermission perm = 727 ReflectionHelpers.callConstructor( 728 UriPermission.class, 729 ClassParameter.from(Uri.class, uri), 730 ClassParameter.from(int.class, modeFlags), 731 ClassParameter.from(long.class, System.currentTimeMillis())); 732 uriPermissions.add(perm); 733 } 734 getProvider(Uri uri)735 public static ContentProvider getProvider(Uri uri) { 736 return getProvider(uri, RuntimeEnvironment.getApplication()); 737 } 738 getProvider(Uri uri, Context context)739 private static ContentProvider getProvider(Uri uri, Context context) { 740 if (uri == null || !SCHEME_CONTENT.equals(uri.getScheme())) { 741 return null; 742 } 743 return getProvider(uri.getAuthority(), context); 744 } 745 getProvider(String authority, Context context)746 private static ContentProvider getProvider(String authority, Context context) { 747 synchronized (providers) { 748 if (!providers.containsKey(authority)) { 749 ProviderInfo providerInfo = 750 context.getPackageManager().resolveContentProvider(authority, 0); 751 if (providerInfo != null) { 752 ContentProvider contentProvider = createAndInitialize(providerInfo); 753 for (String auth : Splitter.on(';').split(providerInfo.authority)) { 754 providers.put(auth, contentProvider); 755 } 756 } 757 } 758 return providers.get(authority); 759 } 760 } 761 762 /** 763 * Internal-only method, do not use! 764 * 765 * <p>Instead, use 766 * 767 * <pre> 768 * ProviderInfo info = new ProviderInfo(); 769 * info.authority = authority; 770 * Robolectric.buildContentProvider(ContentProvider.class).create(info); 771 * </pre> 772 */ registerProviderInternal(String authority, ContentProvider provider)773 public static void registerProviderInternal(String authority, ContentProvider provider) { 774 providers.put(authority, provider); 775 } 776 getStatus(Account account, String authority)777 public static Status getStatus(Account account, String authority) { 778 return getStatus(account, authority, false); 779 } 780 781 /** 782 * Retrieve information on the status of the given account. 783 * 784 * @param account the account 785 * @param authority the authority 786 * @param create whether to create if no such account is found 787 * @return the account's status 788 */ getStatus(Account account, String authority, boolean create)789 public static Status getStatus(Account account, String authority, boolean create) { 790 Map<Account, Status> map = syncableAccounts.get(authority); 791 if (map == null) { 792 map = new HashMap<>(); 793 syncableAccounts.put(authority, map); 794 } 795 Status status = map.get(account); 796 if (status == null && create) { 797 status = new Status(); 798 map.put(account, status); 799 } 800 return status; 801 } 802 803 /** 804 * @deprecated This method affects all calls, and does not work with {@link 805 * android.content.ContentResolver#acquireContentProviderClient} 806 */ 807 @Deprecated setCursor(BaseCursor cursor)808 public void setCursor(BaseCursor cursor) { 809 this.cursor = cursor; 810 } 811 812 /** 813 * @deprecated This method does not work with {@link 814 * android.content.ContentResolver#acquireContentProviderClient} 815 */ 816 @Deprecated setCursor(Uri uri, BaseCursor cursorForUri)817 public void setCursor(Uri uri, BaseCursor cursorForUri) { 818 uriCursorMap.put(uri, cursorForUri); 819 } 820 821 /** 822 * @deprecated This method affects all calls, and does not work with {@link 823 * android.content.ContentResolver#acquireContentProviderClient} 824 */ 825 @Deprecated 826 @SuppressWarnings({"unused", "WeakerAccess"}) setNextDatabaseIdForInserts(int nextId)827 public void setNextDatabaseIdForInserts(int nextId) { 828 nextDatabaseIdForInserts = nextId; 829 } 830 831 /** 832 * Returns the list of {@link InsertStatement}s, {@link UpdateStatement}s, and {@link 833 * DeleteStatement}s invoked on this {@link ContentResolver}. 834 * 835 * @return a list of statements 836 * @deprecated This method does not work with {@link 837 * android.content.ContentResolver#acquireContentProviderClient} 838 */ 839 @Deprecated 840 @SuppressWarnings({"unused", "WeakerAccess"}) getStatements()841 public List<Statement> getStatements() { 842 return statements; 843 } 844 845 /** 846 * Returns the list of {@link InsertStatement}s for corresponding calls to {@link 847 * ContentResolver#insert(Uri, ContentValues)} or {@link ContentResolver#bulkInsert(Uri, 848 * ContentValues[])}. 849 * 850 * @return a list of insert statements 851 * @deprecated This method does not work with {@link 852 * android.content.ContentResolver#acquireContentProviderClient} 853 */ 854 @Deprecated 855 @SuppressWarnings({"unused", "WeakerAccess"}) getInsertStatements()856 public List<InsertStatement> getInsertStatements() { 857 return insertStatements; 858 } 859 860 /** 861 * Returns the list of {@link UpdateStatement}s for corresponding calls to {@link 862 * ContentResolver#update(Uri, ContentValues, String, String[])}. 863 * 864 * @return a list of update statements 865 * @deprecated This method does not work with {@link 866 * android.content.ContentResolver#acquireContentProviderClient} 867 */ 868 @Deprecated 869 @SuppressWarnings({"unused", "WeakerAccess"}) getUpdateStatements()870 public List<UpdateStatement> getUpdateStatements() { 871 return updateStatements; 872 } 873 874 @Deprecated 875 @SuppressWarnings({"unused", "WeakerAccess"}) getDeletedUris()876 public List<Uri> getDeletedUris() { 877 List<Uri> uris = new ArrayList<>(); 878 for (DeleteStatement deleteStatement : deleteStatements) { 879 uris.add(deleteStatement.getUri()); 880 } 881 return uris; 882 } 883 884 /** 885 * Returns the list of {@link DeleteStatement}s for corresponding calls to {@link 886 * ContentResolver#delete(Uri, String, String[])}. 887 * 888 * @return a list of delete statements 889 */ 890 @Deprecated 891 @SuppressWarnings({"unused", "WeakerAccess"}) getDeleteStatements()892 public List<DeleteStatement> getDeleteStatements() { 893 return deleteStatements; 894 } 895 896 @Deprecated 897 @SuppressWarnings({"unused", "WeakerAccess"}) getNotifiedUris()898 public List<NotifiedUri> getNotifiedUris() { 899 return notifiedUris; 900 } 901 902 @Deprecated getContentProviderOperations(String authority)903 public List<ContentProviderOperation> getContentProviderOperations(String authority) { 904 List<ContentProviderOperation> operations = contentProviderOperations.get(authority); 905 if (operations == null) { 906 return new ArrayList<>(); 907 } 908 return operations; 909 } 910 911 private final Map<Uri, RuntimeException> registerContentProviderExceptions = new HashMap<>(); 912 913 /** Makes {@link #registerContentObserver} throw the specified exception for the specified URI. */ setRegisterContentProviderException(Uri uri, RuntimeException exception)914 public void setRegisterContentProviderException(Uri uri, RuntimeException exception) { 915 registerContentProviderExceptions.put(uri, exception); 916 } 917 918 /** 919 * Clears an exception previously set with {@link #setRegisterContentProviderException(Uri, 920 * RuntimeException)}. 921 */ clearRegisterContentProviderException(Uri uri)922 public void clearRegisterContentProviderException(Uri uri) { 923 registerContentProviderExceptions.remove(uri); 924 } 925 926 @Implementation registerContentObserver( Uri uri, boolean notifyForDescendents, ContentObserver observer)927 protected void registerContentObserver( 928 Uri uri, boolean notifyForDescendents, ContentObserver observer) { 929 if (uri == null || observer == null) { 930 throw new NullPointerException(); 931 } 932 if (registerContentProviderExceptions.containsKey(uri)) { 933 throw registerContentProviderExceptions.get(uri); 934 } 935 contentObservers.add(new ContentObserverEntry(uri, notifyForDescendents, observer)); 936 } 937 938 @Implementation registerContentObserver( Uri uri, boolean notifyForDescendents, ContentObserver observer, int userHandle)939 protected void registerContentObserver( 940 Uri uri, boolean notifyForDescendents, ContentObserver observer, int userHandle) { 941 registerContentObserver(uri, notifyForDescendents, observer); 942 } 943 944 @Implementation unregisterContentObserver(ContentObserver observer)945 protected void unregisterContentObserver(ContentObserver observer) { 946 synchronized (contentObservers) { 947 for (ContentObserverEntry entry : contentObservers) { 948 if (entry.observer == observer) { 949 contentObservers.remove(entry); 950 } 951 } 952 } 953 } 954 955 @Implementation getSyncAdapterTypes()956 protected static SyncAdapterType[] getSyncAdapterTypes() { 957 return syncAdapterTypes; 958 } 959 960 /** Sets the SyncAdapterType array which will be returned by {@link #getSyncAdapterTypes()}. */ setSyncAdapterTypes(SyncAdapterType[] syncAdapterTypes)961 public static void setSyncAdapterTypes(SyncAdapterType[] syncAdapterTypes) { 962 ShadowContentResolver.syncAdapterTypes = syncAdapterTypes; 963 } 964 965 /** 966 * Returns the content observers registered for updates under the given URI. 967 * 968 * <p>Will be empty if no observer is registered. 969 * 970 * @param uri Given URI 971 * @return The content observers, or null 972 */ getContentObservers(Uri uri)973 public Collection<ContentObserver> getContentObservers(Uri uri) { 974 ArrayList<ContentObserver> observers = new ArrayList<>(1); 975 for (ContentObserverEntry entry : contentObservers) { 976 if (entry.matches(uri)) { 977 observers.add(entry.observer); 978 } 979 } 980 return observers; 981 } 982 983 @Implementation(minSdk = Q) onDbCorruption(String tag, String message, Throwable stacktrace)984 protected static void onDbCorruption(String tag, String message, Throwable stacktrace) { 985 // No-op. 986 } 987 createAndInitialize(ProviderInfo providerInfo)988 private static ContentProvider createAndInitialize(ProviderInfo providerInfo) { 989 try { 990 ContentProvider provider = 991 (ContentProvider) Class.forName(providerInfo.name).getDeclaredConstructor().newInstance(); 992 provider.attachInfo(RuntimeEnvironment.application, providerInfo); 993 return provider; 994 } catch (InstantiationException 995 | ClassNotFoundException 996 | IllegalAccessException 997 | NoSuchMethodException 998 | InvocationTargetException e) { 999 throw new RuntimeException("Error instantiating class " + providerInfo.name, e); 1000 } 1001 } 1002 getCursor(Uri uri)1003 private BaseCursor getCursor(Uri uri) { 1004 if (uriCursorMap.get(uri) != null) { 1005 return uriCursorMap.get(uri); 1006 } else if (cursor != null) { 1007 return cursor; 1008 } else { 1009 return null; 1010 } 1011 } 1012 isBundleEqual(Bundle bundle1, Bundle bundle2)1013 private static boolean isBundleEqual(Bundle bundle1, Bundle bundle2) { 1014 if (bundle1 == null || bundle2 == null) { 1015 return false; 1016 } 1017 if (bundle1.size() != bundle2.size()) { 1018 return false; 1019 } 1020 for (String key : bundle1.keySet()) { 1021 if (!bundle1.get(key).equals(bundle2.get(key))) { 1022 return false; 1023 } 1024 } 1025 return true; 1026 } 1027 1028 /** A statement used to modify content in a {@link ContentProvider}. */ 1029 public static class Statement { 1030 private final Uri uri; 1031 private final ContentProvider contentProvider; 1032 Statement(Uri uri, ContentProvider contentProvider)1033 Statement(Uri uri, ContentProvider contentProvider) { 1034 this.uri = uri; 1035 this.contentProvider = contentProvider; 1036 } 1037 getUri()1038 public Uri getUri() { 1039 return uri; 1040 } 1041 1042 @SuppressWarnings({"unused", "WeakerAccess"}) getContentProvider()1043 public ContentProvider getContentProvider() { 1044 return contentProvider; 1045 } 1046 } 1047 1048 /** A statement used to insert content into a {@link ContentProvider}. */ 1049 public static class InsertStatement extends Statement { 1050 private final ContentValues[] bulkContentValues; 1051 InsertStatement(Uri uri, ContentProvider contentProvider, ContentValues contentValues)1052 InsertStatement(Uri uri, ContentProvider contentProvider, ContentValues contentValues) { 1053 super(uri, contentProvider); 1054 this.bulkContentValues = new ContentValues[] {contentValues}; 1055 } 1056 InsertStatement(Uri uri, ContentProvider contentProvider, ContentValues[] bulkContentValues)1057 InsertStatement(Uri uri, ContentProvider contentProvider, ContentValues[] bulkContentValues) { 1058 super(uri, contentProvider); 1059 this.bulkContentValues = bulkContentValues; 1060 } 1061 1062 @SuppressWarnings({"unused", "WeakerAccess"}) getContentValues()1063 public ContentValues getContentValues() { 1064 if (bulkContentValues.length != 1) { 1065 throw new ArrayIndexOutOfBoundsException("bulk insert, use getBulkContentValues() instead"); 1066 } 1067 return bulkContentValues[0]; 1068 } 1069 1070 @SuppressWarnings({"unused", "WeakerAccess"}) getBulkContentValues()1071 public ContentValues[] getBulkContentValues() { 1072 return bulkContentValues; 1073 } 1074 } 1075 1076 /** A statement used to update content in a {@link ContentProvider}. */ 1077 public static class UpdateStatement extends Statement { 1078 private final ContentValues values; 1079 private final String where; 1080 private final String[] selectionArgs; 1081 UpdateStatement( Uri uri, ContentProvider contentProvider, ContentValues values, String where, String[] selectionArgs)1082 UpdateStatement( 1083 Uri uri, 1084 ContentProvider contentProvider, 1085 ContentValues values, 1086 String where, 1087 String[] selectionArgs) { 1088 super(uri, contentProvider); 1089 this.values = values; 1090 this.where = where; 1091 this.selectionArgs = selectionArgs; 1092 } 1093 1094 @SuppressWarnings({"unused", "WeakerAccess"}) getContentValues()1095 public ContentValues getContentValues() { 1096 return values; 1097 } 1098 1099 @SuppressWarnings({"unused", "WeakerAccess"}) getWhere()1100 public String getWhere() { 1101 return where; 1102 } 1103 1104 @SuppressWarnings({"unused", "WeakerAccess"}) getSelectionArgs()1105 public String[] getSelectionArgs() { 1106 return selectionArgs; 1107 } 1108 } 1109 1110 /** A statement used to delete content in a {@link ContentProvider}. */ 1111 public static class DeleteStatement extends Statement { 1112 private final String where; 1113 private final String[] selectionArgs; 1114 DeleteStatement( Uri uri, ContentProvider contentProvider, String where, String[] selectionArgs)1115 DeleteStatement( 1116 Uri uri, ContentProvider contentProvider, String where, String[] selectionArgs) { 1117 super(uri, contentProvider); 1118 this.where = where; 1119 this.selectionArgs = selectionArgs; 1120 } 1121 1122 @SuppressWarnings({"unused", "WeakerAccess"}) getWhere()1123 public String getWhere() { 1124 return where; 1125 } 1126 1127 @SuppressWarnings({"unused", "WeakerAccess"}) getSelectionArgs()1128 public String[] getSelectionArgs() { 1129 return selectionArgs; 1130 } 1131 } 1132 1133 private static class UnregisteredInputStream extends InputStream implements NamedStream { 1134 private final Uri uri; 1135 UnregisteredInputStream(Uri uri)1136 UnregisteredInputStream(Uri uri) { 1137 this.uri = uri; 1138 } 1139 1140 @Override read()1141 public int read() throws IOException { 1142 throw new UnsupportedOperationException( 1143 "You must use ShadowContentResolver.registerInputStream() in order to call read()"); 1144 } 1145 1146 @Override read(byte[] b)1147 public int read(byte[] b) throws IOException { 1148 throw new UnsupportedOperationException( 1149 "You must use ShadowContentResolver.registerInputStream() in order to call read()"); 1150 } 1151 1152 @Override read(byte[] b, int off, int len)1153 public int read(byte[] b, int off, int len) throws IOException { 1154 throw new UnsupportedOperationException( 1155 "You must use ShadowContentResolver.registerInputStream() in order to call read()"); 1156 } 1157 1158 @Override toString()1159 public String toString() { 1160 return "stream for " + uri; 1161 } 1162 } 1163 1164 @ForType(ContentResolver.class) 1165 interface ContentResolverReflector { 1166 @Accessor("mContext") getContext()1167 Context getContext(); 1168 1169 @Direct openInputStream(Uri uri)1170 InputStream openInputStream(Uri uri) throws FileNotFoundException; 1171 1172 @Direct openOutputStream(Uri uri, String mode)1173 OutputStream openOutputStream(Uri uri, String mode) throws FileNotFoundException; 1174 } 1175 } 1176