Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2013 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package com.android.externalstorage; |
| 18 | |
Jeff Sharkey | db5ef12 | 2013-10-25 17:12:49 -0700 | [diff] [blame] | 19 | import android.content.ContentResolver; |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 20 | import android.content.Context; |
Jeff Sharkey | ab1e9bd | 2014-08-04 15:32:42 -0700 | [diff] [blame] | 21 | import android.content.Intent; |
Jeff Sharkey | 6398343 | 2013-08-21 11:33:50 -0700 | [diff] [blame] | 22 | import android.content.res.AssetFileDescriptor; |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 23 | import android.database.Cursor; |
| 24 | import android.database.MatrixCursor; |
Jeff Sharkey | 9d0843d | 2013-05-07 12:41:33 -0700 | [diff] [blame] | 25 | import android.database.MatrixCursor.RowBuilder; |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 26 | import android.graphics.Point; |
Jeff Sharkey | db5ef12 | 2013-10-25 17:12:49 -0700 | [diff] [blame] | 27 | import android.net.Uri; |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 28 | import android.os.CancellationSignal; |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 29 | import android.os.Environment; |
Jeff Sharkey | db5ef12 | 2013-10-25 17:12:49 -0700 | [diff] [blame] | 30 | import android.os.FileObserver; |
Jeff Sharkey | 21de56a | 2014-04-05 19:05:24 -0700 | [diff] [blame] | 31 | import android.os.FileUtils; |
Jeff Sharkey | ab1e9bd | 2014-08-04 15:32:42 -0700 | [diff] [blame] | 32 | import android.os.Handler; |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 33 | import android.os.ParcelFileDescriptor; |
Jeff Sharkey | ab1e9bd | 2014-08-04 15:32:42 -0700 | [diff] [blame] | 34 | import android.os.ParcelFileDescriptor.OnCloseListener; |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 35 | import android.os.storage.StorageManager; |
| 36 | import android.os.storage.StorageVolume; |
| 37 | import android.provider.DocumentsContract; |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 38 | import android.provider.DocumentsContract.Document; |
| 39 | import android.provider.DocumentsContract.Root; |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 40 | import android.provider.DocumentsProvider; |
Jeff Sharkey | b7e1255 | 2014-05-21 22:22:03 -0700 | [diff] [blame] | 41 | import android.text.TextUtils; |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 42 | import android.util.Log; |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 43 | import android.webkit.MimeTypeMap; |
| 44 | |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 45 | import com.android.internal.annotations.GuardedBy; |
Jeff Sharkey | 0cce535 | 2014-11-26 13:38:26 -0800 | [diff] [blame] | 46 | import com.android.internal.annotations.VisibleForTesting; |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 47 | import com.google.android.collect.Lists; |
Jeff Sharkey | 20d96d8 | 2013-07-30 17:08:39 -0700 | [diff] [blame] | 48 | import com.google.android.collect.Maps; |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 49 | |
| 50 | import java.io.File; |
| 51 | import java.io.FileNotFoundException; |
Jeff Sharkey | 20d96d8 | 2013-07-30 17:08:39 -0700 | [diff] [blame] | 52 | import java.io.IOException; |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 53 | import java.util.ArrayList; |
Jeff Sharkey | 20d96d8 | 2013-07-30 17:08:39 -0700 | [diff] [blame] | 54 | import java.util.HashMap; |
| 55 | import java.util.LinkedList; |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 56 | import java.util.Map; |
Jeff Sharkey | 0cce535 | 2014-11-26 13:38:26 -0800 | [diff] [blame] | 57 | import java.util.Objects; |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 58 | |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 59 | public class ExternalStorageProvider extends DocumentsProvider { |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 60 | private static final String TAG = "ExternalStorage"; |
| 61 | |
Jeff Sharkey | db5ef12 | 2013-10-25 17:12:49 -0700 | [diff] [blame] | 62 | private static final boolean LOG_INOTIFY = false; |
| 63 | |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 64 | public static final String AUTHORITY = "com.android.externalstorage.documents"; |
| 65 | |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 66 | // docId format: root:path/to/file |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 67 | |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 68 | private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { |
Jeff Sharkey | 6efba22 | 2013-09-27 16:44:11 -0700 | [diff] [blame] | 69 | Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE, |
| 70 | Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES, |
Jeff Sharkey | 9d0843d | 2013-05-07 12:41:33 -0700 | [diff] [blame] | 71 | }; |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 72 | |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 73 | private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { |
| 74 | Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, |
| 75 | Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE, |
| 76 | }; |
| 77 | |
| 78 | private static class RootInfo { |
| 79 | public String rootId; |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 80 | public int flags; |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 81 | public String title; |
| 82 | public String docId; |
| 83 | } |
| 84 | |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 85 | private static final String ROOT_ID_PRIMARY_EMULATED = "primary"; |
| 86 | |
| 87 | private StorageManager mStorageManager; |
Jeff Sharkey | ab1e9bd | 2014-08-04 15:32:42 -0700 | [diff] [blame] | 88 | private Handler mHandler; |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 89 | |
| 90 | private final Object mRootsLock = new Object(); |
| 91 | |
| 92 | @GuardedBy("mRootsLock") |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 93 | private ArrayList<RootInfo> mRoots; |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 94 | @GuardedBy("mRootsLock") |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 95 | private HashMap<String, RootInfo> mIdToRoot; |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 96 | @GuardedBy("mRootsLock") |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 97 | private HashMap<String, File> mIdToPath; |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 98 | |
Jeff Sharkey | db5ef12 | 2013-10-25 17:12:49 -0700 | [diff] [blame] | 99 | @GuardedBy("mObservers") |
| 100 | private Map<File, DirectoryObserver> mObservers = Maps.newHashMap(); |
| 101 | |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 102 | @Override |
| 103 | public boolean onCreate() { |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 104 | mStorageManager = (StorageManager) getContext().getSystemService(Context.STORAGE_SERVICE); |
Jeff Sharkey | ab1e9bd | 2014-08-04 15:32:42 -0700 | [diff] [blame] | 105 | mHandler = new Handler(); |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 106 | |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 107 | mRoots = Lists.newArrayList(); |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 108 | mIdToRoot = Maps.newHashMap(); |
| 109 | mIdToPath = Maps.newHashMap(); |
Jeff Sharkey | 20d96d8 | 2013-07-30 17:08:39 -0700 | [diff] [blame] | 110 | |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 111 | updateVolumes(); |
Jeff Sharkey | 20d96d8 | 2013-07-30 17:08:39 -0700 | [diff] [blame] | 112 | |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 113 | return true; |
| 114 | } |
| 115 | |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 116 | public void updateVolumes() { |
| 117 | synchronized (mRootsLock) { |
| 118 | updateVolumesLocked(); |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | private void updateVolumesLocked() { |
| 123 | mRoots.clear(); |
| 124 | mIdToPath.clear(); |
| 125 | mIdToRoot.clear(); |
| 126 | |
| 127 | final StorageVolume[] volumes = mStorageManager.getVolumeList(); |
| 128 | for (StorageVolume volume : volumes) { |
| 129 | final boolean mounted = Environment.MEDIA_MOUNTED.equals(volume.getState()) |
| 130 | || Environment.MEDIA_MOUNTED_READ_ONLY.equals(volume.getState()); |
| 131 | if (!mounted) continue; |
| 132 | |
| 133 | final String rootId; |
| 134 | if (volume.isPrimary() && volume.isEmulated()) { |
| 135 | rootId = ROOT_ID_PRIMARY_EMULATED; |
| 136 | } else if (volume.getUuid() != null) { |
| 137 | rootId = volume.getUuid(); |
| 138 | } else { |
| 139 | Log.d(TAG, "Missing UUID for " + volume.getPath() + "; skipping"); |
| 140 | continue; |
| 141 | } |
| 142 | |
| 143 | if (mIdToPath.containsKey(rootId)) { |
| 144 | Log.w(TAG, "Duplicate UUID " + rootId + "; skipping"); |
| 145 | continue; |
| 146 | } |
| 147 | |
| 148 | try { |
| 149 | final File path = volume.getPathFile(); |
| 150 | mIdToPath.put(rootId, path); |
| 151 | |
| 152 | final RootInfo root = new RootInfo(); |
| 153 | root.rootId = rootId; |
| 154 | root.flags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED |
Jeff Sharkey | b9fbb72 | 2014-06-04 16:42:47 -0700 | [diff] [blame] | 155 | | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD; |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 156 | if (ROOT_ID_PRIMARY_EMULATED.equals(rootId)) { |
| 157 | root.title = getContext().getString(R.string.root_internal_storage); |
| 158 | } else { |
Jeff Sharkey | c99d00b | 2014-10-24 13:57:28 -0700 | [diff] [blame] | 159 | final String userLabel = volume.getUserLabel(); |
| 160 | if (!TextUtils.isEmpty(userLabel)) { |
| 161 | root.title = userLabel; |
| 162 | } else { |
| 163 | root.title = volume.getDescription(getContext()); |
| 164 | } |
Jeff Sharkey | 1407d4c | 2015-04-12 21:52:24 -0700 | [diff] [blame] | 165 | root.flags |= Root.FLAG_HAS_SETTINGS; |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 166 | } |
| 167 | root.docId = getDocIdForFile(path); |
| 168 | mRoots.add(root); |
| 169 | mIdToRoot.put(rootId, root); |
| 170 | } catch (FileNotFoundException e) { |
| 171 | throw new IllegalStateException(e); |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | Log.d(TAG, "After updating volumes, found " + mRoots.size() + " active roots"); |
| 176 | |
| 177 | getContext().getContentResolver() |
| 178 | .notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null, false); |
| 179 | } |
| 180 | |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 181 | private static String[] resolveRootProjection(String[] projection) { |
| 182 | return projection != null ? projection : DEFAULT_ROOT_PROJECTION; |
| 183 | } |
| 184 | |
| 185 | private static String[] resolveDocumentProjection(String[] projection) { |
| 186 | return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION; |
| 187 | } |
| 188 | |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 189 | private String getDocIdForFile(File file) throws FileNotFoundException { |
| 190 | String path = file.getAbsolutePath(); |
Jeff Sharkey | 92d7e69 | 2013-08-02 10:33:21 -0700 | [diff] [blame] | 191 | |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 192 | // Find the most-specific root path |
| 193 | Map.Entry<String, File> mostSpecific = null; |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 194 | synchronized (mRootsLock) { |
| 195 | for (Map.Entry<String, File> root : mIdToPath.entrySet()) { |
| 196 | final String rootPath = root.getValue().getPath(); |
| 197 | if (path.startsWith(rootPath) && (mostSpecific == null |
| 198 | || rootPath.length() > mostSpecific.getValue().getPath().length())) { |
| 199 | mostSpecific = root; |
| 200 | } |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 201 | } |
| 202 | } |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 203 | |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 204 | if (mostSpecific == null) { |
| 205 | throw new FileNotFoundException("Failed to find root that contains " + path); |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 206 | } |
Jeff Sharkey | 20d96d8 | 2013-07-30 17:08:39 -0700 | [diff] [blame] | 207 | |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 208 | // Start at first char of path under root |
| 209 | final String rootPath = mostSpecific.getValue().getPath(); |
| 210 | if (rootPath.equals(path)) { |
| 211 | path = ""; |
| 212 | } else if (rootPath.endsWith("/")) { |
| 213 | path = path.substring(rootPath.length()); |
Jeff Sharkey | 92d7e69 | 2013-08-02 10:33:21 -0700 | [diff] [blame] | 214 | } else { |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 215 | path = path.substring(rootPath.length() + 1); |
Jeff Sharkey | 92d7e69 | 2013-08-02 10:33:21 -0700 | [diff] [blame] | 216 | } |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 217 | |
| 218 | return mostSpecific.getKey() + ':' + path; |
Jeff Sharkey | 20d96d8 | 2013-07-30 17:08:39 -0700 | [diff] [blame] | 219 | } |
| 220 | |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 221 | private File getFileForDocId(String docId) throws FileNotFoundException { |
| 222 | final int splitIndex = docId.indexOf(':', 1); |
| 223 | final String tag = docId.substring(0, splitIndex); |
| 224 | final String path = docId.substring(splitIndex + 1); |
| 225 | |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 226 | File target; |
| 227 | synchronized (mRootsLock) { |
| 228 | target = mIdToPath.get(tag); |
| 229 | } |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 230 | if (target == null) { |
| 231 | throw new FileNotFoundException("No root for " + tag); |
| 232 | } |
Jeff Sharkey | 3e1189b | 2013-09-12 21:59:06 -0700 | [diff] [blame] | 233 | if (!target.exists()) { |
| 234 | target.mkdirs(); |
| 235 | } |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 236 | target = new File(target, path); |
| 237 | if (!target.exists()) { |
| 238 | throw new FileNotFoundException("Missing file for " + docId + " at " + target); |
| 239 | } |
| 240 | return target; |
| 241 | } |
| 242 | |
| 243 | private void includeFile(MatrixCursor result, String docId, File file) |
| 244 | throws FileNotFoundException { |
| 245 | if (docId == null) { |
| 246 | docId = getDocIdForFile(file); |
Jeff Sharkey | 92d7e69 | 2013-08-02 10:33:21 -0700 | [diff] [blame] | 247 | } else { |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 248 | file = getFileForDocId(docId); |
Jeff Sharkey | 92d7e69 | 2013-08-02 10:33:21 -0700 | [diff] [blame] | 249 | } |
Jeff Sharkey | 92d7e69 | 2013-08-02 10:33:21 -0700 | [diff] [blame] | 250 | |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 251 | int flags = 0; |
| 252 | |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 253 | if (file.canWrite()) { |
Jeff Sharkey | 2a030b0 | 2013-09-26 10:54:16 -0700 | [diff] [blame] | 254 | if (file.isDirectory()) { |
| 255 | flags |= Document.FLAG_DIR_SUPPORTS_CREATE; |
Jeff Sharkey | b7e1255 | 2014-05-21 22:22:03 -0700 | [diff] [blame] | 256 | flags |= Document.FLAG_SUPPORTS_DELETE; |
| 257 | flags |= Document.FLAG_SUPPORTS_RENAME; |
Jeff Sharkey | 2a030b0 | 2013-09-26 10:54:16 -0700 | [diff] [blame] | 258 | } else { |
| 259 | flags |= Document.FLAG_SUPPORTS_WRITE; |
Jeff Sharkey | 21de56a | 2014-04-05 19:05:24 -0700 | [diff] [blame] | 260 | flags |= Document.FLAG_SUPPORTS_DELETE; |
Jeff Sharkey | b7e1255 | 2014-05-21 22:22:03 -0700 | [diff] [blame] | 261 | flags |= Document.FLAG_SUPPORTS_RENAME; |
Jeff Sharkey | 2a030b0 | 2013-09-26 10:54:16 -0700 | [diff] [blame] | 262 | } |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 263 | } |
| 264 | |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 265 | final String displayName = file.getName(); |
Jeff Sharkey | 20d96d8 | 2013-07-30 17:08:39 -0700 | [diff] [blame] | 266 | final String mimeType = getTypeForFile(file); |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 267 | if (mimeType.startsWith("image/")) { |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 268 | flags |= Document.FLAG_SUPPORTS_THUMBNAIL; |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 269 | } |
| 270 | |
Jeff Sharkey | 9d0843d | 2013-05-07 12:41:33 -0700 | [diff] [blame] | 271 | final RowBuilder row = result.newRow(); |
Jeff Sharkey | b7757a6 | 2013-09-09 17:46:54 -0700 | [diff] [blame] | 272 | row.add(Document.COLUMN_DOCUMENT_ID, docId); |
| 273 | row.add(Document.COLUMN_DISPLAY_NAME, displayName); |
| 274 | row.add(Document.COLUMN_SIZE, file.length()); |
| 275 | row.add(Document.COLUMN_MIME_TYPE, mimeType); |
Jeff Sharkey | b7757a6 | 2013-09-09 17:46:54 -0700 | [diff] [blame] | 276 | row.add(Document.COLUMN_FLAGS, flags); |
Jeff Sharkey | d5a4658 | 2013-10-11 09:49:03 -0700 | [diff] [blame] | 277 | |
| 278 | // Only publish dates reasonably after epoch |
| 279 | long lastModified = file.lastModified(); |
| 280 | if (lastModified > 31536000000L) { |
| 281 | row.add(Document.COLUMN_LAST_MODIFIED, lastModified); |
| 282 | } |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 283 | } |
| 284 | |
| 285 | @Override |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 286 | public Cursor queryRoots(String[] projection) throws FileNotFoundException { |
| 287 | final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 288 | synchronized (mRootsLock) { |
| 289 | for (String rootId : mIdToPath.keySet()) { |
| 290 | final RootInfo root = mIdToRoot.get(rootId); |
| 291 | final File path = mIdToPath.get(rootId); |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 292 | |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 293 | final RowBuilder row = result.newRow(); |
| 294 | row.add(Root.COLUMN_ROOT_ID, root.rootId); |
| 295 | row.add(Root.COLUMN_FLAGS, root.flags); |
| 296 | row.add(Root.COLUMN_TITLE, root.title); |
| 297 | row.add(Root.COLUMN_DOCUMENT_ID, root.docId); |
| 298 | row.add(Root.COLUMN_AVAILABLE_BYTES, path.getFreeSpace()); |
| 299 | } |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 300 | } |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 301 | return result; |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 302 | } |
| 303 | |
| 304 | @Override |
Jeff Sharkey | 21de56a | 2014-04-05 19:05:24 -0700 | [diff] [blame] | 305 | public boolean isChildDocument(String parentDocId, String docId) { |
| 306 | try { |
| 307 | final File parent = getFileForDocId(parentDocId).getCanonicalFile(); |
| 308 | final File doc = getFileForDocId(docId).getCanonicalFile(); |
| 309 | return FileUtils.contains(parent, doc); |
| 310 | } catch (IOException e) { |
| 311 | throw new IllegalArgumentException( |
| 312 | "Failed to determine if " + docId + " is child of " + parentDocId + ": " + e); |
| 313 | } |
| 314 | } |
| 315 | |
| 316 | @Override |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 317 | public String createDocument(String docId, String mimeType, String displayName) |
| 318 | throws FileNotFoundException { |
Jeff Sharkey | 0cce535 | 2014-11-26 13:38:26 -0800 | [diff] [blame] | 319 | displayName = FileUtils.buildValidFatFilename(displayName); |
| 320 | |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 321 | final File parent = getFileForDocId(docId); |
Jeff Sharkey | 21de56a | 2014-04-05 19:05:24 -0700 | [diff] [blame] | 322 | if (!parent.isDirectory()) { |
| 323 | throw new IllegalArgumentException("Parent document isn't a directory"); |
| 324 | } |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 325 | |
Jeff Sharkey | 0cce535 | 2014-11-26 13:38:26 -0800 | [diff] [blame] | 326 | final File file = buildUniqueFile(parent, mimeType, displayName); |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 327 | if (Document.MIME_TYPE_DIR.equals(mimeType)) { |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 328 | if (!file.mkdir()) { |
| 329 | throw new IllegalStateException("Failed to mkdir " + file); |
Jeff Sharkey | a5599ef | 2013-08-15 16:17:41 -0700 | [diff] [blame] | 330 | } |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 331 | } else { |
| 332 | try { |
| 333 | if (!file.createNewFile()) { |
| 334 | throw new IllegalStateException("Failed to touch " + file); |
| 335 | } |
| 336 | } catch (IOException e) { |
| 337 | throw new IllegalStateException("Failed to touch " + file + ": " + e); |
Jeff Sharkey | a5599ef | 2013-08-15 16:17:41 -0700 | [diff] [blame] | 338 | } |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 339 | } |
Jeff Sharkey | 0cce535 | 2014-11-26 13:38:26 -0800 | [diff] [blame] | 340 | |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 341 | return getDocIdForFile(file); |
| 342 | } |
| 343 | |
Jeff Sharkey | 0cce535 | 2014-11-26 13:38:26 -0800 | [diff] [blame] | 344 | private static File buildFile(File parent, String name, String ext) { |
| 345 | if (TextUtils.isEmpty(ext)) { |
| 346 | return new File(parent, name); |
| 347 | } else { |
| 348 | return new File(parent, name + "." + ext); |
| 349 | } |
| 350 | } |
| 351 | |
| 352 | @VisibleForTesting |
| 353 | public static File buildUniqueFile(File parent, String mimeType, String displayName) |
| 354 | throws FileNotFoundException { |
| 355 | String name; |
| 356 | String ext; |
| 357 | |
| 358 | if (Document.MIME_TYPE_DIR.equals(mimeType)) { |
| 359 | name = displayName; |
| 360 | ext = null; |
| 361 | } else { |
| 362 | String mimeTypeFromExt; |
| 363 | |
| 364 | // Extract requested extension from display name |
| 365 | final int lastDot = displayName.lastIndexOf('.'); |
| 366 | if (lastDot >= 0) { |
| 367 | name = displayName.substring(0, lastDot); |
| 368 | ext = displayName.substring(lastDot + 1); |
| 369 | mimeTypeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension( |
| 370 | ext.toLowerCase()); |
| 371 | } else { |
| 372 | name = displayName; |
| 373 | ext = null; |
| 374 | mimeTypeFromExt = null; |
| 375 | } |
| 376 | |
| 377 | if (mimeTypeFromExt == null) { |
| 378 | mimeTypeFromExt = "application/octet-stream"; |
| 379 | } |
| 380 | |
| 381 | final String extFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType( |
| 382 | mimeType); |
| 383 | if (Objects.equals(mimeType, mimeTypeFromExt) || Objects.equals(ext, extFromMimeType)) { |
| 384 | // Extension maps back to requested MIME type; allow it |
| 385 | } else { |
| 386 | // No match; insist that create file matches requested MIME |
| 387 | name = displayName; |
| 388 | ext = extFromMimeType; |
| 389 | } |
| 390 | } |
| 391 | |
| 392 | File file = buildFile(parent, name, ext); |
| 393 | |
| 394 | // If conflicting file, try adding counter suffix |
| 395 | int n = 0; |
| 396 | while (file.exists()) { |
| 397 | if (n++ >= 32) { |
| 398 | throw new FileNotFoundException("Failed to create unique file"); |
| 399 | } |
| 400 | file = buildFile(parent, name + " (" + n + ")", ext); |
| 401 | } |
| 402 | |
| 403 | return file; |
| 404 | } |
| 405 | |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 406 | @Override |
Jeff Sharkey | b7e1255 | 2014-05-21 22:22:03 -0700 | [diff] [blame] | 407 | public String renameDocument(String docId, String displayName) throws FileNotFoundException { |
Jeff Sharkey | 0cce535 | 2014-11-26 13:38:26 -0800 | [diff] [blame] | 408 | // Since this provider treats renames as generating a completely new |
| 409 | // docId, we're okay with letting the MIME type change. |
| 410 | displayName = FileUtils.buildValidFatFilename(displayName); |
| 411 | |
Jeff Sharkey | b7e1255 | 2014-05-21 22:22:03 -0700 | [diff] [blame] | 412 | final File before = getFileForDocId(docId); |
| 413 | final File after = new File(before.getParentFile(), displayName); |
| 414 | if (after.exists()) { |
| 415 | throw new IllegalStateException("Already exists " + after); |
| 416 | } |
| 417 | if (!before.renameTo(after)) { |
| 418 | throw new IllegalStateException("Failed to rename to " + after); |
| 419 | } |
| 420 | final String afterDocId = getDocIdForFile(after); |
| 421 | if (!TextUtils.equals(docId, afterDocId)) { |
| 422 | return afterDocId; |
| 423 | } else { |
| 424 | return null; |
| 425 | } |
| 426 | } |
| 427 | |
| 428 | @Override |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 429 | public void deleteDocument(String docId) throws FileNotFoundException { |
| 430 | final File file = getFileForDocId(docId); |
Jeff Sharkey | b7e1255 | 2014-05-21 22:22:03 -0700 | [diff] [blame] | 431 | if (file.isDirectory()) { |
| 432 | FileUtils.deleteContents(file); |
| 433 | } |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 434 | if (!file.delete()) { |
| 435 | throw new IllegalStateException("Failed to delete " + file); |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 436 | } |
| 437 | } |
| 438 | |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 439 | @Override |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 440 | public Cursor queryDocument(String documentId, String[] projection) |
| 441 | throws FileNotFoundException { |
| 442 | final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); |
| 443 | includeFile(result, documentId, null); |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 444 | return result; |
| 445 | } |
| 446 | |
| 447 | @Override |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 448 | public Cursor queryChildDocuments( |
| 449 | String parentDocumentId, String[] projection, String sortOrder) |
| 450 | throws FileNotFoundException { |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 451 | final File parent = getFileForDocId(parentDocumentId); |
Jeff Sharkey | db5ef12 | 2013-10-25 17:12:49 -0700 | [diff] [blame] | 452 | final MatrixCursor result = new DirectoryCursor( |
| 453 | resolveDocumentProjection(projection), parentDocumentId, parent); |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 454 | for (File file : parent.listFiles()) { |
| 455 | includeFile(result, null, file); |
| 456 | } |
| 457 | return result; |
| 458 | } |
| 459 | |
| 460 | @Override |
Jeff Sharkey | 3e1189b | 2013-09-12 21:59:06 -0700 | [diff] [blame] | 461 | public Cursor querySearchDocuments(String rootId, String query, String[] projection) |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 462 | throws FileNotFoundException { |
| 463 | final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); |
Jeff Sharkey | 1f706c6 | 2013-10-17 10:52:17 -0700 | [diff] [blame] | 464 | |
| 465 | final File parent; |
| 466 | synchronized (mRootsLock) { |
| 467 | parent = mIdToPath.get(rootId); |
| 468 | } |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 469 | |
| 470 | final LinkedList<File> pending = new LinkedList<File>(); |
| 471 | pending.add(parent); |
Jeff Sharkey | 4ec9739 | 2013-09-10 12:04:26 -0700 | [diff] [blame] | 472 | while (!pending.isEmpty() && result.getCount() < 24) { |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 473 | final File file = pending.removeFirst(); |
| 474 | if (file.isDirectory()) { |
| 475 | for (File child : file.listFiles()) { |
| 476 | pending.add(child); |
| 477 | } |
Jeff Sharkey | 4ec9739 | 2013-09-10 12:04:26 -0700 | [diff] [blame] | 478 | } |
| 479 | if (file.getName().toLowerCase().contains(query)) { |
| 480 | includeFile(result, null, file); |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 481 | } |
| 482 | } |
| 483 | return result; |
| 484 | } |
| 485 | |
| 486 | @Override |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 487 | public String getDocumentType(String documentId) throws FileNotFoundException { |
| 488 | final File file = getFileForDocId(documentId); |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 489 | return getTypeForFile(file); |
| 490 | } |
| 491 | |
| 492 | @Override |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 493 | public ParcelFileDescriptor openDocument( |
| 494 | String documentId, String mode, CancellationSignal signal) |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 495 | throws FileNotFoundException { |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 496 | final File file = getFileForDocId(documentId); |
Jeff Sharkey | ab1e9bd | 2014-08-04 15:32:42 -0700 | [diff] [blame] | 497 | final int pfdMode = ParcelFileDescriptor.parseMode(mode); |
| 498 | if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY) { |
| 499 | return ParcelFileDescriptor.open(file, pfdMode); |
| 500 | } else { |
| 501 | try { |
| 502 | // When finished writing, kick off media scanner |
| 503 | return ParcelFileDescriptor.open(file, pfdMode, mHandler, new OnCloseListener() { |
| 504 | @Override |
| 505 | public void onClose(IOException e) { |
| 506 | final Intent intent = new Intent( |
| 507 | Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); |
| 508 | intent.setData(Uri.fromFile(file)); |
| 509 | getContext().sendBroadcast(intent); |
| 510 | } |
| 511 | }); |
| 512 | } catch (IOException e) { |
| 513 | throw new FileNotFoundException("Failed to open for writing: " + e); |
| 514 | } |
| 515 | } |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 516 | } |
| 517 | |
| 518 | @Override |
| 519 | public AssetFileDescriptor openDocumentThumbnail( |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 520 | String documentId, Point sizeHint, CancellationSignal signal) |
| 521 | throws FileNotFoundException { |
| 522 | final File file = getFileForDocId(documentId); |
Jeff Sharkey | c1c8f3f | 2013-10-14 14:57:33 -0700 | [diff] [blame] | 523 | return DocumentsContract.openImageThumbnail(file); |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 524 | } |
| 525 | |
| 526 | private static String getTypeForFile(File file) { |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 527 | if (file.isDirectory()) { |
Jeff Sharkey | ae9b51b | 2013-08-31 15:02:20 -0700 | [diff] [blame] | 528 | return Document.MIME_TYPE_DIR; |
Jeff Sharkey | 20d96d8 | 2013-07-30 17:08:39 -0700 | [diff] [blame] | 529 | } else { |
| 530 | return getTypeForName(file.getName()); |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 531 | } |
Jeff Sharkey | 20d96d8 | 2013-07-30 17:08:39 -0700 | [diff] [blame] | 532 | } |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 533 | |
Jeff Sharkey | aeb16e2 | 2013-08-27 18:26:48 -0700 | [diff] [blame] | 534 | private static String getTypeForName(String name) { |
Jeff Sharkey | 20d96d8 | 2013-07-30 17:08:39 -0700 | [diff] [blame] | 535 | final int lastDot = name.lastIndexOf('.'); |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 536 | if (lastDot >= 0) { |
Jeff Sharkey | 96c6205 | 2013-10-25 16:30:54 -0700 | [diff] [blame] | 537 | final String extension = name.substring(lastDot + 1).toLowerCase(); |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 538 | final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); |
| 539 | if (mime != null) { |
| 540 | return mime; |
| 541 | } |
| 542 | } |
| 543 | |
| 544 | return "application/octet-stream"; |
| 545 | } |
| 546 | |
Jeff Sharkey | db5ef12 | 2013-10-25 17:12:49 -0700 | [diff] [blame] | 547 | private void startObserving(File file, Uri notifyUri) { |
| 548 | synchronized (mObservers) { |
| 549 | DirectoryObserver observer = mObservers.get(file); |
| 550 | if (observer == null) { |
| 551 | observer = new DirectoryObserver( |
| 552 | file, getContext().getContentResolver(), notifyUri); |
| 553 | observer.startWatching(); |
| 554 | mObservers.put(file, observer); |
| 555 | } |
| 556 | observer.mRefCount++; |
| 557 | |
| 558 | if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer); |
| 559 | } |
| 560 | } |
| 561 | |
| 562 | private void stopObserving(File file) { |
| 563 | synchronized (mObservers) { |
| 564 | DirectoryObserver observer = mObservers.get(file); |
| 565 | if (observer == null) return; |
| 566 | |
| 567 | observer.mRefCount--; |
| 568 | if (observer.mRefCount == 0) { |
| 569 | mObservers.remove(file); |
| 570 | observer.stopWatching(); |
| 571 | } |
| 572 | |
| 573 | if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer); |
| 574 | } |
| 575 | } |
| 576 | |
| 577 | private static class DirectoryObserver extends FileObserver { |
| 578 | private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO |
| 579 | | CREATE | DELETE | DELETE_SELF | MOVE_SELF; |
| 580 | |
| 581 | private final File mFile; |
| 582 | private final ContentResolver mResolver; |
| 583 | private final Uri mNotifyUri; |
| 584 | |
| 585 | private int mRefCount = 0; |
| 586 | |
| 587 | public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) { |
| 588 | super(file.getAbsolutePath(), NOTIFY_EVENTS); |
| 589 | mFile = file; |
| 590 | mResolver = resolver; |
| 591 | mNotifyUri = notifyUri; |
| 592 | } |
| 593 | |
| 594 | @Override |
| 595 | public void onEvent(int event, String path) { |
| 596 | if ((event & NOTIFY_EVENTS) != 0) { |
| 597 | if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path); |
| 598 | mResolver.notifyChange(mNotifyUri, null, false); |
| 599 | } |
| 600 | } |
| 601 | |
| 602 | @Override |
| 603 | public String toString() { |
| 604 | return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}"; |
| 605 | } |
| 606 | } |
| 607 | |
| 608 | private class DirectoryCursor extends MatrixCursor { |
| 609 | private final File mFile; |
| 610 | |
| 611 | public DirectoryCursor(String[] columnNames, String docId, File file) { |
| 612 | super(columnNames); |
| 613 | |
| 614 | final Uri notifyUri = DocumentsContract.buildChildDocumentsUri( |
| 615 | AUTHORITY, docId); |
| 616 | setNotificationUri(getContext().getContentResolver(), notifyUri); |
| 617 | |
| 618 | mFile = file; |
| 619 | startObserving(mFile, notifyUri); |
| 620 | } |
| 621 | |
| 622 | @Override |
| 623 | public void close() { |
| 624 | super.close(); |
| 625 | stopObserving(mFile); |
| 626 | } |
| 627 | } |
Jeff Sharkey | 9e0036e | 2013-04-26 16:54:55 -0700 | [diff] [blame] | 628 | } |