1 /* 2 * Copyright (C) 2017 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.internal.content; 18 19 import android.annotation.CallSuper; 20 import android.annotation.Nullable; 21 import android.content.ContentResolver; 22 import android.content.Intent; 23 import android.content.res.AssetFileDescriptor; 24 import android.database.Cursor; 25 import android.database.MatrixCursor; 26 import android.database.MatrixCursor.RowBuilder; 27 import android.graphics.Point; 28 import android.net.Uri; 29 import android.os.Binder; 30 import android.os.Bundle; 31 import android.os.CancellationSignal; 32 import android.os.FileObserver; 33 import android.os.FileUtils; 34 import android.os.Handler; 35 import android.os.ParcelFileDescriptor; 36 import android.provider.DocumentsContract; 37 import android.provider.DocumentsContract.Document; 38 import android.provider.DocumentsProvider; 39 import android.provider.MediaStore; 40 import android.provider.MetadataReader; 41 import android.system.Int64Ref; 42 import android.text.TextUtils; 43 import android.util.ArrayMap; 44 import android.util.Log; 45 import android.webkit.MimeTypeMap; 46 47 import com.android.internal.annotations.GuardedBy; 48 import com.android.internal.util.ArrayUtils; 49 50 import libcore.io.IoUtils; 51 52 import java.io.File; 53 import java.io.FileInputStream; 54 import java.io.FileNotFoundException; 55 import java.io.IOException; 56 import java.io.InputStream; 57 import java.nio.file.FileSystems; 58 import java.nio.file.FileVisitResult; 59 import java.nio.file.FileVisitor; 60 import java.nio.file.Files; 61 import java.nio.file.Path; 62 import java.nio.file.attribute.BasicFileAttributes; 63 import java.util.Arrays; 64 import java.util.LinkedList; 65 import java.util.List; 66 import java.util.Set; 67 import java.util.concurrent.CopyOnWriteArrayList; 68 69 /** 70 * A helper class for {@link android.provider.DocumentsProvider} to perform file operations on local 71 * files. 72 */ 73 public abstract class FileSystemProvider extends DocumentsProvider { 74 75 private static final String TAG = "FileSystemProvider"; 76 77 private static final boolean LOG_INOTIFY = false; 78 79 protected static final String SUPPORTED_QUERY_ARGS = joinNewline( 80 DocumentsContract.QUERY_ARG_DISPLAY_NAME, 81 DocumentsContract.QUERY_ARG_FILE_SIZE_OVER, 82 DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER, 83 DocumentsContract.QUERY_ARG_MIME_TYPES); 84 joinNewline(String... args)85 private static String joinNewline(String... args) { 86 return TextUtils.join("\n", args); 87 } 88 89 private String[] mDefaultProjection; 90 91 @GuardedBy("mObservers") 92 private final ArrayMap<File, DirectoryObserver> mObservers = new ArrayMap<>(); 93 94 private Handler mHandler; 95 getFileForDocId(String docId, boolean visible)96 protected abstract File getFileForDocId(String docId, boolean visible) 97 throws FileNotFoundException; 98 getDocIdForFile(File file)99 protected abstract String getDocIdForFile(File file) throws FileNotFoundException; 100 buildNotificationUri(String docId)101 protected abstract Uri buildNotificationUri(String docId); 102 103 /** 104 * Callback indicating that the given document has been modified. This gives 105 * the provider a hook to invalidate cached data, such as {@code sdcardfs}. 106 */ onDocIdChanged(String docId)107 protected void onDocIdChanged(String docId) { 108 // Default is no-op 109 } 110 111 @Override onCreate()112 public boolean onCreate() { 113 throw new UnsupportedOperationException( 114 "Subclass should override this and call onCreate(defaultDocumentProjection)"); 115 } 116 117 @CallSuper onCreate(String[] defaultProjection)118 protected void onCreate(String[] defaultProjection) { 119 mHandler = new Handler(); 120 mDefaultProjection = defaultProjection; 121 } 122 123 @Override isChildDocument(String parentDocId, String docId)124 public boolean isChildDocument(String parentDocId, String docId) { 125 try { 126 final File parent = getFileForDocId(parentDocId).getCanonicalFile(); 127 final File doc = getFileForDocId(docId).getCanonicalFile(); 128 return FileUtils.contains(parent, doc); 129 } catch (IOException e) { 130 throw new IllegalArgumentException( 131 "Failed to determine if " + docId + " is child of " + parentDocId + ": " + e); 132 } 133 } 134 135 @Override getDocumentMetadata(String documentId)136 public @Nullable Bundle getDocumentMetadata(String documentId) 137 throws FileNotFoundException { 138 File file = getFileForDocId(documentId); 139 140 if (!file.exists()) { 141 throw new FileNotFoundException("Can't find the file for documentId: " + documentId); 142 } 143 144 final String mimeType = getDocumentType(documentId); 145 if (Document.MIME_TYPE_DIR.equals(mimeType)) { 146 final Int64Ref treeCount = new Int64Ref(0); 147 final Int64Ref treeSize = new Int64Ref(0); 148 try { 149 final Path path = FileSystems.getDefault().getPath(file.getAbsolutePath()); 150 Files.walkFileTree(path, new FileVisitor<Path>() { 151 @Override 152 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { 153 return FileVisitResult.CONTINUE; 154 } 155 156 @Override 157 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { 158 treeCount.value += 1; 159 treeSize.value += attrs.size(); 160 return FileVisitResult.CONTINUE; 161 } 162 163 @Override 164 public FileVisitResult visitFileFailed(Path file, IOException exc) { 165 return FileVisitResult.CONTINUE; 166 } 167 168 @Override 169 public FileVisitResult postVisitDirectory(Path dir, IOException exc) { 170 return FileVisitResult.CONTINUE; 171 } 172 }); 173 } catch (IOException e) { 174 Log.e(TAG, "An error occurred retrieving the metadata", e); 175 return null; 176 } 177 178 final Bundle res = new Bundle(); 179 res.putLong(DocumentsContract.METADATA_TREE_COUNT, treeCount.value); 180 res.putLong(DocumentsContract.METADATA_TREE_SIZE, treeSize.value); 181 return res; 182 } 183 184 if (!file.isFile()) { 185 Log.w(TAG, "Can't stream non-regular file. Returning empty metadata."); 186 return null; 187 } 188 if (!file.canRead()) { 189 Log.w(TAG, "Can't stream non-readable file. Returning empty metadata."); 190 return null; 191 } 192 if (!MetadataReader.isSupportedMimeType(mimeType)) { 193 Log.w(TAG, "Unsupported type " + mimeType + ". Returning empty metadata."); 194 return null; 195 } 196 197 InputStream stream = null; 198 try { 199 Bundle metadata = new Bundle(); 200 stream = new FileInputStream(file.getAbsolutePath()); 201 MetadataReader.getMetadata(metadata, stream, mimeType, null); 202 return metadata; 203 } catch (IOException e) { 204 Log.e(TAG, "An error occurred retrieving the metadata", e); 205 return null; 206 } finally { 207 IoUtils.closeQuietly(stream); 208 } 209 } 210 findDocumentPath(File parent, File doc)211 protected final List<String> findDocumentPath(File parent, File doc) 212 throws FileNotFoundException { 213 214 if (!doc.exists()) { 215 throw new FileNotFoundException(doc + " is not found."); 216 } 217 218 if (!FileUtils.contains(parent, doc)) { 219 throw new FileNotFoundException(doc + " is not found under " + parent); 220 } 221 222 LinkedList<String> path = new LinkedList<>(); 223 while (doc != null && FileUtils.contains(parent, doc)) { 224 path.addFirst(getDocIdForFile(doc)); 225 226 doc = doc.getParentFile(); 227 } 228 229 return path; 230 } 231 232 @Override createDocument(String docId, String mimeType, String displayName)233 public String createDocument(String docId, String mimeType, String displayName) 234 throws FileNotFoundException { 235 displayName = FileUtils.buildValidFatFilename(displayName); 236 237 final File parent = getFileForDocId(docId); 238 if (!parent.isDirectory()) { 239 throw new IllegalArgumentException("Parent document isn't a directory"); 240 } 241 242 final File file = FileUtils.buildUniqueFile(parent, mimeType, displayName); 243 final String childId; 244 if (Document.MIME_TYPE_DIR.equals(mimeType)) { 245 if (!file.mkdir()) { 246 throw new IllegalStateException("Failed to mkdir " + file); 247 } 248 childId = getDocIdForFile(file); 249 onDocIdChanged(childId); 250 } else { 251 try { 252 if (!file.createNewFile()) { 253 throw new IllegalStateException("Failed to touch " + file); 254 } 255 childId = getDocIdForFile(file); 256 onDocIdChanged(childId); 257 } catch (IOException e) { 258 throw new IllegalStateException("Failed to touch " + file + ": " + e); 259 } 260 } 261 MediaStore.scanFile(getContext(), file); 262 263 return childId; 264 } 265 266 @Override renameDocument(String docId, String displayName)267 public String renameDocument(String docId, String displayName) throws FileNotFoundException { 268 // Since this provider treats renames as generating a completely new 269 // docId, we're okay with letting the MIME type change. 270 displayName = FileUtils.buildValidFatFilename(displayName); 271 272 final File before = getFileForDocId(docId); 273 final File beforeVisibleFile = getFileForDocId(docId, true); 274 final File after = FileUtils.buildUniqueFile(before.getParentFile(), displayName); 275 if (!before.renameTo(after)) { 276 throw new IllegalStateException("Failed to rename to " + after); 277 } 278 279 final String afterDocId = getDocIdForFile(after); 280 onDocIdChanged(docId); 281 onDocIdChanged(afterDocId); 282 283 final File afterVisibleFile = getFileForDocId(afterDocId, true); 284 moveInMediaStore(beforeVisibleFile, afterVisibleFile); 285 286 if (!TextUtils.equals(docId, afterDocId)) { 287 return afterDocId; 288 } else { 289 return null; 290 } 291 } 292 293 @Override moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId)294 public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, 295 String targetParentDocumentId) 296 throws FileNotFoundException { 297 final File before = getFileForDocId(sourceDocumentId); 298 final File after = new File(getFileForDocId(targetParentDocumentId), before.getName()); 299 final File visibleFileBefore = getFileForDocId(sourceDocumentId, true); 300 301 if (after.exists()) { 302 throw new IllegalStateException("Already exists " + after); 303 } 304 if (!before.renameTo(after)) { 305 throw new IllegalStateException("Failed to move to " + after); 306 } 307 308 final String docId = getDocIdForFile(after); 309 onDocIdChanged(sourceDocumentId); 310 onDocIdChanged(docId); 311 moveInMediaStore(visibleFileBefore, getFileForDocId(docId, true)); 312 313 return docId; 314 } 315 moveInMediaStore(@ullable File oldVisibleFile, @Nullable File newVisibleFile)316 private void moveInMediaStore(@Nullable File oldVisibleFile, @Nullable File newVisibleFile) { 317 if (oldVisibleFile != null) { 318 MediaStore.scanFile(getContext(), oldVisibleFile); 319 } 320 if (newVisibleFile != null) { 321 MediaStore.scanFile(getContext(), newVisibleFile); 322 } 323 } 324 325 @Override deleteDocument(String docId)326 public void deleteDocument(String docId) throws FileNotFoundException { 327 final File file = getFileForDocId(docId); 328 final File visibleFile = getFileForDocId(docId, true); 329 330 final boolean isDirectory = file.isDirectory(); 331 if (isDirectory) { 332 FileUtils.deleteContents(file); 333 } 334 if (!file.delete()) { 335 throw new IllegalStateException("Failed to delete " + file); 336 } 337 338 onDocIdChanged(docId); 339 removeFromMediaStore(visibleFile, isDirectory); 340 } 341 removeFromMediaStore(@ullable File visibleFile, boolean isFolder)342 private void removeFromMediaStore(@Nullable File visibleFile, boolean isFolder) 343 throws FileNotFoundException { 344 // visibleFolder is null if we're removing a document from external thumb drive or SD card. 345 if (visibleFile != null) { 346 final long token = Binder.clearCallingIdentity(); 347 348 try { 349 final ContentResolver resolver = getContext().getContentResolver(); 350 final Uri externalUri = MediaStore.Files.getContentUri("external"); 351 352 // Remove media store entries for any files inside this directory, using 353 // path prefix match. Logic borrowed from MtpDatabase. 354 if (isFolder) { 355 final String path = visibleFile.getAbsolutePath() + "/"; 356 resolver.delete(externalUri, 357 "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)", 358 new String[]{path + "%", Integer.toString(path.length()), path}); 359 } 360 361 // Remove media store entry for this exact file. 362 final String path = visibleFile.getAbsolutePath(); 363 resolver.delete(externalUri, 364 "_data LIKE ?1 AND lower(_data)=lower(?2)", 365 new String[]{path, path}); 366 } finally { 367 Binder.restoreCallingIdentity(token); 368 } 369 } 370 } 371 372 @Override queryDocument(String documentId, String[] projection)373 public Cursor queryDocument(String documentId, String[] projection) 374 throws FileNotFoundException { 375 final MatrixCursor result = new MatrixCursor(resolveProjection(projection)); 376 includeFile(result, documentId, null); 377 return result; 378 } 379 380 @Override queryChildDocuments( String parentDocumentId, String[] projection, String sortOrder)381 public Cursor queryChildDocuments( 382 String parentDocumentId, String[] projection, String sortOrder) 383 throws FileNotFoundException { 384 385 final File parent = getFileForDocId(parentDocumentId); 386 final MatrixCursor result = new DirectoryCursor( 387 resolveProjection(projection), parentDocumentId, parent); 388 if (parent.isDirectory()) { 389 for (File file : FileUtils.listFilesOrEmpty(parent)) { 390 includeFile(result, null, file); 391 } 392 } else { 393 Log.w(TAG, "parentDocumentId '" + parentDocumentId + "' is not Directory"); 394 } 395 return result; 396 } 397 398 /** 399 * Searches documents under the given folder. 400 * 401 * To avoid runtime explosion only returns the at most 23 items. 402 * 403 * @param folder the root folder where recursive search begins 404 * @param query the search condition used to match file names 405 * @param projection projection of the returned cursor 406 * @param exclusion absolute file paths to exclude from result 407 * @param queryArgs the query arguments for search 408 * @return cursor containing search result. Include 409 * {@link ContentResolver#EXTRA_HONORED_ARGS} in {@link Cursor} 410 * extras {@link Bundle} when any QUERY_ARG_* value was honored 411 * during the preparation of the results. 412 * @throws FileNotFoundException when root folder doesn't exist or search fails 413 * 414 * @see ContentResolver#EXTRA_HONORED_ARGS 415 */ querySearchDocuments( File folder, String[] projection, Set<String> exclusion, Bundle queryArgs)416 protected final Cursor querySearchDocuments( 417 File folder, String[] projection, Set<String> exclusion, Bundle queryArgs) 418 throws FileNotFoundException { 419 final MatrixCursor result = new MatrixCursor(resolveProjection(projection)); 420 final LinkedList<File> pending = new LinkedList<>(); 421 pending.add(folder); 422 while (!pending.isEmpty() && result.getCount() < 24) { 423 final File file = pending.removeFirst(); 424 if (file.isDirectory()) { 425 for (File child : file.listFiles()) { 426 pending.add(child); 427 } 428 } 429 if (!exclusion.contains(file.getAbsolutePath()) && matchSearchQueryArguments(file, 430 queryArgs)) { 431 includeFile(result, null, file); 432 } 433 } 434 435 final String[] handledQueryArgs = DocumentsContract.getHandledQueryArguments(queryArgs); 436 if (handledQueryArgs.length > 0) { 437 final Bundle extras = new Bundle(); 438 extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, handledQueryArgs); 439 result.setExtras(extras); 440 } 441 return result; 442 } 443 444 @Override getDocumentType(String documentId)445 public String getDocumentType(String documentId) throws FileNotFoundException { 446 return getDocumentType(documentId, getFileForDocId(documentId)); 447 } 448 getDocumentType(final String documentId, final File file)449 private String getDocumentType(final String documentId, final File file) 450 throws FileNotFoundException { 451 if (file.isDirectory()) { 452 return Document.MIME_TYPE_DIR; 453 } else { 454 final int lastDot = documentId.lastIndexOf('.'); 455 if (lastDot >= 0) { 456 final String extension = documentId.substring(lastDot + 1).toLowerCase(); 457 final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 458 if (mime != null) { 459 return mime; 460 } 461 } 462 return ContentResolver.MIME_TYPE_DEFAULT; 463 } 464 } 465 466 @Override openDocument( String documentId, String mode, CancellationSignal signal)467 public ParcelFileDescriptor openDocument( 468 String documentId, String mode, CancellationSignal signal) 469 throws FileNotFoundException { 470 final File file = getFileForDocId(documentId); 471 final File visibleFile = getFileForDocId(documentId, true); 472 473 final int pfdMode = ParcelFileDescriptor.parseMode(mode); 474 if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY || visibleFile == null) { 475 return ParcelFileDescriptor.open(file, pfdMode); 476 } else { 477 try { 478 // When finished writing, kick off media scanner 479 return ParcelFileDescriptor.open( 480 file, pfdMode, mHandler, (IOException e) -> { 481 onDocIdChanged(documentId); 482 scanFile(visibleFile); 483 }); 484 } catch (IOException e) { 485 throw new FileNotFoundException("Failed to open for writing: " + e); 486 } 487 } 488 } 489 490 /** 491 * Test if the file matches the query arguments. 492 * 493 * @param file the file to test 494 * @param queryArgs the query arguments 495 */ matchSearchQueryArguments(File file, Bundle queryArgs)496 private boolean matchSearchQueryArguments(File file, Bundle queryArgs) { 497 if (file == null) { 498 return false; 499 } 500 501 final String fileMimeType; 502 final String fileName = file.getName(); 503 504 if (file.isDirectory()) { 505 fileMimeType = DocumentsContract.Document.MIME_TYPE_DIR; 506 } else { 507 int dotPos = fileName.lastIndexOf('.'); 508 if (dotPos < 0) { 509 return false; 510 } 511 final String extension = fileName.substring(dotPos + 1); 512 fileMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 513 } 514 return DocumentsContract.matchSearchQueryArguments(queryArgs, fileName, fileMimeType, 515 file.lastModified(), file.length()); 516 } 517 scanFile(File visibleFile)518 private void scanFile(File visibleFile) { 519 final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); 520 intent.setData(Uri.fromFile(visibleFile)); 521 getContext().sendBroadcast(intent); 522 } 523 524 @Override openDocumentThumbnail( String documentId, Point sizeHint, CancellationSignal signal)525 public AssetFileDescriptor openDocumentThumbnail( 526 String documentId, Point sizeHint, CancellationSignal signal) 527 throws FileNotFoundException { 528 final File file = getFileForDocId(documentId); 529 return DocumentsContract.openImageThumbnail(file); 530 } 531 includeFile(final MatrixCursor result, String docId, File file)532 protected RowBuilder includeFile(final MatrixCursor result, String docId, File file) 533 throws FileNotFoundException { 534 final String[] columns = result.getColumnNames(); 535 final RowBuilder row = result.newRow(); 536 537 if (docId == null) { 538 docId = getDocIdForFile(file); 539 } else { 540 file = getFileForDocId(docId); 541 } 542 final String mimeType = getDocumentType(docId, file); 543 row.add(Document.COLUMN_DOCUMENT_ID, docId); 544 row.add(Document.COLUMN_MIME_TYPE, mimeType); 545 546 final int flagIndex = ArrayUtils.indexOf(columns, Document.COLUMN_FLAGS); 547 if (flagIndex != -1) { 548 int flags = 0; 549 if (file.canWrite()) { 550 if (mimeType.equals(Document.MIME_TYPE_DIR)) { 551 flags |= Document.FLAG_DIR_SUPPORTS_CREATE; 552 flags |= Document.FLAG_SUPPORTS_DELETE; 553 flags |= Document.FLAG_SUPPORTS_RENAME; 554 flags |= Document.FLAG_SUPPORTS_MOVE; 555 } else { 556 flags |= Document.FLAG_SUPPORTS_WRITE; 557 flags |= Document.FLAG_SUPPORTS_DELETE; 558 flags |= Document.FLAG_SUPPORTS_RENAME; 559 flags |= Document.FLAG_SUPPORTS_MOVE; 560 } 561 } 562 563 if (mimeType.startsWith("image/")) { 564 flags |= Document.FLAG_SUPPORTS_THUMBNAIL; 565 } 566 567 if (typeSupportsMetadata(mimeType)) { 568 flags |= Document.FLAG_SUPPORTS_METADATA; 569 } 570 row.add(flagIndex, flags); 571 } 572 573 final int displayNameIndex = ArrayUtils.indexOf(columns, Document.COLUMN_DISPLAY_NAME); 574 if (displayNameIndex != -1) { 575 row.add(displayNameIndex, file.getName()); 576 } 577 578 final int lastModifiedIndex = ArrayUtils.indexOf(columns, Document.COLUMN_LAST_MODIFIED); 579 if (lastModifiedIndex != -1) { 580 final long lastModified = file.lastModified(); 581 // Only publish dates reasonably after epoch 582 if (lastModified > 31536000000L) { 583 row.add(lastModifiedIndex, lastModified); 584 } 585 } 586 final int sizeIndex = ArrayUtils.indexOf(columns, Document.COLUMN_SIZE); 587 if (sizeIndex != -1) { 588 row.add(sizeIndex, file.length()); 589 } 590 591 // Return the row builder just in case any subclass want to add more stuff to it. 592 return row; 593 } 594 typeSupportsMetadata(String mimeType)595 protected boolean typeSupportsMetadata(String mimeType) { 596 return MetadataReader.isSupportedMimeType(mimeType) 597 || Document.MIME_TYPE_DIR.equals(mimeType); 598 } 599 getFileForDocId(String docId)600 protected final File getFileForDocId(String docId) throws FileNotFoundException { 601 return getFileForDocId(docId, false); 602 } 603 resolveProjection(String[] projection)604 private String[] resolveProjection(String[] projection) { 605 return projection == null ? mDefaultProjection : projection; 606 } 607 startObserving(File file, Uri notifyUri, DirectoryCursor cursor)608 private void startObserving(File file, Uri notifyUri, DirectoryCursor cursor) { 609 synchronized (mObservers) { 610 DirectoryObserver observer = mObservers.get(file); 611 if (observer == null) { 612 observer = 613 new DirectoryObserver(file, getContext().getContentResolver(), notifyUri); 614 observer.startWatching(); 615 mObservers.put(file, observer); 616 } 617 observer.mCursors.add(cursor); 618 619 if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer); 620 } 621 } 622 stopObserving(File file, DirectoryCursor cursor)623 private void stopObserving(File file, DirectoryCursor cursor) { 624 synchronized (mObservers) { 625 DirectoryObserver observer = mObservers.get(file); 626 if (observer == null) return; 627 628 observer.mCursors.remove(cursor); 629 if (observer.mCursors.size() == 0) { 630 mObservers.remove(file); 631 observer.stopWatching(); 632 } 633 634 if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer); 635 } 636 } 637 638 private static class DirectoryObserver extends FileObserver { 639 private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO 640 | CREATE | DELETE | DELETE_SELF | MOVE_SELF; 641 642 private final File mFile; 643 private final ContentResolver mResolver; 644 private final Uri mNotifyUri; 645 private final CopyOnWriteArrayList<DirectoryCursor> mCursors; 646 DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri)647 DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) { 648 super(file.getAbsolutePath(), NOTIFY_EVENTS); 649 mFile = file; 650 mResolver = resolver; 651 mNotifyUri = notifyUri; 652 mCursors = new CopyOnWriteArrayList<>(); 653 } 654 655 @Override onEvent(int event, String path)656 public void onEvent(int event, String path) { 657 if ((event & NOTIFY_EVENTS) != 0) { 658 if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path); 659 for (DirectoryCursor cursor : mCursors) { 660 cursor.notifyChanged(); 661 } 662 mResolver.notifyChange(mNotifyUri, null, false); 663 } 664 } 665 666 @Override toString()667 public String toString() { 668 String filePath = mFile.getAbsolutePath(); 669 return "DirectoryObserver{file=" + filePath + ", ref=" + mCursors.size() + "}"; 670 } 671 } 672 673 private class DirectoryCursor extends MatrixCursor { 674 private final File mFile; 675 DirectoryCursor(String[] columnNames, String docId, File file)676 public DirectoryCursor(String[] columnNames, String docId, File file) { 677 super(columnNames); 678 679 final Uri notifyUri = buildNotificationUri(docId); 680 boolean registerSelfObserver = false; // Our FileObserver sees all relevant changes. 681 setNotificationUris(getContext().getContentResolver(), Arrays.asList(notifyUri), 682 getContext().getContentResolver().getUserId(), registerSelfObserver); 683 684 mFile = file; 685 startObserving(mFile, notifyUri, this); 686 } 687 notifyChanged()688 public void notifyChanged() { 689 onChange(false); 690 } 691 692 @Override close()693 public void close() { 694 super.close(); 695 stopObserving(mFile, this); 696 } 697 } 698 } 699