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 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 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 resolver.notifyChange(sNotificationUri, null, false); 205 return null; 206 } 207 208 private void handleDocumentsProvider(ProviderInfo info) { 209 // Ignore stopped packages for now; we might query them 210 // later during UI interaction. 211 if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) { 212 if (LOGD) Log.d(TAG, "Ignoring stopped authority " + info.authority); 213 mTaskStoppedAuthorities.add(info.authority); 214 return; 215 } 216 217 // Try using cached roots if filtering 218 boolean cacheHit = false; 219 if (mFilterPackage != null && !mFilterPackage.equals(info.packageName)) { 220 synchronized (mLock) { 221 if (mTaskRoots.putAll(info.authority, mRoots.get(info.authority))) { 222 if (LOGD) Log.d(TAG, "Used cached roots for " + info.authority); 223 cacheHit = true; 224 } 225 } 226 } 227 228 // Cache miss, or loading everything 229 if (!cacheHit) { 230 mTaskRoots.putAll(info.authority, 231 loadRootsForAuthority(mContext.getContentResolver(), info.authority)); 232 } 233 } 234 } 235 236 /** 237 * Bring up requested provider and query for all active roots. 238 */ 239 private Collection<RootInfo> loadRootsForAuthority(ContentResolver resolver, String authority) { 240 if (LOGD) Log.d(TAG, "Loading roots for " + authority); 241 242 synchronized (mObservedAuthorities) { 243 if (mObservedAuthorities.add(authority)) { 244 // Watch for any future updates 245 final Uri rootsUri = DocumentsContract.buildRootsUri(authority); 246 mContext.getContentResolver().registerContentObserver(rootsUri, true, mObserver); 247 } 248 } 249 250 final List<RootInfo> roots = Lists.newArrayList(); 251 final Uri rootsUri = DocumentsContract.buildRootsUri(authority); 252 253 ContentProviderClient client = null; 254 Cursor cursor = null; 255 try { 256 client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority); 257 cursor = client.query(rootsUri, null, null, null, null); 258 while (cursor.moveToNext()) { 259 final RootInfo root = RootInfo.fromRootsCursor(authority, cursor); 260 roots.add(root); 261 } 262 } catch (Exception e) { 263 Log.w(TAG, "Failed to load some roots from " + authority + ": " + e); 264 } finally { 265 IoUtils.closeQuietly(cursor); 266 ContentProviderClient.releaseQuietly(client); 267 } 268 return roots; 269 } 270 271 /** 272 * Return the requested {@link RootInfo}, but only loading the roots for the 273 * requested authority. This is useful when we want to load fast without 274 * waiting for all the other roots to come back. 275 */ 276 public RootInfo getRootOneshot(String authority, String rootId) { 277 synchronized (mLock) { 278 RootInfo root = getRootLocked(authority, rootId); 279 if (root == null) { 280 mRoots.putAll( 281 authority, loadRootsForAuthority(mContext.getContentResolver(), authority)); 282 root = getRootLocked(authority, rootId); 283 } 284 return root; 285 } 286 } 287 288 public RootInfo getRootBlocking(String authority, String rootId) { 289 waitForFirstLoad(); 290 loadStoppedAuthorities(); 291 synchronized (mLock) { 292 return getRootLocked(authority, rootId); 293 } 294 } 295 296 private RootInfo getRootLocked(String authority, String rootId) { 297 for (RootInfo root : mRoots.get(authority)) { 298 if (Objects.equal(root.rootId, rootId)) { 299 return root; 300 } 301 } 302 return null; 303 } 304 305 public boolean isIconUniqueBlocking(RootInfo root) { 306 waitForFirstLoad(); 307 loadStoppedAuthorities(); 308 synchronized (mLock) { 309 final int rootIcon = root.derivedIcon != 0 ? root.derivedIcon : root.icon; 310 for (RootInfo test : mRoots.get(root.authority)) { 311 if (Objects.equal(test.rootId, root.rootId)) { 312 continue; 313 } 314 final int testIcon = test.derivedIcon != 0 ? test.derivedIcon : test.icon; 315 if (testIcon == rootIcon) { 316 return false; 317 } 318 } 319 return true; 320 } 321 } 322 323 public RootInfo getRecentsRoot() { 324 return mRecentsRoot; 325 } 326 327 public boolean isRecentsRoot(RootInfo root) { 328 return mRecentsRoot == root; 329 } 330 331 public Collection<RootInfo> getRootsBlocking() { 332 waitForFirstLoad(); 333 loadStoppedAuthorities(); 334 synchronized (mLock) { 335 return mRoots.values(); 336 } 337 } 338 339 public Collection<RootInfo> getMatchingRootsBlocking(State state) { 340 waitForFirstLoad(); 341 loadStoppedAuthorities(); 342 synchronized (mLock) { 343 return getMatchingRoots(mRoots.values(), state); 344 } 345 } 346 347 @VisibleForTesting 348 static List<RootInfo> getMatchingRoots(Collection<RootInfo> roots, State state) { 349 final List<RootInfo> matching = Lists.newArrayList(); 350 for (RootInfo root : roots) { 351 final boolean supportsCreate = (root.flags & Root.FLAG_SUPPORTS_CREATE) != 0; 352 final boolean advanced = (root.flags & Root.FLAG_ADVANCED) != 0; 353 final boolean localOnly = (root.flags & Root.FLAG_LOCAL_ONLY) != 0; 354 final boolean empty = (root.flags & Root.FLAG_EMPTY) != 0; 355 356 // Exclude read-only devices when creating 357 if (state.action == State.ACTION_CREATE && !supportsCreate) continue; 358 // Exclude advanced devices when not requested 359 if (!state.showAdvanced && advanced) continue; 360 // Exclude non-local devices when local only 361 if (state.localOnly && !localOnly) continue; 362 // Only show empty roots when creating 363 if (state.action != State.ACTION_CREATE && empty) continue; 364 365 // Only include roots that serve requested content 366 final boolean overlap = 367 MimePredicate.mimeMatches(root.derivedMimeTypes, state.acceptMimes) || 368 MimePredicate.mimeMatches(state.acceptMimes, root.derivedMimeTypes); 369 if (!overlap) { 370 continue; 371 } 372 373 matching.add(root); 374 } 375 return matching; 376 } 377 } 378