1 /*
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 
17 package com.android.documentsui;
18 
19 import android.content.Context;
20 import android.content.SharedPreferences;
21 import android.content.pm.ProviderInfo;
22 import android.content.res.AssetFileDescriptor;
23 import android.database.Cursor;
24 import android.database.MatrixCursor;
25 import android.database.MatrixCursor.RowBuilder;
26 import android.graphics.Point;
27 import android.net.Uri;
28 import android.os.Bundle;
29 import android.os.CancellationSignal;
30 import android.os.FileUtils;
31 import android.os.ParcelFileDescriptor;
32 import android.provider.DocumentsContract;
33 import android.provider.DocumentsContract.Document;
34 import android.provider.DocumentsContract.Root;
35 import android.provider.DocumentsProvider;
36 import android.support.annotation.VisibleForTesting;
37 import android.text.TextUtils;
38 import android.util.Log;
39 
40 import libcore.io.IoUtils;
41 
42 import java.io.File;
43 import java.io.FileNotFoundException;
44 import java.io.FileOutputStream;
45 import java.io.IOException;
46 import java.io.InputStream;
47 import java.io.OutputStream;
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.Collection;
51 import java.util.HashMap;
52 import java.util.HashSet;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Set;
56 
57 public class StubProvider extends DocumentsProvider {
58 
59     public static final String DEFAULT_AUTHORITY = "com.android.documentsui.stubprovider";
60     public static final String ROOT_0_ID = "TEST_ROOT_0";
61     public static final String ROOT_1_ID = "TEST_ROOT_1";
62 
63     public static final String EXTRA_SIZE = "com.android.documentsui.stubprovider.SIZE";
64     public static final String EXTRA_ROOT = "com.android.documentsui.stubprovider.ROOT";
65     public static final String EXTRA_PATH = "com.android.documentsui.stubprovider.PATH";
66     public static final String EXTRA_STREAM_TYPES
67             = "com.android.documentsui.stubprovider.STREAM_TYPES";
68     public static final String EXTRA_CONTENT = "com.android.documentsui.stubprovider.CONTENT";
69 
70     public static final String EXTRA_FLAGS = "com.android.documentsui.stubprovider.FLAGS";
71     public static final String EXTRA_PARENT_ID = "com.android.documentsui.stubprovider.PARENT";
72 
73     private static final String TAG = "StubProvider";
74 
75     private static final String STORAGE_SIZE_KEY = "documentsui.stubprovider.size";
76     private static int DEFAULT_ROOT_SIZE = 1024 * 1024 * 100; // 100 MB.
77 
78     private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
79             Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
80             Root.COLUMN_AVAILABLE_BYTES
81     };
82     private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
83             Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
84             Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
85     };
86 
87     private final Map<String, StubDocument> mStorage = new HashMap<>();
88     private final Map<String, RootInfo> mRoots = new HashMap<>();
89     private final Object mWriteLock = new Object();
90 
91     private String mAuthority = DEFAULT_AUTHORITY;
92     private SharedPreferences mPrefs;
93     private Set<String> mSimulateReadErrorIds = new HashSet<>();
94 
95     @Override
attachInfo(Context context, ProviderInfo info)96     public void attachInfo(Context context, ProviderInfo info) {
97         mAuthority = info.authority;
98         super.attachInfo(context, info);
99     }
100 
101     @Override
onCreate()102     public boolean onCreate() {
103         clearCacheAndBuildRoots();
104         return true;
105     }
106 
107     @VisibleForTesting
clearCacheAndBuildRoots()108     public void clearCacheAndBuildRoots() {
109         Log.d(TAG, "Resetting storage.");
110         removeChildrenRecursively(getContext().getCacheDir());
111         mStorage.clear();
112         mSimulateReadErrorIds.clear();
113 
114         mPrefs = getContext().getSharedPreferences(
115                 "com.android.documentsui.stubprovider.preferences", Context.MODE_PRIVATE);
116         Collection<String> rootIds = mPrefs.getStringSet("roots", null);
117         if (rootIds == null) {
118             rootIds = Arrays.asList(new String[] { ROOT_0_ID, ROOT_1_ID });
119         }
120 
121         mRoots.clear();
122         for (String rootId : rootIds) {
123             // Make a subdir in the cache dir for each root.
124             final File file = new File(getContext().getCacheDir(), rootId);
125             if (file.mkdir()) {
126                 Log.i(TAG, "Created new root directory @ " + file.getPath());
127             }
128             final RootInfo rootInfo = new RootInfo(file, getSize(rootId));
129 
130             if(rootId.equals(ROOT_1_ID)) {
131                 rootInfo.setSearchEnabled(false);
132             }
133 
134             mStorage.put(rootInfo.document.documentId, rootInfo.document);
135             mRoots.put(rootId, rootInfo);
136         }
137     }
138 
139     /**
140      * @return Storage size, in bytes.
141      */
getSize(String rootId)142     private long getSize(String rootId) {
143         final String key = STORAGE_SIZE_KEY + "." + rootId;
144         return mPrefs.getLong(key, DEFAULT_ROOT_SIZE);
145     }
146 
147     @Override
queryRoots(String[] projection)148     public Cursor queryRoots(String[] projection) throws FileNotFoundException {
149         final MatrixCursor result = new MatrixCursor(projection != null ? projection
150                 : DEFAULT_ROOT_PROJECTION);
151         for (Map.Entry<String, RootInfo> entry : mRoots.entrySet()) {
152             final String id = entry.getKey();
153             final RootInfo info = entry.getValue();
154             final RowBuilder row = result.newRow();
155             row.add(Root.COLUMN_ROOT_ID, id);
156             row.add(Root.COLUMN_FLAGS, info.flags);
157             row.add(Root.COLUMN_TITLE, id);
158             row.add(Root.COLUMN_DOCUMENT_ID, info.document.documentId);
159             row.add(Root.COLUMN_AVAILABLE_BYTES, info.getRemainingCapacity());
160         }
161         return result;
162     }
163 
164     @Override
queryDocument(String documentId, String[] projection)165     public Cursor queryDocument(String documentId, String[] projection)
166             throws FileNotFoundException {
167         final MatrixCursor result = new MatrixCursor(projection != null ? projection
168                 : DEFAULT_DOCUMENT_PROJECTION);
169         final StubDocument file = mStorage.get(documentId);
170         if (file == null) {
171             throw new FileNotFoundException();
172         }
173         includeDocument(result, file);
174         return result;
175     }
176 
177     @Override
isChildDocument(String parentDocId, String docId)178     public boolean isChildDocument(String parentDocId, String docId) {
179         final StubDocument parentDocument = mStorage.get(parentDocId);
180         final StubDocument childDocument = mStorage.get(docId);
181         return FileUtils.contains(parentDocument.file, childDocument.file);
182     }
183 
184     @Override
createDocument(String parentId, String mimeType, String displayName)185     public String createDocument(String parentId, String mimeType, String displayName)
186             throws FileNotFoundException {
187         StubDocument parent = mStorage.get(parentId);
188         File file = createFile(parent, mimeType, displayName);
189 
190         final StubDocument document = StubDocument.createRegularDocument(file, mimeType, parent);
191         mStorage.put(document.documentId, document);
192         Log.d(TAG, "Created document " + document.documentId);
193         notifyParentChanged(document.parentId);
194         getContext().getContentResolver().notifyChange(
195                 DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
196                 null, false);
197 
198         return document.documentId;
199     }
200 
201     @Override
deleteDocument(String documentId)202     public void deleteDocument(String documentId)
203             throws FileNotFoundException {
204         final StubDocument document = mStorage.get(documentId);
205         final long fileSize = document.file.length();
206         if (document == null || !document.file.delete())
207             throw new FileNotFoundException();
208         synchronized (mWriteLock) {
209             document.rootInfo.size -= fileSize;
210             mStorage.remove(documentId);
211         }
212         Log.d(TAG, "Document deleted: " + documentId);
213         notifyParentChanged(document.parentId);
214         getContext().getContentResolver().notifyChange(
215                 DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
216                 null, false);
217     }
218 
219     @Override
queryChildDocumentsForManage(String parentDocumentId, String[] projection, String sortOrder)220     public Cursor queryChildDocumentsForManage(String parentDocumentId, String[] projection,
221             String sortOrder) throws FileNotFoundException {
222         return queryChildDocuments(parentDocumentId, projection, sortOrder);
223     }
224 
225     @Override
queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)226     public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
227             throws FileNotFoundException {
228         final StubDocument parentDocument = mStorage.get(parentDocumentId);
229         if (parentDocument == null || parentDocument.file.isFile()) {
230             throw new FileNotFoundException();
231         }
232         final MatrixCursor result = new MatrixCursor(projection != null ? projection
233                 : DEFAULT_DOCUMENT_PROJECTION);
234         result.setNotificationUri(getContext().getContentResolver(),
235                 DocumentsContract.buildChildDocumentsUri(mAuthority, parentDocumentId));
236         StubDocument document;
237         for (File file : parentDocument.file.listFiles()) {
238             document = mStorage.get(getDocumentIdForFile(file));
239             if (document != null) {
240                 includeDocument(result, document);
241             }
242         }
243         return result;
244     }
245 
246     @Override
queryRecentDocuments(String rootId, String[] projection)247     public Cursor queryRecentDocuments(String rootId, String[] projection)
248             throws FileNotFoundException {
249         final MatrixCursor result = new MatrixCursor(projection != null ? projection
250                 : DEFAULT_DOCUMENT_PROJECTION);
251         return result;
252     }
253 
254     @Override
querySearchDocuments(String rootId, String query, String[] projection)255     public Cursor querySearchDocuments(String rootId, String query, String[] projection)
256             throws FileNotFoundException {
257 
258         StubDocument parentDocument = mRoots.get(rootId).document;
259         if (parentDocument == null || parentDocument.file.isFile()) {
260             throw new FileNotFoundException();
261         }
262 
263         final MatrixCursor result = new MatrixCursor(
264                 projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
265 
266         for (File file : parentDocument.file.listFiles()) {
267             if (file.getName().toLowerCase().contains(query)) {
268                 StubDocument document = mStorage.get(getDocumentIdForFile(file));
269                 if (document != null) {
270                     includeDocument(result, document);
271                 }
272             }
273         }
274         return result;
275     }
276 
277     @Override
renameDocument(String documentId, String displayName)278     public String renameDocument(String documentId, String displayName)
279             throws FileNotFoundException {
280 
281         StubDocument oldDoc = mStorage.get(documentId);
282 
283         File before = oldDoc.file;
284         File after = new File(before.getParentFile(), displayName);
285 
286         if (after.exists()) {
287             throw new IllegalStateException("Already exists " + after);
288         }
289 
290         boolean result = before.renameTo(after);
291 
292         if (!result) {
293             throw new IllegalStateException("Failed to rename to " + after);
294         }
295 
296         StubDocument newDoc = StubDocument.createRegularDocument(after, oldDoc.mimeType,
297                 mStorage.get(oldDoc.parentId));
298 
299         mStorage.remove(documentId);
300         notifyParentChanged(oldDoc.parentId);
301         getContext().getContentResolver().notifyChange(
302                 DocumentsContract.buildDocumentUri(mAuthority, oldDoc.documentId), null, false);
303 
304         mStorage.put(newDoc.documentId, newDoc);
305         notifyParentChanged(newDoc.parentId);
306         getContext().getContentResolver().notifyChange(
307                 DocumentsContract.buildDocumentUri(mAuthority, newDoc.documentId), null, false);
308 
309         if (!TextUtils.equals(documentId, newDoc.documentId)) {
310             return newDoc.documentId;
311         } else {
312             return null;
313         }
314     }
315 
316     @Override
openDocument(String docId, String mode, CancellationSignal signal)317     public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
318             throws FileNotFoundException {
319 
320         final StubDocument document = mStorage.get(docId);
321         if (document == null || !document.file.isFile()) {
322             throw new FileNotFoundException();
323         }
324         if ((document.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0) {
325             throw new IllegalStateException("Tried to open a virtual file.");
326         }
327 
328         if ("r".equals(mode)) {
329             if (mSimulateReadErrorIds.contains(docId)) {
330                 Log.d(TAG, "Simulated errs enabled. Open in the wrong mode.");
331                 return ParcelFileDescriptor.open(
332                         document.file, ParcelFileDescriptor.MODE_WRITE_ONLY);
333             }
334             return ParcelFileDescriptor.open(document.file, ParcelFileDescriptor.MODE_READ_ONLY);
335         }
336         if ("w".equals(mode)) {
337             return startWrite(document);
338         }
339 
340         throw new FileNotFoundException();
341     }
342 
343     @VisibleForTesting
simulateReadErrorsForFile(Uri uri)344     public void simulateReadErrorsForFile(Uri uri) {
345         simulateReadErrorsForFile(DocumentsContract.getDocumentId(uri));
346     }
347 
simulateReadErrorsForFile(String id)348     public void simulateReadErrorsForFile(String id) {
349         mSimulateReadErrorIds.add(id);
350     }
351 
352     @Override
openDocumentThumbnail( String docId, Point sizeHint, CancellationSignal signal)353     public AssetFileDescriptor openDocumentThumbnail(
354             String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
355         throw new FileNotFoundException();
356     }
357 
358     @Override
openTypedDocument( String docId, String mimeTypeFilter, Bundle opts, CancellationSignal signal)359     public AssetFileDescriptor openTypedDocument(
360             String docId, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
361             throws FileNotFoundException {
362         final StubDocument document = mStorage.get(docId);
363         if (document == null || !document.file.isFile() || document.streamTypes == null) {
364             throw new FileNotFoundException();
365         }
366         for (final String mimeType : document.streamTypes) {
367             // Strict compare won't accept wildcards, but that's OK for tests, as DocumentsUI
368             // doesn't use them for getStreamTypes nor openTypedDocument.
369             if (mimeType.equals(mimeTypeFilter)) {
370                 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
371                             document.file, ParcelFileDescriptor.MODE_READ_ONLY);
372                 if (mSimulateReadErrorIds.contains(docId)) {
373                     pfd = new ParcelFileDescriptor(pfd) {
374                         @Override
375                         public void checkError() throws IOException {
376                             throw new IOException("Test error");
377                         }
378                     };
379                 }
380                 return new AssetFileDescriptor(pfd, 0, document.file.length());
381             }
382         }
383         throw new IllegalArgumentException("Invalid MIME type filter for openTypedDocument().");
384     }
385 
386     @Override
getStreamTypes(Uri uri, String mimeTypeFilter)387     public String[] getStreamTypes(Uri uri, String mimeTypeFilter) {
388         final StubDocument document = mStorage.get(DocumentsContract.getDocumentId(uri));
389         if (document == null) {
390             throw new IllegalArgumentException(
391                     "The provided Uri is incorrect, or the file is gone.");
392         }
393         if (!"*/*".equals(mimeTypeFilter)) {
394             // Not used by DocumentsUI, so don't bother implementing it.
395             throw new UnsupportedOperationException();
396         }
397         if (document.streamTypes == null) {
398             return null;
399         }
400         return document.streamTypes.toArray(new String[document.streamTypes.size()]);
401     }
402 
startWrite(final StubDocument document)403     private ParcelFileDescriptor startWrite(final StubDocument document)
404             throws FileNotFoundException {
405         ParcelFileDescriptor[] pipe;
406         try {
407             pipe = ParcelFileDescriptor.createReliablePipe();
408         } catch (IOException exception) {
409             throw new FileNotFoundException();
410         }
411         final ParcelFileDescriptor readPipe = pipe[0];
412         final ParcelFileDescriptor writePipe = pipe[1];
413 
414         new Thread() {
415             @Override
416             public void run() {
417                 InputStream inputStream = null;
418                 OutputStream outputStream = null;
419                 try {
420                     Log.d(TAG, "Opening write stream on file " + document.documentId);
421                     inputStream = new ParcelFileDescriptor.AutoCloseInputStream(readPipe);
422                     outputStream = new FileOutputStream(document.file);
423                     byte[] buffer = new byte[32 * 1024];
424                     int bytesToRead;
425                     int bytesRead = 0;
426                     while (bytesRead != -1) {
427                         synchronized (mWriteLock) {
428                             // This cast is safe because the max possible value is buffer.length.
429                             bytesToRead = (int) Math.min(document.rootInfo.getRemainingCapacity(),
430                                     buffer.length);
431                             if (bytesToRead == 0) {
432                                 closePipeWithErrorSilently(readPipe, "Not enough space.");
433                                 break;
434                             }
435                             bytesRead = inputStream.read(buffer, 0, bytesToRead);
436                             if (bytesRead == -1) {
437                                 break;
438                             }
439                             outputStream.write(buffer, 0, bytesRead);
440                             document.rootInfo.size += bytesRead;
441                         }
442                     }
443                 } catch (IOException e) {
444                     Log.e(TAG, "Error on close", e);
445                     closePipeWithErrorSilently(readPipe, e.getMessage());
446                 } finally {
447                     IoUtils.closeQuietly(inputStream);
448                     IoUtils.closeQuietly(outputStream);
449                     Log.d(TAG, "Closing write stream on file " + document.documentId);
450                     notifyParentChanged(document.parentId);
451                     getContext().getContentResolver().notifyChange(
452                             DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
453                             null, false);
454                 }
455             }
456         }.start();
457 
458         return writePipe;
459     }
460 
closePipeWithErrorSilently(ParcelFileDescriptor pipe, String error)461     private void closePipeWithErrorSilently(ParcelFileDescriptor pipe, String error) {
462         try {
463             pipe.closeWithError(error);
464         } catch (IOException ignore) {
465         }
466     }
467 
468     @Override
call(String method, String arg, Bundle extras)469     public Bundle call(String method, String arg, Bundle extras) {
470         // We're not supposed to override any of the default DocumentsProvider
471         // methods that are supported by "call", so javadoc asks that we
472         // always call super.call first and return if response is not null.
473         Bundle result = super.call(method, arg, extras);
474         if (result != null) {
475             return result;
476         }
477 
478         switch (method) {
479             case "clear":
480                 clearCacheAndBuildRoots();
481                 return null;
482             case "configure":
483                 configure(arg, extras);
484                 return null;
485             case "createVirtualFile":
486                 return createVirtualFileFromBundle(extras);
487             case "simulateReadErrorsForFile":
488                 simulateReadErrorsForFile(arg);
489                 return null;
490             case "createDocumentWithFlags":
491                 return dispatchCreateDocumentWithFlags(extras);
492         }
493 
494         return null;
495     }
496 
createVirtualFileFromBundle(Bundle extras)497     private Bundle createVirtualFileFromBundle(Bundle extras) {
498         try {
499             Uri uri = createVirtualFile(
500                     extras.getString(EXTRA_ROOT),
501                     extras.getString(EXTRA_PATH),
502                     extras.getString(Document.COLUMN_MIME_TYPE),
503                     extras.getStringArrayList(EXTRA_STREAM_TYPES),
504                     extras.getByteArray(EXTRA_CONTENT));
505 
506             String documentId = DocumentsContract.getDocumentId(uri);
507             Bundle result = new Bundle();
508             result.putString(Document.COLUMN_DOCUMENT_ID, documentId);
509             return result;
510         } catch (IOException e) {
511             Log.e(TAG, "Couldn't create virtual file.");
512         }
513 
514         return null;
515     }
516 
dispatchCreateDocumentWithFlags(Bundle extras)517     private Bundle dispatchCreateDocumentWithFlags(Bundle extras) {
518         String rootId = extras.getString(EXTRA_PARENT_ID);
519         String mimeType = extras.getString(Document.COLUMN_MIME_TYPE);
520         String name = extras.getString(Document.COLUMN_DISPLAY_NAME);
521         List<String> streamTypes = extras.getStringArrayList(EXTRA_STREAM_TYPES);
522         int flags = extras.getInt(EXTRA_FLAGS);
523 
524         Bundle out = new Bundle();
525         String documentId = null;
526         try {
527             documentId = createDocument(rootId, mimeType, name, flags, streamTypes);
528             Uri uri = DocumentsContract.buildDocumentUri(mAuthority, documentId);
529             out.putParcelable(DocumentsContract.EXTRA_URI, uri);
530         } catch (FileNotFoundException e) {
531             Log.d(TAG, "Creating document with flags failed" + name);
532         }
533         return out;
534     }
535 
createDocument(String parentId, String mimeType, String displayName, int flags, List<String> streamTypes)536     public String createDocument(String parentId, String mimeType, String displayName, int flags,
537             List<String> streamTypes) throws FileNotFoundException {
538 
539         StubDocument parent = mStorage.get(parentId);
540         File file = createFile(parent, mimeType, displayName);
541 
542         final StubDocument document = StubDocument.createDocumentWithFlags(file, mimeType, parent,
543                 flags, streamTypes);
544         mStorage.put(document.documentId, document);
545         Log.d(TAG, "Created document " + document.documentId);
546         notifyParentChanged(document.parentId);
547         getContext().getContentResolver().notifyChange(
548                 DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
549                 null, false);
550 
551         return document.documentId;
552     }
553 
createFile(StubDocument parent, String mimeType, String displayName)554     private File createFile(StubDocument parent, String mimeType, String displayName)
555             throws FileNotFoundException {
556         if (parent == null) {
557             throw new IllegalArgumentException(
558                     "Can't create file " + displayName + " in null parent.");
559         }
560         if (!parent.file.isDirectory()) {
561             throw new IllegalArgumentException(
562                     "Can't create file " + displayName + " inside non-directory parent "
563                             + parent.file.getName());
564         }
565 
566         final File file = new File(parent.file, displayName);
567         if (file.exists()) {
568             throw new FileNotFoundException(
569                     "Duplicate file names not supported for " + file);
570         }
571 
572         if (mimeType.equals(Document.MIME_TYPE_DIR)) {
573             if (!file.mkdirs()) {
574                 throw new FileNotFoundException("Failed to create directory(s): " + file);
575             }
576             Log.i(TAG, "Created new directory: " + file);
577         } else {
578             boolean created = false;
579             try {
580                 created = file.createNewFile();
581             } catch (IOException e) {
582                 // We'll throw an FNF exception later :)
583                 Log.e(TAG, "createNewFile operation failed for file: " + file, e);
584             }
585             if (!created) {
586                 throw new FileNotFoundException("createNewFile operation failed for: " + file);
587             }
588             Log.i(TAG, "Created new file: " + file);
589         }
590         return file;
591     }
592 
configure(String arg, Bundle extras)593     private void configure(String arg, Bundle extras) {
594         Log.d(TAG, "Configure " + arg);
595         String rootName = extras.getString(EXTRA_ROOT, ROOT_0_ID);
596         long rootSize = extras.getLong(EXTRA_SIZE, 1) * 1024 * 1024;
597         setSize(rootName, rootSize);
598     }
599 
notifyParentChanged(String parentId)600     private void notifyParentChanged(String parentId) {
601         getContext().getContentResolver().notifyChange(
602                 DocumentsContract.buildChildDocumentsUri(mAuthority, parentId), null, false);
603         // Notify also about possible change in remaining space on the root.
604         getContext().getContentResolver().notifyChange(DocumentsContract.buildRootsUri(mAuthority),
605                 null, false);
606     }
607 
includeDocument(MatrixCursor result, StubDocument document)608     private void includeDocument(MatrixCursor result, StubDocument document) {
609         final RowBuilder row = result.newRow();
610         row.add(Document.COLUMN_DOCUMENT_ID, document.documentId);
611         row.add(Document.COLUMN_DISPLAY_NAME, document.file.getName());
612         row.add(Document.COLUMN_SIZE, document.file.length());
613         row.add(Document.COLUMN_MIME_TYPE, document.mimeType);
614         row.add(Document.COLUMN_FLAGS, document.flags);
615         row.add(Document.COLUMN_LAST_MODIFIED, document.file.lastModified());
616     }
617 
removeChildrenRecursively(File file)618     private void removeChildrenRecursively(File file) {
619         for (File childFile : file.listFiles()) {
620             if (childFile.isDirectory()) {
621                 removeChildrenRecursively(childFile);
622             }
623             childFile.delete();
624         }
625     }
626 
setSize(String rootId, long rootSize)627     public void setSize(String rootId, long rootSize) {
628         RootInfo root = mRoots.get(rootId);
629         if (root != null) {
630             final String key = STORAGE_SIZE_KEY + "." + rootId;
631             Log.d(TAG, "Set size of " + key + " : " + rootSize);
632 
633             // Persist the size.
634             SharedPreferences.Editor editor = mPrefs.edit();
635             editor.putLong(key, rootSize);
636             editor.apply();
637             // Apply the size in the current instance of this provider.
638             root.capacity = rootSize;
639             getContext().getContentResolver().notifyChange(
640                     DocumentsContract.buildRootsUri(mAuthority),
641                     null, false);
642         } else {
643             Log.e(TAG, "Attempt to configure non-existent root: " + rootId);
644         }
645     }
646 
647     @VisibleForTesting
createRegularFile(String rootId, String path, String mimeType, byte[] content)648     public Uri createRegularFile(String rootId, String path, String mimeType, byte[] content)
649             throws FileNotFoundException, IOException {
650         final File file = createFile(rootId, path, mimeType, content);
651         final StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile()));
652         if (parent == null) {
653             throw new FileNotFoundException("Parent not found.");
654         }
655         final StubDocument document = StubDocument.createRegularDocument(file, mimeType, parent);
656         mStorage.put(document.documentId, document);
657         return DocumentsContract.buildDocumentUri(mAuthority, document.documentId);
658     }
659 
660     @VisibleForTesting
createVirtualFile( String rootId, String path, String mimeType, List<String> streamTypes, byte[] content)661     public Uri createVirtualFile(
662             String rootId, String path, String mimeType, List<String> streamTypes, byte[] content)
663             throws FileNotFoundException, IOException {
664 
665         final File file = createFile(rootId, path, mimeType, content);
666         final StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile()));
667         if (parent == null) {
668             throw new FileNotFoundException("Parent not found.");
669         }
670         final StubDocument document = StubDocument.createVirtualDocument(
671                 file, mimeType, streamTypes, parent);
672         mStorage.put(document.documentId, document);
673         return DocumentsContract.buildDocumentUri(mAuthority, document.documentId);
674     }
675 
676     @VisibleForTesting
getFile(String rootId, String path)677     public File getFile(String rootId, String path) throws FileNotFoundException {
678         StubDocument root = mRoots.get(rootId).document;
679         if (root == null) {
680             throw new FileNotFoundException("No roots with the ID " + rootId + " were found");
681         }
682         // Convert the path string into a path that's relative to the root.
683         File needle = new File(root.file, path.substring(1));
684 
685         StubDocument found = mStorage.get(getDocumentIdForFile(needle));
686         if (found == null) {
687             return null;
688         }
689         return found.file;
690     }
691 
createFile(String rootId, String path, String mimeType, byte[] content)692     private File createFile(String rootId, String path, String mimeType, byte[] content)
693             throws FileNotFoundException, IOException {
694         Log.d(TAG, "Creating test file " + rootId + " : " + path);
695         StubDocument root = mRoots.get(rootId).document;
696         if (root == null) {
697             throw new FileNotFoundException("No roots with the ID " + rootId + " were found");
698         }
699         final File file = new File(root.file, path.substring(1));
700         if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
701             if (!file.mkdirs()) {
702                 throw new FileNotFoundException("Couldn't create directory " + file.getPath());
703             }
704         } else {
705             if (!file.createNewFile()) {
706                 throw new FileNotFoundException("Couldn't create file " + file.getPath());
707             }
708             try (final FileOutputStream fout = new FileOutputStream(file)) {
709                 fout.write(content);
710             }
711         }
712         return file;
713     }
714 
715     final static class RootInfo {
716         private static final int DEFAULT_ROOTS_FLAGS = Root.FLAG_SUPPORTS_SEARCH
717                 | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD;
718 
719         public final String name;
720         public final StubDocument document;
721         public long capacity;
722         public long size;
723         public int flags;
724 
RootInfo(File file, long capacity)725         RootInfo(File file, long capacity) {
726             this.name = file.getName();
727             this.capacity = 1024 * 1024;
728             this.flags = DEFAULT_ROOTS_FLAGS;
729             this.capacity = capacity;
730             this.size = 0;
731             this.document = StubDocument.createRootDocument(file, this);
732         }
733 
getRemainingCapacity()734         public long getRemainingCapacity() {
735             return capacity - size;
736         }
737 
setSearchEnabled(boolean enabled)738         public void setSearchEnabled(boolean enabled) {
739             flags = enabled ? (flags | Root.FLAG_SUPPORTS_SEARCH)
740                     : (flags & ~Root.FLAG_SUPPORTS_SEARCH);
741         }
742 
743     }
744 
745     final static class StubDocument {
746         public final File file;
747         public final String documentId;
748         public final String mimeType;
749         public final List<String> streamTypes;
750         public final int flags;
751         public final String parentId;
752         public final RootInfo rootInfo;
753 
StubDocument(File file, String mimeType, List<String> streamTypes, int flags, StubDocument parent)754         private StubDocument(File file, String mimeType, List<String> streamTypes, int flags,
755                 StubDocument parent) {
756             this.file = file;
757             this.documentId = getDocumentIdForFile(file);
758             this.mimeType = mimeType;
759             this.streamTypes = streamTypes;
760             this.flags = flags;
761             this.parentId = parent.documentId;
762             this.rootInfo = parent.rootInfo;
763         }
764 
StubDocument(File file, RootInfo rootInfo)765         private StubDocument(File file, RootInfo rootInfo) {
766             this.file = file;
767             this.documentId = getDocumentIdForFile(file);
768             this.mimeType = Document.MIME_TYPE_DIR;
769             this.streamTypes = new ArrayList<String>();
770             this.flags = Document.FLAG_DIR_SUPPORTS_CREATE | Document.FLAG_SUPPORTS_RENAME;
771             this.parentId = null;
772             this.rootInfo = rootInfo;
773         }
774 
createRootDocument(File file, RootInfo rootInfo)775         public static StubDocument createRootDocument(File file, RootInfo rootInfo) {
776             return new StubDocument(file, rootInfo);
777         }
778 
createRegularDocument( File file, String mimeType, StubDocument parent)779         public static StubDocument createRegularDocument(
780                 File file, String mimeType, StubDocument parent) {
781             int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME;
782             if (file.isDirectory()) {
783                 flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
784             } else {
785                 flags |= Document.FLAG_SUPPORTS_WRITE;
786             }
787             return new StubDocument(file, mimeType, new ArrayList<String>(), flags, parent);
788         }
789 
createDocumentWithFlags( File file, String mimeType, StubDocument parent, int flags, List<String> streamTypes)790         public static StubDocument createDocumentWithFlags(
791                 File file, String mimeType, StubDocument parent, int flags,
792                 List<String> streamTypes) {
793             return new StubDocument(file, mimeType, streamTypes, flags, parent);
794         }
795 
createVirtualDocument( File file, String mimeType, List<String> streamTypes, StubDocument parent)796         public static StubDocument createVirtualDocument(
797                 File file, String mimeType, List<String> streamTypes, StubDocument parent) {
798             int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE
799                     | Document.FLAG_VIRTUAL_DOCUMENT;
800             return new StubDocument(file, mimeType, streamTypes, flags, parent);
801         }
802 
803         @Override
toString()804         public String toString() {
805             return "StubDocument{"
806                     + "path:" + file.getPath()
807                     + ", documentId:" + documentId
808                     + ", mimeType:" + mimeType
809                     + ", streamTypes:" + streamTypes.toString()
810                     + ", flags:" + flags
811                     + ", parentId:" + parentId
812                     + ", rootInfo:" + rootInfo
813                     + "}";
814         }
815     }
816 
getDocumentIdForFile(File file)817     private static String getDocumentIdForFile(File file) {
818         return file.getAbsolutePath();
819     }
820 }
821