Home | History | Annotate | Download | only in documentprovider
      1 /*
      2  * Copyright (C) 2014 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.cts.documentprovider;
     18 
     19 import android.app.PendingIntent;
     20 import android.content.Intent;
     21 import android.content.IntentSender;
     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.net.Uri;
     27 import android.os.AsyncTask;
     28 import android.os.Bundle;
     29 import android.os.CancellationSignal;
     30 import android.os.ParcelFileDescriptor;
     31 import android.provider.DocumentsContract;
     32 import android.provider.DocumentsContract.Document;
     33 import android.provider.DocumentsContract.Path;
     34 import android.provider.DocumentsContract.Root;
     35 import android.provider.DocumentsProvider;
     36 import android.text.TextUtils;
     37 import android.util.Log;
     38 
     39 import java.io.ByteArrayOutputStream;
     40 import java.io.FileNotFoundException;
     41 import java.io.IOException;
     42 import java.io.InputStream;
     43 import java.io.OutputStream;
     44 import java.util.ArrayList;
     45 import java.util.HashMap;
     46 import java.util.LinkedList;
     47 import java.util.List;
     48 import java.util.Map;
     49 import java.util.concurrent.atomic.AtomicInteger;
     50 
     51 public class MyDocumentsProvider extends DocumentsProvider {
     52     private static final String TAG = "TestDocumentsProvider";
     53 
     54     private static final String AUTHORITY = "com.android.cts.documentprovider";
     55 
     56     private static final int WEB_LINK_REQUEST_CODE = 321;
     57 
     58     private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
     59             Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
     60             Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES,
     61     };
     62 
     63     private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
     64             Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
     65             Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
     66     };
     67 
     68     private static String[] resolveRootProjection(String[] projection) {
     69         return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
     70     }
     71 
     72     private static String[] resolveDocumentProjection(String[] projection) {
     73         return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
     74     }
     75 
     76     private boolean mEjected = false;
     77 
     78     @Override
     79     public boolean onCreate() {
     80         resetRoots();
     81         return true;
     82     }
     83 
     84     @Override
     85     public Cursor queryRoots(String[] projection) throws FileNotFoundException {
     86         final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
     87 
     88         RowBuilder row = result.newRow();
     89         row.add(Root.COLUMN_ROOT_ID, "local");
     90         row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_SEARCH);
     91         row.add(Root.COLUMN_TITLE, "CtsLocal");
     92         row.add(Root.COLUMN_SUMMARY, "CtsLocalSummary");
     93         row.add(Root.COLUMN_DOCUMENT_ID, "doc:local");
     94 
     95         row = result.newRow();
     96         row.add(Root.COLUMN_ROOT_ID, "create");
     97         row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD);
     98         row.add(Root.COLUMN_TITLE, "CtsCreate");
     99         row.add(Root.COLUMN_DOCUMENT_ID, "doc:create");
    100 
    101         if (!mEjected) {
    102             row = result.newRow();
    103             row.add(Root.COLUMN_ROOT_ID, "eject");
    104             row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_EJECT);
    105             row.add(Root.COLUMN_TITLE, "eject");
    106             // Reuse local docs, but not used for testing
    107             row.add(Root.COLUMN_DOCUMENT_ID, "doc:local");
    108         }
    109 
    110         return result;
    111     }
    112 
    113     private Map<String, Doc> mDocs = new HashMap<>();
    114 
    115     private Doc mLocalRoot;
    116     private Doc mCreateRoot;
    117     private final AtomicInteger mNextDocId = new AtomicInteger(0);
    118 
    119     private Doc buildDoc(String docId, String displayName, String mimeType,
    120             String[] streamTypes) {
    121         final Doc doc = new Doc();
    122         doc.docId = docId;
    123         doc.displayName = displayName;
    124         doc.mimeType = mimeType;
    125         doc.streamTypes = streamTypes;
    126         mDocs.put(doc.docId, doc);
    127         return doc;
    128     }
    129 
    130     public void resetRoots() {
    131         Log.d(TAG, "resetRoots()");
    132 
    133         mEjected = false;
    134 
    135         mDocs.clear();
    136 
    137         mLocalRoot = buildDoc("doc:local", null, Document.MIME_TYPE_DIR, null);
    138 
    139         mCreateRoot = buildDoc("doc:create", null, Document.MIME_TYPE_DIR, null);
    140         mCreateRoot.flags = Document.FLAG_DIR_SUPPORTS_CREATE;
    141 
    142         {
    143             Doc file1 = buildDoc("doc:file1", "FILE1", "mime1/file1", null);
    144             file1.contents = "fileone".getBytes();
    145             file1.flags = Document.FLAG_SUPPORTS_WRITE;
    146             mLocalRoot.children.add(file1);
    147             mCreateRoot.children.add(file1);
    148         }
    149 
    150         {
    151             Doc file2 = buildDoc("doc:file2", "FILE2", "mime2/file2", null);
    152             file2.contents = "filetwo".getBytes();
    153             file2.flags = Document.FLAG_SUPPORTS_WRITE;
    154             mLocalRoot.children.add(file2);
    155             mCreateRoot.children.add(file2);
    156         }
    157 
    158         {
    159             Doc virtualFile = buildDoc("doc:virtual-file", "VIRTUAL_FILE", "application/icecream",
    160                     new String[] { "text/plain" });
    161             virtualFile.flags = Document.FLAG_VIRTUAL_DOCUMENT;
    162             virtualFile.contents = "Converted contents.".getBytes();
    163             mLocalRoot.children.add(virtualFile);
    164             mCreateRoot.children.add(virtualFile);
    165         }
    166 
    167         {
    168             Doc webLinkableFile = buildDoc("doc:web-linkable-file", "WEB_LINKABLE_FILE",
    169                     "application/icecream", new String[] { "text/plain" });
    170             webLinkableFile.flags = Document.FLAG_VIRTUAL_DOCUMENT | Document.FLAG_WEB_LINKABLE;
    171             webLinkableFile.contents = "Fake contents.".getBytes();
    172             mLocalRoot.children.add(webLinkableFile);
    173             mCreateRoot.children.add(webLinkableFile);
    174         }
    175 
    176         Doc dir1 = buildDoc("doc:dir1", "DIR1", Document.MIME_TYPE_DIR, null);
    177         mLocalRoot.children.add(dir1);
    178 
    179         {
    180             Doc file3 = buildDoc("doc:file3", "FILE3", "mime3/file3", null);
    181             file3.contents = "filethree".getBytes();
    182             file3.flags = Document.FLAG_SUPPORTS_WRITE;
    183             dir1.children.add(file3);
    184         }
    185 
    186         Doc dir2 = buildDoc("doc:dir2", "DIR2", Document.MIME_TYPE_DIR, null);
    187         mCreateRoot.children.add(dir2);
    188 
    189         {
    190             Doc file4 = buildDoc("doc:file4", "FILE4", "mime4/file4", null);
    191             file4.contents = "filefour".getBytes();
    192             file4.flags = Document.FLAG_SUPPORTS_WRITE |
    193                     Document.FLAG_SUPPORTS_COPY |
    194                     Document.FLAG_SUPPORTS_MOVE |
    195                     Document.FLAG_SUPPORTS_REMOVE;
    196             dir2.children.add(file4);
    197 
    198             Doc subDir2 = buildDoc("doc:sub_dir2", "SUB_DIR2", Document.MIME_TYPE_DIR, null);
    199             dir2.children.add(subDir2);
    200         }
    201     }
    202 
    203     private static class Doc {
    204         public String docId;
    205         public int flags;
    206         public String displayName;
    207         public long size;
    208         public String mimeType;
    209         public String[] streamTypes;
    210         public long lastModified;
    211         public byte[] contents;
    212         public List<Doc> children = new ArrayList<>();
    213 
    214         public void include(MatrixCursor result) {
    215             final RowBuilder row = result.newRow();
    216             row.add(Document.COLUMN_DOCUMENT_ID, docId);
    217             row.add(Document.COLUMN_DISPLAY_NAME, displayName);
    218             row.add(Document.COLUMN_SIZE, size);
    219             row.add(Document.COLUMN_MIME_TYPE, mimeType);
    220             row.add(Document.COLUMN_FLAGS, flags);
    221             row.add(Document.COLUMN_LAST_MODIFIED, lastModified);
    222         }
    223     }
    224 
    225     @Override
    226     public boolean isChildDocument(String parentDocumentId, String documentId) {
    227         for (Doc doc : mDocs.get(parentDocumentId).children) {
    228             if (doc.docId.equals(documentId)) {
    229                 return true;
    230             }
    231             if (Document.MIME_TYPE_DIR.equals(doc.mimeType)) {
    232                 if (isChildDocument(doc.docId, documentId)) {
    233                     return true;
    234                 }
    235             }
    236         }
    237         return false;
    238     }
    239 
    240     @Override
    241     public String createDocument(String parentDocumentId, String mimeType, String displayName)
    242             throws FileNotFoundException {
    243         final String docId = "doc:" + mNextDocId.getAndIncrement();
    244         final Doc doc = buildDoc(docId, displayName, mimeType, null);
    245         doc.flags = Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_RENAME;
    246         mDocs.get(parentDocumentId).children.add(doc);
    247         return docId;
    248     }
    249 
    250     @Override
    251     public String renameDocument(String documentId, String displayName)
    252             throws FileNotFoundException {
    253         mDocs.get(documentId).displayName = displayName;
    254         return null;
    255     }
    256 
    257     @Override
    258     public void deleteDocument(String documentId) throws FileNotFoundException {
    259         final Doc doc = mDocs.get(documentId);
    260         mDocs.remove(doc.docId);
    261         for (Doc parentDoc : mDocs.values()) {
    262             parentDoc.children.remove(doc);
    263         }
    264     }
    265 
    266     @Override
    267     public void removeDocument(String documentId, String parentDocumentId)
    268             throws FileNotFoundException {
    269         // There are no multi-parented documents in this provider, so it's safe to remove the
    270         // document from mDocs.
    271         final Doc doc = mDocs.get(documentId);
    272         mDocs.remove(doc.docId);
    273         mDocs.get(parentDocumentId).children.remove(doc);
    274     }
    275 
    276     @Override
    277     public String copyDocument(String sourceDocumentId, String targetParentDocumentId)
    278             throws FileNotFoundException {
    279         final Doc doc = mDocs.get(sourceDocumentId);
    280         if (doc.children.size() > 0) {
    281             throw new UnsupportedOperationException("Recursive copy not supported for tests.");
    282         }
    283 
    284         final Doc docCopy = buildDoc(doc.docId + "_copy", doc.displayName + "_COPY", doc.mimeType,
    285                 doc.streamTypes);
    286         mDocs.get(targetParentDocumentId).children.add(docCopy);
    287         return docCopy.docId;
    288     }
    289 
    290     @Override
    291     public String moveDocument(String sourceDocumentId, String sourceParentDocumentId,
    292             String targetParentDocumentId)
    293             throws FileNotFoundException {
    294         final Doc doc = mDocs.get(sourceDocumentId);
    295         mDocs.get(sourceParentDocumentId).children.remove(doc);
    296         mDocs.get(targetParentDocumentId).children.add(doc);
    297         return doc.docId;
    298     }
    299 
    300     @Override
    301     public Path findDocumentPath(String parentDocumentId, String documentId)
    302             throws FileNotFoundException {
    303         if (!mDocs.containsKey(documentId)) {
    304             throw new FileNotFoundException(documentId + " is not found.");
    305         }
    306 
    307         final Map<String, String> parentMap = new HashMap<>();
    308         for (Doc doc : mDocs.values()) {
    309             for (Doc childDoc : doc.children) {
    310                 parentMap.put(childDoc.docId, doc.docId);
    311             }
    312         }
    313 
    314         String currentDocId = documentId;
    315         final LinkedList<String> path = new LinkedList<>();
    316         while (!currentDocId.equals(parentDocumentId)
    317                 && !currentDocId.equals(mLocalRoot.docId)
    318                 && !currentDocId.equals(mCreateRoot.docId)) {
    319             path.addFirst(currentDocId);
    320             currentDocId = parentMap.get(currentDocId);
    321         }
    322 
    323         if (parentDocumentId != null && !currentDocId.equals(parentDocumentId)) {
    324             throw new FileNotFoundException(documentId + " is not found under " + parentDocumentId);
    325         }
    326 
    327         // Add the root doc / parent doc
    328         path.addFirst(currentDocId);
    329 
    330         String rootId = null;
    331         if (parentDocumentId == null) {
    332             rootId = currentDocId.equals(mLocalRoot.docId) ? "local" : "create";
    333         }
    334         return new Path(rootId, path);
    335     }
    336 
    337     @Override
    338     public Cursor querySearchDocuments(String rootId, String query, String[] projection)
    339             throws FileNotFoundException {
    340         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
    341         final String lowerCaseQuery = query.toLowerCase();
    342         for (Doc doc : mDocs.values()) {
    343             if (!TextUtils.isEmpty(doc.displayName) && doc.displayName.toLowerCase().contains(
    344                     lowerCaseQuery)) {
    345                 doc.include(result);
    346             }
    347         }
    348         return result;
    349     }
    350 
    351     @Override
    352     public Cursor queryDocument(String documentId, String[] projection)
    353             throws FileNotFoundException {
    354         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
    355         mDocs.get(documentId).include(result);
    356         return result;
    357     }
    358 
    359     @Override
    360     public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
    361             String sortOrder) throws FileNotFoundException {
    362         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
    363         for (Doc doc : mDocs.get(parentDocumentId).children) {
    364             doc.include(result);
    365         }
    366         return result;
    367     }
    368 
    369     @Override
    370     public ParcelFileDescriptor openDocument(String documentId, String mode,
    371             CancellationSignal signal) throws FileNotFoundException {
    372         final Doc doc = mDocs.get(documentId);
    373         if (doc == null) {
    374             throw new FileNotFoundException();
    375         }
    376         if ((doc.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0) {
    377             throw new IllegalArgumentException("Tried to open a virtual file.");
    378         }
    379         return openDocumentUnchecked(doc, mode, signal);
    380     }
    381 
    382     private ParcelFileDescriptor openDocumentUnchecked(final Doc doc, String mode,
    383             CancellationSignal signal) throws FileNotFoundException {
    384         final ParcelFileDescriptor[] pipe;
    385         try {
    386             pipe = ParcelFileDescriptor.createPipe();
    387         } catch (IOException e) {
    388             throw new IllegalStateException(e);
    389         }
    390         if (mode.contains("w")) {
    391             new AsyncTask<Void, Void, Void>() {
    392                 @Override
    393                 protected Void doInBackground(Void... params) {
    394                     synchronized (doc) {
    395                         try {
    396                             final InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(
    397                                     pipe[0]);
    398                             doc.contents = readFullyNoClose(is);
    399                             is.close();
    400                             doc.notifyAll();
    401                         } catch (IOException e) {
    402                             Log.w(TAG, "Failed to stream", e);
    403                         }
    404                     }
    405                     return null;
    406                 }
    407             }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
    408             return pipe[1];
    409         } else {
    410             new AsyncTask<Void, Void, Void>() {
    411                 @Override
    412                 protected Void doInBackground(Void... params) {
    413                     synchronized (doc) {
    414                         try {
    415                             final OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream(
    416                                     pipe[1]);
    417                             while (doc.contents == null) {
    418                                 doc.wait();
    419                             }
    420                             os.write(doc.contents);
    421                             os.close();
    422                         } catch (IOException e) {
    423                             Log.w(TAG, "Failed to stream", e);
    424                         } catch (InterruptedException e) {
    425                             Log.w(TAG, "Interuppted", e);
    426                         }
    427                     }
    428                     return null;
    429                 }
    430             }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
    431             return pipe[0];
    432         }
    433     }
    434 
    435     @Override
    436     public String[] getStreamTypes(Uri documentUri, String mimeTypeFilter) {
    437         // TODO: Add enforceTree(uri); b/27156282
    438         final String documentId = DocumentsContract.getDocumentId(documentUri);
    439 
    440         if (!"*/*".equals(mimeTypeFilter)) {
    441             throw new UnsupportedOperationException(
    442                     "Unsupported MIME type filter supported for tests.");
    443         }
    444 
    445         final Doc doc = mDocs.get(documentId);
    446         if (doc == null) {
    447             return null;
    448         }
    449 
    450         return doc.streamTypes;
    451     }
    452 
    453     @Override
    454     public AssetFileDescriptor openTypedDocument(
    455             String documentId, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
    456             throws FileNotFoundException {
    457         final Doc doc = mDocs.get(documentId);
    458         if (doc == null) {
    459             throw new FileNotFoundException();
    460         }
    461 
    462         if (mimeTypeFilter.contains("*")) {
    463             throw new UnsupportedOperationException(
    464                     "MIME type filters with Wildcards not supported for tests.");
    465         }
    466 
    467         for (String streamType : doc.streamTypes) {
    468             if (streamType.equals(mimeTypeFilter)) {
    469                 return new AssetFileDescriptor(openDocumentUnchecked(
    470                         doc, "r", signal), 0, doc.contents.length);
    471             }
    472         }
    473 
    474         throw new UnsupportedOperationException("Unsupported MIME type filter for tests.");
    475     }
    476 
    477     @Override
    478     public IntentSender createWebLinkIntent(String documentId, Bundle options)
    479             throws FileNotFoundException {
    480         final Doc doc = mDocs.get(documentId);
    481         if (doc == null) {
    482             throw new FileNotFoundException();
    483         }
    484         if ((doc.flags & Document.FLAG_WEB_LINKABLE) == 0) {
    485             throw new IllegalArgumentException("The file is not web linkable");
    486         }
    487 
    488         final Intent intent = new Intent(getContext(), WebLinkActivity.class);
    489         intent.putExtra(WebLinkActivity.EXTRA_DOCUMENT_ID, documentId);
    490         if (options != null) {
    491             intent.putExtras(options);
    492         }
    493 
    494         final PendingIntent pendingIntent = PendingIntent.getActivity(
    495                 getContext(), WEB_LINK_REQUEST_CODE, intent,
    496                 PendingIntent.FLAG_ONE_SHOT);
    497         return pendingIntent.getIntentSender();
    498     }
    499 
    500     @Override
    501     public void ejectRoot(String rootId) {
    502         if ("eject".equals(rootId)) {
    503             mEjected = true;
    504             getContext().getContentResolver()
    505                     .notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null);
    506         }
    507 
    508         throw new IllegalStateException("Root " + rootId + " doesn't support ejection.");
    509     }
    510 
    511     private static byte[] readFullyNoClose(InputStream in) throws IOException {
    512         ByteArrayOutputStream bytes = new ByteArrayOutputStream();
    513         byte[] buffer = new byte[1024];
    514         int count;
    515         while ((count = in.read(buffer)) != -1) {
    516             bytes.write(buffer, 0, count);
    517         }
    518         return bytes.toByteArray();
    519     }
    520 }
    521