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 17package com.android.externalstorage; 18 19import android.content.ContentResolver; 20import android.content.Context; 21import android.content.Intent; 22import android.content.res.AssetFileDescriptor; 23import android.database.Cursor; 24import android.database.MatrixCursor; 25import android.database.MatrixCursor.RowBuilder; 26import android.graphics.Point; 27import android.net.Uri; 28import android.os.Bundle; 29import android.os.CancellationSignal; 30import android.os.Environment; 31import android.os.FileObserver; 32import android.os.FileUtils; 33import android.os.Handler; 34import android.os.ParcelFileDescriptor; 35import android.os.ParcelFileDescriptor.OnCloseListener; 36import android.os.UserHandle; 37import android.os.storage.DiskInfo; 38import android.os.storage.StorageManager; 39import android.os.storage.VolumeInfo; 40import android.provider.DocumentsContract; 41import android.provider.DocumentsContract.Document; 42import android.provider.DocumentsContract.Root; 43import android.provider.DocumentsProvider; 44import android.provider.MediaStore; 45import android.provider.Settings; 46import android.support.provider.DocumentArchiveHelper; 47import android.text.TextUtils; 48import android.util.ArrayMap; 49import android.util.DebugUtils; 50import android.util.Log; 51import android.webkit.MimeTypeMap; 52 53import com.android.internal.annotations.GuardedBy; 54import com.android.internal.util.IndentingPrintWriter; 55 56import java.io.File; 57import java.io.FileDescriptor; 58import java.io.FileNotFoundException; 59import java.io.IOException; 60import java.io.PrintWriter; 61import java.util.LinkedList; 62import java.util.List; 63 64public 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 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 122 public void updateVolumes() { 123 synchronized (mRootsLock) { 124 updateVolumesLocked(); 125 } 126 } 127 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 261 private static String[] resolveRootProjection(String[] projection) { 262 return projection != null ? projection : DEFAULT_ROOT_PROJECTION; 263 } 264 265 private static String[] resolveDocumentProjection(String[] projection) { 266 return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION; 267 } 268 269 270 private String getDocIdForFile(File file) throws FileNotFoundException { 271 return getDocIdForFileMaybeCreate(file, false); 272 } 273 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 317 private File getFileForDocId(String docId) throws FileNotFoundException { 318 return getFileForDocId(docId, false); 319 } 320 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 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 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 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 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 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 485 public void deleteDocument(String docId) throws FileNotFoundException { 486 final File file = getFileForDocId(docId); 487 final boolean isDirectory = file.isDirectory(); 488 if (isDirectory) { 489 FileUtils.deleteContents(file); 490 } 491 if (!file.delete()) { 492 throw new IllegalStateException("Failed to delete " + file); 493 } 494 495 final ContentResolver resolver = getContext().getContentResolver(); 496 final Uri externalUri = MediaStore.Files.getContentUri("external"); 497 498 // Remove media store entries for any files inside this directory, using 499 // path prefix match. Logic borrowed from MtpDatabase. 500 if (isDirectory) { 501 final String path = file.getAbsolutePath() + "/"; 502 resolver.delete(externalUri, 503 "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)", 504 new String[] { path + "%", Integer.toString(path.length()), path }); 505 } 506 507 // Remove media store entry for this exact file. 508 final String path = file.getAbsolutePath(); 509 resolver.delete(externalUri, 510 "_data LIKE ?1 AND lower(_data)=lower(?2)", 511 new String[] { path, path }); 512 } 513 514 @Override 515 public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, 516 String targetParentDocumentId) 517 throws FileNotFoundException { 518 final File before = getFileForDocId(sourceDocumentId); 519 final File after = new File(getFileForDocId(targetParentDocumentId), before.getName()); 520 521 if (after.exists()) { 522 throw new IllegalStateException("Already exists " + after); 523 } 524 if (!before.renameTo(after)) { 525 throw new IllegalStateException("Failed to move to " + after); 526 } 527 return getDocIdForFile(after); 528 } 529 530 @Override 531 public Cursor queryDocument(String documentId, String[] projection) 532 throws FileNotFoundException { 533 if (mArchiveHelper.isArchivedDocument(documentId)) { 534 return mArchiveHelper.queryDocument(documentId, projection); 535 } 536 537 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 538 includeFile(result, documentId, null); 539 return result; 540 } 541 542 @Override 543 public Cursor queryChildDocuments( 544 String parentDocumentId, String[] projection, String sortOrder) 545 throws FileNotFoundException { 546 if (mArchiveHelper.isArchivedDocument(parentDocumentId) || 547 mArchiveHelper.isSupportedArchiveType(getDocumentType(parentDocumentId))) { 548 return mArchiveHelper.queryChildDocuments(parentDocumentId, projection, sortOrder); 549 } 550 551 final File parent = getFileForDocId(parentDocumentId); 552 final MatrixCursor result = new DirectoryCursor( 553 resolveDocumentProjection(projection), parentDocumentId, parent); 554 for (File file : parent.listFiles()) { 555 includeFile(result, null, file); 556 } 557 return result; 558 } 559 560 @Override 561 public Cursor querySearchDocuments(String rootId, String query, String[] projection) 562 throws FileNotFoundException { 563 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 564 565 final File parent; 566 synchronized (mRootsLock) { 567 parent = mRoots.get(rootId).path; 568 } 569 570 final LinkedList<File> pending = new LinkedList<File>(); 571 pending.add(parent); 572 while (!pending.isEmpty() && result.getCount() < 24) { 573 final File file = pending.removeFirst(); 574 if (file.isDirectory()) { 575 for (File child : file.listFiles()) { 576 pending.add(child); 577 } 578 } 579 if (file.getName().toLowerCase().contains(query)) { 580 includeFile(result, null, file); 581 } 582 } 583 return result; 584 } 585 586 @Override 587 public String getDocumentType(String documentId) throws FileNotFoundException { 588 if (mArchiveHelper.isArchivedDocument(documentId)) { 589 return mArchiveHelper.getDocumentType(documentId); 590 } 591 592 final File file = getFileForDocId(documentId); 593 return getTypeForFile(file); 594 } 595 596 @Override 597 public ParcelFileDescriptor openDocument( 598 String documentId, String mode, CancellationSignal signal) 599 throws FileNotFoundException { 600 if (mArchiveHelper.isArchivedDocument(documentId)) { 601 return mArchiveHelper.openDocument(documentId, mode, signal); 602 } 603 604 final File file = getFileForDocId(documentId); 605 final File visibleFile = getFileForDocId(documentId, true); 606 607 final int pfdMode = ParcelFileDescriptor.parseMode(mode); 608 if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY || visibleFile == null) { 609 return ParcelFileDescriptor.open(file, pfdMode); 610 } else { 611 try { 612 // When finished writing, kick off media scanner 613 return ParcelFileDescriptor.open(file, pfdMode, mHandler, new OnCloseListener() { 614 @Override 615 public void onClose(IOException e) { 616 final Intent intent = new Intent( 617 Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); 618 intent.setData(Uri.fromFile(visibleFile)); 619 getContext().sendBroadcast(intent); 620 } 621 }); 622 } catch (IOException e) { 623 throw new FileNotFoundException("Failed to open for writing: " + e); 624 } 625 } 626 } 627 628 @Override 629 public AssetFileDescriptor openDocumentThumbnail( 630 String documentId, Point sizeHint, CancellationSignal signal) 631 throws FileNotFoundException { 632 if (mArchiveHelper.isArchivedDocument(documentId)) { 633 return mArchiveHelper.openDocumentThumbnail(documentId, sizeHint, signal); 634 } 635 636 final File file = getFileForDocId(documentId); 637 return DocumentsContract.openImageThumbnail(file); 638 } 639 640 @Override 641 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 642 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 160); 643 synchronized (mRootsLock) { 644 for (int i = 0; i < mRoots.size(); i++) { 645 final RootInfo root = mRoots.valueAt(i); 646 pw.println("Root{" + root.rootId + "}:"); 647 pw.increaseIndent(); 648 pw.printPair("flags", DebugUtils.flagsToString(Root.class, "FLAG_", root.flags)); 649 pw.println(); 650 pw.printPair("title", root.title); 651 pw.printPair("docId", root.docId); 652 pw.println(); 653 pw.printPair("path", root.path); 654 pw.printPair("visiblePath", root.visiblePath); 655 pw.decreaseIndent(); 656 pw.println(); 657 } 658 } 659 } 660 661 @Override 662 public Bundle call(String method, String arg, Bundle extras) { 663 Bundle bundle = super.call(method, arg, extras); 664 if (bundle == null && !TextUtils.isEmpty(method)) { 665 switch (method) { 666 case "getDocIdForFileCreateNewDir": { 667 getContext().enforceCallingPermission( 668 android.Manifest.permission.MANAGE_DOCUMENTS, null); 669 if (TextUtils.isEmpty(arg)) { 670 return null; 671 } 672 try { 673 final String docId = getDocIdForFileMaybeCreate(new File(arg), true); 674 bundle = new Bundle(); 675 bundle.putString("DOC_ID", docId); 676 } catch (FileNotFoundException e) { 677 Log.w(TAG, "file '" + arg + "' not found"); 678 return null; 679 } 680 break; 681 } 682 default: 683 Log.w(TAG, "unknown method passed to call(): " + method); 684 } 685 } 686 return bundle; 687 } 688 689 private static String getTypeForFile(File file) { 690 if (file.isDirectory()) { 691 return Document.MIME_TYPE_DIR; 692 } else { 693 return getTypeForName(file.getName()); 694 } 695 } 696 697 private static String getTypeForName(String name) { 698 final int lastDot = name.lastIndexOf('.'); 699 if (lastDot >= 0) { 700 final String extension = name.substring(lastDot + 1).toLowerCase(); 701 final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 702 if (mime != null) { 703 return mime; 704 } 705 } 706 707 return "application/octet-stream"; 708 } 709 710 private void startObserving(File file, Uri notifyUri) { 711 synchronized (mObservers) { 712 DirectoryObserver observer = mObservers.get(file); 713 if (observer == null) { 714 observer = new DirectoryObserver( 715 file, getContext().getContentResolver(), notifyUri); 716 observer.startWatching(); 717 mObservers.put(file, observer); 718 } 719 observer.mRefCount++; 720 721 if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer); 722 } 723 } 724 725 private void stopObserving(File file) { 726 synchronized (mObservers) { 727 DirectoryObserver observer = mObservers.get(file); 728 if (observer == null) return; 729 730 observer.mRefCount--; 731 if (observer.mRefCount == 0) { 732 mObservers.remove(file); 733 observer.stopWatching(); 734 } 735 736 if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer); 737 } 738 } 739 740 private static class DirectoryObserver extends FileObserver { 741 private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO 742 | CREATE | DELETE | DELETE_SELF | MOVE_SELF; 743 744 private final File mFile; 745 private final ContentResolver mResolver; 746 private final Uri mNotifyUri; 747 748 private int mRefCount = 0; 749 750 public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) { 751 super(file.getAbsolutePath(), NOTIFY_EVENTS); 752 mFile = file; 753 mResolver = resolver; 754 mNotifyUri = notifyUri; 755 } 756 757 @Override 758 public void onEvent(int event, String path) { 759 if ((event & NOTIFY_EVENTS) != 0) { 760 if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path); 761 mResolver.notifyChange(mNotifyUri, null, false); 762 } 763 } 764 765 @Override 766 public String toString() { 767 return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}"; 768 } 769 } 770 771 private class DirectoryCursor extends MatrixCursor { 772 private final File mFile; 773 774 public DirectoryCursor(String[] columnNames, String docId, File file) { 775 super(columnNames); 776 777 final Uri notifyUri = DocumentsContract.buildChildDocumentsUri( 778 AUTHORITY, docId); 779 setNotificationUri(getContext().getContentResolver(), notifyUri); 780 781 mFile = file; 782 startObserving(mFile, notifyUri); 783 } 784 785 @Override 786 public void close() { 787 super.close(); 788 stopObserving(mFile); 789 } 790 } 791} 792