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