blob: 17a598a00ca9b0bbf47753f4461b81a83db509b7 [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 Mancinid522fe62017-07-11 12:51:35 -070031import android.os.Build;
Julian Mancinib6505152017-06-27 13:29:09 -070032import android.os.Bundle;
Garfield Tan75379db2017-02-08 15:32:56 -080033import android.os.CancellationSignal;
34import android.os.FileObserver;
35import android.os.FileUtils;
36import android.os.Handler;
37import android.os.ParcelFileDescriptor;
38import android.provider.DocumentsContract;
39import android.provider.DocumentsContract.Document;
40import android.provider.DocumentsProvider;
41import android.provider.MediaStore;
Julian Mancinib6505152017-06-27 13:29:09 -070042import android.provider.MetadataReader;
Garfield Tan75379db2017-02-08 15:32:56 -080043import android.text.TextUtils;
44import android.util.ArrayMap;
45import android.util.Log;
46import android.webkit.MimeTypeMap;
47
48import com.android.internal.annotations.GuardedBy;
49
Julian Mancinib6505152017-06-27 13:29:09 -070050import libcore.io.IoUtils;
51
Garfield Tan75379db2017-02-08 15:32:56 -080052import java.io.File;
Julian Mancinib6505152017-06-27 13:29:09 -070053import java.io.FileInputStream;
Garfield Tan75379db2017-02-08 15:32:56 -080054import java.io.FileNotFoundException;
55import java.io.IOException;
56import 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 Mancinid522fe62017-07-11 12:51:35 -070077 private static final String MIMETYPE_PDF = "application/pdf";
78
Julian Mancinib6505152017-06-27 13:29:09 -070079 private static final String MIMETYPE_JPEG = "image/jpeg";
80
81 private static final String MIMETYPE_JPG = "image/jpg";
82
83
84
Garfield Tan75379db2017-02-08 15:32:56 -080085 protected abstract File getFileForDocId(String docId, boolean visible)
86 throws FileNotFoundException;
87
88 protected abstract String getDocIdForFile(File file) throws FileNotFoundException;
89
90 protected abstract Uri buildNotificationUri(String docId);
91
92 @Override
93 public boolean onCreate() {
94 throw new UnsupportedOperationException(
95 "Subclass should override this and call onCreate(defaultDocumentProjection)");
96 }
97
98 @CallSuper
99 protected void onCreate(String[] defaultProjection) {
100 mHandler = new Handler();
101 mDefaultProjection = defaultProjection;
102 }
103
104 @Override
105 public boolean isChildDocument(String parentDocId, String docId) {
106 try {
107 final File parent = getFileForDocId(parentDocId).getCanonicalFile();
108 final File doc = getFileForDocId(docId).getCanonicalFile();
109 return FileUtils.contains(parent, doc);
110 } catch (IOException e) {
111 throw new IllegalArgumentException(
112 "Failed to determine if " + docId + " is child of " + parentDocId + ": " + e);
113 }
114 }
115
Julian Mancinib6505152017-06-27 13:29:09 -0700116 @Override
117 public @Nullable Bundle getDocumentMetadata(String documentId, @Nullable String[] tags)
118 throws FileNotFoundException {
119 File file = getFileForDocId(documentId);
120 if (!(file.exists() && file.isFile() && file.canRead())) {
121 return Bundle.EMPTY;
122 }
123 String filePath = file.getAbsolutePath();
124 Bundle metadata = new Bundle();
125 if (getTypeForFile(file).equals(MIMETYPE_JPEG)
126 || getTypeForFile(file).equals(MIMETYPE_JPG)) {
127 FileInputStream stream = new FileInputStream(filePath);
128 try {
129 MetadataReader.getMetadata(metadata, stream, getTypeForFile(file), tags);
130 return metadata;
131 } catch (IOException e) {
132 Log.e(TAG, "An error occurred retrieving the metadata", e);
133 } finally {
134 IoUtils.closeQuietly(stream);
135 }
136 }
137 return null;
138 }
139
Garfield Tan75379db2017-02-08 15:32:56 -0800140 protected final List<String> findDocumentPath(File parent, File doc)
141 throws FileNotFoundException {
142
143 if (!doc.exists()) {
144 throw new FileNotFoundException(doc + " is not found.");
145 }
146
147 if (!FileUtils.contains(parent, doc)) {
148 throw new FileNotFoundException(doc + " is not found under " + parent);
149 }
150
151 LinkedList<String> path = new LinkedList<>();
152 while (doc != null && FileUtils.contains(parent, doc)) {
153 path.addFirst(getDocIdForFile(doc));
154
155 doc = doc.getParentFile();
156 }
157
158 return path;
159 }
160
161 @Override
162 public String createDocument(String docId, String mimeType, String displayName)
163 throws FileNotFoundException {
164 displayName = FileUtils.buildValidFatFilename(displayName);
165
166 final File parent = getFileForDocId(docId);
167 if (!parent.isDirectory()) {
168 throw new IllegalArgumentException("Parent document isn't a directory");
169 }
170
171 final File file = FileUtils.buildUniqueFile(parent, mimeType, displayName);
Garfield Tan93615412017-03-20 17:21:55 -0700172 final String childId;
Garfield Tan75379db2017-02-08 15:32:56 -0800173 if (Document.MIME_TYPE_DIR.equals(mimeType)) {
174 if (!file.mkdir()) {
175 throw new IllegalStateException("Failed to mkdir " + file);
176 }
Garfield Tan93615412017-03-20 17:21:55 -0700177 childId = getDocIdForFile(file);
178 addFolderToMediaStore(getFileForDocId(childId, true));
Garfield Tan75379db2017-02-08 15:32:56 -0800179 } else {
180 try {
181 if (!file.createNewFile()) {
182 throw new IllegalStateException("Failed to touch " + file);
183 }
Garfield Tan93615412017-03-20 17:21:55 -0700184 childId = getDocIdForFile(file);
Garfield Tan75379db2017-02-08 15:32:56 -0800185 } catch (IOException e) {
186 throw new IllegalStateException("Failed to touch " + file + ": " + e);
187 }
188 }
189
Garfield Tan93615412017-03-20 17:21:55 -0700190 return childId;
191 }
192
Garfield Tand21af532017-04-17 14:22:32 -0700193 private void addFolderToMediaStore(@Nullable File visibleFolder) {
194 // visibleFolder is null if we're adding a folder to external thumb drive or SD card.
195 if (visibleFolder != null) {
196 assert (visibleFolder.isDirectory());
Garfield Tan93615412017-03-20 17:21:55 -0700197
Garfield Tanfac8aea2017-06-27 11:03:42 -0700198 final long token = Binder.clearCallingIdentity();
199
200 try {
201 final ContentResolver resolver = getContext().getContentResolver();
202 final Uri uri = MediaStore.Files.getDirectoryUri("external");
203 ContentValues values = new ContentValues();
204 values.put(MediaStore.Files.FileColumns.DATA, visibleFolder.getAbsolutePath());
205 resolver.insert(uri, values);
206 } finally {
207 Binder.restoreCallingIdentity(token);
208 }
Garfield Tand21af532017-04-17 14:22:32 -0700209 }
Garfield Tan75379db2017-02-08 15:32:56 -0800210 }
211
212 @Override
213 public String renameDocument(String docId, String displayName) throws FileNotFoundException {
214 // Since this provider treats renames as generating a completely new
215 // docId, we're okay with letting the MIME type change.
216 displayName = FileUtils.buildValidFatFilename(displayName);
217
218 final File before = getFileForDocId(docId);
219 final File after = FileUtils.buildUniqueFile(before.getParentFile(), displayName);
220 final File visibleFileBefore = getFileForDocId(docId, true);
221 if (!before.renameTo(after)) {
222 throw new IllegalStateException("Failed to rename to " + after);
223 }
Garfield Tan75379db2017-02-08 15:32:56 -0800224
225 final String afterDocId = getDocIdForFile(after);
Garfield Tan9bd2f6c2017-03-10 15:38:15 -0800226 moveInMediaStore(visibleFileBefore, getFileForDocId(afterDocId, true));
Garfield Tan75379db2017-02-08 15:32:56 -0800227
228 if (!TextUtils.equals(docId, afterDocId)) {
229 return afterDocId;
230 } else {
231 return null;
232 }
233 }
234
235 @Override
Garfield Tan75379db2017-02-08 15:32:56 -0800236 public String moveDocument(String sourceDocumentId, String sourceParentDocumentId,
237 String targetParentDocumentId)
238 throws FileNotFoundException {
239 final File before = getFileForDocId(sourceDocumentId);
240 final File after = new File(getFileForDocId(targetParentDocumentId), before.getName());
241 final File visibleFileBefore = getFileForDocId(sourceDocumentId, true);
242
243 if (after.exists()) {
244 throw new IllegalStateException("Already exists " + after);
245 }
246 if (!before.renameTo(after)) {
247 throw new IllegalStateException("Failed to move to " + after);
248 }
249
Garfield Tan75379db2017-02-08 15:32:56 -0800250 final String docId = getDocIdForFile(after);
Garfield Tan9bd2f6c2017-03-10 15:38:15 -0800251 moveInMediaStore(visibleFileBefore, getFileForDocId(docId, true));
Garfield Tan75379db2017-02-08 15:32:56 -0800252
253 return docId;
254 }
255
Garfield Tand21af532017-04-17 14:22:32 -0700256 private void moveInMediaStore(@Nullable File oldVisibleFile, @Nullable File newVisibleFile) {
257 // visibleFolders are null if we're moving a document in external thumb drive or SD card.
258 //
259 // They should be all null or not null at the same time. File#renameTo() doesn't work across
260 // volumes so an exception will be thrown before calling this method.
261 if (oldVisibleFile != null && newVisibleFile != null) {
Garfield Tanfac8aea2017-06-27 11:03:42 -0700262 final long token = Binder.clearCallingIdentity();
Garfield Tan9bd2f6c2017-03-10 15:38:15 -0800263
Garfield Tanfac8aea2017-06-27 11:03:42 -0700264 try {
265 final ContentResolver resolver = getContext().getContentResolver();
266 final Uri externalUri = newVisibleFile.isDirectory()
267 ? MediaStore.Files.getDirectoryUri("external")
268 : MediaStore.Files.getContentUri("external");
Garfield Tan9bd2f6c2017-03-10 15:38:15 -0800269
Garfield Tanfac8aea2017-06-27 11:03:42 -0700270 ContentValues values = new ContentValues();
271 values.put(MediaStore.Files.FileColumns.DATA, newVisibleFile.getAbsolutePath());
272
273 // Logic borrowed from MtpDatabase.
274 // note - we are relying on a special case in MediaProvider.update() to update
275 // the paths for all children in the case where this is a directory.
276 final String path = oldVisibleFile.getAbsolutePath();
277 resolver.update(externalUri,
278 values,
279 "_data LIKE ? AND lower(_data)=lower(?)",
280 new String[]{path, path});
281 } finally {
282 Binder.restoreCallingIdentity(token);
283 }
Garfield Tan9bd2f6c2017-03-10 15:38:15 -0800284 }
285 }
286
287 @Override
288 public void deleteDocument(String docId) throws FileNotFoundException {
289 final File file = getFileForDocId(docId);
290 final File visibleFile = getFileForDocId(docId, true);
291
292 final boolean isDirectory = file.isDirectory();
293 if (isDirectory) {
294 FileUtils.deleteContents(file);
295 }
296 if (!file.delete()) {
297 throw new IllegalStateException("Failed to delete " + file);
298 }
299
300 removeFromMediaStore(visibleFile, isDirectory);
301 }
302
Garfield Tand21af532017-04-17 14:22:32 -0700303 private void removeFromMediaStore(@Nullable File visibleFile, boolean isFolder)
Garfield Tan9bd2f6c2017-03-10 15:38:15 -0800304 throws FileNotFoundException {
Garfield Tand21af532017-04-17 14:22:32 -0700305 // visibleFolder is null if we're removing a document from external thumb drive or SD card.
Garfield Tan75379db2017-02-08 15:32:56 -0800306 if (visibleFile != null) {
Garfield Tanfac8aea2017-06-27 11:03:42 -0700307 final long token = Binder.clearCallingIdentity();
Garfield Tan75379db2017-02-08 15:32:56 -0800308
Garfield Tanfac8aea2017-06-27 11:03:42 -0700309 try {
310 final ContentResolver resolver = getContext().getContentResolver();
311 final Uri externalUri = MediaStore.Files.getContentUri("external");
312
313 // Remove media store entries for any files inside this directory, using
314 // path prefix match. Logic borrowed from MtpDatabase.
315 if (isFolder) {
316 final String path = visibleFile.getAbsolutePath() + "/";
317 resolver.delete(externalUri,
318 "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
319 new String[]{path + "%", Integer.toString(path.length()), path});
320 }
321
322 // Remove media store entry for this exact file.
323 final String path = visibleFile.getAbsolutePath();
Garfield Tan75379db2017-02-08 15:32:56 -0800324 resolver.delete(externalUri,
Garfield Tanfac8aea2017-06-27 11:03:42 -0700325 "_data LIKE ?1 AND lower(_data)=lower(?2)",
326 new String[]{path, path});
327 } finally {
328 Binder.restoreCallingIdentity(token);
Garfield Tan75379db2017-02-08 15:32:56 -0800329 }
Garfield Tan75379db2017-02-08 15:32:56 -0800330 }
331 }
332
333 @Override
334 public Cursor queryDocument(String documentId, String[] projection)
335 throws FileNotFoundException {
336 final MatrixCursor result = new MatrixCursor(resolveProjection(projection));
337 includeFile(result, documentId, null);
338 return result;
339 }
340
341 @Override
342 public Cursor queryChildDocuments(
343 String parentDocumentId, String[] projection, String sortOrder)
344 throws FileNotFoundException {
345
346 final File parent = getFileForDocId(parentDocumentId);
347 final MatrixCursor result = new DirectoryCursor(
348 resolveProjection(projection), parentDocumentId, parent);
349 for (File file : parent.listFiles()) {
350 includeFile(result, null, file);
351 }
352 return result;
353 }
354
355 /**
356 * Searches documents under the given folder.
357 *
358 * To avoid runtime explosion only returns the at most 23 items.
359 *
360 * @param folder the root folder where recursive search begins
361 * @param query the search condition used to match file names
362 * @param projection projection of the returned cursor
363 * @param exclusion absolute file paths to exclude from result
364 * @return cursor containing search result
365 * @throws FileNotFoundException when root folder doesn't exist or search fails
366 */
367 protected final Cursor querySearchDocuments(
368 File folder, String query, String[] projection, Set<String> exclusion)
369 throws FileNotFoundException {
370
371 query = query.toLowerCase();
372 final MatrixCursor result = new MatrixCursor(resolveProjection(projection));
373 final LinkedList<File> pending = new LinkedList<>();
374 pending.add(folder);
375 while (!pending.isEmpty() && result.getCount() < 24) {
376 final File file = pending.removeFirst();
377 if (file.isDirectory()) {
378 for (File child : file.listFiles()) {
379 pending.add(child);
380 }
381 }
382 if (file.getName().toLowerCase().contains(query)
383 && !exclusion.contains(file.getAbsolutePath())) {
384 includeFile(result, null, file);
385 }
386 }
387 return result;
388 }
389
390 @Override
391 public String getDocumentType(String documentId) throws FileNotFoundException {
392 final File file = getFileForDocId(documentId);
393 return getTypeForFile(file);
394 }
395
396 @Override
397 public ParcelFileDescriptor openDocument(
398 String documentId, String mode, CancellationSignal signal)
399 throws FileNotFoundException {
400 final File file = getFileForDocId(documentId);
401 final File visibleFile = getFileForDocId(documentId, true);
402
403 final int pfdMode = ParcelFileDescriptor.parseMode(mode);
404 if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY || visibleFile == null) {
405 return ParcelFileDescriptor.open(file, pfdMode);
406 } else {
407 try {
408 // When finished writing, kick off media scanner
409 return ParcelFileDescriptor.open(
410 file, pfdMode, mHandler, (IOException e) -> scanFile(visibleFile));
411 } catch (IOException e) {
412 throw new FileNotFoundException("Failed to open for writing: " + e);
413 }
414 }
415 }
416
417 private void scanFile(File visibleFile) {
418 final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
419 intent.setData(Uri.fromFile(visibleFile));
420 getContext().sendBroadcast(intent);
421 }
422
423 @Override
424 public AssetFileDescriptor openDocumentThumbnail(
425 String documentId, Point sizeHint, CancellationSignal signal)
426 throws FileNotFoundException {
427 final File file = getFileForDocId(documentId);
Julian Mancinid522fe62017-07-11 12:51:35 -0700428 if (getTypeForFile(file).equals(MIMETYPE_PDF)) {
429 try {
430 return PdfUtils.openPdfThumbnail(file, sizeHint);
431 } catch (Exception e) {
432 Log.v(TAG, "Could not load PDF's thumbnail", e);
433 }
434 }
Garfield Tan75379db2017-02-08 15:32:56 -0800435 return DocumentsContract.openImageThumbnail(file);
436 }
437
438 protected RowBuilder includeFile(MatrixCursor result, String docId, File file)
439 throws FileNotFoundException {
440 if (docId == null) {
441 docId = getDocIdForFile(file);
442 } else {
443 file = getFileForDocId(docId);
444 }
445
446 int flags = 0;
447
448 if (file.canWrite()) {
449 if (file.isDirectory()) {
450 flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
451 flags |= Document.FLAG_SUPPORTS_DELETE;
452 flags |= Document.FLAG_SUPPORTS_RENAME;
453 flags |= Document.FLAG_SUPPORTS_MOVE;
454 } else {
455 flags |= Document.FLAG_SUPPORTS_WRITE;
456 flags |= Document.FLAG_SUPPORTS_DELETE;
457 flags |= Document.FLAG_SUPPORTS_RENAME;
458 flags |= Document.FLAG_SUPPORTS_MOVE;
459 }
460 }
461
462 final String mimeType = getTypeForFile(file);
463 final String displayName = file.getName();
Julian Mancinid522fe62017-07-11 12:51:35 -0700464 // As of right now, we aren't sure on the performance affect of loading all PDF Thumbnails
465 // Until a solution is found, it will be behind a debuggable flag.
466 if (mimeType.startsWith("image/")
467 || (mimeType.equals(MIMETYPE_PDF) && Build.IS_DEBUGGABLE)) {
Garfield Tan75379db2017-02-08 15:32:56 -0800468 flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
469 }
470
471 final RowBuilder row = result.newRow();
472 row.add(Document.COLUMN_DOCUMENT_ID, docId);
473 row.add(Document.COLUMN_DISPLAY_NAME, displayName);
474 row.add(Document.COLUMN_SIZE, file.length());
475 row.add(Document.COLUMN_MIME_TYPE, mimeType);
476 row.add(Document.COLUMN_FLAGS, flags);
477
478 // Only publish dates reasonably after epoch
479 long lastModified = file.lastModified();
480 if (lastModified > 31536000000L) {
481 row.add(Document.COLUMN_LAST_MODIFIED, lastModified);
482 }
483
484 // Return the row builder just in case any subclass want to add more stuff to it.
485 return row;
486 }
487
488 private static String getTypeForFile(File file) {
489 if (file.isDirectory()) {
490 return Document.MIME_TYPE_DIR;
491 } else {
492 return getTypeForName(file.getName());
493 }
494 }
495
496 private static String getTypeForName(String name) {
497 final int lastDot = name.lastIndexOf('.');
498 if (lastDot >= 0) {
499 final String extension = name.substring(lastDot + 1).toLowerCase();
500 final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
501 if (mime != null) {
502 return mime;
503 }
504 }
505
506 return "application/octet-stream";
507 }
508
509 protected final File getFileForDocId(String docId) throws FileNotFoundException {
510 return getFileForDocId(docId, false);
511 }
512
513 private String[] resolveProjection(String[] projection) {
514 return projection == null ? mDefaultProjection : projection;
515 }
516
517 private void startObserving(File file, Uri notifyUri) {
518 synchronized (mObservers) {
519 DirectoryObserver observer = mObservers.get(file);
520 if (observer == null) {
521 observer = new DirectoryObserver(
522 file, getContext().getContentResolver(), notifyUri);
523 observer.startWatching();
524 mObservers.put(file, observer);
525 }
526 observer.mRefCount++;
527
528 if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer);
529 }
530 }
531
532 private void stopObserving(File file) {
533 synchronized (mObservers) {
534 DirectoryObserver observer = mObservers.get(file);
535 if (observer == null) return;
536
537 observer.mRefCount--;
538 if (observer.mRefCount == 0) {
539 mObservers.remove(file);
540 observer.stopWatching();
541 }
542
543 if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer);
544 }
545 }
546
547 private static class DirectoryObserver extends FileObserver {
548 private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO
549 | CREATE | DELETE | DELETE_SELF | MOVE_SELF;
550
551 private final File mFile;
552 private final ContentResolver mResolver;
553 private final Uri mNotifyUri;
554
555 private int mRefCount = 0;
556
557 public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) {
558 super(file.getAbsolutePath(), NOTIFY_EVENTS);
559 mFile = file;
560 mResolver = resolver;
561 mNotifyUri = notifyUri;
562 }
563
564 @Override
565 public void onEvent(int event, String path) {
566 if ((event & NOTIFY_EVENTS) != 0) {
567 if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path);
568 mResolver.notifyChange(mNotifyUri, null, false);
569 }
570 }
571
572 @Override
573 public String toString() {
574 return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}";
575 }
576 }
577
578 private class DirectoryCursor extends MatrixCursor {
579 private final File mFile;
580
581 public DirectoryCursor(String[] columnNames, String docId, File file) {
582 super(columnNames);
583
584 final Uri notifyUri = buildNotificationUri(docId);
585 setNotificationUri(getContext().getContentResolver(), notifyUri);
586
587 mFile = file;
588 startObserving(mFile, notifyUri);
589 }
590
591 @Override
592 public void close() {
593 super.close();
594 stopObserving(mFile);
595 }
596 }
597}