From a9ce049db87259e302e2368d2a4a1c11a94fd831 Mon Sep 17 00:00:00 2001 From: Jeff Sharkey Date: Thu, 19 Sep 2013 15:25:56 -0700 Subject: [PATCH] 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 --- packages/DocumentsUI/AndroidManifest.xml | 8 - .../documentsui/DirectoryFragment.java | 9 +- .../documentsui/DocumentChangedReceiver.java | 36 -- .../documentsui/DocumentsActivity.java | 69 +++- .../documentsui/DocumentsApplication.java | 12 +- .../com/android/documentsui/RecentLoader.java | 14 +- .../documentsui/RecentsCreateFragment.java | 1 - .../com/android/documentsui/RootsCache.java | 315 +++++++++++++----- .../android/documentsui/RootsFragment.java | 58 +++- .../com/android/documentsui/RootsLoader.java | 81 +++++ .../com/android/documentsui/SaveFragment.java | 1 - .../android/documentsui/model/RootInfo.java | 2 + .../TestDocumentsProvider.java | 32 +- 13 files changed, 476 insertions(+), 162 deletions(-) delete mode 100644 packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java create mode 100644 packages/DocumentsUI/src/com/android/documentsui/RootsLoader.java diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml index 1ef7bff3e210b..19a29f22e74ee 100644 --- a/packages/DocumentsUI/AndroidManifest.xml +++ b/packages/DocumentsUI/AndroidManifest.xml @@ -48,14 +48,6 @@ android:authorities="com.android.documentsui.recents" android:exported="false" /> - - - - - - - - diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java index 4c2c99c2e2c00..911e9ed99244b 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java @@ -249,8 +249,7 @@ public class DirectoryFragment extends Fragment { context, mType, root, doc, contentsUri, state.userSortOrder); case TYPE_RECENT_OPEN: final RootsCache roots = DocumentsApplication.getRootsCache(context); - final List 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 @@ public class DirectoryFragment extends Fragment { 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 @@ public class DirectoryFragment extends Fragment { 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 54f62ef56ebe2..0000000000000 --- 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 3b71f606ebd90..457bb1916b008 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.ColorDrawable; 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.FileNotFoundException; 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 @@ public class DocumentsActivity extends Activity { 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 @@ public class DocumentsActivity extends Activity { mState.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false); mState.showAdvanced = SettingsActivity.getDisplayAdvancedDevices(this); + } + + private class RestoreRootTask extends AsyncTask { + 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 (mState.action == ACTION_MANAGE) { - final Uri uri = intent.getData(); - final String rootId = DocumentsContract.getRootId(uri); - final RootInfo root = mRoots.getRoot(uri.getAuthority(), rootId); 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 { + 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 @@ public class DocumentsActivity extends Activity { 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 @@ public class DocumentsActivity extends Activity { // If restored root isn't valid, fall back to recents final RootInfo root = getCurrentRoot(); - final List matchingRoots = mRoots.getMatchingRoots(mState); + final Collection 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 class DocumentsActivity extends Activity { 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 @@ public class DocumentsActivity extends Activity { 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 @@ public class DocumentsActivity extends Activity { 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 180ddefa9c013..960181afcb955 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.Context; 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 @@ public class DocumentsApplication extends Application { 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 @@ public class DocumentsApplication extends Application { 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 191201003929f..3659c6e11e7a1 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java +++ b/packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java @@ -41,6 +41,7 @@ import libcore.io.IoUtils; 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 @@ public class RecentLoader extends AsyncTaskLoader { return executor; } - private final List mRoots; - private final String[] mAcceptMimes; + private final RootsCache mRoots; + private final State mState; private final HashMap mTasks = Maps.newHashMap(); @@ -138,10 +139,10 @@ public class RecentLoader extends AsyncTaskLoader { } } - public RecentLoader(Context context, List roots, String[] acceptMimes) { + public RecentLoader(Context context, RootsCache roots, State state) { super(context); mRoots = roots; - mAcceptMimes = acceptMimes; + mState = state; } @Override @@ -150,7 +151,8 @@ public class RecentLoader extends AsyncTaskLoader { // 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 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 @@ public class RecentLoader extends AsyncTaskLoader { 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 b53392532d81d..50763703c56a0 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java @@ -176,7 +176,6 @@ public class RecentsCreateFragment extends Fragment { @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 9b5494851adc2..52d6cc883ff99 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java +++ b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java @@ -21,10 +21,15 @@ import static com.android.documentsui.DocumentsActivity.TAG; 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 android.util.Log; 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 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 mRoots = ArrayListMultimap.create(); + @GuardedBy("mLock") + private HashSet mStoppedAuthorities = Sets.newHashSet(); + + @GuardedBy("mObservedAuthorities") + private final HashSet 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 { + 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 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 roots = ArrayListMultimap.create(); + final HashSet stoppedAuthorities = Sets.newHashSet(); + + final ContentResolver resolver = mContext.getContentResolver(); + final PackageManager pm = mContext.getPackageManager(); + final List 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; + } + + // 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)); } - } catch (Exception e) { - Log.w(TAG, "Failed to load some roots from " + info.authority + ": " + e); - } finally { - IoUtils.closeQuietly(cursor); - ContentProviderClient.closeQuietly(client); } } - } - 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 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 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 @@ public class RootsCache { 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 getRoots() { - return mRoots; + public Collection getRootsBlocking() { + waitForFirstLoad(); + loadStoppedAuthorities(); + synchronized (mLock) { + return mRoots.values(); + } } - @GuardedBy("ActivityThread") - public List getMatchingRoots(State state) { - return getMatchingRoots(mRoots, state); + public Collection getMatchingRootsBlocking(State state) { + waitForFirstLoad(); + loadStoppedAuthorities(); + synchronized (mLock) { + return getMatchingRoots(mRoots.values(), state); + } } - public static List getMatchingRoots(List roots, State state) { - ArrayList matching = Lists.newArrayList(); + @VisibleForTesting + static List getMatchingRoots(Collection roots, State state) { + final List 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 908729c8758f7..df9bce19d07fb 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java @@ -19,8 +19,10 @@ package com.android.documentsui; 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.DocumentInfo; 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 @@ public class RootsFragment extends Fragment { private ListView mList; private SectionedRootsAdapter mAdapter; + private LoaderCallbacks> mCallbacks; + private static final String EXTRA_INCLUDE_APPS = "includeApps"; public static void show(FragmentManager fm, Intent includeApps) { @@ -87,25 +92,49 @@ public class RootsFragment extends Fragment { } @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>() { + @Override + public Loader> onCreateLoader(int id, Bundle args) { + return new RootsLoader(context, roots, state); + } + + @Override + public void onLoadFinished( + Loader> loader, Collection 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> 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 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 @@ public class RootsFragment extends Fragment { private final RootsAdapter mDevices; private final AppsAdapter mApps; - public SectionedRootsAdapter(Context context, List roots, Intent includeApps) { + public SectionedRootsAdapter( + Context context, Collection 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 0000000000000..710897103f5f7 --- /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> { + private final RootsCache mRoots; + private final State mState; + + private Collection mResult; + + public RootsLoader(Context context, RootsCache roots, State state) { + super(context); + mRoots = roots; + mState = state; + } + + @Override + public final Collection loadInBackground() { + return mRoots.getMatchingRootsBlocking(mState); + } + + @Override + public void deliverResult(Collection result) { + if (isReset()) { + return; + } + Collection 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 dc5b64a826060..23e047cd33762 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java @@ -68,7 +68,6 @@ public class SaveFragment extends Fragment { 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 1afc80a569304..a870c7bb808bf 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 class RootInfo implements Durable, Parcelable { public String mimeTypes; /** Derived fields that aren't persisted */ + public String derivedPackageName; public String[] derivedMimeTypes; public int derivedIcon; @@ -75,6 +76,7 @@ public class RootInfo implements Durable, Parcelable { 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 a917e3fb7d5a1..e6fbb1b762e08 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 @@ import java.lang.ref.WeakReference; 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 @@ public class TestDocumentsProvider extends DocumentsProvider { 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() { + @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;