Files
frameworks_base/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
Daichi Hirono 9be34298be DocumentsUI: Filter out downloads root if the source documents include directory.
This is the short term solution for the downloads root, which does not support
directory creation. For the long term solution, we need to add a flag to Root
model to check if the root support directory creation or not.

BUG=20186663

Change-Id: I15c4795892f38e3f968776f732a6f2160ae775fc
2015-04-15 13:27:55 +09:00

391 lines
14 KiB
Java

/*
* 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.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
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;
import com.android.documentsui.BaseActivity.State;
import com.android.documentsui.model.RootInfo;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
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.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
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 = false;
public static final Uri sNotificationUri = Uri.parse(
"content://com.android.documentsui.roots/");
private final Context mContext;
private final ContentObserver mObserver;
private final RootInfo mRecentsRoot = new RootInfo();
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;
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.
*/
public void updateAsync() {
// Special root for recents
mRecentsRoot.authority = null;
mRecentsRoot.rootId = null;
mRecentsRoot.derivedIcon = R.drawable.ic_root_recent;
mRecentsRoot.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE
| Root.FLAG_SUPPORTS_IS_CHILD;
mRecentsRoot.title = mContext.getString(R.string.root_recent);
mRecentsRoot.availableBytes = -1;
new UpdateTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
/**
* Gather roots from storage providers belonging to given package name.
*/
public void updatePackageAsync(String packageName) {
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;
private final Multimap<String, RootInfo> mTaskRoots = ArrayListMultimap.create();
private final HashSet<String> mTaskStoppedAuthorities = Sets.newHashSet();
/**
* 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();
if (mFilterPackage != null) {
// Need at least first load, since we're going to be using
// previously cached values for non-matching packages.
waitForFirstLoad();
}
mTaskRoots.put(mRecentsRoot.authority, mRecentsRoot);
final ContentResolver resolver = mContext.getContentResolver();
final PackageManager pm = mContext.getPackageManager();
// Pick up provider with action string
final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE);
final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, 0);
for (ResolveInfo info : providers) {
handleDocumentsProvider(info.providerInfo);
}
final long delta = SystemClock.elapsedRealtime() - start;
Log.d(TAG, "Update found " + mTaskRoots.size() + " roots in " + delta + "ms");
synchronized (mLock) {
mRoots = mTaskRoots;
mStoppedAuthorities = mTaskStoppedAuthorities;
}
mFirstLoad.countDown();
resolver.notifyChange(sNotificationUri, null, false);
return null;
}
private void handleDocumentsProvider(ProviderInfo info) {
// 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);
mTaskStoppedAuthorities.add(info.authority);
return;
}
// Try using cached roots if filtering
boolean cacheHit = false;
if (mFilterPackage != null && !mFilterPackage.equals(info.packageName)) {
synchronized (mLock) {
if (mTaskRoots.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) {
mTaskRoots.putAll(info.authority,
loadRootsForAuthority(mContext.getContentResolver(), info.authority));
}
}
}
/**
* 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);
ContentProviderClient client = null;
Cursor cursor = null;
try {
client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
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.releaseQuietly(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.equals(root.rootId, rootId)) {
return root;
}
}
return null;
}
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.equals(test.rootId, root.rootId)) {
continue;
}
final int testIcon = test.derivedIcon != 0 ? test.derivedIcon : test.icon;
if (testIcon == rootIcon) {
return false;
}
}
return true;
}
}
public RootInfo getRecentsRoot() {
return mRecentsRoot;
}
public boolean isRecentsRoot(RootInfo root) {
return mRecentsRoot == root;
}
public Collection<RootInfo> getRootsBlocking() {
waitForFirstLoad();
loadStoppedAuthorities();
synchronized (mLock) {
return mRoots.values();
}
}
public Collection<RootInfo> getMatchingRootsBlocking(State state) {
waitForFirstLoad();
loadStoppedAuthorities();
synchronized (mLock) {
return getMatchingRoots(mRoots.values(), state);
}
}
@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 supportsIsChild = (root.flags & Root.FLAG_SUPPORTS_IS_CHILD) != 0;
final boolean advanced = (root.flags & Root.FLAG_ADVANCED) != 0;
final boolean localOnly = (root.flags & Root.FLAG_LOCAL_ONLY) != 0;
final boolean empty = (root.flags & Root.FLAG_EMPTY) != 0;
// Exclude read-only devices when creating
if (state.action == State.ACTION_CREATE && !supportsCreate) continue;
if (state.action == State.ACTION_OPEN_COPY_DESTINATION && !supportsCreate) continue;
// Exclude roots that don't support directory picking
if (state.action == State.ACTION_OPEN_TREE && !supportsIsChild) continue;
// Exclude advanced devices when not requested
if (!state.showAdvanced && advanced) continue;
// Exclude non-local devices when local only
if (state.localOnly && !localOnly) continue;
// Exclude downloads roots that don't support directory creation
// TODO: Add flag to check the root supports directory creation or not.
if (state.directoryCopy && root.isDownloads()) continue;
// Only show empty roots when creating
if ((state.action != State.ACTION_CREATE ||
state.action != State.ACTION_OPEN_TREE ||
state.action != State.ACTION_OPEN_COPY_DESTINATION) && empty) continue;
// Only include roots that serve requested content
final boolean overlap =
MimePredicate.mimeMatches(root.derivedMimeTypes, state.acceptMimes) ||
MimePredicate.mimeMatches(state.acceptMimes, root.derivedMimeTypes);
if (!overlap) {
continue;
}
matching.add(root);
}
return matching;
}
}