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