Isolate calls to each remote DocumentsProvider.

All background work is going through AsyncTasks, which uses a shared
thread pool.  Even with the new ContentProviderClient logic to detect
ANRs, the UI can still appear to be unresponsive for 20 seconds, even
if the user attempted to switch to a different backend.  In the worst
case, a backlog of thumbnail requests would end up wedging Loaders
for a long time, since they all share the same THREAD_POOL_EXECUTOR.

This change isolates calls to each provider onto their own thread,
which they're free to wedge and recover from over time.

It also means we no longer need a dedicated thread pool for recents
loading, and can use a simpler Semaphore instead.

Disables thumbnails in recents on svelte devices.

Bug: 10993301, 11014856
Change-Id: I7f8a5bbb5f64437e006cb2c48b7e854136d5c38c
diff --git a/core/java/android/content/AsyncTaskLoader.java b/core/java/android/content/AsyncTaskLoader.java
index 612c67f..eb7426e 100644
--- a/core/java/android/content/AsyncTaskLoader.java
+++ b/core/java/android/content/AsyncTaskLoader.java
@@ -26,6 +26,7 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
 
 /**
  * Abstract Loader that provides an {@link AsyncTask} to do the work.  See
@@ -123,6 +124,8 @@
         }
     }
 
+    private final Executor mExecutor;
+
     volatile LoadTask mTask;
     volatile LoadTask mCancellingTask;
 
@@ -131,7 +134,13 @@
     Handler mHandler;
 
     public AsyncTaskLoader(Context context) {
+        this(context, AsyncTask.THREAD_POOL_EXECUTOR);
+    }
+
+    /** {@hide} */
+    public AsyncTaskLoader(Context context, Executor executor) {
         super(context);
+        mExecutor = executor;
     }
 
     /**
@@ -223,7 +232,7 @@
                 }
             }
             if (DEBUG) Slog.v(TAG, "Executing: " + mTask);
-            mTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
+            mTask.executeOnExecutor(mExecutor, (Void[]) null);
         }
     }
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
index 23a3f22..22dd6e4 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
@@ -69,7 +69,12 @@
             @Override
             public void onClick(DialogInterface dialog, int which) {
                 final String displayName = text1.getText().toString();
-                new CreateDirectoryTask(displayName).execute();
+
+                final DocumentsActivity activity = (DocumentsActivity) getActivity();
+                final DocumentInfo cwd = activity.getCurrentDirectory();
+
+                new CreateDirectoryTask(displayName).executeOnExecutor(
+                        ProviderExecutor.forAuthority(cwd.authority));
             }
         });
         builder.setNegativeButton(android.R.string.cancel, null);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index 6ff47f8..59caad0 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -27,6 +27,7 @@
 import static com.android.documentsui.model.DocumentInfo.getCursorLong;
 import static com.android.documentsui.model.DocumentInfo.getCursorString;
 
+import android.app.ActivityManager;
 import android.app.Fragment;
 import android.app.FragmentManager;
 import android.app.FragmentTransaction;
@@ -113,6 +114,7 @@
 
     private boolean mHideGridTitles = false;
 
+    private boolean mSvelteRecents;
     private Point mThumbSize;
 
     private DocumentsAdapter mAdapter;
@@ -204,6 +206,19 @@
     }
 
     @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+
+        // Cancel any outstanding thumbnail requests
+        final ViewGroup target = (mListView.getAdapter() != null) ? mListView : mGridView;
+        final int count = target.getChildCount();
+        for (int i = 0; i < count; i++) {
+            final View view = target.getChildAt(i);
+            mRecycleListener.onMovedToScrapHeap(view);
+        }
+    }
+
+    @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
 
@@ -225,6 +240,10 @@
             mHideGridTitles = (doc != null) && doc.isGridTitlesHidden();
         }
 
+        final ActivityManager am = (ActivityManager) context.getSystemService(
+                Context.ACTIVITY_SERVICE);
+        mSvelteRecents = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
+
         mCallbacks = new LoaderCallbacks<DirectoryResult>() {
             @Override
             public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
@@ -260,7 +279,7 @@
             public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
                 if (!isAdded()) return;
 
-                mAdapter.swapResult(result.cursor, result.exception);
+                mAdapter.swapResult(result);
 
                 // Push latest state up to UI
                 // TODO: if mode change was racing with us, don't overwrite it
@@ -286,7 +305,7 @@
 
             @Override
             public void onLoaderReset(Loader<DirectoryResult> loader) {
-                mAdapter.swapResult(null, null);
+                mAdapter.swapResult(null);
             }
         };
 
@@ -654,13 +673,13 @@
 
         private List<Footer> mFooters = Lists.newArrayList();
 
-        public void swapResult(Cursor cursor, Exception e) {
-            mCursor = cursor;
-            mCursorCount = cursor != null ? cursor.getCount() : 0;
+        public void swapResult(DirectoryResult result) {
+            mCursor = result != null ? result.cursor : null;
+            mCursorCount = mCursor != null ? mCursor.getCount() : 0;
 
             mFooters.clear();
 
-            final Bundle extras = cursor != null ? cursor.getExtras() : null;
+            final Bundle extras = mCursor != null ? mCursor.getExtras() : null;
             if (extras != null) {
                 final String info = extras.getString(DocumentsContract.EXTRA_INFO);
                 if (info != null) {
@@ -675,7 +694,7 @@
                 }
             }
 
-            if (e != null) {
+            if (result != null && result.exception != null) {
                 mFooters.add(new MessageFooter(
                         3, R.drawable.ic_dialog_alert, getString(R.string.query_error)));
             }
@@ -776,7 +795,7 @@
             final boolean supportsThumbnail = (docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
             final boolean allowThumbnail = (state.derivedMode == MODE_GRID)
                     || MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, docMimeType);
-            final boolean showThumbnail = supportsThumbnail && allowThumbnail;
+            final boolean showThumbnail = supportsThumbnail && allowThumbnail && !mSvelteRecents;
 
             boolean cacheHit = false;
             if (showThumbnail) {
@@ -790,7 +809,7 @@
                     final ThumbnailAsyncTask task = new ThumbnailAsyncTask(
                             uri, iconMime, iconThumb, mThumbSize);
                     iconThumb.setTag(task);
-                    task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+                    task.executeOnExecutor(ProviderExecutor.forAuthority(docAuthority));
                 }
             }
 
@@ -983,6 +1002,8 @@
 
         @Override
         protected Bitmap doInBackground(Uri... params) {
+            if (isCancelled()) return null;
+
             final Context context = mIconThumb.getContext();
             final ContentResolver resolver = context.getContentResolver();
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
index da0f526..163615d 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryLoader.java
@@ -79,7 +79,7 @@
 
     public DirectoryLoader(Context context, int type, RootInfo root, DocumentInfo doc, Uri uri,
             int userSortOrder) {
-        super(context);
+        super(context, ProviderExecutor.forAuthority(root.authority));
         mType = type;
         mRoot = root;
         mDoc = doc;
@@ -157,11 +157,11 @@
         Log.d(TAG, "userMode=" + userMode + ", userSortOrder=" + mUserSortOrder + " --> mode="
                 + result.mode + ", sortOrder=" + result.sortOrder);
 
+        ContentProviderClient client = null;
         try {
-            result.client = DocumentsApplication.acquireUnstableProviderOrThrow(
-                    resolver, authority);
+            client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
 
-            cursor = result.client.query(
+            cursor = client.query(
                     mUri, null, null, null, getQuerySortOrder(result.sortOrder), mSignal);
             cursor.registerContentObserver(mObserver);
 
@@ -175,11 +175,12 @@
                 cursor = new SortingCursorWrapper(cursor, result.sortOrder);
             }
 
+            result.client = client;
             result.cursor = cursor;
         } catch (Exception e) {
             Log.w(TAG, "Failed to query", e);
             result.exception = e;
-            ContentProviderClient.releaseQuietly(result.client);
+            ContentProviderClient.releaseQuietly(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 7a45641..7660779 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -91,6 +91,7 @@
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
+import java.util.concurrent.Executor;
 
 public class DocumentsActivity extends Activity {
     public static final String TAG = "Documents";
@@ -215,7 +216,7 @@
         if (!mState.restored) {
             if (mState.action == ACTION_MANAGE) {
                 final Uri rootUri = getIntent().getData();
-                new RestoreRootTask(rootUri).execute();
+                new RestoreRootTask(rootUri).executeOnExecutor(getCurrentExecutor());
             } else {
                 new RestoreStackTask().execute();
             }
@@ -782,6 +783,15 @@
         return mState.stack.peek();
     }
 
+    public Executor getCurrentExecutor() {
+        final DocumentInfo cwd = getCurrentDirectory();
+        if (cwd != null && cwd.authority != null) {
+            return ProviderExecutor.forAuthority(cwd.authority);
+        } else {
+            return AsyncTask.THREAD_POOL_EXECUTOR;
+        }
+    }
+
     public State getDisplayState() {
         return mState;
     }
@@ -855,7 +865,7 @@
         mState.stackTouched = true;
 
         if (!mRoots.isRecentsRoot(root)) {
-            new PickRootTask(root).execute();
+            new PickRootTask(root).executeOnExecutor(getCurrentExecutor());
         } else {
             onCurrentDirectoryChanged(ANIM_SIDE);
         }
@@ -932,7 +942,7 @@
             onCurrentDirectoryChanged(ANIM_DOWN);
         } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
             // Explicit file picked, return
-            new ExistingFinishTask(doc.derivedUri).execute();
+            new ExistingFinishTask(doc.derivedUri).executeOnExecutor(getCurrentExecutor());
         } else if (mState.action == ACTION_CREATE) {
             // Replace selected file
             SaveFragment.get(fm).setReplaceTarget(doc);
@@ -966,16 +976,16 @@
             for (int i = 0; i < size; i++) {
                 uris[i] = docs.get(i).derivedUri;
             }
-            new ExistingFinishTask(uris).execute();
+            new ExistingFinishTask(uris).executeOnExecutor(getCurrentExecutor());
         }
     }
 
     public void onSaveRequested(DocumentInfo replaceTarget) {
-        new ExistingFinishTask(replaceTarget.derivedUri).execute();
+        new ExistingFinishTask(replaceTarget.derivedUri).executeOnExecutor(getCurrentExecutor());
     }
 
     public void onSaveRequested(String mimeType, String displayName) {
-        new CreateFinishTask(mimeType, displayName).execute();
+        new CreateFinishTask(mimeType, displayName).executeOnExecutor(getCurrentExecutor());
     }
 
     private void saveStackBlocking() {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/ProviderExecutor.java b/packages/DocumentsUI/src/com/android/documentsui/ProviderExecutor.java
new file mode 100644
index 0000000..2105cb41
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/ProviderExecutor.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+import com.google.android.collect.Maps;
+
+import java.util.HashMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+
+public class ProviderExecutor extends Thread implements Executor {
+
+    @GuardedBy("sExecutors")
+    private static HashMap<String, ProviderExecutor> sExecutors = Maps.newHashMap();
+
+    public static Executor forAuthority(String authority) {
+        synchronized (sExecutors) {
+            ProviderExecutor executor = sExecutors.get(authority);
+            if (executor == null) {
+                executor = new ProviderExecutor();
+                executor.setName("ProviderExecutor: " + authority);
+                executor.start();
+                sExecutors.put(authority, executor);
+            }
+            return executor;
+        }
+    }
+
+    private final LinkedBlockingQueue<Runnable> mQueue = new LinkedBlockingQueue<Runnable>();
+
+    @Override
+    public void execute(Runnable command) {
+        Preconditions.checkNotNull(command);
+        mQueue.add(command);
+    }
+
+    @Override
+    public void run() {
+        while (true) {
+            try {
+                final Runnable command = mQueue.take();
+                command.run();
+            } catch (InterruptedException e) {
+                // That was weird; let's go look for more tasks.
+            }
+        }
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java b/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java
index 47dbcdf..3a8a3fb 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java
@@ -49,9 +49,7 @@
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 
 public class RecentLoader extends AsyncTaskLoader<DirectoryResult> {
@@ -74,30 +72,7 @@
     /** MIME types that should always be excluded from recents. */
     private static final String[] RECENT_REJECT_MIMES = new String[] { Document.MIME_TYPE_DIR };
 
