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
17package com.android.documentsui;
18
19import static com.android.documentsui.Shared.DEBUG;
20
21import android.content.BroadcastReceiver.PendingResult;
22import android.content.ContentProviderClient;
23import android.content.ContentResolver;
24import android.content.Context;
25import android.content.Intent;
26import android.content.pm.ApplicationInfo;
27import android.content.pm.PackageManager;
28import android.content.pm.ProviderInfo;
29import android.content.pm.ResolveInfo;
30import android.database.ContentObserver;
31import android.database.Cursor;
32import android.net.Uri;
33import android.os.AsyncTask;
34import android.os.Bundle;
35import android.os.Handler;
36import android.os.SystemClock;
37import android.provider.DocumentsContract;
38import android.provider.DocumentsContract.Root;
39import android.support.annotation.VisibleForTesting;
40import android.util.Log;
41
42import com.android.documentsui.model.RootInfo;
43import com.android.internal.annotations.GuardedBy;
44
45import libcore.io.IoUtils;
46
47import com.google.common.collect.ArrayListMultimap;
48import com.google.common.collect.Multimap;
49
50import java.util.ArrayList;
51import java.util.Collection;
52import java.util.Collections;
53import java.util.HashSet;
54import java.util.List;
55import java.util.Objects;
56import java.util.concurrent.CountDownLatch;
57import java.util.concurrent.TimeUnit;
58
59/**
60 * Cache of known storage backends and their roots.
61 */
62public 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