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