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