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.Shared.DEBUG;
20 import static com.android.documentsui.base.Shared.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.putAll(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.putAll(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         synchronized (mObservedAuthoritiesDetails) {
244             if (!mObservedAuthoritiesDetails.containsKey(authority)) {
245                 ProviderInfo provider = mContext.getPackageManager().resolveContentProvider(
246                         authority, PackageManager.GET_META_DATA);
247                 PackageManager pm = mContext.getPackageManager();
248                 CharSequence appName = pm.getApplicationLabel(provider.applicationInfo);
249                 String packageName = provider.applicationInfo.packageName;
250 
251                 mObservedAuthoritiesDetails.put(
252                         authority, new PackageDetails(appName.toString(), packageName));
253 
254                 // Watch for any future updates
255                 final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
256                 mContext.getContentResolver().registerContentObserver(rootsUri, true, mObserver);
257             }
258         }
259 
260         final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
261         if (!forceRefresh) {
262             // Look for roots data that we might have cached for ourselves in the
263             // long-lived system process.
264             final Bundle systemCache = resolver.getCache(rootsUri);
265             if (systemCache != null) {
266                 ArrayList<RootInfo> cachedRoots = systemCache.getParcelableArrayList(TAG);
267                 assert(cachedRoots != null);
268                 if (!cachedRoots.isEmpty() || PERMIT_EMPTY_CACHE.contains(authority)) {
269                     if (VERBOSE) Log.v(TAG, "System cache hit for " + authority);
270                     return cachedRoots;
271                 } else {
272                     Log.w(TAG, "Ignoring empty system cache hit for " + authority);
273                 }
274             }
275         }
276 
277         final ArrayList<RootInfo> roots = new ArrayList<>();
278         ContentProviderClient client = null;
279         Cursor cursor = null;
280         try {
281             client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
282             cursor = client.query(rootsUri, null, null, null, null);
283             while (cursor.moveToNext()) {
284                 final RootInfo root = RootInfo.fromRootsCursor(authority, cursor);
285                 roots.add(root);
286             }
287         } catch (Exception e) {
288             Log.w(TAG, "Failed to load some roots from " + authority, e);
289             // We didn't load every root from the provider. Don't put it to
290             // system cache so that we'll try loading them again next time even
291             // if forceRefresh is false.
292             return roots;
293         } finally {
294             IoUtils.closeQuietly(cursor);
295             ContentProviderClient.releaseQuietly(client);
296         }
297 
298         // Cache these freshly parsed roots over in the long-lived system
299         // process, in case our process goes away. The system takes care of
300         // invalidating the cache if the package or Uri changes.
301         final Bundle systemCache = new Bundle();
302         if (roots.isEmpty() && !PERMIT_EMPTY_CACHE.contains(authority)) {
303             Log.i(TAG, "Provider returned no roots. Possibly naughty: " + authority);
304         } else {
305             systemCache.putParcelableArrayList(TAG, roots);
306             resolver.putCache(rootsUri, systemCache);
307         }
308 
309         return roots;
310     }
311 
312     @Override
getRootOneshot(String authority, String rootId)313     public RootInfo getRootOneshot(String authority, String rootId) {
314         return getRootOneshot(authority, rootId, false);
315     }
316 
getRootOneshot(String authority, String rootId, boolean forceRefresh)317     public RootInfo getRootOneshot(String authority, String rootId, boolean forceRefresh) {
318         synchronized (mLock) {
319             RootInfo root = forceRefresh ? null : getRootLocked(authority, rootId);
320             if (root == null) {
321                 mRoots.putAll(authority, loadRootsForAuthority(
322                                 mContext.getContentResolver(), authority, forceRefresh));
323                 root = getRootLocked(authority, rootId);
324             }
325             return root;
326         }
327     }
328 
getRootBlocking(String authority, String rootId)329     public RootInfo getRootBlocking(String authority, String rootId) {
330         waitForFirstLoad();
331         loadStoppedAuthorities();
332         synchronized (mLock) {
333             return getRootLocked(authority, rootId);
334         }
335     }
336 
getRootLocked(String authority, String rootId)337     private RootInfo getRootLocked(String authority, String rootId) {
338         for (RootInfo root : mRoots.get(authority)) {
339             if (Objects.equals(root.rootId, rootId)) {
340                 return root;
341             }
342         }
343         return null;
344     }
345 
346     @Override
getRecentsRoot()347     public RootInfo getRecentsRoot() {
348         return mRecentsRoot;
349     }
350 
isRecentsRoot(RootInfo root)351     public boolean isRecentsRoot(RootInfo root) {
352         return mRecentsRoot.equals(root);
353     }
354 
355     @Override
getRootsBlocking()356     public Collection<RootInfo> getRootsBlocking() {
357         waitForFirstLoad();
358         loadStoppedAuthorities();
359         synchronized (mLock) {
360             return mRoots.values();
361         }
362     }
363 
364     @Override
getMatchingRootsBlocking(State state)365     public Collection<RootInfo> getMatchingRootsBlocking(State state) {
366         waitForFirstLoad();
367         loadStoppedAuthorities();
368         synchronized (mLock) {
369             return ProvidersAccess.getMatchingRoots(mRoots.values(), state);
370         }
371     }
372 
373     @Override
getRootsForAuthorityBlocking(String authority)374     public Collection<RootInfo> getRootsForAuthorityBlocking(String authority) {
375         waitForFirstLoad();
376         loadStoppedAuthority(authority);
377         synchronized (mLock) {
378             final Collection<RootInfo> roots = mRoots.get(authority);
379             return roots != null ? roots : Collections.<RootInfo>emptyList();
380         }
381     }
382 
getDefaultRootBlocking(State state)383     public RootInfo getDefaultRootBlocking(State state) {
384         for (RootInfo root : ProvidersAccess.getMatchingRoots(getRootsBlocking(), state)) {
385             if (root.isDownloads()) {
386                 return root;
387             }
388         }
389         return mRecentsRoot;
390     }
391 
logCache()392     public void logCache() {
393         ContentResolver resolver = mContext.getContentResolver();
394         StringBuilder output = new StringBuilder();
395 
396         for (String authority : mObservedAuthoritiesDetails.keySet()) {
397             List<String> roots = new ArrayList<>();
398             Uri rootsUri = DocumentsContract.buildRootsUri(authority);
399             Bundle systemCache = resolver.getCache(rootsUri);
400             if (systemCache != null) {
401                 ArrayList<RootInfo> cachedRoots = systemCache.getParcelableArrayList(TAG);
402                 for (RootInfo root : cachedRoots) {
403                     roots.add(root.toDebugString());
404                 }
405             }
406 
407             output.append((output.length() == 0) ? "System cache: " : ", ");
408             output.append(authority).append("=").append(roots);
409         }
410 
411         Log.i(TAG, output.toString());
412     }
413 
414     private class UpdateTask extends AsyncTask<Void, Void, Void> {
415         private final boolean mForceRefreshAll;
416         private final String mForceRefreshPackage;
417 
418         private final Multimap<String, RootInfo> mTaskRoots = ArrayListMultimap.create();
419         private final HashSet<String> mTaskStoppedAuthorities = new HashSet<>();
420 
421         /**
422          * Create task to update roots cache.
423          *
424          * @param forceRefreshAll when true, all previously cached values for
425          *            all packages should be ignored.
426          * @param forceRefreshPackage when non-null, all previously cached
427          *            values for this specific package should be ignored.
428          */
UpdateTask(boolean forceRefreshAll, String forceRefreshPackage)429         public UpdateTask(boolean forceRefreshAll, String forceRefreshPackage) {
430             mForceRefreshAll = forceRefreshAll;
431             mForceRefreshPackage = forceRefreshPackage;
432         }
433 
434         @Override
doInBackground(Void... params)435         protected Void doInBackground(Void... params) {
436             final long start = SystemClock.elapsedRealtime();
437 
438             mTaskRoots.put(mRecentsRoot.authority, mRecentsRoot);
439 
440             final PackageManager pm = mContext.getPackageManager();
441 
442             // Pick up provider with action string
443             final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE);
444             final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, 0);
445             for (ResolveInfo info : providers) {
446                 handleDocumentsProvider(info.providerInfo);
447             }
448 
449             final long delta = SystemClock.elapsedRealtime() - start;
450             if (VERBOSE) Log.v(TAG,
451                     "Update found " + mTaskRoots.size() + " roots in " + delta + "ms");
452             synchronized (mLock) {
453                 mFirstLoadDone = true;
454                 if (mBootCompletedResult != null) {
455                     mBootCompletedResult.finish();
456                     mBootCompletedResult = null;
457                 }
458                 mRoots = mTaskRoots;
459                 mStoppedAuthorities = mTaskStoppedAuthorities;
460             }
461             mFirstLoad.countDown();
462             LocalBroadcastManager.getInstance(mContext).sendBroadcast(new Intent(BROADCAST_ACTION));
463             return null;
464         }
465 
handleDocumentsProvider(ProviderInfo info)466         private void handleDocumentsProvider(ProviderInfo info) {
467             // Ignore stopped packages for now; we might query them
468             // later during UI interaction.
469             if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) {
470                 if (VERBOSE) Log.v(TAG, "Ignoring stopped authority " + info.authority);
471                 mTaskStoppedAuthorities.add(info.authority);
472                 return;
473             }
474 
475             final boolean forceRefresh = mForceRefreshAll
476                     || Objects.equals(info.packageName, mForceRefreshPackage);
477             mTaskRoots.putAll(info.authority, loadRootsForAuthority(mContext.getContentResolver(),
478                     info.authority, forceRefresh));
479         }
480 
481     }
482 
483     private static class PackageDetails {
484         private String applicationName;
485         private String packageName;
486 
PackageDetails(String appName, String pckgName)487         public PackageDetails(String appName, String pckgName) {
488             applicationName = appName;
489             packageName = pckgName;
490         }
491     }
492 }
493