blob: 97527ddf3222d78f3d5835b65b07c14ef24c6ad3 [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;
Felipe Lemeb012f912016-01-22 16:49:55 -080028import android.os.Bundle;
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -070029import android.os.CancellationSignal;
Steve McKay5c462a02016-01-29 16:13:21 -080030import android.os.Environment;
Jeff Sharkeydb5ef122013-10-25 17:12:49 -070031import android.os.FileObserver;
Jeff Sharkey21de56a2014-04-05 19:05:24 -070032import android.os.FileUtils;
Jeff Sharkeyab1e9bd2014-08-04 15:32:42 -070033import android.os.Handler;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070034import android.os.ParcelFileDescriptor;
Jeff Sharkeyab1e9bd2014-08-04 15:32:42 -070035import android.os.ParcelFileDescriptor.OnCloseListener;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070036import android.os.UserHandle;
Jeff Sharkey1f706c62013-10-17 10:52:17 -070037import android.os.storage.StorageManager;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070038import android.os.storage.VolumeInfo;
Jeff Sharkey1f706c62013-10-17 10:52:17 -070039import android.provider.DocumentsContract;
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070040import android.provider.DocumentsContract.Document;
41import android.provider.DocumentsContract.Root;
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -070042import android.provider.DocumentsProvider;
Jeff Sharkey307d4242015-09-24 18:15:13 -070043import android.provider.MediaStore;
Tomasz Mikolajewski68dec402015-11-25 12:54:25 +090044import android.support.provider.DocumentArchiveHelper;
Jeff Sharkeyb7e12552014-05-21 22:22:03 -070045import android.text.TextUtils;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070046import android.util.ArrayMap;
47import android.util.DebugUtils;
Jeff Sharkey1f706c62013-10-17 10:52:17 -070048import android.util.Log;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070049import android.webkit.MimeTypeMap;
50
Jeff Sharkey1f706c62013-10-17 10:52:17 -070051import com.android.internal.annotations.GuardedBy;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070052import com.android.internal.util.IndentingPrintWriter;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070053
54import java.io.File;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070055import java.io.FileDescriptor;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070056import java.io.FileNotFoundException;
Jeff Sharkey20d96d82013-07-30 17:08:39 -070057import java.io.IOException;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070058import java.io.PrintWriter;
Jeff Sharkey20d96d82013-07-30 17:08:39 -070059import java.util.LinkedList;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070060import java.util.List;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070061
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -070062public class ExternalStorageProvider extends DocumentsProvider {
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070063 private static final String TAG = "ExternalStorage";
64
Jeff Sharkeydb5ef122013-10-25 17:12:49 -070065 private static final boolean LOG_INOTIFY = false;
66
Jeff Sharkey1f706c62013-10-17 10:52:17 -070067 public static final String AUTHORITY = "com.android.externalstorage.documents";
68
Makoto Onuki14a6df72015-07-01 14:55:14 -070069 private static final Uri BASE_URI =
70 new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build();
71
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -070072 // docId format: root:path/to/file
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070073
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070074 private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
Jeff Sharkey6efba222013-09-27 16:44:11 -070075 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
76 Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES,
Jeff Sharkey9d0843d2013-05-07 12:41:33 -070077 };
Jeff Sharkey9e0036e2013-04-26 16:54:55 -070078
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070079 private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
80 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
81 Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
82 };
83
84 private static class RootInfo {
85 public String rootId;
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070086 public int flags;
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070087 public String title;
88 public String docId;
Jeff Sharkey27de30d2015-04-18 16:20:27 -070089 public File visiblePath;
90 public File path;
Steve McKayc6a4cd82015-11-18 14:56:50 -080091 public boolean reportAvailableBytes = true;
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -070092 }
93
Jeff Sharkey1f706c62013-10-17 10:52:17 -070094 private static final String ROOT_ID_PRIMARY_EMULATED = "primary";
Steve McKayc6a4cd82015-11-18 14:56:50 -080095 private static final String ROOT_ID_HOME = "home";
Jeff Sharkey1f706c62013-10-17 10:52:17 -070096
97 private StorageManager mStorageManager;
Jeff Sharkeyab1e9bd2014-08-04 15:32:42 -070098 private Handler mHandler;
Tomasz Mikolajewski68dec402015-11-25 12:54:25 +090099 private DocumentArchiveHelper mArchiveHelper;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700100
101 private final Object mRootsLock = new Object();
102
103 @GuardedBy("mRootsLock")
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700104 private ArrayMap<String, RootInfo> mRoots = new ArrayMap<>();
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700105
Jeff Sharkeydb5ef122013-10-25 17:12:49 -0700106 @GuardedBy("mObservers")
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700107 private ArrayMap<File, DirectoryObserver> mObservers = new ArrayMap<>();
Jeff Sharkeydb5ef122013-10-25 17:12:49 -0700108
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700109 @Override
110 public boolean onCreate() {
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700111 mStorageManager = (StorageManager) getContext().getSystemService(Context.STORAGE_SERVICE);
Jeff Sharkeyab1e9bd2014-08-04 15:32:42 -0700112 mHandler = new Handler();
Tomasz Mikolajewski68dec402015-11-25 12:54:25 +0900113 mArchiveHelper = new DocumentArchiveHelper(this, (char) 0);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700114
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700115 updateVolumes();
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700116 return true;
117 }
118
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700119 public void updateVolumes() {
120 synchronized (mRootsLock) {
121 updateVolumesLocked();
122 }
123 }
124
125 private void updateVolumesLocked() {
126 mRoots.clear();
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700127
Steve McKayc6a4cd82015-11-18 14:56:50 -0800128 VolumeInfo primaryVolume = null;
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700129 final int userId = UserHandle.myUserId();
130 final List<VolumeInfo> volumes = mStorageManager.getVolumes();
131 for (VolumeInfo volume : volumes) {
132 if (!volume.isMountedReadable()) continue;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700133
134 final String rootId;
Jeff Sharkeyb521fea2015-06-15 21:09:10 -0700135 final String title;
136 if (volume.getType() == VolumeInfo.TYPE_EMULATED) {
137 // We currently only support a single emulated volume mounted at
138 // a time, and it's always considered the primary
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700139 rootId = ROOT_ID_PRIMARY_EMULATED;
Jeff Sharkeyb521fea2015-06-15 21:09:10 -0700140 if (VolumeInfo.ID_EMULATED_INTERNAL.equals(volume.getId())) {
141 title = getContext().getString(R.string.root_internal_storage);
142 } else {
143 final VolumeInfo privateVol = mStorageManager.findPrivateForEmulated(volume);
144 title = mStorageManager.getBestVolumeDescription(privateVol);
145 }
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700146 } else if (volume.getType() == VolumeInfo.TYPE_PUBLIC) {
147 rootId = volume.getFsUuid();
Jeff Sharkeyb521fea2015-06-15 21:09:10 -0700148 title = mStorageManager.getBestVolumeDescription(volume);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700149 } else {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700150 // Unsupported volume; ignore
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700151 continue;
152 }
153
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700154 if (TextUtils.isEmpty(rootId)) {
155 Log.d(TAG, "Missing UUID for " + volume.getId() + "; skipping");
156 continue;
157 }
158 if (mRoots.containsKey(rootId)) {
159 Log.w(TAG, "Duplicate UUID " + rootId + " for " + volume.getId() + "; skipping");
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700160 continue;
161 }
162
Steve McKayc6a4cd82015-11-18 14:56:50 -0800163 final RootInfo root = new RootInfo();
164 mRoots.put(rootId, root);
165
166 root.rootId = rootId;
Steve McKayefa17612016-01-29 18:15:39 -0800167 root.flags = Root.FLAG_LOCAL_ONLY
Steve McKayc6a4cd82015-11-18 14:56:50 -0800168 | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD;
169
Steve McKayefa17612016-01-29 18:15:39 -0800170 if (volume.isPrimary()) {
171 // save off the primary volume for subsequent "Home" dir initialization.
172 primaryVolume = volume;
173 root.flags |= Root.FLAG_ADVANCED;
174 }
Steve McKayc6a4cd82015-11-18 14:56:50 -0800175 // Dunno when this would NOT be the case, but never hurts to be correct.
176 if (volume.isMountedWritable()) {
177 root.flags |= Root.FLAG_SUPPORTS_CREATE;
178 }
179 root.title = title;
180 if (volume.getType() == VolumeInfo.TYPE_PUBLIC) {
181 root.flags |= Root.FLAG_HAS_SETTINGS;
182 }
183 if (volume.isVisibleForRead(userId)) {
184 root.visiblePath = volume.getPathForUser(userId);
185 } else {
186 root.visiblePath = null;
187 }
188 root.path = volume.getInternalPathForUser(userId);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700189 try {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700190 root.docId = getDocIdForFile(root.path);
Steve McKayc6a4cd82015-11-18 14:56:50 -0800191 } catch (FileNotFoundException e) {
192 throw new IllegalStateException(e);
193 }
194 }
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700195
Steve McKayc6a4cd82015-11-18 14:56:50 -0800196 // Finally, if primary storage is available we add the "Home" directory,
197 // creating it as needed.
198 if (primaryVolume != null && primaryVolume.isVisible()) {
199 final RootInfo root = new RootInfo();
200 root.rootId = ROOT_ID_HOME;
201 mRoots.put(root.rootId, root);
Steve McKayab3b8932016-02-16 11:37:03 -0800202 root.title = getContext().getString(R.string.root_documents);
Steve McKayc6a4cd82015-11-18 14:56:50 -0800203
204 // Only report bytes on *volumes*...as a matter of policy.
205 root.reportAvailableBytes = false;
206 root.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_SEARCH
207 | Root.FLAG_SUPPORTS_IS_CHILD;
208
209 // Dunno when this would NOT be the case, but never hurts to be correct.
210 if (primaryVolume.isMountedWritable()) {
211 root.flags |= Root.FLAG_SUPPORTS_CREATE;
212 }
213
Steve McKay5c462a02016-01-29 16:13:21 -0800214 // Create the "Home" directory on disk, but don't the localized root.title
215 // since the directories shouldn't be localized.
Steve McKayc6a4cd82015-11-18 14:56:50 -0800216 root.visiblePath = new File(
Steve McKayab3b8932016-02-16 11:37:03 -0800217 primaryVolume.getPathForUser(userId), Environment.DIRECTORY_DOCUMENTS);
Steve McKayc6a4cd82015-11-18 14:56:50 -0800218 root.path = new File(
Steve McKayab3b8932016-02-16 11:37:03 -0800219 primaryVolume.getInternalPathForUser(userId), Environment.DIRECTORY_DOCUMENTS);
Steve McKayc6a4cd82015-11-18 14:56:50 -0800220 try {
221 root.docId = getDocIdForFile(root.path);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700222 } catch (FileNotFoundException e) {
223 throw new IllegalStateException(e);
224 }
225 }
226
227 Log.d(TAG, "After updating volumes, found " + mRoots.size() + " active roots");
228
Makoto Onuki14a6df72015-07-01 14:55:14 -0700229 // Note this affects content://com.android.externalstorage.documents/root/39BD-07C5
230 // as well as content://com.android.externalstorage.documents/document/*/children,
231 // so just notify on content://com.android.externalstorage.documents/.
232 getContext().getContentResolver().notifyChange(BASE_URI, null, false);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700233 }
234
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700235 private static String[] resolveRootProjection(String[] projection) {
236 return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
237 }
238
239 private static String[] resolveDocumentProjection(String[] projection) {
240 return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
241 }
242
Felipe Lemeb012f912016-01-22 16:49:55 -0800243
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700244 private String getDocIdForFile(File file) throws FileNotFoundException {
Felipe Lemeb012f912016-01-22 16:49:55 -0800245 return getDocIdForFileMaybeCreate(file, false);
246 }
247
248 private String getDocIdForFileMaybeCreate(File file, boolean createNewDir)
249 throws FileNotFoundException {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700250 String path = file.getAbsolutePath();
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700251
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700252 // Find the most-specific root path
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700253 String mostSpecificId = null;
254 String mostSpecificPath = null;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700255 synchronized (mRootsLock) {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700256 for (int i = 0; i < mRoots.size(); i++) {
257 final String rootId = mRoots.keyAt(i);
258 final String rootPath = mRoots.valueAt(i).path.getAbsolutePath();
259 if (path.startsWith(rootPath) && (mostSpecificPath == null
260 || rootPath.length() > mostSpecificPath.length())) {
261 mostSpecificId = rootId;
262 mostSpecificPath = rootPath;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700263 }
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700264 }
265 }
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700266
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700267 if (mostSpecificPath == null) {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700268 throw new FileNotFoundException("Failed to find root that contains " + path);
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700269 }
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700270
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700271 // Start at first char of path under root
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700272 final String rootPath = mostSpecificPath;
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700273 if (rootPath.equals(path)) {
274 path = "";
275 } else if (rootPath.endsWith("/")) {
276 path = path.substring(rootPath.length());
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700277 } else {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700278 path = path.substring(rootPath.length() + 1);
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700279 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700280
Felipe Lemeb012f912016-01-22 16:49:55 -0800281 if (!file.exists() && createNewDir) {
282 Log.i(TAG, "Creating new directory " + file);
283 if (!file.mkdir()) {
284 Log.e(TAG, "Could not create directory " + file);
285 }
286 }
287
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700288 return mostSpecificId + ':' + path;
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700289 }
290
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700291 private File getFileForDocId(String docId) throws FileNotFoundException {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700292 return getFileForDocId(docId, false);
293 }
294
295 private File getFileForDocId(String docId, boolean visible) throws FileNotFoundException {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700296 final int splitIndex = docId.indexOf(':', 1);
297 final String tag = docId.substring(0, splitIndex);
298 final String path = docId.substring(splitIndex + 1);
299
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700300 RootInfo root;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700301 synchronized (mRootsLock) {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700302 root = mRoots.get(tag);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700303 }
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700304 if (root == null) {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700305 throw new FileNotFoundException("No root for " + tag);
306 }
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700307
308 File target = visible ? root.visiblePath : root.path;
309 if (target == null) {
310 return null;
311 }
Jeff Sharkey3e1189b2013-09-12 21:59:06 -0700312 if (!target.exists()) {
313 target.mkdirs();
314 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700315 target = new File(target, path);
316 if (!target.exists()) {
317 throw new FileNotFoundException("Missing file for " + docId + " at " + target);
318 }
319 return target;
320 }
321
322 private void includeFile(MatrixCursor result, String docId, File file)
323 throws FileNotFoundException {
324 if (docId == null) {
325 docId = getDocIdForFile(file);
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700326 } else {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700327 file = getFileForDocId(docId);
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700328 }
Jeff Sharkey92d7e692013-08-02 10:33:21 -0700329
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700330 int flags = 0;
331
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700332 if (file.canWrite()) {
Jeff Sharkey2a030b02013-09-26 10:54:16 -0700333 if (file.isDirectory()) {
334 flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
Jeff Sharkeyb7e12552014-05-21 22:22:03 -0700335 flags |= Document.FLAG_SUPPORTS_DELETE;
336 flags |= Document.FLAG_SUPPORTS_RENAME;
Tomasz Mikolajewski2273e0f2015-11-04 18:24:09 +0900337 flags |= Document.FLAG_SUPPORTS_MOVE;
Jeff Sharkey2a030b02013-09-26 10:54:16 -0700338 } else {
339 flags |= Document.FLAG_SUPPORTS_WRITE;
Jeff Sharkey21de56a2014-04-05 19:05:24 -0700340 flags |= Document.FLAG_SUPPORTS_DELETE;
Jeff Sharkeyb7e12552014-05-21 22:22:03 -0700341 flags |= Document.FLAG_SUPPORTS_RENAME;
Tomasz Mikolajewski2273e0f2015-11-04 18:24:09 +0900342 flags |= Document.FLAG_SUPPORTS_MOVE;
Jeff Sharkey2a030b02013-09-26 10:54:16 -0700343 }
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700344 }
345
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700346 final String mimeType = getTypeForFile(file);
Tomasz Mikolajewski68dec402015-11-25 12:54:25 +0900347 if (mArchiveHelper.isSupportedArchiveType(mimeType)) {
348 flags |= Document.FLAG_ARCHIVE;
349 }
350
351 final String displayName = file.getName();
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700352 if (mimeType.startsWith("image/")) {
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700353 flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700354 }
355
Jeff Sharkey9d0843d2013-05-07 12:41:33 -0700356 final RowBuilder row = result.newRow();
Jeff Sharkeyb7757a62013-09-09 17:46:54 -0700357 row.add(Document.COLUMN_DOCUMENT_ID, docId);
358 row.add(Document.COLUMN_DISPLAY_NAME, displayName);
359 row.add(Document.COLUMN_SIZE, file.length());
360 row.add(Document.COLUMN_MIME_TYPE, mimeType);
Jeff Sharkeyb7757a62013-09-09 17:46:54 -0700361 row.add(Document.COLUMN_FLAGS, flags);
Tomasz Mikolajewski68dec402015-11-25 12:54:25 +0900362 row.add(DocumentArchiveHelper.COLUMN_LOCAL_FILE_PATH, file.getPath());
Jeff Sharkeyd5a46582013-10-11 09:49:03 -0700363
364 // Only publish dates reasonably after epoch
365 long lastModified = file.lastModified();
366 if (lastModified > 31536000000L) {
367 row.add(Document.COLUMN_LAST_MODIFIED, lastModified);
368 }
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700369 }
370
371 @Override
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700372 public Cursor queryRoots(String[] projection) throws FileNotFoundException {
373 final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700374 synchronized (mRootsLock) {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700375 for (RootInfo root : mRoots.values()) {
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700376 final RowBuilder row = result.newRow();
377 row.add(Root.COLUMN_ROOT_ID, root.rootId);
378 row.add(Root.COLUMN_FLAGS, root.flags);
379 row.add(Root.COLUMN_TITLE, root.title);
380 row.add(Root.COLUMN_DOCUMENT_ID, root.docId);
Steve McKayc6a4cd82015-11-18 14:56:50 -0800381 row.add(Root.COLUMN_AVAILABLE_BYTES,
382 root.reportAvailableBytes ? root.path.getFreeSpace() : -1);
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700383 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700384 }
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700385 return result;
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700386 }
387
388 @Override
Jeff Sharkey21de56a2014-04-05 19:05:24 -0700389 public boolean isChildDocument(String parentDocId, String docId) {
390 try {
Tomasz Mikolajewski68dec402015-11-25 12:54:25 +0900391 if (mArchiveHelper.isArchivedDocument(docId)) {
392 return mArchiveHelper.isChildDocument(parentDocId, docId);
393 }
Tomasz Mikolajewskie192cb72016-03-01 10:36:10 +0900394 // Archives do not contain regular files.
395 if (mArchiveHelper.isArchivedDocument(parentDocId)) {
396 return false;
397 }
Tomasz Mikolajewski68dec402015-11-25 12:54:25 +0900398
Jeff Sharkey21de56a2014-04-05 19:05:24 -0700399 final File parent = getFileForDocId(parentDocId).getCanonicalFile();
400 final File doc = getFileForDocId(docId).getCanonicalFile();
401 return FileUtils.contains(parent, doc);
402 } catch (IOException e) {
403 throw new IllegalArgumentException(
404 "Failed to determine if " + docId + " is child of " + parentDocId + ": " + e);
405 }
406 }
407
408 @Override
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700409 public String createDocument(String docId, String mimeType, String displayName)
410 throws FileNotFoundException {
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800411 displayName = FileUtils.buildValidFatFilename(displayName);
412
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700413 final File parent = getFileForDocId(docId);
Jeff Sharkey21de56a2014-04-05 19:05:24 -0700414 if (!parent.isDirectory()) {
415 throw new IllegalArgumentException("Parent document isn't a directory");
416 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700417
Ben Kwa62539a22015-04-22 15:43:17 -0700418 final File file = FileUtils.buildUniqueFile(parent, mimeType, displayName);
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700419 if (Document.MIME_TYPE_DIR.equals(mimeType)) {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700420 if (!file.mkdir()) {
421 throw new IllegalStateException("Failed to mkdir " + file);
Jeff Sharkeya5599ef2013-08-15 16:17:41 -0700422 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700423 } else {
424 try {
425 if (!file.createNewFile()) {
426 throw new IllegalStateException("Failed to touch " + file);
427 }
428 } catch (IOException e) {
429 throw new IllegalStateException("Failed to touch " + file + ": " + e);
Jeff Sharkeya5599ef2013-08-15 16:17:41 -0700430 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700431 }
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800432
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700433 return getDocIdForFile(file);
434 }
435
436 @Override
Jeff Sharkeyb7e12552014-05-21 22:22:03 -0700437 public String renameDocument(String docId, String displayName) throws FileNotFoundException {
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800438 // Since this provider treats renames as generating a completely new
439 // docId, we're okay with letting the MIME type change.
440 displayName = FileUtils.buildValidFatFilename(displayName);
441
Jeff Sharkeyb7e12552014-05-21 22:22:03 -0700442 final File before = getFileForDocId(docId);
443 final File after = new File(before.getParentFile(), displayName);
444 if (after.exists()) {
445 throw new IllegalStateException("Already exists " + after);
446 }
447 if (!before.renameTo(after)) {
448 throw new IllegalStateException("Failed to rename to " + after);
449 }
450 final String afterDocId = getDocIdForFile(after);
451 if (!TextUtils.equals(docId, afterDocId)) {
452 return afterDocId;
453 } else {
454 return null;
455 }
456 }
457
458 @Override
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700459 public void deleteDocument(String docId) throws FileNotFoundException {
460 final File file = getFileForDocId(docId);
Jeff Sharkey307d4242015-09-24 18:15:13 -0700461 final boolean isDirectory = file.isDirectory();
462 if (isDirectory) {
Jeff Sharkeyb7e12552014-05-21 22:22:03 -0700463 FileUtils.deleteContents(file);
464 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700465 if (!file.delete()) {
466 throw new IllegalStateException("Failed to delete " + file);
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700467 }
Jeff Sharkey307d4242015-09-24 18:15:13 -0700468
469 final ContentResolver resolver = getContext().getContentResolver();
470 final Uri externalUri = MediaStore.Files.getContentUri("external");
471
472 // Remove media store entries for any files inside this directory, using
473 // path prefix match. Logic borrowed from MtpDatabase.
474 if (isDirectory) {
475 final String path = file.getAbsolutePath() + "/";
476 resolver.delete(externalUri,
477 "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
478 new String[] { path + "%", Integer.toString(path.length()), path });
479 }
480
481 // Remove media store entry for this exact file.
482 final String path = file.getAbsolutePath();
483 resolver.delete(externalUri,
484 "_data LIKE ?1 AND lower(_data)=lower(?2)",
485 new String[] { path, path });
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700486 }
487
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700488 @Override
Tomasz Mikolajewskid46ecbc2016-01-25 14:26:54 +0900489 public String moveDocument(String sourceDocumentId, String sourceParentDocumentId,
490 String targetParentDocumentId)
Tomasz Mikolajewski2273e0f2015-11-04 18:24:09 +0900491 throws FileNotFoundException {
492 final File before = getFileForDocId(sourceDocumentId);
493 final File after = new File(getFileForDocId(targetParentDocumentId), before.getName());
494
495 if (after.exists()) {
496 throw new IllegalStateException("Already exists " + after);
497 }
498 if (!before.renameTo(after)) {
499 throw new IllegalStateException("Failed to move to " + after);
500 }
501 return getDocIdForFile(after);
502 }
503
504 @Override
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700505 public Cursor queryDocument(String documentId, String[] projection)
506 throws FileNotFoundException {
Tomasz Mikolajewski68dec402015-11-25 12:54:25 +0900507 if (mArchiveHelper.isArchivedDocument(documentId)) {
508 return mArchiveHelper.queryDocument(documentId, projection);
509 }
510
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700511 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
512 includeFile(result, documentId, null);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700513 return result;
514 }
515
516 @Override
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700517 public Cursor queryChildDocuments(
518 String parentDocumentId, String[] projection, String sortOrder)
519 throws FileNotFoundException {
Tomasz Mikolajewski68dec402015-11-25 12:54:25 +0900520 if (mArchiveHelper.isArchivedDocument(parentDocumentId) ||
521 mArchiveHelper.isSupportedArchiveType(getDocumentType(parentDocumentId))) {
522 return mArchiveHelper.queryChildDocuments(parentDocumentId, projection, sortOrder);
523 }
524
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700525 final File parent = getFileForDocId(parentDocumentId);
Jeff Sharkeydb5ef122013-10-25 17:12:49 -0700526 final MatrixCursor result = new DirectoryCursor(
527 resolveDocumentProjection(projection), parentDocumentId, parent);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700528 for (File file : parent.listFiles()) {
529 includeFile(result, null, file);
530 }
531 return result;
532 }
533
534 @Override
Jeff Sharkey3e1189b2013-09-12 21:59:06 -0700535 public Cursor querySearchDocuments(String rootId, String query, String[] projection)
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700536 throws FileNotFoundException {
537 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700538
539 final File parent;
540 synchronized (mRootsLock) {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700541 parent = mRoots.get(rootId).path;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700542 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700543
544 final LinkedList<File> pending = new LinkedList<File>();
545 pending.add(parent);
Jeff Sharkey4ec97392013-09-10 12:04:26 -0700546 while (!pending.isEmpty() && result.getCount() < 24) {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700547 final File file = pending.removeFirst();
548 if (file.isDirectory()) {
549 for (File child : file.listFiles()) {
550 pending.add(child);
551 }
Jeff Sharkey4ec97392013-09-10 12:04:26 -0700552 }
553 if (file.getName().toLowerCase().contains(query)) {
554 includeFile(result, null, file);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700555 }
556 }
557 return result;
558 }
559
560 @Override
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700561 public String getDocumentType(String documentId) throws FileNotFoundException {
Tomasz Mikolajewski68dec402015-11-25 12:54:25 +0900562 if (mArchiveHelper.isArchivedDocument(documentId)) {
563 return mArchiveHelper.getDocumentType(documentId);
564 }
565
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700566 final File file = getFileForDocId(documentId);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700567 return getTypeForFile(file);
568 }
569
570 @Override
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700571 public ParcelFileDescriptor openDocument(
572 String documentId, String mode, CancellationSignal signal)
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700573 throws FileNotFoundException {
Tomasz Mikolajewski68dec402015-11-25 12:54:25 +0900574 if (mArchiveHelper.isArchivedDocument(documentId)) {
575 return mArchiveHelper.openDocument(documentId, mode, signal);
576 }
577
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700578 final File file = getFileForDocId(documentId);
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700579 final File visibleFile = getFileForDocId(documentId, true);
580
Jeff Sharkeyab1e9bd2014-08-04 15:32:42 -0700581 final int pfdMode = ParcelFileDescriptor.parseMode(mode);
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700582 if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY || visibleFile == null) {
Jeff Sharkeyab1e9bd2014-08-04 15:32:42 -0700583 return ParcelFileDescriptor.open(file, pfdMode);
584 } else {
585 try {
586 // When finished writing, kick off media scanner
587 return ParcelFileDescriptor.open(file, pfdMode, mHandler, new OnCloseListener() {
588 @Override
589 public void onClose(IOException e) {
590 final Intent intent = new Intent(
591 Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700592 intent.setData(Uri.fromFile(visibleFile));
Jeff Sharkeyab1e9bd2014-08-04 15:32:42 -0700593 getContext().sendBroadcast(intent);
594 }
595 });
596 } catch (IOException e) {
597 throw new FileNotFoundException("Failed to open for writing: " + e);
598 }
599 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700600 }
601
602 @Override
603 public AssetFileDescriptor openDocumentThumbnail(
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700604 String documentId, Point sizeHint, CancellationSignal signal)
605 throws FileNotFoundException {
Tomasz Mikolajewski7e326a82015-12-22 11:14:35 +0900606 if (mArchiveHelper.isArchivedDocument(documentId)) {
607 return mArchiveHelper.openDocumentThumbnail(documentId, sizeHint, signal);
608 }
609
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700610 final File file = getFileForDocId(documentId);
Jeff Sharkeyc1c8f3f2013-10-14 14:57:33 -0700611 return DocumentsContract.openImageThumbnail(file);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700612 }
613
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700614 @Override
615 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
616 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 160);
617 synchronized (mRootsLock) {
618 for (int i = 0; i < mRoots.size(); i++) {
619 final RootInfo root = mRoots.valueAt(i);
620 pw.println("Root{" + root.rootId + "}:");
621 pw.increaseIndent();
622 pw.printPair("flags", DebugUtils.flagsToString(Root.class, "FLAG_", root.flags));
623 pw.println();
624 pw.printPair("title", root.title);
625 pw.printPair("docId", root.docId);
626 pw.println();
627 pw.printPair("path", root.path);
628 pw.printPair("visiblePath", root.visiblePath);
629 pw.decreaseIndent();
630 pw.println();
631 }
632 }
633 }
634
Felipe Lemeb012f912016-01-22 16:49:55 -0800635 @Override
636 public Bundle call(String method, String arg, Bundle extras) {
637 Bundle bundle = super.call(method, arg, extras);
638 if (bundle == null && !TextUtils.isEmpty(method)) {
639 switch (method) {
640 case "getDocIdForFileCreateNewDir": {
641 getContext().enforceCallingPermission(
642 android.Manifest.permission.MANAGE_DOCUMENTS, null);
643 if (TextUtils.isEmpty(arg)) {
644 return null;
645 }
646 try {
647 final String docId = getDocIdForFileMaybeCreate(new File(arg), true);
648 bundle = new Bundle();
649 bundle.putString("DOC_ID", docId);
650 } catch (FileNotFoundException e) {
651 Log.w(TAG, "file '" + arg + "' not found");
652 return null;
653 }
654 break;
655 }
656 default:
657 Log.w(TAG, "unknown method passed to call(): " + method);
658 }
659 }
660 return bundle;
661 }
662
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700663 private static String getTypeForFile(File file) {
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700664 if (file.isDirectory()) {
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700665 return Document.MIME_TYPE_DIR;
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700666 } else {
667 return getTypeForName(file.getName());
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700668 }
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700669 }
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700670
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700671 private static String getTypeForName(String name) {
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700672 final int lastDot = name.lastIndexOf('.');
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700673 if (lastDot >= 0) {
Jeff Sharkey96c62052013-10-25 16:30:54 -0700674 final String extension = name.substring(lastDot + 1).toLowerCase();
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700675 final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
676 if (mime != null) {
677 return mime;
678 }
679 }
680
681 return "application/octet-stream";
682 }
683
Jeff Sharkeydb5ef122013-10-25 17:12:49 -0700684 private void startObserving(File file, Uri notifyUri) {
685 synchronized (mObservers) {
686 DirectoryObserver observer = mObservers.get(file);
687 if (observer == null) {
688 observer = new DirectoryObserver(
689 file, getContext().getContentResolver(), notifyUri);
690 observer.startWatching();
691 mObservers.put(file, observer);
692 }
693 observer.mRefCount++;
694
695 if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer);
696 }
697 }
698
699 private void stopObserving(File file) {
700 synchronized (mObservers) {
701 DirectoryObserver observer = mObservers.get(file);
702 if (observer == null) return;
703
704 observer.mRefCount--;
705 if (observer.mRefCount == 0) {
706 mObservers.remove(file);
707 observer.stopWatching();
708 }
709
710 if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer);
711 }
712 }
713
714 private static class DirectoryObserver extends FileObserver {
715 private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO
716 | CREATE | DELETE | DELETE_SELF | MOVE_SELF;
717
718 private final File mFile;
719 private final ContentResolver mResolver;
720 private final Uri mNotifyUri;
721
722 private int mRefCount = 0;
723
724 public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) {
725 super(file.getAbsolutePath(), NOTIFY_EVENTS);
726 mFile = file;
727 mResolver = resolver;
728 mNotifyUri = notifyUri;
729 }
730
731 @Override
732 public void onEvent(int event, String path) {
733 if ((event & NOTIFY_EVENTS) != 0) {
734 if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path);
735 mResolver.notifyChange(mNotifyUri, null, false);
736 }
737 }
738
739 @Override
740 public String toString() {
741 return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}";
742 }
743 }
744
745 private class DirectoryCursor extends MatrixCursor {
746 private final File mFile;
747
748 public DirectoryCursor(String[] columnNames, String docId, File file) {
749 super(columnNames);
750
751 final Uri notifyUri = DocumentsContract.buildChildDocumentsUri(
752 AUTHORITY, docId);
753 setNotificationUri(getContext().getContentResolver(), notifyUri);
754
755 mFile = file;
756 startObserving(mFile, notifyUri);
757 }
758
759 @Override
760 public void close() {
761 super.close();
762 stopObserving(mFile);
763 }
764 }
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700765}