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.Context;
     20 import android.content.SharedPreferences;
     21 import android.content.pm.ProviderInfo;
     22 import android.content.res.AssetFileDescriptor;
     23 import android.database.Cursor;
     24 import android.database.MatrixCursor;
     25 import android.database.MatrixCursor.RowBuilder;
     26 import android.graphics.Point;
     27 import android.net.Uri;
     28 import android.os.Bundle;
     29 import android.os.CancellationSignal;
     30 import android.os.FileUtils;
     31 import android.os.ParcelFileDescriptor;
     32 import android.provider.DocumentsContract;
     33 import android.provider.DocumentsContract.Document;
     34 import android.provider.DocumentsContract.Root;
     35 import android.provider.DocumentsProvider;
     36 import android.support.annotation.VisibleForTesting;
     37 import android.text.TextUtils;
     38 import android.util.Log;
     39 
     40 import libcore.io.IoUtils;
     41 
     42 import java.io.File;
     43 import java.io.FileNotFoundException;
     44 import java.io.FileOutputStream;
     45 import java.io.IOException;
     46 import java.io.InputStream;
     47 import java.io.OutputStream;
     48 import java.util.ArrayList;
     49 import java.util.Arrays;
     50 import java.util.Collection;
     51 import java.util.HashMap;
     52 import java.util.HashSet;
     53 import java.util.List;
     54 import java.util.Map;
     55 import java.util.Set;
     56 
     57 public class StubProvider extends DocumentsProvider {
     58 
     59     public static final String DEFAULT_AUTHORITY = "com.android.documentsui.stubprovider";
     60     public static final String ROOT_0_ID = "TEST_ROOT_0";
     61     public static final String ROOT_1_ID = "TEST_ROOT_1";
     62 
     63     public static final String EXTRA_SIZE = "com.android.documentsui.stubprovider.SIZE";
     64     public static final String EXTRA_ROOT = "com.android.documentsui.stubprovider.ROOT";
     65     public static final String EXTRA_PATH = "com.android.documentsui.stubprovider.PATH";
     66     public static final String EXTRA_STREAM_TYPES
     67             = "com.android.documentsui.stubprovider.STREAM_TYPES";
     68     public static final String EXTRA_CONTENT = "com.android.documentsui.stubprovider.CONTENT";
     69 
     70     public static final String EXTRA_FLAGS = "com.android.documentsui.stubprovider.FLAGS";
     71     public static final String EXTRA_PARENT_ID = "com.android.documentsui.stubprovider.PARENT";
     72 
     73     private static final String TAG = "StubProvider";
     74 
     75     private static final String STORAGE_SIZE_KEY = "documentsui.stubprovider.size";
     76     private static int DEFAULT_ROOT_SIZE = 1024 * 1024 * 100; // 100 MB.
     77 
     78     private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
     79             Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
     80             Root.COLUMN_AVAILABLE_BYTES
     81     };
     82     private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
     83             Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
     84             Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
     85     };
     86 
     87     private final Map<String, StubDocument> mStorage = new HashMap<>();
     88     private final Map<String, RootInfo> mRoots = new HashMap<>();
     89     private final Object mWriteLock = new Object();
     90 
     91     private String mAuthority = DEFAULT_AUTHORITY;
     92     private SharedPreferences mPrefs;
     93     private Set<String> mSimulateReadErrorIds = new HashSet<>();
     94 
     95     @Override
     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