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