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     // TODO: cache roots in local provider to avoid spinning up backends
     64     // TODO: root updates should trigger UI refresh
     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             return null;
    205         }
    206 
    207         private void handleDocumentsProvider(ProviderInfo info) {
    208             // Ignore stopped packages for now; we might query them
    209             // later during UI interaction.
    210             if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) {
    211                 if (LOGD) Log.d(TAG, "Ignoring stopped authority " + info.authority);
    212                 mTaskStoppedAuthorities.add(info.authority);
    213                 return;
    214             }
    215 
    216             // Try using cached roots if filtering
    217             boolean cacheHit = false;
    218             if (mFilterPackage != null && !mFilterPackage.equals(info.packageName)) {
    219                 synchronized (mLock) {
    220                     if (mTaskRoots.putAll(info.authority, mRoots.get(info.authority))) {
    221                         if (LOGD) Log.d(TAG, "Used cached roots for " + info.authority);
    222                         cacheHit = true;
    223                     }
    224                 }
    225             }
    226 
    227             // Cache miss, or loading everything
    228             if (!cacheHit) {
    229                 mTaskRoots.putAll(info.authority,
    230                         loadRootsForAuthority(mContext.getContentResolver(), info.authority));
    231             }
    232         }
    233     }
    234 
    235     /**
    236      * Bring up requested provider and query for all active roots.
    237      */
    238     private Collection<RootInfo> loadRootsForAuthority(ContentResolver resolver, String authority) {
    239         if (LOGD) Log.d(TAG, "Loading roots for " + authority);
    240 
    241         synchronized (mObservedAuthorities) {
    242             if (mObservedAuthorities.add(authority)) {
    243                 // Watch for any future updates
    244                 final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
    245                 mContext.getContentResolver().registerContentObserver(rootsUri, true, mObserver);
    246             }
    247         }
    248 
    249         final List<RootInfo> roots = Lists.newArrayList();
    250         final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
    251 
    252         ContentProviderClient client = null;
    253         Cursor cursor = null;
    254         try {
    255             client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
    256             cursor = client.query(rootsUri, null, null, null, null);
    257             while (cursor.moveToNext()) {
    258                 final RootInfo root = RootInfo.fromRootsCursor(authority, cursor);
    259                 roots.add(root);
    260             }
    261         } catch (Exception e) {
    262             Log.w(TAG, "Failed to load some roots from " + authority + ": " + e);
    263         } finally {
    264             IoUtils.closeQuietly(cursor);
    265             ContentProviderClient.releaseQuietly(client);
    266         }
    267         return roots;
    268     }
    269 
    270     /**
    271      * Return the requested {@link RootInfo}, but only loading the roots for the
    272      * requested authority. This is useful when we want to load fast without
    273      * waiting for all the other roots to come back.
    274      */
    275     public RootInfo getRootOneshot(String authority, String rootId) {
    276         synchronized (mLock) {
    277             RootInfo root = getRootLocked(authority, rootId);
    278             if (root == null) {
    279                 mRoots.putAll(
    280                         authority, loadRootsForAuthority(mContext.getContentResolver(), authority));
    281                 root = getRootLocked(authority, rootId);
    282             }
    283             return root;
    284         }
    285     }
    286 
    287     public RootInfo getRootBlocking(String authority, String rootId) {
    288         waitForFirstLoad();
    289         loadStoppedAuthorities();
    290         synchronized (mLock) {
    291             return getRootLocked(authority, rootId);
    292         }
    293     }
    294 
    295     private RootInfo getRootLocked(String authority, String rootId) {
    296         for (RootInfo root : mRoots.get(authority)) {
    297             if (Objects.equal(root.rootId, rootId)) {
    298                 return root;
    299             }
    300         }
    301         return null;
    302     }
    303 
    304     public boolean isIconUniqueBlocking(RootInfo root) {
    305         waitForFirstLoad();
    306         loadStoppedAuthorities();
    307         synchronized (mLock) {
    308             final int rootIcon = root.derivedIcon != 0 ? root.derivedIcon : root.icon;
    309             for (RootInfo test : mRoots.get(root.authority)) {
    310                 if (Objects.equal(test.rootId, root.rootId)) {
    311                     continue;
    312                 }
    313                 final int testIcon = test.derivedIcon != 0 ? test.derivedIcon : test.icon;
    314                 if (testIcon == rootIcon) {
    315                     return false;
    316                 }
    317             }
    318             return true;
    319         }
    320     }
    321 
    322     public RootInfo getRecentsRoot() {
    323         return mRecentsRoot;
    324     }
    325 
    326     public boolean isRecentsRoot(RootInfo root) {
    327         return mRecentsRoot == root;
    328     }
    329 
    330     public Collection<RootInfo> getRootsBlocking() {
    331         waitForFirstLoad();
    332         loadStoppedAuthorities();
    333         synchronized (mLock) {
    334             return mRoots.values();
    335         }
    336     }
    337 
    338     public Collection<RootInfo> getMatchingRootsBlocking(State state) {
    339         waitForFirstLoad();
    340         loadStoppedAuthorities();
    341         synchronized (mLock) {
    342             return getMatchingRoots(mRoots.values(), state);
    343         }
    344     }
    345 
    346     @VisibleForTesting
    347     static List<RootInfo> getMatchingRoots(Collection<RootInfo> roots, State state) {
    348         final List<RootInfo> matching = Lists.newArrayList();
    349         for (RootInfo root : roots) {
    350             final boolean supportsCreate = (root.flags & Root.FLAG_SUPPORTS_CREATE) != 0;
    351             final boolean advanced = (root.flags & Root.FLAG_ADVANCED) != 0;
    352             final boolean localOnly = (root.flags & Root.FLAG_LOCAL_ONLY) != 0;
    353             final boolean empty = (root.flags & Root.FLAG_EMPTY) != 0;
    354 
    355             // Exclude read-only devices when creating
    356             if (state.action == State.ACTION_CREATE && !supportsCreate) continue;
    357             // Exclude advanced devices when not requested
    358             if (!state.showAdvanced && advanced) continue;
    359             // Exclude non-local devices when local only
    360             if (state.localOnly && !localOnly) continue;
    361             // Only show empty roots when creating
    362             if (state.action != State.ACTION_CREATE && empty) continue;
    363 
    364             // Only include roots that serve requested content
    365             final boolean overlap =
    366                     MimePredicate.mimeMatches(root.derivedMimeTypes, state.acceptMimes) ||
    367                     MimePredicate.mimeMatches(state.acceptMimes, root.derivedMimeTypes);
    368             if (!overlap) {
    369                 continue;
    370             }
    371 
    372             matching.add(root);
    373         }
    374         return matching;
    375     }
    376 }
    377