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.DocumentsActivity.TAG;
     20 
     21 import android.content.ContentProviderClient;
     22 import android.content.ContentResolver;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.pm.ApplicationInfo;
     26 import android.content.pm.PackageManager;
     27 import android.content.pm.ProviderInfo;
     28 import android.content.pm.ResolveInfo;
     29 import android.database.ContentObserver;
     30 import android.database.Cursor;
     31 import android.net.Uri;
     32 import android.os.AsyncTask;
     33 import android.os.Handler;
     34 import android.os.SystemClock;
     35 import android.provider.DocumentsContract;
     36 import android.provider.DocumentsContract.Root;
     37 import android.util.Log;
     38 
     39 import com.android.documentsui.DocumentsActivity.State;
     40 import com.android.documentsui.model.RootInfo;
     41 import com.android.internal.annotations.GuardedBy;
     42 import com.android.internal.annotations.VisibleForTesting;
     43 import com.google.android.collect.Lists;
     44 import com.google.android.collect.Sets;
     45 import com.google.common.collect.ArrayListMultimap;
     46 import com.google.common.collect.Multimap;
     47 
     48 import libcore.io.IoUtils;
     49 
     50 import java.util.Collection;
     51 import java.util.HashSet;
     52 import java.util.List;
     53 import java.util.Objects;
     54 import java.util.concurrent.CountDownLatch;
     55 import java.util.concurrent.TimeUnit;
     56 
     57 /**
     58  * Cache of known storage backends and their roots.
     59  */
     60 public class RootsCache {
     61     private static final boolean LOGD = false;
     62 
     63     public static final Uri sNotificationUri = Uri.parse(
     64             "content://com.android.documentsui.roots/");
     65 
     66     private final Context mContext;
     67     private final ContentObserver mObserver;
     68 
     69     private final RootInfo mRecentsRoot = new RootInfo();
     70 
     71     private final Object mLock = new Object();
     72     private final CountDownLatch mFirstLoad = new CountDownLatch(1);
     73 
     74     @GuardedBy("mLock")
     75     private Multimap<String, RootInfo> mRoots = ArrayListMultimap.create();
     76     @GuardedBy("mLock")
     77     private HashSet<String> mStoppedAuthorities = Sets.newHashSet();
     78 
     79     @GuardedBy("mObservedAuthorities")
     80     private final HashSet<String> mObservedAuthorities = Sets.newHashSet();
     81 
     82     public RootsCache(Context context) {
     83         mContext = context;
     84         mObserver = new RootsChangedObserver();
     85     }
     86 
     87     private class RootsChangedObserver extends ContentObserver {
     88         public RootsChangedObserver() {
     89             super(new Handler());
     90         }
     91 
     92         @Override
     93         public void onChange(boolean selfChange, Uri uri) {
     94             if (LOGD) Log.d(TAG, "Updating roots due to change at " + uri);
     95             updateAuthorityAsync(uri.getAuthority());
     96         }
     97     }
     98 
     99     /**
    100      * Gather roots from all known storage providers.
    101      */
    102     public void updateAsync() {
    103         // Special root for recents
    104         mRecentsRoot.authority = null;
    105         mRecentsRoot.rootId = null;
    106         mRecentsRoot.derivedIcon = R.drawable.ic_root_recent;
    107         mRecentsRoot.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE
    108                 | Root.FLAG_SUPPORTS_IS_CHILD;
    109         mRecentsRoot.title = mContext.getString(R.string.root_recent);
    110         mRecentsRoot.availableBytes = -1;
    111 
    112         new UpdateTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    113     }
    114 
    115     /**
    116      * Gather roots from storage providers belonging to given package name.
    117      */
    118     public void updatePackageAsync(String packageName) {
    119         new UpdateTask(packageName).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    120     }
    121 
    122     /**
    123      * Gather roots from storage providers belonging to given authority.
    124      */
    125     public void updateAuthorityAsync(String authority) {
    126         final ProviderInfo info = mContext.getPackageManager().resolveContentProvider(authority, 0);
    127         if (info != null) {
    128             updatePackageAsync(info.packageName);
    129         }
    130     }
    131 
    132     private void waitForFirstLoad() {
    133         boolean success = false;
    134         try {
    135             success = mFirstLoad.await(15, TimeUnit.SECONDS);
    136         } catch (InterruptedException e) {
    137         }
    138         if (!success) {
    139             Log.w(TAG, "Timeout waiting for first update");
    140         }
    141     }
    142 
    143     /**
    144      * Load roots from authorities that are in stopped state. Normal
    145      * {@link UpdateTask} passes ignore stopped applications.
    146      */
    147     private void loadStoppedAuthorities() {
    148         final ContentResolver resolver = mContext.getContentResolver();
    149         synchronized (mLock) {
    150             for (String authority : mStoppedAuthorities) {
    151                 if (LOGD) Log.d(TAG, "Loading stopped authority " + authority);
    152                 mRoots.putAll(authority, loadRootsForAuthority(resolver, authority));
    153             }
    154             mStoppedAuthorities.clear();
    155         }
    156     }
    157 
    158     private class UpdateTask extends AsyncTask<Void, Void, Void> {
    159         private final String mFilterPackage;
    160 
    161         private final Multimap<String, RootInfo> mTaskRoots = ArrayListMultimap.create();
    162         private final HashSet<String> mTaskStoppedAuthorities = Sets.newHashSet();
    163 
    164         /**
    165          * Update all roots.
    166          */
    167         public UpdateTask() {
    168             this(null);
    169         }
    170 
    171         /**
    172          * Only update roots belonging to given package name. Other roots will
    173          * be copied from cached {@link #mRoots} values.
    174          */
    175         public UpdateTask(String filterPackage) {
    176             mFilterPackage = filterPackage;
    177         }
    178 
    179         @Override
    180         protected Void doInBackground(Void... params) {
    181             final long start = SystemClock.elapsedRealtime();
    182 
    183             if (mFilterPackage != null) {
    184                 // Need at least first load, since we're going to be using
    185                 // previously cached values for non-matching packages.
    186                 waitForFirstLoad();
    187             }
    188 
    189             mTaskRoots.put(mRecentsRoot.authority, mRecentsRoot);
    190 
    191             final ContentResolver resolver = mContext.getContentResolver();
    192             final PackageManager pm = mContext.getPackageManager();
    193 
    194             // Pick up provider with action string
    195             final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE);
    196             final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, 0);
    197             for (ResolveInfo info : providers) {
    198                 handleDocumentsProvider(info.providerInfo);
    199             }
    200 
    201             final long delta = SystemClock.elapsedRealtime() - start;
    202             Log.d(TAG, "Update found " + mTaskRoots.size() + " roots in " + delta + "ms");
    203             synchronized (mLock) {
    204                 mRoots = mTaskRoots;
    205                 mStoppedAuthorities = mTaskStoppedAuthorities;
    206             }
    207             mFirstLoad.countDown();
    208             resolver.notifyChange(sNotificationUri, null, false);
    209             return null;
    210         }
    211 
    212         private void handleDocumentsProvider(ProviderInfo info) {
    213             // Ignore stopped packages for now; we might query them
    214             // later during UI interaction.
    215             if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) {
    216                 if (LOGD) Log.d(TAG, "Ignoring stopped authority " + info.authority);
    217                 mTaskStoppedAuthorities.add(info.authority);
    218                 return;
    219             }
    220 
    221             // Try using cached roots if filtering
    222             boolean cacheHit = false;
    223             if (mFilterPackage != null && !mFilterPackage.equals(info.packageName)) {
    224                 synchronized (mLock) {
    225                     if (mTaskRoots.putAll(info.authority, mRoots.get(info.authority))) {
    226                         if (LOGD) Log.d(TAG, "Used cached roots for " + info.authority);
    227                         cacheHit = true;
    228                     }
    229                 }
    230             }
    231 
    232             // Cache miss, or loading everything
    233             if (!cacheHit) {
    234                 mTaskRoots.putAll(info.authority,
    235                         loadRootsForAuthority(mContext.getContentResolver(), info.authority));
    236             }
    237         }
    238     }
    239 
    240     /**
    241      * Bring up requested provider and query for all active roots.
    242      */
    243     private Collection<RootInfo> loadRootsForAuthority(ContentResolver resolver, String authority) {
    244         if (LOGD) Log.d(TAG, "Loading roots for " + authority);
    245 
    246         synchronized (mObservedAuthorities) {
    247             if (mObservedAuthorities.add(authority)) {
    248                 // Watch for any future updates
    249                 final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
    250                 mContext.getContentResolver().registerContentObserver(rootsUri, true, mObserver);
    251             }
    252         }
    253 
    254         final List<RootInfo> roots = Lists.newArrayList();
    255         final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
    256 
    257         ContentProviderClient client = null;
    258         Cursor cursor = null;
    259         try {
    260             client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
    261             cursor = client.query(rootsUri, null, null, null, null);
    262             while (cursor.moveToNext()) {
    263                 final RootInfo root = RootInfo.fromRootsCursor(authority, cursor);
    264                 roots.add(root);
    265             }
    266         } catch (Exception e) {
    267             Log.w(TAG, "Failed to load some roots from " + authority + ": " + e);
    268         } finally {
    269             IoUtils.closeQuietly(cursor);
    270             ContentProviderClient.releaseQuietly(client);
    271         }
    272         return roots;
    273     }
    274 
    275     /**
    276      * Return the requested {@link RootInfo}, but only loading the roots for the
    277      * requested authority. This is useful when we want to load fast without
    278      * waiting for all the other roots to come back.
    279      */
    280     public RootInfo getRootOneshot(String authority, String rootId) {
    281         synchronized (mLock) {
    282             RootInfo root = getRootLocked(authority, rootId);
    283             if (root == null) {
    284                 mRoots.putAll(
    285                         authority, loadRootsForAuthority(mContext.getContentResolver(), authority));
    286                 root = getRootLocked(authority, rootId);
    287             }
    288             return root;
    289         }
    290     }
    291 
    292     public RootInfo getRootBlocking(String authority, String rootId) {
    293         waitForFirstLoad();
    294         loadStoppedAuthorities();
    295         synchronized (mLock) {
    296             return getRootLocked(authority, rootId);
    297         }
    298     }
    299 
    300     private RootInfo getRootLocked(String authority, String rootId) {
    301         for (RootInfo root : mRoots.get(authority)) {
    302             if (Objects.equals(root.rootId, rootId)) {
    303                 return root;
    304             }
    305         }
    306         return null;
    307     }
    308 
    309     public boolean isIconUniqueBlocking(RootInfo root) {
    310         waitForFirstLoad();
    311         loadStoppedAuthorities();
    312         synchronized (mLock) {
    313             final int rootIcon = root.derivedIcon != 0 ? root.derivedIcon : root.icon;
    314             for (RootInfo test : mRoots.get(root.authority)) {
    315                 if (Objects.equals(test.rootId, root.rootId)) {
    316                     continue;
    317                 }
    318                 final int testIcon = test.derivedIcon != 0 ? test.derivedIcon : test.icon;
    319                 if (testIcon == rootIcon) {
    320                     return false;
    321                 }
    322             }
    323             return true;
    324         }
    325     }
    326 
    327     public RootInfo getRecentsRoot() {
    328         return mRecentsRoot;
    329     }
    330 
    331     public boolean isRecentsRoot(RootInfo root) {
    332         return mRecentsRoot == root;
    333     }
    334 
    335     public Collection<RootInfo> getRootsBlocking() {
    336         waitForFirstLoad();
    337         loadStoppedAuthorities();
    338         synchronized (mLock) {
    339             return mRoots.values();
    340         }
    341     }
    342 
    343     public Collection<RootInfo> getMatchingRootsBlocking(State state) {
    344         waitForFirstLoad();
    345         loadStoppedAuthorities();
    346         synchronized (mLock) {
    347             return getMatchingRoots(mRoots.values(), state);
    348         }
    349     }
    350 
    351     @VisibleForTesting
    352     static List<RootInfo> getMatchingRoots(Collection<RootInfo> roots, State state) {
    353         final List<RootInfo> matching = Lists.newArrayList();
    354         for (RootInfo root : roots) {
    355             final boolean supportsCreate = (root.flags & Root.FLAG_SUPPORTS_CREATE) != 0;
    356             final boolean supportsIsChild = (root.flags & Root.FLAG_SUPPORTS_IS_CHILD) != 0;
    357             final boolean advanced = (root.flags & Root.FLAG_ADVANCED) != 0;
    358             final boolean localOnly = (root.flags & Root.FLAG_LOCAL_ONLY) != 0;
    359             final boolean empty = (root.flags & Root.FLAG_EMPTY) != 0;
    360 
    361             // Exclude read-only devices when creating
    362             if (state.action == State.ACTION_CREATE && !supportsCreate) continue;
    363             // Exclude roots that don't support directory picking
    364             if (state.action == State.ACTION_OPEN_TREE && !supportsIsChild) continue;
    365             // Exclude advanced devices when not requested
    366             if (!state.showAdvanced && advanced) continue;
    367             // Exclude non-local devices when local only
    368             if (state.localOnly && !localOnly) continue;
    369             // Only show empty roots when creating
    370             if (state.action != State.ACTION_CREATE && empty) continue;
    371 
    372             // Only include roots that serve requested content
    373             final boolean overlap =
    374                     MimePredicate.mimeMatches(root.derivedMimeTypes, state.acceptMimes) ||
    375                     MimePredicate.mimeMatches(state.acceptMimes, root.derivedMimeTypes);
    376             if (!overlap) {
    377                 continue;
    378             }
    379 
    380             matching.add(root);
    381         }
    382         return matching;
    383     }
    384 }
    385