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