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