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.roots; 18 19 import static android.provider.DocumentsContract.QUERY_ARG_MIME_TYPES; 20 21 import static androidx.core.util.Preconditions.checkNotNull; 22 23 import static com.android.documentsui.base.SharedMinimal.DEBUG; 24 import static com.android.documentsui.base.SharedMinimal.VERBOSE; 25 26 import android.content.BroadcastReceiver.PendingResult; 27 import android.content.ContentProviderClient; 28 import android.content.ContentResolver; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.pm.ApplicationInfo; 32 import android.content.pm.PackageManager; 33 import android.content.pm.ProviderInfo; 34 import android.content.pm.ResolveInfo; 35 import android.database.ContentObserver; 36 import android.database.Cursor; 37 import android.net.Uri; 38 import android.os.AsyncTask; 39 import android.os.Bundle; 40 import android.os.FileUtils; 41 import android.os.Handler; 42 import android.os.Looper; 43 import android.os.SystemClock; 44 import android.provider.DocumentsContract; 45 import android.provider.DocumentsContract.Root; 46 import android.util.Log; 47 48 import androidx.annotation.GuardedBy; 49 import androidx.annotation.Nullable; 50 import androidx.localbroadcastmanager.content.LocalBroadcastManager; 51 52 import com.android.documentsui.DocumentsApplication; 53 import com.android.documentsui.R; 54 import com.android.documentsui.UserIdManager; 55 import com.android.documentsui.UserPackage; 56 import com.android.documentsui.archives.ArchivesProvider; 57 import com.android.documentsui.base.LookupApplicationName; 58 import com.android.documentsui.base.Providers; 59 import com.android.documentsui.base.RootInfo; 60 import com.android.documentsui.base.State; 61 import com.android.documentsui.base.UserId; 62 63 import com.google.common.collect.ArrayListMultimap; 64 import com.google.common.collect.Multimap; 65 66 import java.util.ArrayList; 67 import java.util.Collection; 68 import java.util.Collections; 69 import java.util.HashMap; 70 import java.util.HashSet; 71 import java.util.List; 72 import java.util.Map; 73 import java.util.Objects; 74 import java.util.concurrent.CountDownLatch; 75 import java.util.concurrent.TimeUnit; 76 import java.util.function.Function; 77 78 /** 79 * Cache of known storage backends and their roots. 80 */ 81 public class ProvidersCache implements ProvidersAccess, LookupApplicationName { 82 private static final String TAG = "ProvidersCache"; 83 84 // Not all providers are equally well written. If a provider returns 85 // empty results we don't cache them...unless they're in this magical list 86 // of beloved providers. 87 private static final List<String> PERMIT_EMPTY_CACHE = new ArrayList<String>() {{ 88 // MTP provider commonly returns no roots (if no devices are attached). 89 add(Providers.AUTHORITY_MTP); 90 // ArchivesProvider doesn't support any roots. 91 add(ArchivesProvider.AUTHORITY); 92 }}; 93 94 private final Context mContext; 95 96 @GuardedBy("mRootsChangedObservers") 97 private final Map<UserId, RootsChangedObserver> mRootsChangedObservers = new HashMap<>(); 98 99 @GuardedBy("mRecentsRoots") 100 private final Map<UserId, RootInfo> mRecentsRoots = new HashMap<>(); 101 102 private final Object mLock = new Object(); 103 private final CountDownLatch mFirstLoad = new CountDownLatch(1); 104 105 @GuardedBy("mLock") 106 private boolean mFirstLoadDone; 107 @GuardedBy("mLock") 108 private PendingResult mBootCompletedResult; 109 110 @GuardedBy("mLock") 111 private Multimap<UserAuthority, RootInfo> mRoots = ArrayListMultimap.create(); 112 @GuardedBy("mLock") 113 private HashSet<UserAuthority> mStoppedAuthorities = new HashSet<>(); 114 115 @GuardedBy("mObservedAuthoritiesDetails") 116 private final Map<UserAuthority, PackageDetails> mObservedAuthoritiesDetails = new HashMap<>(); 117 118 private final UserIdManager mUserIdManager; 119 ProvidersCache(Context context, UserIdManager userIdManager)120 public ProvidersCache(Context context, UserIdManager userIdManager) { 121 mContext = context; 122 mUserIdManager = userIdManager; 123 } 124 generateRecentsRoot(UserId rootUserId)125 private RootInfo generateRecentsRoot(UserId rootUserId) { 126 return new RootInfo() {{ 127 // Special root for recents 128 userId = rootUserId; 129 derivedIcon = R.drawable.ic_root_recent; 130 derivedType = RootInfo.TYPE_RECENTS; 131 flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_SEARCH; 132 queryArgs = QUERY_ARG_MIME_TYPES; 133 title = mContext.getString(R.string.root_recent); 134 availableBytes = -1; 135 }}; 136 } 137 138 private RootInfo createOrGetRecentsRoot(UserId userId) { 139 return createOrGetByUserId(mRecentsRoots, userId, user -> generateRecentsRoot(user)); 140 } 141 142 private RootsChangedObserver createOrGetRootsChangedObserver(UserId userId) { 143 return createOrGetByUserId(mRootsChangedObservers, userId, 144 user -> new RootsChangedObserver(user)); 145 } 146 147 private static <T> T createOrGetByUserId(Map<UserId, T> map, UserId userId, 148 Function<UserId, T> supplier) { 149 synchronized (map) { 150 if (!map.containsKey(userId)) { 151 map.put(userId, supplier.apply(userId)); 152 } 153 } 154 return map.get(userId); 155 } 156 157 private class RootsChangedObserver extends ContentObserver { 158 159 private final UserId mUserId; 160 161 RootsChangedObserver(UserId userId) { 162 super(new Handler(Looper.getMainLooper())); 163 mUserId = userId; 164 } 165 166 @Override 167 public void onChange(boolean selfChange, Uri uri) { 168 if (uri == null) { 169 Log.w(TAG, "Received onChange event for null uri. Skipping."); 170 return; 171 } 172 if (DEBUG) { 173 Log.i(TAG, "Updating roots due to change on user " + mUserId + "at " + uri); 174 } 175 updateAuthorityAsync(mUserId, uri.getAuthority()); 176 } 177 } 178 179 @Override 180 public String getApplicationName(UserId userId, String authority) { 181 return mObservedAuthoritiesDetails.get( 182 new UserAuthority(userId, authority)).applicationName; 183 } 184 185 @Override 186 public String getPackageName(UserId userId, String authority) { 187 return mObservedAuthoritiesDetails.get(new UserAuthority(userId, authority)).packageName; 188 } 189 190 public void updateAsync(boolean forceRefreshAll, @Nullable Runnable callback) { 191 192 // NOTE: This method is called when the UI language changes. 193 // For that reason we update our RecentsRoot to reflect 194 // the current language. 195 final String title = mContext.getString(R.string.root_recent); 196 for (UserId userId : mUserIdManager.getUserIds()) { 197 RootInfo recentRoot = createOrGetRecentsRoot(userId); 198 recentRoot.title = title; 199 // Nothing else about the root should ever change. 200 assert (recentRoot.authority == null); 201 assert (recentRoot.rootId == null); 202 assert (recentRoot.derivedIcon == R.drawable.ic_root_recent); 203 assert (recentRoot.derivedType == RootInfo.TYPE_RECENTS); 204 assert (recentRoot.flags == (Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD)); 205 assert (recentRoot.availableBytes == -1); 206 } 207 208 new UpdateTask(forceRefreshAll, null, callback).executeOnExecutor( 209 AsyncTask.THREAD_POOL_EXECUTOR); 210 } 211 212 public void updatePackageAsync(UserId userId, String packageName) { 213 new UpdateTask(false, new UserPackage(userId, packageName), 214 /* callback= */ null).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 215 } 216 217 public void updateAuthorityAsync(UserId userId, String authority) { 218 final ProviderInfo info = userId.getPackageManager(mContext).resolveContentProvider( 219 authority, 0); 220 if (info != null) { 221 updatePackageAsync(userId, info.packageName); 222 } 223 } 224 225 void setBootCompletedResult(PendingResult result) { 226 synchronized (mLock) { 227 // Quickly check if we've already finished loading, otherwise hang 228 // out until first pass is finished. 229 if (mFirstLoadDone) { 230 result.finish(); 231 } else { 232 mBootCompletedResult = result; 233 } 234 } 235 } 236 237 /** 238 * Block until the first {@link UpdateTask} pass has finished. 239 * 240 * @return {@code true} if cached roots is ready to roll, otherwise 241 * {@code false} if we timed out while waiting. 242 */ 243 private boolean waitForFirstLoad() { 244 boolean success = false; 245 try { 246 success = mFirstLoad.await(15, TimeUnit.SECONDS); 247 } catch (InterruptedException e) { 248 } 249 if (!success) { 250 Log.w(TAG, "Timeout waiting for first update"); 251 } 252 return success; 253 } 254 255 /** 256 * Load roots from authorities that are in stopped state. Normal 257 * {@link UpdateTask} passes ignore stopped applications. 258 */ 259 private void loadStoppedAuthorities() { 260 synchronized (mLock) { 261 for (UserAuthority userAuthority : mStoppedAuthorities) { 262 mRoots.replaceValues(userAuthority, loadRootsForAuthority(userAuthority, true)); 263 } 264 mStoppedAuthorities.clear(); 265 } 266 } 267 268 /** 269 * Load roots from a stopped authority. Normal {@link UpdateTask} passes 270 * ignore stopped applications. 271 */ 272 private void loadStoppedAuthority(UserAuthority userAuthority) { 273 synchronized (mLock) { 274 if (!mStoppedAuthorities.contains(userAuthority)) { 275 return; 276 } 277 if (DEBUG) { 278 Log.d(TAG, "Loading stopped authority " + userAuthority); 279 } 280 mRoots.replaceValues(userAuthority, loadRootsForAuthority(userAuthority, true)); 281 mStoppedAuthorities.remove(userAuthority); 282 } 283 } 284 285 /** 286 * Bring up requested provider and query for all active roots. Will consult cached 287 * roots if not forceRefresh. Will query when cached roots is empty (which should never happen). 288 */ 289 private Collection<RootInfo> loadRootsForAuthority(UserAuthority userAuthority, 290 boolean forceRefresh) { 291 UserId userId = userAuthority.userId; 292 String authority = userAuthority.authority; 293 if (VERBOSE) Log.v(TAG, "Loading roots on user " + userId + " for " + authority); 294 295 ContentResolver resolver = userId.getContentResolver(mContext); 296 final ArrayList<RootInfo> roots = new ArrayList<>(); 297 final PackageManager pm = userId.getPackageManager(mContext); 298 ProviderInfo provider = pm.resolveContentProvider( 299 authority, PackageManager.GET_META_DATA); 300 if (provider == null) { 301 Log.w(TAG, "Failed to get provider info for " + authority); 302 return roots; 303 } 304 if (!provider.exported) { 305 Log.w(TAG, "Provider is not exported. Failed to load roots for " + authority); 306 return roots; 307 } 308 if (!provider.grantUriPermissions) { 309 Log.w(TAG, "Provider doesn't grantUriPermissions. Failed to load roots for " 310 + authority); 311 return roots; 312 } 313 if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(provider.readPermission) 314 || !android.Manifest.permission.MANAGE_DOCUMENTS.equals(provider.writePermission)) { 315 Log.w(TAG, "Provider is not protected by MANAGE_DOCUMENTS. Failed to load roots for " 316 + authority); 317 return roots; 318 } 319 320 synchronized (mObservedAuthoritiesDetails) { 321 if (!mObservedAuthoritiesDetails.containsKey(userAuthority)) { 322 CharSequence appName = pm.getApplicationLabel(provider.applicationInfo); 323 String packageName = provider.applicationInfo.packageName; 324 325 mObservedAuthoritiesDetails.put( 326 userAuthority, new PackageDetails(appName.toString(), packageName)); 327 328 // Watch for any future updates 329 final Uri rootsUri = DocumentsContract.buildRootsUri(authority); 330 resolver.registerContentObserver(rootsUri, true, 331 createOrGetRootsChangedObserver(userId)); 332 } 333 } 334 335 final Uri rootsUri = DocumentsContract.buildRootsUri(authority); 336 if (!forceRefresh) { 337 // Look for roots data that we might have cached for ourselves in the 338 // long-lived system process. 339 final Bundle systemCache = resolver.getCache(rootsUri); 340 if (systemCache != null) { 341 ArrayList<RootInfo> cachedRoots = systemCache.getParcelableArrayList(TAG); 342 assert (cachedRoots != null); 343 if (!cachedRoots.isEmpty() || PERMIT_EMPTY_CACHE.contains(authority)) { 344 if (VERBOSE) Log.v(TAG, "System cache hit for " + authority); 345 return cachedRoots; 346 } else { 347 Log.w(TAG, "Ignoring empty system cache hit for " + authority); 348 } 349 } 350 } 351 352 ContentProviderClient client = null; 353 Cursor cursor = null; 354 try { 355 client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority); 356 cursor = client.query(rootsUri, null, null, null, null); 357 while (cursor.moveToNext()) { 358 final RootInfo root = RootInfo.fromRootsCursor(userId, authority, cursor); 359 roots.add(root); 360 } 361 } catch (Exception e) { 362 Log.w(TAG, "Failed to load some roots from " + authority, e); 363 // We didn't load every root from the provider. Don't put it to 364 // system cache so that we'll try loading them again next time even 365 // if forceRefresh is false. 366 return roots; 367 } finally { 368 FileUtils.closeQuietly(cursor); 369 FileUtils.closeQuietly(client); 370 } 371 372 // Cache these freshly parsed roots over in the long-lived system 373 // process, in case our process goes away. The system takes care of 374 // invalidating the cache if the package or Uri changes. 375 final Bundle systemCache = new Bundle(); 376 if (roots.isEmpty() && !PERMIT_EMPTY_CACHE.contains(authority)) { 377 Log.i(TAG, "Provider returned no roots. Possibly naughty: " + authority); 378 } else { 379 systemCache.putParcelableArrayList(TAG, roots); 380 resolver.putCache(rootsUri, systemCache); 381 } 382 383 return roots; 384 } 385 386 @Override 387 public RootInfo getRootOneshot(UserId userId, String authority, String rootId) { 388 return getRootOneshot(userId, authority, rootId, false); 389 } 390 391 public RootInfo getRootOneshot(UserId userId, String authority, String rootId, 392 boolean forceRefresh) { 393 synchronized (mLock) { 394 UserAuthority userAuthority = new UserAuthority(userId, authority); 395 RootInfo root = forceRefresh ? null : getRootLocked(userAuthority, rootId); 396 if (root == null) { 397 mRoots.replaceValues(userAuthority, 398 loadRootsForAuthority(userAuthority, forceRefresh)); 399 root = getRootLocked(userAuthority, rootId); 400 } 401 return root; 402 } 403 } 404 405 public RootInfo getRootBlocking(UserId userId, String authority, String rootId) { 406 waitForFirstLoad(); 407 loadStoppedAuthorities(); 408 synchronized (mLock) { 409 return getRootLocked(new UserAuthority(userId, authority), rootId); 410 } 411 } 412 413 private RootInfo getRootLocked(UserAuthority userAuthority, String rootId) { 414 for (RootInfo root : mRoots.get(userAuthority)) { 415 if (Objects.equals(root.rootId, rootId)) { 416 return root; 417 } 418 } 419 return null; 420 } 421 422 @Override 423 public RootInfo getRecentsRoot(UserId userId) { 424 return createOrGetRecentsRoot(userId); 425 } 426 427 public boolean isRecentsRoot(RootInfo root) { 428 return mRecentsRoots.containsValue(root); 429 } 430 431 @Override 432 public Collection<RootInfo> getRootsBlocking() { 433 waitForFirstLoad(); 434 loadStoppedAuthorities(); 435 synchronized (mLock) { 436 return mRoots.values(); 437 } 438 } 439 440 @Override 441 public Collection<RootInfo> getMatchingRootsBlocking(State state) { 442 waitForFirstLoad(); 443 loadStoppedAuthorities(); 444 synchronized (mLock) { 445 return ProvidersAccess.getMatchingRoots(mRoots.values(), state); 446 } 447 } 448 449 @Override 450 public Collection<RootInfo> getRootsForAuthorityBlocking(UserId userId, String authority) { 451 waitForFirstLoad(); 452 UserAuthority userAuthority = new UserAuthority(userId, authority); 453 loadStoppedAuthority(userAuthority); 454 synchronized (mLock) { 455 final Collection<RootInfo> roots = mRoots.get(userAuthority); 456 return roots != null ? roots : Collections.<RootInfo>emptyList(); 457 } 458 } 459 460 @Override 461 public RootInfo getDefaultRootBlocking(State state) { 462 RootInfo root = ProvidersAccess.getDefaultRoot(getRootsBlocking(), state); 463 return root != null ? root : createOrGetRecentsRoot(UserId.CURRENT_USER); 464 } 465 466 public void logCache() { 467 StringBuilder output = new StringBuilder(); 468 469 for (UserAuthority userAuthority : mObservedAuthoritiesDetails.keySet()) { 470 List<String> roots = new ArrayList<>(); 471 Uri rootsUri = DocumentsContract.buildRootsUri(userAuthority.authority); 472 Bundle systemCache = userAuthority.userId.getContentResolver(mContext).getCache( 473 rootsUri); 474 if (systemCache != null) { 475 ArrayList<RootInfo> cachedRoots = systemCache.getParcelableArrayList(TAG); 476 for (RootInfo root : cachedRoots) { 477 roots.add(root.toDebugString()); 478 } 479 } 480 481 output.append((output.length() == 0) ? "System cache: " : ", "); 482 output.append(userAuthority).append("=").append(roots); 483 } 484 485 Log.i(TAG, output.toString()); 486 } 487 488 private class UpdateTask extends AsyncTask<Void, Void, Void> { 489 private final boolean mForceRefreshAll; 490 @Nullable 491 private final UserPackage mForceRefreshUserPackage; 492 @Nullable 493 private final Runnable mCallback; 494 495 private final Multimap<UserAuthority, RootInfo> mTaskRoots = ArrayListMultimap.create(); 496 private final HashSet<UserAuthority> mTaskStoppedAuthorities = new HashSet<>(); 497 498 /** 499 * Create task to update roots cache. 500 * 501 * @param forceRefreshAll when true, all previously cached values for 502 * all packages should be ignored. 503 * @param forceRefreshUserPackage when non-null, all previously cached 504 * values for this specific user package should be ignored. 505 * @param callback when non-null, it will be invoked after the task is executed. 506 */ 507 UpdateTask(boolean forceRefreshAll, @Nullable UserPackage forceRefreshUserPackage, 508 @Nullable Runnable callback) { 509 mForceRefreshAll = forceRefreshAll; 510 mForceRefreshUserPackage = forceRefreshUserPackage; 511 mCallback = callback; 512 } 513 514 @Override 515 protected Void doInBackground(Void... params) { 516 final long start = SystemClock.elapsedRealtime(); 517 518 for (UserId userId : mUserIdManager.getUserIds()) { 519 final RootInfo recents = createOrGetRecentsRoot(userId); 520 mTaskRoots.put(new UserAuthority(recents.userId, recents.authority), recents); 521 522 final PackageManager pm = userId.getPackageManager(mContext); 523 // Pick up provider with action string 524 final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE); 525 final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, 0); 526 for (ResolveInfo info : providers) { 527 ProviderInfo providerInfo = info.providerInfo; 528 if (providerInfo.authority != null) { 529 handleDocumentsProvider(providerInfo, userId); 530 } 531 } 532 } 533 534 final long delta = SystemClock.elapsedRealtime() - start; 535 if (VERBOSE) Log.v(TAG, 536 "Update found " + mTaskRoots.size() + " roots in " + delta + "ms"); 537 synchronized (mLock) { 538 mFirstLoadDone = true; 539 if (mBootCompletedResult != null) { 540 mBootCompletedResult.finish(); 541 mBootCompletedResult = null; 542 } 543 mRoots = mTaskRoots; 544 mStoppedAuthorities = mTaskStoppedAuthorities; 545 } 546 mFirstLoad.countDown(); 547 LocalBroadcastManager.getInstance(mContext).sendBroadcast(new Intent(BROADCAST_ACTION)); 548 return null; 549 } 550 551 @Override 552 protected void onPostExecute(Void aVoid) { 553 if (mCallback != null) { 554 mCallback.run(); 555 } 556 } 557 558 private void handleDocumentsProvider(ProviderInfo info, UserId userId) { 559 UserAuthority userAuthority = new UserAuthority(userId, info.authority); 560 // Ignore stopped packages for now; we might query them 561 // later during UI interaction. 562 if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) { 563 if (VERBOSE) { 564 Log.v(TAG, "Ignoring stopped authority " + info.authority + ", user " + userId); 565 } 566 mTaskStoppedAuthorities.add(userAuthority); 567 return; 568 } 569 570 final boolean forceRefresh = mForceRefreshAll 571 || Objects.equals(new UserPackage(userId, info.packageName), 572 mForceRefreshUserPackage); 573 mTaskRoots.putAll(userAuthority, loadRootsForAuthority(userAuthority, forceRefresh)); 574 } 575 576 } 577 578 private static class UserAuthority { 579 private final UserId userId; 580 @Nullable 581 private final String authority; 582 583 private UserAuthority(UserId userId, @Nullable String authority) { 584 this.userId = checkNotNull(userId); 585 this.authority = authority; 586 } 587 588 @Override 589 public boolean equals(Object o) { 590 if (o == null) { 591 return false; 592 } 593 594 if (this == o) { 595 return true; 596 } 597 598 if (o instanceof UserAuthority) { 599 UserAuthority other = (UserAuthority) o; 600 return Objects.equals(userId, other.userId) 601 && Objects.equals(authority, other.authority); 602 } 603 604 return false; 605 } 606 607 608 @Override 609 public int hashCode() { 610 return Objects.hash(userId, authority); 611 } 612 } 613 614 private static class PackageDetails { 615 private String applicationName; 616 private String packageName; 617 618 public PackageDetails(String appName, String pckgName) { 619 applicationName = appName; 620 packageName = pckgName; 621 } 622 } 623 } 624