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