blob: 97dfd47aea9f48192a1b211bbd001ef08f301f9e [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 }
394
Jeff Sharkey21de56a2014-04-05 19:05:24 -0700395 final File parent = getFileForDocId(parentDocId).getCanonicalFile();
396 final File doc = getFileForDocId(docId).getCanonicalFile();
397 return FileUtils.contains(parent, doc);
398 } catch (IOException e) {
399 throw new IllegalArgumentException(
400 "Failed to determine if " + docId + " is child of " + parentDocId + ": " + e);
401 }
402 }
403
404 @Override
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700405 public String createDocument(String docId, String mimeType, String displayName)
406 throws FileNotFoundException {
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800407 displayName = FileUtils.buildValidFatFilename(displayName);
408
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700409 final File parent = getFileForDocId(docId);
Jeff Sharkey21de56a2014-04-05 19:05:24 -0700410 if (!parent.isDirectory()) {
411 throw new IllegalArgumentException("Parent document isn't a directory");
412 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700413
Ben Kwa62539a22015-04-22 15:43:17 -0700414 final File file = FileUtils.buildUniqueFile(parent, mimeType, displayName);
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700415 if (Document.MIME_TYPE_DIR.equals(mimeType)) {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700416 if (!file.mkdir()) {
417 throw new IllegalStateException("Failed to mkdir " + file);
Jeff Sharkeya5599ef2013-08-15 16:17:41 -0700418 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700419 } else {
420 try {
421 if (!file.createNewFile()) {
422 throw new IllegalStateException("Failed to touch " + file);
423 }
424 } catch (IOException e) {
425 throw new IllegalStateException("Failed to touch " + file + ": " + e);
Jeff Sharkeya5599ef2013-08-15 16:17:41 -0700426 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700427 }
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800428
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700429 return getDocIdForFile(file);
430 }
431
432 @Override
Jeff Sharkeyb7e12552014-05-21 22:22:03 -0700433 public String renameDocument(String docId, String displayName) throws FileNotFoundException {
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800434 // Since this provider treats renames as generating a completely new
435 // docId, we're okay with letting the MIME type change.
436 displayName = FileUtils.buildValidFatFilename(displayName);
437
Jeff Sharkeyb7e12552014-05-21 22:22:03 -0700438 final File before = getFileForDocId(docId);
439 final File after = new File(before.getParentFile(), displayName);
440 if (after.exists()) {
441 throw new IllegalStateException("Already exists " + after);
442 }
443 if (!before.renameTo(after)) {
444 throw new IllegalStateException("Failed to rename to " + after);
445 }
446 final String afterDocId = getDocIdForFile(after);
447 if (!TextUtils.equals(docId, afterDocId)) {
448 return afterDocId;
449 } else {
450 return null;
451 }
452 }
453
454 @Override
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700455 public void deleteDocument(String docId) throws FileNotFoundException {
456 final File file = getFileForDocId(docId);
Jeff Sharkey307d4242015-09-24 18:15:13 -0700457 final boolean isDirectory = file.isDirectory();
458 if (isDirectory) {
Jeff Sharkeyb7e12552014-05-21 22:22:03 -0700459 FileUtils.deleteContents(file);
460 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700461 if (!file.delete()) {
462 throw new IllegalStateException("Failed to delete " + file);
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700463 }
Jeff Sharkey307d4242015-09-24 18:15:13 -0700464
465 final ContentResolver resolver = getContext().getContentResolver();
466 final Uri externalUri = MediaStore.Files.getContentUri("external");
467
468 // Remove media store entries for any files inside this directory, using
469 // path prefix match. Logic borrowed from MtpDatabase.
470 if (isDirectory) {
471 final String path = file.getAbsolutePath() + "/";
472 resolver.delete(externalUri,
473 "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
474 new String[] { path + "%", Integer.toString(path.length()), path });
475 }
476
477 // Remove media store entry for this exact file.
478 final String path = file.getAbsolutePath();
479 resolver.delete(externalUri,
480 "_data LIKE ?1 AND lower(_data)=lower(?2)",
481 new String[] { path, path });
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700482 }
483
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700484 @Override
Tomasz Mikolajewskid46ecbc2016-01-25 14:26:54 +0900485 public String moveDocument(String sourceDocumentId, String sourceParentDocumentId,
486 String targetParentDocumentId)
Tomasz Mikolajewski2273e0f2015-11-04 18:24:09 +0900487 throws FileNotFoundException {
488 final File before = getFileForDocId(sourceDocumentId);
489 final File after = new File(getFileForDocId(targetParentDocumentId), before.getName());
490
491 if (after.exists()) {
492 throw new IllegalStateException("Already exists " + after);
493 }
494 if (!before.renameTo(after)) {
495 throw new IllegalStateException("Failed to move to " + after);
496 }
497 return getDocIdForFile(after);
498 }
499
500 @Override
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700501 public Cursor queryDocument(String documentId, String[] projection)
502 throws FileNotFoundException {
Tomasz Mikolajewski68dec402015-11-25 12:54:25 +0900503 if (mArchiveHelper.isArchivedDocument(documentId)) {
504 return mArchiveHelper.queryDocument(documentId, projection);
505 }
506
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700507 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
508 includeFile(result, documentId, null);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700509 return result;
510 }
511
512 @Override
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700513 public Cursor queryChildDocuments(
514 String parentDocumentId, String[] projection, String sortOrder)
515 throws FileNotFoundException {
Tomasz Mikolajewski68dec402015-11-25 12:54:25 +0900516 if (mArchiveHelper.isArchivedDocument(parentDocumentId) ||
517 mArchiveHelper.isSupportedArchiveType(getDocumentType(parentDocumentId))) {
518 return mArchiveHelper.queryChildDocuments(parentDocumentId, projection, sortOrder);
519 }
520
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700521 final File parent = getFileForDocId(parentDocumentId);
Jeff Sharkeydb5ef122013-10-25 17:12:49 -0700522 final MatrixCursor result = new DirectoryCursor(
523 resolveDocumentProjection(projection), parentDocumentId, parent);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700524 for (File file : parent.listFiles()) {
525 includeFile(result, null, file);
526 }
527 return result;
528 }
529
530 @Override
Jeff Sharkey3e1189b2013-09-12 21:59:06 -0700531 public Cursor querySearchDocuments(String rootId, String query, String[] projection)
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700532 throws FileNotFoundException {
533 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700534
535 final File parent;
536 synchronized (mRootsLock) {
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700537 parent = mRoots.get(rootId).path;
Jeff Sharkey1f706c62013-10-17 10:52:17 -0700538 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700539
540 final LinkedList<File> pending = new LinkedList<File>();
541 pending.add(parent);
Jeff Sharkey4ec97392013-09-10 12:04:26 -0700542 while (!pending.isEmpty() && result.getCount() < 24) {
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700543 final File file = pending.removeFirst();
544 if (file.isDirectory()) {
545 for (File child : file.listFiles()) {
546 pending.add(child);
547 }
Jeff Sharkey4ec97392013-09-10 12:04:26 -0700548 }
549 if (file.getName().toLowerCase().contains(query)) {
550 includeFile(result, null, file);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700551 }
552 }
553 return result;
554 }
555
556 @Override
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700557 public String getDocumentType(String documentId) throws FileNotFoundException {
Tomasz Mikolajewski68dec402015-11-25 12:54:25 +0900558 if (mArchiveHelper.isArchivedDocument(documentId)) {
559 return mArchiveHelper.getDocumentType(documentId);
560 }
561
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700562 final File file = getFileForDocId(documentId);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700563 return getTypeForFile(file);
564 }
565
566 @Override
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700567 public ParcelFileDescriptor openDocument(
568 String documentId, String mode, CancellationSignal signal)
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700569 throws FileNotFoundException {
Tomasz Mikolajewski68dec402015-11-25 12:54:25 +0900570 if (mArchiveHelper.isArchivedDocument(documentId)) {
571 return mArchiveHelper.openDocument(documentId, mode, signal);
572 }
573
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700574 final File file = getFileForDocId(documentId);
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700575 final File visibleFile = getFileForDocId(documentId, true);
576
Jeff Sharkeyab1e9bd2014-08-04 15:32:42 -0700577 final int pfdMode = ParcelFileDescriptor.parseMode(mode);
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700578 if (pfdMode == ParcelFileDescriptor.MODE_READ_ONLY || visibleFile == null) {
Jeff Sharkeyab1e9bd2014-08-04 15:32:42 -0700579 return ParcelFileDescriptor.open(file, pfdMode);
580 } else {
581 try {
582 // When finished writing, kick off media scanner
583 return ParcelFileDescriptor.open(file, pfdMode, mHandler, new OnCloseListener() {
584 @Override
585 public void onClose(IOException e) {
586 final Intent intent = new Intent(
587 Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700588 intent.setData(Uri.fromFile(visibleFile));
Jeff Sharkeyab1e9bd2014-08-04 15:32:42 -0700589 getContext().sendBroadcast(intent);
590 }
591 });
592 } catch (IOException e) {
593 throw new FileNotFoundException("Failed to open for writing: " + e);
594 }
595 }
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700596 }
597
598 @Override
599 public AssetFileDescriptor openDocumentThumbnail(
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700600 String documentId, Point sizeHint, CancellationSignal signal)
601 throws FileNotFoundException {
Tomasz Mikolajewski7e326a82015-12-22 11:14:35 +0900602 if (mArchiveHelper.isArchivedDocument(documentId)) {
603 return mArchiveHelper.openDocumentThumbnail(documentId, sizeHint, signal);
604 }
605
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700606 final File file = getFileForDocId(documentId);
Jeff Sharkeyc1c8f3f2013-10-14 14:57:33 -0700607 return DocumentsContract.openImageThumbnail(file);
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700608 }
609
Jeff Sharkey27de30d2015-04-18 16:20:27 -0700610 @Override
611 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
612 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 160);
613 synchronized (mRootsLock) {
614 for (int i = 0; i < mRoots.size(); i++) {
615 final RootInfo root = mRoots.valueAt(i);
616 pw.println("Root{" + root.rootId + "}:");
617 pw.increaseIndent();
618 pw.printPair("flags", DebugUtils.flagsToString(Root.class, "FLAG_", root.flags));
619 pw.println();
620 pw.printPair("title", root.title);
621 pw.printPair("docId", root.docId);
622 pw.println();
623 pw.printPair("path", root.path);
624 pw.printPair("visiblePath", root.visiblePath);
625 pw.decreaseIndent();
626 pw.println();
627 }
628 }
629 }
630
Felipe Lemeb012f912016-01-22 16:49:55 -0800631 @Override
632 public Bundle call(String method, String arg, Bundle extras) {
633 Bundle bundle = super.call(method, arg, extras);
634 if (bundle == null && !TextUtils.isEmpty(method)) {
635 switch (method) {
636 case "getDocIdForFileCreateNewDir": {
637 getContext().enforceCallingPermission(
638 android.Manifest.permission.MANAGE_DOCUMENTS, null);
639 if (TextUtils.isEmpty(arg)) {
640 return null;
641 }
642 try {
643 final String docId = getDocIdForFileMaybeCreate(new File(arg), true);
644 bundle = new Bundle();
645 bundle.putString("DOC_ID", docId);
646 } catch (FileNotFoundException e) {
647 Log.w(TAG, "file '" + arg + "' not found");
648 return null;
649 }
650 break;
651 }
652 default:
653 Log.w(TAG, "unknown method passed to call(): " + method);
654 }
655 }
656 return bundle;
657 }
658
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700659 private static String getTypeForFile(File file) {
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700660 if (file.isDirectory()) {
Jeff Sharkeyae9b51b2013-08-31 15:02:20 -0700661 return Document.MIME_TYPE_DIR;
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700662 } else {
663 return getTypeForName(file.getName());
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700664 }
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700665 }
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700666
Jeff Sharkeyaeb16e22013-08-27 18:26:48 -0700667 private static String getTypeForName(String name) {
Jeff Sharkey20d96d82013-07-30 17:08:39 -0700668 final int lastDot = name.lastIndexOf('.');
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700669 if (lastDot >= 0) {
Jeff Sharkey96c62052013-10-25 16:30:54 -0700670 final String extension = name.substring(lastDot + 1).toLowerCase();
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700671 final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
672 if (mime != null) {
673 return mime;
674 }
675 }
676
677 return "application/octet-stream";
678 }
679
Jeff Sharkeydb5ef122013-10-25 17:12:49 -0700680 private void startObserving(File file, Uri notifyUri) {
681 synchronized (mObservers) {
682 DirectoryObserver observer = mObservers.get(file);
683 if (observer == null) {
684 observer = new DirectoryObserver(
685 file, getContext().getContentResolver(), notifyUri);
686 observer.startWatching();
687 mObservers.put(file, observer);
688 }
689 observer.mRefCount++;
690
691 if (LOG_INOTIFY) Log.d(TAG, "after start: " + observer);
692 }
693 }
694
695 private void stopObserving(File file) {
696 synchronized (mObservers) {
697 DirectoryObserver observer = mObservers.get(file);
698 if (observer == null) return;
699
700 observer.mRefCount--;
701 if (observer.mRefCount == 0) {
702 mObservers.remove(file);
703 observer.stopWatching();
704 }
705
706 if (LOG_INOTIFY) Log.d(TAG, "after stop: " + observer);
707 }
708 }
709
710 private static class DirectoryObserver extends FileObserver {
711 private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO
712 | CREATE | DELETE | DELETE_SELF | MOVE_SELF;
713
714 private final File mFile;
715 private final ContentResolver mResolver;
716 private final Uri mNotifyUri;
717
718 private int mRefCount = 0;
719
720 public DirectoryObserver(File file, ContentResolver resolver, Uri notifyUri) {
721 super(file.getAbsolutePath(), NOTIFY_EVENTS);
722 mFile = file;
723 mResolver = resolver;
724 mNotifyUri = notifyUri;
725 }
726
727 @Override
728 public void onEvent(int event, String path) {
729 if ((event & NOTIFY_EVENTS) != 0) {
730 if (LOG_INOTIFY) Log.d(TAG, "onEvent() " + event + " at " + path);
731 mResolver.notifyChange(mNotifyUri, null, false);
732 }
733 }
734
735 @Override
736 public String toString() {
737 return "DirectoryObserver{file=" + mFile.getAbsolutePath() + ", ref=" + mRefCount + "}";
738 }
739 }
740
741 private class DirectoryCursor extends MatrixCursor {
742 private final File mFile;
743
744 public DirectoryCursor(String[] columnNames, String docId, File file) {
745 super(columnNames);
746
747 final Uri notifyUri = DocumentsContract.buildChildDocumentsUri(
748 AUTHORITY, docId);
749 setNotificationUri(getContext().getContentResolver(), notifyUri);
750
751 mFile = file;
752 startObserving(mFile, notifyUri);
753 }
754
755 @Override
756 public void close() {
757 super.close();
758 stopObserving(mFile);
759 }
760 }
Jeff Sharkey9e0036e2013-04-26 16:54:55 -0700761}