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