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