• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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