diff options
Diffstat (limited to 'packages')
13 files changed, 470 insertions, 156 deletions
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 @@ public class DirectoryFragment extends Fragment { 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 @@ 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 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.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<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 (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<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 @@ 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<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 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 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.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 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 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<DirectoryResult> { 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 class RecentLoader extends AsyncTaskLoader<DirectoryResult> { } } - 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 @@ public class RecentLoader extends AsyncTaskLoader<DirectoryResult> { // 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 @@ public class RecentLoader extends AsyncTaskLoader<DirectoryResult> { 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 @@ 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 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 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; + + private final RootInfo mRecentsRoot = new RootInfo(); - public List<RootInfo> mRoots = Lists.newArrayList(); + private final Object mLock = new Object(); + private final CountDownLatch mFirstLoad = new CountDownLatch(1); - private RootInfo mRecentsRoot; + @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); } + } - // Query for other storage backends + 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(); - 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)) { - - // 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); + 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); + } + + /** + * Only update roots belonging to given package name. Other roots will + * be copied from cached {@link #mRoots} values. + */ + public UpdateTask(String filterPackage) { + mFilterPackage = filterPackage; + } + + @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; + } + + // 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); } } + + 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; + } + } + + /** + * 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); + } } - Log.d(TAG, "Update found " + mRoots.size() + " roots"); + 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; } - @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)) { - return root; + /** + * 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; } - 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)) { + 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 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<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 @@ 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<Collection<RootInfo>> 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 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); + } + }; + } + + @Override public void onResume() { super.onResume(); - updateRootsAdapter(); - } - private void updateRootsAdapter() { 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 @@ public class RootsFragment extends Fragment { 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 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 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 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 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 @@ 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<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; |