Load RootInfo in background, invalidation.

Move all RootInfo queries to background threads to avoid janking
the UI.  Update passes happen on spawed task, which swaps out updated
cache results when finished.  Support partial updates when only a
single package/authority has changed.  Watch for change notifications
for roots, since flags can change over time.

Ignore stopped packages when in background, but query them for roots
when launching any picker UI.

Optimize management launches by treating as one-shot requests that
don't need to wait for all RootInfo.

Bug: 10600454, 10745490
Change-Id: Ibc7b15688ef6b41bd7e9dd0d7564b501e60e49a9
diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml
index 1ef7bff..19a29f2 100644
--- a/packages/DocumentsUI/AndroidManifest.xml
+++ b/packages/DocumentsUI/AndroidManifest.xml
@@ -48,14 +48,6 @@
             android:authorities="com.android.documentsui.recents"
             android:exported="false" />
 
-        <receiver android:name=".DocumentChangedReceiver">
-            <intent-filter>
-                <action android:name="android.provider.action.DOCUMENT_CHANGED" />
-                <data android:mimeType="vnd.android.cursor.dir/root" />
-                <data android:mimeType="vnd.android.cursor.item/root" />
-            </intent-filter>
-        </receiver>
-
         <!-- TODO: remove when we have real clients -->
         <activity android:name=".TestActivity" android:enabled="false">
             <intent-filter>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index 4c2c99c..911e9ed 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -249,8 +249,7 @@
                                 context, mType, root, doc, contentsUri, state.userSortOrder);
                     case TYPE_RECENT_OPEN:
                         final RootsCache roots = DocumentsApplication.getRootsCache(context);
-                        final List<RootInfo> matchingRoots = roots.getMatchingRoots(state);
-                        return new RecentLoader(context, matchingRoots, state.acceptMimes);
+                        return new RecentLoader(context, roots, state);
                     default:
                         throw new IllegalStateException("Unknown type " + mType);
                 }
