Detect wedged ContentProviders, treat as ANR.

All ContentProvider calls are currently blocking, making it hard for
an app to recover when a remote provider is wedged.  This change adds
hidden support to ContentProviderClient to timeout remote calls,
treating them as ANRs.  This behavior is disabled by default.

Update DocumentsUI to use a 20 second timeout whenever interacting
with a storage provider.

Bug: 10993301, 10819461, 10852518
Change-Id: I10fa3c425c6a7225fff9cb7a0a07659028230cd3
diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml
index 71a0567..6faf7f8 100644
--- a/packages/DocumentsUI/AndroidManifest.xml
+++ b/packages/DocumentsUI/AndroidManifest.xml
@@ -2,6 +2,7 @@
         package="com.android.documentsui">
 
     <uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
+    <uses-permission android:name="android.permission.REMOVE_TASKS" />
 
     <application
         android:name=".DocumentsApplication"
diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml
index b7dcb71..92c30ba 100644
--- a/packages/DocumentsUI/res/values/strings.xml
+++ b/packages/DocumentsUI/res/values/strings.xml
@@ -64,6 +64,8 @@
     <string name="save_error">Failed to save document</string>
     <!-- Toast shown when creating a folder failed with an error [CHAR LIMIT=48] -->
     <string name="create_error">Failed to create folder</string>
+    <!-- Error message shown when querying for a list of documents failed [CHAR LIMIT=48] -->
+    <string name="query_error">Failed to query documents</string>
 
     <!-- Title of storage root location that contains recently modified or used documents [CHAR LIMIT=24] -->
     <string name="root_recent">Recent</string>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
index 48bfaf0..23a3f22 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
@@ -16,10 +16,13 @@
 
 package com.android.documentsui;
 
+import static com.android.documentsui.DocumentsActivity.TAG;
+
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.DialogFragment;
 import android.app.FragmentManager;
+import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
@@ -29,6 +32,7 @@
 import android.os.Bundle;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.EditText;
@@ -36,8 +40,6 @@
 
 import com.android.documentsui.model.DocumentInfo;
 
-import java.io.FileNotFoundException;
-
 /**
  * Dialog to create a new directory.
  */
@@ -88,12 +90,19 @@
             final ContentResolver resolver = activity.getContentResolver();
 
             final DocumentInfo cwd = activity.getCurrentDirectory();
-            final Uri childUri = DocumentsContract.createDocument(
-                    resolver, cwd.derivedUri, Document.MIME_TYPE_DIR, mDisplayName);
+
+            ContentProviderClient client = null;
             try {
+                client = DocumentsApplication.acquireUnstableProviderOrThrow(
+                        resolver, cwd.derivedUri.getAuthority());
+                final Uri childUri = DocumentsContract.createDocument(
+                        client, cwd.derivedUri, Document.MIME_TYPE_DIR, mDisplayName);
                 return DocumentInfo.fromUri(resolver, childUri);
-            } catch (FileNotFoundException e) {
+            } catch (Exception e) {
+                Log.w(TAG, "Failed to create directory", e);
                 return null;
+            } finally {
+                ContentProviderClient.releaseQuietly(client);
             }
         }
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index 1f11aed..6ff47f8 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -31,6 +31,7 @@
 import android.app.FragmentManager;
 import android.app.FragmentTransaction;
 import android.app.LoaderManager.LoaderCallbacks;
+import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
@@ -259,7 +260,7 @@
             public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
                 if (!isAdded()) return;
 
-                mAdapter.swapCursor(result.cursor);
+                mAdapter.swapResult(result.cursor, result.exception);
 
                 // Push latest state up to UI
                 // TODO: if mode change was racing with us, don't overwrite it
@@ -285,7 +286,7 @@
 
             @Override
             public void onLoaderReset(Loader<DirectoryResult> loader) {
-                mAdapter.swapCursor(null);
+                mAdapter.swapResult(null, null);
             }
         };
 
@@ -552,9 +553,16 @@
                 continue;
             }
 
-            if (!DocumentsContract.deleteDocument(resolver, doc.derivedUri)) {
+            ContentProviderClient client = null;
+            try {
+                client = DocumentsApplication.acquireUnstableProviderOrThrow(
+                        resolver, doc.derivedUri.getAuthority());
+                DocumentsContract.deleteDocument(client, doc.derivedUri);
+            } catch (Exception e) {
                 Log.w(TAG, "Failed to delete " + doc);
                 hadTrouble = true;
+            } finally {
+                ContentProviderClient.releaseQuietly(client);
             }
         }
 
