OSDN Git Service

b98e1eedd7e294a27da1f8543e227f1a8c3fb8a6
[android-x86/frameworks-base.git] / packages / DocumentsUI / src / com / android / documentsui / RootsCache.java
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 }