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