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