@@ -797,7 +796,9 @@
 
             Drawable iconDrawable = null;
             if (mType == TYPE_RECENT_OPEN) {
-                final RootInfo root = roots.getRoot(docAuthority, docRootId);
+                // We've already had to enumerate roots before any results can
+                // be shown, so this will never block.
+                final RootInfo root = roots.getRootBlocking(docAuthority, docRootId);
                 iconDrawable = root.loadIcon(context);
 
                 if (summary != null) {
@@ -808,7 +809,7 @@
                         summary.setVisibility(View.VISIBLE);
                         hasLine2 = true;
                     } else {
-                        if (iconDrawable != null && roots.isIconUnique(root)) {
+                        if (iconDrawable != null && roots.isIconUniqueBlocking(root)) {
                             // No summary needed if icon speaks for itself
                             summary.setVisibility(View.INVISIBLE);
                         } else {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java
deleted file mode 100644
index 54f62ef..0000000
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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 static com.android.documentsui.DocumentsActivity.TAG;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-/**
- * Handles changes which invalidate cached data.
- */
-public class DocumentChangedReceiver extends BroadcastReceiver {
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        Log.d(TAG, "Regenerating roots cache");
-        DocumentsApplication.getRootsCache(context).update();
-        // TODO: invalidate cached data in recents provider
-    }
-}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index 3b71f60..457bb19 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -46,6 +46,7 @@
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.InsetDrawable;
 import android.net.Uri;
+import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -87,6 +88,7 @@
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.Collection;
 import java.util.List;
 
 public class DocumentsActivity extends Activity {
@@ -207,7 +209,16 @@
             RootsFragment.show(getFragmentManager(), null);
         }
 
-        onCurrentDirectoryChanged(ANIM_NONE);
+        if (!mState.restored) {
+            if (mState.action == ACTION_MANAGE) {
+                final Uri rootUri = getIntent().getData();
+                new RestoreRootTask(rootUri).execute();
+            } else {
+                new RestoreStackTask().execute();
+            }
+        } else {
+            onCurrentDirectoryChanged(ANIM_NONE);
+        }
     }
 
     private void buildDefaultState() {
@@ -241,22 +252,41 @@
 
         mState.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
         mState.showAdvanced = SettingsActivity.getDisplayAdvancedDevices(this);
+    }
 
-        if (mState.action == ACTION_MANAGE) {
-            final Uri uri = intent.getData();
-            final String rootId = DocumentsContract.getRootId(uri);
-            final RootInfo root = mRoots.getRoot(uri.getAuthority(), rootId);
+    private class RestoreRootTask extends AsyncTask<Void, Void, RootInfo> {
+        private Uri mRootUri;
+
+        public RestoreRootTask(Uri rootUri) {
+            mRootUri = rootUri;
+        }
+
+        @Override
+        protected RootInfo doInBackground(Void... params) {
+            final String rootId = DocumentsContract.getRootId(mRootUri);
+            return mRoots.getRootOneshot(mRootUri.getAuthority(), rootId);
+        }
+
+        @Override
+        protected void onPostExecute(RootInfo root) {
+            if (isDestroyed()) return;
+            mState.restored = true;
+
             if (root != null) {
                 onRootPicked(root, true);
             } else {
-                Log.w(TAG, "Failed to find root: " + uri);
+                Log.w(TAG, "Failed to find root: " + mRootUri);
                 finish();
             }
+        }
+    }
 
-        } else {
+    private class RestoreStackTask extends AsyncTask<Void, Void, Void> {
+        private volatile boolean mRestoredStack;
+
+        @Override
+        protected Void doInBackground(Void... params) {
             // Restore last stack for calling package
-            // TODO: move into async loader
-            boolean restoredStack = false;
             final String packageName = getCallingPackage();
             final Cursor cursor = getContentResolver()
                     .query(RecentsProvider.buildResume(packageName), null, null, null, null);
@@ -265,7 +295,7 @@
                     final byte[] rawStack = cursor.getBlob(
                             cursor.getColumnIndex(ResumeColumns.STACK));
                     DurableUtils.readFromArray(rawStack, mState.stack);
-                    restoredStack = true;
+                    mRestoredStack = true;
                 }
             } catch (IOException e) {
                 Log.w(TAG, "Failed to resume", e);
@@ -275,18 +305,28 @@
 
             // If restored root isn't valid, fall back to recents
             final RootInfo root = getCurrentRoot();
-            final List<RootInfo> matchingRoots = mRoots.getMatchingRoots(mState);
+            final Collection<RootInfo> matchingRoots = mRoots.getMatchingRootsBlocking(mState);
             if (!matchingRoots.contains(root)) {
                 mState.stack.reset();
-                restoredStack = false;
+                mRestoredStack = false;
             }
 
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void result) {
+            if (isDestroyed()) return;
+            mState.restored = true;
+
             // Only open drawer when not restoring stack, and when not showing
             // visual content.
-            if (!restoredStack
+            if (!mRestoredStack
                     && !MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mState.acceptMimes)) {
                 setRootsDrawerOpen(true);
             }
+
+            onCurrentDirectoryChanged(ANIM_NONE);
         }
     }
 
@@ -935,6 +975,7 @@
         public boolean localOnly = false;
         public boolean showAdvanced = false;
         public boolean stackTouched = false;
+        public boolean restored = false;
 
         /** Current user navigation stack; empty implies recents. */
         public DocumentStack stack = new DocumentStack();
@@ -974,6 +1015,7 @@
             out.writeInt(localOnly ? 1 : 0);
             out.writeInt(showAdvanced ? 1 : 0);
             out.writeInt(stackTouched ? 1 : 0);
+            out.writeInt(restored ? 1 : 0);
             DurableUtils.writeToParcel(out, stack);
             out.writeString(currentSearch);
             out.writeMap(dirState);
@@ -992,6 +1034,7 @@
                 state.localOnly = in.readInt() != 0;
                 state.showAdvanced = in.readInt() != 0;
                 state.stackTouched = in.readInt() != 0;
+                state.restored = in.readInt() != 0;
                 DurableUtils.readFromParcel(in, state.stack);
                 state.currentSearch = in.readString();
                 in.readMap(state.dirState, null);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java
index 180ddef..960181a 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java
@@ -23,6 +23,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.graphics.Point;
+import android.net.Uri;
 
 public class DocumentsApplication extends Application {
     private RootsCache mRoots;
@@ -49,6 +50,8 @@
         final int memoryClassBytes = am.getMemoryClass() * 1024 * 1024;
 
         mRoots = new RootsCache(this);
+        mRoots.updateAsync();
+
         mThumbnails = new ThumbnailCache(memoryClassBytes / 4);
 
         final IntentFilter packageFilter = new IntentFilter();
@@ -77,8 +80,13 @@
     private BroadcastReceiver mCacheReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
-            // TODO: narrow changed/removed to only packages that have backends
-            mRoots.update();
+            final Uri data = intent.getData();
+            if (data != null) {
+                final String packageName = data.getSchemeSpecificPart();
+                mRoots.updatePackageAsync(packageName);
+            } else {
+                mRoots.updateAsync();
+            }
         }
     };
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java b/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java
index 1912010..3659c6e 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java
@@ -41,6 +41,7 @@
 
 import java.io.Closeable;
 import java.io.IOException;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
@@ -80,8 +81,8 @@
         return executor;
     }
 