@@ -646,7 +654,7 @@
 
         private List<Footer> mFooters = Lists.newArrayList();
 
-        public void swapCursor(Cursor cursor) {
+        public void swapResult(Cursor cursor, Exception e) {
             mCursor = cursor;
             mCursorCount = cursor != null ? cursor.getCount() : 0;
 
@@ -667,6 +675,11 @@
                 }
             }
 
+            if (e != null) {
+                mFooters.add(new MessageFooter(
+                        3, R.drawable.ic_dialog_alert, getString(R.string.query_error)));
+            }
+
             if (isEmpty()) {
                 mEmptyView.setVisibility(View.VISIBLE);
             } else {
@@ -971,19 +984,23 @@
         @Override
         protected Bitmap doInBackground(Uri... params) {
             final Context context = mIconThumb.getContext();
+            final ContentResolver resolver = context.getContentResolver();
 
+            ContentProviderClient client = null;
             Bitmap result = null;
             try {
-                // TODO: switch to using unstable provider
-                result = DocumentsContract.getDocumentThumbnail(
-                        context.getContentResolver(), mUri, mThumbSize, mSignal);
+                client = DocumentsApplication.acquireUnstableProviderOrThrow(
+                        resolver, mUri.getAuthority());
+                result = DocumentsContract.getDocumentThumbnail(client, mUri, mThumbSize, mSignal);
                 if (result != null) {
                     final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
                             context, mThumbSize);
                     thumbs.put(mUri, result);
                 }
             } catch (Exception e) {
-                Log.w(TAG, "Failed to load thumbnail: " + e);
+                Log.w(TAG, "Failed to load thumbnail for " + mUri + ": " + e);
+            } finally {
+                ContentProviderClient.releaseQuietly(client);
             }
             return result;
         }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
index 0b3ecf8..da0f526 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
@@ -56,7 +56,7 @@
     @Override
     public void close() {
         IoUtils.closeQuietly(cursor);
-        ContentProviderClient.closeQuietly(client);
+        ContentProviderClient.releaseQuietly(client);
         cursor = null;
         client = null;
     }
@@ -158,7 +158,9 @@
                 + result.mode + ", sortOrder=" + result.sortOrder);
 
         try {
-            result.client = resolver.acquireUnstableContentProviderClient(authority);
+            result.client = DocumentsApplication.acquireUnstableProviderOrThrow(
+                    resolver, authority);
+
             cursor = result.client.query(
                     mUri, null, null, null, getQuerySortOrder(result.sortOrder), mSignal);
             cursor.registerContentObserver(mObserver);
@@ -177,7 +179,7 @@
         } catch (Exception e) {
             Log.w(TAG, "Failed to query", e);
             result.exception = e;
-            ContentProviderClient.closeQuietly(result.client);
+            ContentProviderClient.releaseQuietly(result.client);
         } finally {
             synchronized (this) {
                 mSignal = null;
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index 4caec8f..7a45641 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -35,6 +35,7 @@
 import android.content.ActivityNotFoundException;
 import android.content.ClipData;
 import android.content.ComponentName;
+import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Intent;
@@ -878,6 +879,7 @@
                         mRoot.authority, mRoot.documentId);
                 return DocumentInfo.fromUri(getContentResolver(), uri);
             } catch (FileNotFoundException e) {
+                Log.w(TAG, "Failed to find root", e);
                 return null;
             }
         }
@@ -1035,12 +1037,26 @@
 
         @Override
         protected Uri doInBackground(Void... params) {
+            final ContentResolver resolver = getContentResolver();
             final DocumentInfo cwd = getCurrentDirectory();
-            final Uri childUri = DocumentsContract.createDocument(
-                    getContentResolver(), cwd.derivedUri, mMimeType, mDisplayName);
+
+            ContentProviderClient client = null;
+            Uri childUri = null;
+            try {
+                client = DocumentsApplication.acquireUnstableProviderOrThrow(
+                        resolver, cwd.derivedUri.getAuthority());
+                childUri = DocumentsContract.createDocument(
+                        client, cwd.derivedUri, mMimeType, mDisplayName);
+            } catch (Exception e) {
+                Log.w(TAG, "Failed to create document", e);
+            } finally {
+                ContentProviderClient.releaseQuietly(client);
+            }
+
             if (childUri != null) {
                 saveStackBlocking();
             }
+
             return childUri;
         }
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java
index 960181a..6b46e3a 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java
@@ -19,13 +19,19 @@
 import android.app.ActivityManager;
 import android.app.Application;
 import android.content.BroadcastReceiver;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.graphics.Point;
 import android.net.Uri;
