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.Bundle; 29 import android.os.CancellationSignal; 30 import android.os.Environment; 31 import android.os.FileObserver; 32 import android.os.FileUtils; 33 import android.os.Handler; 34 import android.os.ParcelFileDescriptor; 35 import android.os.ParcelFileDescriptor.OnCloseListener; 36 import android.os.UserHandle; 37 import android.os.storage.DiskInfo; 38 import android.os.storage.StorageManager; 39 import android.os.storage.VolumeInfo; 40 import android.provider.DocumentsContract; 41 import android.provider.DocumentsContract.Document; 42 import android.provider.DocumentsContract.Root; 43 import android.provider.DocumentsProvider; 44 import android.provider.MediaStore; 45 import android.provider.Settings; 46 import android.support.provider.DocumentArchiveHelper; 47 import android.text.TextUtils; 48 import android.util.ArrayMap; 49 import android.util.DebugUtils; 50 import android.util.Log; 51 import android.webkit.MimeTypeMap; 52 53 import com.android.internal.annotations.GuardedBy; 54 import com.android.internal.util.IndentingPrintWriter; 55 56 import java.io.File; 57 import java.io.FileDescriptor; 58 import java.io.FileNotFoundException; 59 import java.io.IOException; 60 import java.io.PrintWriter; 61 import java.util.LinkedList; 62 import java.util.List; 63 64 public class ExternalStorageProvider extends DocumentsProvider { 65 private static final String TAG = "ExternalStorage"; 66 67 private static final boolean DEBUG = false; 68 private static final boolean LOG_INOTIFY = false; 69 70 public static final String AUTHORITY = "com.android.externalstorage.documents"; 71 72 private static final Uri BASE_URI = 73 new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build(); 74 75 // docId format: root:path/to/file 76 77 private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { 78 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE, 79 Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES, 80 }; 81 82 private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { 83 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, 84 Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE, 85 }; 86 87 private static class RootInfo { 88 public String rootId; 89 public int flags; 90 public String title; 91 public String docId; 92 public File visiblePath; 93 public File path; 94 public boolean reportAvailableBytes = true; 95 } 96 97 private static final String ROOT_ID_PRIMARY_EMULATED = "primary"; 98 private static final String ROOT_ID_HOME = "home"; 99 100 private StorageManager mStorageManager; 101 private Handler mHandler; 102 private DocumentArchiveHelper mArchiveHelper; 103 104 private final Object mRootsLock = new Object(); 105 106 @GuardedBy("mRootsLock") 107 private ArrayMap<String, RootInfo> mRoots = new ArrayMap<>(); 108 109 @GuardedBy("mObservers") 110 private ArrayMap<File, DirectoryObserver> mObservers = new ArrayMap<>(); 111 112 @Override onCreate()113 public boolean onCreate() { 114 mStorageManager = (StorageManager) getContext().getSystemService(Context.STORAGE_SERVICE); 115 mHandler = new Handler(); 116 mArchiveHelper = new DocumentArchiveHelper(this, (char) 0); 117 118 updateVolumes(); 119 return true; 120 } 121 updateVolumes()122 public void updateVolumes() { 123 synchronized (mRootsLock) { 124 updateVolumesLocked(); 125 } 126 } 127 updateVolumesLocked()128 private void updateVolumesLocked() { 129 mRoots.clear(); 130 131 VolumeInfo primaryVolume = null; 132 final int userId = UserHandle.myUserId(); 133 final List<VolumeInfo> volumes = mStorageManager.getVolumes(); 134 for (VolumeInfo volume : volumes) { 135 if (!volume.isMountedReadable()) continue; 136 137 final String rootId; 138 final String title; 139 if (volume.getType() == VolumeInfo.TYPE_EMULATED) { 140 // We currently only support a single emulated volume mounted at 141 // a time, and it's always considered the primary 142 if (DEBUG) Log.d(TAG, "Found primary volume: " + volume); 143 rootId = ROOT_ID_PRIMARY_EMULATED; 144 145 if (VolumeInfo.ID_EMULATED_INTERNAL.equals(volume.getId())) { 146 // This is basically the user's primary device storage. 147 // Use device name for the volume since this is likely same thing 148 // the user sees when they mount their phone on another device. 149 String deviceName = Settings.Global.getString( 150 getContext().getContentResolver(), Settings.Global.DEVICE_NAME); 151 152 // Device name should always be set. In case it isn't, though, 153 // fall back to a localized "Internal Storage" string. 154 title = !TextUtils.isEmpty(deviceName) 155 ? deviceName 156 : getContext().getString(R.string.root_internal_storage); 157 } else { 158 // This should cover all other storage devices, like an SD card 159 // or USB OTG drive plugged in. Using getBestVolumeDescription() 160 // will give us a nice string like "Samsung SD card" or "SanDisk USB drive" 161 final VolumeInfo privateVol = mStorageManager.findPrivateForEmulated(volume); 162 title = mStorageManager.getBestVolumeDescription(privateVol); 163 } 164 } else if (volume.getType() == VolumeInfo.TYPE_PUBLIC) { 165 rootId = volume.getFsUuid(); 166 title = mStorageManager.getBestVolumeDescription(volume); 167 } else { 168 // Unsupported volume; ignore 169 continue; 170 } 171 172 if (TextUtils.isEmpty(rootId)) { 173 Log.d(TAG, "Missing UUID for " + volume.getId() + "; skipping"); 174 continue; 175 } 176 if (mRoots.containsKey(rootId)) { 177 Log.w(TAG, "Duplicate UUID " + rootId + " for " + volume.getId() + "; skipping"); 178 continue; 179 } 180 181 final RootInfo root = new RootInfo(); 182 mRoots.put(rootId, root); 183 184 root.rootId = rootId; 185 root.flags = Root.FLAG_LOCAL_ONLY 186 | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD; 187 188 final DiskInfo disk = volume.getDisk(); 189 if (DEBUG) Log.d(TAG, "Disk for root " + rootId + " is " + disk); 190 if (disk != null && disk.isSd()) { 191 root.flags |= Root.FLAG_REMOVABLE_SD; 192 } else if (disk != null && disk.isUsb()) { 193 root.flags |= Root.FLAG_REMOVABLE_USB; 194 } 195 196 if (volume.isPrimary()) { 197 // save off the primary volume for subsequent "Home" dir initialization. 198 primaryVolume = volume; 199 root.flags |= Root.FLAG_ADVANCED; 200 } 201 // Dunno when this would NOT be the case, but never hurts to be correct. 202 if (volume.isMountedWritable()) { 203 root.flags |= Root.FLAG_SUPPORTS_CREATE; 204 } 205 root.title = title; 206 if (volume.getType() == VolumeInfo.TYPE_PUBLIC) { 207 root.flags |= Root.FLAG_HAS_SETTINGS; 208 } 209 if (volume.isVisibleForRead(userId)) { 210 root.visiblePath = volume.getPathForUser(userId); 211 } else { 212 root.visiblePath = null; 213 } 214 root.path = volume.getInternalPathForUser(userId); 215 try { 216 root.docId = getDocIdForFile(root.path); 217 } catch (FileNotFoundException e) { 218 throw new IllegalStateException(e); 219 } 220 } 221 222 // Finally, if primary storage is available we add the "Documents" directory. 223 // If I recall correctly the actual directory is created on demand 224 // by calling either getPathForUser, or getInternalPathForUser. 225 if (primaryVolume != null && primaryVolume.isVisible()) { 226 final RootInfo root = new RootInfo(); 227 root.rootId = ROOT_ID_HOME; 228 mRoots.put(root.rootId, root); 229 root.title = getContext().getString(R.string.root_documents); 230 231 // Only report bytes on *volumes*...as a matter of policy. 232 root.reportAvailableBytes = false; 233 root.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_SEARCH 234 | Root.FLAG_SUPPORTS_IS_CHILD; 235 236 // Dunno when this would NOT be the case, but never hurts to be correct. 237 if (primaryVolume.isMountedWritable()) { 238 root.flags |= Root.FLAG_SUPPORTS_CREATE; 239 } 240 241 // Create the "Documents" directory on disk (don't use the localized title). 242 root.visiblePath = new File( 243 primaryVolume.getPathForUser(userId), Environment.DIRECTORY_DOCUMENTS); 244 root.path = new File( 245 primaryVolume.getInternalPathForUser(userId), Environment.DIRECTORY_DOCUMENTS); 246 try { 247 root.docId = getDocIdForFile(root.path); 248 } catch (FileNotFoundException e) { 249 throw new IllegalStateException(e); 250 } 251 } 252 253 Log.d(TAG, "After updating volumes, found " + mRoots.size() + " active roots"); 254 255 // Note this affects content://com.android.externalstorage.documents/root/39BD-07C5 256 // as well as content://com.android.externalstorage.documents/document/*/children, 257 // so just notify on content://com.android.externalstorage.documents/. 258 getContext().getContentResolver().notifyChange(BASE_URI, null, false); 259 } 260 resolveRootProjection(String[] projection)261 private static String[] resolveRootProjection(String[] projection) { 262 return projection != null ? projection : DEFAULT_ROOT_PROJECTION; 263 } 264 resolveDocumentProjection(String[] projection)265 private static String[] resolveDocumentProjection(String[] projection) { 266 return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION; 267 } 268 269 getDocIdForFile(File file)270 private String getDocIdForFile(File file) throws FileNotFoundException { 271 return getDocIdForFileMaybeCreate(file, false); 272 } 273 getDocIdForFileMaybeCreate(File file, boolean createNewDir)274 private String getDocIdForFileMaybeCreate(File file, boolean createNewDir) 275 throws FileNotFoundException { 276 String path = file.getAbsolutePath(); 277 278 // Find the most-specific root path 279 String mostSpecificId = null; 280 String mostSpecificPath = null; 281 synchronized (mRootsLock) { 282 for (int i = 0; i < mRoots.size(); i++) { 283 final String rootId = mRoots.keyAt(i); 284 final String rootPath = mRoots.valueAt(i).path.getAbsolutePath(); 285 if (path.startsWith(rootPath) && (mostSpecificPath == null 286 || rootPath.length() > mostSpecificPath.length())) { 287 mostSpecificId = rootId; 288 mostSpecificPath = rootPath; 289 } 290 } 291 } 292 293 if (mostSpecificPath == null) { 294 throw new FileNotFoundException("Failed to find root that contains " + path); 295 } 296 297 // Start at first char of path under root 298 final String rootPath = mostSpecificPath; 299 if (rootPath.equals(path)) { 300 path = ""; 301 } else if (rootPath.endsWith("/")) { 302 path = path.substring(rootPath.length()); 303 } else { 304 path = path.substring(rootPath.length() + 1); 305 } 306 307 if (!file.exists() && createNewDir) { 308 Log.i(TAG, "Creating new directory " + file); 309 if (!file.mkdir()) { 310 Log.e(TAG, "Could not create directory " + file); 311 } 312 } 313 314 return mostSpecificId + ':' + path; 315 } 316 getFileForDocId(String docId)317 private File getFileForDocId(String docId) throws FileNotFoundException { 318 return getFileForDocId(docId, false); 319 } 320 getFileForDocId(String docId, boolean visible)321 private File getFileForDocId(String docId, boolean visible) throws FileNotFoundException { 322 final int splitIndex = docId.indexOf(':', 1); 323 final String tag = docId.substring(0, splitIndex); 324 final String path = docId.substring(splitIndex + 1); 325 326 RootInfo root; 327 synchronized (mRootsLock) { 328 root = mRoots.get(tag); 329 } 330 if (root == null) { 331 throw new FileNotFoundException("No root for " + tag); 332 } 333 334 File target = visible ? root.visiblePath : root.path; 335 if (target == null) { 336 return null; 337 } 338 if (!target.exists()) { 339 target.mkdirs(); 340 } 341 target = new File(target, path); 342 if (!target.exists()) { 343 throw new FileNotFoundException("Missing file for " + docId + " at " + target); 344 } 345 return target; 346 } 347 includeFile(MatrixCursor result, String docId, File file)348 private void includeFile(MatrixCursor result, String docId, File file) 349 throws FileNotFoundException { 350 if (docId == null) { 351 docId = getDocIdForFile(file); 352 } else { 353 file = getFileForDocId(docId); 354 } 355 356 int flags = 0; 357 358 if (file.canWrite()) { 359 if (file.isDirectory()) { 360 flags |= Document.FLAG_DIR_SUPPORTS_CREATE; 361 flags |= Document.FLAG_SUPPORTS_DELETE; 362 flags |= Document.FLAG_SUPPORTS_RENAME; 363 flags |= Document.FLAG_SUPPORTS_MOVE; 364 } else { 365 flags |= Document.FLAG_SUPPORTS_WRITE; 366 flags |= Document.FLAG_SUPPORTS_DELETE; 367 flags |= Document.FLAG_SUPPORTS_RENAME; 368 flags |= Document.FLAG_SUPPORTS_MOVE; 369 } 370 } 371 372 final String mimeType = getTypeForFile(file); 373 if (mArchiveHelper.isSupportedArchiveType(mimeType)) { 374 flags |= Document.FLAG_ARCHIVE; 375 } 376 377 final String displayName = file.getName(); 378 if (mimeType.startsWith("image/")) { 379 flags |= Document.FLAG_SUPPORTS_THUMBNAIL; 380 } 381 382 final RowBuilder row = result.newRow(); 383 row.add(Document.COLUMN_DOCUMENT_ID, docId); 384 row.add(Document.COLUMN_DISPLAY_NAME, displayName); 385 row.add(Document.COLUMN_SIZE, file.length()); 386 row.add(Document.COLUMN_MIME_TYPE, mimeType); 387 row.add(Document.COLUMN_FLAGS, flags); 388 row.add(DocumentArchiveHelper.COLUMN_LOCAL_FILE_PATH, file.getPath()); 389 390 // Only publish dates reasonably after epoch 391 long lastModified = file.lastModified(); 392 if (lastModified > 31536000000L) { 393 row.add(Document.COLUMN_LAST_MODIFIED, lastModified); 394 } 395 } 396 397 @Override queryRoots(String[] projection)398 public Cursor queryRoots(String[] projection) throws FileNotFoundException { 399 final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); 400 synchronized (mRootsLock) { 401 for (RootInfo root : mRoots.values()) { 402 final RowBuilder row = result.newRow(); 403 row.add(Root.COLUMN_ROOT_ID, root.rootId); 404 row.add(Root.COLUMN_FLAGS, root.flags); 405 row.add(Root.COLUMN_TITLE, root.title); 406 row.add(Root.COLUMN_DOCUMENT_ID, root.docId); 407 row.add(Root.COLUMN_AVAILABLE_BYTES, 408 root.reportAvailableBytes ? root.path.getFreeSpace() : -1); 409 } 410 } 411 return result; 412 } 413 414 @Override isChildDocument(String parentDocId, String docId)415 public boolean isChildDocument(String parentDocId, String docId) { 416 try { 417 if (mArchiveHelper.isArchivedDocument(docId)) { 418 return mArchiveHelper.isChildDocument(parentDocId, docId); 419 } 420 // Archives do not contain regular files. 421 if (mArchiveHelper.isArchivedDocument(parentDocId)) { 422 return false; 423 } 424 425 final File parent = getFileForDocId(parentDocId).getCanonicalFile(); 426 final File doc = getFileForDocId(docId).getCanonicalFile(); 427 return FileUtils.contains(parent, doc); 428 } catch (IOException e) { 429 throw new IllegalArgumentException( 430 "Failed to determine if " + docId + " is child of " + parentDocId + ": " + e); 431 } 432 } 433 434 @Override createDocument(String docId, String mimeType, String displayName)435 public String createDocument(String docId, String mimeType, String displayName) 436 throws FileNotFoundException { 437 displayName = FileUtils.buildValidFatFilename(displayName); 438 439 final File parent = getFileForDocId(docId); 440 if (!parent.isDirectory()) { 441 throw new IllegalArgumentException("Parent document isn't a directory"); 442 } 443 444 final File file = FileUtils.buildUniqueFile(parent, mimeType, displayName); 445 if (Document.MIME_TYPE_DIR.equals(mimeType)) { 446 if (!file.mkdir()) { 447 throw new IllegalStateException("Failed to mkdir " + file); 448 } 449 } else { 450 try { 451 if (!file.createNewFile()) { 452 throw new IllegalStateException("Failed to touch " + file); 453 } 454 } catch (IOException e) { 455 throw new IllegalStateException("Failed to touch " + file + ": " + e); 456 } 457 } 458 459 return getDocIdForFile(file); 460 } 461 462 @Override renameDocument(String docId, String displayName)463 public String renameDocument(String docId, String displayName) throws FileNotFoundException { 464 // Since this provider treats renames as generating a completely new 465 // docId, we're okay with letting the MIME type change. 466 displayName = FileUtils.buildValidFatFilename(displayName); 467 468 final File before = getFileForDocId(docId); 469 final File after = new File(before.getParentFile(), displayName); 470 if (after.exists()) { 471 throw new IllegalStateException("Already exists " + after); 472 } 473 if (!before.renameTo(after)) { 474 throw new IllegalStateException("Failed to rename to " + after); 475 } 476 final String afterDocId = getDocIdForFile(after); 477 if (!TextUtils.equals(docId, afterDocId)) { 478 return afterDocId; 479 } else { 480 return null; 481 } 482 } 483 484 @Override deleteDocument(String docId)485 public void deleteDocument(String docId) throws FileNotFoundException { 486 final File file = getFileForDocId(docId); 487 final File visibleFile = getFileForDocId(docId, true); 488 489 final boolean isDirectory = file.isDirectory(); 490 if (isDirectory) { 491 FileUtils.deleteContents(file); 492 } 493 if (!file.delete()) { 494 throw new IllegalStateException("Failed to delete " + file); 495 } 496 497 if (visibleFile != null) { 498 final ContentResolver resolver = getContext().getContentResolver(); 499 final Uri externalUri = MediaStore.Files.getContentUri("external"); 500 501 // Remove media store entries for any files inside this directory, using 502 // path prefix match. Logic borrowed from MtpDatabase. 503 if (isDirectory) { 504 final String path = visibleFile.getAbsolutePath() + "/"; 505 resolver.delete(externalUri, 506 "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)", 507 new String[] { path + "%", Integer.toString(path.length()), path }); 508 } 509 510 // Remove media store entry for this exact file. 511 final String path = visibleFile.getAbsolutePath(); 512 resolver.delete(externalUri, 513 "_data LIKE ?1 AND lower(_data)=lower(?2)", 514 new String[] { path, path }); 515 } 516 } 517 518 @Override moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId)519 public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, 520 String targetParentDocumentId) 521 throws FileNotFoundException { 522 final File before = getFileForDocId(sourceDocumentId); 523 final File after = new File(getFileForDocId(targetParentDocumentId), before.getName()); 524 525 if (after.exists()) { 526 throw new IllegalStateException("Already exists " + after); 527 } 528 if (!before.renameTo(after)) { 529 throw new IllegalStateException("Failed to move to " + after); 530 } 531 return getDocIdForFile(after); 532 } 533 534 @Override queryDocument(String documentId, String[] projection)535 public Cursor queryDocument(String documentId, String[] projection) 536 throws FileNotFoundException { 537 if (mArchiveHelper.isArchivedDocument(documentId)) { 538 return mArchiveHelper.queryDocument(documentId, projection); 539 } 540 541 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 542 includeFile(result, documentId, null); 543 return result; 544 } 545 546 @Override queryChildDocuments( String parentDocumentId, String[] projection, String sortOrder)547 public Cursor queryChildDocuments( 548 String parentDocumentId, String[] projection, String sortOrder) 549 throws FileNotFoundException { 550 if (mArchiveHelper.isArchivedDocument(parentDocumentId) || 551 mArchiveHelper.isSupportedArchiveType(getDocumentType(parentDocumentId))) { 552 return mArchiveHelper.queryChildDocuments(parentDocumentId, projection, sortOrder); 553 } 554 555 final File parent = getFileForDocId(parentDocumentId); 556 final MatrixCursor result = new DirectoryCursor( 557 resolveDocumentProjection(projection), parentDocumentId, parent); 558 for (File file : parent.listFiles()) { 559 includeFile(result, null, file); 560 } 561 return result; 562 } 563 564 @Override querySearchDocuments(String rootId, String query, String[] projection)565 public Cursor querySearchDocuments(String rootId, String query, String[] projection) 566 throws FileNotFoundException { 567 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 568 569 final File parent; 570 synchronized (mRootsLock) { 571 parent = mRoots.get(rootId).path; 572 } 573 574 final LinkedList<File> pending = new LinkedList<File>(); 575 pending.add(parent); 576 while (!pending.isEmpty() && result.getCount() < 24) { 577 final File file = pending.removeFirst(); 578 if (file.isDirectory()) { 579 for (File child : file.listFiles()) { 580 pending.add(child); 581 } 582 } 583 if (file.getName().toLowerCase().contains(query)) { 584 includeFile(result, null, file); 585 } 586 } 587 return result; 588 } 589 590 @Override getDocumentType(String documentId)591 public String getDocumentType(String documentId) throws FileNotFoundException { 592 if (mArchiveHelper.isArchivedDocument(documentId)) { 593 return mArchiveHelper.getDocumentType(documentId); 594 } 595 596 final File file = getFileForDocId(documentId); 597 return getTypeForFile(file); 598 } 599 600 @Override openDocument( String documentId, String mode, CancellationSignal signal)601 public ParcelFileDescriptor openDocument( 602 String documentId, String mode, CancellationSignal signal) 603 throws FileNotFoundException { 604 if (mArchiveHelper.isArchivedDocument(documentId)) { 605 return mArchiveHelper.openDocument(documentId, mode, signal); 606 } 607 608 final File file = getFileForDocId(documentId); 609 final File visibleFile = getFileForDocId(documentId, true); 610 611 final int pfdMode = ParcelFileDescriptor.parseMode(mode); 612 if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY || visibleFile == null) { 613 return ParcelFileDescriptor.open(file, pfdMode); 614 } else { 615 try { 616 // When finished writing, kick off media scanner 617 return ParcelFileDescriptor.open(file, pfdMode, mHandler, new OnCloseListener() { 618 @Override 619 public void onClose(IOException e) { 620 final Intent intent = new Intent( 621 Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); 622 intent.setData(Uri.fromFile(visibleFile)); 623 getContext().sendBroadcast(intent); 624 } 625 }); 626 } catch (IOException e) { 627 throw new FileNotFoundException("Failed to open for writing: " + e); 628 } 629 } 630 } 631 632 @Override 633 public AssetFileDescriptor openDocumentThumbnail( 634 String documentId, Point sizeHint, CancellationSignal signal) 635 throws FileNotFoundException { 636 if (mArchiveHelper.isArchivedDocument(documentId)) { 637 return mArchiveHelper.openDocumentThumbnail(documentId, sizeHint, signal); 638 } 639 640 final File file = getFileForDocId(documentId); 641 return DocumentsContract.openImageThumbnail(file); 642 } 643 644 @Override 645 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 646 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 160); 647 synchronized (mRootsLock) { 648 for (int i = 0; i < mRoots.size(); i++) { 649 final RootInfo root = mRoots.valueAt(i); 650 pw.println("Root{" + root.rootId + "}:"); 651 pw.increaseIndent(); 652 pw.printPair("flags", DebugUtils.flagsToString(Root.class, "FLAG_", root.flags)); 653 pw.println(); 654 pw.printPair("title", root.title); 655 pw.printPair("docId", root.docId); 656 pw.println(); 657 pw.printPair("path", root.path); 658 pw.printPair("visiblePath", root.visiblePath); 659 pw.decreaseIndent(); 660 pw.println(); 661 } 662 } 663 } 664 665 @Override 666 public Bundle call(String method, String arg, Bundle extras) { 667 Bundle bundle = super.call(method, arg, extras); 668 if (bundle == null && !TextUtils.isEmpty(method)) { 669 switch (method) { 670 case "getDocIdForFileCreateNewDir": { 671 getContext().enforceCallingPermission( 672 android.Manifest.permission.MANAGE_DOCUMENTS, null); 673 if (TextUtils.isEmpty(arg)) { 674 return null; 675 } 676 try { 677 final String docId = getDocIdForFileMaybeCreate(new File(arg), true); 678 bundle = new Bundle(); 679 bundle.putString("DOC_ID", docId); 680 } catch (FileNotFoundException e) { 681 Log.w(TAG, "file '" + arg + "' not found"); 682 return null; 683 } 684 break; 685 } 686 default: 687 Log.w(TAG, "unknown method passed to call(): " + method); 688 } 689 } 690 return bundle; 691 } 692 693 private static String getTypeForFile(File file) { 694 if (file.isDirectory()) { 695 return Document.MIME_TYPE_DIR; 696 } else { 697 return getTypeForName(file.getName()); 698 } 699 } 700 701 private static String getTypeForName(String name) { 702 final int lastDot = name.lastIndexOf('.'); 703 if (lastDot >= 0) { 704 final String extension = name.substring(lastDot + 1).toLowerCase(); 705 final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 706 if (mime != null) { 707 return mime; 708 } 709 } 710 711 return "application/octet-stream"; 712 } 713 714 private void startObserving(File file, Uri notifyUri) { 715 synchronized (mObservers) { 716 DirectoryObserver observer = mObservers.get(file); 717 if (observer == null) { 718 observer = new DirectoryObserver( 719 file, getContext().getContentResolver(), notifyUri); 720 observer.startWatching(); 721 mObservers.put(file, observer); 722 } 723 observer.mRefCount++; 724 725 if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer); 726 } 727 } 728 729 private void stopObserving(File file) { 730 synchronized (mObservers) { 731 DirectoryObserver observer = mObservers.get(file); 732 if (observer == null) return; 733 734 observer.mRefCount--; 735 if (observer.mRefCount == 0) { 736 mObservers.remove(file); 737 observer.stopWatching(); 738 } 739 740 if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer); 741 } 742 } 743 744 private static class DirectoryObserver extends FileObserver { 745 private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO 746 | CREATE | DELETE | DELETE_SELF | MOVE_SELF; 747 748 private final File mFile; 749 private final ContentResolver mResolver; 750 private final Uri mNotifyUri; 751 752 private int mRefCount = 0; 753 754 public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) { 755 super(file.getAbsolutePath(), NOTIFY_EVENTS); 756 mFile = file; 757 mResolver = resolver; 758 mNotifyUri = notifyUri; 759 } 760 761 @Override 762 public void onEvent(int event, String path) { 763 if ((event & NOTIFY_EVENTS) != 0) { 764 if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path); 765 mResolver.notifyChange(mNotifyUri, null, false); 766 } 767 } 768 769 @Override 770 public String toString() { 771 return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}"; 772 } 773 } 774 775 private class DirectoryCursor extends MatrixCursor { 776 private final File mFile; 777 778 public DirectoryCursor(String[] columnNames, String docId, File file) { 779 super(columnNames); 780 781 final Uri notifyUri = DocumentsContract.buildChildDocumentsUri( 782 AUTHORITY, docId); 783 setNotificationUri(getContext().getContentResolver(), notifyUri); 784 785 mFile = file; 786 startObserving(mFile, notifyUri); 787 } 788 789 @Override 790 public void close() { 791 super.close(); 792 stopObserving(mFile); 793 } 794 } 795 } 796