diff --git a/core/java/com/android/internal/content/FileSystemProvider.java b/core/java/com/android/internal/content/FileSystemProvider.java new file mode 100644 index 0000000000000..3ac5a72e087b8 --- /dev/null +++ b/core/java/com/android/internal/content/FileSystemProvider.java @@ -0,0 +1,487 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.content; + +import android.annotation.CallSuper; +import android.content.ContentResolver; +import android.content.Intent; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.database.MatrixCursor.RowBuilder; +import android.graphics.Point; +import android.net.Uri; +import android.os.CancellationSignal; +import android.os.FileObserver; +import android.os.FileUtils; +import android.os.Handler; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; +import android.provider.DocumentsContract.Document; +import android.provider.DocumentsProvider; +import android.provider.MediaStore; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import com.android.internal.annotations.GuardedBy; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * A helper class for {@link android.provider.DocumentsProvider} to perform file operations on local + * files. + */ +public abstract class FileSystemProvider extends DocumentsProvider { + + private static final String TAG = "FileSystemProvider"; + + private static final boolean LOG_INOTIFY = false; + + private String[] mDefaultProjection; + + @GuardedBy("mObservers") + private final ArrayMap mObservers = new ArrayMap<>(); + + private Handler mHandler; + + protected abstract File getFileForDocId(String docId, boolean visible) + throws FileNotFoundException; + + protected abstract String getDocIdForFile(File file) throws FileNotFoundException; + + protected abstract Uri buildNotificationUri(String docId); + + @Override + public boolean onCreate() { + throw new UnsupportedOperationException( + "Subclass should override this and call onCreate(defaultDocumentProjection)"); + } + + @CallSuper + protected void onCreate(String[] defaultProjection) { + mHandler = new Handler(); + mDefaultProjection = defaultProjection; + } + + @Override + public boolean isChildDocument(String parentDocId, String docId) { + try { + final File parent = getFileForDocId(parentDocId).getCanonicalFile(); + final File doc = getFileForDocId(docId).getCanonicalFile(); + return FileUtils.contains(parent, doc); + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to determine if " + docId + " is child of " + parentDocId + ": " + e); + } + } + + protected final List findDocumentPath(File parent, File doc) + throws FileNotFoundException { + + if (!doc.exists()) { + throw new FileNotFoundException(doc + " is not found."); + } + + if (!FileUtils.contains(parent, doc)) { + throw new FileNotFoundException(doc + " is not found under " + parent); + } + + LinkedList path = new LinkedList<>(); + while (doc != null && FileUtils.contains(parent, doc)) { + path.addFirst(getDocIdForFile(doc)); + + doc = doc.getParentFile(); + } + + return path; + } + + @Override + public String createDocument(String docId, String mimeType, String displayName) + throws FileNotFoundException { + displayName = FileUtils.buildValidFatFilename(displayName); + + final File parent = getFileForDocId(docId); + if (!parent.isDirectory()) { + throw new IllegalArgumentException("Parent document isn't a directory"); + } + + final File file = FileUtils.buildUniqueFile(parent, mimeType, displayName); + if (Document.MIME_TYPE_DIR.equals(mimeType)) { + if (!file.mkdir()) { + throw new IllegalStateException("Failed to mkdir " + file); + } + } else { + try { + if (!file.createNewFile()) { + throw new IllegalStateException("Failed to touch " + file); + } + } catch (IOException e) { + throw new IllegalStateException("Failed to touch " + file + ": " + e); + } + } + + return getDocIdForFile(file); + } + + @Override + public String renameDocument(String docId, String displayName) throws FileNotFoundException { + // Since this provider treats renames as generating a completely new + // docId, we're okay with letting the MIME type change. + displayName = FileUtils.buildValidFatFilename(displayName); + + final File before = getFileForDocId(docId); + final File after = FileUtils.buildUniqueFile(before.getParentFile(), displayName); + final File visibleFileBefore = getFileForDocId(docId, true); + if (!before.renameTo(after)) { + throw new IllegalStateException("Failed to rename to " + after); + } + removeFromMediaStore(visibleFileBefore); + + final String afterDocId = getDocIdForFile(after); + scanFile(getFileForDocId(afterDocId, true)); + + if (!TextUtils.equals(docId, afterDocId)) { + return afterDocId; + } else { + return null; + } + } + + @Override + public void deleteDocument(String docId) throws FileNotFoundException { + final File file = getFileForDocId(docId); + final File visibleFile = getFileForDocId(docId, true); + + final boolean isDirectory = file.isDirectory(); + if (isDirectory) { + FileUtils.deleteContents(file); + } + if (!file.delete()) { + throw new IllegalStateException("Failed to delete " + file); + } + + removeFromMediaStore(visibleFile); + } + + @Override + public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, + String targetParentDocumentId) + throws FileNotFoundException { + final File before = getFileForDocId(sourceDocumentId); + final File after = new File(getFileForDocId(targetParentDocumentId), before.getName()); + final File visibleFileBefore = getFileForDocId(sourceDocumentId, true); + + if (after.exists()) { + throw new IllegalStateException("Already exists " + after); + } + if (!before.renameTo(after)) { + throw new IllegalStateException("Failed to move to " + after); + } + + // Notify media store to update its content + removeFromMediaStore(visibleFileBefore); + final String docId = getDocIdForFile(after); + scanFile(getFileForDocId(docId, true)); + + return docId; + } + + private void removeFromMediaStore(File visibleFile) throws FileNotFoundException { + if (visibleFile != null) { + final ContentResolver resolver = getContext().getContentResolver(); + final Uri externalUri = MediaStore.Files.getContentUri("external"); + + // Remove media store entries for any files inside this directory, using + // path prefix match. Logic borrowed from MtpDatabase. + if (visibleFile.isDirectory()) { + final String path = visibleFile.getAbsolutePath() + "/"; + resolver.delete(externalUri, + "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)", + new String[] { path + "%", Integer.toString(path.length()), path }); + } + + // Remove media store entry for this exact file. + final String path = visibleFile.getAbsolutePath(); + resolver.delete(externalUri, + "_data LIKE ?1 AND lower(_data)=lower(?2)", + new String[] { path, path }); + } + } + + @Override + public Cursor queryDocument(String documentId, String[] projection) + throws FileNotFoundException { + final MatrixCursor result = new MatrixCursor(resolveProjection(projection)); + includeFile(result, documentId, null); + return result; + } + + @Override + public Cursor queryChildDocuments( + String parentDocumentId, String[] projection, String sortOrder) + throws FileNotFoundException { + + final File parent = getFileForDocId(parentDocumentId); + final MatrixCursor result = new DirectoryCursor( + resolveProjection(projection), parentDocumentId, parent); + for (File file : parent.listFiles()) { + includeFile(result, null, file); + } + return result; + } + + /** + * Searches documents under the given folder. + * + * To avoid runtime explosion only returns the at most 23 items. + * + * @param folder the root folder where recursive search begins + * @param query the search condition used to match file names + * @param projection projection of the returned cursor + * @param exclusion absolute file paths to exclude from result + * @return cursor containing search result + * @throws FileNotFoundException when root folder doesn't exist or search fails + */ + protected final Cursor querySearchDocuments( + File folder, String query, String[] projection, Set exclusion) + throws FileNotFoundException { + + query = query.toLowerCase(); + final MatrixCursor result = new MatrixCursor(resolveProjection(projection)); + final LinkedList pending = new LinkedList<>(); + pending.add(folder); + while (!pending.isEmpty() && result.getCount() < 24) { + final File file = pending.removeFirst(); + if (file.isDirectory()) { + for (File child : file.listFiles()) { + pending.add(child); + } + } + if (file.getName().toLowerCase().contains(query) + && !exclusion.contains(file.getAbsolutePath())) { + includeFile(result, null, file); + } + } + return result; + } + + @Override + public String getDocumentType(String documentId) throws FileNotFoundException { + final File file = getFileForDocId(documentId); + return getTypeForFile(file); + } + + @Override + public ParcelFileDescriptor openDocument( + String documentId, String mode, CancellationSignal signal) + throws FileNotFoundException { + final File file = getFileForDocId(documentId); + final File visibleFile = getFileForDocId(documentId, true); + + final int pfdMode = ParcelFileDescriptor.parseMode(mode); + if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY || visibleFile == null) { + return ParcelFileDescriptor.open(file, pfdMode); + } else { + try { + // When finished writing, kick off media scanner + return ParcelFileDescriptor.open( + file, pfdMode, mHandler, (IOException e) -> scanFile(visibleFile)); + } catch (IOException e) { + throw new FileNotFoundException("Failed to open for writing: " + e); + } + } + } + + private void scanFile(File visibleFile) { + final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(Uri.fromFile(visibleFile)); + getContext().sendBroadcast(intent); + } + + @Override + public AssetFileDescriptor openDocumentThumbnail( + String documentId, Point sizeHint, CancellationSignal signal) + throws FileNotFoundException { + final File file = getFileForDocId(documentId); + return DocumentsContract.openImageThumbnail(file); + } + + protected RowBuilder includeFile(MatrixCursor result, String docId, File file) + throws FileNotFoundException { + if (docId == null) { + docId = getDocIdForFile(file); + } else { + file = getFileForDocId(docId); + } + + int flags = 0; + + if (file.canWrite()) { + if (file.isDirectory()) { + flags |= Document.FLAG_DIR_SUPPORTS_CREATE; + flags |= Document.FLAG_SUPPORTS_DELETE; + flags |= Document.FLAG_SUPPORTS_RENAME; + flags |= Document.FLAG_SUPPORTS_MOVE; + } else { + flags |= Document.FLAG_SUPPORTS_WRITE; + flags |= Document.FLAG_SUPPORTS_DELETE; + flags |= Document.FLAG_SUPPORTS_RENAME; + flags |= Document.FLAG_SUPPORTS_MOVE; + } + } + + final String mimeType = getTypeForFile(file); + final String displayName = file.getName(); + if (mimeType.startsWith("image/")) { + flags |= Document.FLAG_SUPPORTS_THUMBNAIL; + } + + final RowBuilder row = result.newRow(); + row.add(Document.COLUMN_DOCUMENT_ID, docId); + row.add(Document.COLUMN_DISPLAY_NAME, displayName); + row.add(Document.COLUMN_SIZE, file.length()); + row.add(Document.COLUMN_MIME_TYPE, mimeType); + row.add(Document.COLUMN_FLAGS, flags); + + // Only publish dates reasonably after epoch + long lastModified = file.lastModified(); + if (lastModified > 31536000000L) { + row.add(Document.COLUMN_LAST_MODIFIED, lastModified); + } + + // Return the row builder just in case any subclass want to add more stuff to it. + return row; + } + + private static String getTypeForFile(File file) { + if (file.isDirectory()) { + return Document.MIME_TYPE_DIR; + } else { + return getTypeForName(file.getName()); + } + } + + private static String getTypeForName(String name) { + final int lastDot = name.lastIndexOf('.'); + if (lastDot >= 0) { + final String extension = name.substring(lastDot + 1).toLowerCase(); + final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + if (mime != null) { + return mime; + } + } + + return "application/octet-stream"; + } + + protected final File getFileForDocId(String docId) throws FileNotFoundException { + return getFileForDocId(docId, false); + } + + private String[] resolveProjection(String[] projection) { + return projection == null ? mDefaultProjection : projection; + } + + private void startObserving(File file, Uri notifyUri) { + synchronized (mObservers) { + DirectoryObserver observer = mObservers.get(file); + if (observer == null) { + observer = new DirectoryObserver( + file, getContext().getContentResolver(), notifyUri); + observer.startWatching(); + mObservers.put(file, observer); + } + observer.mRefCount++; + + if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer); + } + } + + private void stopObserving(File file) { + synchronized (mObservers) { + DirectoryObserver observer = mObservers.get(file); + if (observer == null) return; + + observer.mRefCount--; + if (observer.mRefCount == 0) { + mObservers.remove(file); + observer.stopWatching(); + } + + if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer); + } + } + + private static class DirectoryObserver extends FileObserver { + private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO + | CREATE | DELETE | DELETE_SELF | MOVE_SELF; + + private final File mFile; + private final ContentResolver mResolver; + private final Uri mNotifyUri; + + private int mRefCount = 0; + + public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) { + super(file.getAbsolutePath(), NOTIFY_EVENTS); + mFile = file; + mResolver = resolver; + mNotifyUri = notifyUri; + } + + @Override + public void onEvent(int event, String path) { + if ((event & NOTIFY_EVENTS) != 0) { + if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path); + mResolver.notifyChange(mNotifyUri, null, false); + } + } + + @Override + public String toString() { + return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}"; + } + } + + private class DirectoryCursor extends MatrixCursor { + private final File mFile; + + public DirectoryCursor(String[] columnNames, String docId, File file) { + super(columnNames); + + final Uri notifyUri = buildNotificationUri(docId); + setNotificationUri(getContext().getContentResolver(), notifyUri); + + mFile = file; + startObserving(mFile, notifyUri); + } + + @Override + public void close() { + super.close(); + stopObserving(mFile); + } + } +} diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java index 22a5b7ffd5206..3cc9f65ecd85c 100644 --- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java +++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java @@ -56,6 +56,7 @@ import android.util.Pair; import android.webkit.MimeTypeMap; import com.android.internal.annotations.GuardedBy; +import com.android.internal.content.FileSystemProvider; import com.android.internal.util.IndentingPrintWriter; import java.io.File; @@ -63,15 +64,15 @@ import java.io.FileDescriptor; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Objects; -public class ExternalStorageProvider extends DocumentsProvider { +public class ExternalStorageProvider extends FileSystemProvider { private static final String TAG = "ExternalStorage"; private static final boolean DEBUG = false; - private static final boolean LOG_INOTIFY = false; public static final String AUTHORITY = "com.android.externalstorage.documents"; @@ -105,20 +106,17 @@ public class ExternalStorageProvider extends DocumentsProvider { private static final String ROOT_ID_HOME = "home"; private StorageManager mStorageManager; - private Handler mHandler; private final Object mRootsLock = new Object(); @GuardedBy("mRootsLock") private ArrayMap mRoots = new ArrayMap<>(); - @GuardedBy("mObservers") - private ArrayMap mObservers = new ArrayMap<>(); - @Override public boolean onCreate() { + super.onCreate(DEFAULT_DOCUMENT_PROJECTION); + mStorageManager = (StorageManager) getContext().getSystemService(Context.STORAGE_SERVICE); - mHandler = new Handler(); updateVolumes(); return true; @@ -274,11 +272,8 @@ public class ExternalStorageProvider extends DocumentsProvider { return projection != null ? projection : DEFAULT_ROOT_PROJECTION; } - private static String[] resolveDocumentProjection(String[] projection) { - return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION; - } - - private String getDocIdForFile(File file) throws FileNotFoundException { + @Override + protected String getDocIdForFile(File file) throws FileNotFoundException { return getDocIdForFileMaybeCreate(file, false); } @@ -344,11 +339,8 @@ public class ExternalStorageProvider extends DocumentsProvider { return mostSpecificRoot; } - private File getFileForDocId(String docId) throws FileNotFoundException { - return getFileForDocId(docId, false); - } - - private File getFileForDocId(String docId, boolean visible) throws FileNotFoundException { + @Override + protected File getFileForDocId(String docId, boolean visible) throws FileNotFoundException { RootInfo root = getRootFromDocId(docId); return buildFile(root, docId, visible); } @@ -393,48 +385,9 @@ public class ExternalStorageProvider extends DocumentsProvider { return target; } - private void includeFile(MatrixCursor result, String docId, File file) - throws FileNotFoundException { - if (docId == null) { - docId = getDocIdForFile(file); - } else { - file = getFileForDocId(docId); - } - - int flags = 0; - - if (file.canWrite()) { - if (file.isDirectory()) { - flags |= Document.FLAG_DIR_SUPPORTS_CREATE; - flags |= Document.FLAG_SUPPORTS_DELETE; - flags |= Document.FLAG_SUPPORTS_RENAME; - flags |= Document.FLAG_SUPPORTS_MOVE; - } else { - flags |= Document.FLAG_SUPPORTS_WRITE; - flags |= Document.FLAG_SUPPORTS_DELETE; - flags |= Document.FLAG_SUPPORTS_RENAME; - flags |= Document.FLAG_SUPPORTS_MOVE; - } - } - - final String mimeType = getTypeForFile(file); - final String displayName = file.getName(); - if (mimeType.startsWith("image/")) { - flags |= Document.FLAG_SUPPORTS_THUMBNAIL; - } - - final RowBuilder row = result.newRow(); - row.add(Document.COLUMN_DOCUMENT_ID, docId); - row.add(Document.COLUMN_DISPLAY_NAME, displayName); - row.add(Document.COLUMN_SIZE, file.length()); - row.add(Document.COLUMN_MIME_TYPE, mimeType); - row.add(Document.COLUMN_FLAGS, flags); - - // Only publish dates reasonably after epoch - long lastModified = file.lastModified(); - if (lastModified > 31536000000L) { - row.add(Document.COLUMN_LAST_MODIFIED, lastModified); - } + @Override + protected Uri buildNotificationUri(String docId) { + return DocumentsContract.buildChildDocumentsUri(AUTHORITY, docId); } @Override @@ -454,23 +407,9 @@ public class ExternalStorageProvider extends DocumentsProvider { return result; } - @Override - public boolean isChildDocument(String parentDocId, String docId) { - try { - final File parent = getFileForDocId(parentDocId).getCanonicalFile(); - final File doc = getFileForDocId(docId).getCanonicalFile(); - return FileUtils.contains(parent, doc); - } catch (IOException e) { - throw new IllegalArgumentException( - "Failed to determine if " + docId + " is child of " + parentDocId + ": " + e); - } - } - @Override public Path findDocumentPath(String childDocId, @Nullable String parentDocId) throws FileNotFoundException { - LinkedList path = new LinkedList<>(); - final Pair resolvedDocId = resolveDocId(childDocId, false); final RootInfo root = resolvedDocId.first; File child = resolvedDocId.second; @@ -479,49 +418,7 @@ public class ExternalStorageProvider extends DocumentsProvider { ? root.path : getFileForDocId(parentDocId); - if (!child.exists()) { - throw new FileNotFoundException(childDocId + " is not found."); - } - - if (!child.getAbsolutePath().startsWith(parent.getAbsolutePath())) { - throw new FileNotFoundException(childDocId + " is not found under " + parentDocId); - } - - while (child != null && child.getAbsolutePath().startsWith(parent.getAbsolutePath())) { - path.addFirst(getDocIdForFile(child)); - - child = child.getParentFile(); - } - - return new Path(parentDocId == null ? root.rootId : null, path); - } - - @Override - public String createDocument(String docId, String mimeType, String displayName) - throws FileNotFoundException { - displayName = FileUtils.buildValidFatFilename(displayName); - - final File parent = getFileForDocId(docId); - if (!parent.isDirectory()) { - throw new IllegalArgumentException("Parent document isn't a directory"); - } - - final File file = FileUtils.buildUniqueFile(parent, mimeType, displayName); - if (Document.MIME_TYPE_DIR.equals(mimeType)) { - if (!file.mkdir()) { - throw new IllegalStateException("Failed to mkdir " + file); - } - } else { - try { - if (!file.createNewFile()) { - throw new IllegalStateException("Failed to touch " + file); - } - } catch (IOException e) { - throw new IllegalStateException("Failed to touch " + file + ": " + e); - } - } - - return getDocIdForFile(file); + return new Path(parentDocId == null ? root.rootId : null, findDocumentPath(parent, child)); } private Uri getDocumentUri(String path, List accessUriPermissions) @@ -586,121 +483,15 @@ public class ExternalStorageProvider extends DocumentsProvider { && permission.isWritePermission(); } - @Override - public String renameDocument(String docId, String displayName) throws FileNotFoundException { - // Since this provider treats renames as generating a completely new - // docId, we're okay with letting the MIME type change. - displayName = FileUtils.buildValidFatFilename(displayName); - - final File before = getFileForDocId(docId); - final File after = FileUtils.buildUniqueFile(before.getParentFile(), displayName); - if (!before.renameTo(after)) { - throw new IllegalStateException("Failed to rename to " + after); - } - final String afterDocId = getDocIdForFile(after); - if (!TextUtils.equals(docId, afterDocId)) { - return afterDocId; - } else { - return null; - } - } - - @Override - public void deleteDocument(String docId) throws FileNotFoundException { - final File file = getFileForDocId(docId); - final File visibleFile = getFileForDocId(docId, true); - - final boolean isDirectory = file.isDirectory(); - if (isDirectory) { - FileUtils.deleteContents(file); - } - if (!file.delete()) { - throw new IllegalStateException("Failed to delete " + file); - } - - if (visibleFile != null) { - final ContentResolver resolver = getContext().getContentResolver(); - final Uri externalUri = MediaStore.Files.getContentUri("external"); - - // Remove media store entries for any files inside this directory, using - // path prefix match. Logic borrowed from MtpDatabase. - if (isDirectory) { - final String path = visibleFile.getAbsolutePath() + "/"; - resolver.delete(externalUri, - "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)", - new String[] { path + "%", Integer.toString(path.length()), path }); - } - - // Remove media store entry for this exact file. - final String path = visibleFile.getAbsolutePath(); - resolver.delete(externalUri, - "_data LIKE ?1 AND lower(_data)=lower(?2)", - new String[] { path, path }); - } - } - - @Override - public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, - String targetParentDocumentId) - throws FileNotFoundException { - final File before = getFileForDocId(sourceDocumentId); - final File after = new File(getFileForDocId(targetParentDocumentId), before.getName()); - - if (after.exists()) { - throw new IllegalStateException("Already exists " + after); - } - if (!before.renameTo(after)) { - throw new IllegalStateException("Failed to move to " + after); - } - return getDocIdForFile(after); - } - - @Override - public Cursor queryDocument(String documentId, String[] projection) - throws FileNotFoundException { - final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); - includeFile(result, documentId, null); - return result; - } - - @Override - public Cursor queryChildDocuments( - String parentDocumentId, String[] projection, String sortOrder) - throws FileNotFoundException { - final File parent = getFileForDocId(parentDocumentId); - final MatrixCursor result = new DirectoryCursor( - resolveDocumentProjection(projection), parentDocumentId, parent); - for (File file : parent.listFiles()) { - includeFile(result, null, file); - } - return result; - } - @Override public Cursor querySearchDocuments(String rootId, String query, String[] projection) throws FileNotFoundException { - final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); - - query = query.toLowerCase(); final File parent; synchronized (mRootsLock) { parent = mRoots.get(rootId).path; } - final LinkedList pending = new LinkedList<>(); - pending.add(parent); - while (!pending.isEmpty() && result.getCount() < 24) { - final File file = pending.removeFirst(); - if (file.isDirectory()) { - for (File child : file.listFiles()) { - pending.add(child); - } - } - if (file.getName().toLowerCase().contains(query)) { - includeFile(result, null, file); - } - } - return result; + return querySearchDocuments(parent, query, projection, Collections.emptySet()); } @Override @@ -721,48 +512,6 @@ public class ExternalStorageProvider extends DocumentsProvider { return ejected; } - @Override - public String getDocumentType(String documentId) throws FileNotFoundException { - final File file = getFileForDocId(documentId); - return getTypeForFile(file); - } - - @Override - public ParcelFileDescriptor openDocument( - String documentId, String mode, CancellationSignal signal) - throws FileNotFoundException { - final File file = getFileForDocId(documentId); - final File visibleFile = getFileForDocId(documentId, true); - - final int pfdMode = ParcelFileDescriptor.parseMode(mode); - if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY || visibleFile == null) { - return ParcelFileDescriptor.open(file, pfdMode); - } else { - try { - // When finished writing, kick off media scanner - return ParcelFileDescriptor.open(file, pfdMode, mHandler, new OnCloseListener() { - @Override - public void onClose(IOException e) { - final Intent intent = new Intent( - Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); - intent.setData(Uri.fromFile(visibleFile)); - getContext().sendBroadcast(intent); - } - }); - } catch (IOException e) { - throw new FileNotFoundException("Failed to open for writing: " + e); - } - } - } - - @Override - public AssetFileDescriptor openDocumentThumbnail( - String documentId, Point sizeHint, CancellationSignal signal) - throws FileNotFoundException { - final File file = getFileForDocId(documentId); - return DocumentsContract.openImageThumbnail(file); - } - @Override public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 160); @@ -826,107 +575,4 @@ public class ExternalStorageProvider extends DocumentsProvider { } return bundle; } - - private static String getTypeForFile(File file) { - if (file.isDirectory()) { - return Document.MIME_TYPE_DIR; - } else { - return getTypeForName(file.getName()); - } - } - - private static String getTypeForName(String name) { - final int lastDot = name.lastIndexOf('.'); - if (lastDot >= 0) { - final String extension = name.substring(lastDot + 1).toLowerCase(); - final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - if (mime != null) { - return mime; - } - } - - return "application/octet-stream"; - } - - private void startObserving(File file, Uri notifyUri) { - synchronized (mObservers) { - DirectoryObserver observer = mObservers.get(file); - if (observer == null) { - observer = new DirectoryObserver( - file, getContext().getContentResolver(), notifyUri); - observer.startWatching(); - mObservers.put(file, observer); - } - observer.mRefCount++; - - if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer); - } - } - - private void stopObserving(File file) { - synchronized (mObservers) { - DirectoryObserver observer = mObservers.get(file); - if (observer == null) return; - - observer.mRefCount--; - if (observer.mRefCount == 0) { - mObservers.remove(file); - observer.stopWatching(); - } - - if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer); - } - } - - private static class DirectoryObserver extends FileObserver { - private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO - | CREATE | DELETE | DELETE_SELF | MOVE_SELF; - - private final File mFile; - private final ContentResolver mResolver; - private final Uri mNotifyUri; - - private int mRefCount = 0; - - public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) { - super(file.getAbsolutePath(), NOTIFY_EVENTS); - mFile = file; - mResolver = resolver; - mNotifyUri = notifyUri; - } - - @Override - public void onEvent(int event, String path) { - if ((event & NOTIFY_EVENTS) != 0) { - if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path); - mResolver.notifyChange(mNotifyUri, null, false); - } - } - - @Override - public String toString() { - return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}"; - } - } - - private class DirectoryCursor extends MatrixCursor { - private final File mFile; - - public DirectoryCursor(String[] columnNames, String docId, File file) { - super(columnNames); - - final Uri notifyUri = DocumentsContract.buildChildDocumentsUri( - AUTHORITY, docId); - setNotificationUri(getContext().getContentResolver(), notifyUri); - - mFile = file; - startObserving(mFile, notifyUri); - } - - @Override - public void close() { - super.close(); - stopObserving(mFile); - } - } }