summaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/DocumentsUI/AndroidManifest.xml8
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java9
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DocumentChangedReceiver.java36
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java69
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java12
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RecentLoader.java14
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java1
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RootsCache.java305
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java56
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/RootsLoader.java81
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/SaveFragment.java1
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java2
-rw-r--r--packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java32
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;