am a9ce049d: Load RootInfo in background, invalidation.
* commit 'a9ce049db87259e302e2368d2a4a1c11a94fd831': Load RootInfo in background, invalidation.
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<RootInfo> mRoots = Lists.newArrayList();
|
||||
private final RootInfo mRecentsRoot = new RootInfo();
|
||||
|
||||
private RootInfo mRecentsRoot;
|
||||
private final Object mLock = new Object();
|
||||
private final CountDownLatch mFirstLoad = new CountDownLatch(1);
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private Multimap<String, RootInfo> mRoots = ArrayListMultimap.create();
|
||||
@GuardedBy("mLock")
|
||||
private HashSet<String> mStoppedAuthorities = Sets.newHashSet();
|
||||
|
||||
@GuardedBy("mObservedAuthorities")
|
||||
private final HashSet<String> mObservedAuthorities = Sets.newHashSet();
|
||||
|
||||
public RootsCache(Context context) {
|
||||
mContext = context;
|
||||
update();
|
||||
mObserver = new RootsChangedObserver();
|
||||
}
|
||||
|
||||
private class RootsChangedObserver extends ContentObserver {
|
||||
public RootsChangedObserver() {
|
||||
super(new Handler());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(boolean selfChange, Uri uri) {
|
||||
if (LOGD) Log.d(TAG, "Updating roots due to change at " + uri);
|
||||
updateAuthorityAsync(uri.getAuthority());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather roots from all known storage providers.
|
||||
*/
|
||||
@GuardedBy("ActivityThread")
|
||||
public void update() {
|
||||
mRoots.clear();
|
||||
public void updateAsync() {
|
||||
// Special root for recents
|
||||
mRecentsRoot.rootType = Root.ROOT_TYPE_SHORTCUT;
|
||||
mRecentsRoot.icon = R.drawable.ic_root_recent;
|
||||
mRecentsRoot.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE;
|
||||
mRecentsRoot.title = mContext.getString(R.string.root_recent);
|
||||
mRecentsRoot.availableBytes = -1;
|
||||
|
||||
if (RECENTS_ENABLED) {
|
||||
// Create special root for recents
|
||||
final RootInfo root = new RootInfo();
|
||||
root.rootType = Root.ROOT_TYPE_SHORTCUT;
|
||||
root.icon = R.drawable.ic_root_recent;
|
||||
root.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE;
|
||||
root.title = mContext.getString(R.string.root_recent);
|
||||
root.availableBytes = -1;
|
||||
new UpdateTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
mRoots.add(root);
|
||||
mRecentsRoot = root;
|
||||
/**
|
||||
* Gather roots from storage providers belonging to given package name.
|
||||
*/
|
||||
public void updatePackageAsync(String packageName) {
|
||||
// Need at least first load, since we're going to be using previously
|
||||
// cached values for non-matching packages.
|
||||
waitForFirstLoad();
|
||||
new UpdateTask(packageName).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather roots from storage providers belonging to given authority.
|
||||
*/
|
||||
public void updateAuthorityAsync(String authority) {
|
||||
final ProviderInfo info = mContext.getPackageManager().resolveContentProvider(authority, 0);
|
||||
if (info != null) {
|
||||
updatePackageAsync(info.packageName);
|
||||
}
|
||||
}
|
||||
|
||||
private void waitForFirstLoad() {
|
||||
boolean success = false;
|
||||
try {
|
||||
success = mFirstLoad.await(15, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
if (!success) {
|
||||
Log.w(TAG, "Timeout waiting for first update");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load roots from authorities that are in stopped state. Normal
|
||||
* {@link UpdateTask} passes ignore stopped applications.
|
||||
*/
|
||||
private void loadStoppedAuthorities() {
|
||||
final ContentResolver resolver = mContext.getContentResolver();
|
||||
synchronized (mLock) {
|
||||
for (String authority : mStoppedAuthorities) {
|
||||
if (LOGD) Log.d(TAG, "Loading stopped authority " + authority);
|
||||
mRoots.putAll(authority, loadRootsForAuthority(resolver, authority));
|
||||
}
|
||||
mStoppedAuthorities.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateTask extends AsyncTask<Void, Void, Void> {
|
||||
private final String mFilterPackage;
|
||||
|
||||
/**
|
||||
* Update all roots.
|
||||
*/
|
||||
public UpdateTask() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
// Query for other storage backends
|
||||
final ContentResolver resolver = mContext.getContentResolver();
|
||||
final PackageManager pm = mContext.getPackageManager();
|
||||
final List<ProviderInfo> providers = pm.queryContentProviders(
|
||||
null, -1, PackageManager.GET_META_DATA);
|
||||
for (ProviderInfo info : providers) {
|
||||
if (info.metaData != null && info.metaData.containsKey(
|
||||
DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
|
||||
/**
|
||||
* Only update roots belonging to given package name. Other roots will
|
||||
* be copied from cached {@link #mRoots} values.
|
||||
*/
|
||||
public UpdateTask(String filterPackage) {
|
||||
mFilterPackage = filterPackage;
|
||||
}
|
||||
|
||||
// TODO: populate roots on background thread, and cache results
|
||||
final Uri rootsUri = DocumentsContract.buildRootsUri(info.authority);
|
||||
final ContentProviderClient client = resolver
|
||||
.acquireUnstableContentProviderClient(info.authority);
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = client.query(rootsUri, null, null, null, null);
|
||||
while (cursor.moveToNext()) {
|
||||
final RootInfo root = RootInfo.fromRootsCursor(info.authority, cursor);
|
||||
mRoots.add(root);
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
final long start = SystemClock.elapsedRealtime();
|
||||
|
||||
final Multimap<String, RootInfo> roots = ArrayListMultimap.create();
|
||||
final HashSet<String> stoppedAuthorities = Sets.newHashSet();
|
||||
|
||||
final ContentResolver resolver = mContext.getContentResolver();
|
||||
final PackageManager pm = mContext.getPackageManager();
|
||||
final List<ProviderInfo> providers = pm.queryContentProviders(
|
||||
null, -1, PackageManager.GET_META_DATA);
|
||||
for (ProviderInfo info : providers) {
|
||||
if (info.metaData != null && info.metaData.containsKey(
|
||||
DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
|
||||
// Ignore stopped packages for now; we might query them
|
||||
// later during UI interaction.
|
||||
if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) {
|
||||
if (LOGD) Log.d(TAG, "Ignoring stopped authority " + info.authority);
|
||||
stoppedAuthorities.add(info.authority);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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<RootInfo> loadRootsForAuthority(ContentResolver resolver, String authority) {
|
||||
if (LOGD) Log.d(TAG, "Loading roots for " + authority);
|
||||
|
||||
synchronized (mObservedAuthorities) {
|
||||
if (mObservedAuthorities.add(authority)) {
|
||||
// Watch for any future updates
|
||||
final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
|
||||
mContext.getContentResolver().registerContentObserver(rootsUri, true, mObserver);
|
||||
}
|
||||
}
|
||||
|
||||
final List<RootInfo> roots = Lists.newArrayList();
|
||||
final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
|
||||
final ContentProviderClient client = resolver
|
||||
.acquireUnstableContentProviderClient(authority);
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = client.query(rootsUri, null, null, null, null);
|
||||
while (cursor.moveToNext()) {
|
||||
final RootInfo root = RootInfo.fromRootsCursor(authority, cursor);
|
||||
roots.add(root);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Failed to load some roots from " + authority + ": " + e);
|
||||
} finally {
|
||||
IoUtils.closeQuietly(cursor);
|
||||
ContentProviderClient.closeQuietly(client);
|
||||
}
|
||||
return roots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the requested {@link RootInfo}, but only loading the roots for the
|
||||
* requested authority. This is useful when we want to load fast without
|
||||
* waiting for all the other roots to come back.
|
||||
*/
|
||||
public RootInfo getRootOneshot(String authority, String rootId) {
|
||||
synchronized (mLock) {
|
||||
RootInfo root = getRootLocked(authority, rootId);
|
||||
if (root == null) {
|
||||
mRoots.putAll(
|
||||
authority, loadRootsForAuthority(mContext.getContentResolver(), authority));
|
||||
root = getRootLocked(authority, rootId);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
||||
public RootInfo getRootBlocking(String authority, String rootId) {
|
||||
waitForFirstLoad();
|
||||
loadStoppedAuthorities();
|
||||
synchronized (mLock) {
|
||||
return getRootLocked(authority, rootId);
|
||||
}
|
||||
}
|
||||
|
||||
private RootInfo getRootLocked(String authority, String rootId) {
|
||||
for (RootInfo root : mRoots.get(authority)) {
|
||||
if (Objects.equal(root.rootId, rootId)) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@GuardedBy("ActivityThread")
|
||||
public RootInfo getRoot(String authority, String rootId) {
|
||||
for (RootInfo root : mRoots) {
|
||||
if (Objects.equal(root.authority, authority) && Objects.equal(root.rootId, rootId)) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@GuardedBy("ActivityThread")
|
||||
public boolean isIconUnique(RootInfo root) {
|
||||
final int rootIcon = root.derivedIcon != 0 ? root.derivedIcon : root.icon;
|
||||
for (RootInfo test : mRoots) {
|
||||
if (Objects.equal(test.authority, root.authority)) {
|
||||
public boolean isIconUniqueBlocking(RootInfo root) {
|
||||
waitForFirstLoad();
|
||||
loadStoppedAuthorities();
|
||||
synchronized (mLock) {
|
||||
final int rootIcon = root.derivedIcon != 0 ? root.derivedIcon : root.icon;
|
||||
for (RootInfo test : mRoots.get(root.authority)) {
|
||||
if (Objects.equal(test.rootId, root.rootId)) {
|
||||
continue;
|
||||
}
|
||||
@@ -148,32 +306,37 @@ 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;
|
||||
|
||||
@@ -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 onResume() {
|
||||
super.onResume();
|
||||
updateRootsAdapter();
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
final Context context = getActivity();
|
||||
final RootsCache roots = DocumentsApplication.getRootsCache(context);
|
||||
final State state = ((DocumentsActivity) context).getDisplayState();
|
||||
|
||||
mCallbacks = new LoaderCallbacks<Collection<RootInfo>>() {
|
||||
@Override
|
||||
public Loader<Collection<RootInfo>> onCreateLoader(int id, Bundle args) {
|
||||
return new RootsLoader(context, roots, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(
|
||||
Loader<Collection<RootInfo>> loader, Collection<RootInfo> result) {
|
||||
if (!isAdded()) return;
|
||||
|
||||
final Intent includeApps = getArguments().getParcelable(EXTRA_INCLUDE_APPS);
|
||||
|
||||
mAdapter = new SectionedRootsAdapter(context, result, includeApps);
|
||||
mList.setAdapter(mAdapter);
|
||||
|
||||
onCurrentRootChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Collection<RootInfo>> loader) {
|
||||
mAdapter = null;
|
||||
mList.setAdapter(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void updateRootsAdapter() {
|
||||
final Context context = getActivity();
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
final Context context = getActivity();
|
||||
final State state = ((DocumentsActivity) context).getDisplayState();
|
||||
state.showAdvanced = SettingsActivity.getDisplayAdvancedDevices(context);
|
||||
|
||||
final RootsCache roots = DocumentsApplication.getRootsCache(context);
|
||||
final List<RootInfo> matchingRoots = roots.getMatchingRoots(state);
|
||||
final Intent includeApps = getArguments().getParcelable(EXTRA_INCLUDE_APPS);
|
||||
|
||||
mAdapter = new SectionedRootsAdapter(context, matchingRoots, includeApps);
|
||||
mList.setAdapter(mAdapter);
|
||||
|
||||
onCurrentRootChanged();
|
||||
getLoaderManager().restartLoader(2, null, mCallbacks);
|
||||
}
|
||||
|
||||
public void onCurrentRootChanged() {
|
||||
@@ -229,7 +258,8 @@ 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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user