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