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