blob: 857235be85b5525e0d68f63ceb03709435508198 [file] [log] [blame]
Tomasz Mikolajewski55194742015-04-08 09:21:08 +09001/*
2 * Copyright (C) 2015 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.documentsui;
18
19import android.content.Context;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070020import android.content.SharedPreferences;
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +090021import android.content.pm.ProviderInfo;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090022import android.content.res.AssetFileDescriptor;
23import android.database.Cursor;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090024import android.database.MatrixCursor;
Ben Kwac06f3fd2015-04-24 15:35:25 -070025import android.database.MatrixCursor.RowBuilder;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090026import android.graphics.Point;
Ben Kwac06f3fd2015-04-24 15:35:25 -070027import android.net.Uri;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070028import android.os.Bundle;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090029import android.os.CancellationSignal;
Tomasz Mikolajewski67f9bed2015-04-08 19:38:55 +090030import android.os.FileUtils;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090031import android.os.ParcelFileDescriptor;
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +090032import android.provider.DocumentsContract;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090033import android.provider.DocumentsContract.Document;
34import android.provider.DocumentsContract.Root;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090035import android.provider.DocumentsProvider;
Ben Kwac06f3fd2015-04-24 15:35:25 -070036import android.support.annotation.VisibleForTesting;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070037import android.util.Log;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090038
Ben Kwaaac9e2e2015-04-16 18:14:35 -070039import libcore.io.IoUtils;
40
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090041import java.io.File;
42import java.io.FileNotFoundException;
Ben Kwac06f3fd2015-04-24 15:35:25 -070043import java.io.FileOutputStream;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090044import java.io.IOException;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070045import java.io.InputStream;
46import java.io.OutputStream;
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +090047import java.util.ArrayList;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070048import java.util.Arrays;
49import java.util.Collection;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090050import java.util.HashMap;
Steve McKaybbeba522016-01-13 17:17:39 -080051import java.util.HashSet;
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +090052import java.util.List;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070053import java.util.Map;
Steve McKaybbeba522016-01-13 17:17:39 -080054import java.util.Set;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090055
56public class StubProvider extends DocumentsProvider {
Steve McKay99bcc6a2015-10-26 17:03:55 -070057
58 public static final String DEFAULT_AUTHORITY = "com.android.documentsui.stubprovider";
59 public static final String ROOT_0_ID = "TEST_ROOT_0";
60 public static final String ROOT_1_ID = "TEST_ROOT_1";
61
Steve McKaybbeba522016-01-13 17:17:39 -080062 public static final String EXTRA_SIZE = "com.android.documentsui.stubprovider.SIZE";
63 public static final String EXTRA_ROOT = "com.android.documentsui.stubprovider.ROOT";
64 public static final String EXTRA_PATH = "com.android.documentsui.stubprovider.PATH";
65 public static final String EXTRA_STREAM_TYPES
66 = "com.android.documentsui.stubprovider.STREAM_TYPES";
67 public static final String EXTRA_CONTENT = "com.android.documentsui.stubprovider.CONTENT";
68
Steve McKay99bcc6a2015-10-26 17:03:55 -070069 private static final String TAG = "StubProvider";
Steve McKaybbeba522016-01-13 17:17:39 -080070
Ben Kwaaac9e2e2015-04-16 18:14:35 -070071 private static final String STORAGE_SIZE_KEY = "documentsui.stubprovider.size";
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +090072 private static int DEFAULT_ROOT_SIZE = 1024 * 1024 * 100; // 100 MB.
Steve McKay99bcc6a2015-10-26 17:03:55 -070073
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090074 private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
75 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
76 Root.COLUMN_AVAILABLE_BYTES
77 };
78 private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
79 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
80 Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
81 };
82
Steve McKay99bcc6a2015-10-26 17:03:55 -070083 private final Map<String, StubDocument> mStorage = new HashMap<>();
84 private final Map<String, RootInfo> mRoots = new HashMap<>();
85 private final Object mWriteLock = new Object();
86
87 private String mAuthority = DEFAULT_AUTHORITY;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070088 private SharedPreferences mPrefs;
Steve McKaybbeba522016-01-13 17:17:39 -080089 private Set<String> mSimulateReadErrorIds = new HashSet<>();
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +090090
91 @Override
92 public void attachInfo(Context context, ProviderInfo info) {
93 mAuthority = info.authority;
94 super.attachInfo(context, info);
95 }
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090096
97 @Override
98 public boolean onCreate() {
Ben Kwaaac9e2e2015-04-16 18:14:35 -070099 clearCacheAndBuildRoots();
100 return true;
101 }
102
Ben Kwac06f3fd2015-04-24 15:35:25 -0700103 @VisibleForTesting
104 public void clearCacheAndBuildRoots() {
Steve McKay99bcc6a2015-10-26 17:03:55 -0700105 Log.d(TAG, "Resetting storage.");
106 removeChildrenRecursively(getContext().getCacheDir());
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700107 mStorage.clear();
Steve McKaybbeba522016-01-13 17:17:39 -0800108 mSimulateReadErrorIds.clear();
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700109
110 mPrefs = getContext().getSharedPreferences(
111 "com.android.documentsui.stubprovider.preferences", Context.MODE_PRIVATE);
112 Collection<String> rootIds = mPrefs.getStringSet("roots", null);
113 if (rootIds == null) {
Steve McKay99bcc6a2015-10-26 17:03:55 -0700114 rootIds = Arrays.asList(new String[] { ROOT_0_ID, ROOT_1_ID });
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700115 }
Steve McKay99bcc6a2015-10-26 17:03:55 -0700116
117 mRoots.clear();
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700118 for (String rootId : rootIds) {
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900119 // Make a subdir in the cache dir for each root.
120 final File file = new File(getContext().getCacheDir(), rootId);
121 if (file.mkdir()) {
122 Log.i(TAG, "Created new root directory @ " + file.getPath());
123 }
124 final RootInfo rootInfo = new RootInfo(file, getSize(rootId));
125 mStorage.put(rootInfo.document.documentId, rootInfo.document);
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700126 mRoots.put(rootId, rootInfo);
127 }
128 }
129
130 /**
131 * @return Storage size, in bytes.
132 */
133 private long getSize(String rootId) {
134 final String key = STORAGE_SIZE_KEY + "." + rootId;
Steve McKay99bcc6a2015-10-26 17:03:55 -0700135 return mPrefs.getLong(key, DEFAULT_ROOT_SIZE);
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900136 }
137
138 @Override
139 public Cursor queryRoots(String[] projection) throws FileNotFoundException {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700140 final MatrixCursor result = new MatrixCursor(projection != null ? projection
141 : DEFAULT_ROOT_PROJECTION);
142 for (Map.Entry<String, RootInfo> entry : mRoots.entrySet()) {
143 final String id = entry.getKey();
144 final RootInfo info = entry.getValue();
145 final RowBuilder row = result.newRow();
146 row.add(Root.COLUMN_ROOT_ID, id);
147 row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD);
148 row.add(Root.COLUMN_TITLE, id);
Steve McKay99bcc6a2015-10-26 17:03:55 -0700149 row.add(Root.COLUMN_DOCUMENT_ID, info.document.documentId);
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700150 row.add(Root.COLUMN_AVAILABLE_BYTES, info.getRemainingCapacity());
151 }
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900152 return result;
153 }
154
155 @Override
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700156 public Cursor queryDocument(String documentId, String[] projection)
157 throws FileNotFoundException {
158 final MatrixCursor result = new MatrixCursor(projection != null ? projection
159 : DEFAULT_DOCUMENT_PROJECTION);
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900160 final StubDocument file = mStorage.get(documentId);
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900161 if (file == null) {
162 throw new FileNotFoundException();
163 }
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900164 includeDocument(result, file);
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900165 return result;
166 }
167
168 @Override
Tomasz Mikolajewski67f9bed2015-04-08 19:38:55 +0900169 public boolean isChildDocument(String parentDocId, String docId) {
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900170 final StubDocument parentDocument = mStorage.get(parentDocId);
171 final StubDocument childDocument = mStorage.get(docId);
172 return FileUtils.contains(parentDocument.file, childDocument.file);
Tomasz Mikolajewski67f9bed2015-04-08 19:38:55 +0900173 }
174
175 @Override
Steve McKay99bcc6a2015-10-26 17:03:55 -0700176 public String createDocument(String parentId, String mimeType, String displayName)
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900177 throws FileNotFoundException {
Steve McKay99bcc6a2015-10-26 17:03:55 -0700178
179 final StubDocument parent = mStorage.get(parentId);
180 if (parent == null) {
181 throw new IllegalArgumentException(
182 "Can't create file " + displayName + " in null parent.");
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900183 }
Steve McKay99bcc6a2015-10-26 17:03:55 -0700184 if (!parent.file.isDirectory()) {
185 throw new IllegalArgumentException(
186 "Can't create file " + displayName + " inside non-directory parent "
187 + parent.file.getName());
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900188 }
189
Steve McKay99bcc6a2015-10-26 17:03:55 -0700190 final File file = new File(parent.file, displayName);
191 if (file.exists()) {
192 throw new FileNotFoundException(
193 "Duplicate file names not supported for " + file);
194 }
195
196 if (mimeType.equals(Document.MIME_TYPE_DIR)) {
197 if (!file.mkdirs()) {
198 throw new FileNotFoundException(
199 "Failed to create directory(s): " + file);
200 }
201 Log.i(TAG, "Created new directory: " + file);
202 } else {
203 boolean created = false;
204 try {
205 created = file.createNewFile();
206 } catch (IOException e) {
207 // We'll throw an FNF exception later :)
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900208 Log.e(TAG, "createNewFile operation failed for file: " + file, e);
Steve McKay99bcc6a2015-10-26 17:03:55 -0700209 }
210 if (!created) {
211 throw new FileNotFoundException(
212 "createNewFile operation failed for: " + file);
213 }
214 Log.i(TAG, "Created new file: " + file);
215 }
216
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900217 final StubDocument document = StubDocument.createRegularDocument(file, mimeType, parent);
218 mStorage.put(document.documentId, document);
Ben Kwacb4461f2015-05-05 11:50:11 -0700219 Log.d(TAG, "Created document " + document.documentId);
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900220 notifyParentChanged(document.parentId);
Ben Kwac06f3fd2015-04-24 15:35:25 -0700221 getContext().getContentResolver().notifyChange(
222 DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
223 null, false);
224
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900225 return document.documentId;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900226 }
227
228 @Override
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +0900229 public void deleteDocument(String documentId)
230 throws FileNotFoundException {
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900231 final StubDocument document = mStorage.get(documentId);
232 final long fileSize = document.file.length();
233 if (document == null || !document.file.delete())
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +0900234 throw new FileNotFoundException();
235 synchronized (mWriteLock) {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700236 document.rootInfo.size -= fileSize;
Ben Kwacb4461f2015-05-05 11:50:11 -0700237 mStorage.remove(documentId);
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +0900238 }
Ben Kwacb4461f2015-05-05 11:50:11 -0700239 Log.d(TAG, "Document deleted: " + documentId);
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900240 notifyParentChanged(document.parentId);
Ben Kwac06f3fd2015-04-24 15:35:25 -0700241 getContext().getContentResolver().notifyChange(
242 DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
243 null, false);
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +0900244 }
245
246 @Override
Steve McKay31e104f2015-12-02 11:20:54 -0800247 public Cursor queryChildDocumentsForManage(String parentDocumentId, String[] projection,
248 String sortOrder) throws FileNotFoundException {
249 return queryChildDocuments(parentDocumentId, projection, sortOrder);
250 }
251
252 @Override
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900253 public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
254 throws FileNotFoundException {
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900255 final StubDocument parentDocument = mStorage.get(parentDocumentId);
256 if (parentDocument == null || parentDocument.file.isFile()) {
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900257 throw new FileNotFoundException();
258 }
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700259 final MatrixCursor result = new MatrixCursor(projection != null ? projection
260 : DEFAULT_DOCUMENT_PROJECTION);
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900261 result.setNotificationUri(getContext().getContentResolver(),
262 DocumentsContract.buildChildDocumentsUri(mAuthority, parentDocumentId));
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900263 StubDocument document;
264 for (File file : parentDocument.file.listFiles()) {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700265 document = mStorage.get(getDocumentIdForFile(file));
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900266 if (document != null) {
267 includeDocument(result, document);
268 }
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900269 }
270 return result;
271 }
272
273 @Override
274 public Cursor queryRecentDocuments(String rootId, String[] projection)
275 throws FileNotFoundException {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700276 final MatrixCursor result = new MatrixCursor(projection != null ? projection
277 : DEFAULT_DOCUMENT_PROJECTION);
278 return result;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900279 }
280
281 @Override
282 public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
283 throws FileNotFoundException {
Steve McKaybbeba522016-01-13 17:17:39 -0800284
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900285 final StubDocument document = mStorage.get(docId);
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900286 if (document == null || !document.file.isFile()) {
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900287 throw new FileNotFoundException();
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900288 }
289 if ((document.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0) {
290 throw new IllegalStateException("Tried to open a virtual file.");
291 }
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900292
293 if ("r".equals(mode)) {
Steve McKaybbeba522016-01-13 17:17:39 -0800294 if (mSimulateReadErrorIds.contains(docId)) {
295 Log.d(TAG, "Simulated errs enabled. Open in the wrong mode.");
296 return ParcelFileDescriptor.open(
297 document.file, ParcelFileDescriptor.MODE_WRITE_ONLY);
Ben Kwac06f3fd2015-04-24 15:35:25 -0700298 }
Steve McKaybbeba522016-01-13 17:17:39 -0800299 return ParcelFileDescriptor.open(document.file, ParcelFileDescriptor.MODE_READ_ONLY);
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900300 }
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900301 if ("w".equals(mode)) {
302 return startWrite(document);
303 }
304
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900305 throw new FileNotFoundException();
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900306 }
307
Ben Kwac06f3fd2015-04-24 15:35:25 -0700308 @VisibleForTesting
Ben Kwacb4461f2015-05-05 11:50:11 -0700309 public void simulateReadErrorsForFile(Uri uri) {
Steve McKaybbeba522016-01-13 17:17:39 -0800310 simulateReadErrorsForFile(DocumentsContract.getDocumentId(uri));
311 }
312
313 public void simulateReadErrorsForFile(String id) {
314 mSimulateReadErrorIds.add(id);
Ben Kwac06f3fd2015-04-24 15:35:25 -0700315 }
316
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900317 @Override
318 public AssetFileDescriptor openDocumentThumbnail(
319 String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
320 throw new FileNotFoundException();
321 }
322
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900323 @Override
324 public AssetFileDescriptor openTypedDocument(
Steve McKaybbeba522016-01-13 17:17:39 -0800325 String docId, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900326 throws FileNotFoundException {
Steve McKaybbeba522016-01-13 17:17:39 -0800327 final StubDocument document = mStorage.get(docId);
Tomasz Mikolajewskie475d3b2015-12-24 13:24:00 +0900328 if (document == null || !document.file.isFile() || document.streamTypes == null) {
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900329 throw new FileNotFoundException();
330 }
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900331 for (final String mimeType : document.streamTypes) {
332 // Strict compare won't accept wildcards, but that's OK for tests, as DocumentsUI
Tomasz Mikolajewskidcec9ac2015-12-16 16:23:00 +0900333 // doesn't use them for getStreamTypes nor openTypedDocument.
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900334 if (mimeType.equals(mimeTypeFilter)) {
335 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
336 document.file, ParcelFileDescriptor.MODE_READ_ONLY);
Steve McKaybbeba522016-01-13 17:17:39 -0800337 if (mSimulateReadErrorIds.contains(docId)) {
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900338 pfd = new ParcelFileDescriptor(pfd) {
339 @Override
340 public void checkError() throws IOException {
341 throw new IOException("Test error");
342 }
343 };
344 }
345 return new AssetFileDescriptor(pfd, 0, document.file.length());
346 }
347 }
348 throw new IllegalArgumentException("Invalid MIME type filter for openTypedDocument().");
349 }
350
Tomasz Mikolajewskidcec9ac2015-12-16 16:23:00 +0900351 @Override
352 public String[] getStreamTypes(Uri uri, String mimeTypeFilter) {
353 final StubDocument document = mStorage.get(DocumentsContract.getDocumentId(uri));
354 if (document == null) {
355 throw new IllegalArgumentException(
356 "The provided Uri is incorrect, or the file is gone.");
357 }
Tomasz Mikolajewskidcec9ac2015-12-16 16:23:00 +0900358 if (!"*/*".equals(mimeTypeFilter)) {
359 // Not used by DocumentsUI, so don't bother implementing it.
360 throw new UnsupportedOperationException();
361 }
Tomasz Mikolajewskie475d3b2015-12-24 13:24:00 +0900362 if (document.streamTypes == null) {
363 return null;
364 }
Tomasz Mikolajewskidcec9ac2015-12-16 16:23:00 +0900365 return document.streamTypes.toArray(new String[document.streamTypes.size()]);
366 }
367
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900368 private ParcelFileDescriptor startWrite(final StubDocument document)
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900369 throws FileNotFoundException {
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900370 ParcelFileDescriptor[] pipe;
371 try {
372 pipe = ParcelFileDescriptor.createReliablePipe();
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700373 } catch (IOException exception) {
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900374 throw new FileNotFoundException();
375 }
376 final ParcelFileDescriptor readPipe = pipe[0];
377 final ParcelFileDescriptor writePipe = pipe[1];
378
379 new Thread() {
380 @Override
381 public void run() {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700382 InputStream inputStream = null;
383 OutputStream outputStream = null;
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900384 try {
Ben Kwacb4461f2015-05-05 11:50:11 -0700385 Log.d(TAG, "Opening write stream on file " + document.documentId);
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700386 inputStream = new ParcelFileDescriptor.AutoCloseInputStream(readPipe);
387 outputStream = new FileOutputStream(document.file);
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900388 byte[] buffer = new byte[32 * 1024];
389 int bytesToRead;
390 int bytesRead = 0;
391 while (bytesRead != -1) {
392 synchronized (mWriteLock) {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700393 // This cast is safe because the max possible value is buffer.length.
394 bytesToRead = (int) Math.min(document.rootInfo.getRemainingCapacity(),
395 buffer.length);
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900396 if (bytesToRead == 0) {
397 closePipeWithErrorSilently(readPipe, "Not enough space.");
398 break;
399 }
400 bytesRead = inputStream.read(buffer, 0, bytesToRead);
401 if (bytesRead == -1) {
402 break;
403 }
404 outputStream.write(buffer, 0, bytesRead);
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700405 document.rootInfo.size += bytesRead;
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900406 }
407 }
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700408 } catch (IOException e) {
Ben Kwac06f3fd2015-04-24 15:35:25 -0700409 Log.e(TAG, "Error on close", e);
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900410 closePipeWithErrorSilently(readPipe, e.getMessage());
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700411 } finally {
412 IoUtils.closeQuietly(inputStream);
413 IoUtils.closeQuietly(outputStream);
Ben Kwacb4461f2015-05-05 11:50:11 -0700414 Log.d(TAG, "Closing write stream on file " + document.documentId);
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900415 notifyParentChanged(document.parentId);
Ben Kwac06f3fd2015-04-24 15:35:25 -0700416 getContext().getContentResolver().notifyChange(
417 DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
418 null, false);
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900419 }
420 }
421 }.start();
422
423 return writePipe;
424 }
425
426 private void closePipeWithErrorSilently(ParcelFileDescriptor pipe, String error) {
427 try {
428 pipe.closeWithError(error);
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700429 } catch (IOException ignore) {
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900430 }
431 }
432
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700433 @Override
434 public Bundle call(String method, String arg, Bundle extras) {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700435 switch (method) {
436 case "clear":
437 clearCacheAndBuildRoots();
438 return null;
439 case "configure":
440 configure(arg, extras);
441 return null;
Steve McKaybbeba522016-01-13 17:17:39 -0800442 case "createVirtualFile":
443 return createVirtualFileFromBundle(extras);
444 case "simulateReadErrorsForFile":
445 simulateReadErrorsForFile(arg);
446 return null;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700447 default:
448 return super.call(method, arg, extras);
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900449 }
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700450 }
451
Steve McKaybbeba522016-01-13 17:17:39 -0800452 private Bundle createVirtualFileFromBundle(Bundle extras) {
453 try {
454 Uri uri = createVirtualFile(
455 extras.getString(EXTRA_ROOT),
456 extras.getString(EXTRA_PATH),
457 extras.getString(Document.COLUMN_MIME_TYPE),
458 extras.getStringArrayList(EXTRA_STREAM_TYPES),
459 extras.getByteArray(EXTRA_CONTENT));
460
461 String documentId = DocumentsContract.getDocumentId(uri);
462 Bundle result = new Bundle();
463 result.putString(Document.COLUMN_DOCUMENT_ID, documentId);
464 return result;
465 } catch (IOException e) {
466 Log.e(TAG, "Couldn't create virtual file.");
467 }
468
469 return null;
470 }
471
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700472 private void configure(String arg, Bundle extras) {
473 Log.d(TAG, "Configure " + arg);
Steve McKay99bcc6a2015-10-26 17:03:55 -0700474 String rootName = extras.getString(EXTRA_ROOT, ROOT_0_ID);
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700475 long rootSize = extras.getLong(EXTRA_SIZE, 1) * 1024 * 1024;
476 setSize(rootName, rootSize);
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900477 }
478
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900479 private void notifyParentChanged(String parentId) {
480 getContext().getContentResolver().notifyChange(
481 DocumentsContract.buildChildDocumentsUri(mAuthority, parentId), null, false);
482 // Notify also about possible change in remaining space on the root.
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700483 getContext().getContentResolver().notifyChange(DocumentsContract.buildRootsUri(mAuthority),
484 null, false);
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +0900485 }
486
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900487 private void includeDocument(MatrixCursor result, StubDocument document) {
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900488 final RowBuilder row = result.newRow();
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900489 row.add(Document.COLUMN_DOCUMENT_ID, document.documentId);
490 row.add(Document.COLUMN_DISPLAY_NAME, document.file.getName());
491 row.add(Document.COLUMN_SIZE, document.file.length());
492 row.add(Document.COLUMN_MIME_TYPE, document.mimeType);
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900493 row.add(Document.COLUMN_FLAGS, document.flags);
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900494 row.add(Document.COLUMN_LAST_MODIFIED, document.file.lastModified());
Tomasz Mikolajewski67f9bed2015-04-08 19:38:55 +0900495 }
496
Steve McKay99bcc6a2015-10-26 17:03:55 -0700497 private void removeChildrenRecursively(File file) {
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900498 for (File childFile : file.listFiles()) {
499 if (childFile.isDirectory()) {
Steve McKay99bcc6a2015-10-26 17:03:55 -0700500 removeChildrenRecursively(childFile);
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900501 }
502 childFile.delete();
503 }
504 }
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900505
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700506 public void setSize(String rootId, long rootSize) {
507 RootInfo root = mRoots.get(rootId);
508 if (root != null) {
509 final String key = STORAGE_SIZE_KEY + "." + rootId;
510 Log.d(TAG, "Set size of " + key + " : " + rootSize);
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900511
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700512 // Persist the size.
513 SharedPreferences.Editor editor = mPrefs.edit();
514 editor.putLong(key, rootSize);
515 editor.apply();
516 // Apply the size in the current instance of this provider.
517 root.capacity = rootSize;
518 getContext().getContentResolver().notifyChange(
519 DocumentsContract.buildRootsUri(mAuthority),
520 null, false);
521 } else {
522 Log.e(TAG, "Attempt to configure non-existent root: " + rootId);
523 }
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900524 }
525
Ben Kwac06f3fd2015-04-24 15:35:25 -0700526 @VisibleForTesting
Tomasz Mikolajewskidcec9ac2015-12-16 16:23:00 +0900527 public Uri createRegularFile(String rootId, String path, String mimeType, byte[] content)
528 throws FileNotFoundException, IOException {
529 final File file = createFile(rootId, path, mimeType, content);
530 final StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile()));
531 if (parent == null) {
532 throw new FileNotFoundException("Parent not found.");
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700533 }
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900534 final StubDocument document = StubDocument.createRegularDocument(file, mimeType, parent);
535 mStorage.put(document.documentId, document);
Ben Kwac06f3fd2015-04-24 15:35:25 -0700536 return DocumentsContract.buildDocumentUri(mAuthority, document.documentId);
537 }
538
539 @VisibleForTesting
Tomasz Mikolajewskidcec9ac2015-12-16 16:23:00 +0900540 public Uri createVirtualFile(
541 String rootId, String path, String mimeType, List<String> streamTypes, byte[] content)
542 throws FileNotFoundException, IOException {
Steve McKaybbeba522016-01-13 17:17:39 -0800543
Tomasz Mikolajewskidcec9ac2015-12-16 16:23:00 +0900544 final File file = createFile(rootId, path, mimeType, content);
545 final StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile()));
546 if (parent == null) {
547 throw new FileNotFoundException("Parent not found.");
548 }
549 final StubDocument document = StubDocument.createVirtualDocument(
550 file, mimeType, streamTypes, parent);
551 mStorage.put(document.documentId, document);
552 return DocumentsContract.buildDocumentUri(mAuthority, document.documentId);
553 }
554
555 @VisibleForTesting
Ben Kwac06f3fd2015-04-24 15:35:25 -0700556 public File getFile(String rootId, String path) throws FileNotFoundException {
Steve McKay99bcc6a2015-10-26 17:03:55 -0700557 StubDocument root = mRoots.get(rootId).document;
Ben Kwac06f3fd2015-04-24 15:35:25 -0700558 if (root == null) {
559 throw new FileNotFoundException("No roots with the ID " + rootId + " were found");
560 }
561 // Convert the path string into a path that's relative to the root.
562 File needle = new File(root.file, path.substring(1));
563
564 StubDocument found = mStorage.get(getDocumentIdForFile(needle));
565 if (found == null) {
566 return null;
567 }
568 return found.file;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700569 }
570
Tomasz Mikolajewski049eb562015-12-18 11:29:48 +0900571 private File createFile(String rootId, String path, String mimeType, byte[] content)
572 throws FileNotFoundException, IOException {
Steve McKaybbeba522016-01-13 17:17:39 -0800573 Log.d(TAG, "Creating test file " + rootId + " : " + path);
Tomasz Mikolajewski049eb562015-12-18 11:29:48 +0900574 StubDocument root = mRoots.get(rootId).document;
575 if (root == null) {
576 throw new FileNotFoundException("No roots with the ID " + rootId + " were found");
577 }
578 final File file = new File(root.file, path.substring(1));
579 if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
580 if (!file.mkdirs()) {
581 throw new FileNotFoundException("Couldn't create directory " + file.getPath());
582 }
583 } else {
584 if (!file.createNewFile()) {
585 throw new FileNotFoundException("Couldn't create file " + file.getPath());
586 }
587 try (final FileOutputStream fout = new FileOutputStream(file)) {
588 fout.write(content);
589 }
590 }
591 return file;
592 }
593
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900594 final static class RootInfo {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700595 public final String name;
Steve McKay99bcc6a2015-10-26 17:03:55 -0700596 public final StubDocument document;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700597 public long capacity;
598 public long size;
599
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900600 RootInfo(File file, long capacity) {
601 this.name = file.getName();
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700602 this.capacity = 1024 * 1024;
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900603 this.document = StubDocument.createRootDocument(file, this);
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700604 this.capacity = capacity;
605 this.size = 0;
606 }
607
608 public long getRemainingCapacity() {
609 return capacity - size;
610 }
611 }
612
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900613 final static class StubDocument {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700614 public final File file;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700615 public final String documentId;
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900616 public final String mimeType;
617 public final List<String> streamTypes;
618 public final int flags;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700619 public final String parentId;
620 public final RootInfo rootInfo;
621
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900622 private StubDocument(
623 File file, String mimeType, List<String> streamTypes, int flags,
624 StubDocument parent) {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700625 this.file = file;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700626 this.documentId = getDocumentIdForFile(file);
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900627 this.mimeType = mimeType;
628 this.streamTypes = streamTypes;
629 this.flags = flags;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700630 this.parentId = parent.documentId;
631 this.rootInfo = parent.rootInfo;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700632 }
633
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900634 private StubDocument(File file, RootInfo rootInfo) {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700635 this.file = file;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700636 this.documentId = getDocumentIdForFile(file);
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900637 this.mimeType = Document.MIME_TYPE_DIR;
638 this.streamTypes = new ArrayList<String>();
639 this.flags = Document.FLAG_DIR_SUPPORTS_CREATE;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700640 this.parentId = null;
641 this.rootInfo = rootInfo;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700642 }
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900643
644 public static StubDocument createRootDocument(File file, RootInfo rootInfo) {
645 return new StubDocument(file, rootInfo);
646 }
647
648 public static StubDocument createRegularDocument(
649 File file, String mimeType, StubDocument parent) {
650 int flags = Document.FLAG_SUPPORTS_DELETE;
651 if (file.isDirectory()) {
652 flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
653 } else {
654 flags |= Document.FLAG_SUPPORTS_WRITE;
655 }
656 return new StubDocument(file, mimeType, new ArrayList<String>(), flags, parent);
657 }
658
659 public static StubDocument createVirtualDocument(
660 File file, String mimeType, List<String> streamTypes, StubDocument parent) {
661 int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE
662 | Document.FLAG_VIRTUAL_DOCUMENT;
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900663 return new StubDocument(file, mimeType, streamTypes, flags, parent);
664 }
665
Steve McKayf712a202015-11-19 17:27:12 -0800666 @Override
667 public String toString() {
668 return "StubDocument{"
669 + "path:" + file.getPath()
Steve McKayf712a202015-11-19 17:27:12 -0800670 + ", documentId:" + documentId
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900671 + ", mimeType:" + mimeType
672 + ", streamTypes:" + streamTypes.toString()
673 + ", flags:" + flags
Steve McKayf712a202015-11-19 17:27:12 -0800674 + ", parentId:" + parentId
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900675 + ", rootInfo:" + rootInfo
Steve McKayf712a202015-11-19 17:27:12 -0800676 + "}";
677 }
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700678 }
679
680 private static String getDocumentIdForFile(File file) {
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900681 return file.getAbsolutePath();
682 }
683}