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