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