/* * 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; } // Exclude roots from the calling package. if (state.excludedAuthorities.contains(root.authority)) { if (LOGD) Log.d(TAG, "Excluding root " + root.authority + " from calling package."); continue; } matching.add(root); } return matching; } }