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.Shared.DEBUG; 20 import static com.android.documentsui.base.Shared.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 ProvidersCache(Context context)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 { RootsChangedObserver()120 public RootsChangedObserver() { 121 super(new Handler()); 122 } 123 124 @Override onChange(boolean selfChange, Uri uri)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 getApplicationName(String authority)136 public String getApplicationName(String authority) { 137 return mObservedAuthoritiesDetails.get(authority).applicationName; 138 } 139 140 @Override getPackageName(String authority)141 public String getPackageName(String authority) { 142 return mObservedAuthoritiesDetails.get(authority).packageName; 143 } 144 updateAsync(boolean forceRefreshAll)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 updatePackageAsync(String packageName)164 public void updatePackageAsync(String packageName) { 165 new UpdateTask(false, packageName).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 166 } 167 updateAuthorityAsync(String authority)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 setBootCompletedResult(PendingResult result)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 */ waitForFirstLoad()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 */ loadStoppedAuthorities()209 private void loadStoppedAuthorities() { 210 final ContentResolver resolver = mContext.getContentResolver(); 211 synchronized (mLock) { 212 for (String authority : mStoppedAuthorities) { 213 mRoots.putAll(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 */ loadStoppedAuthority(String authority)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.putAll(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 */ loadRootsForAuthority( ContentResolver resolver, String authority, boolean forceRefresh)239 private Collection<RootInfo> loadRootsForAuthority( 240 ContentResolver resolver, String authority, boolean forceRefresh) { 241 if (VERBOSE) Log.v(TAG, "Loading roots for " + authority); 242 243 synchronized (mObservedAuthoritiesDetails) { 244 if (!mObservedAuthoritiesDetails.containsKey(authority)) { 245 ProviderInfo provider = mContext.getPackageManager().resolveContentProvider( 246 authority, PackageManager.GET_META_DATA); 247 PackageManager pm = mContext.getPackageManager(); 248 CharSequence appName = pm.getApplicationLabel(provider.applicationInfo); 249 String packageName = provider.applicationInfo.packageName; 250 251 mObservedAuthoritiesDetails.put( 252 authority, new PackageDetails(appName.toString(), packageName)); 253 254 // Watch for any future updates 255 final Uri rootsUri = DocumentsContract.buildRootsUri(authority); 256 mContext.getContentResolver().registerContentObserver(rootsUri, true, mObserver); 257 } 258 } 259 260 final Uri rootsUri = DocumentsContract.buildRootsUri(authority); 261 if (!forceRefresh) { 262 // Look for roots data that we might have cached for ourselves in the 263 // long-lived system process. 264 final Bundle systemCache = resolver.getCache(rootsUri); 265 if (systemCache != null) { 266 ArrayList<RootInfo> cachedRoots = systemCache.getParcelableArrayList(TAG); 267 assert(cachedRoots != null); 268 if (!cachedRoots.isEmpty() || PERMIT_EMPTY_CACHE.contains(authority)) { 269 if (VERBOSE) Log.v(TAG, "System cache hit for " + authority); 270 return cachedRoots; 271 } else { 272 Log.w(TAG, "Ignoring empty system cache hit for " + authority); 273 } 274 } 275 } 276 277 final ArrayList<RootInfo> roots = new ArrayList<>(); 278 ContentProviderClient client = null; 279 Cursor cursor = null; 280 try { 281 client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority); 282 cursor = client.query(rootsUri, null, null, null, null); 283 while (cursor.moveToNext()) { 284 final RootInfo root = RootInfo.fromRootsCursor(authority, cursor); 285 roots.add(root); 286 } 287 } catch (Exception e) { 288 Log.w(TAG, "Failed to load some roots from " + authority, e); 289 // We didn't load every root from the provider. Don't put it to 290 // system cache so that we'll try loading them again next time even 291 // if forceRefresh is false. 292 return roots; 293 } finally { 294 IoUtils.closeQuietly(cursor); 295 ContentProviderClient.releaseQuietly(client); 296 } 297 298 // Cache these freshly parsed roots over in the long-lived system 299 // process, in case our process goes away. The system takes care of 300 // invalidating the cache if the package or Uri changes. 301 final Bundle systemCache = new Bundle(); 302 if (roots.isEmpty() && !PERMIT_EMPTY_CACHE.contains(authority)) { 303 Log.i(TAG, "Provider returned no roots. Possibly naughty: " + authority); 304 } else { 305 systemCache.putParcelableArrayList(TAG, roots); 306 resolver.putCache(rootsUri, systemCache); 307 } 308 309 return roots; 310 } 311 312 @Override getRootOneshot(String authority, String rootId)313 public RootInfo getRootOneshot(String authority, String rootId) { 314 return getRootOneshot(authority, rootId, false); 315 } 316 getRootOneshot(String authority, String rootId, boolean forceRefresh)317 public RootInfo getRootOneshot(String authority, String rootId, boolean forceRefresh) { 318 synchronized (mLock) { 319 RootInfo root = forceRefresh ? null : getRootLocked(authority, rootId); 320 if (root == null) { 321 mRoots.putAll(authority, loadRootsForAuthority( 322 mContext.getContentResolver(), authority, forceRefresh)); 323 root = getRootLocked(authority, rootId); 324 } 325 return root; 326 } 327 } 328 getRootBlocking(String authority, String rootId)329 public RootInfo getRootBlocking(String authority, String rootId) { 330 waitForFirstLoad(); 331 loadStoppedAuthorities(); 332 synchronized (mLock) { 333 return getRootLocked(authority, rootId); 334 } 335 } 336 getRootLocked(String authority, String rootId)337 private RootInfo getRootLocked(String authority, String rootId) { 338 for (RootInfo root : mRoots.get(authority)) { 339 if (Objects.equals(root.rootId, rootId)) { 340 return root; 341 } 342 } 343 return null; 344 } 345 346 @Override getRecentsRoot()347 public RootInfo getRecentsRoot() { 348 return mRecentsRoot; 349 } 350 isRecentsRoot(RootInfo root)351 public boolean isRecentsRoot(RootInfo root) { 352 return mRecentsRoot.equals(root); 353 } 354 355 @Override getRootsBlocking()356 public Collection<RootInfo> getRootsBlocking() { 357 waitForFirstLoad(); 358 loadStoppedAuthorities(); 359 synchronized (mLock) { 360 return mRoots.values(); 361 } 362 } 363 364 @Override getMatchingRootsBlocking(State state)365 public Collection<RootInfo> getMatchingRootsBlocking(State state) { 366 waitForFirstLoad(); 367 loadStoppedAuthorities(); 368 synchronized (mLock) { 369 return ProvidersAccess.getMatchingRoots(mRoots.values(), state); 370 } 371 } 372 373 @Override getRootsForAuthorityBlocking(String authority)374 public Collection<RootInfo> getRootsForAuthorityBlocking(String authority) { 375 waitForFirstLoad(); 376 loadStoppedAuthority(authority); 377 synchronized (mLock) { 378 final Collection<RootInfo> roots = mRoots.get(authority); 379 return roots != null ? roots : Collections.<RootInfo>emptyList(); 380 } 381 } 382 getDefaultRootBlocking(State state)383 public RootInfo getDefaultRootBlocking(State state) { 384 for (RootInfo root : ProvidersAccess.getMatchingRoots(getRootsBlocking(), state)) { 385 if (root.isDownloads()) { 386 return root; 387 } 388 } 389 return mRecentsRoot; 390 } 391 logCache()392 public void logCache() { 393 ContentResolver resolver = mContext.getContentResolver(); 394 StringBuilder output = new StringBuilder(); 395 396 for (String authority : mObservedAuthoritiesDetails.keySet()) { 397 List<String> roots = new ArrayList<>(); 398 Uri rootsUri = DocumentsContract.buildRootsUri(authority); 399 Bundle systemCache = resolver.getCache(rootsUri); 400 if (systemCache != null) { 401 ArrayList<RootInfo> cachedRoots = systemCache.getParcelableArrayList(TAG); 402 for (RootInfo root : cachedRoots) { 403 roots.add(root.toDebugString()); 404 } 405 } 406 407 output.append((output.length() == 0) ? "System cache: " : ", "); 408 output.append(authority).append("=").append(roots); 409 } 410 411 Log.i(TAG, output.toString()); 412 } 413 414 private class UpdateTask extends AsyncTask<Void, Void, Void> { 415 private final boolean mForceRefreshAll; 416 private final String mForceRefreshPackage; 417 418 private final Multimap<String, RootInfo> mTaskRoots = ArrayListMultimap.create(); 419 private final HashSet<String> mTaskStoppedAuthorities = new HashSet<>(); 420 421 /** 422 * Create task to update roots cache. 423 * 424 * @param forceRefreshAll when true, all previously cached values for 425 * all packages should be ignored. 426 * @param forceRefreshPackage when non-null, all previously cached 427 * values for this specific package should be ignored. 428 */ UpdateTask(boolean forceRefreshAll, String forceRefreshPackage)429 public UpdateTask(boolean forceRefreshAll, String forceRefreshPackage) { 430 mForceRefreshAll = forceRefreshAll; 431 mForceRefreshPackage = forceRefreshPackage; 432 } 433 434 @Override doInBackground(Void... params)435 protected Void doInBackground(Void... params) { 436 final long start = SystemClock.elapsedRealtime(); 437 438 mTaskRoots.put(mRecentsRoot.authority, mRecentsRoot); 439 440 final PackageManager pm = mContext.getPackageManager(); 441 442 // Pick up provider with action string 443 final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE); 444 final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, 0); 445 for (ResolveInfo info : providers) { 446 handleDocumentsProvider(info.providerInfo); 447 } 448 449 final long delta = SystemClock.elapsedRealtime() - start; 450 if (VERBOSE) Log.v(TAG, 451 "Update found " + mTaskRoots.size() + " roots in " + delta + "ms"); 452 synchronized (mLock) { 453 mFirstLoadDone = true; 454 if (mBootCompletedResult != null) { 455 mBootCompletedResult.finish(); 456 mBootCompletedResult = null; 457 } 458 mRoots = mTaskRoots; 459 mStoppedAuthorities = mTaskStoppedAuthorities; 460 } 461 mFirstLoad.countDown(); 462 LocalBroadcastManager.getInstance(mContext).sendBroadcast(new Intent(BROADCAST_ACTION)); 463 return null; 464 } 465 handleDocumentsProvider(ProviderInfo info)466 private void handleDocumentsProvider(ProviderInfo info) { 467 // Ignore stopped packages for now; we might query them 468 // later during UI interaction. 469 if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) { 470 if (VERBOSE) Log.v(TAG, "Ignoring stopped authority " + info.authority); 471 mTaskStoppedAuthorities.add(info.authority); 472 return; 473 } 474 475 final boolean forceRefresh = mForceRefreshAll 476 || Objects.equals(info.packageName, mForceRefreshPackage); 477 mTaskRoots.putAll(info.authority, loadRootsForAuthority(mContext.getContentResolver(), 478 info.authority, forceRefresh)); 479 } 480 481 } 482 483 private static class PackageDetails { 484 private String applicationName; 485 private String packageName; 486 PackageDetails(String appName, String pckgName)487 public PackageDetails(String appName, String pckgName) { 488 applicationName = appName; 489 packageName = pckgName; 490 } 491 } 492 } 493