blob: d49d572310d799db4c9486508deca3783141a5a1 [file] [log] [blame]
Garfield Tan75379db2017-02-08 15:32:56 -08001/*
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
17package com.android.internal.content;
18
19import android.annotation.CallSuper;
Garfield Tand21af532017-04-17 14:22:32 -070020import android.annotation.Nullable;
Garfield Tan75379db2017-02-08 15:32:56 -080021import android.content.ContentResolver;
Garfield Tan9bd2f6c2017-03-10 15:38:15 -080022import android.content.ContentValues;
Garfield Tan75379db2017-02-08 15:32:56 -080023import android.content.Intent;
24import android.content.res.AssetFileDescriptor;
25import android.database.Cursor;
26import android.database.MatrixCursor;
27import android.database.MatrixCursor.RowBuilder;
28import android.graphics.Point;
29import android.net.Uri;
Garfield Tanfac8aea2017-06-27 11:03:42 -070030import android.os.Binder;
Julian Mancinib6505152017-06-27 13:29:09 -070031import android.os.Bundle;
Garfield Tan75379db2017-02-08 15:32:56 -080032import android.os.CancellationSignal;
33import android.os.FileObserver;
34import android.os.FileUtils;
35import android.os.Handler;
36import android.os.ParcelFileDescriptor;
37import android.provider.DocumentsContract;
38import android.provider.DocumentsContract.Document;
39import android.provider.DocumentsProvider;
40import android.provider.MediaStore;
Steve McKay5a10ff12017-08-01 15:02:50 -070041import android.provider.MetadataReader;
Garfield Tan75379db2017-02-08 15:32:56 -080042import android.text.TextUtils;
43import android.util.ArrayMap;
44import android.util.Log;
45import android.webkit.MimeTypeMap;
46
47import com.android.internal.annotations.GuardedBy;
48
Julian Mancinib6505152017-06-27 13:29:09 -070049import libcore.io.IoUtils;
50
Garfield Tan75379db2017-02-08 15:32:56 -080051import java.io.File;
Julian Mancinib6505152017-06-27 13:29:09 -070052import java.io.FileInputStream;
Garfield Tan75379db2017-02-08 15:32:56 -080053import java.io.FileNotFoundException;
54import java.io.IOException;
Steve McKay5a10ff12017-08-01 15:02:50 -070055import java.io.InputStream;
Garfield Tan75379db2017-02-08 15:32:56 -080056import java.util.LinkedList;
57import java.util.List;
58import java.util.Set;
59
60/**
61 * A helper class for {@link android.provider.DocumentsProvider} to perform file operations on local
62 * files.
63 */
64public abstract class FileSystemProvider extends DocumentsProvider {
65
66 private static final String TAG = "FileSystemProvider";
67
68 private static final boolean LOG_INOTIFY = false;
69
70 private String[] mDefaultProjection;
71
72 @GuardedBy("mObservers")
73 private final ArrayMap<File, DirectoryObserver> mObservers = new ArrayMap<>();
74
75 private Handler mHandler;
76
Julian Mancinib6505152017-06-27 13:29:09 -070077 private static final String MIMETYPE_JPEG = "image/jpeg";
Julian Mancinib6505152017-06-27 13:29:09 -070078 private static final String MIMETYPE_JPG = "image/jpg";
Steve McKay36f1d7e2017-07-20 11:41:58 -070079 private static final String MIMETYPE_OCTET_STREAM = "application/octet-stream";
Julian Mancinib6505152017-06-27 13:29:09 -070080
Garfield Tan75379db2017-02-08 15:32:56 -080081 protected abstract File getFileForDocId(String docId, boolean visible)
82 throws FileNotFoundException;
83
84 protected abstract String getDocIdForFile(File file) throws FileNotFoundException;
85
86 protected abstract Uri buildNotificationUri(String docId);
87
88 @Override
89 public boolean onCreate() {
90 throw new UnsupportedOperationException(
91 "Subclass should override this and call onCreate(defaultDocumentProjection)");
92 }
93
94 @CallSuper
95 protected void onCreate(String[] defaultProjection) {
96 mHandler = new Handler();
97 mDefaultProjection = defaultProjection;
98 }
99
100 @Override
101 public boolean isChildDocument(String parentDocId, String docId) {
102 try {
103 final File parent = getFileForDocId(parentDocId).getCanonicalFile();
104 final File doc = getFileForDocId(docId).getCanonicalFile();
105 return FileUtils.contains(parent, doc);
106 } catch (IOException e) {
107 throw new IllegalArgumentException(
108 "Failed to determine if " + docId + " is child of " + parentDocId + ": " + e);
109 }
110 }
111
Julian Mancinib6505152017-06-27 13:29:09 -0700112 @Override
Steve McKay17a9ce32017-07-27 13:37:14 -0700113 public @Nullable Bundle getDocumentMetadata(String documentId)
Julian Mancinib6505152017-06-27 13:29:09 -0700114 throws FileNotFoundException {
115 File file = getFileForDocId(documentId);
Steve McKay36f1d7e2017-07-20 11:41:58 -0700116
117 if (!file.exists()) {
118 throw new FileNotFoundException("Can't find the file for documentId: " + documentId);
119 }
120
121 if (!file.isFile()) {
122 Log.w(TAG, "Can't stream non-regular file. Returning empty metadata.");
Steve McKay5a10ff12017-08-01 15:02:50 -0700123 return null;
Julian Mancinib6505152017-06-27 13:29:09 -0700124 }
Steve McKay36f1d7e2017-07-20 11:41:58 -0700125
126 if (!file.canRead()) {
127 Log.w(TAG, "Can't stream non-readable file. Returning empty metadata.");
Steve McKay5a10ff12017-08-01 15:02:50 -0700128 return null;
Julian Mancinib6505152017-06-27 13:29:09 -0700129 }
Steve McKay36f1d7e2017-07-20 11:41:58 -0700130
Steve McKay5a10ff12017-08-01 15:02:50 -0700131 String mimeType = getTypeForFile(file);
132 if (!MetadataReader.isSupportedMimeType(mimeType)) {
133 return null;
134 }
Steve McKay36f1d7e2017-07-20 11:41:58 -0700135
Steve McKay5a10ff12017-08-01 15:02:50 -0700136 InputStream stream = null;
Steve McKay36f1d7e2017-07-20 11:41:58 -0700137 try {
Steve McKay5a10ff12017-08-01 15:02:50 -0700138 Bundle metadata = new Bundle();
139 stream = new FileInputStream(file.getAbsolutePath());
140 MetadataReader.getMetadata(metadata, stream, mimeType, null);
141 return metadata;
Steve McKay36f1d7e2017-07-20 11:41:58 -0700142 } catch (IOException e) {
143 Log.e(TAG, "An error occurred retrieving the metadata", e);
Steve McKay5a10ff12017-08-01 15:02:50 -0700144 return null;
Steve McKay36f1d7e2017-07-20 11:41:58 -0700145 } finally {
146 IoUtils.closeQuietly(stream);
147 }
Julian Mancinib6505152017-06-27 13:29:09 -0700148 }
149
Garfield Tan75379db2017-02-08 15:32:56 -0800150 protected final List<String> findDocumentPath(File parent, File doc)
151 throws FileNotFoundException {
152
153 if (!doc.exists()) {
154 throw new FileNotFoundException(doc + " is not found.");
155 }
156
157 if (!FileUtils.contains(parent, doc)) {
158 throw new FileNotFoundException(doc + " is not found under " + parent);
159 }
160
161 LinkedList<String> path = new LinkedList<>();
162 while (doc != null && FileUtils.contains(parent, doc)) {
163 path.addFirst(getDocIdForFile(doc));
164
165 doc = doc.getParentFile();
166 }
167
168 return path;
169 }
170
171 @Override
172 public String createDocument(String docId, String mimeType, String displayName)
173 throws FileNotFoundException {
174 displayName = FileUtils.buildValidFatFilename(displayName);
175
176 final File parent = getFileForDocId(docId);
177 if (!parent.isDirectory()) {
178 throw new IllegalArgumentException("Parent document isn't a directory");
179 }
180
181 final File file = FileUtils.buildUniqueFile(parent, mimeType, displayName);
Garfield Tan93615412017-03-20 17:21:55 -0700182 final String childId;
Garfield Tan75379db2017-02-08 15:32:56 -0800183 if (Document.MIME_TYPE_DIR.equals(mimeType)) {
184 if (!file.mkdir()) {
185 throw new IllegalStateException("Failed to mkdir " + file);
186 }
Garfield Tan93615412017-03-20 17:21:55 -0700187 childId = getDocIdForFile(file);
188 addFolderToMediaStore(getFileForDocId(childId, true));
Garfield Tan75379db2017-02-08 15:32:56 -0800189 } else {
190 try {
191 if (!file.createNewFile()) {
192 throw new IllegalStateException("Failed to touch " + file);
193 }
Garfield Tan93615412017-03-20 17:21:55 -0700194 childId = getDocIdForFile(file);
Garfield Tan75379db2017-02-08 15:32:56 -0800195 } catch (IOException e) {
196 throw new IllegalStateException("Failed to touch " + file + ": " + e);
197 }
198 }
199
Garfield Tan93615412017-03-20 17:21:55 -0700200 return childId;
201 }
202
Garfield Tand21af532017-04-17 14:22:32 -0700203 private void addFolderToMediaStore(@Nullable File visibleFolder) {
204 // visibleFolder is null if we're adding a folder to external thumb drive or SD card.
205 if (visibleFolder != null) {
206 assert (visibleFolder.isDirectory());
Garfield Tan93615412017-03-20 17:21:55 -0700207
Garfield Tanfac8aea2017-06-27 11:03:42 -0700208 final long token = Binder.clearCallingIdentity();
209
210 try {
211 final ContentResolver resolver = getContext().getContentResolver();
212 final Uri uri = MediaStore.Files.getDirectoryUri("external");
213 ContentValues values = new ContentValues();
214 values.put(MediaStore.Files.FileColumns.DATA, visibleFolder.getAbsolutePath());
215 resolver.insert(uri, values);
216 } finally {
217 Binder.restoreCallingIdentity(token);
218 }
Garfield Tand21af532017-04-17 14:22:32 -0700219 }
Garfield Tan75379db2017-02-08 15:32:56 -0800220 }
221
222 @Override
223 public String renameDocument(String docId, String displayName) throws FileNotFoundException {
224 // Since this provider treats renames as generating a completely new
225 // docId, we're okay with letting the MIME type change.
226 displayName = FileUtils.buildValidFatFilename(displayName);
227
228 final File before = getFileForDocId(docId);
229 final File after = FileUtils.buildUniqueFile(before.getParentFile(), displayName);
230 final File visibleFileBefore = getFileForDocId(docId, true);
231 if (!before.renameTo(after)) {
232 throw new IllegalStateException("Failed to rename to " + after);
233 }
Garfield Tan75379db2017-02-08 15:32:56 -0800234
235 final String afterDocId = getDocIdForFile(after);
Garfield Tan9bd2f6c2017-03-10 15:38:15 -0800236 moveInMediaStore(visibleFileBefore, getFileForDocId(afterDocId, true));
Garfield Tan75379db2017-02-08 15:32:56 -0800237
238 if (!TextUtils.equals(docId, afterDocId)) {
239 return afterDocId;
240 } else {
241 return null;
242 }
243 }
244
245 @Override
Garfield Tan75379db2017-02-08 15:32:56 -0800246 public String moveDocument(String sourceDocumentId, String sourceParentDocumentId,
247 String targetParentDocumentId)
248 throws FileNotFoundException {
249 final File before = getFileForDocId(sourceDocumentId);
250 final File after = new File(getFileForDocId(targetParentDocumentId), before.getName());
251 final File visibleFileBefore = getFileForDocId(sourceDocumentId, true);
252
253 if (after.exists()) {
254 throw new IllegalStateException("Already exists " + after);
255 }
256 if (!before.renameTo(after)) {
257 throw new IllegalStateException("Failed to move to " + after);
258 }
259
Garfield Tan75379db2017-02-08 15:32:56 -0800260 final String docId = getDocIdForFile(after);
Garfield Tan9bd2f6c2017-03-10 15:38:15 -0800261 moveInMediaStore(visibleFileBefore, getFileForDocId(docId, true));
Garfield Tan75379db2017-02-08 15:32:56 -0800262
263 return docId;
264 }
265
Garfield Tand21af532017-04-17 14:22:32 -0700266 private void moveInMediaStore(@Nullable File oldVisibleFile, @Nullable File newVisibleFile) {
267 // visibleFolders are null if we're moving a document in external thumb drive or SD card.
268 //
269 // They should be all null or not null at the same time. File#renameTo() doesn't work across
270 // volumes so an exception will be thrown before calling this method.
271 if (oldVisibleFile != null && newVisibleFile != null) {
Garfield Tanfac8aea2017-06-27 11:03:42 -0700272 final long token = Binder.clearCallingIdentity();
Garfield Tan9bd2f6c2017-03-10 15:38:15 -0800273
Garfield Tanfac8aea2017-06-27 11:03:42 -0700274 try {
275 final ContentResolver resolver = getContext().getContentResolver();
276 final Uri externalUri = newVisibleFile.isDirectory()
277 ? MediaStore.Files.getDirectoryUri("external")
278 : MediaStore.Files.getContentUri("external");
Garfield Tan9bd2f6c2017-03-10 15:38:15 -0800279
Garfield Tanfac8aea2017-06-27 11:03:42 -0700280 ContentValues values = new ContentValues();
281 values.put(MediaStore.Files.FileColumns.DATA, newVisibleFile.getAbsolutePath());
282
283 // Logic borrowed from MtpDatabase.
284 // note - we are relying on a special case in MediaProvider.update() to update
285 // the paths for all children in the case where this is a directory.
286 final String path = oldVisibleFile.getAbsolutePath();
287 resolver.update(externalUri,
288 values,
289 "_data LIKE ? AND lower(_data)=lower(?)",
290 new String[]{path, path});
291 } finally {
292 Binder.restoreCallingIdentity(token);
293 }
Garfield Tan9bd2f6c2017-03-10 15:38:15 -0800294 }
295 }
296
297 @Override
298 public void deleteDocument(String docId) throws FileNotFoundException {
299 final File file = getFileForDocId(docId);
300 final File visibleFile = getFileForDocId(docId, true);
301
302 final boolean isDirectory = file.isDirectory();
303 if (isDirectory) {
304 FileUtils.deleteContents(file);
305 }
306 if (!file.delete()) {
307 throw new IllegalStateException("Failed to delete " + file);
308 }
309
310 removeFromMediaStore(visibleFile, isDirectory);
311 }
312
Garfield Tand21af532017-04-17 14:22:32 -0700313 private void removeFromMediaStore(@Nullable File visibleFile, boolean isFolder)
Garfield Tan9bd2f6c2017-03-10 15:38:15 -0800314 throws FileNotFoundException {
Garfield Tand21af532017-04-17 14:22:32 -0700315 // visibleFolder is null if we're removing a document from external thumb drive or SD card.
Garfield Tan75379db2017-02-08 15:32:56 -0800316 if (visibleFile != null) {
Garfield Tanfac8aea2017-06-27 11:03:42 -0700317 final long token = Binder.clearCallingIdentity();
Garfield Tan75379db2017-02-08 15:32:56 -0800318
Garfield Tanfac8aea2017-06-27 11:03:42 -0700319 try {
320 final ContentResolver resolver = getContext().getContentResolver();
321 final Uri externalUri = MediaStore.Files.getContentUri("external");
322
323 // Remove media store entries for any files inside this directory, using
324 // path prefix match. Logic borrowed from MtpDatabase.
325 if (isFolder) {
326 final String path = visibleFile.getAbsolutePath() + "/";
327 resolver.delete(externalUri,
328 "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
329 new String[]{path + "%", Integer.toString(path.length()), path});
330 }
331
332 // Remove media store entry for this exact file.
333 final String path = visibleFile.getAbsolutePath();
Garfield Tan75379db2017-02-08 15:32:56 -0800334 resolver.delete(externalUri,
Garfield Tanfac8aea2017-06-27 11:03:42 -0700335 "_data LIKE ?1 AND lower(_data)=lower(?2)",
336 new String[]{path, path});
337 } finally {
338 Binder.restoreCallingIdentity(token);
Garfield Tan75379db2017-02-08 15:32:56 -0800339 }
Garfield Tan75379db2017-02-08 15:32:56 -0800340 }
341 }
342
343 @Override
344 public Cursor queryDocument(String documentId, String[] projection)
345 throws FileNotFoundException {
346 final MatrixCursor result = new MatrixCursor(resolveProjection(projection));
347 includeFile(result, documentId, null);
348 return result;
349 }
350
351 @Override
352 public Cursor queryChildDocuments(
353 String parentDocumentId, String[] projection, String sortOrder)
354 throws FileNotFoundException {
355
356 final File parent = getFileForDocId(parentDocumentId);
357 final MatrixCursor result = new DirectoryCursor(
358 resolveProjection(projection), parentDocumentId, parent);
359 for (File file : parent.listFiles()) {
360 includeFile(result, null, file);
361 }
362 return result;
363 }
364
365 /**
366 * Searches documents under the given folder.
367 *
368 * To avoid runtime explosion only returns the at most 23 items.
369 *
370 * @param folder the root folder where recursive search begins
371 * @param query the search condition used to match file names
372 * @param projection projection of the returned cursor
373 * @param exclusion absolute file paths to exclude from result
374 * @return cursor containing search result
375 * @throws FileNotFoundException when root folder doesn't exist or search fails
376 */
377 protected final Cursor querySearchDocuments(
378 File folder, String query, String[] projection, Set<String> exclusion)
379 throws FileNotFoundException {
380
381 query = query.toLowerCase();
382 final MatrixCursor result = new MatrixCursor(resolveProjection(projection));
383 final LinkedList<File> pending = new LinkedList<>();
384 pending.add(folder);
385 while (!pending.isEmpty() && result.getCount() < 24) {
386 final File file = pending.removeFirst();
387 if (file.isDirectory()) {
388 for (File child : file.listFiles()) {
389 pending.add(child);
390 }
391 }
392 if (file.getName().toLowerCase().contains(query)
393 && !exclusion.contains(file.getAbsolutePath())) {
394 includeFile(result, null, file);
395 }
396 }
397 return result;
398 }
399
400 @Override
401 public String getDocumentType(String documentId) throws FileNotFoundException {
402 final File file = getFileForDocId(documentId);
403 return getTypeForFile(file);
404 }
405
406 @Override
407 public ParcelFileDescriptor openDocument(
408 String documentId, String mode, CancellationSignal signal)
409 throws FileNotFoundException {
410 final File file = getFileForDocId(documentId);
411 final File visibleFile = getFileForDocId(documentId, true);
412
413 final int pfdMode = ParcelFileDescriptor.parseMode(mode);
414 if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY || visibleFile == null) {
415 return ParcelFileDescriptor.open(file, pfdMode);
416 } else {
417 try {
418 // When finished writing, kick off media scanner
419 return ParcelFileDescriptor.open(
420 file, pfdMode, mHandler, (IOException e) -> scanFile(visibleFile));
421 } catch (IOException e) {
422 throw new FileNotFoundException("Failed to open for writing: " + e);
423 }
424 }
425 }
426
427 private void scanFile(File visibleFile) {
428 final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
429 intent.setData(Uri.fromFile(visibleFile));
430 getContext().sendBroadcast(intent);
431 }
432
433 @Override
434 public AssetFileDescriptor openDocumentThumbnail(
435 String documentId, Point sizeHint, CancellationSignal signal)
436 throws FileNotFoundException {
437 final File file = getFileForDocId(documentId);
438 return DocumentsContract.openImageThumbnail(file);
439 }
440
441 protected RowBuilder includeFile(MatrixCursor result, String docId, File file)
442 throws FileNotFoundException {
443 if (docId == null) {
444 docId = getDocIdForFile(file);
445 } else {
446 file = getFileForDocId(docId);
447 }
448
449 int flags = 0;
450
451 if (file.canWrite()) {
452 if (file.isDirectory()) {
453 flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
454 flags |= Document.FLAG_SUPPORTS_DELETE;
455 flags |= Document.FLAG_SUPPORTS_RENAME;
456 flags |= Document.FLAG_SUPPORTS_MOVE;
457 } else {
458 flags |= Document.FLAG_SUPPORTS_WRITE;
459 flags |= Document.FLAG_SUPPORTS_DELETE;
460 flags |= Document.FLAG_SUPPORTS_RENAME;
461 flags |= Document.FLAG_SUPPORTS_MOVE;
462 }
463 }
464
465 final String mimeType = getTypeForFile(file);
466 final String displayName = file.getName();
Julian Mancini6d091d62017-08-01 13:23:08 -0700467 if (mimeType.startsWith("image/")) {
Garfield Tan75379db2017-02-08 15:32:56 -0800468 flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
469 }
470
Steve McKay36f1d7e2017-07-20 11:41:58 -0700471 if (typeSupportsMetadata(mimeType)) {
472 flags |= Document.FLAG_SUPPORTS_METADATA;
473 }
474
Garfield Tan75379db2017-02-08 15:32:56 -0800475 final RowBuilder row = result.newRow();
476 row.add(Document.COLUMN_DOCUMENT_ID, docId);
477 row.add(Document.COLUMN_DISPLAY_NAME, displayName);
478 row.add(Document.COLUMN_SIZE, file.length());
479 row.add(Document.COLUMN_MIME_TYPE, mimeType);
480 row.add(Document.COLUMN_FLAGS, flags);
481
482 // Only publish dates reasonably after epoch
483 long lastModified = file.lastModified();
484 if (lastModified > 31536000000L) {
485 row.add(Document.COLUMN_LAST_MODIFIED, lastModified);
486 }
487
488 // Return the row builder just in case any subclass want to add more stuff to it.
489 return row;
490 }
491
492 private static String getTypeForFile(File file) {
493 if (file.isDirectory()) {
494 return Document.MIME_TYPE_DIR;
495 } else {
496 return getTypeForName(file.getName());
497 }
498 }
499
Steve McKay36f1d7e2017-07-20 11:41:58 -0700500 protected boolean typeSupportsMetadata(String mimeType) {
Steve McKay5a10ff12017-08-01 15:02:50 -0700501 return MetadataReader.isSupportedMimeType(mimeType);
Steve McKay36f1d7e2017-07-20 11:41:58 -0700502 }
503
Garfield Tan75379db2017-02-08 15:32:56 -0800504 private static String getTypeForName(String name) {
505 final int lastDot = name.lastIndexOf('.');
506 if (lastDot >= 0) {
507 final String extension = name.substring(lastDot + 1).toLowerCase();
508 final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
509 if (mime != null) {
510 return mime;
511 }
512 }
513
Steve McKay36f1d7e2017-07-20 11:41:58 -0700514 return MIMETYPE_OCTET_STREAM;
Garfield Tan75379db2017-02-08 15:32:56 -0800515 }
516
517 protected final File getFileForDocId(String docId) throws FileNotFoundException {
518 return getFileForDocId(docId, false);
519 }
520
521 private String[] resolveProjection(String[] projection) {
522 return projection == null ? mDefaultProjection : projection;
523 }
524
525 private void startObserving(File file, Uri notifyUri) {
526 synchronized (mObservers) {
527 DirectoryObserver observer = mObservers.get(file);
528 if (observer == null) {
529 observer = new DirectoryObserver(
530 file, getContext().getContentResolver(), notifyUri);
531 observer.startWatching();
532 mObservers.put(file, observer);
533 }
534 observer.mRefCount++;
535
536 if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer);
537 }
538 }
539
540 private void stopObserving(File file) {
541 synchronized (mObservers) {
542 DirectoryObserver observer = mObservers.get(file);
543 if (observer == null) return;
544
545 observer.mRefCount--;
546 if (observer.mRefCount == 0) {
547 mObservers.remove(file);
548 observer.stopWatching();
549 }
550
551 if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer);
552 }
553 }
554
555 private static class DirectoryObserver extends FileObserver {
556 private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO
557 | CREATE | DELETE | DELETE_SELF | MOVE_SELF;
558
559 private final File mFile;
560 private final ContentResolver mResolver;
561 private final Uri mNotifyUri;
562
563 private int mRefCount = 0;
564
565 public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) {
566 super(file.getAbsolutePath(), NOTIFY_EVENTS);
567 mFile = file;
568 mResolver = resolver;
569 mNotifyUri = notifyUri;
570 }
571
572 @Override
573 public void onEvent(int event, String path) {
574 if ((event & NOTIFY_EVENTS) != 0) {
575 if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path);
576 mResolver.notifyChange(mNotifyUri, null, false);
577 }
578 }
579
580 @Override
581 public String toString() {
582 return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}";
583 }
584 }
585
586 private class DirectoryCursor extends MatrixCursor {
587 private final File mFile;
588
589 public DirectoryCursor(String[] columnNames, String docId, File file) {
590 super(columnNames);
591
592 final Uri notifyUri = buildNotificationUri(docId);
593 setNotificationUri(getContext().getContentResolver(), notifyUri);
594
595 mFile = file;
596 startObserving(mFile, notifyUri);
597 }
598
599 @Override
600 public void close() {
601 super.close();
602 stopObserving(mFile);
603 }
604 }
605}