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