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