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.Shared.DEBUG;
20 
21 import android.content.BroadcastReceiver.PendingResult;
22 import android.content.ContentProviderClient;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.pm.ApplicationInfo;
27 import android.content.pm.PackageManager;
28 import android.content.pm.ProviderInfo;
29 import android.content.pm.ResolveInfo;
30 import android.database.ContentObserver;
31 import android.database.Cursor;
32 import android.net.Uri;
33 import android.os.AsyncTask;
34 import android.os.Bundle;
35 import android.os.Handler;
36 import android.os.SystemClock;
37 import android.provider.DocumentsContract;
38 import android.provider.DocumentsContract.Root;
39 import android.support.annotation.VisibleForTesting;
40 import android.util.Log;
41 
42 import com.android.documentsui.model.RootInfo;
43 import com.android.internal.annotations.GuardedBy;
44 
45 import libcore.io.IoUtils;
46 
47 import com.google.common.collect.ArrayListMultimap;
48 import com.google.common.collect.Multimap;
49 
50 import java.util.ArrayList;
51 import java.util.Collection;
52 import java.util.Collections;
53 import java.util.HashSet;
54 import java.util.List;
55 import java.util.Objects;
56 import java.util.concurrent.CountDownLatch;
57 import java.util.concurrent.TimeUnit;
58 
59 /**
60  * Cache of known storage backends and their roots.
61  */
62 public class RootsCache {
63     public static final Uri sNotificationUri = Uri.parse(
64             "content://com.android.documentsui.roots/");
65 
66     private static final String TAG = "RootsCache";
67 
68     private final Context mContext;
69     private final ContentObserver mObserver;
70 
71     private final RootInfo mRecentsRoot;
72 
73     private final Object mLock = new Object();
74     private final CountDownLatch mFirstLoad = new CountDownLatch(1);
75 
76     @GuardedBy("mLock")
77     private boolean mFirstLoadDone;
78     @GuardedBy("mLock")
79     private PendingResult mBootCompletedResult;
80 
81     @GuardedBy("mLock")
82     private Multimap<String, RootInfo> mRoots = ArrayListMultimap.create();
83     @GuardedBy("mLock")
84     private HashSet<String> mStoppedAuthorities = new HashSet<>();
85 
86     @GuardedBy("mObservedAuthorities")
87     private final HashSet<String> mObservedAuthorities = new HashSet<>();
88 
RootsCache(Context context)89     public RootsCache(Context context) {
90         mContext = context;
91         mObserver = new RootsChangedObserver();
92 
93         // Create a new anonymous "Recents" RootInfo. It's a faker.
94         mRecentsRoot = new RootInfo() {{
95                 // Special root for recents
96                 derivedIcon = R.drawable.ic_root_recent;
97                 derivedType = RootInfo.TYPE_RECENTS;
98                 flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD
99                         | Root.FLAG_SUPPORTS_CREATE;
100                 title = mContext.getString(R.string.root_recent);
101                 availableBytes = -1;
102             }};
103     }
104 
105     private class RootsChangedObserver extends ContentObserver {
RootsChangedObserver()106         public RootsChangedObserver() {
107             super(new Handler());
108         }
109 
110         @Override
onChange(boolean selfChange, Uri uri)111         public void onChange(boolean selfChange, Uri uri) {
112             if (uri == null) {
113                 Log.w(TAG, "Received onChange event for null uri. Skipping.");
114                 return;
115             }
116             if (DEBUG) Log.d(TAG, "Updating roots due to change at " + uri);
117             updateAuthorityAsync(uri.getAuthority());
118         }
119     }
120 
121     /**
122      * Gather roots from all known storage providers.
123      */
updateAsync(boolean forceRefreshAll)124     public void updateAsync(boolean forceRefreshAll) {
125 
126         // NOTE: This method is called when the UI language changes.
127         // For that reason we update our RecentsRoot to reflect
128         // the current language.
129         mRecentsRoot.title = mContext.getString(R.string.root_recent);
130 
131         // Nothing else about the root should ever change.
132         assert(mRecentsRoot.authority == null);
133         assert(mRecentsRoot.rootId == null);
134         assert(mRecentsRoot.derivedIcon == R.drawable.ic_root_recent);
135         assert(mRecentsRoot.derivedType == RootInfo.TYPE_RECENTS);
136         assert(mRecentsRoot.flags == (Root.FLAG_LOCAL_ONLY
137                 | Root.FLAG_SUPPORTS_IS_CHILD
138                 | Root.FLAG_SUPPORTS_CREATE));
139         assert(mRecentsRoot.availableBytes == -1);
140 
141         new UpdateTask(forceRefreshAll, null)
142                 .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
143     }
144 
145     /**
146      * Gather roots from storage providers belonging to given package name.
147      */
updatePackageAsync(String packageName)148     public void updatePackageAsync(String packageName) {
149         new UpdateTask(false, packageName).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
150     }
151 
152     /**
153      * Gather roots from storage providers belonging to given authority.
154      */
updateAuthorityAsync(String authority)155     public void updateAuthorityAsync(String authority) {
156         final ProviderInfo info = mContext.getPackageManager().resolveContentProvider(authority, 0);
157         if (info != null) {
158             updatePackageAsync(info.packageName);
159         }
160     }
161 
setBootCompletedResult(PendingResult result)162     public void setBootCompletedResult(PendingResult result) {
163         synchronized (mLock) {
164             // Quickly check if we've already finished loading, otherwise hang
165             // out until first pass is finished.
166             if (mFirstLoadDone) {
167                 result.finish();
168             } else {
169                 mBootCompletedResult = result;
170             }
171         }
172     }
173 
174     /**
175      * Block until the first {@link UpdateTask} pass has finished.
176      *
177      * @return {@code true} if cached roots is ready to roll, otherwise
178      *         {@code false} if we timed out while waiting.
179      */
waitForFirstLoad()180     private boolean waitForFirstLoad() {
181         boolean success = false;
182         try {
183             success = mFirstLoad.await(15, TimeUnit.SECONDS);
184         } catch (InterruptedException e) {
185         }
186         if (!success) {
187             Log.w(TAG, "Timeout waiting for first update");
188         }
189         return success;
190     }
191 
192     /**
193      * Load roots from authorities that are in stopped state. Normal
194      * {@link UpdateTask} passes ignore stopped applications.
195      */
loadStoppedAuthorities()196     private void loadStoppedAuthorities() {
197         final ContentResolver resolver = mContext.getContentResolver();
198         synchronized (mLock) {
199             for (String authority : mStoppedAuthorities) {
200                 if (DEBUG) Log.d(TAG, "Loading stopped authority " + authority);
201                 mRoots.putAll(authority, loadRootsForAuthority(resolver, authority, true));
202             }
203             mStoppedAuthorities.clear();
204         }
205     }
206 
207     /**
208      * Load roots from a stopped authority. Normal {@link UpdateTask} passes
209      * ignore stopped applications.
210      */
loadStoppedAuthority(String authority)211     private void loadStoppedAuthority(String authority) {
212         final ContentResolver resolver = mContext.getContentResolver();
213         synchronized (mLock) {
214             if (!mStoppedAuthorities.contains(authority)) {
215                 return;
216             }
217             if (DEBUG) {
218                 Log.d(TAG, "Loading stopped authority " + authority);
219             }
220             mRoots.putAll(authority, loadRootsForAuthority(resolver, authority, true));
221             mStoppedAuthorities.remove(authority);
222         }
223     }
224 
225     private class UpdateTask extends AsyncTask<Void, Void, Void> {
226         private final boolean mForceRefreshAll;
227         private final String mForceRefreshPackage;
228 
229         private final Multimap<String, RootInfo> mTaskRoots = ArrayListMultimap.create();
230         private final HashSet<String> mTaskStoppedAuthorities = new HashSet<>();
231 
232         /**
233          * Create task to update roots cache.
234          *
235          * @param forceRefreshAll when true, all previously cached values for
236          *            all packages should be ignored.
237          * @param forceRefreshPackage when non-null, all previously cached
238          *            values for this specific package should be ignored.
239          */
UpdateTask(boolean forceRefreshAll, String forceRefreshPackage)240         public UpdateTask(boolean forceRefreshAll, String forceRefreshPackage) {
241             mForceRefreshAll = forceRefreshAll;
242             mForceRefreshPackage = forceRefreshPackage;
243         }
244 
245         @Override
doInBackground(Void... params)246         protected Void doInBackground(Void... params) {
247             final long start = SystemClock.elapsedRealtime();
248 
249             mTaskRoots.put(mRecentsRoot.authority, mRecentsRoot);
250 
251             final ContentResolver resolver = mContext.getContentResolver();
252             final PackageManager pm = mContext.getPackageManager();
253 
254             // Pick up provider with action string
255             final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE);
256             final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, 0);
257             for (ResolveInfo info : providers) {
258                 handleDocumentsProvider(info.providerInfo);
259             }
260 
261             final long delta = SystemClock.elapsedRealtime() - start;
262             if (DEBUG)
263                 Log.d(TAG, "Update found " + mTaskRoots.size() + " roots in " + delta + "ms");
264             synchronized (mLock) {
265                 mFirstLoadDone = true;
266                 if (mBootCompletedResult != null) {
267                     mBootCompletedResult.finish();
268                     mBootCompletedResult = null;
269                 }
270                 mRoots = mTaskRoots;
271                 mStoppedAuthorities = mTaskStoppedAuthorities;
272             }
273             mFirstLoad.countDown();
274             resolver.notifyChange(sNotificationUri, null, false);
275             return null;
276         }
277 
handleDocumentsProvider(ProviderInfo info)278         private void handleDocumentsProvider(ProviderInfo info) {
279             // Ignore stopped packages for now; we might query them
280             // later during UI interaction.
281             if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) {
282                 if (DEBUG) Log.d(TAG, "Ignoring stopped authority " + info.authority);
283                 mTaskStoppedAuthorities.add(info.authority);
284                 return;
285             }
286 
287             final boolean forceRefresh = mForceRefreshAll
288                     || Objects.equals(info.packageName, mForceRefreshPackage);
289             mTaskRoots.putAll(info.authority, loadRootsForAuthority(mContext.getContentResolver(),
290                     info.authority, forceRefresh));
291         }
292     }
293 
294     /**
295      * Bring up requested provider and query for all active roots.
296      */
loadRootsForAuthority(ContentResolver resolver, String authority, boolean forceRefresh)297     private Collection<RootInfo> loadRootsForAuthority(ContentResolver resolver, String authority,
298             boolean forceRefresh) {
299         if (DEBUG) Log.d(TAG, "Loading roots for " + authority);
300 
301         synchronized (mObservedAuthorities) {
302             if (mObservedAuthorities.add(authority)) {
303                 // Watch for any future updates
304                 final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
305                 mContext.getContentResolver().registerContentObserver(rootsUri, true, mObserver);
306             }
307         }
308 
309         final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
310         if (!forceRefresh) {
311             // Look for roots data that we might have cached for ourselves in the
312             // long-lived system process.
313             final Bundle systemCache = resolver.getCache(rootsUri);
314             if (systemCache != null) {
315                 if (DEBUG) Log.d(TAG, "System cache hit for " + authority);
316                 return systemCache.getParcelableArrayList(TAG);
317             }
318         }
319 
320         final ArrayList<RootInfo> roots = new ArrayList<>();
321         ContentProviderClient client = null;
322         Cursor cursor = null;
323         try {
324             client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
325             cursor = client.query(rootsUri, null, null, null, null);
326             while (cursor.moveToNext()) {
327                 final RootInfo root = RootInfo.fromRootsCursor(authority, cursor);
328                 roots.add(root);
329             }
330         } catch (Exception e) {
331             Log.w(TAG, "Failed to load some roots from " + authority + ": " + e);
332         } finally {
333             IoUtils.closeQuietly(cursor);
334             ContentProviderClient.releaseQuietly(client);
335         }
336 
337         // Cache these freshly parsed roots over in the long-lived system
338         // process, in case our process goes away. The system takes care of
339         // invalidating the cache if the package or Uri changes.
340         final Bundle systemCache = new Bundle();
341         systemCache.putParcelableArrayList(TAG, roots);
342         resolver.putCache(rootsUri, systemCache);
343 
344         return roots;
345     }
346 
347     /**
348      * Return the requested {@link RootInfo}, but only loading the roots for the
349      * requested authority. This is useful when we want to load fast without
350      * waiting for all the other roots to come back.
351      */
getRootOneshot(String authority, String rootId)352     public RootInfo getRootOneshot(String authority, String rootId) {
353         synchronized (mLock) {
354             RootInfo root = getRootLocked(authority, rootId);
355             if (root == null) {
356                 mRoots.putAll(authority,
357                         loadRootsForAuthority(mContext.getContentResolver(), authority, false));
358                 root = getRootLocked(authority, rootId);
359             }
360             return root;
361         }
362     }
363 
getRootBlocking(String authority, String rootId)364     public RootInfo getRootBlocking(String authority, String rootId) {
365         waitForFirstLoad();
366         loadStoppedAuthorities();
367         synchronized (mLock) {
368             return getRootLocked(authority, rootId);
369         }
370     }
371 
getRootLocked(String authority, String rootId)372     private RootInfo getRootLocked(String authority, String rootId) {
373         for (RootInfo root : mRoots.get(authority)) {
374             if (Objects.equals(root.rootId, rootId)) {
375                 return root;
376             }
377         }
378         return null;
379     }
380 
isIconUniqueBlocking(RootInfo root)381     public boolean isIconUniqueBlocking(RootInfo root) {
382         waitForFirstLoad();
383         loadStoppedAuthorities();
384         synchronized (mLock) {
385             final int rootIcon = root.derivedIcon != 0 ? root.derivedIcon : root.icon;
386             for (RootInfo test : mRoots.get(root.authority)) {
387                 if (Objects.equals(test.rootId, root.rootId)) {
388                     continue;
389                 }
390                 final int testIcon = test.derivedIcon != 0 ? test.derivedIcon : test.icon;
391                 if (testIcon == rootIcon) {
392                     return false;
393                 }
394             }
395             return true;
396         }
397     }
398 
getRecentsRoot()399     public RootInfo getRecentsRoot() {
400         return mRecentsRoot;
401     }
402 
isRecentsRoot(RootInfo root)403     public boolean isRecentsRoot(RootInfo root) {
404         return mRecentsRoot.equals(root);
405     }
406 
getRootsBlocking()407     public Collection<RootInfo> getRootsBlocking() {
408         waitForFirstLoad();
409         loadStoppedAuthorities();
410         synchronized (mLock) {
411             return mRoots.values();
412         }
413     }
414 
getMatchingRootsBlocking(State state)415     public Collection<RootInfo> getMatchingRootsBlocking(State state) {
416         waitForFirstLoad();
417         loadStoppedAuthorities();
418         synchronized (mLock) {
419             return getMatchingRoots(mRoots.values(), state);
420         }
421     }
422 
423     /**
424      * Returns a list of roots for the specified authority. If not found, then
425      * an empty list is returned.
426      */
getRootsForAuthorityBlocking(String authority)427     public Collection<RootInfo> getRootsForAuthorityBlocking(String authority) {
428         waitForFirstLoad();
429         loadStoppedAuthority(authority);
430         synchronized (mLock) {
431             final Collection<RootInfo> roots = mRoots.get(authority);
432             return roots != null ? roots : Collections.<RootInfo>emptyList();
433         }
434     }
435 
436     /**
437      * Returns the default root for the specified state.
438      */
getDefaultRootBlocking(State state)439     public RootInfo getDefaultRootBlocking(State state) {
440         for (RootInfo root : getMatchingRoots(getRootsBlocking(), state)) {
441             if (root.isDownloads()) {
442                 return root;
443             }
444         }
445         return mRecentsRoot;
446     }
447 
448     @VisibleForTesting
getMatchingRoots(Collection<RootInfo> roots, State state)449     static List<RootInfo> getMatchingRoots(Collection<RootInfo> roots, State state) {
450         final List<RootInfo> matching = new ArrayList<>();
451         for (RootInfo root : roots) {
452 
453             if (DEBUG) Log.d(TAG, "Evaluating " + root);
454 
455             if (state.action == State.ACTION_CREATE && !root.supportsCreate()) {
456                 if (DEBUG) Log.d(TAG, "Excluding read-only root because: ACTION_CREATE.");
457                 continue;
458             }
459 
460             if (state.action == State.ACTION_PICK_COPY_DESTINATION
461                     && !root.supportsCreate()) {
462                 if (DEBUG) Log.d(
463                         TAG, "Excluding read-only root because: ACTION_PICK_COPY_DESTINATION.");
464                 continue;
465             }
466 
467             if (state.action == State.ACTION_OPEN_TREE && !root.supportsChildren()) {
468                 if (DEBUG) Log.d(
469                         TAG, "Excluding root !supportsChildren because: ACTION_OPEN_TREE.");
470                 continue;
471             }
472 
473             if (!state.showAdvanced && root.isAdvanced()) {
474                 if (DEBUG) Log.d(TAG, "Excluding root because: unwanted advanced device.");
475                 continue;
476             }
477 
478             if (state.localOnly && !root.isLocalOnly()) {
479                 if (DEBUG) Log.d(TAG, "Excluding root because: unwanted non-local device.");
480                 continue;
481             }
482 
483             if (state.directoryCopy && root.isDownloads()) {
484                 if (DEBUG) Log.d(
485                         TAG, "Excluding downloads root because: unsupported directory copy.");
486                 continue;
487             }
488 
489             if (state.action == State.ACTION_OPEN && root.isEmpty()) {
490                 if (DEBUG) Log.d(TAG, "Excluding empty root because: ACTION_OPEN.");
491                 continue;
492             }
493 
494             if (state.action == State.ACTION_GET_CONTENT && root.isEmpty()) {
495                 if (DEBUG) Log.d(TAG, "Excluding empty root because: ACTION_GET_CONTENT.");
496                 continue;
497             }
498 
499             final boolean overlap =
500                     MimePredicate.mimeMatches(root.derivedMimeTypes, state.acceptMimes) ||
501                     MimePredicate.mimeMatches(state.acceptMimes, root.derivedMimeTypes);
502             if (!overlap) {
503                 if (DEBUG) Log.d(
504                         TAG, "Excluding root because: unsupported content types > "
505                         + state.acceptMimes);
506                 continue;
507             }
508 
509             if (state.excludedAuthorities.contains(root.authority)) {
510                 if (DEBUG) Log.d(TAG, "Excluding root because: owned by calling package.");
511                 continue;
512             }
513 
514             if (DEBUG) Log.d(TAG, "Including " + root);
515             matching.add(root);
516         }
517         return matching;
518     }
519 }
520