blob: 4366d318a6d50dd7a6c2ce7bb34d0f8ecb1c9185 [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
Garfield Tan15666182017-01-18 16:27:55 -080019import android.content.ContentResolver;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090020import android.content.Context;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070021import android.content.SharedPreferences;
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +090022import android.content.pm.ProviderInfo;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090023import android.content.res.AssetFileDescriptor;
24import android.database.Cursor;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090025import android.database.MatrixCursor;
Ben Kwac06f3fd2015-04-24 15:35:25 -070026import android.database.MatrixCursor.RowBuilder;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090027import android.graphics.Point;
Ben Kwac06f3fd2015-04-24 15:35:25 -070028import android.net.Uri;
Garfield Tan15666182017-01-18 16:27:55 -080029import android.os.*;
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +090030import android.provider.DocumentsContract;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090031import android.provider.DocumentsContract.Document;
32import android.provider.DocumentsContract.Root;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090033import android.provider.DocumentsProvider;
KOUSHIK PANUGANTI6ca7acc2018-04-17 16:00:10 -070034import androidx.annotation.VisibleForTesting;
Aga Wronska18410b72016-01-26 14:06:29 -080035import android.text.TextUtils;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070036import android.util.Log;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090037
Jeff Sharkey94785ef2018-07-09 16:37:41 -060038import android.os.FileUtils;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070039
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090040import java.io.File;
41import java.io.FileNotFoundException;
Ben Kwac06f3fd2015-04-24 15:35:25 -070042import java.io.FileOutputStream;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090043import java.io.IOException;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070044import java.io.InputStream;
45import java.io.OutputStream;
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +090046import java.util.ArrayList;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070047import java.util.Arrays;
48import java.util.Collection;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090049import java.util.HashMap;
Steve McKaybbeba522016-01-13 17:17:39 -080050import java.util.HashSet;
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +090051import java.util.List;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070052import java.util.Map;
Steve McKaybbeba522016-01-13 17:17:39 -080053import java.util.Set;
Garfield Tan1e80be72017-02-27 18:16:00 -080054import java.util.concurrent.CountDownLatch;
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";
Garfield Tanceedd1f2017-05-17 14:10:12 -070068 public static final String EXTRA_ENABLE_ROOT_NOTIFICATION
69 = "com.android.documentsui.stubprovider.ROOT_NOTIFICATION";
Steve McKaybbeba522016-01-13 17:17:39 -080070
Aga Wronska18410b72016-01-26 14:06:29 -080071 public static final String EXTRA_FLAGS = "com.android.documentsui.stubprovider.FLAGS";
72 public static final String EXTRA_PARENT_ID = "com.android.documentsui.stubprovider.PARENT";
73
Steve McKay99bcc6a2015-10-26 17:03:55 -070074 private static final String TAG = "StubProvider";
Steve McKaybbeba522016-01-13 17:17:39 -080075
Ben Kwaaac9e2e2015-04-16 18:14:35 -070076 private static final String STORAGE_SIZE_KEY = "documentsui.stubprovider.size";
Jun Ono418e5a32017-11-22 10:29:30 +090077 private static int DEFAULT_ROOT_SIZE = 1024 * 1024 * 500; // 500 MB.
Steve McKay99bcc6a2015-10-26 17:03:55 -070078
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090079 private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
80 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
81 Root.COLUMN_AVAILABLE_BYTES
82 };
83 private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
84 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
85 Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
86 };
87
Steve McKay99bcc6a2015-10-26 17:03:55 -070088 private final Map<String, StubDocument> mStorage = new HashMap<>();
89 private final Map<String, RootInfo> mRoots = new HashMap<>();
90 private final Object mWriteLock = new Object();
91
92 private String mAuthority = DEFAULT_AUTHORITY;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070093 private SharedPreferences mPrefs;
Steve McKaybbeba522016-01-13 17:17:39 -080094 private Set<String> mSimulateReadErrorIds = new HashSet<>();
Garfield Tan15666182017-01-18 16:27:55 -080095 private long mLoadingDuration = 0;
Garfield Tanceedd1f2017-05-17 14:10:12 -070096 private boolean mRootNotification = true;
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +090097
98 @Override
99 public void attachInfo(Context context, ProviderInfo info) {
100 mAuthority = info.authority;
101 super.attachInfo(context, info);
102 }
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900103
104 @Override
105 public boolean onCreate() {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700106 clearCacheAndBuildRoots();
107 return true;
108 }
109
Ben Kwac06f3fd2015-04-24 15:35:25 -0700110 @VisibleForTesting
111 public void clearCacheAndBuildRoots() {
Steve McKay99bcc6a2015-10-26 17:03:55 -0700112 Log.d(TAG, "Resetting storage.");
113 removeChildrenRecursively(getContext().getCacheDir());
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700114 mStorage.clear();
Steve McKaybbeba522016-01-13 17:17:39 -0800115 mSimulateReadErrorIds.clear();
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700116
117 mPrefs = getContext().getSharedPreferences(
118 "com.android.documentsui.stubprovider.preferences", Context.MODE_PRIVATE);
119 Collection<String> rootIds = mPrefs.getStringSet("roots", null);
120 if (rootIds == null) {
Steve McKay99bcc6a2015-10-26 17:03:55 -0700121 rootIds = Arrays.asList(new String[] { ROOT_0_ID, ROOT_1_ID });
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700122 }
Steve McKay99bcc6a2015-10-26 17:03:55 -0700123
124 mRoots.clear();
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700125 for (String rootId : rootIds) {
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900126 // Make a subdir in the cache dir for each root.
127 final File file = new File(getContext().getCacheDir(), rootId);
128 if (file.mkdir()) {
129 Log.i(TAG, "Created new root directory @ " + file.getPath());
130 }
131 final RootInfo rootInfo = new RootInfo(file, getSize(rootId));
Aga Wronskaee8ad9c2016-02-01 12:23:48 -0800132
133 if(rootId.equals(ROOT_1_ID)) {
134 rootInfo.setSearchEnabled(false);
135 }
136
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900137 mStorage.put(rootInfo.document.documentId, rootInfo.document);
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700138 mRoots.put(rootId, rootInfo);
139 }
Garfield Tan15666182017-01-18 16:27:55 -0800140
141 mLoadingDuration = 0;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700142 }
143
144 /**
145 * @return Storage size, in bytes.
146 */
147 private long getSize(String rootId) {
148 final String key = STORAGE_SIZE_KEY + "." + rootId;
Steve McKay99bcc6a2015-10-26 17:03:55 -0700149 return mPrefs.getLong(key, DEFAULT_ROOT_SIZE);
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900150 }
151
152 @Override
153 public Cursor queryRoots(String[] projection) throws FileNotFoundException {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700154 final MatrixCursor result = new MatrixCursor(projection != null ? projection
155 : DEFAULT_ROOT_PROJECTION);
156 for (Map.Entry<String, RootInfo> entry : mRoots.entrySet()) {
157 final String id = entry.getKey();
158 final RootInfo info = entry.getValue();
159 final RowBuilder row = result.newRow();
160 row.add(Root.COLUMN_ROOT_ID, id);
Aga Wronskaee8ad9c2016-02-01 12:23:48 -0800161 row.add(Root.COLUMN_FLAGS, info.flags);
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700162 row.add(Root.COLUMN_TITLE, id);
Steve McKay99bcc6a2015-10-26 17:03:55 -0700163 row.add(Root.COLUMN_DOCUMENT_ID, info.document.documentId);
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700164 row.add(Root.COLUMN_AVAILABLE_BYTES, info.getRemainingCapacity());
165 }
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900166 return result;
167 }
168
169 @Override
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700170 public Cursor queryDocument(String documentId, String[] projection)
171 throws FileNotFoundException {
172 final MatrixCursor result = new MatrixCursor(projection != null ? projection
173 : DEFAULT_DOCUMENT_PROJECTION);
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900174 final StubDocument file = mStorage.get(documentId);
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900175 if (file == null) {
176 throw new FileNotFoundException();
177 }
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900178 includeDocument(result, file);
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900179 return result;
180 }
181
182 @Override
Tomasz Mikolajewski67f9bed2015-04-08 19:38:55 +0900183 public boolean isChildDocument(String parentDocId, String docId) {
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900184 final StubDocument parentDocument = mStorage.get(parentDocId);
185 final StubDocument childDocument = mStorage.get(docId);
186 return FileUtils.contains(parentDocument.file, childDocument.file);
Tomasz Mikolajewski67f9bed2015-04-08 19:38:55 +0900187 }
188
189 @Override
Steve McKay99bcc6a2015-10-26 17:03:55 -0700190 public String createDocument(String parentId, String mimeType, String displayName)
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900191 throws FileNotFoundException {
Aga Wronska18410b72016-01-26 14:06:29 -0800192 StubDocument parent = mStorage.get(parentId);
193 File file = createFile(parent, mimeType, displayName);
Steve McKay99bcc6a2015-10-26 17:03:55 -0700194
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900195 final StubDocument document = StubDocument.createRegularDocument(file, mimeType, parent);
196 mStorage.put(document.documentId, document);
Ben Kwacb4461f2015-05-05 11:50:11 -0700197 Log.d(TAG, "Created document " + document.documentId);
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900198 notifyParentChanged(document.parentId);
Ben Kwac06f3fd2015-04-24 15:35:25 -0700199 getContext().getContentResolver().notifyChange(
200 DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
201 null, false);
202
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900203 return document.documentId;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900204 }
205
206 @Override
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +0900207 public void deleteDocument(String documentId)
208 throws FileNotFoundException {
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900209 final StubDocument document = mStorage.get(documentId);
210 final long fileSize = document.file.length();
211 if (document == null || !document.file.delete())
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +0900212 throw new FileNotFoundException();
213 synchronized (mWriteLock) {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700214 document.rootInfo.size -= fileSize;
Ben Kwacb4461f2015-05-05 11:50:11 -0700215 mStorage.remove(documentId);
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +0900216 }
Ben Kwacb4461f2015-05-05 11:50:11 -0700217 Log.d(TAG, "Document deleted: " + documentId);
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900218 notifyParentChanged(document.parentId);
Ben Kwac06f3fd2015-04-24 15:35:25 -0700219 getContext().getContentResolver().notifyChange(
220 DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
221 null, false);
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +0900222 }
223
224 @Override
Steve McKay31e104f2015-12-02 11:20:54 -0800225 public Cursor queryChildDocumentsForManage(String parentDocumentId, String[] projection,
226 String sortOrder) throws FileNotFoundException {
227 return queryChildDocuments(parentDocumentId, projection, sortOrder);
228 }
229
230 @Override
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900231 public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
232 throws FileNotFoundException {
Garfield Tan15666182017-01-18 16:27:55 -0800233 if (mLoadingDuration > 0) {
234 final Uri notifyUri = DocumentsContract.buildDocumentUri(mAuthority, parentDocumentId);
235 final ContentResolver resolver = getContext().getContentResolver();
236 new Handler(Looper.getMainLooper()).postDelayed(
237 () -> resolver.notifyChange(notifyUri, null, false),
238 mLoadingDuration);
239 mLoadingDuration = 0;
240
241 MatrixCursor cursor = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
242 Bundle bundle = new Bundle();
243 bundle.putBoolean(DocumentsContract.EXTRA_LOADING, true);
244 cursor.setExtras(bundle);
245 cursor.setNotificationUri(resolver, notifyUri);
246 return cursor;
247 } else {
248 final StubDocument parentDocument = mStorage.get(parentDocumentId);
249 if (parentDocument == null || parentDocument.file.isFile()) {
250 throw new FileNotFoundException();
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900251 }
Garfield Tan15666182017-01-18 16:27:55 -0800252 final MatrixCursor result = new MatrixCursor(projection != null ? projection
253 : DEFAULT_DOCUMENT_PROJECTION);
254 result.setNotificationUri(getContext().getContentResolver(),
255 DocumentsContract.buildChildDocumentsUri(mAuthority, parentDocumentId));
256 StubDocument document;
257 for (File file : parentDocument.file.listFiles()) {
258 document = mStorage.get(getDocumentIdForFile(file));
259 if (document != null) {
260 includeDocument(result, document);
261 }
262 }
263 return result;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900264 }
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900265 }
266
267 @Override
268 public Cursor queryRecentDocuments(String rootId, String[] projection)
269 throws FileNotFoundException {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700270 final MatrixCursor result = new MatrixCursor(projection != null ? projection
271 : DEFAULT_DOCUMENT_PROJECTION);
272 return result;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900273 }
274
275 @Override
Aga Wronska1f5b8922016-01-14 11:15:20 -0800276 public Cursor querySearchDocuments(String rootId, String query, String[] projection)
277 throws FileNotFoundException {
278
279 StubDocument parentDocument = mRoots.get(rootId).document;
280 if (parentDocument == null || parentDocument.file.isFile()) {
281 throw new FileNotFoundException();
282 }
283
284 final MatrixCursor result = new MatrixCursor(
285 projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
286
287 for (File file : parentDocument.file.listFiles()) {
288 if (file.getName().toLowerCase().contains(query)) {
289 StubDocument document = mStorage.get(getDocumentIdForFile(file));
290 if (document != null) {
291 includeDocument(result, document);
292 }
293 }
294 }
295 return result;
296 }
297
298 @Override
Aga Wronska18410b72016-01-26 14:06:29 -0800299 public String renameDocument(String documentId, String displayName)
300 throws FileNotFoundException {
301
302 StubDocument oldDoc = mStorage.get(documentId);
303
304 File before = oldDoc.file;
305 File after = new File(before.getParentFile(), displayName);
306
307 if (after.exists()) {
308 throw new IllegalStateException("Already exists " + after);
309 }
310
311 boolean result = before.renameTo(after);
312
313 if (!result) {
314 throw new IllegalStateException("Failed to rename to " + after);
315 }
316
317 StubDocument newDoc = StubDocument.createRegularDocument(after, oldDoc.mimeType,
318 mStorage.get(oldDoc.parentId));
319
320 mStorage.remove(documentId);
321 notifyParentChanged(oldDoc.parentId);
322 getContext().getContentResolver().notifyChange(
323 DocumentsContract.buildDocumentUri(mAuthority, oldDoc.documentId), null, false);
324
325 mStorage.put(newDoc.documentId, newDoc);
326 notifyParentChanged(newDoc.parentId);
327 getContext().getContentResolver().notifyChange(
328 DocumentsContract.buildDocumentUri(mAuthority, newDoc.documentId), null, false);
329
330 if (!TextUtils.equals(documentId, newDoc.documentId)) {
331 return newDoc.documentId;
332 } else {
333 return null;
334 }
335 }
336
337 @Override
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900338 public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
339 throws FileNotFoundException {
Steve McKaybbeba522016-01-13 17:17:39 -0800340
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900341 final StubDocument document = mStorage.get(docId);
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900342 if (document == null || !document.file.isFile()) {
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900343 throw new FileNotFoundException();
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900344 }
345 if ((document.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0) {
346 throw new IllegalStateException("Tried to open a virtual file.");
347 }
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900348
349 if ("r".equals(mode)) {
Steve McKaybbeba522016-01-13 17:17:39 -0800350 if (mSimulateReadErrorIds.contains(docId)) {
351 Log.d(TAG, "Simulated errs enabled. Open in the wrong mode.");
352 return ParcelFileDescriptor.open(
353 document.file, ParcelFileDescriptor.MODE_WRITE_ONLY);
Ben Kwac06f3fd2015-04-24 15:35:25 -0700354 }
Steve McKaybbeba522016-01-13 17:17:39 -0800355 return ParcelFileDescriptor.open(document.file, ParcelFileDescriptor.MODE_READ_ONLY);
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900356 }
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900357 if ("w".equals(mode)) {
358 return startWrite(document);
359 }
Jun Ono418e5a32017-11-22 10:29:30 +0900360 if ("wa".equals(mode)) {
361 return startWrite(document, true);
362 }
363
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900364
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900365 throw new FileNotFoundException();
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900366 }
367
Ben Kwac06f3fd2015-04-24 15:35:25 -0700368 @VisibleForTesting
Ben Kwacb4461f2015-05-05 11:50:11 -0700369 public void simulateReadErrorsForFile(Uri uri) {
Steve McKaybbeba522016-01-13 17:17:39 -0800370 simulateReadErrorsForFile(DocumentsContract.getDocumentId(uri));
371 }
372
373 public void simulateReadErrorsForFile(String id) {
374 mSimulateReadErrorIds.add(id);
Ben Kwac06f3fd2015-04-24 15:35:25 -0700375 }
376
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900377 @Override
378 public AssetFileDescriptor openDocumentThumbnail(
379 String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
380 throw new FileNotFoundException();
381 }
382
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900383 @Override
384 public AssetFileDescriptor openTypedDocument(
Steve McKaybbeba522016-01-13 17:17:39 -0800385 String docId, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900386 throws FileNotFoundException {
Steve McKaybbeba522016-01-13 17:17:39 -0800387 final StubDocument document = mStorage.get(docId);
Tomasz Mikolajewskie475d3b2015-12-24 13:24:00 +0900388 if (document == null || !document.file.isFile() || document.streamTypes == null) {
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900389 throw new FileNotFoundException();
390 }
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900391 for (final String mimeType : document.streamTypes) {
392 // Strict compare won't accept wildcards, but that's OK for tests, as DocumentsUI
Tomasz Mikolajewskidcec9ac2015-12-16 16:23:00 +0900393 // doesn't use them for getStreamTypes nor openTypedDocument.
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900394 if (mimeType.equals(mimeTypeFilter)) {
395 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
396 document.file, ParcelFileDescriptor.MODE_READ_ONLY);
Steve McKaybbeba522016-01-13 17:17:39 -0800397 if (mSimulateReadErrorIds.contains(docId)) {
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900398 pfd = new ParcelFileDescriptor(pfd) {
399 @Override
400 public void checkError() throws IOException {
401 throw new IOException("Test error");
402 }
403 };
404 }
405 return new AssetFileDescriptor(pfd, 0, document.file.length());
406 }
407 }
408 throw new IllegalArgumentException("Invalid MIME type filter for openTypedDocument().");
409 }
410
Tomasz Mikolajewskidcec9ac2015-12-16 16:23:00 +0900411 @Override
412 public String[] getStreamTypes(Uri uri, String mimeTypeFilter) {
413 final StubDocument document = mStorage.get(DocumentsContract.getDocumentId(uri));
414 if (document == null) {
415 throw new IllegalArgumentException(
416 "The provided Uri is incorrect, or the file is gone.");
417 }
Tomasz Mikolajewskidcec9ac2015-12-16 16:23:00 +0900418 if (!"*/*".equals(mimeTypeFilter)) {
419 // Not used by DocumentsUI, so don't bother implementing it.
420 throw new UnsupportedOperationException();
421 }
Tomasz Mikolajewskie475d3b2015-12-24 13:24:00 +0900422 if (document.streamTypes == null) {
423 return null;
424 }
Tomasz Mikolajewskidcec9ac2015-12-16 16:23:00 +0900425 return document.streamTypes.toArray(new String[document.streamTypes.size()]);
426 }
427
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900428 private ParcelFileDescriptor startWrite(final StubDocument document)
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900429 throws FileNotFoundException {
Jun Ono418e5a32017-11-22 10:29:30 +0900430 return startWrite(document, false);
431 }
432
433 private ParcelFileDescriptor startWrite(final StubDocument document, boolean append)
434 throws FileNotFoundException {
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900435 ParcelFileDescriptor[] pipe;
436 try {
437 pipe = ParcelFileDescriptor.createReliablePipe();
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700438 } catch (IOException exception) {
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900439 throw new FileNotFoundException();
440 }
441 final ParcelFileDescriptor readPipe = pipe[0];
442 final ParcelFileDescriptor writePipe = pipe[1];
443
Garfield Tan1e80be72017-02-27 18:16:00 -0800444 postToMainThread(() -> {
445 InputStream inputStream = null;
446 OutputStream outputStream = null;
447 try {
448 Log.d(TAG, "Opening write stream on file " + document.documentId);
449 inputStream = new ParcelFileDescriptor.AutoCloseInputStream(readPipe);
Jun Ono418e5a32017-11-22 10:29:30 +0900450 outputStream = new FileOutputStream(document.file, append);
Garfield Tan1e80be72017-02-27 18:16:00 -0800451 byte[] buffer = new byte[32 * 1024];
452 int bytesToRead;
453 int bytesRead = 0;
454 while (bytesRead != -1) {
455 synchronized (mWriteLock) {
456 // This cast is safe because the max possible value is buffer.length.
457 bytesToRead = (int) Math.min(document.rootInfo.getRemainingCapacity(),
458 buffer.length);
459 if (bytesToRead == 0) {
460 closePipeWithErrorSilently(readPipe, "Not enough space.");
461 break;
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900462 }
Garfield Tan1e80be72017-02-27 18:16:00 -0800463 bytesRead = inputStream.read(buffer, 0, bytesToRead);
464 if (bytesRead == -1) {
465 break;
466 }
467 outputStream.write(buffer, 0, bytesRead);
468 document.rootInfo.size += bytesRead;
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900469 }
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900470 }
Garfield Tan1e80be72017-02-27 18:16:00 -0800471 } catch (IOException e) {
472 Log.e(TAG, "Error on close", e);
473 closePipeWithErrorSilently(readPipe, e.getMessage());
474 } finally {
Jeff Sharkey94785ef2018-07-09 16:37:41 -0600475 FileUtils.closeQuietly(inputStream);
476 FileUtils.closeQuietly(outputStream);
Garfield Tan1e80be72017-02-27 18:16:00 -0800477 Log.d(TAG, "Closing write stream on file " + document.documentId);
478 notifyParentChanged(document.parentId);
479 getContext().getContentResolver().notifyChange(
480 DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
481 null, false);
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900482 }
Garfield Tan1e80be72017-02-27 18:16:00 -0800483 });
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900484
485 return writePipe;
486 }
487
488 private void closePipeWithErrorSilently(ParcelFileDescriptor pipe, String error) {
489 try {
490 pipe.closeWithError(error);
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700491 } catch (IOException ignore) {
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900492 }
493 }
494
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700495 @Override
496 public Bundle call(String method, String arg, Bundle extras) {
Steve McKaya495c482016-02-04 10:02:09 -0800497 // We're not supposed to override any of the default DocumentsProvider
498 // methods that are supported by "call", so javadoc asks that we
499 // always call super.call first and return if response is not null.
500 Bundle result = super.call(method, arg, extras);
501 if (result != null) {
502 return result;
503 }
504
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700505 switch (method) {
506 case "clear":
507 clearCacheAndBuildRoots();
508 return null;
509 case "configure":
510 configure(arg, extras);
511 return null;
Steve McKaybbeba522016-01-13 17:17:39 -0800512 case "createVirtualFile":
513 return createVirtualFileFromBundle(extras);
514 case "simulateReadErrorsForFile":
515 simulateReadErrorsForFile(arg);
516 return null;
Aga Wronska18410b72016-01-26 14:06:29 -0800517 case "createDocumentWithFlags":
Steve McKaya495c482016-02-04 10:02:09 -0800518 return dispatchCreateDocumentWithFlags(extras);
Garfield Tan15666182017-01-18 16:27:55 -0800519 case "setLoadingDuration":
520 mLoadingDuration = extras.getLong(DocumentsContract.EXTRA_LOADING);
521 return null;
Garfield Tan1e80be72017-02-27 18:16:00 -0800522 case "waitForWrite":
523 waitForWrite();
524 return null;
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900525 }
Steve McKaya495c482016-02-04 10:02:09 -0800526
527 return null;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700528 }
529
Steve McKaybbeba522016-01-13 17:17:39 -0800530 private Bundle createVirtualFileFromBundle(Bundle extras) {
531 try {
532 Uri uri = createVirtualFile(
533 extras.getString(EXTRA_ROOT),
534 extras.getString(EXTRA_PATH),
535 extras.getString(Document.COLUMN_MIME_TYPE),
536 extras.getStringArrayList(EXTRA_STREAM_TYPES),
537 extras.getByteArray(EXTRA_CONTENT));
538
539 String documentId = DocumentsContract.getDocumentId(uri);
540 Bundle result = new Bundle();
541 result.putString(Document.COLUMN_DOCUMENT_ID, documentId);
542 return result;
543 } catch (IOException e) {
544 Log.e(TAG, "Couldn't create virtual file.");
545 }
546
547 return null;
548 }
549
Aga Wronska18410b72016-01-26 14:06:29 -0800550 private Bundle dispatchCreateDocumentWithFlags(Bundle extras) {
551 String rootId = extras.getString(EXTRA_PARENT_ID);
552 String mimeType = extras.getString(Document.COLUMN_MIME_TYPE);
553 String name = extras.getString(Document.COLUMN_DISPLAY_NAME);
Tomasz Mikolajewskife7f5362016-03-02 17:41:40 +0900554 List<String> streamTypes = extras.getStringArrayList(EXTRA_STREAM_TYPES);
Aga Wronska18410b72016-01-26 14:06:29 -0800555 int flags = extras.getInt(EXTRA_FLAGS);
556
557 Bundle out = new Bundle();
558 String documentId = null;
559 try {
Tomasz Mikolajewskife7f5362016-03-02 17:41:40 +0900560 documentId = createDocument(rootId, mimeType, name, flags, streamTypes);
Aga Wronska18410b72016-01-26 14:06:29 -0800561 Uri uri = DocumentsContract.buildDocumentUri(mAuthority, documentId);
562 out.putParcelable(DocumentsContract.EXTRA_URI, uri);
563 } catch (FileNotFoundException e) {
Tomasz Mikolajewskife7f5362016-03-02 17:41:40 +0900564 Log.d(TAG, "Creating document with flags failed" + name);
Aga Wronska18410b72016-01-26 14:06:29 -0800565 }
566 return out;
567 }
568
Garfield Tan1e80be72017-02-27 18:16:00 -0800569 private void waitForWrite() {
570 try {
571 CountDownLatch latch = new CountDownLatch(1);
572 postToMainThread(latch::countDown);
573 latch.await();
574 Log.d(TAG, "All writing is done.");
575 } catch (InterruptedException e) {
576 // should never happen
577 throw new RuntimeException(e);
578 }
579 }
580
581 private void postToMainThread(Runnable r) {
582 new Handler(Looper.getMainLooper()).post(r);
583 }
584
Tomasz Mikolajewskife7f5362016-03-02 17:41:40 +0900585 public String createDocument(String parentId, String mimeType, String displayName, int flags,
586 List<String> streamTypes) throws FileNotFoundException {
Aga Wronska18410b72016-01-26 14:06:29 -0800587
588 StubDocument parent = mStorage.get(parentId);
589 File file = createFile(parent, mimeType, displayName);
590
591 final StubDocument document = StubDocument.createDocumentWithFlags(file, mimeType, parent,
Tomasz Mikolajewskife7f5362016-03-02 17:41:40 +0900592 flags, streamTypes);
Aga Wronska18410b72016-01-26 14:06:29 -0800593 mStorage.put(document.documentId, document);
594 Log.d(TAG, "Created document " + document.documentId);
595 notifyParentChanged(document.parentId);
596 getContext().getContentResolver().notifyChange(
597 DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
598 null, false);
599
600 return document.documentId;
601 }
602
603 private File createFile(StubDocument parent, String mimeType, String displayName)
604 throws FileNotFoundException {
605 if (parent == null) {
606 throw new IllegalArgumentException(
607 "Can't create file " + displayName + " in null parent.");
608 }
609 if (!parent.file.isDirectory()) {
610 throw new IllegalArgumentException(
611 "Can't create file " + displayName + " inside non-directory parent "
612 + parent.file.getName());
613 }
614
615 final File file = new File(parent.file, displayName);
616 if (file.exists()) {
617 throw new FileNotFoundException(
618 "Duplicate file names not supported for " + file);
619 }
620
621 if (mimeType.equals(Document.MIME_TYPE_DIR)) {
622 if (!file.mkdirs()) {
623 throw new FileNotFoundException("Failed to create directory(s): " + file);
624 }
625 Log.i(TAG, "Created new directory: " + file);
626 } else {
627 boolean created = false;
628 try {
629 created = file.createNewFile();
630 } catch (IOException e) {
631 // We'll throw an FNF exception later :)
632 Log.e(TAG, "createNewFile operation failed for file: " + file, e);
633 }
634 if (!created) {
635 throw new FileNotFoundException("createNewFile operation failed for: " + file);
636 }
637 Log.i(TAG, "Created new file: " + file);
638 }
639 return file;
640 }
641
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700642 private void configure(String arg, Bundle extras) {
643 Log.d(TAG, "Configure " + arg);
Steve McKay99bcc6a2015-10-26 17:03:55 -0700644 String rootName = extras.getString(EXTRA_ROOT, ROOT_0_ID);
Garfield Tanceedd1f2017-05-17 14:10:12 -0700645 long rootSize = extras.getLong(EXTRA_SIZE, 100) * 1024 * 1024;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700646 setSize(rootName, rootSize);
Garfield Tanceedd1f2017-05-17 14:10:12 -0700647 mRootNotification = extras.getBoolean(EXTRA_ENABLE_ROOT_NOTIFICATION, true);
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900648 }
649
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900650 private void notifyParentChanged(String parentId) {
651 getContext().getContentResolver().notifyChange(
652 DocumentsContract.buildChildDocumentsUri(mAuthority, parentId), null, false);
Garfield Tanceedd1f2017-05-17 14:10:12 -0700653 if (mRootNotification) {
654 // Notify also about possible change in remaining space on the root.
655 getContext().getContentResolver().notifyChange(
656 DocumentsContract.buildRootsUri(mAuthority), null, false);
657 }
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +0900658 }
659
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900660 private void includeDocument(MatrixCursor result, StubDocument document) {
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900661 final RowBuilder row = result.newRow();
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900662 row.add(Document.COLUMN_DOCUMENT_ID, document.documentId);
663 row.add(Document.COLUMN_DISPLAY_NAME, document.file.getName());
664 row.add(Document.COLUMN_SIZE, document.file.length());
665 row.add(Document.COLUMN_MIME_TYPE, document.mimeType);
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900666 row.add(Document.COLUMN_FLAGS, document.flags);
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900667 row.add(Document.COLUMN_LAST_MODIFIED, document.file.lastModified());
Tomasz Mikolajewski67f9bed2015-04-08 19:38:55 +0900668 }
669
Steve McKay99bcc6a2015-10-26 17:03:55 -0700670 private void removeChildrenRecursively(File file) {
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900671 for (File childFile : file.listFiles()) {
672 if (childFile.isDirectory()) {
Steve McKay99bcc6a2015-10-26 17:03:55 -0700673 removeChildrenRecursively(childFile);
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900674 }
675 childFile.delete();
676 }
677 }
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900678
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700679 public void setSize(String rootId, long rootSize) {
680 RootInfo root = mRoots.get(rootId);
681 if (root != null) {
682 final String key = STORAGE_SIZE_KEY + "." + rootId;
683 Log.d(TAG, "Set size of " + key + " : " + rootSize);
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900684
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700685 // Persist the size.
686 SharedPreferences.Editor editor = mPrefs.edit();
687 editor.putLong(key, rootSize);
688 editor.apply();
689 // Apply the size in the current instance of this provider.
690 root.capacity = rootSize;
691 getContext().getContentResolver().notifyChange(
692 DocumentsContract.buildRootsUri(mAuthority),
693 null, false);
694 } else {
695 Log.e(TAG, "Attempt to configure non-existent root: " + rootId);
696 }
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900697 }
698
Ben Kwac06f3fd2015-04-24 15:35:25 -0700699 @VisibleForTesting
Tomasz Mikolajewskidcec9ac2015-12-16 16:23:00 +0900700 public Uri createRegularFile(String rootId, String path, String mimeType, byte[] content)
701 throws FileNotFoundException, IOException {
702 final File file = createFile(rootId, path, mimeType, content);
703 final StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile()));
704 if (parent == null) {
705 throw new FileNotFoundException("Parent not found.");
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700706 }
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900707 final StubDocument document = StubDocument.createRegularDocument(file, mimeType, parent);
708 mStorage.put(document.documentId, document);
Aga Wronska18410b72016-01-26 14:06:29 -0800709 return DocumentsContract.buildDocumentUri(mAuthority, document.documentId);
Ben Kwac06f3fd2015-04-24 15:35:25 -0700710 }
711
712 @VisibleForTesting
Tomasz Mikolajewskidcec9ac2015-12-16 16:23:00 +0900713 public Uri createVirtualFile(
714 String rootId, String path, String mimeType, List<String> streamTypes, byte[] content)
715 throws FileNotFoundException, IOException {
Steve McKaybbeba522016-01-13 17:17:39 -0800716
Tomasz Mikolajewskidcec9ac2015-12-16 16:23:00 +0900717 final File file = createFile(rootId, path, mimeType, content);
718 final StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile()));
719 if (parent == null) {
720 throw new FileNotFoundException("Parent not found.");
721 }
722 final StubDocument document = StubDocument.createVirtualDocument(
723 file, mimeType, streamTypes, parent);
724 mStorage.put(document.documentId, document);
Aga Wronska18410b72016-01-26 14:06:29 -0800725 return DocumentsContract.buildDocumentUri(mAuthority, document.documentId);
Tomasz Mikolajewskidcec9ac2015-12-16 16:23:00 +0900726 }
727
728 @VisibleForTesting
Ben Kwac06f3fd2015-04-24 15:35:25 -0700729 public File getFile(String rootId, String path) throws FileNotFoundException {
Steve McKay99bcc6a2015-10-26 17:03:55 -0700730 StubDocument root = mRoots.get(rootId).document;
Ben Kwac06f3fd2015-04-24 15:35:25 -0700731 if (root == null) {
732 throw new FileNotFoundException("No roots with the ID " + rootId + " were found");
733 }
734 // Convert the path string into a path that's relative to the root.
735 File needle = new File(root.file, path.substring(1));
736
737 StubDocument found = mStorage.get(getDocumentIdForFile(needle));
738 if (found == null) {
739 return null;
740 }
741 return found.file;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700742 }
743
Tomasz Mikolajewski049eb562015-12-18 11:29:48 +0900744 private File createFile(String rootId, String path, String mimeType, byte[] content)
745 throws FileNotFoundException, IOException {
Steve McKaybbeba522016-01-13 17:17:39 -0800746 Log.d(TAG, "Creating test file " + rootId + " : " + path);
Tomasz Mikolajewski049eb562015-12-18 11:29:48 +0900747 StubDocument root = mRoots.get(rootId).document;
748 if (root == null) {
749 throw new FileNotFoundException("No roots with the ID " + rootId + " were found");
750 }
751 final File file = new File(root.file, path.substring(1));
752 if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
753 if (!file.mkdirs()) {
754 throw new FileNotFoundException("Couldn't create directory " + file.getPath());
755 }
756 } else {
757 if (!file.createNewFile()) {
758 throw new FileNotFoundException("Couldn't create file " + file.getPath());
759 }
760 try (final FileOutputStream fout = new FileOutputStream(file)) {
761 fout.write(content);
762 }
763 }
764 return file;
765 }
766
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900767 final static class RootInfo {
Aga Wronskaee8ad9c2016-02-01 12:23:48 -0800768 private static final int DEFAULT_ROOTS_FLAGS = Root.FLAG_SUPPORTS_SEARCH
769 | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD;
770
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700771 public final String name;
Steve McKay99bcc6a2015-10-26 17:03:55 -0700772 public final StubDocument document;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700773 public long capacity;
774 public long size;
Aga Wronskaee8ad9c2016-02-01 12:23:48 -0800775 public int flags;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700776
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900777 RootInfo(File file, long capacity) {
778 this.name = file.getName();
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700779 this.capacity = 1024 * 1024;
Aga Wronskaee8ad9c2016-02-01 12:23:48 -0800780 this.flags = DEFAULT_ROOTS_FLAGS;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700781 this.capacity = capacity;
782 this.size = 0;
Aga Wronskaee8ad9c2016-02-01 12:23:48 -0800783 this.document = StubDocument.createRootDocument(file, this);
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700784 }
785
786 public long getRemainingCapacity() {
787 return capacity - size;
788 }
Aga Wronskaee8ad9c2016-02-01 12:23:48 -0800789
790 public void setSearchEnabled(boolean enabled) {
791 flags = enabled ? (flags | Root.FLAG_SUPPORTS_SEARCH)
792 : (flags & ~Root.FLAG_SUPPORTS_SEARCH);
793 }
794
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700795 }
796
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900797 final static class StubDocument {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700798 public final File file;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700799 public final String documentId;
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900800 public final String mimeType;
801 public final List<String> streamTypes;
802 public final int flags;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700803 public final String parentId;
804 public final RootInfo rootInfo;
805
Aga Wronska18410b72016-01-26 14:06:29 -0800806 private StubDocument(File file, String mimeType, List<String> streamTypes, int flags,
807 StubDocument parent) {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700808 this.file = file;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700809 this.documentId = getDocumentIdForFile(file);
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900810 this.mimeType = mimeType;
811 this.streamTypes = streamTypes;
812 this.flags = flags;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700813 this.parentId = parent.documentId;
814 this.rootInfo = parent.rootInfo;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700815 }
816
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900817 private StubDocument(File file, RootInfo rootInfo) {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700818 this.file = file;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700819 this.documentId = getDocumentIdForFile(file);
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900820 this.mimeType = Document.MIME_TYPE_DIR;
Steve McKayb324dfd2016-10-28 12:52:36 -0700821 this.streamTypes = new ArrayList<>();
Aga Wronska3c237182016-01-20 16:32:33 -0800822 this.flags = Document.FLAG_DIR_SUPPORTS_CREATE | Document.FLAG_SUPPORTS_RENAME;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700823 this.parentId = null;
824 this.rootInfo = rootInfo;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700825 }
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900826
827 public static StubDocument createRootDocument(File file, RootInfo rootInfo) {
828 return new StubDocument(file, rootInfo);
829 }
830
831 public static StubDocument createRegularDocument(
832 File file, String mimeType, StubDocument parent) {
Aga Wronska18410b72016-01-26 14:06:29 -0800833 int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME;
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900834 if (file.isDirectory()) {
835 flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
836 } else {
837 flags |= Document.FLAG_SUPPORTS_WRITE;
838 }
839 return new StubDocument(file, mimeType, new ArrayList<String>(), flags, parent);
840 }
841
Aga Wronska18410b72016-01-26 14:06:29 -0800842 public static StubDocument createDocumentWithFlags(
Tomasz Mikolajewskife7f5362016-03-02 17:41:40 +0900843 File file, String mimeType, StubDocument parent, int flags,
844 List<String> streamTypes) {
845 return new StubDocument(file, mimeType, streamTypes, flags, parent);
Aga Wronska18410b72016-01-26 14:06:29 -0800846 }
847
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900848 public static StubDocument createVirtualDocument(
849 File file, String mimeType, List<String> streamTypes, StubDocument parent) {
850 int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE
851 | Document.FLAG_VIRTUAL_DOCUMENT;
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900852 return new StubDocument(file, mimeType, streamTypes, flags, parent);
853 }
854
Steve McKayf712a202015-11-19 17:27:12 -0800855 @Override
856 public String toString() {
857 return "StubDocument{"
858 + "path:" + file.getPath()
Steve McKayf712a202015-11-19 17:27:12 -0800859 + ", documentId:" + documentId
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900860 + ", mimeType:" + mimeType
861 + ", streamTypes:" + streamTypes.toString()
862 + ", flags:" + flags
Steve McKayf712a202015-11-19 17:27:12 -0800863 + ", parentId:" + parentId
Tomasz Mikolajewskicf08d6d2015-12-14 18:50:24 +0900864 + ", rootInfo:" + rootInfo
Steve McKayf712a202015-11-19 17:27:12 -0800865 + "}";
866 }
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700867 }
868
869 private static String getDocumentIdForFile(File file) {
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900870 return file.getAbsolutePath();
871 }
872}