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 = FileUtils.buildUniqueFile(before.getParentFile(), displayName); 470 if (!before.renameTo(after)) { 471 throw new IllegalStateException("Failed to rename to " + after); 472 } 473 final String afterDocId = getDocIdForFile(after); 474 if (!TextUtils.equals(docId, afterDocId)) { 475 return afterDocId; 476 } else { 477 return null; 478 } 479 } 480 481 @Override 482 public void deleteDocument(String docId) throws FileNotFoundException { 483 final File file = getFileForDocId(docId); 484 final File visibleFile = getFileForDocId(docId, true); 485 486 final boolean isDirectory = file.isDirectory(); 487 if (isDirectory) { 488 FileUtils.deleteContents(file); 489 } 490 if (!file.delete()) { 491 throw new IllegalStateException("Failed to delete " + file); 492 } 493 494 if (visibleFile != null) { 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 = visibleFile.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 = visibleFile.getAbsolutePath(); 509 resolver.delete(externalUri, 510 "_data LIKE ?1 AND lower(_data)=lower(?2)", 511 new String[] { path, path }); 512 } 513 } 514 515 @Override 516 public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, 517 String targetParentDocumentId) 518 throws FileNotFoundException { 519 final File before = getFileForDocId(sourceDocumentId); 520 final File after = new File(getFileForDocId(targetParentDocumentId), before.getName()); 521 522 if (after.exists()) { 523 throw new IllegalStateException("Already exists " + after); 524 } 525 if (!before.renameTo(after)) { 526 throw new IllegalStateException("Failed to move to " + after); 527 } 528 return getDocIdForFile(after); 529 } 530 531 @Override 532 public Cursor queryDocument(String documentId, String[] projection) 533 throws FileNotFoundException { 534 if (mArchiveHelper.isArchivedDocument(documentId)) { 535 return mArchiveHelper.queryDocument(documentId, projection); 536 } 537 538 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 539 includeFile(result, documentId, null); 540 return result; 541 } 542 543 @Override 544 public Cursor queryChildDocuments( 545 String parentDocumentId, String[] projection, String sortOrder) 546 throws FileNotFoundException { 547 if (mArchiveHelper.isArchivedDocument(parentDocumentId) || 548 mArchiveHelper.isSupportedArchiveType(getDocumentType(parentDocumentId))) { 549 return mArchiveHelper.queryChildDocuments(parentDocumentId, projection, sortOrder); 550 } 551 552 final File parent = getFileForDocId(parentDocumentId); 553 final MatrixCursor result = new DirectoryCursor( 554 resolveDocumentProjection(projection), parentDocumentId, parent); 555 for (File file : parent.listFiles()) { 556 includeFile(result, null, file); 557 } 558 return result; 559 } 560 561 @Override 562 public Cursor querySearchDocuments(String rootId, String query, String[] projection) 563 throws FileNotFoundException { 564 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 565 566 query = query.toLowerCase(); 567 final File parent; 568 synchronized (mRootsLock) { 569 parent = mRoots.get(rootId).path; 570 } 571 572 final LinkedList<File> pending = new LinkedList<File>(); 573 pending.add(parent); 574 while (!pending.isEmpty() && result.getCount() < 24) { 575 final File file = pending.removeFirst(); 576 if (file.isDirectory()) { 577 for (File child : file.listFiles()) { 578 pending.add(child); 579 } 580 } 581 if (file.getName().toLowerCase().contains(query)) { 582 includeFile(result, null, file); 583 } 584 } 585 return result; 586 } 587 588 @Override 589 public String getDocumentType(String documentId) throws FileNotFoundException { 590 if (mArchiveHelper.isArchivedDocument(documentId)) { 591 return mArchiveHelper.getDocumentType(documentId); 592 } 593 594 final File file = getFileForDocId(documentId); 595 return getTypeForFile(file); 596 } 597 598 @Override 599 public ParcelFileDescriptor openDocument( 600 String documentId, String mode, CancellationSignal signal) 601 throws FileNotFoundException { 602 if (mArchiveHelper.isArchivedDocument(documentId)) { 603 return mArchiveHelper.openDocument(documentId, mode, signal); 604 } 605 606 final File file = getFileForDocId(documentId); 607 final File visibleFile = getFileForDocId(documentId, true); 608 609 final int pfdMode = ParcelFileDescriptor.parseMode(mode); 610 if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY || visibleFile == null) { 611 return ParcelFileDescriptor.open(file, pfdMode); 612 } else { 613 try { 614 // When finished writing, kick off media scanner 615 return ParcelFileDescriptor.open(file, pfdMode, mHandler, new OnCloseListener() { 616 @Override 617 public void onClose(IOException e) { 618 final Intent intent = new Intent( 619 Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); 620 intent.setData(Uri.fromFile(visibleFile)); 621 getContext().sendBroadcast(intent); 622 } 623 }); 624 } catch (IOException e) { 625 throw new FileNotFoundException("Failed to open for writing: " + e); 626 } 627 } 628 } 629 630 @Override 631 public AssetFileDescriptor openDocumentThumbnail( 632 String documentId, Point sizeHint, CancellationSignal signal) 633 throws FileNotFoundException { 634 if (mArchiveHelper.isArchivedDocument(documentId)) { 635 return mArchiveHelper.openDocumentThumbnail(documentId, sizeHint, signal); 636 } 637 638 final File file = getFileForDocId(documentId); 639 return DocumentsContract.openImageThumbnail(file); 640 } 641 642 @Override 643 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 644 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 160); 645 synchronized (mRootsLock) { 646 for (int i = 0; i < mRoots.size(); i++) { 647 final RootInfo root = mRoots.valueAt(i); 648 pw.println("Root{" + root.rootId + "}:"); 649 pw.increaseIndent(); 650 pw.printPair("flags", DebugUtils.flagsToString(Root.class, "FLAG_", root.flags)); 651 pw.println(); 652 pw.printPair("title", root.title); 653 pw.printPair("docId", root.docId); 654 pw.println(); 655 pw.printPair("path", root.path); 656 pw.printPair("visiblePath", root.visiblePath); 657 pw.decreaseIndent(); 658 pw.println(); 659 } 660 } 661 } 662 663 @Override 664 public Bundle call(String method, String arg, Bundle extras) { 665 Bundle bundle = super.call(method, arg, extras); 666 if (bundle == null && !TextUtils.isEmpty(method)) { 667 switch (method) { 668 case "getDocIdForFileCreateNewDir": { 669 getContext().enforceCallingPermission( 670 android.Manifest.permission.MANAGE_DOCUMENTS, null); 671 if (TextUtils.isEmpty(arg)) { 672 return null; 673 } 674 try { 675 final String docId = getDocIdForFileMaybeCreate(new File(arg), true); 676 bundle = new Bundle(); 677 bundle.putString("DOC_ID", docId); 678 } catch (FileNotFoundException e) { 679 Log.w(TAG, "file '" + arg + "' not found"); 680 return null; 681 } 682 break; 683 } 684 default: 685 Log.w(TAG, "unknown method passed to call(): " + method); 686 } 687 } 688 return bundle; 689 } 690 691 private static String getTypeForFile(File file) { 692 if (file.isDirectory()) { 693 return Document.MIME_TYPE_DIR; 694 } else { 695 return getTypeForName(file.getName()); 696 } 697 } 698 699 private static String getTypeForName(String name) { 700 final int lastDot = name.lastIndexOf('.'); 701 if (lastDot >= 0) { 702 final String extension = name.substring(lastDot + 1).toLowerCase(); 703 final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 704 if (mime != null) { 705 return mime; 706 } 707 } 708 709 return "application/octet-stream"; 710 } 711 712 private void startObserving(File file, Uri notifyUri) { 713 synchronized (mObservers) { 714 DirectoryObserver observer = mObservers.get(file); 715 if (observer == null) { 716 observer = new DirectoryObserver( 717 file, getContext().getContentResolver(), notifyUri); 718 observer.startWatching(); 719 mObservers.put(file, observer); 720 } 721 observer.mRefCount++; 722 723 if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer); 724 } 725 } 726 727 private void stopObserving(File file) { 728 synchronized (mObservers) { 729 DirectoryObserver observer = mObservers.get(file); 730 if (observer == null) return; 731 732 observer.mRefCount--; 733 if (observer.mRefCount == 0) { 734 mObservers.remove(file); 735 observer.stopWatching(); 736 } 737 738 if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer); 739 } 740 } 741 742 private static class DirectoryObserver extends FileObserver { 743 private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO 744 | CREATE | DELETE | DELETE_SELF | MOVE_SELF; 745 746 private final File mFile; 747 private final ContentResolver mResolver; 748 private final Uri mNotifyUri; 749 750 private int mRefCount = 0; 751 752 public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) { 753 super(file.getAbsolutePath(), NOTIFY_EVENTS); 754 mFile = file; 755 mResolver = resolver; 756 mNotifyUri = notifyUri; 757 } 758 759 @Override 760 public void onEvent(int event, String path) { 761 if ((event & NOTIFY_EVENTS) != 0) { 762 if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path); 763 mResolver.notifyChange(mNotifyUri, null, false); 764 } 765 } 766 767 @Override 768 public String toString() { 769 return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}"; 770 } 771 } 772 773 private class DirectoryCursor extends MatrixCursor { 774 private final File mFile; 775 776 public DirectoryCursor(String[] columnNames, String docId, File file) { 777 super(columnNames); 778 779 final Uri notifyUri = DocumentsContract.buildChildDocumentsUri( 780 AUTHORITY, docId); 781 setNotificationUri(getContext().getContentResolver(), notifyUri); 782 783 mFile = file; 784 startObserving(mFile, notifyUri); 785 } 786 787 @Override 788 public void close() { 789 super.close(); 790 stopObserving(mFile); 791 } 792 } 793} 794