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