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