blob: 8f7311853e447daea2b369f15c3f43cdbc9ec9e1 [file] [log] [blame]
Jeff Sharkey9e0036e2013-04-26 16:54:55 -07001/*
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
17package com.android.externalstorage;
18
Jeff Sharkeydb5ef122013-10-25 17:12:49 -070019import android.content.ContentResolver;
Jeff Sharkey1f706c62013-10-17 10:52:17 -070020import android.content.Context;
Jeff Sharkeyab1e9bd2014-08-04 15:32:42 -070021import android.content.Intent;
Jeff Sharkey63983432013-08-21 11:33:50 -070022import android.content.res.AssetFileDescriptor;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070023import android.database.Cursor;
24import android.database.MatrixCursor;
Jeff Sharkey9d0843d2013-05-07 12:41:33 -070025import android.database.MatrixCursor.RowBuilder;
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -070026import android.graphics.Point;
Jeff Sharkeydb5ef122013-10-25 17:12:49 -070027import android.net.Uri;
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -070028import android.os.CancellationSignal;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070029import android.os.Environment;
Jeff Sharkeydb5ef122013-10-25 17:12:49 -070030import android.os.FileObserver;
Jeff Sharkey21de56a2014-04-05 19:05:24 -070031import android.os.FileUtils;
Jeff Sharkeyab1e9bd2014-08-04 15:32:42 -070032import android.os.Handler;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070033import android.os.ParcelFileDescriptor;
Jeff Sharkeyab1e9bd2014-08-04 15:32:42 -070034import android.os.ParcelFileDescriptor.OnCloseListener;
Jeff Sharkey1f706c62013-10-17 10:52:17 -070035import android.os.storage.StorageManager;
36import android.os.storage.StorageVolume;
37import android.provider.DocumentsContract;
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070038import android.provider.DocumentsContract.Document;
39import android.provider.DocumentsContract.Root;
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -070040import android.provider.DocumentsProvider;
Jeff Sharkeyb7e12552014-05-21 22:22:03 -070041import android.text.TextUtils;
Jeff Sharkey1f706c62013-10-17 10:52:17 -070042import android.util.Log;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070043import android.webkit.MimeTypeMap;
44
Jeff Sharkey1f706c62013-10-17 10:52:17 -070045import com.android.internal.annotations.GuardedBy;
Jeff Sharkey0cce5352014-11-26 13:38:26 -080046import com.android.internal.annotations.VisibleForTesting;
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -070047import com.google.android.collect.Lists;
Jeff Sharkey20d96d82013-07-30 17:08:39 -070048import com.google.android.collect.Maps;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070049
50import java.io.File;
51import java.io.FileNotFoundException;
Jeff Sharkey20d96d82013-07-30 17:08:39 -070052import java.io.IOException;
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -070053import java.util.ArrayList;
Jeff Sharkey20d96d82013-07-30 17:08:39 -070054import java.util.HashMap;
55import java.util.LinkedList;
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -070056import java.util.Map;
Jeff Sharkey0cce5352014-11-26 13:38:26 -080057import java.util.Objects;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070058
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -070059public class ExternalStorageProvider extends DocumentsProvider {
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070060 private static final String TAG = "ExternalStorage";
61
Jeff Sharkeydb5ef122013-10-25 17:12:49 -070062 private static final boolean LOG_INOTIFY = false;
63
Jeff Sharkey1f706c62013-10-17 10:52:17 -070064 public static final String AUTHORITY = "com.android.externalstorage.documents";
65
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -070066 // docId format: root:path/to/file
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070067
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070068 private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
Jeff Sharkey6efba222013-09-27 16:44:11 -070069 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
70 Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES,
Jeff Sharkey9d0843d2013-05-07 12:41:33 -070071 };
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070072
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070073 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 Sharkeyae9b51b2013-08-31 15:02:20 -070080 public int flags;
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070081 public String title;
82 public String docId;
83 }
84
Jeff Sharkey1f706c62013-10-17 10:52:17 -070085 private static final String ROOT_ID_PRIMARY_EMULATED = "primary";
86
87 private StorageManager mStorageManager;
Jeff Sharkeyab1e9bd2014-08-04 15:32:42 -070088 private Handler mHandler;
Jeff Sharkey1f706c62013-10-17 10:52:17 -070089
90 private final Object mRootsLock = new Object();
91
92 @GuardedBy("mRootsLock")
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070093 private ArrayList<RootInfo> mRoots;
Jeff Sharkey1f706c62013-10-17 10:52:17 -070094 @GuardedBy("mRootsLock")
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070095 private HashMap<String, RootInfo> mIdToRoot;
Jeff Sharkey1f706c62013-10-17 10:52:17 -070096 @GuardedBy("mRootsLock")
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070097 private HashMap<String, File> mIdToPath;
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -070098
Jeff Sharkeydb5ef122013-10-25 17:12:49 -070099 @GuardedBy("mObservers")
100 private Map<File, DirectoryObserver> mObservers = Maps.newHashMap();
101
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700102 @Override
103 public boolean onCreate() {
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700104 mStorageManager = (StorageManager) getContext().getSystemService(Context.STORAGE_SERVICE);
Jeff Sharkeyab1e9bd2014-08-04 15:32:42 -0700105 mHandler = new Handler();
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700106
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700107 mRoots = Lists.newArrayList();
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700108 mIdToRoot = Maps.newHashMap();
109 mIdToPath = Maps.newHashMap();
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700110
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700111 updateVolumes();
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700112
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700113 return true;
114 }
115
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700116 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 Sharkeyb9fbb722014-06-04 16:42:47 -0700155 | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700156 if (ROOT_ID_PRIMARY_EMULATED.equals(rootId)) {
157 root.title = getContext().getString(R.string.root_internal_storage);
158 } else {
Jeff Sharkeyc99d00b2014-10-24 13:57:28 -0700159 final String userLabel = volume.getUserLabel();
160 if (!TextUtils.isEmpty(userLabel)) {
161 root.title = userLabel;
162 } else {
163 root.title = volume.getDescription(getContext());
164 }
Jeff Sharkey1407d4c2015-04-12 21:52:24 -0700165 root.flags |= Root.FLAG_HAS_SETTINGS;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700166 }
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 Sharkeyae9b51b2013-08-31 15:02:20 -0700181 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 Sharkeyaeb16e22013-08-27 18:26:48 -0700189 private String getDocIdForFile(File file) throws FileNotFoundException {
190 String path = file.getAbsolutePath();
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700191
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700192 // Find the most-specific root path
193 Map.Entry<String, File> mostSpecific = null;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700194 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 Sharkey9e0036e2013-04-26 16:54:55 -0700201 }
202 }
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700203
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700204 if (mostSpecific == null) {
205 throw new FileNotFoundException("Failed to find root that contains " + path);
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700206 }
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700207
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700208 // 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 Sharkey92d7e692013-08-02 10:33:21 -0700214 } else {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700215 path = path.substring(rootPath.length() + 1);
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700216 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700217
218 return mostSpecific.getKey() + ':' + path;
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700219 }
220
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700221 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 Sharkey1f706c62013-10-17 10:52:17 -0700226 File target;
227 synchronized (mRootsLock) {
228 target = mIdToPath.get(tag);
229 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700230 if (target == null) {
231 throw new FileNotFoundException("No root for " + tag);
232 }
Jeff Sharkey3e1189b2013-09-12 21:59:06 -0700233 if (!target.exists()) {
234 target.mkdirs();
235 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700236 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 Sharkey92d7e692013-08-02 10:33:21 -0700247 } else {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700248 file = getFileForDocId(docId);
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700249 }
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700250
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700251 int flags = 0;
252
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700253 if (file.canWrite()) {
Jeff Sharkey2a030b02013-09-26 10:54:16 -0700254 if (file.isDirectory()) {
255 flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
Jeff Sharkeyb7e12552014-05-21 22:22:03 -0700256 flags |= Document.FLAG_SUPPORTS_DELETE;
257 flags |= Document.FLAG_SUPPORTS_RENAME;
Jeff Sharkey2a030b02013-09-26 10:54:16 -0700258 } else {
259 flags |= Document.FLAG_SUPPORTS_WRITE;
Jeff Sharkey21de56a2014-04-05 19:05:24 -0700260 flags |= Document.FLAG_SUPPORTS_DELETE;
Jeff Sharkeyb7e12552014-05-21 22:22:03 -0700261 flags |= Document.FLAG_SUPPORTS_RENAME;
Jeff Sharkey2a030b02013-09-26 10:54:16 -0700262 }
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700263 }
264
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700265 final String displayName = file.getName();
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700266 final String mimeType = getTypeForFile(file);
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700267 if (mimeType.startsWith("image/")) {
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700268 flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700269 }
270
Jeff Sharkey9d0843d2013-05-07 12:41:33 -0700271 final RowBuilder row = result.newRow();
Jeff Sharkeyb7757a62013-09-09 17:46:54 -0700272 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 Sharkeyb7757a62013-09-09 17:46:54 -0700276 row.add(Document.COLUMN_FLAGS, flags);
Jeff Sharkeyd5a46582013-10-11 09:49:03 -0700277
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 Sharkey9e0036e2013-04-26 16:54:55 -0700283 }
284
285 @Override
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700286 public Cursor queryRoots(String[] projection) throws FileNotFoundException {
287 final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700288 synchronized (mRootsLock) {
289 for (String rootId : mIdToPath.keySet()) {
290 final RootInfo root = mIdToRoot.get(rootId);
291 final File path = mIdToPath.get(rootId);
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700292
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700293 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 Sharkeyaeb16e22013-08-27 18:26:48 -0700300 }
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700301 return result;
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700302 }
303
304 @Override
Jeff Sharkey21de56a2014-04-05 19:05:24 -0700305 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 Sharkeyaeb16e22013-08-27 18:26:48 -0700317 public String createDocument(String docId, String mimeType, String displayName)
318 throws FileNotFoundException {
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800319 displayName = FileUtils.buildValidFatFilename(displayName);
320
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700321 final File parent = getFileForDocId(docId);
Jeff Sharkey21de56a2014-04-05 19:05:24 -0700322 if (!parent.isDirectory()) {
323 throw new IllegalArgumentException("Parent document isn't a directory");
324 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700325
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800326 final File file = buildUniqueFile(parent, mimeType, displayName);
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700327 if (Document.MIME_TYPE_DIR.equals(mimeType)) {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700328 if (!file.mkdir()) {
329 throw new IllegalStateException("Failed to mkdir " + file);
Jeff Sharkeya5599ef2013-08-15 16:17:41 -0700330 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700331 } 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 Sharkeya5599ef2013-08-15 16:17:41 -0700338 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700339 }
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800340
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700341 return getDocIdForFile(file);
342 }
343
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800344 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 Sharkeyaeb16e22013-08-27 18:26:48 -0700406 @Override
Jeff Sharkeyb7e12552014-05-21 22:22:03 -0700407 public String renameDocument(String docId, String displayName) throws FileNotFoundException {
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800408 // 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 Sharkeyb7e12552014-05-21 22:22:03 -0700412 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 Sharkeyaeb16e22013-08-27 18:26:48 -0700429 public void deleteDocument(String docId) throws FileNotFoundException {
430 final File file = getFileForDocId(docId);
Jeff Sharkeyb7e12552014-05-21 22:22:03 -0700431 if (file.isDirectory()) {
432 FileUtils.deleteContents(file);
433 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700434 if (!file.delete()) {
435 throw new IllegalStateException("Failed to delete " + file);
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700436 }
437 }
438
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700439 @Override
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700440 public Cursor queryDocument(String documentId, String[] projection)
441 throws FileNotFoundException {
442 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
443 includeFile(result, documentId, null);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700444 return result;
445 }
446
447 @Override
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700448 public Cursor queryChildDocuments(
449 String parentDocumentId, String[] projection, String sortOrder)
450 throws FileNotFoundException {
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700451 final File parent = getFileForDocId(parentDocumentId);
Jeff Sharkeydb5ef122013-10-25 17:12:49 -0700452 final MatrixCursor result = new DirectoryCursor(
453 resolveDocumentProjection(projection), parentDocumentId, parent);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700454 for (File file : parent.listFiles()) {
455 includeFile(result, null, file);
456 }
457 return result;
458 }
459
460 @Override
Jeff Sharkey3e1189b2013-09-12 21:59:06 -0700461 public Cursor querySearchDocuments(String rootId, String query, String[] projection)
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700462 throws FileNotFoundException {
463 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700464
465 final File parent;
466 synchronized (mRootsLock) {
467 parent = mIdToPath.get(rootId);
468 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700469
470 final LinkedList<File> pending = new LinkedList<File>();
471 pending.add(parent);
Jeff Sharkey4ec97392013-09-10 12:04:26 -0700472 while (!pending.isEmpty() && result.getCount() < 24) {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700473 final File file = pending.removeFirst();
474 if (file.isDirectory()) {
475 for (File child : file.listFiles()) {
476 pending.add(child);
477 }
Jeff Sharkey4ec97392013-09-10 12:04:26 -0700478 }
479 if (file.getName().toLowerCase().contains(query)) {
480 includeFile(result, null, file);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700481 }
482 }
483 return result;
484 }
485
486 @Override
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700487 public String getDocumentType(String documentId) throws FileNotFoundException {
488 final File file = getFileForDocId(documentId);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700489 return getTypeForFile(file);
490 }
491
492 @Override
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700493 public ParcelFileDescriptor openDocument(
494 String documentId, String mode, CancellationSignal signal)
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700495 throws FileNotFoundException {
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700496 final File file = getFileForDocId(documentId);
Jeff Sharkeyab1e9bd2014-08-04 15:32:42 -0700497 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 Sharkeyaeb16e22013-08-27 18:26:48 -0700516 }
517
518 @Override
519 public AssetFileDescriptor openDocumentThumbnail(
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700520 String documentId, Point sizeHint, CancellationSignal signal)
521 throws FileNotFoundException {
522 final File file = getFileForDocId(documentId);
Jeff Sharkeyc1c8f3f2013-10-14 14:57:33 -0700523 return DocumentsContract.openImageThumbnail(file);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700524 }
525
526 private static String getTypeForFile(File file) {
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700527 if (file.isDirectory()) {
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700528 return Document.MIME_TYPE_DIR;
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700529 } else {
530 return getTypeForName(file.getName());
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700531 }
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700532 }
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700533
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700534 private static String getTypeForName(String name) {
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700535 final int lastDot = name.lastIndexOf('.');
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700536 if (lastDot >= 0) {
Jeff Sharkey96c62052013-10-25 16:30:54 -0700537 final String extension = name.substring(lastDot + 1).toLowerCase();
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700538 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 Sharkeydb5ef122013-10-25 17:12:49 -0700547 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 Sharkey9e0036e2013-04-26 16:54:55 -0700628}