• 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.ContentValues;
23 import android.content.Intent;
24 import android.content.res.AssetFileDescriptor;
25 import android.database.Cursor;
26 import android.database.MatrixCursor;
27 import android.database.MatrixCursor.RowBuilder;
28 import android.graphics.Point;
29 import android.net.Uri;
30 import android.os.CancellationSignal;
31 import android.os.FileObserver;
32 import android.os.FileUtils;
33 import android.os.Handler;
34 import android.os.ParcelFileDescriptor;
35 import android.provider.DocumentsContract;
36 import android.provider.DocumentsContract.Document;
37 import android.provider.DocumentsProvider;
38 import android.provider.MediaStore;
39 import android.text.TextUtils;
40 import android.util.ArrayMap;
41 import android.util.Log;
42 import android.webkit.MimeTypeMap;
43 
44 import com.android.internal.annotations.GuardedBy;
45 
46 import java.io.File;
47 import java.io.FileNotFoundException;
48 import java.io.IOException;
49 import java.util.LinkedList;
50 import java.util.List;
51 import java.util.Set;
52 
53 /**
54  * A helper class for {@link android.provider.DocumentsProvider} to perform file operations on local
55  * files.
56  */
57 public abstract class FileSystemProvider extends DocumentsProvider {
58 
59     private static final String TAG = "FileSystemProvider";
60 
61     private static final boolean LOG_INOTIFY = false;
62 
63     private String[] mDefaultProjection;
64 
65     @GuardedBy("mObservers")
66     private final ArrayMap<File, DirectoryObserver> mObservers = new ArrayMap<>();
67 
68     private Handler mHandler;
69 
getFileForDocId(String docId, boolean visible)70     protected abstract File getFileForDocId(String docId, boolean visible)
71             throws FileNotFoundException;
72 
getDocIdForFile(File file)73     protected abstract String getDocIdForFile(File file) throws FileNotFoundException;
74 
buildNotificationUri(String docId)75     protected abstract Uri buildNotificationUri(String docId);
76 
77     @Override
onCreate()78     public boolean onCreate() {
79         throw new UnsupportedOperationException(
80                 "Subclass should override this and call onCreate(defaultDocumentProjection)");
81     }
82 
83     @CallSuper
onCreate(String[] defaultProjection)84     protected void onCreate(String[] defaultProjection) {
85         mHandler = new Handler();
86         mDefaultProjection = defaultProjection;
87     }
88 
89     @Override
isChildDocument(String parentDocId, String docId)90     public boolean isChildDocument(String parentDocId, String docId) {
91         try {
92             final File parent = getFileForDocId(parentDocId).getCanonicalFile();
93             final File doc = getFileForDocId(docId).getCanonicalFile();
94             return FileUtils.contains(parent, doc);
95         } catch (IOException e) {
96             throw new IllegalArgumentException(
97                     "Failed to determine if " + docId + " is child of " + parentDocId + ": " + e);
98         }
99     }
100 
findDocumentPath(File parent, File doc)101     protected final List<String> findDocumentPath(File parent, File doc)
102             throws FileNotFoundException {
103 
104         if (!doc.exists()) {
105             throw new FileNotFoundException(doc + " is not found.");
106         }
107 
108         if (!FileUtils.contains(parent, doc)) {
109             throw new FileNotFoundException(doc + " is not found under " + parent);
110         }
111 
112         LinkedList<String> path = new LinkedList<>();
113         while (doc != null && FileUtils.contains(parent, doc)) {
114             path.addFirst(getDocIdForFile(doc));
115 
116             doc = doc.getParentFile();
117         }
118 
119         return path;
120     }
121 
122     @Override
createDocument(String docId, String mimeType, String displayName)123     public String createDocument(String docId, String mimeType, String displayName)
124             throws FileNotFoundException {
125         displayName = FileUtils.buildValidFatFilename(displayName);
126 
127         final File parent = getFileForDocId(docId);
128         if (!parent.isDirectory()) {
129             throw new IllegalArgumentException("Parent document isn't a directory");
130         }
131 
132         final File file = FileUtils.buildUniqueFile(parent, mimeType, displayName);
133         final String childId;
134         if (Document.MIME_TYPE_DIR.equals(mimeType)) {
135             if (!file.mkdir()) {
136                 throw new IllegalStateException("Failed to mkdir " + file);
137             }
138             childId = getDocIdForFile(file);
139             addFolderToMediaStore(getFileForDocId(childId, true));
140         } else {
141             try {
142                 if (!file.createNewFile()) {
143                     throw new IllegalStateException("Failed to touch " + file);
144                 }
145                 childId = getDocIdForFile(file);
146             } catch (IOException e) {
147                 throw new IllegalStateException("Failed to touch " + file + ": " + e);
148             }
149         }
150 
151         return childId;
152     }
153 
addFolderToMediaStore(@ullable File visibleFolder)154     private void addFolderToMediaStore(@Nullable File visibleFolder) {
155         // visibleFolder is null if we're adding a folder to external thumb drive or SD card.
156         if (visibleFolder != null) {
157             assert (visibleFolder.isDirectory());
158 
159             final ContentResolver resolver = getContext().getContentResolver();
160             final Uri uri = MediaStore.Files.getDirectoryUri("external");
161             ContentValues values = new ContentValues();
162             values.put(MediaStore.Files.FileColumns.DATA, visibleFolder.getAbsolutePath());
163             resolver.insert(uri, values);
164         }
165     }
166 
167     @Override
renameDocument(String docId, String displayName)168     public String renameDocument(String docId, String displayName) throws FileNotFoundException {
169         // Since this provider treats renames as generating a completely new
170         // docId, we're okay with letting the MIME type change.
171         displayName = FileUtils.buildValidFatFilename(displayName);
172 
173         final File before = getFileForDocId(docId);
174         final File after = FileUtils.buildUniqueFile(before.getParentFile(), displayName);
175         final File visibleFileBefore = getFileForDocId(docId, true);
176         if (!before.renameTo(after)) {
177             throw new IllegalStateException("Failed to rename to " + after);
178         }
179 
180         final String afterDocId = getDocIdForFile(after);
181         moveInMediaStore(visibleFileBefore, getFileForDocId(afterDocId, true));
182 
183         if (!TextUtils.equals(docId, afterDocId)) {
184             return afterDocId;
185         } else {
186             return null;
187         }
188     }
189 
190     @Override
moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId)191     public String moveDocument(String sourceDocumentId, String sourceParentDocumentId,
192             String targetParentDocumentId)
193             throws FileNotFoundException {
194         final File before = getFileForDocId(sourceDocumentId);
195         final File after = new File(getFileForDocId(targetParentDocumentId), before.getName());
196         final File visibleFileBefore = getFileForDocId(sourceDocumentId, true);
197 
198         if (after.exists()) {
199             throw new IllegalStateException("Already exists " + after);
200         }
201         if (!before.renameTo(after)) {
202             throw new IllegalStateException("Failed to move to " + after);
203         }
204 
205         final String docId = getDocIdForFile(after);
206         moveInMediaStore(visibleFileBefore, getFileForDocId(docId, true));
207 
208         return docId;
209     }
210 
moveInMediaStore(@ullable File oldVisibleFile, @Nullable File newVisibleFile)211     private void moveInMediaStore(@Nullable File oldVisibleFile, @Nullable File newVisibleFile) {
212         // visibleFolders are null if we're moving a document in external thumb drive or SD card.
213         //
214         // They should be all null or not null at the same time. File#renameTo() doesn't work across
215         // volumes so an exception will be thrown before calling this method.
216         if (oldVisibleFile != null && newVisibleFile != null) {
217             final ContentResolver resolver = getContext().getContentResolver();
218             final Uri externalUri = newVisibleFile.isDirectory()
219                     ? MediaStore.Files.getDirectoryUri("external")
220                     : MediaStore.Files.getContentUri("external");
221 
222             ContentValues values = new ContentValues();
223             values.put(MediaStore.Files.FileColumns.DATA, newVisibleFile.getAbsolutePath());
224 
225             // Logic borrowed from MtpDatabase.
226             // note - we are relying on a special case in MediaProvider.update() to update
227             // the paths for all children in the case where this is a directory.
228             final String path = oldVisibleFile.getAbsolutePath();
229             resolver.update(externalUri,
230                     values,
231                     "_data LIKE ? AND lower(_data)=lower(?)",
232                     new String[] { path, path });
233         }
234     }
235 
236     @Override
deleteDocument(String docId)237     public void deleteDocument(String docId) throws FileNotFoundException {
238         final File file = getFileForDocId(docId);
239         final File visibleFile = getFileForDocId(docId, true);
240 
241         final boolean isDirectory = file.isDirectory();
242         if (isDirectory) {
243             FileUtils.deleteContents(file);
244         }
245         if (!file.delete()) {
246             throw new IllegalStateException("Failed to delete " + file);
247         }
248 
249         removeFromMediaStore(visibleFile, isDirectory);
250     }
251 
removeFromMediaStore(@ullable File visibleFile, boolean isFolder)252     private void removeFromMediaStore(@Nullable File visibleFile, boolean isFolder)
253             throws FileNotFoundException {
254         // visibleFolder is null if we're removing a document from external thumb drive or SD card.
255         if (visibleFile != null) {
256             final ContentResolver resolver = getContext().getContentResolver();
257             final Uri externalUri = MediaStore.Files.getContentUri("external");
258 
259             // Remove media store entries for any files inside this directory, using
260             // path prefix match. Logic borrowed from MtpDatabase.
261             if (isFolder) {
262                 final String path = visibleFile.getAbsolutePath() + "/";
263                 resolver.delete(externalUri,
264                         "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
265                         new String[] { path + "%", Integer.toString(path.length()), path });
266             }
267 
268             // Remove media store entry for this exact file.
269             final String path = visibleFile.getAbsolutePath();
270             resolver.delete(externalUri,
271                     "_data LIKE ?1 AND lower(_data)=lower(?2)",
272                     new String[] { path, path });
273         }
274     }
275 
276     @Override
queryDocument(String documentId, String[] projection)277     public Cursor queryDocument(String documentId, String[] projection)
278             throws FileNotFoundException {
279         final MatrixCursor result = new MatrixCursor(resolveProjection(projection));
280         includeFile(result, documentId, null);
281         return result;
282     }
283 
284     @Override
queryChildDocuments( String parentDocumentId, String[] projection, String sortOrder)285     public Cursor queryChildDocuments(
286             String parentDocumentId, String[] projection, String sortOrder)
287             throws FileNotFoundException {
288 
289         final File parent = getFileForDocId(parentDocumentId);
290         final MatrixCursor result = new DirectoryCursor(
291                 resolveProjection(projection), parentDocumentId, parent);
292         for (File file : parent.listFiles()) {
293             includeFile(result, null, file);
294         }
295         return result;
296     }
297 
298     /**
299      * Searches documents under the given folder.
300      *
301      * To avoid runtime explosion only returns the at most 23 items.
302      *
303      * @param folder the root folder where recursive search begins
304      * @param query the search condition used to match file names
305      * @param projection projection of the returned cursor
306      * @param exclusion absolute file paths to exclude from result
307      * @return cursor containing search result
308      * @throws FileNotFoundException when root folder doesn't exist or search fails
309      */
querySearchDocuments( File folder, String query, String[] projection, Set<String> exclusion)310     protected final Cursor querySearchDocuments(
311             File folder, String query, String[] projection, Set<String> exclusion)
312             throws FileNotFoundException {
313 
314         query = query.toLowerCase();
315         final MatrixCursor result = new MatrixCursor(resolveProjection(projection));
316         final LinkedList<File> pending = new LinkedList<>();
317         pending.add(folder);
318         while (!pending.isEmpty() && result.getCount() < 24) {
319             final File file = pending.removeFirst();
320             if (file.isDirectory()) {
321                 for (File child : file.listFiles()) {
322                     pending.add(child);
323                 }
324             }
325             if (file.getName().toLowerCase().contains(query)
326                     && !exclusion.contains(file.getAbsolutePath())) {
327                 includeFile(result, null, file);
328             }
329         }
330         return result;
331     }
332 
333     @Override
getDocumentType(String documentId)334     public String getDocumentType(String documentId) throws FileNotFoundException {
335         final File file = getFileForDocId(documentId);
336         return getTypeForFile(file);
337     }
338 
339     @Override
openDocument( String documentId, String mode, CancellationSignal signal)340     public ParcelFileDescriptor openDocument(
341             String documentId, String mode, CancellationSignal signal)
342             throws FileNotFoundException {
343         final File file = getFileForDocId(documentId);
344         final File visibleFile = getFileForDocId(documentId, true);
345 
346         final int pfdMode = ParcelFileDescriptor.parseMode(mode);
347         if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY || visibleFile == null) {
348             return ParcelFileDescriptor.open(file, pfdMode);
349         } else {
350             try {
351                 // When finished writing, kick off media scanner
352                 return ParcelFileDescriptor.open(
353                         file, pfdMode, mHandler, (IOException e) -> scanFile(visibleFile));
354             } catch (IOException e) {
355                 throw new FileNotFoundException("Failed to open for writing: " + e);
356             }
357         }
358     }
359 
scanFile(File visibleFile)360     private void scanFile(File visibleFile) {
361         final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
362         intent.setData(Uri.fromFile(visibleFile));
363         getContext().sendBroadcast(intent);
364     }
365 
366     @Override
openDocumentThumbnail( String documentId, Point sizeHint, CancellationSignal signal)367     public AssetFileDescriptor openDocumentThumbnail(
368             String documentId, Point sizeHint, CancellationSignal signal)
369             throws FileNotFoundException {
370         final File file = getFileForDocId(documentId);
371         return DocumentsContract.openImageThumbnail(file);
372     }
373 
includeFile(MatrixCursor result, String docId, File file)374     protected RowBuilder includeFile(MatrixCursor result, String docId, File file)
375             throws FileNotFoundException {
376         if (docId == null) {
377             docId = getDocIdForFile(file);
378         } else {
379             file = getFileForDocId(docId);
380         }
381 
382         int flags = 0;
383 
384         if (file.canWrite()) {
385             if (file.isDirectory()) {
386                 flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
387                 flags |= Document.FLAG_SUPPORTS_DELETE;
388                 flags |= Document.FLAG_SUPPORTS_RENAME;
389                 flags |= Document.FLAG_SUPPORTS_MOVE;
390             } else {
391                 flags |= Document.FLAG_SUPPORTS_WRITE;
392                 flags |= Document.FLAG_SUPPORTS_DELETE;
393                 flags |= Document.FLAG_SUPPORTS_RENAME;
394                 flags |= Document.FLAG_SUPPORTS_MOVE;
395             }
396         }
397 
398         final String mimeType = getTypeForFile(file);
399         final String displayName = file.getName();
400         if (mimeType.startsWith("image/")) {
401             flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
402         }
403 
404         final RowBuilder row = result.newRow();
405         row.add(Document.COLUMN_DOCUMENT_ID, docId);
406         row.add(Document.COLUMN_DISPLAY_NAME, displayName);
407         row.add(Document.COLUMN_SIZE, file.length());
408         row.add(Document.COLUMN_MIME_TYPE, mimeType);
409         row.add(Document.COLUMN_FLAGS, flags);
410 
411         // Only publish dates reasonably after epoch
412         long lastModified = file.lastModified();
413         if (lastModified > 31536000000L) {
414             row.add(Document.COLUMN_LAST_MODIFIED, lastModified);
415         }
416 
417         // Return the row builder just in case any subclass want to add more stuff to it.
418         return row;
419     }
420 
getTypeForFile(File file)421     private static String getTypeForFile(File file) {
422         if (file.isDirectory()) {
423             return Document.MIME_TYPE_DIR;
424         } else {
425             return getTypeForName(file.getName());
426         }
427     }
428 
getTypeForName(String name)429     private static String getTypeForName(String name) {
430         final int lastDot = name.lastIndexOf('.');
431         if (lastDot >= 0) {
432             final String extension = name.substring(lastDot + 1).toLowerCase();
433             final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
434             if (mime != null) {
435                 return mime;
436             }
437         }
438 
439         return "application/octet-stream";
440     }
441 
getFileForDocId(String docId)442     protected final File getFileForDocId(String docId) throws FileNotFoundException {
443         return getFileForDocId(docId, false);
444     }
445 
resolveProjection(String[] projection)446     private String[] resolveProjection(String[] projection) {
447         return projection == null ? mDefaultProjection : projection;
448     }
449 
startObserving(File file, Uri notifyUri)450     private void startObserving(File file, Uri notifyUri) {
451         synchronized (mObservers) {
452             DirectoryObserver observer = mObservers.get(file);
453             if (observer == null) {
454                 observer = new DirectoryObserver(
455                         file, getContext().getContentResolver(), notifyUri);
456                 observer.startWatching();
457                 mObservers.put(file, observer);
458             }
459             observer.mRefCount++;
460 
461             if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer);
462         }
463     }
464 
stopObserving(File file)465     private void stopObserving(File file) {
466         synchronized (mObservers) {
467             DirectoryObserver observer = mObservers.get(file);
468             if (observer == null) return;
469 
470             observer.mRefCount--;
471             if (observer.mRefCount == 0) {
472                 mObservers.remove(file);
473                 observer.stopWatching();
474             }
475 
476             if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer);
477         }
478     }
479 
480     private static class DirectoryObserver extends FileObserver {
481         private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO
482                 | CREATE | DELETE | DELETE_SELF | MOVE_SELF;
483 
484         private final File mFile;
485         private final ContentResolver mResolver;
486         private final Uri mNotifyUri;
487 
488         private int mRefCount = 0;
489 
DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri)490         public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) {
491             super(file.getAbsolutePath(), NOTIFY_EVENTS);
492             mFile = file;
493             mResolver = resolver;
494             mNotifyUri = notifyUri;
495         }
496 
497         @Override
onEvent(int event, String path)498         public void onEvent(int event, String path) {
499             if ((event & NOTIFY_EVENTS) != 0) {
500                 if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path);
501                 mResolver.notifyChange(mNotifyUri, null, false);
502             }
503         }
504 
505         @Override
toString()506         public String toString() {
507             return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}";
508         }
509     }
510 
511     private class DirectoryCursor extends MatrixCursor {
512         private final File mFile;
513 
DirectoryCursor(String[] columnNames, String docId, File file)514         public DirectoryCursor(String[] columnNames, String docId, File file) {
515             super(columnNames);
516 
517             final Uri notifyUri = buildNotificationUri(docId);
518             setNotificationUri(getContext().getContentResolver(), notifyUri);
519 
520             mFile = file;
521             startObserving(mFile, notifyUri);
522         }
523 
524         @Override
close()525         public void close() {
526             super.close();
527             stopObserving(mFile);
528         }
529     }
530 }
531