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