am 89dec7e6: am a9ce049d: Load RootInfo in background, invalidation.

* commit '89dec7e6e9ab98141de89096ef052cb2c158c557':
  Load RootInfo in background, invalidation.
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;