• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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