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.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 
    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.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      */
    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      */
    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
    313     public RootInfo getRootOneshot(String authority, String rootId) {
    314         return getRootOneshot(authority, rootId, false);
    315     }
    316 
    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 
    329     public RootInfo getRootBlocking(String authority, String rootId) {
    330         waitForFirstLoad();
    331         loadStoppedAuthorities();
    332         synchronized (mLock) {
    333             return getRootLocked(authority, rootId);
    334         }
    335     }
    336 
    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
    347     public RootInfo getRecentsRoot() {
    348         return mRecentsRoot;
    349     }
    350 
    351     public boolean isRecentsRoot(RootInfo root) {
    352         return mRecentsRoot.equals(root);
    353     }
    354 
    355     @Override
    356     public Collection<RootInfo> getRootsBlocking() {
    357         waitForFirstLoad();
    358         loadStoppedAuthorities();
    359         synchronized (mLock) {
    360             return mRoots.values();
    361         }
    362     }
    363 
    364     @Override
    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
    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 
    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 
    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          */
    429         public UpdateTask(boolean forceRefreshAll, String forceRefreshPackage) {
    430             mForceRefreshAll = forceRefreshAll;
    431             mForceRefreshPackage = forceRefreshPackage;
    432         }
    433 
    434         @Override
    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 
    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 
    487         public PackageDetails(String appName, String pckgName) {
    488             applicationName = appName;
    489             packageName = pckgName;
    490         }
    491     }
    492 }
    493