RootsCache.java revision 85f5f8132015d8a5043ea4413702420d0d157c9f
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.DocumentsActivity.TAG;
20
21import android.content.ContentProviderClient;
22import android.content.ContentResolver;
23import android.content.Context;
24import android.content.Intent;
25import android.content.pm.ApplicationInfo;
26import android.content.pm.PackageManager;
27import android.content.pm.ProviderInfo;
28import android.content.pm.ResolveInfo;
29import android.database.ContentObserver;
30import android.database.Cursor;
31import android.net.Uri;
32import android.os.AsyncTask;
33import android.os.Handler;
34import android.os.SystemClock;
35import android.provider.DocumentsContract;
36import android.provider.DocumentsContract.Root;
37import android.util.Log;
38
39import com.android.documentsui.DocumentsActivity.State;
40import com.android.documentsui.model.RootInfo;
41import com.android.internal.annotations.GuardedBy;
42import com.android.internal.annotations.VisibleForTesting;
43import com.android.internal.util.Objects;
44import com.google.android.collect.Lists;
45import com.google.android.collect.Sets;
46import com.google.common.collect.ArrayListMultimap;
47import com.google.common.collect.Multimap;
48
49import libcore.io.IoUtils;
50
51import java.util.Collection;
52import java.util.HashSet;
53import java.util.List;
54import java.util.concurrent.CountDownLatch;
55import java.util.concurrent.TimeUnit;
56
57/**
58 * Cache of known storage backends and their roots.
59 */
60public 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            // Pick up legacy providers
198            final List<ProviderInfo> legacyProviders = pm.queryContentProviders(
199                    null, -1, PackageManager.GET_META_DATA);
200            for (ProviderInfo info : legacyProviders) {
201                if (info.metaData != null && info.metaData.containsKey(
202                        DocumentsContract.META_DATA_DOCUMENT_PROVIDER)) {
203                    handleDocumentsProvider(info);
204                }
205            }
206
207            final long delta = SystemClock.elapsedRealtime() - start;
208            Log.d(TAG, "Update found " + mTaskRoots.size() + " roots in " + delta + "ms");
209            synchronized (mLock) {
210                mRoots = mTaskRoots;
211                mStoppedAuthorities = mTaskStoppedAuthorities;
212            }
213            mFirstLoad.countDown();
214            return null;
215        }
216
217        private void handleDocumentsProvider(ProviderInfo info) {
218            // Ignore stopped packages for now; we might query them
219            // later during UI interaction.
220            if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) {
221                if (LOGD) Log.d(TAG, "Ignoring stopped authority " + info.authority);
222                mTaskStoppedAuthorities.add(info.authority);
223                return;
224            }
225
226            // Try using cached roots if filtering
227            boolean cacheHit = false;
228            if (mFilterPackage != null && !mFilterPackage.equals(info.packageName)) {
229                synchronized (mLock) {
230                    if (mTaskRoots.putAll(info.authority, mRoots.get(info.authority))) {
231                        if (LOGD) Log.d(TAG, "Used cached roots for " + info.authority);
232                        cacheHit = true;
233                    }
234                }
235            }
236
237            // Cache miss, or loading everything
238            if (!cacheHit) {
239                mTaskRoots.putAll(info.authority,
240                        loadRootsForAuthority(mContext.getContentResolver(), info.authority));
241            }
242        }
243    }
244
245    /**
246     * Bring up requested provider and query for all active roots.
247     */
248    private Collection<RootInfo> loadRootsForAuthority(ContentResolver resolver, String authority) {
249        if (LOGD) Log.d(TAG, "Loading roots for " + authority);
250
251        synchronized (mObservedAuthorities) {
252            if (mObservedAuthorities.add(authority)) {
253                // Watch for any future updates
254                final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
255                mContext.getContentResolver().registerContentObserver(rootsUri, true, mObserver);
256            }
257        }
258
259        final List<RootInfo> roots = Lists.newArrayList();
260        final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
261
262        ContentProviderClient client = null;
263        Cursor cursor = null;
264        try {
265            client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
266            cursor = client.query(rootsUri, null, null, null, null);
267            while (cursor.moveToNext()) {
268                final RootInfo root = RootInfo.fromRootsCursor(authority, cursor);
269                roots.add(root);
270            }
271        } catch (Exception e) {
272            Log.w(TAG, "Failed to load some roots from " + authority + ": " + e);
273        } finally {
274            IoUtils.closeQuietly(cursor);
275            ContentProviderClient.releaseQuietly(client);
276        }
277        return roots;
278    }
279
280    /**
281     * Return the requested {@link RootInfo}, but only loading the roots for the
282     * requested authority. This is useful when we want to load fast without
283     * waiting for all the other roots to come back.
284     */
285    public RootInfo getRootOneshot(String authority, String rootId) {
286        synchronized (mLock) {
287            RootInfo root = getRootLocked(authority, rootId);
288            if (root == null) {
289                mRoots.putAll(
290                        authority, loadRootsForAuthority(mContext.getContentResolver(), authority));
291                root = getRootLocked(authority, rootId);
292            }
293            return root;
294        }
295    }
296
297    public RootInfo getRootBlocking(String authority, String rootId) {
298        waitForFirstLoad();
299        loadStoppedAuthorities();
300        synchronized (mLock) {
301            return getRootLocked(authority, rootId);
302        }
303    }
304
305    private RootInfo getRootLocked(String authority, String rootId) {
306        for (RootInfo root : mRoots.get(authority)) {
307            if (Objects.equal(root.rootId, rootId)) {
308                return root;
309            }
310        }
311        return null;
312    }
313
314    public boolean isIconUniqueBlocking(RootInfo root) {
315        waitForFirstLoad();
316        loadStoppedAuthorities();
317        synchronized (mLock) {
318            final int rootIcon = root.derivedIcon != 0 ? root.derivedIcon : root.icon;
319            for (RootInfo test : mRoots.get(root.authority)) {
320                if (Objects.equal(test.rootId, root.rootId)) {
321                    continue;
322                }
323                final int testIcon = test.derivedIcon != 0 ? test.derivedIcon : test.icon;
324                if (testIcon == rootIcon) {
325                    return false;
326                }
327            }
328            return true;
329        }
330    }
331
332    public RootInfo getRecentsRoot() {
333        return mRecentsRoot;
334    }
335
336    public boolean isRecentsRoot(RootInfo root) {
337        return mRecentsRoot == root;
338    }
339
340    public Collection<RootInfo> getRootsBlocking() {
341        waitForFirstLoad();
342        loadStoppedAuthorities();
343        synchronized (mLock) {
344            return mRoots.values();
345        }
346    }
347
348    public Collection<RootInfo> getMatchingRootsBlocking(State state) {
349        waitForFirstLoad();
350        loadStoppedAuthorities();
351        synchronized (mLock) {
352            return getMatchingRoots(mRoots.values(), state);
353        }
354    }
355
356    @VisibleForTesting
357    static List<RootInfo> getMatchingRoots(Collection<RootInfo> roots, State state) {
358        final List<RootInfo> matching = Lists.newArrayList();
359        for (RootInfo root : roots) {
360            final boolean supportsCreate = (root.flags & Root.FLAG_SUPPORTS_CREATE) != 0;
361            final boolean advanced = (root.flags & Root.FLAG_ADVANCED) != 0;
362            final boolean localOnly = (root.flags & Root.FLAG_LOCAL_ONLY) != 0;
363            final boolean empty = (root.flags & Root.FLAG_EMPTY) != 0;
364
365            // Exclude read-only devices when creating
366            if (state.action == State.ACTION_CREATE && !supportsCreate) continue;
367            // Exclude advanced devices when not requested
368            if (!state.showAdvanced && advanced) continue;
369            // Exclude non-local devices when local only
370            if (state.localOnly && !localOnly) continue;
371            // Only show empty roots when creating
372            if (state.action != State.ACTION_CREATE && empty) continue;
373
374            // Only include roots that serve requested content
375            final boolean overlap =
376                    MimePredicate.mimeMatches(root.derivedMimeTypes, state.acceptMimes) ||
377                    MimePredicate.mimeMatches(state.acceptMimes, root.derivedMimeTypes);
378            if (!overlap) {
379                continue;
380            }
381
382            matching.add(root);
383        }
384        return matching;
385    }
386}
387