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