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