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
196398343e83b3fd11dd6536cf6f390a52c1e19d2eJeff Sharkeyimport android.content.res.AssetFileDescriptor;
209e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkeyimport android.database.Cursor;
219e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkeyimport android.database.MatrixCursor;
229d0843df7e3984293dc4ab6ee2f9502e898b63aaJeff Sharkeyimport android.database.MatrixCursor.RowBuilder;
23aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkeyimport android.graphics.Point;
246398343e83b3fd11dd6536cf6f390a52c1e19d2eJeff Sharkeyimport android.media.ExifInterface;
25aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkeyimport android.os.CancellationSignal;
269e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkeyimport android.os.Environment;
279e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkeyimport android.os.ParcelFileDescriptor;
28ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkeyimport android.provider.DocumentsContract.Document;
29ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkeyimport android.provider.DocumentsContract.Root;
30c1c8f3f97d344a24bfddcb56a8be05e7e2fabe9eJeff Sharkeyimport android.provider.DocumentsContract;
31aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkeyimport android.provider.DocumentsProvider;
329e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkeyimport android.webkit.MimeTypeMap;
339e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
34aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkeyimport com.google.android.collect.Lists;
3520d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkeyimport com.google.android.collect.Maps;
369e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
379e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkeyimport java.io.File;
389e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkeyimport java.io.FileNotFoundException;
3920d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkeyimport java.io.IOException;
40aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkeyimport java.util.ArrayList;
4120d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkeyimport java.util.HashMap;
4220d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkeyimport java.util.LinkedList;
43aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkeyimport java.util.Map;
449e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
45aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkeypublic class ExternalStorageProvider extends DocumentsProvider {
469e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    private static final String TAG = "ExternalStorage";
479e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
48aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    // docId format: root:path/to/file
499e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
50ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
516efba22ce510352bb84910d6efc42fecafd31ed7Jeff Sharkey            Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
526efba22ce510352bb84910d6efc42fecafd31ed7Jeff Sharkey            Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES,
539d0843df7e3984293dc4ab6ee2f9502e898b63aaJeff Sharkey    };
549e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
55ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
56ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
57ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
58ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    };
59ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey
60ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    private static class RootInfo {
61ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        public String rootId;
62ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        public int flags;
63ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        public String title;
64ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        public String docId;
65ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    }
66ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey
67ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    private ArrayList<RootInfo> mRoots;
68ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    private HashMap<String, RootInfo> mIdToRoot;
69ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    private HashMap<String, File> mIdToPath;
70aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey
719e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    @Override
729e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    public boolean onCreate() {
73aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        mRoots = Lists.newArrayList();
74ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        mIdToRoot = Maps.newHashMap();
75ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        mIdToPath = Maps.newHashMap();
76aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey
77d5a4658cea8def24155849a8621608155dcba05cJeff Sharkey        // TODO: support multiple storage devices, requiring that volume serial
78d5a4658cea8def24155849a8621608155dcba05cJeff Sharkey        // number be burned into rootId so we can identify files from different
79d5a4658cea8def24155849a8621608155dcba05cJeff Sharkey        // volumes. currently we only use a static rootId for emulated storage,
80d5a4658cea8def24155849a8621608155dcba05cJeff Sharkey        // since that storage never changes.
81d5a4658cea8def24155849a8621608155dcba05cJeff Sharkey        if (!Environment.isExternalStorageEmulated()) return true;
82aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey
83aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        try {
84ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            final String rootId = "primary";
85aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            final File path = Environment.getExternalStorageDirectory();
86ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            mIdToPath.put(rootId, path);
87ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey
88ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            final RootInfo root = new RootInfo();
893e1189b3590aefb65a2af720ae2ba959bbd4188dJeff Sharkey            root.rootId = rootId;
903e1189b3590aefb65a2af720ae2ba959bbd4188dJeff Sharkey            root.flags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED
913e1189b3590aefb65a2af720ae2ba959bbd4188dJeff Sharkey                    | Root.FLAG_SUPPORTS_SEARCH;
92ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            root.title = getContext().getString(R.string.root_internal_storage);
93ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            root.docId = getDocIdForFile(path);
94aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            mRoots.add(root);
95ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            mIdToRoot.put(rootId, root);
96aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        } catch (FileNotFoundException e) {
97aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            throw new IllegalStateException(e);
98aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        }
9920d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey
1009e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        return true;
1019e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    }
1029e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
103ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    private static String[] resolveRootProjection(String[] projection) {
104ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
105ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    }
106ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey
107ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    private static String[] resolveDocumentProjection(String[] projection) {
108ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
109ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    }
110ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey
111aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    private String getDocIdForFile(File file) throws FileNotFoundException {
112aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        String path = file.getAbsolutePath();
1139e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
114aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        // Find the most-specific root path
115aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        Map.Entry<String, File> mostSpecific = null;
116ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        for (Map.Entry<String, File> root : mIdToPath.entrySet()) {
117aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            final String rootPath = root.getValue().getPath();
118aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            if (path.startsWith(rootPath) && (mostSpecific == null
119aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                    || rootPath.length() > mostSpecific.getValue().getPath().length())) {
120aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                mostSpecific = root;
12192d7e697a864a3e18bef4ef256bb3eb339a66b4eJeff Sharkey            }
122aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        }
123dc2963aecaf38bf53d6de82957412a486049c207Jeff Sharkey
124aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        if (mostSpecific == null) {
125aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            throw new FileNotFoundException("Failed to find root that contains " + path);
126aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        }
127dc2963aecaf38bf53d6de82957412a486049c207Jeff Sharkey
128aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        // Start at first char of path under root
129aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        final String rootPath = mostSpecific.getValue().getPath();
130aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        if (rootPath.equals(path)) {
131aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            path = "";
132aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        } else if (rootPath.endsWith("/")) {
133aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            path = path.substring(rootPath.length());
134aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        } else {
135aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            path = path.substring(rootPath.length() + 1);
1369e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        }
137aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey
138aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        return mostSpecific.getKey() + ':' + path;
1399e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    }
1409e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
141aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    private File getFileForDocId(String docId) throws FileNotFoundException {
142aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        final int splitIndex = docId.indexOf(':', 1);
143aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        final String tag = docId.substring(0, splitIndex);
144aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        final String path = docId.substring(splitIndex + 1);
14520d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey
146ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        File target = mIdToPath.get(tag);
147aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        if (target == null) {
148aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            throw new FileNotFoundException("No root for " + tag);
14992d7e697a864a3e18bef4ef256bb3eb339a66b4eJeff Sharkey        }
1503e1189b3590aefb65a2af720ae2ba959bbd4188dJeff Sharkey        if (!target.exists()) {
1513e1189b3590aefb65a2af720ae2ba959bbd4188dJeff Sharkey            target.mkdirs();
1523e1189b3590aefb65a2af720ae2ba959bbd4188dJeff Sharkey        }
153aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        target = new File(target, path);
154aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        if (!target.exists()) {
155aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            throw new FileNotFoundException("Missing file for " + docId + " at " + target);
15692d7e697a864a3e18bef4ef256bb3eb339a66b4eJeff Sharkey        }
157aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        return target;
15820d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey    }
15920d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey
160aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    private void includeFile(MatrixCursor result, String docId, File file)
161aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            throws FileNotFoundException {
162aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        if (docId == null) {
163aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            docId = getDocIdForFile(file);
16492d7e697a864a3e18bef4ef256bb3eb339a66b4eJeff Sharkey        } else {
165aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            file = getFileForDocId(docId);
16620d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey        }
16720d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey
1689e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        int flags = 0;
1699e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
1709e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        if (file.canWrite()) {
1712a030b05a978281147df4d1cc4f12bc8d61c0729Jeff Sharkey            if (file.isDirectory()) {
1722a030b05a978281147df4d1cc4f12bc8d61c0729Jeff Sharkey                flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
1732a030b05a978281147df4d1cc4f12bc8d61c0729Jeff Sharkey            } else {
1742a030b05a978281147df4d1cc4f12bc8d61c0729Jeff Sharkey                flags |= Document.FLAG_SUPPORTS_WRITE;
1752a030b05a978281147df4d1cc4f12bc8d61c0729Jeff Sharkey            }
176ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            flags |= Document.FLAG_SUPPORTS_DELETE;
1779e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        }
1789e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
179aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        final String displayName = file.getName();
18020d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey        final String mimeType = getTypeForFile(file);
1819e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        if (mimeType.startsWith("image/")) {
182ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
1839e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        }
1849e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
1859d0843df7e3984293dc4ab6ee2f9502e898b63aaJeff Sharkey        final RowBuilder row = result.newRow();
186b7757a6b32edea62a1a9a803ad83579220f26100Jeff Sharkey        row.add(Document.COLUMN_DOCUMENT_ID, docId);
187b7757a6b32edea62a1a9a803ad83579220f26100Jeff Sharkey        row.add(Document.COLUMN_DISPLAY_NAME, displayName);
188b7757a6b32edea62a1a9a803ad83579220f26100Jeff Sharkey        row.add(Document.COLUMN_SIZE, file.length());
189b7757a6b32edea62a1a9a803ad83579220f26100Jeff Sharkey        row.add(Document.COLUMN_MIME_TYPE, mimeType);
190b7757a6b32edea62a1a9a803ad83579220f26100Jeff Sharkey        row.add(Document.COLUMN_FLAGS, flags);
191d5a4658cea8def24155849a8621608155dcba05cJeff Sharkey
192d5a4658cea8def24155849a8621608155dcba05cJeff Sharkey        // Only publish dates reasonably after epoch
193d5a4658cea8def24155849a8621608155dcba05cJeff Sharkey        long lastModified = file.lastModified();
194d5a4658cea8def24155849a8621608155dcba05cJeff Sharkey        if (lastModified > 31536000000L) {
195d5a4658cea8def24155849a8621608155dcba05cJeff Sharkey            row.add(Document.COLUMN_LAST_MODIFIED, lastModified);
196d5a4658cea8def24155849a8621608155dcba05cJeff Sharkey        }
1979e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    }
1989e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
1999e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    @Override
200ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
201ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
202ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        for (String rootId : mIdToPath.keySet()) {
203ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            final RootInfo root = mIdToRoot.get(rootId);
204ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            final File path = mIdToPath.get(rootId);
205ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey
206ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            final RowBuilder row = result.newRow();
207b7757a6b32edea62a1a9a803ad83579220f26100Jeff Sharkey            row.add(Root.COLUMN_ROOT_ID, root.rootId);
208b7757a6b32edea62a1a9a803ad83579220f26100Jeff Sharkey            row.add(Root.COLUMN_FLAGS, root.flags);
209b7757a6b32edea62a1a9a803ad83579220f26100Jeff Sharkey            row.add(Root.COLUMN_TITLE, root.title);
210b7757a6b32edea62a1a9a803ad83579220f26100Jeff Sharkey            row.add(Root.COLUMN_DOCUMENT_ID, root.docId);
211b7757a6b32edea62a1a9a803ad83579220f26100Jeff Sharkey            row.add(Root.COLUMN_AVAILABLE_BYTES, path.getFreeSpace());
2129e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        }
213ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        return result;
2149e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    }
2159e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
216aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    @Override
217aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    public String createDocument(String docId, String mimeType, String displayName)
218aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            throws FileNotFoundException {
219aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        final File parent = getFileForDocId(docId);
2205545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey        File file;
221aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey
222ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        if (Document.MIME_TYPE_DIR.equals(mimeType)) {
2235545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            file = new File(parent, displayName);
224aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            if (!file.mkdir()) {
225aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                throw new IllegalStateException("Failed to mkdir " + file);
226aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            }
22720d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey        } else {
2285545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            displayName = removeExtension(mimeType, displayName);
2295545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            file = new File(parent, addExtension(mimeType, displayName));
2305545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey
2315545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            // If conflicting file, try adding counter suffix
2325545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            int n = 0;
2335545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            while (file.exists() && n++ < 32) {
2345545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey                file = new File(parent, addExtension(mimeType, displayName + " (" + n + ")"));
2355545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            }
2365545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey
237aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            try {
238aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                if (!file.createNewFile()) {
239aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                    throw new IllegalStateException("Failed to touch " + file);
240aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                }
241aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            } catch (IOException e) {
242aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                throw new IllegalStateException("Failed to touch " + file + ": " + e);
243aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            }
2449e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        }
245aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        return getDocIdForFile(file);
24620d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey    }
2479e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
248aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    @Override
249aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    public void deleteDocument(String docId) throws FileNotFoundException {
250aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        final File file = getFileForDocId(docId);
251aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        if (!file.delete()) {
252aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            throw new IllegalStateException("Failed to delete " + file);
2539e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey        }
2549e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    }
2559e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
2569e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    @Override
257ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    public Cursor queryDocument(String documentId, String[] projection)
258ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            throws FileNotFoundException {
259ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
260ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        includeFile(result, documentId, null);
261aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        return result;
262aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    }
263aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey
264aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    @Override
265ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    public Cursor queryChildDocuments(
266ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            String parentDocumentId, String[] projection, String sortOrder)
267ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            throws FileNotFoundException {
268ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
269ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        final File parent = getFileForDocId(parentDocumentId);
270aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        for (File file : parent.listFiles()) {
271aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            includeFile(result, null, file);
2726398343e83b3fd11dd6536cf6f390a52c1e19d2eJeff Sharkey        }
273aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        return result;
274aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    }
2756398343e83b3fd11dd6536cf6f390a52c1e19d2eJeff Sharkey
276aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    @Override
2773e1189b3590aefb65a2af720ae2ba959bbd4188dJeff Sharkey    public Cursor querySearchDocuments(String rootId, String query, String[] projection)
278ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            throws FileNotFoundException {
279ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
2803e1189b3590aefb65a2af720ae2ba959bbd4188dJeff Sharkey        final File parent = mIdToPath.get(rootId);
281aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey
282aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        final LinkedList<File> pending = new LinkedList<File>();
283aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        pending.add(parent);
2844ec973925fc2cd18f9ec0d0ca5af588564fded27Jeff Sharkey        while (!pending.isEmpty() && result.getCount() < 24) {
285aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            final File file = pending.removeFirst();
286aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            if (file.isDirectory()) {
287aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                for (File child : file.listFiles()) {
288aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                    pending.add(child);
289aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                }
2904ec973925fc2cd18f9ec0d0ca5af588564fded27Jeff Sharkey            }
2914ec973925fc2cd18f9ec0d0ca5af588564fded27Jeff Sharkey            if (file.getName().toLowerCase().contains(query)) {
2924ec973925fc2cd18f9ec0d0ca5af588564fded27Jeff Sharkey                includeFile(result, null, file);
2936398343e83b3fd11dd6536cf6f390a52c1e19d2eJeff Sharkey            }
2946398343e83b3fd11dd6536cf6f390a52c1e19d2eJeff Sharkey        }
295aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        return result;
2966398343e83b3fd11dd6536cf6f390a52c1e19d2eJeff Sharkey    }
2976398343e83b3fd11dd6536cf6f390a52c1e19d2eJeff Sharkey
2986398343e83b3fd11dd6536cf6f390a52c1e19d2eJeff Sharkey    @Override
299ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    public String getDocumentType(String documentId) throws FileNotFoundException {
300ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        final File file = getFileForDocId(documentId);
301aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        return getTypeForFile(file);
302aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    }
30320d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey
304aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    @Override
305ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey    public ParcelFileDescriptor openDocument(
306ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            String documentId, String mode, CancellationSignal signal)
307aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            throws FileNotFoundException {
308ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        final File file = getFileForDocId(documentId);
309eb8c3f93edc826413ff4143284dec01c1061d5ccAdam Lesinski        return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
3109e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    }
3119e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
3129e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    @Override
313aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    public AssetFileDescriptor openDocumentThumbnail(
314ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            String documentId, Point sizeHint, CancellationSignal signal)
315ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            throws FileNotFoundException {
316ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey        final File file = getFileForDocId(documentId);
317c1c8f3f97d344a24bfddcb56a8be05e7e2fabe9eJeff Sharkey        return DocumentsContract.openImageThumbnail(file);
3189e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    }
3199e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey
320aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    private static String getTypeForFile(File file) {
321aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        if (file.isDirectory()) {
322ae9b51bfa313c51a31af30875a71255d7b6d2e61Jeff Sharkey            return Document.MIME_TYPE_DIR;
323aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        } else {
324aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            return getTypeForName(file.getName());
325aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        }
326aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    }
327aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey
328aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey    private static String getTypeForName(String name) {
329aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        final int lastDot = name.lastIndexOf('.');
330aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        if (lastDot >= 0) {
331aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            final String extension = name.substring(lastDot + 1);
332aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
333aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey            if (mime != null) {
334aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey                return mime;
33520d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey            }
33620d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey        }
337aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey
338aeb16e2435f9975b9fa1fc4b747796647a21292eJeff Sharkey        return "application/octet-stream";
33920d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey    }
34020d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey
3415545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey    /**
3425545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey     * Remove file extension from name, but only if exact MIME type mapping
3435545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey     * exists. This means we can reapply the extension later.
3445545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey     */
3455545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey    private static String removeExtension(String mimeType, String name) {
3465545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey        final int lastDot = name.lastIndexOf('.');
3475545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey        if (lastDot >= 0) {
3485545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            final String extension = name.substring(lastDot + 1);
3495545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            final String nameMime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
3505545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            if (mimeType.equals(nameMime)) {
3515545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey                return name.substring(0, lastDot);
35220d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey            }
3535545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey        }
3545545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey        return name;
3555545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey    }
35620d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey
3575545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey    /**
3585545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey     * Add file extension to name, but only if exact MIME type mapping exists.
3595545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey     */
3605545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey    private static String addExtension(String mimeType, String name) {
3615545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey        final String extension = MimeTypeMap.getSingleton()
3625545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey                .getExtensionFromMimeType(mimeType);
3635545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey        if (extension != null) {
3645545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey            return name + "." + extension;
36520d96d8aff2193d548977e23ce5158657cac94e0Jeff Sharkey        }
3665545f56f7561810187545a1817b6001dd1f9931bJeff Sharkey        return name;
3679e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey    }
3689e0036ed7d3260d79cc5f9ffd8e3bbe760699924Jeff Sharkey}
369