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 = 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 
include(MatrixCursor result)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
isChildDocument(String parentDocumentId, String documentId)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
createDocument(String parentDocumentId, String mimeType, String displayName)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
renameDocument(String documentId, String displayName)251     public String renameDocument(String documentId, String displayName)
252             throws FileNotFoundException {
253         mDocs.get(documentId).displayName = displayName;
254         return null;
255     }
256 
257     @Override
deleteDocument(String documentId)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
removeDocument(String documentId, String parentDocumentId)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
copyDocument(String sourceDocumentId, String targetParentDocumentId)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
moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId)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
findDocumentPath(String parentDocumentId, String documentId)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
querySearchDocuments(String rootId, String query, String[] projection)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
queryDocument(String documentId, String[] projection)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
queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)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
openDocument(String documentId, String mode, CancellationSignal signal)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 
openDocumentUnchecked(final Doc doc, String mode, CancellationSignal signal)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
getStreamTypes(Uri documentUri, String mimeTypeFilter)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
openTypedDocument( String documentId, String mimeTypeFilter, Bundle opts, CancellationSignal signal)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
createWebLinkIntent(String documentId, Bundle options)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
ejectRoot(String rootId)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 
readFullyNoClose(InputStream in)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