blob: a47d35017f26a84b6424f818367fb0bb59908d95 [file] [log] [blame]
Tomasz Mikolajewski0fc8beb2015-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 Kwa448dbbb2015-04-16 18:14:35 -070020import android.content.SharedPreferences;
Tomasz Mikolajewski738154e2015-04-10 17:32:44 +090021import android.content.pm.ProviderInfo;
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +090022import android.content.res.AssetFileDescriptor;
23import android.database.Cursor;
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +090024import android.database.MatrixCursor;
Ben Kwadae8c372015-04-24 15:35:25 -070025import android.database.MatrixCursor.RowBuilder;
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +090026import android.graphics.Point;
Ben Kwadae8c372015-04-24 15:35:25 -070027import android.net.Uri;
Ben Kwa448dbbb2015-04-16 18:14:35 -070028import android.os.Bundle;
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +090029import android.os.CancellationSignal;
Tomasz Mikolajewski78699be2015-04-08 19:38:55 +090030import android.os.FileUtils;
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +090031import android.os.ParcelFileDescriptor;
Tomasz Mikolajewski738154e2015-04-10 17:32:44 +090032import android.provider.DocumentsContract;
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +090033import android.provider.DocumentsContract.Document;
34import android.provider.DocumentsContract.Root;
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +090035import android.provider.DocumentsProvider;
Ben Kwadae8c372015-04-24 15:35:25 -070036import android.support.annotation.VisibleForTesting;
Ben Kwa448dbbb2015-04-16 18:14:35 -070037import android.util.Log;
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +090038
Ben Kwa448dbbb2015-04-16 18:14:35 -070039import libcore.io.IoUtils;
40
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +090041import java.io.File;
42import java.io.FileNotFoundException;
Ben Kwadae8c372015-04-24 15:35:25 -070043import java.io.FileOutputStream;
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +090044import java.io.IOException;
Ben Kwa448dbbb2015-04-16 18:14:35 -070045import java.io.InputStream;
46import java.io.OutputStream;
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +090047import java.util.ArrayList;
Ben Kwa448dbbb2015-04-16 18:14:35 -070048import java.util.Arrays;
49import java.util.Collection;
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +090050import java.util.HashMap;
Steve McKayecbf3c52016-01-13 17:17:39 -080051import java.util.HashSet;
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +090052import java.util.List;
Ben Kwa448dbbb2015-04-16 18:14:35 -070053import java.util.Map;
Steve McKayecbf3c52016-01-13 17:17:39 -080054import java.util.Set;
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +090055
56public class StubProvider extends DocumentsProvider {
Steve McKay0c3c6952015-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 McKayecbf3c52016-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 McKay0c3c6952015-10-26 17:03:55 -070069 private static final String TAG = "StubProvider";
Steve McKayecbf3c52016-01-13 17:17:39 -080070
Ben Kwa448dbbb2015-04-16 18:14:35 -070071 private static final String STORAGE_SIZE_KEY = "documentsui.stubprovider.size";
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +090072 private static int DEFAULT_ROOT_SIZE = 1024 * 1024 * 100; // 100 MB.
Steve McKay0c3c6952015-10-26 17:03:55 -070073
Tomasz Mikolajewski0fc8beb2015-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 McKay0c3c6952015-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 Kwa448dbbb2015-04-16 18:14:35 -070088 private SharedPreferences mPrefs;
Steve McKayecbf3c52016-01-13 17:17:39 -080089 private Set<String> mSimulateReadErrorIds = new HashSet<>();
Tomasz Mikolajewski738154e2015-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 Mikolajewski0fc8beb2015-04-08 09:21:08 +090096
97 @Override
98 public boolean onCreate() {
Ben Kwa448dbbb2015-04-16 18:14:35 -070099 clearCacheAndBuildRoots();
100 return true;
101 }
102
Ben Kwadae8c372015-04-24 15:35:25 -0700103 @VisibleForTesting
104 public void clearCacheAndBuildRoots() {
Steve McKay0c3c6952015-10-26 17:03:55 -0700105 Log.d(TAG, "Resetting storage.");
106 removeChildrenRecursively(getContext().getCacheDir());
Ben Kwa448dbbb2015-04-16 18:14:35 -0700107 mStorage.clear();
Steve McKayecbf3c52016-01-13 17:17:39 -0800108 mSimulateReadErrorIds.clear();
Ben Kwa448dbbb2015-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 McKay0c3c6952015-10-26 17:03:55 -0700114 rootIds = Arrays.asList(new String[] { ROOT_0_ID, ROOT_1_ID });
Ben Kwa448dbbb2015-04-16 18:14:35 -0700115 }
Steve McKay0c3c6952015-10-26 17:03:55 -0700116
117 mRoots.clear();
Ben Kwa448dbbb2015-04-16 18:14:35 -0700118 for (String rootId : rootIds) {
Tomasz Mikolajewskicca31eb2015-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 Kwa448dbbb2015-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 McKay0c3c6952015-10-26 17:03:55 -0700135 return mPrefs.getLong(key, DEFAULT_ROOT_SIZE);
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +0900136 }
137
138 @Override
139 public Cursor queryRoots(String[] projection) throws FileNotFoundException {
Ben Kwa448dbbb2015-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);
Aga Wronska619f3be2016-01-14 11:15:20 -0800147 row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD
148 | Root.FLAG_SUPPORTS_SEARCH);
Ben Kwa448dbbb2015-04-16 18:14:35 -0700149 row.add(Root.COLUMN_TITLE, id);
Steve McKay0c3c6952015-10-26 17:03:55 -0700150 row.add(Root.COLUMN_DOCUMENT_ID, info.document.documentId);
Ben Kwa448dbbb2015-04-16 18:14:35 -0700151 row.add(Root.COLUMN_AVAILABLE_BYTES, info.getRemainingCapacity());
152 }
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +0900153 return result;
154 }
155
156 @Override
Ben Kwa448dbbb2015-04-16 18:14:35 -0700157 public Cursor queryDocument(String documentId, String[] projection)
158 throws FileNotFoundException {
159 final MatrixCursor result = new MatrixCursor(projection != null ? projection
160 : DEFAULT_DOCUMENT_PROJECTION);
Tomasz Mikolajewskid5d5c912015-04-13 12:17:51 +0900161 final StubDocument file = mStorage.get(documentId);
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +0900162 if (file == null) {
163 throw new FileNotFoundException();
164 }
Tomasz Mikolajewski97397932015-04-13 13:27:14 +0900165 includeDocument(result, file);
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +0900166 return result;
167 }
168
169 @Override
Tomasz Mikolajewski78699be2015-04-08 19:38:55 +0900170 public boolean isChildDocument(String parentDocId, String docId) {
Tomasz Mikolajewskid5d5c912015-04-13 12:17:51 +0900171 final StubDocument parentDocument = mStorage.get(parentDocId);
172 final StubDocument childDocument = mStorage.get(docId);
173 return FileUtils.contains(parentDocument.file, childDocument.file);
Tomasz Mikolajewski78699be2015-04-08 19:38:55 +0900174 }
175
176 @Override
Steve McKay0c3c6952015-10-26 17:03:55 -0700177 public String createDocument(String parentId, String mimeType, String displayName)
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +0900178 throws FileNotFoundException {
Steve McKay0c3c6952015-10-26 17:03:55 -0700179
180 final StubDocument parent = mStorage.get(parentId);
181 if (parent == null) {
182 throw new IllegalArgumentException(
183 "Can't create file " + displayName + " in null parent.");
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +0900184 }
Steve McKay0c3c6952015-10-26 17:03:55 -0700185 if (!parent.file.isDirectory()) {
186 throw new IllegalArgumentException(
187 "Can't create file " + displayName + " inside non-directory parent "
188 + parent.file.getName());
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +0900189 }
190
Steve McKay0c3c6952015-10-26 17:03:55 -0700191 final File file = new File(parent.file, displayName);
192 if (file.exists()) {
193 throw new FileNotFoundException(
194 "Duplicate file names not supported for " + file);
195 }
196
197 if (mimeType.equals(Document.MIME_TYPE_DIR)) {
198 if (!file.mkdirs()) {
199 throw new FileNotFoundException(
200 "Failed to create directory(s): " + file);
201 }
202 Log.i(TAG, "Created new directory: " + file);
203 } else {
204 boolean created = false;
205 try {
206 created = file.createNewFile();
207 } catch (IOException e) {
208 // We'll throw an FNF exception later :)
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900209 Log.e(TAG, "createNewFile operation failed for file: " + file, e);
Steve McKay0c3c6952015-10-26 17:03:55 -0700210 }
211 if (!created) {
212 throw new FileNotFoundException(
213 "createNewFile operation failed for: " + file);
214 }
215 Log.i(TAG, "Created new file: " + file);
216 }
217
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900218 final StubDocument document = StubDocument.createRegularDocument(file, mimeType, parent);
219 mStorage.put(document.documentId, document);
Ben Kwa0b4a3c42015-05-05 11:50:11 -0700220 Log.d(TAG, "Created document " + document.documentId);
Tomasz Mikolajewski97397932015-04-13 13:27:14 +0900221 notifyParentChanged(document.parentId);
Ben Kwadae8c372015-04-24 15:35:25 -0700222 getContext().getContentResolver().notifyChange(
223 DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
224 null, false);
225
Tomasz Mikolajewskid5d5c912015-04-13 12:17:51 +0900226 return document.documentId;
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +0900227 }
228
229 @Override
Tomasz Mikolajewski738154e2015-04-10 17:32:44 +0900230 public void deleteDocument(String documentId)
231 throws FileNotFoundException {
Tomasz Mikolajewskid5d5c912015-04-13 12:17:51 +0900232 final StubDocument document = mStorage.get(documentId);
233 final long fileSize = document.file.length();
234 if (document == null || !document.file.delete())
Tomasz Mikolajewski738154e2015-04-10 17:32:44 +0900235 throw new FileNotFoundException();
236 synchronized (mWriteLock) {
Ben Kwa448dbbb2015-04-16 18:14:35 -0700237 document.rootInfo.size -= fileSize;
Ben Kwa0b4a3c42015-05-05 11:50:11 -0700238 mStorage.remove(documentId);
Tomasz Mikolajewski738154e2015-04-10 17:32:44 +0900239 }
Ben Kwa0b4a3c42015-05-05 11:50:11 -0700240 Log.d(TAG, "Document deleted: " + documentId);
Tomasz Mikolajewski97397932015-04-13 13:27:14 +0900241 notifyParentChanged(document.parentId);
Ben Kwadae8c372015-04-24 15:35:25 -0700242 getContext().getContentResolver().notifyChange(
243 DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
244 null, false);
Tomasz Mikolajewski738154e2015-04-10 17:32:44 +0900245 }
246
247 @Override
Steve McKaydbdaa492015-12-02 11:20:54 -0800248 public Cursor queryChildDocumentsForManage(String parentDocumentId, String[] projection,
249 String sortOrder) throws FileNotFoundException {
250 return queryChildDocuments(parentDocumentId, projection, sortOrder);
251 }
252
253 @Override
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +0900254 public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
255 throws FileNotFoundException {
Tomasz Mikolajewskid5d5c912015-04-13 12:17:51 +0900256 final StubDocument parentDocument = mStorage.get(parentDocumentId);
257 if (parentDocument == null || parentDocument.file.isFile()) {
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +0900258 throw new FileNotFoundException();
259 }
Ben Kwa448dbbb2015-04-16 18:14:35 -0700260 final MatrixCursor result = new MatrixCursor(projection != null ? projection
261 : DEFAULT_DOCUMENT_PROJECTION);
Tomasz Mikolajewski97397932015-04-13 13:27:14 +0900262 result.setNotificationUri(getContext().getContentResolver(),
263 DocumentsContract.buildChildDocumentsUri(mAuthority, parentDocumentId));
Tomasz Mikolajewskid5d5c912015-04-13 12:17:51 +0900264 StubDocument document;
265 for (File file : parentDocument.file.listFiles()) {
Ben Kwa448dbbb2015-04-16 18:14:35 -0700266 document = mStorage.get(getDocumentIdForFile(file));
Tomasz Mikolajewski97397932015-04-13 13:27:14 +0900267 if (document != null) {
268 includeDocument(result, document);
269 }
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +0900270 }
271 return result;
272 }
273
274 @Override
275 public Cursor queryRecentDocuments(String rootId, String[] projection)
276 throws FileNotFoundException {
Ben Kwa448dbbb2015-04-16 18:14:35 -0700277 final MatrixCursor result = new MatrixCursor(projection != null ? projection
278 : DEFAULT_DOCUMENT_PROJECTION);
279 return result;
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +0900280 }
281
282 @Override
Aga Wronska619f3be2016-01-14 11:15:20 -0800283 public Cursor querySearchDocuments(String rootId, String query, String[] projection)
284 throws FileNotFoundException {
285
286 StubDocument parentDocument = mRoots.get(rootId).document;
287 if (parentDocument == null || parentDocument.file.isFile()) {
288 throw new FileNotFoundException();
289 }
290
291 final MatrixCursor result = new MatrixCursor(
292 projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
293
294 for (File file : parentDocument.file.listFiles()) {
295 if (file.getName().toLowerCase().contains(query)) {
296 StubDocument document = mStorage.get(getDocumentIdForFile(file));
297 if (document != null) {
298 includeDocument(result, document);
299 }
300 }
301 }
302 return result;
303 }
304
305 @Override
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +0900306 public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
307 throws FileNotFoundException {
Steve McKayecbf3c52016-01-13 17:17:39 -0800308
Tomasz Mikolajewskid5d5c912015-04-13 12:17:51 +0900309 final StubDocument document = mStorage.get(docId);
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900310 if (document == null || !document.file.isFile()) {
Tomasz Mikolajewskid5d5c912015-04-13 12:17:51 +0900311 throw new FileNotFoundException();
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900312 }
313 if ((document.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0) {
314 throw new IllegalStateException("Tried to open a virtual file.");
315 }
Tomasz Mikolajewskid5d5c912015-04-13 12:17:51 +0900316
317 if ("r".equals(mode)) {
Steve McKayecbf3c52016-01-13 17:17:39 -0800318 if (mSimulateReadErrorIds.contains(docId)) {
319 Log.d(TAG, "Simulated errs enabled. Open in the wrong mode.");
320 return ParcelFileDescriptor.open(
321 document.file, ParcelFileDescriptor.MODE_WRITE_ONLY);
Ben Kwadae8c372015-04-24 15:35:25 -0700322 }
Steve McKayecbf3c52016-01-13 17:17:39 -0800323 return ParcelFileDescriptor.open(document.file, ParcelFileDescriptor.MODE_READ_ONLY);
Tomasz Mikolajewski30b66942015-04-10 17:28:53 +0900324 }
Tomasz Mikolajewskid5d5c912015-04-13 12:17:51 +0900325 if ("w".equals(mode)) {
326 return startWrite(document);
327 }
328
Tomasz Mikolajewski30b66942015-04-10 17:28:53 +0900329 throw new FileNotFoundException();
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +0900330 }
331
Ben Kwadae8c372015-04-24 15:35:25 -0700332 @VisibleForTesting
Ben Kwa0b4a3c42015-05-05 11:50:11 -0700333 public void simulateReadErrorsForFile(Uri uri) {
Steve McKayecbf3c52016-01-13 17:17:39 -0800334 simulateReadErrorsForFile(DocumentsContract.getDocumentId(uri));
335 }
336
337 public void simulateReadErrorsForFile(String id) {
338 mSimulateReadErrorIds.add(id);
Ben Kwadae8c372015-04-24 15:35:25 -0700339 }
340
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +0900341 @Override
342 public AssetFileDescriptor openDocumentThumbnail(
343 String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
344 throw new FileNotFoundException();
345 }
346
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900347 @Override
348 public AssetFileDescriptor openTypedDocument(
Steve McKayecbf3c52016-01-13 17:17:39 -0800349 String docId, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900350 throws FileNotFoundException {
Steve McKayecbf3c52016-01-13 17:17:39 -0800351 final StubDocument document = mStorage.get(docId);
Tomasz Mikolajewski75395652016-01-07 07:19:22 +0000352 if (document == null || !document.file.isFile() || document.streamTypes == null) {
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900353 throw new FileNotFoundException();
354 }
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900355 for (final String mimeType : document.streamTypes) {
356 // Strict compare won't accept wildcards, but that's OK for tests, as DocumentsUI
Tomasz Mikolajewskif65fdeb2015-12-16 16:23:00 +0900357 // doesn't use them for getStreamTypes nor openTypedDocument.
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900358 if (mimeType.equals(mimeTypeFilter)) {
359 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
360 document.file, ParcelFileDescriptor.MODE_READ_ONLY);
Steve McKayecbf3c52016-01-13 17:17:39 -0800361 if (mSimulateReadErrorIds.contains(docId)) {
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900362 pfd = new ParcelFileDescriptor(pfd) {
363 @Override
364 public void checkError() throws IOException {
365 throw new IOException("Test error");
366 }
367 };
368 }
369 return new AssetFileDescriptor(pfd, 0, document.file.length());
370 }
371 }
372 throw new IllegalArgumentException("Invalid MIME type filter for openTypedDocument().");
373 }
374
Tomasz Mikolajewskif65fdeb2015-12-16 16:23:00 +0900375 @Override
376 public String[] getStreamTypes(Uri uri, String mimeTypeFilter) {
377 final StubDocument document = mStorage.get(DocumentsContract.getDocumentId(uri));
378 if (document == null) {
379 throw new IllegalArgumentException(
380 "The provided Uri is incorrect, or the file is gone.");
381 }
Tomasz Mikolajewskif65fdeb2015-12-16 16:23:00 +0900382 if (!"*/*".equals(mimeTypeFilter)) {
383 // Not used by DocumentsUI, so don't bother implementing it.
384 throw new UnsupportedOperationException();
385 }
Tomasz Mikolajewski75395652016-01-07 07:19:22 +0000386 if (document.streamTypes == null) {
387 return null;
388 }
Tomasz Mikolajewskif65fdeb2015-12-16 16:23:00 +0900389 return document.streamTypes.toArray(new String[document.streamTypes.size()]);
390 }
391
Tomasz Mikolajewskid5d5c912015-04-13 12:17:51 +0900392 private ParcelFileDescriptor startWrite(final StubDocument document)
Tomasz Mikolajewski30b66942015-04-10 17:28:53 +0900393 throws FileNotFoundException {
Tomasz Mikolajewski30b66942015-04-10 17:28:53 +0900394 ParcelFileDescriptor[] pipe;
395 try {
396 pipe = ParcelFileDescriptor.createReliablePipe();
Ben Kwa448dbbb2015-04-16 18:14:35 -0700397 } catch (IOException exception) {
Tomasz Mikolajewski30b66942015-04-10 17:28:53 +0900398 throw new FileNotFoundException();
399 }
400 final ParcelFileDescriptor readPipe = pipe[0];
401 final ParcelFileDescriptor writePipe = pipe[1];
402
403 new Thread() {
404 @Override
405 public void run() {
Ben Kwa448dbbb2015-04-16 18:14:35 -0700406 InputStream inputStream = null;
407 OutputStream outputStream = null;
Tomasz Mikolajewski30b66942015-04-10 17:28:53 +0900408 try {
Ben Kwa0b4a3c42015-05-05 11:50:11 -0700409 Log.d(TAG, "Opening write stream on file " + document.documentId);
Ben Kwa448dbbb2015-04-16 18:14:35 -0700410 inputStream = new ParcelFileDescriptor.AutoCloseInputStream(readPipe);
411 outputStream = new FileOutputStream(document.file);
Tomasz Mikolajewski30b66942015-04-10 17:28:53 +0900412 byte[] buffer = new byte[32 * 1024];
413 int bytesToRead;
414 int bytesRead = 0;
415 while (bytesRead != -1) {
416 synchronized (mWriteLock) {
Ben Kwa448dbbb2015-04-16 18:14:35 -0700417 // This cast is safe because the max possible value is buffer.length.
418 bytesToRead = (int) Math.min(document.rootInfo.getRemainingCapacity(),
419 buffer.length);
Tomasz Mikolajewski30b66942015-04-10 17:28:53 +0900420 if (bytesToRead == 0) {
421 closePipeWithErrorSilently(readPipe, "Not enough space.");
422 break;
423 }
424 bytesRead = inputStream.read(buffer, 0, bytesToRead);
425 if (bytesRead == -1) {
426 break;
427 }
428 outputStream.write(buffer, 0, bytesRead);
Ben Kwa448dbbb2015-04-16 18:14:35 -0700429 document.rootInfo.size += bytesRead;
Tomasz Mikolajewski30b66942015-04-10 17:28:53 +0900430 }
431 }
Ben Kwa448dbbb2015-04-16 18:14:35 -0700432 } catch (IOException e) {
Ben Kwadae8c372015-04-24 15:35:25 -0700433 Log.e(TAG, "Error on close", e);
Tomasz Mikolajewski30b66942015-04-10 17:28:53 +0900434 closePipeWithErrorSilently(readPipe, e.getMessage());
Ben Kwa448dbbb2015-04-16 18:14:35 -0700435 } finally {
436 IoUtils.closeQuietly(inputStream);
437 IoUtils.closeQuietly(outputStream);
Ben Kwa0b4a3c42015-05-05 11:50:11 -0700438 Log.d(TAG, "Closing write stream on file " + document.documentId);
Tomasz Mikolajewski97397932015-04-13 13:27:14 +0900439 notifyParentChanged(document.parentId);
Ben Kwadae8c372015-04-24 15:35:25 -0700440 getContext().getContentResolver().notifyChange(
441 DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
442 null, false);
Tomasz Mikolajewski30b66942015-04-10 17:28:53 +0900443 }
444 }
445 }.start();
446
447 return writePipe;
448 }
449
450 private void closePipeWithErrorSilently(ParcelFileDescriptor pipe, String error) {
451 try {
452 pipe.closeWithError(error);
Ben Kwa448dbbb2015-04-16 18:14:35 -0700453 } catch (IOException ignore) {
Tomasz Mikolajewski30b66942015-04-10 17:28:53 +0900454 }
455 }
456
Ben Kwa448dbbb2015-04-16 18:14:35 -0700457 @Override
458 public Bundle call(String method, String arg, Bundle extras) {
Ben Kwa448dbbb2015-04-16 18:14:35 -0700459 switch (method) {
460 case "clear":
461 clearCacheAndBuildRoots();
462 return null;
463 case "configure":
464 configure(arg, extras);
465 return null;
Steve McKayecbf3c52016-01-13 17:17:39 -0800466 case "createVirtualFile":
467 return createVirtualFileFromBundle(extras);
468 case "simulateReadErrorsForFile":
469 simulateReadErrorsForFile(arg);
470 return null;
Ben Kwa448dbbb2015-04-16 18:14:35 -0700471 default:
472 return super.call(method, arg, extras);
Tomasz Mikolajewski30b66942015-04-10 17:28:53 +0900473 }
Ben Kwa448dbbb2015-04-16 18:14:35 -0700474 }
475
Steve McKayecbf3c52016-01-13 17:17:39 -0800476 private Bundle createVirtualFileFromBundle(Bundle extras) {
477 try {
478 Uri uri = createVirtualFile(
479 extras.getString(EXTRA_ROOT),
480 extras.getString(EXTRA_PATH),
481 extras.getString(Document.COLUMN_MIME_TYPE),
482 extras.getStringArrayList(EXTRA_STREAM_TYPES),
483 extras.getByteArray(EXTRA_CONTENT));
484
485 String documentId = DocumentsContract.getDocumentId(uri);
486 Bundle result = new Bundle();
487 result.putString(Document.COLUMN_DOCUMENT_ID, documentId);
488 return result;
489 } catch (IOException e) {
490 Log.e(TAG, "Couldn't create virtual file.");
491 }
492
493 return null;
494 }
495
Ben Kwa448dbbb2015-04-16 18:14:35 -0700496 private void configure(String arg, Bundle extras) {
497 Log.d(TAG, "Configure " + arg);
Steve McKay0c3c6952015-10-26 17:03:55 -0700498 String rootName = extras.getString(EXTRA_ROOT, ROOT_0_ID);
Ben Kwa448dbbb2015-04-16 18:14:35 -0700499 long rootSize = extras.getLong(EXTRA_SIZE, 1) * 1024 * 1024;
500 setSize(rootName, rootSize);
Tomasz Mikolajewski30b66942015-04-10 17:28:53 +0900501 }
502
Tomasz Mikolajewski97397932015-04-13 13:27:14 +0900503 private void notifyParentChanged(String parentId) {
504 getContext().getContentResolver().notifyChange(
505 DocumentsContract.buildChildDocumentsUri(mAuthority, parentId), null, false);
506 // Notify also about possible change in remaining space on the root.
Ben Kwa448dbbb2015-04-16 18:14:35 -0700507 getContext().getContentResolver().notifyChange(DocumentsContract.buildRootsUri(mAuthority),
508 null, false);
Tomasz Mikolajewski738154e2015-04-10 17:32:44 +0900509 }
510
Tomasz Mikolajewski97397932015-04-13 13:27:14 +0900511 private void includeDocument(MatrixCursor result, StubDocument document) {
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +0900512 final RowBuilder row = result.newRow();
Tomasz Mikolajewskid5d5c912015-04-13 12:17:51 +0900513 row.add(Document.COLUMN_DOCUMENT_ID, document.documentId);
514 row.add(Document.COLUMN_DISPLAY_NAME, document.file.getName());
515 row.add(Document.COLUMN_SIZE, document.file.length());
516 row.add(Document.COLUMN_MIME_TYPE, document.mimeType);
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900517 row.add(Document.COLUMN_FLAGS, document.flags);
Tomasz Mikolajewskid5d5c912015-04-13 12:17:51 +0900518 row.add(Document.COLUMN_LAST_MODIFIED, document.file.lastModified());
Tomasz Mikolajewski78699be2015-04-08 19:38:55 +0900519 }
520
Steve McKay0c3c6952015-10-26 17:03:55 -0700521 private void removeChildrenRecursively(File file) {
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +0900522 for (File childFile : file.listFiles()) {
523 if (childFile.isDirectory()) {
Steve McKay0c3c6952015-10-26 17:03:55 -0700524 removeChildrenRecursively(childFile);
Tomasz Mikolajewski0fc8beb2015-04-08 09:21:08 +0900525 }
526 childFile.delete();
527 }
528 }
Tomasz Mikolajewskid5d5c912015-04-13 12:17:51 +0900529
Ben Kwa448dbbb2015-04-16 18:14:35 -0700530 public void setSize(String rootId, long rootSize) {
531 RootInfo root = mRoots.get(rootId);
532 if (root != null) {
533 final String key = STORAGE_SIZE_KEY + "." + rootId;
534 Log.d(TAG, "Set size of " + key + " : " + rootSize);
Tomasz Mikolajewskid5d5c912015-04-13 12:17:51 +0900535
Ben Kwa448dbbb2015-04-16 18:14:35 -0700536 // Persist the size.
537 SharedPreferences.Editor editor = mPrefs.edit();
538 editor.putLong(key, rootSize);
539 editor.apply();
540 // Apply the size in the current instance of this provider.
541 root.capacity = rootSize;
542 getContext().getContentResolver().notifyChange(
543 DocumentsContract.buildRootsUri(mAuthority),
544 null, false);
545 } else {
546 Log.e(TAG, "Attempt to configure non-existent root: " + rootId);
547 }
Tomasz Mikolajewskid5d5c912015-04-13 12:17:51 +0900548 }
549
Ben Kwadae8c372015-04-24 15:35:25 -0700550 @VisibleForTesting
Tomasz Mikolajewskif65fdeb2015-12-16 16:23:00 +0900551 public Uri createRegularFile(String rootId, String path, String mimeType, byte[] content)
552 throws FileNotFoundException, IOException {
553 final File file = createFile(rootId, path, mimeType, content);
554 final StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile()));
555 if (parent == null) {
556 throw new FileNotFoundException("Parent not found.");
Ben Kwa448dbbb2015-04-16 18:14:35 -0700557 }
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900558 final StubDocument document = StubDocument.createRegularDocument(file, mimeType, parent);
559 mStorage.put(document.documentId, document);
Ben Kwadae8c372015-04-24 15:35:25 -0700560 return DocumentsContract.buildDocumentUri(mAuthority, document.documentId);
561 }
562
563 @VisibleForTesting
Tomasz Mikolajewskif65fdeb2015-12-16 16:23:00 +0900564 public Uri createVirtualFile(
565 String rootId, String path, String mimeType, List<String> streamTypes, byte[] content)
566 throws FileNotFoundException, IOException {
Steve McKayecbf3c52016-01-13 17:17:39 -0800567
Tomasz Mikolajewskif65fdeb2015-12-16 16:23:00 +0900568 final File file = createFile(rootId, path, mimeType, content);
569 final StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile()));
570 if (parent == null) {
571 throw new FileNotFoundException("Parent not found.");
572 }
573 final StubDocument document = StubDocument.createVirtualDocument(
574 file, mimeType, streamTypes, parent);
575 mStorage.put(document.documentId, document);
576 return DocumentsContract.buildDocumentUri(mAuthority, document.documentId);
577 }
578
579 @VisibleForTesting
Ben Kwadae8c372015-04-24 15:35:25 -0700580 public File getFile(String rootId, String path) throws FileNotFoundException {
Steve McKay0c3c6952015-10-26 17:03:55 -0700581 StubDocument root = mRoots.get(rootId).document;
Ben Kwadae8c372015-04-24 15:35:25 -0700582 if (root == null) {
583 throw new FileNotFoundException("No roots with the ID " + rootId + " were found");
584 }
585 // Convert the path string into a path that's relative to the root.
586 File needle = new File(root.file, path.substring(1));
587
588 StubDocument found = mStorage.get(getDocumentIdForFile(needle));
589 if (found == null) {
590 return null;
591 }
592 return found.file;
Ben Kwa448dbbb2015-04-16 18:14:35 -0700593 }
594
Tomasz Mikolajewskic7ec6782015-12-18 11:29:48 +0900595 private File createFile(String rootId, String path, String mimeType, byte[] content)
596 throws FileNotFoundException, IOException {
Steve McKayecbf3c52016-01-13 17:17:39 -0800597 Log.d(TAG, "Creating test file " + rootId + " : " + path);
Tomasz Mikolajewskic7ec6782015-12-18 11:29:48 +0900598 StubDocument root = mRoots.get(rootId).document;
599 if (root == null) {
600 throw new FileNotFoundException("No roots with the ID " + rootId + " were found");
601 }
602 final File file = new File(root.file, path.substring(1));
603 if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
604 if (!file.mkdirs()) {
605 throw new FileNotFoundException("Couldn't create directory " + file.getPath());
606 }
607 } else {
608 if (!file.createNewFile()) {
609 throw new FileNotFoundException("Couldn't create file " + file.getPath());
610 }
611 try (final FileOutputStream fout = new FileOutputStream(file)) {
612 fout.write(content);
613 }
614 }
615 return file;
616 }
617
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900618 final static class RootInfo {
Ben Kwa448dbbb2015-04-16 18:14:35 -0700619 public final String name;
Steve McKay0c3c6952015-10-26 17:03:55 -0700620 public final StubDocument document;
Ben Kwa448dbbb2015-04-16 18:14:35 -0700621 public long capacity;
622 public long size;
623
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900624 RootInfo(File file, long capacity) {
625 this.name = file.getName();
Ben Kwa448dbbb2015-04-16 18:14:35 -0700626 this.capacity = 1024 * 1024;
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900627 this.document = StubDocument.createRootDocument(file, this);
Ben Kwa448dbbb2015-04-16 18:14:35 -0700628 this.capacity = capacity;
629 this.size = 0;
630 }
631
632 public long getRemainingCapacity() {
633 return capacity - size;
634 }
635 }
636
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900637 final static class StubDocument {
Ben Kwa448dbbb2015-04-16 18:14:35 -0700638 public final File file;
Ben Kwa448dbbb2015-04-16 18:14:35 -0700639 public final String documentId;
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900640 public final String mimeType;
641 public final List<String> streamTypes;
642 public final int flags;
Ben Kwa448dbbb2015-04-16 18:14:35 -0700643 public final String parentId;
644 public final RootInfo rootInfo;
645
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900646 private StubDocument(
647 File file, String mimeType, List<String> streamTypes, int flags,
648 StubDocument parent) {
Ben Kwa448dbbb2015-04-16 18:14:35 -0700649 this.file = file;
Ben Kwa448dbbb2015-04-16 18:14:35 -0700650 this.documentId = getDocumentIdForFile(file);
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900651 this.mimeType = mimeType;
652 this.streamTypes = streamTypes;
653 this.flags = flags;
Ben Kwa448dbbb2015-04-16 18:14:35 -0700654 this.parentId = parent.documentId;
655 this.rootInfo = parent.rootInfo;
Ben Kwa448dbbb2015-04-16 18:14:35 -0700656 }
657
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900658 private StubDocument(File file, RootInfo rootInfo) {
Ben Kwa448dbbb2015-04-16 18:14:35 -0700659 this.file = file;
Ben Kwa448dbbb2015-04-16 18:14:35 -0700660 this.documentId = getDocumentIdForFile(file);
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900661 this.mimeType = Document.MIME_TYPE_DIR;
662 this.streamTypes = new ArrayList<String>();
663 this.flags = Document.FLAG_DIR_SUPPORTS_CREATE;
Ben Kwa448dbbb2015-04-16 18:14:35 -0700664 this.parentId = null;
665 this.rootInfo = rootInfo;
Ben Kwa448dbbb2015-04-16 18:14:35 -0700666 }
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900667
668 public static StubDocument createRootDocument(File file, RootInfo rootInfo) {
669 return new StubDocument(file, rootInfo);
670 }
671
672 public static StubDocument createRegularDocument(
673 File file, String mimeType, StubDocument parent) {
674 int flags = Document.FLAG_SUPPORTS_DELETE;
675 if (file.isDirectory()) {
676 flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
677 } else {
678 flags |= Document.FLAG_SUPPORTS_WRITE;
679 }
680 return new StubDocument(file, mimeType, new ArrayList<String>(), flags, parent);
681 }
682
683 public static StubDocument createVirtualDocument(
684 File file, String mimeType, List<String> streamTypes, StubDocument parent) {
685 int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE
686 | Document.FLAG_VIRTUAL_DOCUMENT;
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900687 return new StubDocument(file, mimeType, streamTypes, flags, parent);
688 }
689
Steve McKayd3afdee2015-11-19 17:27:12 -0800690 @Override
691 public String toString() {
692 return "StubDocument{"
693 + "path:" + file.getPath()
Steve McKayd3afdee2015-11-19 17:27:12 -0800694 + ", documentId:" + documentId
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900695 + ", mimeType:" + mimeType
696 + ", streamTypes:" + streamTypes.toString()
697 + ", flags:" + flags
Steve McKayd3afdee2015-11-19 17:27:12 -0800698 + ", parentId:" + parentId
Tomasz Mikolajewskicca31eb2015-12-14 18:50:24 +0900699 + ", rootInfo:" + rootInfo
Steve McKayd3afdee2015-11-19 17:27:12 -0800700 + "}";
701 }
Ben Kwa448dbbb2015-04-16 18:14:35 -0700702 }
703
704 private static String getDocumentIdForFile(File file) {
Tomasz Mikolajewskid5d5c912015-04-13 12:17:51 +0900705 return file.getAbsolutePath();
706 }
707}