2 * Copyright (C) 2013 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.documentsui;
19 import static com.android.documentsui.DocumentsActivity.TAG;
21 import android.content.ContentProviderClient;
22 import android.content.ContentResolver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.pm.ApplicationInfo;
26 import android.content.pm.PackageManager;
27 import android.content.pm.ProviderInfo;
28 import android.content.pm.ResolveInfo;
29 import android.database.ContentObserver;
30 import android.database.Cursor;
31 import android.net.Uri;
32 import android.os.AsyncTask;
33 import android.os.Handler;
34 import android.os.SystemClock;
35 import android.provider.DocumentsContract;
36 import android.provider.DocumentsContract.Root;
37 import android.util.Log;
39 import com.android.documentsui.DocumentsActivity.State;
40 import com.android.documentsui.model.RootInfo;
41 import com.android.internal.annotations.GuardedBy;
42 import com.android.internal.annotations.VisibleForTesting;
43 import com.android.internal.util.Objects;
44 import com.google.android.collect.Lists;
45 import com.google.android.collect.Sets;
46 import com.google.common.collect.ArrayListMultimap;
47 import com.google.common.collect.Multimap;
49 import libcore.io.IoUtils;
51 import java.util.Collection;
52 import java.util.HashSet;
53 import java.util.List;
54 import java.util.concurrent.CountDownLatch;
55 import java.util.concurrent.TimeUnit;
58 * Cache of known storage backends and their roots.
60 public class RootsCache {
61 private static final boolean LOGD = true;
63 // TODO: cache roots in local provider to avoid spinning up backends
64 // TODO: root updates should trigger UI refresh
66 private final Context mContext;
67 private final ContentObserver mObserver;
69 private final RootInfo mRecentsRoot = new RootInfo();
71 private final Object mLock = new Object();
72 private final CountDownLatch mFirstLoad = new CountDownLatch(1);
75 private Multimap<String, RootInfo> mRoots = ArrayListMultimap.create();
77 private HashSet<String> mStoppedAuthorities = Sets.newHashSet();
79 @GuardedBy("mObservedAuthorities")
80 private final HashSet<String> mObservedAuthorities = Sets.newHashSet();
82 public RootsCache(Context context) {
84 mObserver = new RootsChangedObserver();
87 private class RootsChangedObserver extends ContentObserver {
88 public RootsChangedObserver() {
93 public void onChange(boolean selfChange, Uri uri) {
94 if (LOGD) Log.d(TAG, "Updating roots due to change at " + uri);
95 updateAuthorityAsync(uri.getAuthority());
100 * Gather roots from all known storage providers.
102 public void updateAsync() {
103 // Special root for recents
104 mRecentsRoot.authority = null;
105 mRecentsRoot.rootId = null;
106 mRecentsRoot.icon = R.drawable.ic_root_recent;
107 mRecentsRoot.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE;
108 mRecentsRoot.title = mContext.getString(R.string.root_recent);
109 mRecentsRoot.availableBytes = -1;
111 new UpdateTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
115 * Gather roots from storage providers belonging to given package name.
117 public void updatePackageAsync(String packageName) {
118 // Need at least first load, since we're going to be using previously
119 // cached values for non-matching packages.
121 new UpdateTask(packageName).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
125 * Gather roots from storage providers belonging to given authority.
127 public void updateAuthorityAsync(String authority) {
128 final ProviderInfo info = mContext.getPackageManager().resolveContentProvider(authority, 0);
130 updatePackageAsync(info.packageName);
134 private void waitForFirstLoad() {
135 boolean success = false;
137 success = mFirstLoad.await(15, TimeUnit.SECONDS);
138 } catch (InterruptedException e) {
141 Log.w(TAG, "Timeout waiting for first update");
146 * Load roots from authorities that are in stopped state. Normal
147 * {@link UpdateTask} passes ignore stopped applications.
149 private void loadStoppedAuthorities() {
150 final ContentResolver resolver = mContext.getContentResolver();
151 synchronized (mLock) {
152 for (String authority : mStoppedAuthorities) {
153 if (LOGD) Log.d(TAG, "Loading stopped authority " + authority);
154 mRoots.putAll(authority, loadRootsForAuthority(resolver, authority));
156 mStoppedAuthorities.clear();
160 private class UpdateTask extends AsyncTask<Void, Void, Void> {
161 private final String mFilterPackage;
163 private final Multimap<String, RootInfo> mTaskRoots = ArrayListMultimap.create();
164 private final HashSet<String> mTaskStoppedAuthorities = Sets.newHashSet();
169 public UpdateTask() {
174 * Only update roots belonging to given package name. Other roots will
175 * be copied from cached {@link #mRoots} values.
177 public UpdateTask(String filterPackage) {
178 mFilterPackage = filterPackage;
182 protected Void doInBackground(Void... params) {
183 final long start = SystemClock.elapsedRealtime();
185 mTaskRoots.put(mRecentsRoot.authority, mRecentsRoot);
187 final ContentResolver resolver = mContext.getContentResolver();
188 final PackageManager pm = mContext.getPackageManager();
190 // Pick up provider with action string
191 final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE);
192 final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, 0);
193 for (ResolveInfo info : providers) {
194 handleDocumentsProvider(info.providerInfo);
197 final long delta = SystemClock.elapsedRealtime() - start;
198 Log.d(TAG, "Update found " + mTaskRoots.size() + " roots in " + delta + "ms");
199 synchronized (mLock) {
201 mStoppedAuthorities = mTaskStoppedAuthorities;
203 mFirstLoad.countDown();
207 private void handleDocumentsProvider(ProviderInfo info) {
208 // Ignore stopped packages for now; we might query them
209 // later during UI interaction.
210 if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) {
211 if (LOGD) Log.d(TAG, "Ignoring stopped authority " + info.authority);
212 mTaskStoppedAuthorities.add(info.authority);
216 // Try using cached roots if filtering
217 boolean cacheHit = false;
218 if (mFilterPackage != null && !mFilterPackage.equals(info.packageName)) {
219 synchronized (mLock) {
220 if (mTaskRoots.putAll(info.authority, mRoots.get(info.authority))) {
221 if (LOGD) Log.d(TAG, "Used cached roots for " + info.authority);
227 // Cache miss, or loading everything
229 mTaskRoots.putAll(info.authority,
230 loadRootsForAuthority(mContext.getContentResolver(), info.authority));
236 * Bring up requested provider and query for all active roots.
238 private Collection<RootInfo> loadRootsForAuthority(ContentResolver resolver, String authority) {
239 if (LOGD) Log.d(TAG, "Loading roots for " + authority);
241 synchronized (mObservedAuthorities) {
242 if (mObservedAuthorities.add(authority)) {
243 // Watch for any future updates
244 final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
245 mContext.getContentResolver().registerContentObserver(rootsUri, true, mObserver);
249 final List<RootInfo> roots = Lists.newArrayList();
250 final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
252 ContentProviderClient client = null;
253 Cursor cursor = null;
255 client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
256 cursor = client.query(rootsUri, null, null, null, null);
257 while (cursor.moveToNext()) {
258 final RootInfo root = RootInfo.fromRootsCursor(authority, cursor);
261 } catch (Exception e) {
262 Log.w(TAG, "Failed to load some roots from " + authority + ": " + e);
264 IoUtils.closeQuietly(cursor);
265 ContentProviderClient.releaseQuietly(client);
271 * Return the requested {@link RootInfo}, but only loading the roots for the
272 * requested authority. This is useful when we want to load fast without
273 * waiting for all the other roots to come back.
275 public RootInfo getRootOneshot(String authority, String rootId) {
276 synchronized (mLock) {
277 RootInfo root = getRootLocked(authority, rootId);
280 authority, loadRootsForAuthority(mContext.getContentResolver(), authority));
281 root = getRootLocked(authority, rootId);
287 public RootInfo getRootBlocking(String authority, String rootId) {
289 loadStoppedAuthorities();
290 synchronized (mLock) {
291 return getRootLocked(authority, rootId);
295 private RootInfo getRootLocked(String authority, String rootId) {
296 for (RootInfo root : mRoots.get(authority)) {
297 if (Objects.equal(root.rootId, rootId)) {
304 public boolean isIconUniqueBlocking(RootInfo root) {
306 loadStoppedAuthorities();
307 synchronized (mLock) {
308 final int rootIcon = root.derivedIcon != 0 ? root.derivedIcon : root.icon;
309 for (RootInfo test : mRoots.get(root.authority)) {
310 if (Objects.equal(test.rootId, root.rootId)) {
313 final int testIcon = test.derivedIcon != 0 ? test.derivedIcon : test.icon;
314 if (testIcon == rootIcon) {
322 public RootInfo getRecentsRoot() {
326 public boolean isRecentsRoot(RootInfo root) {
327 return mRecentsRoot == root;
330 public Collection<RootInfo> getRootsBlocking() {
332 loadStoppedAuthorities();
333 synchronized (mLock) {
334 return mRoots.values();
338 public Collection<RootInfo> getMatchingRootsBlocking(State state) {
340 loadStoppedAuthorities();
341 synchronized (mLock) {
342 return getMatchingRoots(mRoots.values(), state);
347 static List<RootInfo> getMatchingRoots(Collection<RootInfo> roots, State state) {
348 final List<RootInfo> matching = Lists.newArrayList();
349 for (RootInfo root : roots) {
350 final boolean supportsCreate = (root.flags & Root.FLAG_SUPPORTS_CREATE) != 0;
351 final boolean advanced = (root.flags & Root.FLAG_ADVANCED) != 0;
352 final boolean localOnly = (root.flags & Root.FLAG_LOCAL_ONLY) != 0;
353 final boolean empty = (root.flags & Root.FLAG_EMPTY) != 0;
355 // Exclude read-only devices when creating
356 if (state.action == State.ACTION_CREATE && !supportsCreate) continue;
357 // Exclude advanced devices when not requested
358 if (!state.showAdvanced && advanced) continue;
359 // Exclude non-local devices when local only
360 if (state.localOnly && !localOnly) continue;
361 // Only show empty roots when creating
362 if (state.action != State.ACTION_CREATE && empty) continue;
364 // Only include roots that serve requested content
365 final boolean overlap =
366 MimePredicate.mimeMatches(root.derivedMimeTypes, state.acceptMimes) ||
367 MimePredicate.mimeMatches(state.acceptMimes, root.derivedMimeTypes);