| |
| package org.robolectric.shadows; |
| |
| import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION; |
| import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS; |
| import static android.content.ContentResolver.QUERY_ARG_SQL_SORT_ORDER; |
| import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; |
| import static android.os.Build.VERSION_CODES.KITKAT; |
| import static android.os.Build.VERSION_CODES.O; |
| import static android.os.Build.VERSION_CODES.Q; |
| import static org.robolectric.util.reflector.Reflector.reflector; |
| |
| import android.accounts.Account; |
| import android.annotation.NonNull; |
| import android.annotation.SuppressLint; |
| import android.content.ContentProvider; |
| import android.content.ContentProviderClient; |
| import android.content.ContentProviderOperation; |
| import android.content.ContentProviderResult; |
| import android.content.ContentResolver; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.IContentProvider; |
| import android.content.Intent; |
| import android.content.OperationApplicationException; |
| import android.content.PeriodicSync; |
| import android.content.SyncAdapterType; |
| import android.content.UriPermission; |
| import android.content.pm.ProviderInfo; |
| import android.database.ContentObserver; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.CancellationSignal; |
| import com.google.common.base.Splitter; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.lang.reflect.InvocationTargetException; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.concurrent.CopyOnWriteArrayList; |
| import java.util.function.Supplier; |
| import org.robolectric.RuntimeEnvironment; |
| import org.robolectric.annotation.Implementation; |
| import org.robolectric.annotation.Implements; |
| import org.robolectric.annotation.RealObject; |
| import org.robolectric.annotation.Resetter; |
| import org.robolectric.fakes.BaseCursor; |
| import org.robolectric.shadow.api.Shadow; |
| import org.robolectric.util.NamedStream; |
| import org.robolectric.util.ReflectionHelpers; |
| import org.robolectric.util.ReflectionHelpers.ClassParameter; |
| import org.robolectric.util.reflector.Accessor; |
| import org.robolectric.util.reflector.ForType; |
| |
| @Implements(ContentResolver.class) |
| @SuppressLint("NewApi") |
| public class ShadowContentResolver { |
| private int nextDatabaseIdForInserts; |
| |
| @RealObject ContentResolver realContentResolver; |
| |
| private BaseCursor cursor; |
| private final List<Statement> statements = new CopyOnWriteArrayList<>(); |
| private final List<InsertStatement> insertStatements = new CopyOnWriteArrayList<>(); |
| private final List<UpdateStatement> updateStatements = new CopyOnWriteArrayList<>(); |
| private final List<DeleteStatement> deleteStatements = new CopyOnWriteArrayList<>(); |
| private List<NotifiedUri> notifiedUris = new ArrayList<>(); |
| private Map<Uri, BaseCursor> uriCursorMap = new HashMap<>(); |
| private Map<Uri, Supplier<InputStream>> inputStreamMap = new HashMap<>(); |
| private Map<Uri, Supplier<OutputStream>> outputStreamMap = new HashMap<>(); |
| private final Map<String, List<ContentProviderOperation>> contentProviderOperations = |
| new HashMap<>(); |
| private ContentProviderResult[] contentProviderResults; |
| private final List<UriPermission> uriPermissions = new ArrayList<>(); |
| |
| private final CopyOnWriteArrayList<ContentObserverEntry> contentObservers = |
| new CopyOnWriteArrayList<>(); |
| |
| private static final Map<String, Map<Account, Status>> syncableAccounts = new HashMap<>(); |
| private static final Map<String, ContentProvider> providers = |
| Collections.synchronizedMap(new HashMap<>()); |
| private static boolean masterSyncAutomatically; |
| |
| private static SyncAdapterType[] syncAdapterTypes; |
| |
| @Resetter |
| public static void reset() { |
| syncableAccounts.clear(); |
| providers.clear(); |
| masterSyncAutomatically = false; |
| } |
| |
| private static class ContentObserverEntry { |
| public final Uri uri; |
| public final boolean notifyForDescendents; |
| public final ContentObserver observer; |
| |
| private ContentObserverEntry(Uri uri, boolean notifyForDescendents, ContentObserver observer) { |
| this.uri = uri; |
| this.notifyForDescendents = notifyForDescendents; |
| this.observer = observer; |
| |
| if (uri == null || observer == null) { |
| throw new NullPointerException(); |
| } |
| } |
| |
| public boolean matches(Uri test) { |
| if (!Objects.equals(uri.getScheme(), test.getScheme())) { |
| return false; |
| } |
| if (!Objects.equals(uri.getAuthority(), test.getAuthority())) { |
| return false; |
| } |
| |
| String uriPath = uri.getPath(); |
| String testPath = test.getPath(); |
| |
| return Objects.equals(uriPath, testPath) |
| || (notifyForDescendents && testPath != null && testPath.startsWith(uriPath)); |
| } |
| } |
| |
| public static class NotifiedUri { |
| public final Uri uri; |
| public final boolean syncToNetwork; |
| public final ContentObserver observer; |
| |
| public NotifiedUri(Uri uri, ContentObserver observer, boolean syncToNetwork) { |
| this.uri = uri; |
| this.syncToNetwork = syncToNetwork; |
| this.observer = observer; |
| } |
| } |
| |
| public static class Status { |
| public int syncRequests; |
| public int state = -1; |
| public boolean syncAutomatically; |
| public Bundle syncExtras; |
| public List<PeriodicSync> syncs = new ArrayList<>(); |
| } |
| |
| public void registerInputStream(Uri uri, InputStream inputStream) { |
| inputStreamMap.put(uri, () -> inputStream); |
| } |
| |
| public void registerInputStreamSupplier(Uri uri, Supplier<InputStream> supplier) { |
| inputStreamMap.put(uri, supplier); |
| } |
| |
| public void registerOutputStream(Uri uri, OutputStream outputStream) { |
| outputStreamMap.put(uri, () -> outputStream); |
| } |
| |
| public void registerOutputStreamSupplier(Uri uri, Supplier<OutputStream> supplier) { |
| outputStreamMap.put(uri, supplier); |
| } |
| |
| @Implementation |
| protected final InputStream openInputStream(final Uri uri) { |
| Supplier<InputStream> supplier = inputStreamMap.get(uri); |
| if (supplier != null) { |
| InputStream inputStream = supplier.get(); |
| if (inputStream != null) { |
| return inputStream; |
| } |
| } |
| return new UnregisteredInputStream(uri); |
| } |
| |
| @Implementation |
| protected final OutputStream openOutputStream(final Uri uri) { |
| Supplier<OutputStream> supplier = outputStreamMap.get(uri); |
| if (supplier != null) { |
| OutputStream outputStream = supplier.get(); |
| if (outputStream != null) { |
| return outputStream; |
| } |
| } |
| return new OutputStream() { |
| @Override |
| public void write(int arg0) throws IOException {} |
| |
| @Override |
| public String toString() { |
| return "outputstream for " + uri; |
| } |
| }; |
| } |
| |
| /** |
| * If a {@link ContentProvider} is registered for the given {@link Uri}, its {@link |
| * ContentProvider#insert(Uri, ContentValues)} method will be invoked. |
| * |
| * Tests can verify that this method was called using {@link #getStatements()} or {@link |
| * #getInsertStatements()}. |
| * |
| * If no appropriate {@link ContentProvider} is found, no action will be taken and a {@link |
| * Uri} including the incremented value set with {@link #setNextDatabaseIdForInserts(int)} will |
| * returned. |
| */ |
| @Implementation |
| protected final Uri insert(Uri url, ContentValues values) { |
| ContentProvider provider = getProvider(url, getContext()); |
| ContentValues valuesCopy = (values == null) ? null : new ContentValues(values); |
| InsertStatement insertStatement = new InsertStatement(url, provider, valuesCopy); |
| statements.add(insertStatement); |
| insertStatements.add(insertStatement); |
| |
| if (provider != null) { |
| return provider.insert(url, values); |
| } else { |
| return Uri.parse(url.toString() + "/" + ++nextDatabaseIdForInserts); |
| } |
| } |
| |
| private Context getContext() { |
| return reflector(ContentResolverReflector.class, realContentResolver).getContext(); |
| } |
| |
| /** |
| * If a {@link ContentProvider} is registered for the given {@link Uri}, its {@link |
| * ContentProvider#update(Uri, ContentValues, String, String[])} method will be invoked. |
| * |
| * Tests can verify that this method was called using {@link #getStatements()} or {@link |
| * #getUpdateStatements()}. |
| * |
| * @return If no appropriate {@link ContentProvider} is found, no action will be taken and 1 will |
| * be returned. |
| */ |
| @Implementation |
| protected int update(Uri uri, ContentValues values, String where, String[] selectionArgs) { |
| ContentProvider provider = getProvider(uri, getContext()); |
| ContentValues valuesCopy = (values == null) ? null : new ContentValues(values); |
| UpdateStatement updateStatement = |
| new UpdateStatement(uri, provider, valuesCopy, where, selectionArgs); |
| statements.add(updateStatement); |
| updateStatements.add(updateStatement); |
| |
| if (provider != null) { |
| return provider.update(uri, values, where, selectionArgs); |
| } else { |
| return 1; |
| } |
| } |
| |
| @Implementation(minSdk = O) |
| protected final Cursor query( |
| Uri uri, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal) { |
| ContentProvider provider = getProvider(uri, getContext()); |
| if (provider != null) { |
| return provider.query(uri, projection, queryArgs, cancellationSignal); |
| } else { |
| BaseCursor returnCursor = getCursor(uri); |
| if (returnCursor == null) { |
| return null; |
| } |
| String selection = queryArgs.getString(QUERY_ARG_SQL_SELECTION); |
| String[] selectionArgs = queryArgs.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS); |
| String sortOrder = queryArgs.getString(QUERY_ARG_SQL_SORT_ORDER); |
| |
| returnCursor.setQuery(uri, projection, selection, selectionArgs, sortOrder); |
| return returnCursor; |
| } |
| } |
| |
| @Implementation |
| protected final Cursor query( |
| Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { |
| ContentProvider provider = getProvider(uri, getContext()); |
| if (provider != null) { |
| return provider.query(uri, projection, selection, selectionArgs, sortOrder); |
| } else { |
| BaseCursor returnCursor = getCursor(uri); |
| if (returnCursor == null) { |
| return null; |
| } |
| |
| returnCursor.setQuery(uri, projection, selection, selectionArgs, sortOrder); |
| return returnCursor; |
| } |
| } |
| |
| @Implementation |
| protected Cursor query( |
| Uri uri, |
| String[] projection, |
| String selection, |
| String[] selectionArgs, |
| String sortOrder, |
| CancellationSignal cancellationSignal) { |
| ContentProvider provider = getProvider(uri, getContext()); |
| if (provider != null) { |
| return provider.query( |
| uri, projection, selection, selectionArgs, sortOrder, cancellationSignal); |
| } else { |
| BaseCursor returnCursor = getCursor(uri); |
| if (returnCursor == null) { |
| return null; |
| } |
| |
| returnCursor.setQuery(uri, projection, selection, selectionArgs, sortOrder); |
| return returnCursor; |
| } |
| } |
| |
| @Implementation |
| protected String getType(Uri uri) { |
| ContentProvider provider = getProvider(uri, getContext()); |
| if (provider != null) { |
| return provider.getType(uri); |
| } else { |
| return null; |
| } |
| } |
| |
| @Implementation |
| protected Bundle call(Uri uri, String method, String arg, Bundle extras) { |
| ContentProvider cp = getProvider(uri, getContext()); |
| if (cp != null) { |
| return cp.call(method, arg, extras); |
| } else { |
| return null; |
| } |
| } |
| |
| @Implementation |
| protected final ContentProviderClient acquireContentProviderClient(String name) { |
| ContentProvider provider = getProvider(name, getContext()); |
| if (provider == null) { |
| return null; |
| } |
| return getContentProviderClient(provider, true); |
| } |
| |
| @Implementation |
| protected final ContentProviderClient acquireContentProviderClient(Uri uri) { |
| ContentProvider provider = getProvider(uri, getContext()); |
| if (provider == null) { |
| return null; |
| } |
| return getContentProviderClient(provider, true); |
| } |
| |
| @Implementation |
| protected final ContentProviderClient acquireUnstableContentProviderClient(String name) { |
| ContentProvider provider = getProvider(name, getContext()); |
| if (provider == null) { |
| return null; |
| } |
| return getContentProviderClient(provider, false); |
| } |
| |
| @Implementation |
| protected final ContentProviderClient acquireUnstableContentProviderClient(Uri uri) { |
| ContentProvider provider = getProvider(uri, getContext()); |
| if (provider == null) { |
| return null; |
| } |
| return getContentProviderClient(provider, false); |
| } |
| |
| private ContentProviderClient getContentProviderClient(ContentProvider provider, boolean stable) { |
| ContentProviderClient client = |
| ReflectionHelpers.callConstructor( |
| ContentProviderClient.class, |
| ClassParameter.from(ContentResolver.class, realContentResolver), |
| ClassParameter.from(IContentProvider.class, provider.getIContentProvider()), |
| ClassParameter.from(boolean.class, stable)); |
| ShadowContentProviderClient shadowContentProviderClient = Shadow.extract(client); |
| shadowContentProviderClient.setContentProvider(provider); |
| return client; |
| } |
| |
| @Implementation |
| protected final IContentProvider acquireProvider(String name) { |
| return acquireUnstableProvider(name); |
| } |
| |
| @Implementation |
| protected final IContentProvider acquireProvider(Uri uri) { |
| return acquireUnstableProvider(uri); |
| } |
| |
| @Implementation |
| protected final IContentProvider acquireUnstableProvider(String name) { |
| ContentProvider cp = getProvider(name, getContext()); |
| if (cp != null) { |
| return cp.getIContentProvider(); |
| } |
| return null; |
| } |
| |
| @Implementation |
| protected final IContentProvider acquireUnstableProvider(Uri uri) { |
| ContentProvider cp = getProvider(uri, getContext()); |
| if (cp != null) { |
| return cp.getIContentProvider(); |
| } |
| return null; |
| } |
| |
| /** |
| * If a {@link ContentProvider} is registered for the given {@link Uri}, its {@link |
| * ContentProvider#delete(Uri, String, String[])} method will be invoked. |
| * |
| * Tests can verify that this method was called using {@link #getDeleteStatements()} or {@link |
| * #getDeletedUris()}. |
| * |
| * If no appropriate {@link ContentProvider} is found, no action will be taken and {@code 1} |
| * will be returned. |
| */ |
| @Implementation |
| protected final int delete(Uri url, String where, String[] selectionArgs) { |
| ContentProvider provider = getProvider(url, getContext()); |
| |
| DeleteStatement deleteStatement = new DeleteStatement(url, provider, where, selectionArgs); |
| statements.add(deleteStatement); |
| deleteStatements.add(deleteStatement); |
| |
| if (provider != null) { |
| return provider.delete(url, where, selectionArgs); |
| } else { |
| return 1; |
| } |
| } |
| |
| /** |
| * If a {@link ContentProvider} is registered for the given {@link Uri}, its {@link |
| * ContentProvider#bulkInsert(Uri, ContentValues[])} method will be invoked. |
| * |
| * Tests can verify that this method was called using {@link #getStatements()} or {@link |
| * #getInsertStatements()}. |
| * |
| * If no appropriate {@link ContentProvider} is found, no action will be taken and the number |
| * of rows in {@code values} will be returned. |
| */ |
| @Implementation |
| protected final int bulkInsert(Uri url, ContentValues[] values) { |
| ContentProvider provider = getProvider(url, getContext()); |
| |
| InsertStatement insertStatement = new InsertStatement(url, provider, values); |
| statements.add(insertStatement); |
| insertStatements.add(insertStatement); |
| |
| if (provider != null) { |
| return provider.bulkInsert(url, values); |
| } else { |
| return values.length; |
| } |
| } |
| |
| @Implementation |
| protected void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) { |
| notifiedUris.add(new NotifiedUri(uri, observer, syncToNetwork)); |
| |
| for (ContentObserverEntry entry : contentObservers) { |
| if (entry.matches(uri) && entry.observer != observer) { |
| entry.observer.dispatchChange(false, uri); |
| } |
| } |
| if (observer != null && observer.deliverSelfNotifications()) { |
| observer.dispatchChange(true, uri); |
| } |
| } |
| |
| @Implementation |
| protected void notifyChange(Uri uri, ContentObserver observer) { |
| notifyChange(uri, observer, false); |
| } |
| |
| @Implementation |
| protected ContentProviderResult[] applyBatch( |
| String authority, ArrayList<ContentProviderOperation> operations) |
| throws OperationApplicationException { |
| ContentProvider provider = getProvider(authority, getContext()); |
| if (provider != null) { |
| return provider.applyBatch(operations); |
| } else { |
| contentProviderOperations.put(authority, operations); |
| return contentProviderResults; |
| } |
| } |
| |
| @Implementation |
| protected static void requestSync(Account account, String authority, Bundle extras) { |
| validateSyncExtrasBundle(extras); |
| Status status = getStatus(account, authority, true); |
| status.syncRequests++; |
| status.syncExtras = extras; |
| } |
| |
| @Implementation |
| protected static void cancelSync(Account account, String authority) { |
| Status status = getStatus(account, authority); |
| if (status != null) { |
| status.syncRequests = 0; |
| if (status.syncExtras != null) { |
| status.syncExtras.clear(); |
| } |
| // This may be too much, as the above should be sufficient. |
| if (status.syncs != null) { |
| status.syncs.clear(); |
| } |
| } |
| } |
| |
| @Implementation |
| protected static boolean isSyncActive(Account account, String authority) { |
| ShadowContentResolver.Status status = getStatus(account, authority); |
| // TODO: this means a sync is *perpetually* active after one request |
| return status != null && status.syncRequests > 0; |
| } |
| |
| @Implementation |
| protected static void setIsSyncable(Account account, String authority, int syncable) { |
| getStatus(account, authority, true).state = syncable; |
| } |
| |
| @Implementation |
| protected static int getIsSyncable(Account account, String authority) { |
| return getStatus(account, authority, true).state; |
| } |
| |
| @Implementation |
| protected static boolean getSyncAutomatically(Account account, String authority) { |
| return getStatus(account, authority, true).syncAutomatically; |
| } |
| |
| @Implementation |
| protected static void setSyncAutomatically(Account account, String authority, boolean sync) { |
| getStatus(account, authority, true).syncAutomatically = sync; |
| } |
| |
| @Implementation |
| protected static void addPeriodicSync( |
| Account account, String authority, Bundle extras, long pollFrequency) { |
| validateSyncExtrasBundle(extras); |
| removePeriodicSync(account, authority, extras); |
| getStatus(account, authority, true) |
| .syncs |
| .add(new PeriodicSync(account, authority, extras, pollFrequency)); |
| } |
| |
| @Implementation |
| protected static void removePeriodicSync(Account account, String authority, Bundle extras) { |
| validateSyncExtrasBundle(extras); |
| Status status = getStatus(account, authority); |
| if (status != null) { |
| for (int i = 0; i < status.syncs.size(); ++i) { |
| if (isBundleEqual(extras, status.syncs.get(i).extras)) { |
| status.syncs.remove(i); |
| break; |
| } |
| } |
| } |
| } |
| |
| @Implementation |
| protected static List<PeriodicSync> getPeriodicSyncs(Account account, String authority) { |
| return getStatus(account, authority, true).syncs; |
| } |
| |
| @Implementation |
| protected static void validateSyncExtrasBundle(Bundle extras) { |
| for (String key : extras.keySet()) { |
| Object value = extras.get(key); |
| if (value == null |
| || value instanceof Long |
| || value instanceof Integer |
| || value instanceof Boolean |
| || value instanceof Float |
| || value instanceof Double |
| || value instanceof String |
| || value instanceof Account) { |
| continue; |
| } |
| |
| throw new IllegalArgumentException("unexpected value type: " + value.getClass().getName()); |
| } |
| } |
| |
| @Implementation |
| protected static void setMasterSyncAutomatically(boolean sync) { |
| masterSyncAutomatically = sync; |
| } |
| |
| @Implementation |
| protected static boolean getMasterSyncAutomatically() { |
| return masterSyncAutomatically; |
| } |
| |
| @Implementation(minSdk = KITKAT) |
| protected void takePersistableUriPermission(@NonNull Uri uri, int modeFlags) { |
| Objects.requireNonNull(uri, "uri may not be null"); |
| modeFlags &= (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); |
| |
| // If neither read nor write permission is specified there is nothing to do. |
| if (modeFlags == 0) { |
| return; |
| } |
| |
| // Attempt to locate an existing record for the uri. |
| for (Iterator<UriPermission> i = uriPermissions.iterator(); i.hasNext(); ) { |
| UriPermission perm = i.next(); |
| if (uri.equals(perm.getUri())) { |
| if (perm.isReadPermission()) { |
| modeFlags |= Intent.FLAG_GRANT_READ_URI_PERMISSION; |
| } |
| if (perm.isWritePermission()) { |
| modeFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; |
| } |
| i.remove(); |
| break; |
| } |
| } |
| |
| addUriPermission(uri, modeFlags); |
| } |
| |
| @Implementation(minSdk = KITKAT) |
| protected void releasePersistableUriPermission(@NonNull Uri uri, int modeFlags) { |
| Objects.requireNonNull(uri, "uri may not be null"); |
| modeFlags &= (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); |
| |
| // If neither read nor write permission is specified there is nothing to do. |
| if (modeFlags == 0) { |
| return; |
| } |
| |
| // Attempt to locate an existing record for the uri. |
| for (Iterator<UriPermission> i = uriPermissions.iterator(); i.hasNext(); ) { |
| UriPermission perm = i.next(); |
| if (uri.equals(perm.getUri())) { |
| // Reconstruct the current mode flags. |
| int oldModeFlags = |
| (perm.isReadPermission() ? Intent.FLAG_GRANT_READ_URI_PERMISSION : 0) |
| | (perm.isWritePermission() ? Intent.FLAG_GRANT_WRITE_URI_PERMISSION : 0); |
| |
| // Apply the requested permission change. |
| int newModeFlags = oldModeFlags & ~modeFlags; |
| |
| // Update the permission record if a change occurred. |
| if (newModeFlags != oldModeFlags) { |
| i.remove(); |
| if (newModeFlags != 0) { |
| addUriPermission(uri, newModeFlags); |
| } |
| } |
| break; |
| } |
| } |
| } |
| |
| @Implementation(minSdk = KITKAT) |
| @NonNull |
| protected List<UriPermission> getPersistedUriPermissions() { |
| return uriPermissions; |
| } |
| |
| private void addUriPermission(@NonNull Uri uri, int modeFlags) { |
| UriPermission perm = |
| ReflectionHelpers.callConstructor( |
| UriPermission.class, |
| ClassParameter.from(Uri.class, uri), |
| ClassParameter.from(int.class, modeFlags), |
| ClassParameter.from(long.class, System.currentTimeMillis())); |
| uriPermissions.add(perm); |
| } |
| |
| public static ContentProvider getProvider(Uri uri) { |
| return getProvider(uri, RuntimeEnvironment.getApplication()); |
| } |
| |
| private static ContentProvider getProvider(Uri uri, Context context) { |
| if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { |
| return null; |
| } |
| return getProvider(uri.getAuthority(), context); |
| } |
| |
| private static ContentProvider getProvider(String authority, Context context) { |
| synchronized (providers) { |
| if (!providers.containsKey(authority)) { |
| ProviderInfo providerInfo = |
| context.getPackageManager().resolveContentProvider(authority, 0); |
| if (providerInfo != null) { |
| ContentProvider contentProvider = createAndInitialize(providerInfo); |
| for (String auth : Splitter.on(';').split(providerInfo.authority)) { |
| providers.put(auth, contentProvider); |
| } |
| } |
| } |
| return providers.get(authority); |
| } |
| } |
| |
| /** |
| * Internal-only method, do not use! |
| * |
| * <p>Instead, use |
| * |
| * <pre> |
| * ProviderInfo info = new ProviderInfo(); |
| * info.authority = authority; |
| * Robolectric.buildContentProvider(ContentProvider.class).create(info); |
| * </pre> |
| */ |
| public static void registerProviderInternal(String authority, ContentProvider provider) { |
| providers.put(authority, provider); |
| } |
| |
| public static Status getStatus(Account account, String authority) { |
| return getStatus(account, authority, false); |
| } |
| |
| /** |
| * Retrieve information on the status of the given account. |
| * |
| * @param account the account |
| * @param authority the authority |
| * @param create whether to create if no such account is found |
| * @return the account's status |
| */ |
| public static Status getStatus(Account account, String authority, boolean create) { |
| Map<Account, Status> map = syncableAccounts.get(authority); |
| if (map == null) { |
| map = new HashMap<>(); |
| syncableAccounts.put(authority, map); |
| } |
| Status status = map.get(account); |
| if (status == null && create) { |
| status = new Status(); |
| map.put(account, status); |
| } |
| return status; |
| } |
| |
| /** |
| * @deprecated This method affects all calls, and does not work with {@link |
| * android.content.ContentResolver#acquireContentProviderClient} |
| */ |
| @Deprecated |
| public void setCursor(BaseCursor cursor) { |
| this.cursor = cursor; |
| } |
| |
| /** |
| * @deprecated This method does not work with {@link |
| * android.content.ContentResolver#acquireContentProviderClient} |
| */ |
| @Deprecated |
| public void setCursor(Uri uri, BaseCursor cursorForUri) { |
| this.uriCursorMap.put(uri, cursorForUri); |
| } |
| |
| /** |
| * @deprecated This method affects all calls, and does not work with {@link |
| * android.content.ContentResolver#acquireContentProviderClient} |
| */ |
| @Deprecated |
| @SuppressWarnings({"unused", "WeakerAccess"}) |
| public void setNextDatabaseIdForInserts(int nextId) { |
| nextDatabaseIdForInserts = nextId; |
| } |
| |
| /** |
| * Returns the list of {@link InsertStatement}s, {@link UpdateStatement}s, and {@link |
| * DeleteStatement}s invoked on this {@link ContentResolver}. |
| * |
| * @return a list of statements |
| * @deprecated This method does not work with {@link |
| * android.content.ContentResolver#acquireContentProviderClient} |
| */ |
| @Deprecated |
| @SuppressWarnings({"unused", "WeakerAccess"}) |
| public List<Statement> getStatements() { |
| return statements; |
| } |
| |
| /** |
| * Returns the list of {@link InsertStatement}s for corresponding calls to {@link |
| * ContentResolver#insert(Uri, ContentValues)} or {@link ContentResolver#bulkInsert(Uri, |
| * ContentValues[])}. |
| * |
| * @return a list of insert statements |
| * @deprecated This method does not work with {@link |
| * android.content.ContentResolver#acquireContentProviderClient} |
| */ |
| @Deprecated |
| @SuppressWarnings({"unused", "WeakerAccess"}) |
| public List<InsertStatement> getInsertStatements() { |
| return insertStatements; |
| } |
| |
| /** |
| * Returns the list of {@link UpdateStatement}s for corresponding calls to {@link |
| * ContentResolver#update(Uri, ContentValues, String, String[])}. |
| * |
| * @return a list of update statements |
| * @deprecated This method does not work with {@link |
| * android.content.ContentResolver#acquireContentProviderClient} |
| */ |
| @Deprecated |
| @SuppressWarnings({"unused", "WeakerAccess"}) |
| public List<UpdateStatement> getUpdateStatements() { |
| return updateStatements; |
| } |
| |
| @Deprecated |
| @SuppressWarnings({"unused", "WeakerAccess"}) |
| public List<Uri> getDeletedUris() { |
| List<Uri> uris = new ArrayList<>(); |
| for (DeleteStatement deleteStatement : deleteStatements) { |
| uris.add(deleteStatement.getUri()); |
| } |
| return uris; |
| } |
| |
| /** |
| * Returns the list of {@link DeleteStatement}s for corresponding calls to {@link |
| * ContentResolver#delete(Uri, String, String[])}. |
| * |
| * @return a list of delete statements |
| */ |
| @Deprecated |
| @SuppressWarnings({"unused", "WeakerAccess"}) |
| public List<DeleteStatement> getDeleteStatements() { |
| return deleteStatements; |
| } |
| |
| @Deprecated |
| @SuppressWarnings({"unused", "WeakerAccess"}) |
| public List<NotifiedUri> getNotifiedUris() { |
| return notifiedUris; |
| } |
| |
| @Deprecated |
| public List<ContentProviderOperation> getContentProviderOperations(String authority) { |
| List<ContentProviderOperation> operations = contentProviderOperations.get(authority); |
| if (operations == null) { |
| return new ArrayList<>(); |
| } |
| return operations; |
| } |
| |
| @Deprecated |
| public void setContentProviderResult(ContentProviderResult[] contentProviderResults) { |
| this.contentProviderResults = contentProviderResults; |
| } |
| |
| private final Map<Uri, RuntimeException> registerContentProviderExceptions = new HashMap<>(); |
| |
| /** Makes {@link #registerContentObserver} throw the specified exception for the specified URI. */ |
| public void setRegisterContentProviderException(Uri uri, RuntimeException exception) { |
| registerContentProviderExceptions.put(uri, exception); |
| } |
| |
| /** |
| * Clears an exception previously set with {@link #setRegisterContentProviderException(Uri, |
| * RuntimeException)}. |
| */ |
| public void clearRegisterContentProviderException(Uri uri) { |
| registerContentProviderExceptions.remove(uri); |
| } |
| |
| @Implementation |
| protected void registerContentObserver( |
| Uri uri, boolean notifyForDescendents, ContentObserver observer) { |
| if (uri == null || observer == null) { |
| throw new NullPointerException(); |
| } |
| if (registerContentProviderExceptions.containsKey(uri)) { |
| throw registerContentProviderExceptions.get(uri); |
| } |
| contentObservers.add(new ContentObserverEntry(uri, notifyForDescendents, observer)); |
| } |
| |
| @Implementation(minSdk = JELLY_BEAN_MR1) |
| protected void registerContentObserver( |
| Uri uri, boolean notifyForDescendents, ContentObserver observer, int userHandle) { |
| registerContentObserver(uri, notifyForDescendents, observer); |
| } |
| |
| @Implementation |
| protected void unregisterContentObserver(ContentObserver observer) { |
| synchronized (contentObservers) { |
| for (ContentObserverEntry entry : contentObservers) { |
| if (entry.observer == observer) { |
| contentObservers.remove(entry); |
| } |
| } |
| } |
| } |
| |
| @Implementation |
| protected static SyncAdapterType[] getSyncAdapterTypes() { |
| return syncAdapterTypes; |
| } |
| |
| /** Sets the SyncAdapterType array which will be returned by {@link #getSyncAdapterTypes()}. */ |
| public static void setSyncAdapterTypes(SyncAdapterType[] syncAdapterTypes) { |
| ShadowContentResolver.syncAdapterTypes = syncAdapterTypes; |
| } |
| |
| /** |
| * Returns the content observers registered for updates under the given URI. |
| * |
| * Will be empty if no observer is registered. |
| * |
| * @param uri Given URI |
| * @return The content observers, or null |
| */ |
| public Collection<ContentObserver> getContentObservers(Uri uri) { |
| ArrayList<ContentObserver> observers = new ArrayList<>(1); |
| for (ContentObserverEntry entry : contentObservers) { |
| if (entry.matches(uri)) { |
| observers.add(entry.observer); |
| } |
| } |
| return observers; |
| } |
| |
| @Implementation(minSdk = Q) |
| protected static void onDbCorruption(String tag, String message, Throwable stacktrace) { |
| // No-op. |
| } |
| |
| private static ContentProvider createAndInitialize(ProviderInfo providerInfo) { |
| try { |
| ContentProvider provider = |
| (ContentProvider) Class.forName(providerInfo.name).getDeclaredConstructor().newInstance(); |
| provider.attachInfo(RuntimeEnvironment.application, providerInfo); |
| return provider; |
| } catch (InstantiationException |
| | ClassNotFoundException |
| | IllegalAccessException |
| | NoSuchMethodException |
| | InvocationTargetException e) { |
| throw new RuntimeException("Error instantiating class " + providerInfo.name, e); |
| } |
| } |
| |
| private BaseCursor getCursor(Uri uri) { |
| if (uriCursorMap.get(uri) != null) { |
| return uriCursorMap.get(uri); |
| } else if (cursor != null) { |
| return cursor; |
| } else { |
| return null; |
| } |
| } |
| |
| private static boolean isBundleEqual(Bundle bundle1, Bundle bundle2) { |
| if (bundle1 == null || bundle2 == null) { |
| return false; |
| } |
| if (bundle1.size() != bundle2.size()) { |
| return false; |
| } |
| for (String key : bundle1.keySet()) { |
| if (!bundle1.get(key).equals(bundle2.get(key))) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** A statement used to modify content in a {@link ContentProvider}. */ |
| public static class Statement { |
| private final Uri uri; |
| private final ContentProvider contentProvider; |
| |
| Statement(Uri uri, ContentProvider contentProvider) { |
| this.uri = uri; |
| this.contentProvider = contentProvider; |
| } |
| |
| public Uri getUri() { |
| return uri; |
| } |
| |
| @SuppressWarnings({"unused", "WeakerAccess"}) |
| public ContentProvider getContentProvider() { |
| return contentProvider; |
| } |
| } |
| |
| /** A statement used to insert content into a {@link ContentProvider}. */ |
| public static class InsertStatement extends Statement { |
| private final ContentValues[] bulkContentValues; |
| |
| InsertStatement(Uri uri, ContentProvider contentProvider, ContentValues contentValues) { |
| super(uri, contentProvider); |
| this.bulkContentValues = new ContentValues[] {contentValues}; |
| } |
| |
| InsertStatement(Uri uri, ContentProvider contentProvider, ContentValues[] bulkContentValues) { |
| super(uri, contentProvider); |
| this.bulkContentValues = bulkContentValues; |
| } |
| |
| @SuppressWarnings({"unused", "WeakerAccess"}) |
| public ContentValues getContentValues() { |
| if (bulkContentValues.length != 1) { |
| throw new ArrayIndexOutOfBoundsException("bulk insert, use getBulkContentValues() instead"); |
| } |
| return bulkContentValues[0]; |
| } |
| |
| @SuppressWarnings({"unused", "WeakerAccess"}) |
| public ContentValues[] getBulkContentValues() { |
| return bulkContentValues; |
| } |
| } |
| |
| /** A statement used to update content in a {@link ContentProvider}. */ |
| public static class UpdateStatement extends Statement { |
| private final ContentValues values; |
| private final String where; |
| private final String[] selectionArgs; |
| |
| UpdateStatement( |
| Uri uri, |
| ContentProvider contentProvider, |
| ContentValues values, |
| String where, |
| String[] selectionArgs) { |
| super(uri, contentProvider); |
| this.values = values; |
| this.where = where; |
| this.selectionArgs = selectionArgs; |
| } |
| |
| @SuppressWarnings({"unused", "WeakerAccess"}) |
| public ContentValues getContentValues() { |
| return values; |
| } |
| |
| @SuppressWarnings({"unused", "WeakerAccess"}) |
| public String getWhere() { |
| return where; |
| } |
| |
| @SuppressWarnings({"unused", "WeakerAccess"}) |
| public String[] getSelectionArgs() { |
| return selectionArgs; |
| } |
| } |
| |
| /** A statement used to delete content in a {@link ContentProvider}. */ |
| public static class DeleteStatement extends Statement { |
| private final String where; |
| private final String[] selectionArgs; |
| |
| DeleteStatement( |
| Uri uri, ContentProvider contentProvider, String where, String[] selectionArgs) { |
| super(uri, contentProvider); |
| this.where = where; |
| this.selectionArgs = selectionArgs; |
| } |
| |
| @SuppressWarnings({"unused", "WeakerAccess"}) |
| public String getWhere() { |
| return where; |
| } |
| |
| @SuppressWarnings({"unused", "WeakerAccess"}) |
| public String[] getSelectionArgs() { |
| return selectionArgs; |
| } |
| } |
| |
| private static class UnregisteredInputStream extends InputStream implements NamedStream { |
| private final Uri uri; |
| |
| UnregisteredInputStream(Uri uri) { |
| this.uri = uri; |
| } |
| |
| @Override |
| public int read() throws IOException { |
| throw new UnsupportedOperationException( |
| "You must use ShadowContentResolver.registerInputStream() in order to call read()"); |
| } |
| |
| @Override |
| public int read(byte[] b) throws IOException { |
| throw new UnsupportedOperationException( |
| "You must use ShadowContentResolver.registerInputStream() in order to call read()"); |
| } |
| |
| @Override |
| public int read(byte[] b, int off, int len) throws IOException { |
| throw new UnsupportedOperationException( |
| "You must use ShadowContentResolver.registerInputStream() in order to call read()"); |
| } |
| |
| @Override |
| public String toString() { |
| return "stream for " + uri; |
| } |
| } |
| |
| @ForType(ContentResolver.class) |
| interface ContentResolverReflector { |
| @Accessor("mContext") |
| Context getContext(); |
| } |
| } |