-    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 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 Semaphore mQueryPermits;
 
     private final RootsCache mRoots;
     private final State mState;
@@ -129,6 +104,20 @@
         public void run() {
             if (isCancelled()) return;
 
+            try {
+                mQueryPermits.acquire();
+            } catch (InterruptedException e) {
+                return;
+            }
+
+            try {
+                runInternal();
+            } finally {
+                mQueryPermits.release();
+            }
+        }
+
+        public void runInternal() {
             ContentProviderClient client = null;
             try {
                 client = DocumentsApplication.acquireUnstableProviderOrThrow(
@@ -138,6 +127,7 @@
                 final Cursor cursor = client.query(
                         uri, null, null, null, DirectoryLoader.getQuerySortOrder(mSortOrder));
                 mWithRoot = new RootCursorWrapper(authority, rootId, cursor, MAX_DOCS_FROM_ROOT);
+
             } catch (Exception e) {
                 Log.w(TAG, "Failed to load " + authority + ", " + rootId, e);
             } finally {
@@ -162,12 +152,17 @@
         super(context);
         mRoots = roots;
         mState = state;
+
+        // Keep clients around on high-RAM devices, since we'd be spinning them
+        // up moments later to fetch thumbnails anyway.
+        final ActivityManager am = (ActivityManager) getContext().getSystemService(
+                Context.ACTIVITY_SERVICE);
+        mQueryPermits = new Semaphore(
+                am.isLowRamDevice() ? MAX_OUTSTANDING_RECENTS_SVELTE : MAX_OUTSTANDING_RECENTS);
     }
 
     @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.
@@ -182,7 +177,7 @@
 
             mFirstPassLatch = new CountDownLatch(mTasks.size());
             for (RecentTask task : mTasks.values()) {
-                executor.execute(task);
+                ProviderExecutor.forAuthority(task.authority).execute(task);
             }
 
             try {
@@ -224,7 +219,6 @@
 
         if (LOGD) {
             Log.d(TAG, "Found " + cursors.size() + " of " + mTasks.size() + " recent queries done");
-            Log.d(TAG, executor.toString());
         }
 
         final DirectoryResult result = new DirectoryResult();
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java
index e9f2c71..0caddcc 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java
@@ -66,6 +66,7 @@
     private static final boolean CHILD_WEDGE = false;
     private static final boolean CHILD_CRASH = false;
 
+    private static final boolean THUMB_HUNDREDS = false;
     private static final boolean THUMB_WEDGE = false;
     private static final boolean THUMB_CRASH = false;
 
@@ -225,6 +226,12 @@
         includeFile(result, "localfile3", 0);
         includeFile(result, "localfile4", 0);
 
+        if (THUMB_HUNDREDS) {
+            for (int i = 0; i < 256; i++) {
+                includeFile(result, "i maded u an picshure", Document.FLAG_SUPPORTS_THUMBNAIL);
+            }
+        }
+
         synchronized (this) {
             // Try picking up an existing network fetch
             CloudTask task = mTask != null ? mTask.get() : null;
@@ -292,7 +299,7 @@
     public AssetFileDescriptor openDocumentThumbnail(
             String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
 
-        if (THUMB_WEDGE) SystemClock.sleep(Integer.MAX_VALUE);
+        if (THUMB_WEDGE) wedgeUntilCanceled(signal);
         if (THUMB_CRASH) System.exit(12);
 
         final Bitmap bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888);
@@ -332,6 +339,18 @@
         return true;
     }
 
+    private static void wedgeUntilCanceled(CancellationSignal signal) {
+        if (signal != null) {
+            while (true) {
+                signal.throwIfCanceled();
+                SystemClock.sleep(500);
+            }
+        } else {
+            Log.w(TAG, "WEDGING WITHOUT A CANCELLATIONSIGNAL");
+            SystemClock.sleep(Integer.MAX_VALUE);
+        }
+    }
+
     private static void includeFile(MatrixCursor result, String docId, int flags) {
         final RowBuilder row = result.newRow();
         row.add(Document.COLUMN_DOCUMENT_ID, docId);