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.DocumentsActivity.TAG; 20 21 import android.content.ContentProviderClient; 22 import android.content.ContentResolver; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.ApplicationInfo; 26 import android.content.pm.PackageManager; 27 import android.content.pm.ProviderInfo; 28 import android.content.pm.ResolveInfo; 29 import android.database.ContentObserver; 30 import android.database.Cursor; 31 import android.net.Uri; 32 import android.os.AsyncTask; 33 import android.os.Handler; 34 import android.os.SystemClock; 35 import android.provider.DocumentsContract; 36 import android.provider.DocumentsContract.Root; 37 import android.util.Log; 38 39 import com.android.documentsui.BaseActivity.State; 40 import com.android.documentsui.model.RootInfo; 41 import com.android.internal.annotations.GuardedBy; 42 import com.android.internal.annotations.VisibleForTesting; 43 import com.google.android.collect.Lists; 44 import com.google.android.collect.Sets; 45 import com.google.common.collect.ArrayListMultimap; 46 import com.google.common.collect.Multimap; 47 48 import libcore.io.IoUtils; 49 50 import java.util.Collection; 51 import java.util.HashSet; 52 import java.util.List; 53 import java.util.Objects; 54 import java.util.concurrent.CountDownLatch; 55 import java.util.concurrent.TimeUnit; 56 57 /** 58 * Cache of known storage backends and their roots. 59 */ 60 public class RootsCache { 61 private static final boolean LOGD = false; 62 63 public static final Uri sNotificationUri = Uri.parse( 64 "content://com.android.documentsui.roots/"); 65 66 private final Context mContext; 67 private final ContentObserver mObserver; 68 69 private final RootInfo mRecentsRoot = new RootInfo(); 70 71 private final Object mLock = new Object(); 72 private final CountDownLatch mFirstLoad = new CountDownLatch(1); 73 74 @GuardedBy("mLock") 75 private Multimap<String, RootInfo> mRoots = ArrayListMultimap.create(); 76 @GuardedBy("mLock") 77 private HashSet<String> mStoppedAuthorities = Sets.newHashSet(); 78 79 @GuardedBy("mObservedAuthorities") 80 private final HashSet<String> mObservedAuthorities = Sets.newHashSet(); 81 RootsCache(Context context)82 public RootsCache(Context context) { 83 mContext = context; 84 mObserver = new RootsChangedObserver(); 85 } 86 87 private class RootsChangedObserver extends ContentObserver { RootsChangedObserver()88 public RootsChangedObserver() { 89 super(new Handler()); 90 } 91 92 @Override onChange(boolean selfChange, Uri uri)93 public void onChange(boolean selfChange, Uri uri) { 94 if (LOGD) Log.d(TAG, "Updating roots due to change at " + uri); 95 updateAuthorityAsync(uri.getAuthority()); 96 } 97 } 98 99 /** 100 * Gather roots from all known storage providers. 101 */ updateAsync()102 public void updateAsync() { 103 // Special root for recents 104 mRecentsRoot.authority = null; 105 mRecentsRoot.rootId = null; 106 mRecentsRoot.derivedIcon = R.drawable.ic_root_recent; 107 mRecentsRoot.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE 108 | Root.FLAG_SUPPORTS_IS_CHILD; 109 mRecentsRoot.title = mContext.getString(R.string.root_recent); 110 mRecentsRoot.availableBytes = -1; 111 112 new UpdateTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 113 } 114 115 /** 116 * Gather roots from storage providers belonging to given package name. 117 */ updatePackageAsync(String packageName)118 public void updatePackageAsync(String packageName) { 119 new UpdateTask(packageName).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 120 } 121 122 /** 123 * Gather roots from storage providers belonging to given authority. 124 */ updateAuthorityAsync(String authority)125 public void updateAuthorityAsync(String authority) { 126 final ProviderInfo info = mContext.getPackageManager().resolveContentProvider(authority, 0); 127 if (info != null) { 128 updatePackageAsync(info.packageName); 129 } 130 } 131 waitForFirstLoad()132 private void waitForFirstLoad() { 133 boolean success = false; 134 try { 135 success = mFirstLoad.await(15, TimeUnit.SECONDS); 136 } catch (InterruptedException e) { 137 } 138 if (!success) { 139 Log.w(TAG, "Timeout waiting for first update"); 140 } 141 } 142 143 /** 144 * Load roots from authorities that are in stopped state. Normal 145 * {@link UpdateTask} passes ignore stopped applications. 146 */ loadStoppedAuthorities()147 private void loadStoppedAuthorities() { 148 final ContentResolver resolver = mContext.getContentResolver(); 149 synchronized (mLock) { 150 for (String authority : mStoppedAuthorities) { 151 if (LOGD) Log.d(TAG, "Loading stopped authority " + authority); 152 mRoots.putAll(authority, loadRootsForAuthority(resolver, authority)); 153 } 154 mStoppedAuthorities.clear(); 155 } 156 } 157 158 private class UpdateTask extends AsyncTask<Void, Void, Void> { 159 private final String mFilterPackage; 160 161 private final Multimap<String, RootInfo> mTaskRoots = ArrayListMultimap.create(); 162 private final HashSet<String> mTaskStoppedAuthorities = Sets.newHashSet(); 163 164 /** 165 * Update all roots. 166 */ UpdateTask()167 public UpdateTask() { 168 this(null); 169 } 170 171 /** 172 * Only update roots belonging to given package name. Other roots will 173 * be copied from cached {@link #mRoots} values. 174 */ UpdateTask(String filterPackage)175 public UpdateTask(String filterPackage) { 176 mFilterPackage = filterPackage; 177 } 178 179 @Override doInBackground(Void... params)180 protected Void doInBackground(Void... params) { 181 final long start = SystemClock.elapsedRealtime(); 182 183 if (mFilterPackage != null) { 184 // Need at least first load, since we're going to be using 185 // previously cached values for non-matching packages. 186 waitForFirstLoad(); 187 } 188 189 mTaskRoots.put(mRecentsRoot.authority, mRecentsRoot); 190 191 final ContentResolver resolver = mContext.getContentResolver(); 192 final PackageManager pm = mContext.getPackageManager(); 193 194 // Pick up provider with action string 195 final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE); 196 final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, 0); 197 for (ResolveInfo info : providers) { 198 handleDocumentsProvider(info.providerInfo); 199 } 200 201 final long delta = SystemClock.elapsedRealtime() - start; 202 Log.d(TAG, "Update found " + mTaskRoots.size() + " roots in " + delta + "ms"); 203 synchronized (mLock) { 204 mRoots = mTaskRoots; 205 mStoppedAuthorities = mTaskStoppedAuthorities; 206 } 207 mFirstLoad.countDown(); 208 resolver.notifyChange(sNotificationUri, null, false); 209 return null; 210 } 211 handleDocumentsProvider(ProviderInfo info)212 private void handleDocumentsProvider(ProviderInfo info) { 213 // Ignore stopped packages for now; we might query them 214 // later during UI interaction. 215 if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) { 216 if (LOGD) Log.d(TAG, "Ignoring stopped authority " + info.authority); 217 mTaskStoppedAuthorities.add(info.authority); 218 return; 219 } 220 221 // Try using cached roots if filtering 222 boolean cacheHit = false; 223 if (mFilterPackage != null && !mFilterPackage.equals(info.packageName)) { 224 synchronized (mLock) { 225 if (mTaskRoots.putAll(info.authority, mRoots.get(info.authority))) { 226 if (LOGD) Log.d(TAG, "Used cached roots for " + info.authority); 227 cacheHit = true; 228 } 229 } 230 } 231 232 // Cache miss, or loading everything 233 if (!cacheHit) { 234 mTaskRoots.putAll(info.authority, 235 loadRootsForAuthority(mContext.getContentResolver(), info.authority)); 236 } 237 } 238 } 239 240 /** 241 * Bring up requested provider and query for all active roots. 242 */ loadRootsForAuthority(ContentResolver resolver, String authority)243 private Collection<RootInfo> loadRootsForAuthority(ContentResolver resolver, String authority) { 244 if (LOGD) Log.d(TAG, "Loading roots for " + authority); 245 246 synchronized (mObservedAuthorities) { 247 if (mObservedAuthorities.add(authority)) { 248 // Watch for any future updates 249 final Uri rootsUri = DocumentsContract.buildRootsUri(authority); 250 mContext.getContentResolver().registerContentObserver(rootsUri, true, mObserver); 251 } 252 } 253 254 final List<RootInfo> roots = Lists.newArrayList(); 255 final Uri rootsUri = DocumentsContract.buildRootsUri(authority); 256 257 ContentProviderClient client = null; 258 Cursor cursor = null; 259 try { 260 client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority); 261 cursor = client.query(rootsUri, null, null, null, null); 262 while (cursor.moveToNext()) { 263 final RootInfo root = RootInfo.fromRootsCursor(authority, cursor); 264 roots.add(root); 265 } 266 } catch (Exception e) { 267 Log.w(TAG, "Failed to load some roots from " + authority + ": " + e); 268 } finally { 269 IoUtils.closeQuietly(cursor); 270 ContentProviderClient.releaseQuietly(client); 271 } 272 return roots; 273 } 274 275 /** 276 * Return the requested {@link RootInfo}, but only loading the roots for the 277 * requested authority. This is useful when we want to load fast without 278 * waiting for all the other roots to come back. 279 */ getRootOneshot(String authority, String rootId)280 public RootInfo getRootOneshot(String authority, String rootId) { 281 synchronized (mLock) { 282 RootInfo root = getRootLocked(authority, rootId); 283 if (root == null) { 284 mRoots.putAll( 285 authority, loadRootsForAuthority(mContext.getContentResolver(), authority)); 286 root = getRootLocked(authority, rootId); 287 } 288 return root; 289 } 290 } 291 getRootBlocking(String authority, String rootId)292 public RootInfo getRootBlocking(String authority, String rootId) { 293 waitForFirstLoad(); 294 loadStoppedAuthorities(); 295 synchronized (mLock) { 296 return getRootLocked(authority, rootId); 297 } 298 } 299 getRootLocked(String authority, String rootId)300 private RootInfo getRootLocked(String authority, String rootId) { 301 for (RootInfo root : mRoots.get(authority)) { 302 if (Objects.equals(root.rootId, rootId)) { 303 return root; 304 } 305 } 306 return null; 307 } 308 isIconUniqueBlocking(RootInfo root)309 public boolean isIconUniqueBlocking(RootInfo root) { 310 waitForFirstLoad(); 311 loadStoppedAuthorities(); 312 synchronized (mLock) { 313 final int rootIcon = root.derivedIcon != 0 ? root.derivedIcon : root.icon; 314 for (RootInfo test : mRoots.get(root.authority)) { 315 if (Objects.equals(test.rootId, root.rootId)) { 316 continue; 317 } 318 final int testIcon = test.derivedIcon != 0 ? test.derivedIcon : test.icon; 319 if (testIcon == rootIcon) { 320 return false; 321 } 322 } 323 return true; 324 } 325 } 326 getRecentsRoot()327 public RootInfo getRecentsRoot() { 328 return mRecentsRoot; 329 } 330 isRecentsRoot(RootInfo root)331 public boolean isRecentsRoot(RootInfo root) { 332 return mRecentsRoot == root; 333 } 334 getRootsBlocking()335 public Collection<RootInfo> getRootsBlocking() { 336 waitForFirstLoad(); 337 loadStoppedAuthorities(); 338 synchronized (mLock) { 339 return mRoots.values(); 340 } 341 } 342 getMatchingRootsBlocking(State state)343 public Collection<RootInfo> getMatchingRootsBlocking(State state) { 344 waitForFirstLoad(); 345 loadStoppedAuthorities(); 346 synchronized (mLock) { 347 return getMatchingRoots(mRoots.values(), state); 348 } 349 } 350 351 @VisibleForTesting getMatchingRoots(Collection<RootInfo> roots, State state)352 static List<RootInfo> getMatchingRoots(Collection<RootInfo> roots, State state) { 353 final List<RootInfo> matching = Lists.newArrayList(); 354 for (RootInfo root : roots) { 355 final boolean supportsCreate = (root.flags & Root.FLAG_SUPPORTS_CREATE) != 0; 356 final boolean supportsIsChild = (root.flags & Root.FLAG_SUPPORTS_IS_CHILD) != 0; 357 final boolean advanced = (root.flags & Root.FLAG_ADVANCED) != 0; 358 final boolean localOnly = (root.flags & Root.FLAG_LOCAL_ONLY) != 0; 359 final boolean empty = (root.flags & Root.FLAG_EMPTY) != 0; 360 361 // Exclude read-only devices when creating 362 if (state.action == State.ACTION_CREATE && !supportsCreate) continue; 363 if (state.action == State.ACTION_OPEN_COPY_DESTINATION && !supportsCreate) continue; 364 // Exclude roots that don't support directory picking 365 if (state.action == State.ACTION_OPEN_TREE && !supportsIsChild) continue; 366 // Exclude advanced devices when not requested 367 if (!state.showAdvanced && advanced) continue; 368 // Exclude non-local devices when local only 369 if (state.localOnly && !localOnly) continue; 370 // Exclude downloads roots that don't support directory creation 371 // TODO: Add flag to check the root supports directory creation or not. 372 if (state.directoryCopy && root.isDownloads()) continue; 373 // Only show empty roots when creating 374 if ((state.action != State.ACTION_CREATE || 375 state.action != State.ACTION_OPEN_TREE || 376 state.action != State.ACTION_OPEN_COPY_DESTINATION) && empty) continue; 377 378 // Only include roots that serve requested content 379 final boolean overlap = 380 MimePredicate.mimeMatches(root.derivedMimeTypes, state.acceptMimes) || 381 MimePredicate.mimeMatches(state.acceptMimes, root.derivedMimeTypes); 382 if (!overlap) { 383 continue; 384 } 385 386 // Exclude roots from the calling package. 387 if (state.excludedAuthorities.contains(root.authority)) { 388 if (LOGD) Log.d(TAG, "Excluding root " + root.authority + " from calling package."); 389 continue; 390 } 391 392 matching.add(root); 393 } 394 return matching; 395 } 396 } 397