-    private final List<RootInfo> mRoots;
-    private final String[] mAcceptMimes;
+    private final RootsCache mRoots;
+    private final State mState;
 
     private final HashMap<RootInfo, RecentTask> mTasks = Maps.newHashMap();
 
@@ -138,10 +139,10 @@
         }
     }
 
-    public RecentLoader(Context context, List<RootInfo> roots, String[] acceptMimes) {
+    public RecentLoader(Context context, RootsCache roots, State state) {
         super(context);
         mRoots = roots;
-        mAcceptMimes = acceptMimes;
+        mState = state;
     }
 
     @Override
@@ -150,7 +151,8 @@
             // First time through we kick off all the recent tasks, and wait
             // around to see if everyone finishes quickly.
 
-            for (RootInfo root : mRoots) {
+            final Collection<RootInfo> roots = mRoots.getMatchingRootsBlocking(mState);
+            for (RootInfo root : roots) {
                 if ((root.flags & Root.FLAG_SUPPORTS_RECENTS) != 0) {
                     final RecentTask task = new RecentTask(root.authority, root.rootId);
                     mTasks.put(root, task);
@@ -177,7 +179,7 @@
                 try {
                     final Cursor cursor = task.get();
                     final FilteringCursorWrapper filtered = new FilteringCursorWrapper(
-                            cursor, mAcceptMimes, new String[] { Document.MIME_TYPE_DIR }) {
+                            cursor, mState.acceptMimes, new String[] { Document.MIME_TYPE_DIR }) {
                         @Override
                         public void close() {
                             // Ignored, since we manage cursor lifecycle internally
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
index b533925..5076370 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java
@@ -176,7 +176,6 @@
         @Override
         public View getView(int position, View convertView, ViewGroup parent) {
             final Context context = parent.getContext();
-            final RootsCache roots = DocumentsApplication.getRootsCache(context);
 
             if (convertView == null) {
                 final LayoutInflater inflater = LayoutInflater.from(context);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
index 9b54948..52d6cc8 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
@@ -21,10 +21,15 @@
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ProviderInfo;
+import android.database.ContentObserver;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.SystemClock;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Root;
 import android.util.Log;
@@ -32,114 +37,267 @@
 import com.android.documentsui.DocumentsActivity.State;
 import com.android.documentsui.model.RootInfo;
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Objects;
 import com.google.android.collect.Lists;
+import com.google.android.collect.Sets;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
 
 import libcore.io.IoUtils;
 
-import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Cache of known storage backends and their roots.
  */
 public class RootsCache {
+    private static final boolean LOGD = true;
 
     // TODO: cache roots in local provider to avoid spinning up backends
     // TODO: root updates should trigger UI refresh
 
-    private static final boolean RECENTS_ENABLED = true;
-
     private final Context mContext;
+    private final ContentObserver mObserver;
 
-    public List<RootInfo> mRoots = Lists.newArrayList();
+    private final RootInfo mRecentsRoot = new RootInfo();
 
-    private RootInfo mRecentsRoot;
+    private final Object mLock = new Object();
+    private final CountDownLatch mFirstLoad = new CountDownLatch(1);
+
+    @GuardedBy("mLock")
+    private Multimap<String, RootInfo> mRoots = ArrayListMultimap.create();
+    @GuardedBy("mLock")
+    private HashSet<String> mStoppedAuthorities = Sets.newHashSet();
+
+    @GuardedBy("mObservedAuthorities")
+    private final HashSet<String> mObservedAuthorities = Sets.newHashSet();
 
     public RootsCache(Context context) {
         mContext = context;
-        update();
+        mObserver = new RootsChangedObserver();
+    }
+
+    private class RootsChangedObserver extends ContentObserver {
+        public RootsChangedObserver() {
+            super(new Handler());
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            if (LOGD) Log.d(TAG, "Updating roots due to change at " + uri);
+            updateAuthorityAsync(uri.getAuthority());
+        }
     }
 
     /**
      * Gather roots from all known storage providers.
      */
-    @GuardedBy("ActivityThread")
-    public void update() {
-        mRoots.clear();
+    public void updateAsync() {
+        // Special root for recents
+        mRecentsRoot.rootType = Root.ROOT_TYPE_SHORTCUT;
+        mRecentsRoot.icon = R.drawable.ic_root_recent;
+        mRecentsRoot.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE;
+        mRecentsRoot.title = mContext.getString(R.string.root_recent);
+        mRecentsRoot.availableBytes = -1;
 
-        if (RECENTS_ENABLED) {
-            // Create special root for recents
-            final RootInfo root = new RootInfo();
-            root.rootType = Root.ROOT_TYPE_SHORTCUT;
-            root.icon = R.drawable.ic_root_recent;
-            root.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE;
-            root.title = mContext.getString(R.string.root_recent);
-            root.availableBytes = -1;
+        new UpdateTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+    }
 
-            mRoots.add(root);
-            mRecentsRoot = root;
+    /**
+     * Gather roots from storage providers belonging to given package name.
+     */
+    public void updatePackageAsync(String packageName) {
+        // Need at least first load, since we're going to be using previously
+        // cached values for non-matching packages.
+        waitForFirstLoad();
+        new UpdateTask(packageName).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+    }
+
+    /**
+     * Gather roots from storage providers belonging to given authority.
+     */
+    public void updateAuthorityAsync(String authority) {
+        final ProviderInfo info = mContext.getPackageManager().resolveContentProvider(authority, 0);
+        if (info != null) {
+            updatePackageAsync(info.packageName);
+        }
+    }
+
+    private void waitForFirstLoad() {
+        boolean success = false;
+        try {
+            success = mFirstLoad.await(15, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+        }
+        if (!success) {
+            Log.w(TAG, "Timeout waiting for first update");
+        }
+    }
+
+    /**
+     * Load roots from authorities that are in stopped state. Normal
+     * {@link UpdateTask} passes ignore stopped applications.
+     */
+    private void loadStoppedAuthorities() {
+        final ContentResolver resolver = mContext.getContentResolver();
+        synchronized (mLock) {
+            for (String authority : mStoppedAuthorities) {
+                if (LOGD) Log.d(TAG, "Loading stopped authority " + authority);
+                mRoots.putAll(authority, loadRootsForAuthority(resolver, authority));
+            }
+            mStoppedAuthorities.clear();
+        }
+    }
+
+    private class UpdateTask extends AsyncTask<Void, Void, Void> {
+        private final String mFilterPackage;
+
+        /**
+         * Update all roots.
+         */
+        public UpdateTask() {
+            this(null);
         }
 
-        // Query for other storage backends
-        final ContentResolver resolver = mContext.getContentResolver();
-        final PackageManager pm = mContext.getPackageManager();
-        final List<ProviderInfo> providers = pm.queryContentProviders(
-                null, -1, PackageManager.GET_META_DATA);
-        for (ProviderInfo info : providers) {
-            if (info.metaData != null && info.metaData.containsKey(
-                    DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
+        /**
+         * Only update roots belonging to given package name. Other roots will
+         * be copied from cached {@link #mRoots} values.
+         */
+        public UpdateTask(String filterPackage) {
+            mFilterPackage = filterPackage;
+        }
 
-                // TODO: populate roots on background thread, and cache results
-                final Uri rootsUri = DocumentsContract.buildRootsUri(info.authority);
-                final ContentProviderClient client = resolver
-                        .acquireUnstableContentProviderClient(info.authority);
-                Cursor cursor = null;
-                try {
-                    cursor = client.query(rootsUri, null, null, null, null);
-                    while (cursor.moveToNext()) {
-                        final RootInfo root = RootInfo.fromRootsCursor(info.authority, cursor);
-                        mRoots.add(root);
+        @Override
+        protected Void doInBackground(Void... params) {
+            final long start = SystemClock.elapsedRealtime();
+
+            final Multimap<String, RootInfo> roots = ArrayListMultimap.create();
+            final HashSet<String> stoppedAuthorities = Sets.newHashSet();
+
+            final ContentResolver resolver = mContext.getContentResolver();
+            final PackageManager pm = mContext.getPackageManager();
+            final List<ProviderInfo> providers = pm.queryContentProviders(
+                    null, -1, PackageManager.GET_META_DATA);
+            for (ProviderInfo info : providers) {
+                if (info.metaData != null && info.metaData.containsKey(
+                        DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
+                    // Ignore stopped packages for now; we might query them
+                    // later during UI interaction.
+                    if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) {
+                        if (LOGD) Log.d(TAG, "Ignoring stopped authority " + info.authority);
+                        stoppedAuthorities.add(info.authority);
+                        continue;
                     }
-                } catch (Exception e) {
-                    Log.w(TAG, "Failed to load some roots from " + info.authority + ": " + e);
-                } finally {
-                    IoUtils.closeQuietly(cursor);
-                    ContentProviderClient.closeQuietly(client);
+
+                    // Try using cached roots if filtering
+                    boolean cacheHit = false;
+                    if (mFilterPackage != null && !mFilterPackage.equals(info.packageName)) {
+                        synchronized (mLock) {
+                            if (roots.putAll(info.authority, mRoots.get(info.authority))) {
+                                if (LOGD) Log.d(TAG, "Used cached roots for " + info.authority);
+                                cacheHit = true;
+                            }
+                        }
+                    }
+
+                    // Cache miss, or loading everything
+                    if (!cacheHit) {
+                        roots.putAll(
+                                info.authority, loadRootsForAuthority(resolver, info.authority));
+                    }
                 }
             }
-        }
 
-        Log.d(TAG, "Update found " + mRoots.size() + " roots");
+            final long delta = SystemClock.elapsedRealtime() - start;
+            Log.d(TAG, "Update found " + roots.size() + " roots in " + delta + "ms");
+            synchronized (mLock) {
+                mStoppedAuthorities = stoppedAuthorities;
+                mRoots = roots;
+            }
+            mFirstLoad.countDown();
+            return null;
+        }
     }
 
-    @Deprecated
-    public RootInfo findRoot(Uri uri) {
-        final String authority = uri.getAuthority();
-        final String docId = DocumentsContract.getDocumentId(uri);
-        for (RootInfo root : mRoots) {
-            if (Objects.equal(root.authority, authority) && Objects.equal(root.documentId, docId)) {
+    /**
+     * Bring up requested provider and query for all active roots.
+     */
+    private Collection<RootInfo> loadRootsForAuthority(ContentResolver resolver, String authority) {
+        if (LOGD) Log.d(TAG, "Loading roots for " + authority);
+
+        synchronized (mObservedAuthorities) {
+            if (mObservedAuthorities.add(authority)) {
+                // Watch for any future updates
+                final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
+                mContext.getContentResolver().registerContentObserver(rootsUri, true, mObserver);
+            }
+        }
+
+        final List<RootInfo> roots = Lists.newArrayList();
+        final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
+        final ContentProviderClient client = resolver
+                .acquireUnstableContentProviderClient(authority);
+        Cursor cursor = null;
+        try {
+            cursor = client.query(rootsUri, null, null, null, null);
+            while (cursor.moveToNext()) {
+                final RootInfo root = RootInfo.fromRootsCursor(authority, cursor);
+                roots.add(root);
+            }
+        } catch (Exception e) {
+            Log.w(TAG, "Failed to load some roots from " + authority + ": " + e);
+        } finally {
+            IoUtils.closeQuietly(cursor);
+            ContentProviderClient.closeQuietly(client);
+        }
+        return roots;
+    }
+
+    /**
+     * Return the requested {@link RootInfo}, but only loading the roots for the
+     * requested authority. This is useful when we want to load fast without
+     * waiting for all the other roots to come back.
+     */
+    public RootInfo getRootOneshot(String authority, String rootId) {
+        synchronized (mLock) {
+            RootInfo root = getRootLocked(authority, rootId);
+            if (root == null) {
+                mRoots.putAll(
+                        authority, loadRootsForAuthority(mContext.getContentResolver(), authority));
+                root = getRootLocked(authority, rootId);
+            }
+            return root;
+        }
+    }
+
+    public RootInfo getRootBlocking(String authority, String rootId) {
+        waitForFirstLoad();
+        loadStoppedAuthorities();
+        synchronized (mLock) {
+            return getRootLocked(authority, rootId);
+        }
+    }
+
+    private RootInfo getRootLocked(String authority, String rootId) {
+        for (RootInfo root : mRoots.get(authority)) {
+            if (Objects.equal(root.rootId, rootId)) {
                 return root;
             }
         }
         return null;
     }
 
-    @GuardedBy("ActivityThread")
-    public RootInfo getRoot(String authority, String rootId) {
-        for (RootInfo root : mRoots) {
-            if (Objects.equal(root.authority, authority) && Objects.equal(root.rootId, rootId)) {
-                return root;
-            }
-        }
-        return null;
-    }
-
-    @GuardedBy("ActivityThread")
-    public boolean isIconUnique(RootInfo root) {
-        final int rootIcon = root.derivedIcon != 0 ? root.derivedIcon : root.icon;
-        for (RootInfo test : mRoots) {
-            if (Objects.equal(test.authority, root.authority)) {
+    public boolean isIconUniqueBlocking(RootInfo root) {
+        waitForFirstLoad();
+        loadStoppedAuthorities();
+        synchronized (mLock) {
+            final int rootIcon = root.derivedIcon != 0 ? root.derivedIcon : root.icon;
+            for (RootInfo test : mRoots.get(root.authority)) {
                 if (Objects.equal(test.rootId, root.rootId)) {
                     continue;
                 }
@@ -148,32 +306,37 @@
                     return false;
                 }
             }
+            return true;
         }
-        return true;
     }
 
-    @GuardedBy("ActivityThread")
     public RootInfo getRecentsRoot() {
         return mRecentsRoot;
     }
 
-    @GuardedBy("ActivityThread")
     public boolean isRecentsRoot(RootInfo root) {
         return mRecentsRoot == root;
     }
 
-    @GuardedBy("ActivityThread")
-    public List<RootInfo> getRoots() {
-        return mRoots;
+    public Collection<RootInfo> getRootsBlocking() {
+        waitForFirstLoad();
+        loadStoppedAuthorities();
+        synchronized (mLock) {
+            return mRoots.values();
+        }
     }
 
-    @GuardedBy("ActivityThread")
-    public List<RootInfo> getMatchingRoots(State state) {
-        return getMatchingRoots(mRoots, state);
+    public Collection<RootInfo> getMatchingRootsBlocking(State state) {
+        waitForFirstLoad();
+        loadStoppedAuthorities();
+        synchronized (mLock) {
+            return getMatchingRoots(mRoots.values(), state);
+        }
     }
 
-    public static List<RootInfo> getMatchingRoots(List<RootInfo> roots, State state) {
-        ArrayList<RootInfo> matching = Lists.newArrayList();
+    @VisibleForTesting
+    static List<RootInfo> getMatchingRoots(Collection<RootInfo> roots, State state) {
+        final List<RootInfo> matching = Lists.newArrayList();
         for (RootInfo root : roots) {
             final boolean supportsCreate = (root.flags & Root.FLAG_SUPPORTS_CREATE) != 0;
             final boolean advanced = (root.flags & Root.FLAG_ADVANCED) != 0;
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
index 908729c..df9bce1 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
@@ -19,8 +19,10 @@
 import android.app.Fragment;
 import android.app.FragmentManager;
 import android.app.FragmentTransaction;
+import android.app.LoaderManager.LoaderCallbacks;
 import android.content.Context;
 import android.content.Intent;
+import android.content.Loader;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.os.Bundle;
@@ -44,6 +46,7 @@
 import com.android.documentsui.model.RootInfo;
 import com.android.internal.util.Objects;
 
+import java.util.Collection;
 import java.util.Comparator;
 import java.util.List;
 
@@ -55,6 +58,8 @@
     private ListView mList;
     private SectionedRootsAdapter mAdapter;
 
+    private LoaderCallbacks<Collection<RootInfo>> mCallbacks;
+
     private static final String EXTRA_INCLUDE_APPS = "includeApps";
 
     public static void show(FragmentManager fm, Intent includeApps) {
@@ -87,25 +92,49 @@
     }
 
     @Override
-    public void onResume() {
-        super.onResume();
-        updateRootsAdapter();
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        final Context context = getActivity();
+        final RootsCache roots = DocumentsApplication.getRootsCache(context);
+        final State state = ((DocumentsActivity) context).getDisplayState();
+
+        mCallbacks = new LoaderCallbacks<Collection<RootInfo>>() {
+            @Override
+            public Loader<Collection<RootInfo>> onCreateLoader(int id, Bundle args) {
+                return new RootsLoader(context, roots, state);
+            }
+
+            @Override
+            public void onLoadFinished(
+                    Loader<Collection<RootInfo>> loader, Collection<RootInfo> result) {
+                if (!isAdded()) return;
+
+                final Intent includeApps = getArguments().getParcelable(EXTRA_INCLUDE_APPS);
+
+                mAdapter = new SectionedRootsAdapter(context, result, includeApps);
+                mList.setAdapter(mAdapter);
+
+                onCurrentRootChanged();
+            }
+
+            @Override
+            public void onLoaderReset(Loader<Collection<RootInfo>> loader) {
+                mAdapter = null;
+                mList.setAdapter(null);
+            }
+        };
     }
 
-    private void updateRootsAdapter() {
-        final Context context = getActivity();
+    @Override
+    public void onResume() {
+        super.onResume();
 
+        final Context context = getActivity();
         final State state = ((DocumentsActivity) context).getDisplayState();
         state.showAdvanced = SettingsActivity.getDisplayAdvancedDevices(context);
 
-        final RootsCache roots = DocumentsApplication.getRootsCache(context);
-        final List<RootInfo> matchingRoots = roots.getMatchingRoots(state);
-        final Intent includeApps = getArguments().getParcelable(EXTRA_INCLUDE_APPS);
-
-        mAdapter = new SectionedRootsAdapter(context, matchingRoots, includeApps);
-        mList.setAdapter(mAdapter);
-
-        onCurrentRootChanged();
+        getLoaderManager().restartLoader(2, null, mCallbacks);
     }
 
     public void onCurrentRootChanged() {
@@ -229,7 +258,8 @@
         private final RootsAdapter mDevices;
         private final AppsAdapter mApps;
 
-        public SectionedRootsAdapter(Context context, List<RootInfo> roots, Intent includeApps) {
+        public SectionedRootsAdapter(
+                Context context, Collection<RootInfo> roots, Intent includeApps) {
             mServices = new RootsAdapter(context);
             mShortcuts = new RootsAdapter(context);
             mDevices = new RootsAdapter(context);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsLoader.java b/packages/DocumentsUI/src/com/android/documentsui/RootsLoader.java
new file mode 100644
index 0000000..7108971
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsLoader.java
@@ -0,0 +1,81 @@
+/*
+ * 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 android.content.AsyncTaskLoader;
+import android.content.Context;
+
+import com.android.documentsui.DocumentsActivity.State;
+import com.android.documentsui.model.RootInfo;
+
+import java.util.Collection;
+
+public class RootsLoader extends AsyncTaskLoader<Collection<RootInfo>> {
+    private final RootsCache mRoots;
+    private final State mState;
+
+    private Collection<RootInfo> mResult;
+
+    public RootsLoader(Context context, RootsCache roots, State state) {
+        super(context);
+        mRoots = roots;
+        mState = state;
+    }
+
+    @Override
+    public final Collection<RootInfo> loadInBackground() {
+        return mRoots.getMatchingRootsBlocking(mState);
+    }
+
+    @Override
+    public void deliverResult(Collection<RootInfo> result) {
+        if (isReset()) {
+            return;
+        }
+        Collection<RootInfo> oldResult = mResult;
+        mResult = result;
+
+        if (isStarted()) {
+            super.deliverResult(result);
+        }
+    }
+
+    @Override
+    protected void onStartLoading() {
+        if (mResult != null) {
+            deliverResult(mResult);
+        }
+        if (takeContentChanged() || mResult == null) {
+            forceLoad();
+        }
+    }
+
+    @Override
+    protected void onStopLoading() {
+        cancelLoad();
+    }
+
+    @Override
+    protected void onReset() {
+        super.onReset();
+
+        // Ensure the loader is stopped
+        onStopLoading();
+
+        mResult = null;
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java b/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
index dc5b64a..23e047c 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java
@@ -68,7 +68,6 @@
     public View onCreateView(
             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         final Context context = inflater.getContext();
-        final RootsCache roots = DocumentsApplication.getRootsCache(context);
 
         final View view = inflater.inflate(R.layout.fragment_save, container, false);
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java b/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java
index 1afc80a..a870c7b 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java
@@ -55,6 +55,7 @@
     public String mimeTypes;
 
     /** Derived fields that aren't persisted */
+    public String derivedPackageName;
     public String[] derivedMimeTypes;
     public int derivedIcon;
 
@@ -75,6 +76,7 @@
         availableBytes = -1;
         mimeTypes = null;
 
+        derivedPackageName = null;
         derivedMimeTypes = null;
         derivedIcon = 0;
     }
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java
index a917e3f..e6fbb1b 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java
@@ -17,6 +17,8 @@
 package com.android.externalstorage;
 
 import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.ProviderInfo;
 import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.database.MatrixCursor;
@@ -52,7 +54,10 @@
 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 CRASH_DOCUMENT = false;
 
     private static final String MY_ROOT_ID = "myRoot";
@@ -78,17 +83,42 @@
         return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
     }
 
+    private String mAuthority;
+
+    @Override
+    public void attachInfo(Context context, ProviderInfo info) {
+        mAuthority = info.authority;
+        super.attachInfo(context, info);
+    }
+
     @Override
     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 (REFRESH_ROOTS) {
+            new AsyncTask<Void, Void, Void>() {
+                @Override
+                protected Void doInBackground(Void... params) {
+                    SystemClock.sleep(3000);
+                    Log.d(TAG, "Notifying that something changed!!");
+                    final Uri uri = DocumentsContract.buildRootsUri(mAuthority);
+                    getContext().getContentResolver().notifyChange(uri, null, false);
+                    return null;
+                }
+            }.execute();
+        }
+
         final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
         final RowBuilder row = result.newRow();
         row.add(Root.COLUMN_ROOT_ID, MY_ROOT_ID);
         row.add(Root.COLUMN_ROOT_TYPE, Root.ROOT_TYPE_SERVICE);
         row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_RECENTS);
         row.add(Root.COLUMN_TITLE, "_Test title which is really long");
-        row.add(Root.COLUMN_SUMMARY, "_Summary which is also super long text");
+        row.add(Root.COLUMN_SUMMARY,
+                SystemClock.elapsedRealtime() + " summary which is also super long text");
         row.add(Root.COLUMN_DOCUMENT_ID, MY_DOC_ID);
         row.add(Root.COLUMN_AVAILABLE_BYTES, 1024);
         return result;