19e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey/*
29e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey * Copyright (C) 2013 The Android Open Source Project
39e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey *
49e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey * Licensed under the Apache License, Version 2.0 (the "License");
59e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey * you may not use this file except in compliance with the License.
69e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey * You may obtain a copy of the License at
79e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey *
89e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey *      http://www.apache.org/licenses/LICENSE-2.0
99e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey *
109e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey * Unless required by applicable law or agreed to in writing, software
119e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey * distributed under the License is distributed on an "AS IS" BASIS,
129e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
139e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey * See the License for the specific language governing permissions and
149e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey * limitations under the License.
159e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey */
169e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
179e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkeypackage com.android.externalstorage;
189e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
19db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkeyimport android.content.ContentResolver;
201f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkeyimport android.content.Context;
216398343e83b3fd11dd6536cf6f390a52c1e19d2eJeff Sharkeyimport android.content.res.AssetFileDescriptor;
229e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkeyimport android.database.Cursor;
239e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkeyimport android.database.MatrixCursor;
249d0843df7e3984293dc4ab6ee2f9502e898b63aaJeff Sharkeyimport android.database.MatrixCursor.RowBuilder;
25aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkeyimport android.graphics.Point;
26db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkeyimport android.net.Uri;
27aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkeyimport android.os.CancellationSignal;
289e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkeyimport android.os.Environment;
29db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkeyimport android.os.FileObserver;
309e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkeyimport android.os.ParcelFileDescriptor;
311f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkeyimport android.os.storage.StorageManager;
321f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkeyimport android.os.storage.StorageVolume;
331f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkeyimport android.provider.DocumentsContract;
34ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkeyimport android.provider.DocumentsContract.Document;
35ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkeyimport android.provider.DocumentsContract.Root;
36aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkeyimport android.provider.DocumentsProvider;
371f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkeyimport android.util.Log;
389e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkeyimport android.webkit.MimeTypeMap;
399e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
401f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkeyimport com.android.internal.annotations.GuardedBy;
41aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkeyimport com.google.android.collect.Lists;
4220d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkeyimport com.google.android.collect.Maps;
439e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
449e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkeyimport java.io.File;
459e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkeyimport java.io.FileNotFoundException;
4620d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkeyimport java.io.IOException;
47aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkeyimport java.util.ArrayList;
4820d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkeyimport java.util.HashMap;
4920d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkeyimport java.util.LinkedList;
50aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkeyimport java.util.Map;
519e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
52aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkeypublic class ExternalStorageProvider extends DocumentsProvider {
539e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    private static final String TAG = "ExternalStorage";
549e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
55db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey    private static final boolean LOG_INOTIFY = false;
56db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey
571f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey    public static final String AUTHORITY = "com.android.externalstorage.documents";
581f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey
59aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    // docId format: root:path/to/file
609e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
61ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
626efba22ce510352bb84910d6efc42fecafd31ed7Jeff Sharkey            Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
636efba22ce510352bb84910d6efc42fecafd31ed7Jeff Sharkey            Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES,
649d0843df7e3984293dc4ab6ee2f9502e898b63aaJeff Sharkey    };
659e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
66ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
67ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
68ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
69ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    };
70ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey
71ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    private static class RootInfo {
72ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        public String rootId;
73ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        public int flags;
74ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        public String title;
75ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        public String docId;
76ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    }
77ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey
781f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey    private static final String ROOT_ID_PRIMARY_EMULATED = "primary";
791f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey
801f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey    private StorageManager mStorageManager;
811f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey
821f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey    private final Object mRootsLock = new Object();
831f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey
841f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey    @GuardedBy("mRootsLock")
85ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    private ArrayList<RootInfo> mRoots;
861f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey    @GuardedBy("mRootsLock")
87ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    private HashMap<String, RootInfo> mIdToRoot;
881f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey    @GuardedBy("mRootsLock")
89ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    private HashMap<String, File> mIdToPath;
90aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey
91db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey    @GuardedBy("mObservers")
92db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey    private Map<File, DirectoryObserver> mObservers = Maps.newHashMap();
93db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey
949e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    @Override
959e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    public boolean onCreate() {
961f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        mStorageManager = (StorageManager) getContext().getSystemService(Context.STORAGE_SERVICE);
971f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey
98aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        mRoots = Lists.newArrayList();
99ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        mIdToRoot = Maps.newHashMap();
100ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        mIdToPath = Maps.newHashMap();
101aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey
1021f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        updateVolumes();
10320d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey
1049e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        return true;
1059e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    }
1069e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
1071f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey    public void updateVolumes() {
1081f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        synchronized (mRootsLock) {
1091f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey            updateVolumesLocked();
1101f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        }
1111f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey    }
1121f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey
1131f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey    private void updateVolumesLocked() {
1141f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        mRoots.clear();
1151f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        mIdToPath.clear();
1161f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        mIdToRoot.clear();
1171f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey
1181f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        final StorageVolume[] volumes = mStorageManager.getVolumeList();
1191f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        for (StorageVolume volume : volumes) {
1201f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey            final boolean mounted = Environment.MEDIA_MOUNTED.equals(volume.getState())
1211f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                    || Environment.MEDIA_MOUNTED_READ_ONLY.equals(volume.getState());
1221f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey            if (!mounted) continue;
1231f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey
1241f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey            final String rootId;
1251f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey            if (volume.isPrimary() && volume.isEmulated()) {
1261f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                rootId = ROOT_ID_PRIMARY_EMULATED;
1271f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey            } else if (volume.getUuid() != null) {
1281f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                rootId = volume.getUuid();
1291f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey            } else {
1301f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                Log.d(TAG, "Missing UUID for " + volume.getPath() + "; skipping");
1311f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                continue;
1321f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey            }
1331f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey
1341f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey            if (mIdToPath.containsKey(rootId)) {
1351f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                Log.w(TAG, "Duplicate UUID " + rootId + "; skipping");
1361f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                continue;
1371f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey            }
1381f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey
1391f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey            try {
1401f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                final File path = volume.getPathFile();
1411f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                mIdToPath.put(rootId, path);
1421f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey
1431f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                final RootInfo root = new RootInfo();
1441f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                root.rootId = rootId;
1451f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                root.flags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED
1461f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                        | Root.FLAG_SUPPORTS_SEARCH;
1471f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                if (ROOT_ID_PRIMARY_EMULATED.equals(rootId)) {
1481f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                    root.title = getContext().getString(R.string.root_internal_storage);
1491f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                } else {
1501f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                    root.title = volume.getUserLabel();
1511f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                }
1521f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                root.docId = getDocIdForFile(path);
1531f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                mRoots.add(root);
1541f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                mIdToRoot.put(rootId, root);
1551f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey            } catch (FileNotFoundException e) {
1561f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                throw new IllegalStateException(e);
1571f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey            }
1581f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        }
1591f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey
1601f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        Log.d(TAG, "After updating volumes, found " + mRoots.size() + " active roots");
1611f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey
1621f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        getContext().getContentResolver()
1631f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                .notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null, false);
1641f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey    }
1651f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey
166ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    private static String[] resolveRootProjection(String[] projection) {
167ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
168ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    }
169ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey
170ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    private static String[] resolveDocumentProjection(String[] projection) {
171ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
172ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    }
173ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey
174aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    private String getDocIdForFile(File file) throws FileNotFoundException {
175aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        String path = file.getAbsolutePath();
1769e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
177aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        // Find the most-specific root path
178aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        Map.Entry<String, File> mostSpecific = null;
1791f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        synchronized (mRootsLock) {
1801f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey            for (Map.Entry<String, File> root : mIdToPath.entrySet()) {
1811f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                final String rootPath = root.getValue().getPath();
1821f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                if (path.startsWith(rootPath) && (mostSpecific == null
1831f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                        || rootPath.length() > mostSpecific.getValue().getPath().length())) {
1841f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                    mostSpecific = root;
1851f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                }
18692d7e697a864a3e18bef4ef256bb3eb339a66b4eJeff Sharkey            }
187aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        }
188dc2963aecaf38bf53d6de82957412a486049c207Jeff Sharkey
189aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        if (mostSpecific == null) {
190aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            throw new FileNotFoundException("Failed to find root that contains " + path);
191aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        }
192dc2963aecaf38bf53d6de82957412a486049c207Jeff Sharkey
193aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        // Start at first char of path under root
194aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        final String rootPath = mostSpecific.getValue().getPath();
195aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        if (rootPath.equals(path)) {
196aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            path = "";
197aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        } else if (rootPath.endsWith("/")) {
198aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            path = path.substring(rootPath.length());
199aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        } else {
200aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            path = path.substring(rootPath.length() + 1);
2019e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        }
202aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey
203aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        return mostSpecific.getKey() + ':' + path;
2049e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    }
2059e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
206aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    private File getFileForDocId(String docId) throws FileNotFoundException {
207aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        final int splitIndex = docId.indexOf(':', 1);
208aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        final String tag = docId.substring(0, splitIndex);
209aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        final String path = docId.substring(splitIndex + 1);
21020d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey
2111f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        File target;
2121f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        synchronized (mRootsLock) {
2131f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey            target = mIdToPath.get(tag);
2141f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        }
215aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        if (target == null) {
216aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            throw new FileNotFoundException("No root for " + tag);
21792d7e697a864a3e18bef4ef256bb3eb339a66b4eJeff Sharkey        }
2183e1189b3590aefb65a2af720ae2ba959bbd4188dJeff Sharkey        if (!target.exists()) {
2193e1189b3590aefb65a2af720ae2ba959bbd4188dJeff Sharkey            target.mkdirs();
2203e1189b3590aefb65a2af720ae2ba959bbd4188dJeff Sharkey        }
221aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        target = new File(target, path);
222aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        if (!target.exists()) {
223aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            throw new FileNotFoundException("Missing file for " + docId + " at " + target);
22492d7e697a864a3e18bef4ef256bb3eb339a66b4eJeff Sharkey        }
225aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        return target;
22620d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey    }
22720d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey
228aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    private void includeFile(MatrixCursor result, String docId, File file)
229aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            throws FileNotFoundException {
230aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        if (docId == null) {
231aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            docId = getDocIdForFile(file);
23292d7e697a864a3e18bef4ef256bb3eb339a66b4eJeff Sharkey        } else {
233aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            file = getFileForDocId(docId);
23420d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey        }
23520d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey
2369e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        int flags = 0;
2379e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
2389e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        if (file.canWrite()) {
2392a030b05a978281147df4d1cc4f12bc8d61c0729Jeff Sharkey            if (file.isDirectory()) {
2402a030b05a978281147df4d1cc4f12bc8d61c0729Jeff Sharkey                flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
2412a030b05a978281147df4d1cc4f12bc8d61c0729Jeff Sharkey            } else {
2422a030b05a978281147df4d1cc4f12bc8d61c0729Jeff Sharkey                flags |= Document.FLAG_SUPPORTS_WRITE;
2432a030b05a978281147df4d1cc4f12bc8d61c0729Jeff Sharkey            }
244ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            flags |= Document.FLAG_SUPPORTS_DELETE;
2459e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        }
2469e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
247aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        final String displayName = file.getName();
24820d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey        final String mimeType = getTypeForFile(file);
2499e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        if (mimeType.startsWith("image/")) {
250ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
2519e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        }
2529e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
2539d0843df7e3984293dc4ab6ee2f9502e898b63aaJeff Sharkey        final RowBuilder row = result.newRow();
254b7757a6b32edea62a1a9a803ad83579220f26100Jeff Sharkey        row.add(Document.COLUMN_DOCUMENT_ID, docId);
255b7757a6b32edea62a1a9a803ad83579220f26100Jeff Sharkey        row.add(Document.COLUMN_DISPLAY_NAME, displayName);
256b7757a6b32edea62a1a9a803ad83579220f26100Jeff Sharkey        row.add(Document.COLUMN_SIZE, file.length());
257b7757a6b32edea62a1a9a803ad83579220f26100Jeff Sharkey        row.add(Document.COLUMN_MIME_TYPE, mimeType);
258b7757a6b32edea62a1a9a803ad83579220f26100Jeff Sharkey        row.add(Document.COLUMN_FLAGS, flags);
259d5a4658cea8def24155849a8621608155dcba05cJeff Sharkey
260d5a4658cea8def24155849a8621608155dcba05cJeff Sharkey        // Only publish dates reasonably after epoch
261d5a4658cea8def24155849a8621608155dcba05cJeff Sharkey        long lastModified = file.lastModified();
262d5a4658cea8def24155849a8621608155dcba05cJeff Sharkey        if (lastModified > 31536000000L) {
263d5a4658cea8def24155849a8621608155dcba05cJeff Sharkey            row.add(Document.COLUMN_LAST_MODIFIED, lastModified);
264d5a4658cea8def24155849a8621608155dcba05cJeff Sharkey        }
2659e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    }
2669e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
2679e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    @Override
268ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
269ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
2701f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        synchronized (mRootsLock) {
2711f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey            for (String rootId : mIdToPath.keySet()) {
2721f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                final RootInfo root = mIdToRoot.get(rootId);
2731f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                final File path = mIdToPath.get(rootId);
2741f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey
2751f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                final RowBuilder row = result.newRow();
2761f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                row.add(Root.COLUMN_ROOT_ID, root.rootId);
2771f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                row.add(Root.COLUMN_FLAGS, root.flags);
2781f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                row.add(Root.COLUMN_TITLE, root.title);
2791f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                row.add(Root.COLUMN_DOCUMENT_ID, root.docId);
2801f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey                row.add(Root.COLUMN_AVAILABLE_BYTES, path.getFreeSpace());
2811f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey            }
2829e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        }
283ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        return result;
2849e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    }
2859e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
286aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    @Override
287aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    public String createDocument(String docId, String mimeType, String displayName)
288aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            throws FileNotFoundException {
289aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        final File parent = getFileForDocId(docId);
2905545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey        File file;
291aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey
292ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        if (Document.MIME_TYPE_DIR.equals(mimeType)) {
2935545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            file = new File(parent, displayName);
294aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            if (!file.mkdir()) {
295aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                throw new IllegalStateException("Failed to mkdir " + file);
296aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            }
29720d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey        } else {
2985545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            displayName = removeExtension(mimeType, displayName);
2995545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            file = new File(parent, addExtension(mimeType, displayName));
3005545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey
3015545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            // If conflicting file, try adding counter suffix
3025545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            int n = 0;
3035545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            while (file.exists() && n++ < 32) {
3045545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey                file = new File(parent, addExtension(mimeType, displayName + " (" + n + ")"));
3055545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            }
3065545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey
307aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            try {
308aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                if (!file.createNewFile()) {
309aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                    throw new IllegalStateException("Failed to touch " + file);
310aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                }
311aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            } catch (IOException e) {
312aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                throw new IllegalStateException("Failed to touch " + file + ": " + e);
313aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            }
3149e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        }
315aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        return getDocIdForFile(file);
31620d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey    }
3179e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
318aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    @Override
319aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    public void deleteDocument(String docId) throws FileNotFoundException {
320aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        final File file = getFileForDocId(docId);
321aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        if (!file.delete()) {
322aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            throw new IllegalStateException("Failed to delete " + file);
3239e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        }
3249e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    }
3259e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
3269e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    @Override
327ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    public Cursor queryDocument(String documentId, String[] projection)
328ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            throws FileNotFoundException {
329ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
330ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        includeFile(result, documentId, null);
331aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        return result;
332aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    }
333aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey
334aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    @Override
335ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    public Cursor queryChildDocuments(
336ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            String parentDocumentId, String[] projection, String sortOrder)
337ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            throws FileNotFoundException {
338ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        final File parent = getFileForDocId(parentDocumentId);
339db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        final MatrixCursor result = new DirectoryCursor(
340db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey                resolveDocumentProjection(projection), parentDocumentId, parent);
341aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        for (File file : parent.listFiles()) {
342aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            includeFile(result, null, file);
3436398343e83b3fd11dd6536cf6f390a52c1e19d2eJeff Sharkey        }
344aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        return result;
345aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    }
3466398343e83b3fd11dd6536cf6f390a52c1e19d2eJeff Sharkey
347aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    @Override
3483e1189b3590aefb65a2af720ae2ba959bbd4188dJeff Sharkey    public Cursor querySearchDocuments(String rootId, String query, String[] projection)
349ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            throws FileNotFoundException {
350ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
3511f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey
3521f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        final File parent;
3531f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        synchronized (mRootsLock) {
3541f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey            parent = mIdToPath.get(rootId);
3551f706c6cd1cb841adadc2babc57a34e5728983ecJeff Sharkey        }
356aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey
357aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        final LinkedList<File> pending = new LinkedList<File>();
358aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        pending.add(parent);
3594ec973925fc2cd18f9ec0d0ca5af588564fded27Jeff Sharkey        while (!pending.isEmpty() && result.getCount() < 24) {
360aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            final File file = pending.removeFirst();
361aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            if (file.isDirectory()) {
362aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                for (File child : file.listFiles()) {
363aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                    pending.add(child);
364aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                }
3654ec973925fc2cd18f9ec0d0ca5af588564fded27Jeff Sharkey            }
3664ec973925fc2cd18f9ec0d0ca5af588564fded27Jeff Sharkey            if (file.getName().toLowerCase().contains(query)) {
3674ec973925fc2cd18f9ec0d0ca5af588564fded27Jeff Sharkey                includeFile(result, null, file);
3686398343e83b3fd11dd6536cf6f390a52c1e19d2eJeff Sharkey            }
3696398343e83b3fd11dd6536cf6f390a52c1e19d2eJeff Sharkey        }
370aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        return result;
3716398343e83b3fd11dd6536cf6f390a52c1e19d2eJeff Sharkey    }
3726398343e83b3fd11dd6536cf6f390a52c1e19d2eJeff Sharkey
3736398343e83b3fd11dd6536cf6f390a52c1e19d2eJeff Sharkey    @Override
374ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    public String getDocumentType(String documentId) throws FileNotFoundException {
375ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        final File file = getFileForDocId(documentId);
376aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        return getTypeForFile(file);
377aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    }
37820d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey
379aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    @Override
380ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    public ParcelFileDescriptor openDocument(
381ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            String documentId, String mode, CancellationSignal signal)
382aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            throws FileNotFoundException {
383ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        final File file = getFileForDocId(documentId);
384eb8c3f93edc826413ff4143284dec01c1061d5ccAdam Lesinski        return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
3859e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    }
3869e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
3879e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    @Override
388aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    public AssetFileDescriptor openDocumentThumbnail(
389ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            String documentId, Point sizeHint, CancellationSignal signal)
390ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            throws FileNotFoundException {
391ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        final File file = getFileForDocId(documentId);
392c1c8f3f97d344a24bfddcb56a8be05e7e2fabe9eJeff Sharkey        return DocumentsContract.openImageThumbnail(file);
3939e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    }
3949e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
395aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    private static String getTypeForFile(File file) {
396aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        if (file.isDirectory()) {
397ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            return Document.MIME_TYPE_DIR;
398aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        } else {
399aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            return getTypeForName(file.getName());
400aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        }
401aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    }
402aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey
403aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    private static String getTypeForName(String name) {
404aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        final int lastDot = name.lastIndexOf('.');
405aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        if (lastDot >= 0) {
40696c620595bd0585f934b0971b4552c57845e9a78Jeff Sharkey            final String extension = name.substring(lastDot + 1).toLowerCase();
407aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
408aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            if (mime != null) {
409aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                return mime;
41020d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey            }
41120d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey        }
412aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey
413aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        return "application/octet-stream";
41420d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey    }
41520d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey
4165545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey    /**
4175545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey     * Remove file extension from name, but only if exact MIME type mapping
4185545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey     * exists. This means we can reapply the extension later.
4195545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey     */
4205545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey    private static String removeExtension(String mimeType, String name) {
4215545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey        final int lastDot = name.lastIndexOf('.');
4225545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey        if (lastDot >= 0) {
42396c620595bd0585f934b0971b4552c57845e9a78Jeff Sharkey            final String extension = name.substring(lastDot + 1).toLowerCase();
4245545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            final String nameMime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
4255545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            if (mimeType.equals(nameMime)) {
4265545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey                return name.substring(0, lastDot);
42720d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey            }
4285545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey        }
4295545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey        return name;
4305545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey    }
43120d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey
4325545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey    /**
4335545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey     * Add file extension to name, but only if exact MIME type mapping exists.
4345545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey     */
4355545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey    private static String addExtension(String mimeType, String name) {
4365545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey        final String extension = MimeTypeMap.getSingleton()
4375545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey                .getExtensionFromMimeType(mimeType);
4385545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey        if (extension != null) {
4395545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            return name + "." + extension;
44020d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey        }
4415545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey        return name;
4429e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    }
443db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey
444db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey    private void startObserving(File file, Uri notifyUri) {
445db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        synchronized (mObservers) {
446db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            DirectoryObserver observer = mObservers.get(file);
447db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            if (observer == null) {
448db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey                observer = new DirectoryObserver(
449db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey                        file, getContext().getContentResolver(), notifyUri);
450db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey                observer.startWatching();
451db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey                mObservers.put(file, observer);
452db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            }
453db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            observer.mRefCount++;
454db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey
455db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer);
456db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        }
457db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey    }
458db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey
459db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey    private void stopObserving(File file) {
460db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        synchronized (mObservers) {
461db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            DirectoryObserver observer = mObservers.get(file);
462db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            if (observer == null) return;
463db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey
464db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            observer.mRefCount--;
465db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            if (observer.mRefCount == 0) {
466db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey                mObservers.remove(file);
467db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey                observer.stopWatching();
468db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            }
469db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey
470db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer);
471db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        }
472db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey    }
473db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey
474db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey    private static class DirectoryObserver extends FileObserver {
475db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO
476db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey                | CREATE | DELETE | DELETE_SELF | MOVE_SELF;
477db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey
478db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        private final File mFile;
479db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        private final ContentResolver mResolver;
480db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        private final Uri mNotifyUri;
481db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey
482db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        private int mRefCount = 0;
483db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey
484db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) {
485db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            super(file.getAbsolutePath(), NOTIFY_EVENTS);
486db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            mFile = file;
487db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            mResolver = resolver;
488db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            mNotifyUri = notifyUri;
489db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        }
490db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey
491db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        @Override
492db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        public void onEvent(int event, String path) {
493db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            if ((event & NOTIFY_EVENTS) != 0) {
494db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey                if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path);
495db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey                mResolver.notifyChange(mNotifyUri, null, false);
496db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            }
497db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        }
498db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey
499db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        @Override
500db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        public String toString() {
501db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}";
502db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        }
503db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey    }
504db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey
505db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey    private class DirectoryCursor extends MatrixCursor {
506db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        private final File mFile;
507db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey
508db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        public DirectoryCursor(String[] columnNames, String docId, File file) {
509db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            super(columnNames);
510db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey
511db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            final Uri notifyUri = DocumentsContract.buildChildDocumentsUri(
512db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey                    AUTHORITY, docId);
513db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            setNotificationUri(getContext().getContentResolver(), notifyUri);
514db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey
515db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            mFile = file;
516db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            startObserving(mFile, notifyUri);
517db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        }
518db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey
519db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        @Override
520db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        public void close() {
521db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            super.close();
522db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey            stopObserving(mFile);
523db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey        }
524db5ef125007644daa94aeaf1bd8637f4e0095e94Jeff Sharkey    }
5259e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey}
526