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.Context;
20import android.content.SharedPreferences;
21import android.content.pm.ProviderInfo;
22import android.content.res.AssetFileDescriptor;
23import android.database.Cursor;
24import android.database.MatrixCursor;
25import android.database.MatrixCursor.RowBuilder;
26import android.graphics.Point;
27import android.net.Uri;
28import android.os.Bundle;
29import android.os.CancellationSignal;
30import android.os.FileUtils;
31import android.os.ParcelFileDescriptor;
32import android.provider.DocumentsContract;
33import android.provider.DocumentsContract.Document;
34import android.provider.DocumentsContract.Root;
35import android.provider.DocumentsProvider;
36import android.support.annotation.VisibleForTesting;
37import android.text.TextUtils;
38import android.util.Log;
39
40import libcore.io.IoUtils;
41
42import java.io.File;
43import java.io.FileNotFoundException;
44import java.io.FileOutputStream;
45import java.io.IOException;
46import java.io.InputStream;
47import java.io.OutputStream;
48import java.util.ArrayList;
49import java.util.Arrays;
50import java.util.Collection;
51import java.util.HashMap;
52import java.util.HashSet;
53import java.util.List;
54import java.util.Map;
55import java.util.Set;
56
57public 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
96    public void attachInfo(Context context, ProviderInfo info) {
97        mAuthority = info.authority;
98        super.attachInfo(context, info);
99    }
100
101    @Override
102    public boolean onCreate() {
103        clearCacheAndBuildRoots();
104        return true;
105    }
106
107    @VisibleForTesting
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     */
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
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
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
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
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
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
220    public Cursor queryChildDocumentsForManage(String parentDocumentId, String[] projection,
221            String sortOrder) throws FileNotFoundException {
222        return queryChildDocuments(parentDocumentId, projection, sortOrder);
223    }
224
225    @Override
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
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
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
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
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
344    public void simulateReadErrorsForFile(Uri uri) {
345        simulateReadErrorsForFile(DocumentsContract.getDocumentId(uri));
346    }
347
348    public void simulateReadErrorsForFile(String id) {
349        mSimulateReadErrorIds.add(id);
350    }
351
352    @Override
353    public AssetFileDescriptor openDocumentThumbnail(
354            String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
355        throw new FileNotFoundException();
356    }
357
358    @Override
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
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
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
461    private void closePipeWithErrorSilently(ParcelFileDescriptor pipe, String error) {
462        try {
463            pipe.closeWithError(error);
464        } catch (IOException ignore) {
465        }
466    }
467
468    @Override
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
734        public long getRemainingCapacity() {
735            return capacity - size;
736        }
737
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
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
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
775        public static StubDocument createRootDocument(File file, RootInfo rootInfo) {
776            return new StubDocument(file, rootInfo);
777        }
778
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
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
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
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
817    private static String getDocumentIdForFile(File file) {
818        return file.getAbsolutePath();
819    }
820}
821