175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan/* 275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * Copyright (C) 2017 The Android Open Source Project 375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * 475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * Licensed under the Apache License, Version 2.0 (the "License"); 575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * you may not use this file except in compliance with the License. 675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * You may obtain a copy of the License at 775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * 875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * http://www.apache.org/licenses/LICENSE-2.0 975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * 1075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * Unless required by applicable law or agreed to in writing, software 1175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * distributed under the License is distributed on an "AS IS" BASIS, 1275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * See the License for the specific language governing permissions and 1475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * limitations under the License. 1575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan */ 1675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 1775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanpackage com.android.internal.content; 1875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 1975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.annotation.CallSuper; 20d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tanimport android.annotation.Nullable; 2175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.content.ContentResolver; 229bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tanimport android.content.ContentValues; 2375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.content.Intent; 2475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.content.res.AssetFileDescriptor; 2575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.database.Cursor; 2675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.database.MatrixCursor; 2775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.database.MatrixCursor.RowBuilder; 2875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.graphics.Point; 2975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.net.Uri; 3075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.os.CancellationSignal; 3175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.os.FileObserver; 3275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.os.FileUtils; 3375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.os.Handler; 3475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.os.ParcelFileDescriptor; 3575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.provider.DocumentsContract; 3675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.provider.DocumentsContract.Document; 3775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.provider.DocumentsProvider; 3875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.provider.MediaStore; 3975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.text.TextUtils; 4075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.util.ArrayMap; 4175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.util.Log; 4275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport android.webkit.MimeTypeMap; 4375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 4475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport com.android.internal.annotations.GuardedBy; 4575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 4675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport java.io.File; 4775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport java.io.FileNotFoundException; 4875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport java.io.IOException; 4975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport java.util.LinkedList; 5075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport java.util.List; 5175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanimport java.util.Set; 5275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 5375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan/** 5475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * A helper class for {@link android.provider.DocumentsProvider} to perform file operations on local 5575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * files. 5675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan */ 5775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tanpublic abstract class FileSystemProvider extends DocumentsProvider { 5875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 5975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private static final String TAG = "FileSystemProvider"; 6075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 6175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private static final boolean LOG_INOTIFY = false; 6275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 6375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private String[] mDefaultProjection; 6475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 6575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan @GuardedBy("mObservers") 6675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private final ArrayMap<File, DirectoryObserver> mObservers = new ArrayMap<>(); 6775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 6875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private Handler mHandler; 6975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 7075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan protected abstract File getFileForDocId(String docId, boolean visible) 7175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throws FileNotFoundException; 7275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 7375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan protected abstract String getDocIdForFile(File file) throws FileNotFoundException; 7475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 7575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan protected abstract Uri buildNotificationUri(String docId); 7675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 7775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan @Override 7875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan public boolean onCreate() { 7975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throw new UnsupportedOperationException( 8075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan "Subclass should override this and call onCreate(defaultDocumentProjection)"); 8175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 8275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 8375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan @CallSuper 8475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan protected void onCreate(String[] defaultProjection) { 8575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan mHandler = new Handler(); 8675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan mDefaultProjection = defaultProjection; 8775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 8875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 8975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan @Override 9075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan public boolean isChildDocument(String parentDocId, String docId) { 9175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan try { 9275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final File parent = getFileForDocId(parentDocId).getCanonicalFile(); 9375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final File doc = getFileForDocId(docId).getCanonicalFile(); 9475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return FileUtils.contains(parent, doc); 9575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } catch (IOException e) { 9675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throw new IllegalArgumentException( 9775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan "Failed to determine if " + docId + " is child of " + parentDocId + ": " + e); 9875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 9975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 10075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 10175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan protected final List<String> findDocumentPath(File parent, File doc) 10275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throws FileNotFoundException { 10375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 10475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (!doc.exists()) { 10575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throw new FileNotFoundException(doc + " is not found."); 10675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 10775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 10875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (!FileUtils.contains(parent, doc)) { 10975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throw new FileNotFoundException(doc + " is not found under " + parent); 11075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 11175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 11275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan LinkedList<String> path = new LinkedList<>(); 11375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan while (doc != null && FileUtils.contains(parent, doc)) { 11475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan path.addFirst(getDocIdForFile(doc)); 11575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 11675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan doc = doc.getParentFile(); 11775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 11875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 11975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return path; 12075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 12175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 12275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan @Override 12375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan public String createDocument(String docId, String mimeType, String displayName) 12475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throws FileNotFoundException { 12575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan displayName = FileUtils.buildValidFatFilename(displayName); 12675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 12775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final File parent = getFileForDocId(docId); 12875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (!parent.isDirectory()) { 12975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throw new IllegalArgumentException("Parent document isn't a directory"); 13075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 13175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 13275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final File file = FileUtils.buildUniqueFile(parent, mimeType, displayName); 13393615419983fba9b2221b4eb02598d791fc44a04Garfield Tan final String childId; 13475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (Document.MIME_TYPE_DIR.equals(mimeType)) { 13575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (!file.mkdir()) { 13675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throw new IllegalStateException("Failed to mkdir " + file); 13775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 13893615419983fba9b2221b4eb02598d791fc44a04Garfield Tan childId = getDocIdForFile(file); 13993615419983fba9b2221b4eb02598d791fc44a04Garfield Tan addFolderToMediaStore(getFileForDocId(childId, true)); 14075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } else { 14175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan try { 14275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (!file.createNewFile()) { 14375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throw new IllegalStateException("Failed to touch " + file); 14475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 14593615419983fba9b2221b4eb02598d791fc44a04Garfield Tan childId = getDocIdForFile(file); 14675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } catch (IOException e) { 14775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throw new IllegalStateException("Failed to touch " + file + ": " + e); 14875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 14975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 15075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 15193615419983fba9b2221b4eb02598d791fc44a04Garfield Tan return childId; 15293615419983fba9b2221b4eb02598d791fc44a04Garfield Tan } 15393615419983fba9b2221b4eb02598d791fc44a04Garfield Tan 154d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tan private void addFolderToMediaStore(@Nullable File visibleFolder) { 155d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tan // visibleFolder is null if we're adding a folder to external thumb drive or SD card. 156d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tan if (visibleFolder != null) { 157d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tan assert (visibleFolder.isDirectory()); 15893615419983fba9b2221b4eb02598d791fc44a04Garfield Tan 159d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tan final ContentResolver resolver = getContext().getContentResolver(); 160d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tan final Uri uri = MediaStore.Files.getDirectoryUri("external"); 161d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tan ContentValues values = new ContentValues(); 162d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tan values.put(MediaStore.Files.FileColumns.DATA, visibleFolder.getAbsolutePath()); 163d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tan resolver.insert(uri, values); 164d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tan } 16575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 16675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 16775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan @Override 16875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan public String renameDocument(String docId, String displayName) throws FileNotFoundException { 16975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan // Since this provider treats renames as generating a completely new 17075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan // docId, we're okay with letting the MIME type change. 17175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan displayName = FileUtils.buildValidFatFilename(displayName); 17275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 17375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final File before = getFileForDocId(docId); 17475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final File after = FileUtils.buildUniqueFile(before.getParentFile(), displayName); 17575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final File visibleFileBefore = getFileForDocId(docId, true); 17675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (!before.renameTo(after)) { 17775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throw new IllegalStateException("Failed to rename to " + after); 17875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 17975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 18075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final String afterDocId = getDocIdForFile(after); 1819bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan moveInMediaStore(visibleFileBefore, getFileForDocId(afterDocId, true)); 18275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 18375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (!TextUtils.equals(docId, afterDocId)) { 18475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return afterDocId; 18575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } else { 18675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return null; 18775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 18875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 18975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 19075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan @Override 19175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, 19275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan String targetParentDocumentId) 19375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throws FileNotFoundException { 19475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final File before = getFileForDocId(sourceDocumentId); 19575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final File after = new File(getFileForDocId(targetParentDocumentId), before.getName()); 19675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final File visibleFileBefore = getFileForDocId(sourceDocumentId, true); 19775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 19875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (after.exists()) { 19975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throw new IllegalStateException("Already exists " + after); 20075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 20175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (!before.renameTo(after)) { 20275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throw new IllegalStateException("Failed to move to " + after); 20375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 20475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 20575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final String docId = getDocIdForFile(after); 2069bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan moveInMediaStore(visibleFileBefore, getFileForDocId(docId, true)); 20775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 20875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return docId; 20975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 21075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 211d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tan private void moveInMediaStore(@Nullable File oldVisibleFile, @Nullable File newVisibleFile) { 212d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tan // visibleFolders are null if we're moving a document in external thumb drive or SD card. 213d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tan // 214d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tan // They should be all null or not null at the same time. File#renameTo() doesn't work across 215d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tan // volumes so an exception will be thrown before calling this method. 216d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tan if (oldVisibleFile != null && newVisibleFile != null) { 2179bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan final ContentResolver resolver = getContext().getContentResolver(); 21893615419983fba9b2221b4eb02598d791fc44a04Garfield Tan final Uri externalUri = newVisibleFile.isDirectory() 21993615419983fba9b2221b4eb02598d791fc44a04Garfield Tan ? MediaStore.Files.getDirectoryUri("external") 22093615419983fba9b2221b4eb02598d791fc44a04Garfield Tan : MediaStore.Files.getContentUri("external"); 2219bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan 2229bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan ContentValues values = new ContentValues(); 2239bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan values.put(MediaStore.Files.FileColumns.DATA, newVisibleFile.getAbsolutePath()); 2249bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan 2259bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan // Logic borrowed from MtpDatabase. 2269bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan // note - we are relying on a special case in MediaProvider.update() to update 2279bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan // the paths for all children in the case where this is a directory. 2289bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan final String path = oldVisibleFile.getAbsolutePath(); 2299bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan resolver.update(externalUri, 2309bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan values, 2319bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan "_data LIKE ? AND lower(_data)=lower(?)", 2329bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan new String[] { path, path }); 2339bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan } 2349bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan } 2359bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan 2369bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan @Override 2379bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan public void deleteDocument(String docId) throws FileNotFoundException { 2389bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan final File file = getFileForDocId(docId); 2399bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan final File visibleFile = getFileForDocId(docId, true); 2409bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan 2419bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan final boolean isDirectory = file.isDirectory(); 2429bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan if (isDirectory) { 2439bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan FileUtils.deleteContents(file); 2449bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan } 2459bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan if (!file.delete()) { 2469bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan throw new IllegalStateException("Failed to delete " + file); 2479bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan } 2489bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan 2499bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan removeFromMediaStore(visibleFile, isDirectory); 2509bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan } 2519bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan 252d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tan private void removeFromMediaStore(@Nullable File visibleFile, boolean isFolder) 2539bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan throws FileNotFoundException { 254d21af53763d7d87411ccd02e7d8c259976ca7b97Garfield Tan // visibleFolder is null if we're removing a document from external thumb drive or SD card. 25575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (visibleFile != null) { 25675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final ContentResolver resolver = getContext().getContentResolver(); 25775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final Uri externalUri = MediaStore.Files.getContentUri("external"); 25875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 25975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan // Remove media store entries for any files inside this directory, using 26075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan // path prefix match. Logic borrowed from MtpDatabase. 2619bd2f6c99041620fc596edc385f8a8bdd79ee246Garfield Tan if (isFolder) { 26275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final String path = visibleFile.getAbsolutePath() + "/"; 26375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan resolver.delete(externalUri, 26475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)", 26575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan new String[] { path + "%", Integer.toString(path.length()), path }); 26675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 26775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 26875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan // Remove media store entry for this exact file. 26975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final String path = visibleFile.getAbsolutePath(); 27075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan resolver.delete(externalUri, 27175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan "_data LIKE ?1 AND lower(_data)=lower(?2)", 27275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan new String[] { path, path }); 27375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 27475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 27575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 27675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan @Override 27775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan public Cursor queryDocument(String documentId, String[] projection) 27875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throws FileNotFoundException { 27975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final MatrixCursor result = new MatrixCursor(resolveProjection(projection)); 28075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan includeFile(result, documentId, null); 28175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return result; 28275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 28375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 28475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan @Override 28575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan public Cursor queryChildDocuments( 28675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan String parentDocumentId, String[] projection, String sortOrder) 28775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throws FileNotFoundException { 28875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 28975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final File parent = getFileForDocId(parentDocumentId); 29075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final MatrixCursor result = new DirectoryCursor( 29175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan resolveProjection(projection), parentDocumentId, parent); 29275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan for (File file : parent.listFiles()) { 29375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan includeFile(result, null, file); 29475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 29575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return result; 29675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 29775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 29875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan /** 29975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * Searches documents under the given folder. 30075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * 30175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * To avoid runtime explosion only returns the at most 23 items. 30275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * 30375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * @param folder the root folder where recursive search begins 30475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * @param query the search condition used to match file names 30575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * @param projection projection of the returned cursor 30675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * @param exclusion absolute file paths to exclude from result 30775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * @return cursor containing search result 30875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan * @throws FileNotFoundException when root folder doesn't exist or search fails 30975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan */ 31075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan protected final Cursor querySearchDocuments( 31175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan File folder, String query, String[] projection, Set<String> exclusion) 31275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throws FileNotFoundException { 31375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 31475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan query = query.toLowerCase(); 31575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final MatrixCursor result = new MatrixCursor(resolveProjection(projection)); 31675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final LinkedList<File> pending = new LinkedList<>(); 31775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan pending.add(folder); 31875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan while (!pending.isEmpty() && result.getCount() < 24) { 31975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final File file = pending.removeFirst(); 32075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (file.isDirectory()) { 32175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan for (File child : file.listFiles()) { 32275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan pending.add(child); 32375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 32475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 32575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (file.getName().toLowerCase().contains(query) 32675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan && !exclusion.contains(file.getAbsolutePath())) { 32775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan includeFile(result, null, file); 32875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 32975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 33075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return result; 33175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 33275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 33375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan @Override 33475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan public String getDocumentType(String documentId) throws FileNotFoundException { 33575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final File file = getFileForDocId(documentId); 33675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return getTypeForFile(file); 33775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 33875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 33975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan @Override 34075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan public ParcelFileDescriptor openDocument( 34175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan String documentId, String mode, CancellationSignal signal) 34275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throws FileNotFoundException { 34375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final File file = getFileForDocId(documentId); 34475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final File visibleFile = getFileForDocId(documentId, true); 34575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 34675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final int pfdMode = ParcelFileDescriptor.parseMode(mode); 34775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY || visibleFile == null) { 34875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return ParcelFileDescriptor.open(file, pfdMode); 34975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } else { 35075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan try { 35175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan // When finished writing, kick off media scanner 35275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return ParcelFileDescriptor.open( 35375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan file, pfdMode, mHandler, (IOException e) -> scanFile(visibleFile)); 35475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } catch (IOException e) { 35575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throw new FileNotFoundException("Failed to open for writing: " + e); 35675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 35775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 35875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 35975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 36075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private void scanFile(File visibleFile) { 36175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); 36275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan intent.setData(Uri.fromFile(visibleFile)); 36375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan getContext().sendBroadcast(intent); 36475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 36575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 36675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan @Override 36775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan public AssetFileDescriptor openDocumentThumbnail( 36875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan String documentId, Point sizeHint, CancellationSignal signal) 36975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throws FileNotFoundException { 37075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final File file = getFileForDocId(documentId); 37175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return DocumentsContract.openImageThumbnail(file); 37275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 37375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 37475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan protected RowBuilder includeFile(MatrixCursor result, String docId, File file) 37575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan throws FileNotFoundException { 37675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (docId == null) { 37775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan docId = getDocIdForFile(file); 37875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } else { 37975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan file = getFileForDocId(docId); 38075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 38175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 38275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan int flags = 0; 38375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 38475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (file.canWrite()) { 38575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (file.isDirectory()) { 38675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan flags |= Document.FLAG_DIR_SUPPORTS_CREATE; 38775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan flags |= Document.FLAG_SUPPORTS_DELETE; 38875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan flags |= Document.FLAG_SUPPORTS_RENAME; 38975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan flags |= Document.FLAG_SUPPORTS_MOVE; 39075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } else { 39175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan flags |= Document.FLAG_SUPPORTS_WRITE; 39275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan flags |= Document.FLAG_SUPPORTS_DELETE; 39375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan flags |= Document.FLAG_SUPPORTS_RENAME; 39475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan flags |= Document.FLAG_SUPPORTS_MOVE; 39575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 39675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 39775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 39875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final String mimeType = getTypeForFile(file); 39975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final String displayName = file.getName(); 40075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (mimeType.startsWith("image/")) { 40175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan flags |= Document.FLAG_SUPPORTS_THUMBNAIL; 40275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 40375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 40475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final RowBuilder row = result.newRow(); 40575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan row.add(Document.COLUMN_DOCUMENT_ID, docId); 40675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan row.add(Document.COLUMN_DISPLAY_NAME, displayName); 40775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan row.add(Document.COLUMN_SIZE, file.length()); 40875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan row.add(Document.COLUMN_MIME_TYPE, mimeType); 40975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan row.add(Document.COLUMN_FLAGS, flags); 41075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 41175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan // Only publish dates reasonably after epoch 41275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan long lastModified = file.lastModified(); 41375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (lastModified > 31536000000L) { 41475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan row.add(Document.COLUMN_LAST_MODIFIED, lastModified); 41575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 41675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 41775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan // Return the row builder just in case any subclass want to add more stuff to it. 41875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return row; 41975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 42075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 42175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private static String getTypeForFile(File file) { 42275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (file.isDirectory()) { 42375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return Document.MIME_TYPE_DIR; 42475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } else { 42575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return getTypeForName(file.getName()); 42675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 42775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 42875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 42975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private static String getTypeForName(String name) { 43075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final int lastDot = name.lastIndexOf('.'); 43175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (lastDot >= 0) { 43275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final String extension = name.substring(lastDot + 1).toLowerCase(); 43375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 43475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (mime != null) { 43575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return mime; 43675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 43775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 43875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 43975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return "application/octet-stream"; 44075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 44175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 44275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan protected final File getFileForDocId(String docId) throws FileNotFoundException { 44375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return getFileForDocId(docId, false); 44475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 44575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 44675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private String[] resolveProjection(String[] projection) { 44775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return projection == null ? mDefaultProjection : projection; 44875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 44975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 45075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private void startObserving(File file, Uri notifyUri) { 45175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan synchronized (mObservers) { 45275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan DirectoryObserver observer = mObservers.get(file); 45375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (observer == null) { 45475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan observer = new DirectoryObserver( 45575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan file, getContext().getContentResolver(), notifyUri); 45675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan observer.startWatching(); 45775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan mObservers.put(file, observer); 45875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 45975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan observer.mRefCount++; 46075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 46175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer); 46275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 46375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 46475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 46575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private void stopObserving(File file) { 46675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan synchronized (mObservers) { 46775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan DirectoryObserver observer = mObservers.get(file); 46875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (observer == null) return; 46975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 47075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan observer.mRefCount--; 47175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (observer.mRefCount == 0) { 47275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan mObservers.remove(file); 47375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan observer.stopWatching(); 47475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 47575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 47675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer); 47775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 47875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 47975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 48075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private static class DirectoryObserver extends FileObserver { 48175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO 48275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan | CREATE | DELETE | DELETE_SELF | MOVE_SELF; 48375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 48475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private final File mFile; 48575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private final ContentResolver mResolver; 48675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private final Uri mNotifyUri; 48775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 48875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private int mRefCount = 0; 48975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 49075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) { 49175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan super(file.getAbsolutePath(), NOTIFY_EVENTS); 49275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan mFile = file; 49375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan mResolver = resolver; 49475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan mNotifyUri = notifyUri; 49575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 49675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 49775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan @Override 49875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan public void onEvent(int event, String path) { 49975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if ((event & NOTIFY_EVENTS) != 0) { 50075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path); 50175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan mResolver.notifyChange(mNotifyUri, null, false); 50275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 50375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 50475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 50575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan @Override 50675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan public String toString() { 50775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}"; 50875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 50975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 51075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 51175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private class DirectoryCursor extends MatrixCursor { 51275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan private final File mFile; 51375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 51475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan public DirectoryCursor(String[] columnNames, String docId, File file) { 51575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan super(columnNames); 51675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 51775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan final Uri notifyUri = buildNotificationUri(docId); 51875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan setNotificationUri(getContext().getContentResolver(), notifyUri); 51975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 52075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan mFile = file; 52175379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan startObserving(mFile, notifyUri); 52275379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 52375379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan 52475379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan @Override 52575379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan public void close() { 52675379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan super.close(); 52775379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan stopObserving(mFile); 52875379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 52975379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan } 53075379db42dcd7c5081aed8a90b7d6077b637ffe0Garfield Tan} 531