1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 
17 package com.android.documentsui;
18 
19 import static com.android.documentsui.DocumentsActivity.TAG;
20 
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;
38 
39 import com.android.documentsui.BaseActivity.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.google.android.collect.Lists;
44 import com.google.android.collect.Sets;
45 import com.google.common.collect.ArrayListMultimap;
46 import com.google.common.collect.Multimap;
47 
48 import libcore.io.IoUtils;
49 
50 import java.util.Collection;
51 import java.util.HashSet;
52 import java.util.List;
53 import java.util.Objects;
54 import java.util.concurrent.CountDownLatch;
55 import java.util.concurrent.TimeUnit;
56 
57 /**
58  * Cache of known storage backends and their roots.
59  */
60 public class RootsCache {
61     private static final boolean LOGD = false;
62 
63     public static final Uri sNotificationUri = Uri.parse(
64             "content://com.android.documentsui.roots/");
65 
66     private final Context mContext;
67     private final ContentObserver mObserver;
68 
69     private final RootInfo mRecentsRoot = new RootInfo();
70 
71     private final Object mLock = new Object();
72     private final CountDownLatch mFirstLoad = new CountDownLatch(1);
73 
74     @GuardedBy("mLock")
75     private Multimap<String, RootInfo> mRoots = ArrayListMultimap.create();
76     @GuardedBy("mLock")
77     private HashSet<String> mStoppedAuthorities = Sets.newHashSet();
78 
79     @GuardedBy("mObservedAuthorities")
80     private final HashSet<String> mObservedAuthorities = Sets.newHashSet();
81 
RootsCache(Context context)82     public RootsCache(Context context) {
83         mContext = context;
84         mObserver = new RootsChangedObserver();
85     }
86 
87     private class RootsChangedObserver extends ContentObserver {
RootsChangedObserver()88         public RootsChangedObserver() {
89             super(new Handler());
90         }
91 
92         @Override
onChange(boolean selfChange, Uri uri)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());
96         }
97     }
98 
99     /**
100      * Gather roots from all known storage providers.
101      */
updateAsync()102     public void updateAsync() {
103         // Special root for recents
104         mRecentsRoot.authority = null;
105         mRecentsRoot.rootId = null;
106         mRecentsRoot.derivedIcon = R.drawable.ic_root_recent;
107         mRecentsRoot.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE
108                 | Root.FLAG_SUPPORTS_IS_CHILD;
109         mRecentsRoot.title = mContext.getString(R.string.root_recent);
110         mRecentsRoot.availableBytes = -1;
111 
112         new UpdateTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
113     }
114 
115     /**
116      * Gather roots from storage providers belonging to given package name.
117      */
updatePackageAsync(String packageName)118     public void updatePackageAsync(String packageName) {
119         new UpdateTask(packageName).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
120     }
121 
122     /**
123      * Gather roots from storage providers belonging to given authority.
124      */
updateAuthorityAsync(String authority)125     public void updateAuthorityAsync(String authority) {
126         final ProviderInfo info = mContext.getPackageManager().resolveContentProvider(authority, 0);
127         if (info != null) {
128             updatePackageAsync(info.packageName);
129         }
130     }
131 
waitForFirstLoad()132     private void waitForFirstLoad() {
133         boolean success = false;
134         try {
135             success = mFirstLoad.await(15, TimeUnit.SECONDS);
136         } catch (InterruptedException e) {
137         }
138         if (!success) {
139             Log.w(TAG, "Timeout waiting for first update");
140         }
141     }
142 
143     /**
144      * Load roots from authorities that are in stopped state. Normal
145      * {@link UpdateTask} passes ignore stopped applications.
146      */
loadStoppedAuthorities()147     private void loadStoppedAuthorities() {
148         final ContentResolver resolver = mContext.getContentResolver();
149         synchronized (mLock) {
150             for (String authority : mStoppedAuthorities) {
151                 if (LOGD) Log.d(TAG, "Loading stopped authority " + authority);
152                 mRoots.putAll(authority, loadRootsForAuthority(resolver, authority));
153             }
154             mStoppedAuthorities.clear();
155         }
156     }
157 
158     private class UpdateTask extends AsyncTask<Void, Void, Void> {
159         private final String mFilterPackage;
160 
161         private final Multimap<String, RootInfo> mTaskRoots = ArrayListMultimap.create();
162         private final HashSet<String> mTaskStoppedAuthorities = Sets.newHashSet();
163 
164         /**
165          * Update all roots.
166          */
UpdateTask()167         public UpdateTask() {
168             this(null);
169         }
170 
171         /**
172          * Only update roots belonging to given package name. Other roots will
173          * be copied from cached {@link #mRoots} values.
174          */
UpdateTask(String filterPackage)175         public UpdateTask(String filterPackage) {
176             mFilterPackage = filterPackage;
177         }
178 
179         @Override
doInBackground(Void... params)180         protected Void doInBackground(Void... params) {
181             final long start = SystemClock.elapsedRealtime();
182 
183             if (mFilterPackage != null) {
184                 // Need at least first load, since we're going to be using
185                 // previously cached values for non-matching packages.
186                 waitForFirstLoad();
187             }
188 
189             mTaskRoots.put(mRecentsRoot.authority, mRecentsRoot);
190 
191             final ContentResolver resolver = mContext.getContentResolver();
192             final PackageManager pm = mContext.getPackageManager();
193 
194             // Pick up provider with action string
195             final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE);
196             final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, 0);
197             for (ResolveInfo info : providers) {
198                 handleDocumentsProvider(info.providerInfo);
199             }
200 
201             final long delta = SystemClock.elapsedRealtime() - start;
202             Log.d(TAG, "Update found " + mTaskRoots.size() + " roots in " + delta + "ms");
203             synchronized (mLock) {
204                 mRoots = mTaskRoots;
205                 mStoppedAuthorities = mTaskStoppedAuthorities;
206             }
207             mFirstLoad.countDown();
208             resolver.notifyChange(sNotificationUri, null, false);
209             return null;
210         }
211 
handleDocumentsProvider(ProviderInfo info)212         private void handleDocumentsProvider(ProviderInfo info) {
213             // Ignore stopped packages for now; we might query them
214             // later during UI interaction.
215             if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) {
216                 if (LOGD) Log.d(TAG, "Ignoring stopped authority " + info.authority);
217                 mTaskStoppedAuthorities.add(info.authority);
218                 return;
219             }
220 
221             // Try using cached roots if filtering
222             boolean cacheHit = false;
223             if (mFilterPackage != null && !mFilterPackage.equals(info.packageName)) {
224                 synchronized (mLock) {
225                     if (mTaskRoots.putAll(info.authority, mRoots.get(info.authority))) {
226                         if (LOGD) Log.d(TAG, "Used cached roots for " + info.authority);
227                         cacheHit = true;
228                     }
229                 }
230             }
231 
232             // Cache miss, or loading everything
233             if (!cacheHit) {
234                 mTaskRoots.putAll(info.authority,
235                         loadRootsForAuthority(mContext.getContentResolver(), info.authority));
236             }
237         }
238     }
239 
240     /**
241      * Bring up requested provider and query for all active roots.
242      */
loadRootsForAuthority(ContentResolver resolver, String authority)243     private Collection<RootInfo> loadRootsForAuthority(ContentResolver resolver, String authority) {
244         if (LOGD) Log.d(TAG, "Loading roots for " + authority);
245 
246         synchronized (mObservedAuthorities) {
247             if (mObservedAuthorities.add(authority)) {
248                 // Watch for any future updates
249                 final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
250                 mContext.getContentResolver().registerContentObserver(rootsUri, true, mObserver);
251             }
252         }
253 
254         final List<RootInfo> roots = Lists.newArrayList();
255         final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
256 
257         ContentProviderClient client = null;
258         Cursor cursor = null;
259         try {
260             client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
261             cursor = client.query(rootsUri, null, null, null, null);
262             while (cursor.moveToNext()) {
263                 final RootInfo root = RootInfo.fromRootsCursor(authority, cursor);
264                 roots.add(root);
265             }
266         } catch (Exception e) {
267             Log.w(TAG, "Failed to load some roots from " + authority + ": " + e);
268         } finally {
269             IoUtils.closeQuietly(cursor);
270             ContentProviderClient.releaseQuietly(client);
271         }
272         return roots;
273     }
274 
275     /**
276      * Return the requested {@link RootInfo}, but only loading the roots for the
277      * requested authority. This is useful when we want to load fast without
278      * waiting for all the other roots to come back.
279      */
getRootOneshot(String authority, String rootId)280     public RootInfo getRootOneshot(String authority, String rootId) {
281         synchronized (mLock) {
282             RootInfo root = getRootLocked(authority, rootId);
283             if (root == null) {
284                 mRoots.putAll(
285                         authority, loadRootsForAuthority(mContext.getContentResolver(), authority));
286                 root = getRootLocked(authority, rootId);
287             }
288             return root;
289         }
290     }
291 
getRootBlocking(String authority, String rootId)292     public RootInfo getRootBlocking(String authority, String rootId) {
293         waitForFirstLoad();
294         loadStoppedAuthorities();
295         synchronized (mLock) {
296             return getRootLocked(authority, rootId);
297         }
298     }
299 
getRootLocked(String authority, String rootId)300     private RootInfo getRootLocked(String authority, String rootId) {
301         for (RootInfo root : mRoots.get(authority)) {
302             if (Objects.equals(root.rootId, rootId)) {
303                 return root;
304             }
305         }
306         return null;
307     }
308 
isIconUniqueBlocking(RootInfo root)309     public boolean isIconUniqueBlocking(RootInfo root) {
310         waitForFirstLoad();
311         loadStoppedAuthorities();
312         synchronized (mLock) {
313             final int rootIcon = root.derivedIcon != 0 ? root.derivedIcon : root.icon;
314             for (RootInfo test : mRoots.get(root.authority)) {
315                 if (Objects.equals(test.rootId, root.rootId)) {
316                     continue;
317                 }
318                 final int testIcon = test.derivedIcon != 0 ? test.derivedIcon : test.icon;
319                 if (testIcon == rootIcon) {
320                     return false;
321                 }
322             }
323             return true;
324         }
325     }
326 
getRecentsRoot()327     public RootInfo getRecentsRoot() {
328         return mRecentsRoot;
329     }
330 
isRecentsRoot(RootInfo root)331     public boolean isRecentsRoot(RootInfo root) {
332         return mRecentsRoot == root;
333     }
334 
getRootsBlocking()335     public Collection<RootInfo> getRootsBlocking() {
336         waitForFirstLoad();
337         loadStoppedAuthorities();
338         synchronized (mLock) {
339             return mRoots.values();
340         }
341     }
342 
getMatchingRootsBlocking(State state)343     public Collection<RootInfo> getMatchingRootsBlocking(State state) {
344         waitForFirstLoad();
345         loadStoppedAuthorities();
346         synchronized (mLock) {
347             return getMatchingRoots(mRoots.values(), state);
348         }
349     }
350 
351     @VisibleForTesting
getMatchingRoots(Collection<RootInfo> roots, State state)352     static List<RootInfo> getMatchingRoots(Collection<RootInfo> roots, State state) {
353         final List<RootInfo> matching = Lists.newArrayList();
354         for (RootInfo root : roots) {
355             final boolean supportsCreate = (root.flags & Root.FLAG_SUPPORTS_CREATE) != 0;
356             final boolean supportsIsChild = (root.flags & Root.FLAG_SUPPORTS_IS_CHILD) != 0;
357             final boolean advanced = (root.flags & Root.FLAG_ADVANCED) != 0;
358             final boolean localOnly = (root.flags & Root.FLAG_LOCAL_ONLY) != 0;
359             final boolean empty = (root.flags & Root.FLAG_EMPTY) != 0;
360 
361             // Exclude read-only devices when creating
362             if (state.action == State.ACTION_CREATE && !supportsCreate) continue;
363             if (state.action == State.ACTION_OPEN_COPY_DESTINATION && !supportsCreate) continue;
364             // Exclude roots that don't support directory picking
365             if (state.action == State.ACTION_OPEN_TREE && !supportsIsChild) continue;
366             // Exclude advanced devices when not requested
367             if (!state.showAdvanced && advanced) continue;
368             // Exclude non-local devices when local only
369             if (state.localOnly && !localOnly) continue;
370             // Exclude downloads roots that don't support directory creation
371             // TODO: Add flag to check the root supports directory creation or not.
372             if (state.directoryCopy && root.isDownloads()) continue;
373             // Only show empty roots when creating
374             if ((state.action != State.ACTION_CREATE ||
375                  state.action != State.ACTION_OPEN_TREE ||
376                  state.action != State.ACTION_OPEN_COPY_DESTINATION) && empty) continue;
377 
378             // Only include roots that serve requested content
379             final boolean overlap =
380                     MimePredicate.mimeMatches(root.derivedMimeTypes, state.acceptMimes) ||
381                     MimePredicate.mimeMatches(state.acceptMimes, root.derivedMimeTypes);
382             if (!overlap) {
383                 continue;
384             }
385 
386             // Exclude roots from the calling package.
387             if (state.excludedAuthorities.contains(root.authority)) {
388                 if (LOGD) Log.d(TAG, "Excluding root " + root.authority + " from calling package.");
389                 continue;
390             }
391 
392             matching.add(root);
393         }
394         return matching;
395     }
396 }
397