am ef4c3790: am 27d3c0fe: Merge "Isolate calls to each remote DocumentsProvider." into klp-dev

* commit 'ef4c3790e52beab467359f6b5125b66fbe1993ef':
  Isolate calls to each remote DocumentsProvider.
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..2105cb4
--- /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);