+import android.os.RemoteException;
+import android.text.format.DateUtils;
 
 public class DocumentsApplication extends Application {
+    private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;
+
     private RootsCache mRoots;
     private Point mThumbnailsSize;
     private ThumbnailCache mThumbnails;
@@ -44,6 +50,17 @@
         return thumbnails;
     }
 
+    public static ContentProviderClient acquireUnstableProviderOrThrow(
+            ContentResolver resolver, String authority) throws RemoteException {
+        final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
+                authority);
+        if (client == null) {
+            throw new RemoteException("Failed to acquire provider for " + authority);
+        }
+        client.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);
+        return client;
+    }
+
     @Override
     public void onCreate() {
         final ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java b/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java
index 9a4fb7d..47dbcdf 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java
@@ -19,11 +19,12 @@
 import static com.android.documentsui.DocumentsActivity.TAG;
 import static com.android.documentsui.DocumentsActivity.State.SORT_ORDER_LAST_MODIFIED;
 
+import android.app.ActivityManager;
 import android.content.AsyncTaskLoader;
 import android.content.ContentProviderClient;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
+import android.database.MatrixCursor;
 import android.database.MergeCursor;
 import android.net.Uri;
 import android.os.Bundle;
@@ -56,9 +57,8 @@
 public class RecentLoader extends AsyncTaskLoader<DirectoryResult> {
     private static final boolean LOGD = true;
 
-    // TODO: adjust for svelte devices
-    // TODO: add support for oneway queries to avoid wedging loader
-    private static final int MAX_OUTSTANDING_RECENTS = 2;
+    private static final int MAX_OUTSTANDING_RECENTS = 4;
+    private static final int MAX_OUTSTANDING_RECENTS_SVELTE = 2;
 
     /**
      * Time to wait for first pass to complete before returning partial results.
@@ -74,20 +74,29 @@
     /** MIME types that should always be excluded from recents. */
     private static final String[] RECENT_REJECT_MIMES = new String[] { Document.MIME_TYPE_DIR };
 
-    private static final ExecutorService sExecutor = buildExecutor();
+    private static ExecutorService sExecutor;
 
     /**
      * Create a bounded thread pool for fetching recents; it creates threads as
      * needed (up to maximum) and reclaims them when finished.
      */
-    private static ExecutorService buildExecutor() {
-        // Create a bounded thread pool for fetching recents; it creates
-        // threads as needed (up to maximum) and reclaims them when finished.
-        final ThreadPoolExecutor executor = new ThreadPoolExecutor(
-                MAX_OUTSTANDING_RECENTS, MAX_OUTSTANDING_RECENTS, 10, TimeUnit.SECONDS,
-                new LinkedBlockingQueue<Runnable>());
-        executor.allowCoreThreadTimeOut(true);
-        return executor;
+    private synchronized static ExecutorService getExecutor(Context context) {
+        if (sExecutor == null) {
+            final ActivityManager am = (ActivityManager) context.getSystemService(
+                    Context.ACTIVITY_SERVICE);
+            final int maxOutstanding = am.isLowRamDevice() ? MAX_OUTSTANDING_RECENTS_SVELTE
+                    : MAX_OUTSTANDING_RECENTS;
+
+            // Create a bounded thread pool for fetching recents; it creates
+            // threads as needed (up to maximum) and reclaims them when finished.
+            final ThreadPoolExecutor executor = new ThreadPoolExecutor(
+                    maxOutstanding, maxOutstanding, 10, TimeUnit.SECONDS,
+                    new LinkedBlockingQueue<Runnable>());
+            executor.allowCoreThreadTimeOut(true);
+            sExecutor = executor;
+        }
+
+        return sExecutor;
     }
 
     private final RootsCache mRoots;
@@ -120,25 +129,26 @@
         public void run() {
             if (isCancelled()) return;
 
-            final ContentResolver resolver = getContext().getContentResolver();
-            final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
-                    authority);
+            ContentProviderClient client = null;
             try {
+                client = DocumentsApplication.acquireUnstableProviderOrThrow(
+                        getContext().getContentResolver(), authority);
+
                 final Uri uri = DocumentsContract.buildRecentDocumentsUri(authority, rootId);
                 final Cursor cursor = client.query(
                         uri, null, null, null, DirectoryLoader.getQuerySortOrder(mSortOrder));
                 mWithRoot = new RootCursorWrapper(authority, rootId, cursor, MAX_DOCS_FROM_ROOT);
-                set(mWithRoot);
-
-                mFirstPassLatch.countDown();
-                if (mFirstPassDone) {
-                    onContentChanged();
-                }
-
             } catch (Exception e) {
-                setException(e);
+                Log.w(TAG, "Failed to load " + authority + ", " + rootId, e);
             } finally {
-                ContentProviderClient.closeQuietly(client);
+                ContentProviderClient.releaseQuietly(client);
+            }
+
+            set(mWithRoot);
+
+            mFirstPassLatch.countDown();
+            if (mFirstPassDone) {
+                onContentChanged();
             }
         }
 
@@ -156,6 +166,8 @@
 
     @Override
     public DirectoryResult loadInBackground() {
+        final ExecutorService executor = getExecutor(getContext());
+
         if (mFirstPassLatch == null) {
             // First time through we kick off all the recent tasks, and wait
             // around to see if everyone finishes quickly.
@@ -170,7 +182,7 @@
 
             mFirstPassLatch = new CountDownLatch(mTasks.size());
             for (RecentTask task : mTasks.values()) {
-                sExecutor.execute(task);
+                executor.execute(task);
             }
 
             try {
@@ -184,11 +196,14 @@
         final long rejectBefore = System.currentTimeMillis() - REJECT_OLDER_THAN;
 
         // Collect all finished tasks
+        boolean allDone = true;
         List<Cursor> cursors = Lists.newArrayList();
         for (RecentTask task : mTasks.values()) {
             if (task.isDone()) {
                 try {
                     final Cursor cursor = task.get();
+                    if (cursor == null) continue;
+
                     final FilteringCursorWrapper filtered = new FilteringCursorWrapper(
                             cursor, mState.acceptMimes, RECENT_REJECT_MIMES, rejectBefore) {
                         @Override
@@ -200,14 +215,16 @@
                 } catch (InterruptedException e) {
                     throw new RuntimeException(e);
                 } catch (ExecutionException e) {
-                    Log.w(TAG, "Failed to load " + task.authority + ", " + task.rootId, e);
+                    // We already logged on other side
                 }
+            } else {
+                allDone = false;
             }
         }
 
         if (LOGD) {
             Log.d(TAG, "Found " + cursors.size() + " of " + mTasks.size() + " recent queries done");
-            Log.d(TAG, sExecutor.toString());
+            Log.d(TAG, executor.toString());
         }
 
         final DirectoryResult result = new DirectoryResult();
@@ -215,11 +232,18 @@
 
         // Hint to UI if we're still loading
         final Bundle extras = new Bundle();
-        if (cursors.size() != mTasks.size()) {
+        if (!allDone) {
             extras.putBoolean(DocumentsContract.EXTRA_LOADING, true);
         }
 
-        final MergeCursor merged = new MergeCursor(cursors.toArray(new Cursor[cursors.size()]));
+        final Cursor merged;
+        if (cursors.size() > 0) {
+            merged = new MergeCursor(cursors.toArray(new Cursor[cursors.size()]));
+        } else {
+            // Return something when nobody is ready
+            merged = new MatrixCursor(new String[0]);
+        }
+
         final SortingCursorWrapper sorted = new SortingCursorWrapper(merged, result.sortOrder) {
             @Override
             public Bundle getExtras() {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
index e3908e9..bad0a96 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
@@ -243,10 +243,11 @@
 
         final List<RootInfo> roots = Lists.newArrayList();
         final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
-        final ContentProviderClient client = resolver
-                .acquireUnstableContentProviderClient(authority);
+
+        ContentProviderClient client = null;
         Cursor cursor = null;
         try {
+            client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
             cursor = client.query(rootsUri, null, null, null, null);
             while (cursor.moveToNext()) {
                 final RootInfo root = RootInfo.fromRootsCursor(authority, cursor);
@@ -256,7 +257,7 @@
             Log.w(TAG, "Failed to load some roots from " + authority + ": " + e);
         } finally {
             IoUtils.closeQuietly(cursor);
-            ContentProviderClient.closeQuietly(client);
+            ContentProviderClient.releaseQuietly(client);
         }
         return roots;
     }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
index 5091a61..91d9124 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
@@ -23,9 +23,10 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.provider.DocumentsContract;
-import android.provider.DocumentsProvider;
 import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsProvider;
 
+import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.RootCursorWrapper;
 
 import libcore.io.IoUtils;
@@ -178,10 +179,11 @@
     }
 
     public void updateFromUri(ContentResolver resolver, Uri uri) throws FileNotFoundException {
-        final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
-                uri.getAuthority());
+        ContentProviderClient client = null;
         Cursor cursor = null;
         try {
+            client = DocumentsApplication.acquireUnstableProviderOrThrow(
+                    resolver, uri.getAuthority());
             cursor = client.query(uri, null, null, null, null);
             if (!cursor.moveToFirst()) {
                 throw new FileNotFoundException("Missing details for " + uri);
@@ -191,7 +193,7 @@
             throw asFileNotFoundException(t);
         } finally {
             IoUtils.closeQuietly(cursor);
-            ContentProviderClient.closeQuietly(client);
+            ContentProviderClient.releaseQuietly(client);
         }
     }
 
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java
index 5a15cd2..e9f2c71 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java
@@ -54,11 +54,20 @@
 public class TestDocumentsProvider extends DocumentsProvider {
     private static final String TAG = "TestDocuments";
 
-    private static final boolean LAG_ROOTS = false;
-    private static final boolean CRASH_ROOTS = false;
-    private static final boolean REFRESH_ROOTS = false;
+    private static final boolean ROOTS_WEDGE = false;
+    private static final boolean ROOTS_LAG = false;
+    private static final boolean ROOTS_CRASH = false;
+    private static final boolean ROOTS_REFRESH = false;
 
-    private static final boolean CRASH_DOCUMENT = false;
+    private static final boolean DOCUMENT_CRASH = false;
+
+    private static final boolean RECENT_WEDGE = false;
+
+    private static final boolean CHILD_WEDGE = false;
+    private static final boolean CHILD_CRASH = false;
+
+    private static final boolean THUMB_WEDGE = false;
+    private static final boolean THUMB_CRASH = false;
 
     private static final String MY_ROOT_ID = "myRoot";
     private static final String MY_DOC_ID = "myDoc";
@@ -95,10 +104,11 @@
     public Cursor queryRoots(String[] projection) throws FileNotFoundException {
         Log.d(TAG, "Someone asked for our roots!");
 
-        if (LAG_ROOTS) SystemClock.sleep(3000);
-        if (CRASH_ROOTS) System.exit(12);
+        if (ROOTS_WEDGE) SystemClock.sleep(Integer.MAX_VALUE);
+        if (ROOTS_LAG) SystemClock.sleep(3000);
+        if (ROOTS_CRASH) System.exit(12);
 
-        if (REFRESH_ROOTS) {
+        if (ROOTS_REFRESH) {
             new AsyncTask<Void, Void, Void>() {
                 @Override
                 protected Void doInBackground(Void... params) {
@@ -126,7 +136,7 @@
     @Override
     public Cursor queryDocument(String documentId, String[] projection)
             throws FileNotFoundException {
-        if (CRASH_DOCUMENT) System.exit(12);
+        if (DOCUMENT_CRASH) System.exit(12);
 
         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
         includeFile(result, documentId, 0);
@@ -198,6 +208,9 @@
             String parentDocumentId, String[] projection, String sortOrder)
             throws FileNotFoundException {
 
+        if (CHILD_WEDGE) SystemClock.sleep(Integer.MAX_VALUE);
+        if (CHILD_CRASH) System.exit(12);
+
         final ContentResolver resolver = getContext().getContentResolver();
         final Uri notifyUri = DocumentsContract.buildDocumentUri(
                 "com.example.documents", parentDocumentId);
@@ -257,6 +270,9 @@
     @Override
     public Cursor queryRecentDocuments(String rootId, String[] projection)
             throws FileNotFoundException {
+
+        if (RECENT_WEDGE) SystemClock.sleep(Integer.MAX_VALUE);
+
         // Pretend to take a super long time to respond
         SystemClock.sleep(3000);
 
@@ -275,6 +291,10 @@
     @Override
     public AssetFileDescriptor openDocumentThumbnail(
             String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
+
+        if (THUMB_WEDGE) SystemClock.sleep(Integer.MAX_VALUE);
+        if (THUMB_CRASH) System.exit(12);
+
         final Bitmap bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888);
         final Canvas canvas = new Canvas(bitmap);
         final Paint paint = new Paint();