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.externalstorage; 18 19 import android.content.ContentResolver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.res.AssetFileDescriptor; 23 import android.database.Cursor; 24 import android.database.MatrixCursor; 25 import android.database.MatrixCursor.RowBuilder; 26 import android.graphics.Point; 27 import android.net.Uri; 28 import android.os.CancellationSignal; 29 import android.os.FileObserver; 30 import android.os.FileUtils; 31 import android.os.Handler; 32 import android.os.ParcelFileDescriptor; 33 import android.os.ParcelFileDescriptor.OnCloseListener; 34 import android.os.UserHandle; 35 import android.os.storage.StorageManager; 36 import android.os.storage.VolumeInfo; 37 import android.provider.DocumentsContract; 38 import android.provider.DocumentsContract.Document; 39 import android.provider.DocumentsContract.Root; 40 import android.provider.DocumentsProvider; 41 import android.text.TextUtils; 42 import android.util.ArrayMap; 43 import android.util.DebugUtils; 44 import android.util.Log; 45 import android.webkit.MimeTypeMap; 46 47 import com.android.internal.annotations.GuardedBy; 48 import com.android.internal.util.IndentingPrintWriter; 49 50 import java.io.File; 51 import java.io.FileDescriptor; 52 import java.io.FileNotFoundException; 53 import java.io.IOException; 54 import java.io.PrintWriter; 55 import java.util.LinkedList; 56 import java.util.List; 57 58 public class ExternalStorageProvider extends DocumentsProvider { 59 private static final String TAG = "ExternalStorage"; 60 61 private static final boolean LOG_INOTIFY = false; 62 63 public static final String AUTHORITY = "com.android.externalstorage.documents"; 64 65 private static final Uri BASE_URI = 66 new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build(); 67 68 // docId format: root:path/to/file 69 70 private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { 71 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE, 72 Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES, 73 }; 74 75 private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { 76 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, 77 Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE, 78 }; 79 80 private static class RootInfo { 81 public String rootId; 82 public int flags; 83 public String title; 84 public String docId; 85 public File visiblePath; 86 public File path; 87 } 88 89 private static final String ROOT_ID_PRIMARY_EMULATED = "primary"; 90 91 private StorageManager mStorageManager; 92 private Handler mHandler; 93 94 private final Object mRootsLock = new Object(); 95 96 @GuardedBy("mRootsLock") 97 private ArrayMap<String, RootInfo> mRoots = new ArrayMap<>(); 98 99 @GuardedBy("mObservers") 100 private ArrayMap<File, DirectoryObserver> mObservers = new ArrayMap<>(); 101 102 @Override onCreate()103 public boolean onCreate() { 104 mStorageManager = (StorageManager) getContext().getSystemService(Context.STORAGE_SERVICE); 105 mHandler = new Handler(); 106 107 updateVolumes(); 108 return true; 109 } 110 updateVolumes()111 public void updateVolumes() { 112 synchronized (mRootsLock) { 113 updateVolumesLocked(); 114 } 115 } 116 updateVolumesLocked()117 private void updateVolumesLocked() { 118 mRoots.clear(); 119 120 final int userId = UserHandle.myUserId(); 121 final List<VolumeInfo> volumes = mStorageManager.getVolumes(); 122 for (VolumeInfo volume : volumes) { 123 if (!volume.isMountedReadable()) continue; 124 125 final String rootId; 126 final String title; 127 if (volume.getType() == VolumeInfo.TYPE_EMULATED) { 128 // We currently only support a single emulated volume mounted at 129 // a time, and it's always considered the primary 130 rootId = ROOT_ID_PRIMARY_EMULATED; 131 if (VolumeInfo.ID_EMULATED_INTERNAL.equals(volume.getId())) { 132 title = getContext().getString(R.string.root_internal_storage); 133 } else { 134 final VolumeInfo privateVol = mStorageManager.findPrivateForEmulated(volume); 135 title = mStorageManager.getBestVolumeDescription(privateVol); 136 } 137 } else if (volume.getType() == VolumeInfo.TYPE_PUBLIC) { 138 rootId = volume.getFsUuid(); 139 title = mStorageManager.getBestVolumeDescription(volume); 140 } else { 141 // Unsupported volume; ignore 142 continue; 143 } 144 145 if (TextUtils.isEmpty(rootId)) { 146 Log.d(TAG, "Missing UUID for " + volume.getId() + "; skipping"); 147 continue; 148 } 149 if (mRoots.containsKey(rootId)) { 150 Log.w(TAG, "Duplicate UUID " + rootId + " for " + volume.getId() + "; skipping"); 151 continue; 152 } 153 154 try { 155 final RootInfo root = new RootInfo(); 156 mRoots.put(rootId, root); 157 158 root.rootId = rootId; 159 root.flags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED 160 | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD; 161 root.title = title; 162 if (volume.getType() == VolumeInfo.TYPE_PUBLIC) { 163 root.flags |= Root.FLAG_HAS_SETTINGS; 164 } 165 if (volume.isVisibleForRead(userId)) { 166 root.visiblePath = volume.getPathForUser(userId); 167 } else { 168 root.visiblePath = null; 169 } 170 root.path = volume.getInternalPathForUser(userId); 171 root.docId = getDocIdForFile(root.path); 172 173 } catch (FileNotFoundException e) { 174 throw new IllegalStateException(e); 175 } 176 } 177 178 Log.d(TAG, "After updating volumes, found " + mRoots.size() + " active roots"); 179 180 // Note this affects content://com.android.externalstorage.documents/root/39BD-07C5 181 // as well as content://com.android.externalstorage.documents/document/*/children, 182 // so just notify on content://com.android.externalstorage.documents/. 183 getContext().getContentResolver().notifyChange(BASE_URI, null, false); 184 } 185 resolveRootProjection(String[] projection)186 private static String[] resolveRootProjection(String[] projection) { 187 return projection != null ? projection : DEFAULT_ROOT_PROJECTION; 188 } 189 resolveDocumentProjection(String[] projection)190 private static String[] resolveDocumentProjection(String[] projection) { 191 return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION; 192 } 193 getDocIdForFile(File file)194 private String getDocIdForFile(File file) throws FileNotFoundException { 195 String path = file.getAbsolutePath(); 196 197 // Find the most-specific root path 198 String mostSpecificId = null; 199 String mostSpecificPath = null; 200 synchronized (mRootsLock) { 201 for (int i = 0; i < mRoots.size(); i++) { 202 final String rootId = mRoots.keyAt(i); 203 final String rootPath = mRoots.valueAt(i).path.getAbsolutePath(); 204 if (path.startsWith(rootPath) && (mostSpecificPath == null 205 || rootPath.length() > mostSpecificPath.length())) { 206 mostSpecificId = rootId; 207 mostSpecificPath = rootPath; 208 } 209 } 210 } 211 212 if (mostSpecificPath == null) { 213 throw new FileNotFoundException("Failed to find root that contains " + path); 214 } 215 216 // Start at first char of path under root 217 final String rootPath = mostSpecificPath; 218 if (rootPath.equals(path)) { 219 path = ""; 220 } else if (rootPath.endsWith("/")) { 221 path = path.substring(rootPath.length()); 222 } else { 223 path = path.substring(rootPath.length() + 1); 224 } 225 226 return mostSpecificId + ':' + path; 227 } 228 getFileForDocId(String docId)229 private File getFileForDocId(String docId) throws FileNotFoundException { 230 return getFileForDocId(docId, false); 231 } 232 getFileForDocId(String docId, boolean visible)233 private File getFileForDocId(String docId, boolean visible) throws FileNotFoundException { 234 final int splitIndex = docId.indexOf(':', 1); 235 final String tag = docId.substring(0, splitIndex); 236 final String path = docId.substring(splitIndex + 1); 237 238 RootInfo root; 239 synchronized (mRootsLock) { 240 root = mRoots.get(tag); 241 } 242 if (root == null) { 243 throw new FileNotFoundException("No root for " + tag); 244 } 245 246 File target = visible ? root.visiblePath : root.path; 247 if (target == null) { 248 return null; 249 } 250 if (!target.exists()) { 251 target.mkdirs(); 252 } 253 target = new File(target, path); 254 if (!target.exists()) { 255 throw new FileNotFoundException("Missing file for " + docId + " at " + target); 256 } 257 return target; 258 } 259 includeFile(MatrixCursor result, String docId, File file)260 private void includeFile(MatrixCursor result, String docId, File file) 261 throws FileNotFoundException { 262 if (docId == null) { 263 docId = getDocIdForFile(file); 264 } else { 265 file = getFileForDocId(docId); 266 } 267 268 int flags = 0; 269 270 if (file.canWrite()) { 271 if (file.isDirectory()) { 272 flags |= Document.FLAG_DIR_SUPPORTS_CREATE; 273 flags |= Document.FLAG_SUPPORTS_DELETE; 274 flags |= Document.FLAG_SUPPORTS_RENAME; 275 } else { 276 flags |= Document.FLAG_SUPPORTS_WRITE; 277 flags |= Document.FLAG_SUPPORTS_DELETE; 278 flags |= Document.FLAG_SUPPORTS_RENAME; 279 } 280 } 281 282 final String displayName = file.getName(); 283 final String mimeType = getTypeForFile(file); 284 if (mimeType.startsWith("image/")) { 285 flags |= Document.FLAG_SUPPORTS_THUMBNAIL; 286 } 287 288 final RowBuilder row = result.newRow(); 289 row.add(Document.COLUMN_DOCUMENT_ID, docId); 290 row.add(Document.COLUMN_DISPLAY_NAME, displayName); 291 row.add(Document.COLUMN_SIZE, file.length()); 292 row.add(Document.COLUMN_MIME_TYPE, mimeType); 293 row.add(Document.COLUMN_FLAGS, flags); 294 295 // Only publish dates reasonably after epoch 296 long lastModified = file.lastModified(); 297 if (lastModified > 31536000000L) { 298 row.add(Document.COLUMN_LAST_MODIFIED, lastModified); 299 } 300 } 301 302 @Override queryRoots(String[] projection)303 public Cursor queryRoots(String[] projection) throws FileNotFoundException { 304 final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); 305 synchronized (mRootsLock) { 306 for (RootInfo root : mRoots.values()) { 307 final RowBuilder row = result.newRow(); 308 row.add(Root.COLUMN_ROOT_ID, root.rootId); 309 row.add(Root.COLUMN_FLAGS, root.flags); 310 row.add(Root.COLUMN_TITLE, root.title); 311 row.add(Root.COLUMN_DOCUMENT_ID, root.docId); 312 row.add(Root.COLUMN_AVAILABLE_BYTES, root.path.getFreeSpace()); 313 } 314 } 315 return result; 316 } 317 318 @Override isChildDocument(String parentDocId, String docId)319 public boolean isChildDocument(String parentDocId, String docId) { 320 try { 321 final File parent = getFileForDocId(parentDocId).getCanonicalFile(); 322 final File doc = getFileForDocId(docId).getCanonicalFile(); 323 return FileUtils.contains(parent, doc); 324 } catch (IOException e) { 325 throw new IllegalArgumentException( 326 "Failed to determine if " + docId + " is child of " + parentDocId + ": " + e); 327 } 328 } 329 330 @Override createDocument(String docId, String mimeType, String displayName)331 public String createDocument(String docId, String mimeType, String displayName) 332 throws FileNotFoundException { 333 displayName = FileUtils.buildValidFatFilename(displayName); 334 335 final File parent = getFileForDocId(docId); 336 if (!parent.isDirectory()) { 337 throw new IllegalArgumentException("Parent document isn't a directory"); 338 } 339 340 final File file = FileUtils.buildUniqueFile(parent, mimeType, displayName); 341 if (Document.MIME_TYPE_DIR.equals(mimeType)) { 342 if (!file.mkdir()) { 343 throw new IllegalStateException("Failed to mkdir " + file); 344 } 345 } else { 346 try { 347 if (!file.createNewFile()) { 348 throw new IllegalStateException("Failed to touch " + file); 349 } 350 } catch (IOException e) { 351 throw new IllegalStateException("Failed to touch " + file + ": " + e); 352 } 353 } 354 355 return getDocIdForFile(file); 356 } 357 358 @Override renameDocument(String docId, String displayName)359 public String renameDocument(String docId, String displayName) throws FileNotFoundException { 360 // Since this provider treats renames as generating a completely new 361 // docId, we're okay with letting the MIME type change. 362 displayName = FileUtils.buildValidFatFilename(displayName); 363 364 final File before = getFileForDocId(docId); 365 final File after = new File(before.getParentFile(), displayName); 366 if (after.exists()) { 367 throw new IllegalStateException("Already exists " + after); 368 } 369 if (!before.renameTo(after)) { 370 throw new IllegalStateException("Failed to rename to " + after); 371 } 372 final String afterDocId = getDocIdForFile(after); 373 if (!TextUtils.equals(docId, afterDocId)) { 374 return afterDocId; 375 } else { 376 return null; 377 } 378 } 379 380 @Override deleteDocument(String docId)381 public void deleteDocument(String docId) throws FileNotFoundException { 382 final File file = getFileForDocId(docId); 383 if (file.isDirectory()) { 384 FileUtils.deleteContents(file); 385 } 386 if (!file.delete()) { 387 throw new IllegalStateException("Failed to delete " + file); 388 } 389 } 390 391 @Override queryDocument(String documentId, String[] projection)392 public Cursor queryDocument(String documentId, String[] projection) 393 throws FileNotFoundException { 394 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 395 includeFile(result, documentId, null); 396 return result; 397 } 398 399 @Override queryChildDocuments( String parentDocumentId, String[] projection, String sortOrder)400 public Cursor queryChildDocuments( 401 String parentDocumentId, String[] projection, String sortOrder) 402 throws FileNotFoundException { 403 final File parent = getFileForDocId(parentDocumentId); 404 final MatrixCursor result = new DirectoryCursor( 405 resolveDocumentProjection(projection), parentDocumentId, parent); 406 for (File file : parent.listFiles()) { 407 includeFile(result, null, file); 408 } 409 return result; 410 } 411 412 @Override querySearchDocuments(String rootId, String query, String[] projection)413 public Cursor querySearchDocuments(String rootId, String query, String[] projection) 414 throws FileNotFoundException { 415 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 416 417 final File parent; 418 synchronized (mRootsLock) { 419 parent = mRoots.get(rootId).path; 420 } 421 422 final LinkedList<File> pending = new LinkedList<File>(); 423 pending.add(parent); 424 while (!pending.isEmpty() && result.getCount() < 24) { 425 final File file = pending.removeFirst(); 426 if (file.isDirectory()) { 427 for (File child : file.listFiles()) { 428 pending.add(child); 429 } 430 } 431 if (file.getName().toLowerCase().contains(query)) { 432 includeFile(result, null, file); 433 } 434 } 435 return result; 436 } 437 438 @Override getDocumentType(String documentId)439 public String getDocumentType(String documentId) throws FileNotFoundException { 440 final File file = getFileForDocId(documentId); 441 return getTypeForFile(file); 442 } 443 444 @Override openDocument( String documentId, String mode, CancellationSignal signal)445 public ParcelFileDescriptor openDocument( 446 String documentId, String mode, CancellationSignal signal) 447 throws FileNotFoundException { 448 final File file = getFileForDocId(documentId); 449 final File visibleFile = getFileForDocId(documentId, true); 450 451 final int pfdMode = ParcelFileDescriptor.parseMode(mode); 452 if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY || visibleFile == null) { 453 return ParcelFileDescriptor.open(file, pfdMode); 454 } else { 455 try { 456 // When finished writing, kick off media scanner 457 return ParcelFileDescriptor.open(file, pfdMode, mHandler, new OnCloseListener() { 458 @Override 459 public void onClose(IOException e) { 460 final Intent intent = new Intent( 461 Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); 462 intent.setData(Uri.fromFile(visibleFile)); 463 getContext().sendBroadcast(intent); 464 } 465 }); 466 } catch (IOException e) { 467 throw new FileNotFoundException("Failed to open for writing: " + e); 468 } 469 } 470 } 471 472 @Override 473 public AssetFileDescriptor openDocumentThumbnail( 474 String documentId, Point sizeHint, CancellationSignal signal) 475 throws FileNotFoundException { 476 final File file = getFileForDocId(documentId); 477 return DocumentsContract.openImageThumbnail(file); 478 } 479 480 @Override 481 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 482 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 160); 483 synchronized (mRootsLock) { 484 for (int i = 0; i < mRoots.size(); i++) { 485 final RootInfo root = mRoots.valueAt(i); 486 pw.println("Root{" + root.rootId + "}:"); 487 pw.increaseIndent(); 488 pw.printPair("flags", DebugUtils.flagsToString(Root.class, "FLAG_", root.flags)); 489 pw.println(); 490 pw.printPair("title", root.title); 491 pw.printPair("docId", root.docId); 492 pw.println(); 493 pw.printPair("path", root.path); 494 pw.printPair("visiblePath", root.visiblePath); 495 pw.decreaseIndent(); 496 pw.println(); 497 } 498 } 499 } 500 501 private static String getTypeForFile(File file) { 502 if (file.isDirectory()) { 503 return Document.MIME_TYPE_DIR; 504 } else { 505 return getTypeForName(file.getName()); 506 } 507 } 508 509 private static String getTypeForName(String name) { 510 final int lastDot = name.lastIndexOf('.'); 511 if (lastDot >= 0) { 512 final String extension = name.substring(lastDot + 1).toLowerCase(); 513 final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 514 if (mime != null) { 515 return mime; 516 } 517 } 518 519 return "application/octet-stream"; 520 } 521 522 private void startObserving(File file, Uri notifyUri) { 523 synchronized (mObservers) { 524 DirectoryObserver observer = mObservers.get(file); 525 if (observer == null) { 526 observer = new DirectoryObserver( 527 file, getContext().getContentResolver(), notifyUri); 528 observer.startWatching(); 529 mObservers.put(file, observer); 530 } 531 observer.mRefCount++; 532 533 if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer); 534 } 535 } 536 537 private void stopObserving(File file) { 538 synchronized (mObservers) { 539 DirectoryObserver observer = mObservers.get(file); 540 if (observer == null) return; 541 542 observer.mRefCount--; 543 if (observer.mRefCount == 0) { 544 mObservers.remove(file); 545 observer.stopWatching(); 546 } 547 548 if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer); 549 } 550 } 551 552 private static class DirectoryObserver extends FileObserver { 553 private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO 554 | CREATE | DELETE | DELETE_SELF | MOVE_SELF; 555 556 private final File mFile; 557 private final ContentResolver mResolver; 558 private final Uri mNotifyUri; 559 560 private int mRefCount = 0; 561 562 public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) { 563 super(file.getAbsolutePath(), NOTIFY_EVENTS); 564 mFile = file; 565 mResolver = resolver; 566 mNotifyUri = notifyUri; 567 } 568 569 @Override 570 public void onEvent(int event, String path) { 571 if ((event & NOTIFY_EVENTS) != 0) { 572 if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path); 573 mResolver.notifyChange(mNotifyUri, null, false); 574 } 575 } 576 577 @Override 578 public String toString() { 579 return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}"; 580 } 581 } 582 583 private class DirectoryCursor extends MatrixCursor { 584 private final File mFile; 585 586 public DirectoryCursor(String[] columnNames, String docId, File file) { 587 super(columnNames); 588 589 final Uri notifyUri = DocumentsContract.buildChildDocumentsUri( 590 AUTHORITY, docId); 591 setNotificationUri(getContext().getContentResolver(), notifyUri); 592 593 mFile = file; 594 startObserving(mFile, notifyUri); 595 } 596 597 @Override 598 public void close() { 599 super.close(); 600 stopObserving(mFile); 601 } 602 } 603 } 604