When loading roots for the first time, we're okay using any cached data from the system, but if the locale changes we need to force-refresh everything. Now that we're always using the system cache, we have a nice strong signal for "empty" versus "not cached" results, so we don't need to wait around for the first loading pass to finish. Add logic to invalidate system cache when locale changes, and fix locking bug. Bug: 27977906 Change-Id: Ic50083eff360bea887799583f6c9f02c129eec91
520 lines
19 KiB
Java
520 lines
19 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.Shared.DEBUG;
|
|
|
|
import android.content.BroadcastReceiver.PendingResult;
|
|
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.Bundle;
|
|
import android.os.Handler;
|
|
import android.os.SystemClock;
|
|
import android.provider.DocumentsContract;
|
|
import android.provider.DocumentsContract.Root;
|
|
import android.support.annotation.VisibleForTesting;
|
|
import android.util.Log;
|
|
|
|
import com.android.documentsui.model.RootInfo;
|
|
import com.android.internal.annotations.GuardedBy;
|
|
|
|
import libcore.io.IoUtils;
|
|
|
|
import com.google.common.collect.ArrayListMultimap;
|
|
import com.google.common.collect.Multimap;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
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 {
|
|
public static final Uri sNotificationUri = Uri.parse(
|
|
"content://com.android.documentsui.roots/");
|
|
|
|
private static final String TAG = "RootsCache";
|
|
|
|
private final Context mContext;
|
|
private final ContentObserver mObserver;
|
|
private OnCacheUpdateListener mCacheUpdateListener;
|
|
|
|
private final RootInfo mRecentsRoot;
|
|
|
|
private final Object mLock = new Object();
|
|
private final CountDownLatch mFirstLoad = new CountDownLatch(1);
|
|
|
|
@GuardedBy("mLock")
|
|
private boolean mFirstLoadDone;
|
|
@GuardedBy("mLock")
|
|
private PendingResult mBootCompletedResult;
|
|
|
|
@GuardedBy("mLock")
|
|
private Multimap<String, RootInfo> mRoots = ArrayListMultimap.create();
|
|
@GuardedBy("mLock")
|
|
private HashSet<String> mStoppedAuthorities = new HashSet<>();
|
|
|
|
@GuardedBy("mObservedAuthorities")
|
|
private final HashSet<String> mObservedAuthorities = new HashSet<>();
|
|
|
|
public RootsCache(Context context) {
|
|
mContext = context;
|
|
mObserver = new RootsChangedObserver();
|
|
|
|
// Create a new anonymous "Recents" RootInfo. It's a faker.
|
|
mRecentsRoot = new RootInfo() {{
|
|
// Special root for recents
|
|
derivedIcon = R.drawable.ic_root_recent;
|
|
derivedType = RootInfo.TYPE_RECENTS;
|
|
flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD
|
|
| Root.FLAG_SUPPORTS_CREATE;
|
|
title = mContext.getString(R.string.root_recent);
|
|
availableBytes = -1;
|
|
}};
|
|
}
|
|
|
|
private class RootsChangedObserver extends ContentObserver {
|
|
public RootsChangedObserver() {
|
|
super(new Handler());
|
|
}
|
|
|
|
@Override
|
|
public void onChange(boolean selfChange, Uri uri) {
|
|
if (DEBUG) Log.d(TAG, "Updating roots due to change at " + uri);
|
|
updateAuthorityAsync(uri.getAuthority());
|
|
}
|
|
}
|
|
|
|
static interface OnCacheUpdateListener {
|
|
void onCacheUpdate();
|
|
}
|
|
|
|
/**
|
|
* Gather roots from all known storage providers.
|
|
*/
|
|
public void updateAsync(boolean forceRefreshAll) {
|
|
|
|
// NOTE: This method is called when the UI language changes.
|
|
// For that reason we update our RecentsRoot to reflect
|
|
// the current language.
|
|
mRecentsRoot.title = mContext.getString(R.string.root_recent);
|
|
|
|
// Nothing else about the root should ever change.
|
|
assert(mRecentsRoot.authority == null);
|
|
assert(mRecentsRoot.rootId == null);
|
|
assert(mRecentsRoot.derivedIcon == R.drawable.ic_root_recent);
|
|
assert(mRecentsRoot.derivedType == RootInfo.TYPE_RECENTS);
|
|
assert(mRecentsRoot.flags == (Root.FLAG_LOCAL_ONLY
|
|
| Root.FLAG_SUPPORTS_IS_CHILD
|
|
| Root.FLAG_SUPPORTS_CREATE));
|
|
assert(mRecentsRoot.availableBytes == -1);
|
|
|
|
new UpdateTask(forceRefreshAll, null)
|
|
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
|
}
|
|
|
|
/**
|
|
* Gather roots from storage providers belonging to given package name.
|
|
*/
|
|
public void updatePackageAsync(String packageName) {
|
|
new UpdateTask(false, 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);
|
|
}
|
|
}
|
|
|
|
public void setBootCompletedResult(PendingResult result) {
|
|
synchronized (mLock) {
|
|
// Quickly check if we've already finished loading, otherwise hang
|
|
// out until first pass is finished.
|
|
if (mFirstLoadDone) {
|
|
result.finish();
|
|
} else {
|
|
mBootCompletedResult = result;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Block until the first {@link UpdateTask} pass has finished.
|
|
*
|
|
* @return {@code true} if cached roots is ready to roll, otherwise
|
|
* {@code false} if we timed out while waiting.
|
|
*/
|
|
private boolean waitForFirstLoad() {
|
|
boolean success = false;
|
|
try {
|
|
success = mFirstLoad.await(15, TimeUnit.SECONDS);
|
|
} catch (InterruptedException e) {
|
|
}
|
|
if (!success) {
|
|
Log.w(TAG, "Timeout waiting for first update");
|
|
}
|
|
return success;
|
|
}
|
|
|
|
/**
|
|
* 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 (DEBUG) Log.d(TAG, "Loading stopped authority " + authority);
|
|
mRoots.putAll(authority, loadRootsForAuthority(resolver, authority, true));
|
|
}
|
|
mStoppedAuthorities.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load roots from a stopped authority. Normal {@link UpdateTask} passes
|
|
* ignore stopped applications.
|
|
*/
|
|
private void loadStoppedAuthority(String authority) {
|
|
final ContentResolver resolver = mContext.getContentResolver();
|
|
synchronized (mLock) {
|
|
if (!mStoppedAuthorities.contains(authority)) {
|
|
return;
|
|
}
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Loading stopped authority " + authority);
|
|
}
|
|
mRoots.putAll(authority, loadRootsForAuthority(resolver, authority, true));
|
|
mStoppedAuthorities.remove(authority);
|
|
}
|
|
}
|
|
|
|
private class UpdateTask extends AsyncTask<Void, Void, Void> {
|
|
private final boolean mForceRefreshAll;
|
|
private final String mForceRefreshPackage;
|
|
|
|
private final Multimap<String, RootInfo> mTaskRoots = ArrayListMultimap.create();
|
|
private final HashSet<String> mTaskStoppedAuthorities = new HashSet<>();
|
|
|
|
/**
|
|
* Create task to update roots cache.
|
|
*
|
|
* @param forceRefreshAll when true, all previously cached values for
|
|
* all packages should be ignored.
|
|
* @param forceRefreshPackage when non-null, all previously cached
|
|
* values for this specific package should be ignored.
|
|
*/
|
|
public UpdateTask(boolean forceRefreshAll, String forceRefreshPackage) {
|
|
mForceRefreshAll = forceRefreshAll;
|
|
mForceRefreshPackage = forceRefreshPackage;
|
|
}
|
|
|
|
@Override
|
|
protected Void doInBackground(Void... params) {
|
|
final long start = SystemClock.elapsedRealtime();
|
|
|
|
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;
|
|
if (DEBUG)
|
|
Log.d(TAG, "Update found " + mTaskRoots.size() + " roots in " + delta + "ms");
|
|
synchronized (mLock) {
|
|
mFirstLoadDone = true;
|
|
if (mBootCompletedResult != null) {
|
|
mBootCompletedResult.finish();
|
|
mBootCompletedResult = null;
|
|
}
|
|
mRoots = mTaskRoots;
|
|
mStoppedAuthorities = mTaskStoppedAuthorities;
|
|
}
|
|
mFirstLoad.countDown();
|
|
resolver.notifyChange(sNotificationUri, null, false);
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
protected void onPostExecute(Void result) {
|
|
if (mCacheUpdateListener != null) {
|
|
mCacheUpdateListener.onCacheUpdate();
|
|
}
|
|
}
|
|
|
|
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 (DEBUG) Log.d(TAG, "Ignoring stopped authority " + info.authority);
|
|
mTaskStoppedAuthorities.add(info.authority);
|
|
return;
|
|
}
|
|
|
|
final boolean forceRefresh = mForceRefreshAll
|
|
|| Objects.equals(info.packageName, mForceRefreshPackage);
|
|
mTaskRoots.putAll(info.authority, loadRootsForAuthority(mContext.getContentResolver(),
|
|
info.authority, forceRefresh));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bring up requested provider and query for all active roots.
|
|
*/
|
|
private Collection<RootInfo> loadRootsForAuthority(ContentResolver resolver, String authority,
|
|
boolean forceRefresh) {
|
|
if (DEBUG) 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 Uri rootsUri = DocumentsContract.buildRootsUri(authority);
|
|
if (!forceRefresh) {
|
|
// Look for roots data that we might have cached for ourselves in the
|
|
// long-lived system process.
|
|
final Bundle systemCache = resolver.getCache(rootsUri);
|
|
if (systemCache != null) {
|
|
if (DEBUG) Log.d(TAG, "System cache hit for " + authority);
|
|
return systemCache.getParcelableArrayList(TAG);
|
|
}
|
|
}
|
|
|
|
final ArrayList<RootInfo> roots = new ArrayList<>();
|
|
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);
|
|
}
|
|
|
|
// Cache these freshly parsed roots over in the long-lived system
|
|
// process, in case our process goes away. The system takes care of
|
|
// invalidating the cache if the package or Uri changes.
|
|
final Bundle systemCache = new Bundle();
|
|
systemCache.putParcelableArrayList(TAG, roots);
|
|
resolver.putCache(rootsUri, systemCache);
|
|
|
|
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, false));
|
|
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.equals(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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a list of roots for the specified authority. If not found, then
|
|
* an empty list is returned.
|
|
*/
|
|
public Collection<RootInfo> getRootsForAuthorityBlocking(String authority) {
|
|
waitForFirstLoad();
|
|
loadStoppedAuthority(authority);
|
|
synchronized (mLock) {
|
|
final Collection<RootInfo> roots = mRoots.get(authority);
|
|
return roots != null ? roots : Collections.<RootInfo>emptyList();
|
|
}
|
|
}
|
|
|
|
public void setOnCacheUpdateListener(OnCacheUpdateListener cacheUpdateListener) {
|
|
mCacheUpdateListener = cacheUpdateListener;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
static List<RootInfo> getMatchingRoots(Collection<RootInfo> roots, State state) {
|
|
final List<RootInfo> matching = new ArrayList<>();
|
|
for (RootInfo root : roots) {
|
|
|
|
if (DEBUG) Log.d(TAG, "Evaluating " + root);
|
|
|
|
if (state.action == State.ACTION_CREATE && !root.supportsCreate()) {
|
|
if (DEBUG) Log.d(TAG, "Excluding read-only root because: ACTION_CREATE.");
|
|
continue;
|
|
}
|
|
|
|
if (state.action == State.ACTION_PICK_COPY_DESTINATION
|
|
&& !root.supportsCreate()) {
|
|
if (DEBUG) Log.d(
|
|
TAG, "Excluding read-only root because: ACTION_PICK_COPY_DESTINATION.");
|
|
continue;
|
|
}
|
|
|
|
if (state.action == State.ACTION_OPEN_TREE && !root.supportsChildren()) {
|
|
if (DEBUG) Log.d(
|
|
TAG, "Excluding root !supportsChildren because: ACTION_OPEN_TREE.");
|
|
continue;
|
|
}
|
|
|
|
if (!state.showAdvanced && root.isAdvanced()) {
|
|
if (DEBUG) Log.d(TAG, "Excluding root because: unwanted advanced device.");
|
|
continue;
|
|
}
|
|
|
|
if (state.localOnly && !root.isLocalOnly()) {
|
|
if (DEBUG) Log.d(TAG, "Excluding root because: unwanted non-local device.");
|
|
continue;
|
|
}
|
|
|
|
if (state.directoryCopy && root.isDownloads()) {
|
|
if (DEBUG) Log.d(
|
|
TAG, "Excluding downloads root because: unsupported directory copy.");
|
|
continue;
|
|
}
|
|
|
|
if (state.action == State.ACTION_OPEN && root.isEmpty()) {
|
|
if (DEBUG) Log.d(TAG, "Excluding empty root because: ACTION_OPEN.");
|
|
continue;
|
|
}
|
|
|
|
if (state.action == State.ACTION_GET_CONTENT && root.isEmpty()) {
|
|
if (DEBUG) Log.d(TAG, "Excluding empty root because: ACTION_GET_CONTENT.");
|
|
continue;
|
|
}
|
|
|
|
final boolean overlap =
|
|
MimePredicate.mimeMatches(root.derivedMimeTypes, state.acceptMimes) ||
|
|
MimePredicate.mimeMatches(state.acceptMimes, root.derivedMimeTypes);
|
|
if (!overlap) {
|
|
if (DEBUG) Log.d(
|
|
TAG, "Excluding root because: unsupported content types > "
|
|
+ state.acceptMimes);
|
|
continue;
|
|
}
|
|
|
|
if (state.excludedAuthorities.contains(root.authority)) {
|
|
if (DEBUG) Log.d(TAG, "Excluding root because: owned by calling package.");
|
|
continue;
|
|
}
|
|
|
|
if (DEBUG) Log.d(TAG, "Including " + root);
|
|
matching.add(root);
|
|
}
|
|
return matching;
|
|
}
|
|
}
|