Home | History | Annotate | Download | only in roots
      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 
    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 {
    120         public RootsChangedObserver() {
    121             super(new Handler());
    122         }
    123 
    124         @Override
    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
    136     public String getApplicationName(String authority) {
    137         return mObservedAuthoritiesDetails.get(authority).applicationName;
    138     }
    139 
    140     @Override
    141     public String getPackageName(String authority) {
    142         return mObservedAuthoritiesDetails.get(authority).packageName;
    143     }
    144 
    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 
    164     public void updatePackageAsync(String packageName) {
    165         new UpdateTask(false, packageName).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    166     }
    167 
    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 
    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      */
    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      */
    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      */
    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      */
    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
    333     public RootInfo getRootOneshot(String authority, String rootId) {
    334         return getRootOneshot(authority, rootId, false);
    335     }
    336 
    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 
    349     public RootInfo getRootBlocking(String authority, String rootId) {
    350         waitForFirstLoad();
    351         loadStoppedAuthorities();
    352         synchronized (mLock) {
    353             return getRootLocked(authority, rootId);
    354         }
    355     }
    356 
    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
    367     public RootInfo getRecentsRoot() {
    368         return mRecentsRoot;
    369     }
    370 
    371     public boolean isRecentsRoot(RootInfo root) {
    372         return mRecentsRoot.equals(root);
    373     }
    374 
    375     @Override
    376     public Collection<RootInfo> getRootsBlocking() {
    377         waitForFirstLoad();
    378         loadStoppedAuthorities();
    379         synchronized (mLock) {
    380             return mRoots.values();
    381         }
    382     }
    383 
    384     @Override
    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
    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
    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 
    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          */
    450         public UpdateTask(boolean forceRefreshAll, String forceRefreshPackage) {
    451             mForceRefreshAll = forceRefreshAll;
    452             mForceRefreshPackage = forceRefreshPackage;
    453         }
    454 
    455         @Override
    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 
    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 
    511         public PackageDetails(String appName, String pckgName) {
    512             applicationName = appName;
    513             packageName = pckgName;
    514         }
    515     }
    516 }
    517