| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.cts.documentprovider; |
| |
| import android.app.PendingIntent; |
| import android.content.Intent; |
| import android.content.IntentSender; |
| import android.content.res.AssetFileDescriptor; |
| import android.database.Cursor; |
| import android.database.MatrixCursor; |
| import android.database.MatrixCursor.RowBuilder; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.os.Bundle; |
| import android.os.CancellationSignal; |
| import android.os.ParcelFileDescriptor; |
| import android.provider.DocumentsContract; |
| import android.provider.DocumentsContract.Document; |
| import android.provider.DocumentsContract.Path; |
| import android.provider.DocumentsContract.Root; |
| import android.provider.DocumentsProvider; |
| import android.util.Log; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| public class MyDocumentsProvider extends DocumentsProvider { |
| private static final String TAG = "TestDocumentsProvider"; |
| |
| private static final String AUTHORITY = "com.android.cts.documentprovider"; |
| |
| private static final int WEB_LINK_REQUEST_CODE = 321; |
| |
| private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { |
| Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE, |
| Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES, |
| }; |
| |
| private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { |
| Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, |
| Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE, |
| }; |
| |
| private static String[] resolveRootProjection(String[] projection) { |
| return projection != null ? projection : DEFAULT_ROOT_PROJECTION; |
| } |
| |
| private static String[] resolveDocumentProjection(String[] projection) { |
| return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION; |
| } |
| |
| private boolean mEjected = false; |
| |
| @Override |
| public boolean onCreate() { |
| resetRoots(); |
| return true; |
| } |
| |
| @Override |
| public Cursor queryRoots(String[] projection) throws FileNotFoundException { |
| final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); |
| |
| RowBuilder row = result.newRow(); |
| row.add(Root.COLUMN_ROOT_ID, "local"); |
| row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY); |
| row.add(Root.COLUMN_TITLE, "CtsLocal"); |
| row.add(Root.COLUMN_SUMMARY, "CtsLocalSummary"); |
| row.add(Root.COLUMN_DOCUMENT_ID, "doc:local"); |
| |
| row = result.newRow(); |
| row.add(Root.COLUMN_ROOT_ID, "create"); |
| row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD); |
| row.add(Root.COLUMN_TITLE, "CtsCreate"); |
| row.add(Root.COLUMN_DOCUMENT_ID, "doc:create"); |
| |
| if (!mEjected) { |
| row = result.newRow(); |
| row.add(Root.COLUMN_ROOT_ID, "eject"); |
| row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_EJECT); |
| row.add(Root.COLUMN_TITLE, "eject"); |
| // Reuse local docs, but not used for testing |
| row.add(Root.COLUMN_DOCUMENT_ID, "doc:local"); |
| } |
| |
| return result; |
| } |
| |
| private Map<String, Doc> mDocs = new HashMap<>(); |
| |
| private Doc mLocalRoot; |
| private Doc mCreateRoot; |
| private final AtomicInteger mNextDocId = new AtomicInteger(0); |
| |
| private Doc buildDoc(String docId, String displayName, String mimeType, |
| String[] streamTypes) { |
| final Doc doc = new Doc(); |
| doc.docId = docId; |
| doc.displayName = displayName; |
| doc.mimeType = mimeType; |
| doc.streamTypes = streamTypes; |
| mDocs.put(doc.docId, doc); |
| return doc; |
| } |
| |
| public void resetRoots() { |
| Log.d(TAG, "resetRoots()"); |
| |
| mEjected = false; |
| |
| mDocs.clear(); |
| |
| mLocalRoot = buildDoc("doc:local", null, Document.MIME_TYPE_DIR, null); |
| |
| mCreateRoot = buildDoc("doc:create", null, Document.MIME_TYPE_DIR, null); |
| mCreateRoot.flags = Document.FLAG_DIR_SUPPORTS_CREATE; |
| |
| { |
| Doc file1 = buildDoc("doc:file1", "FILE1", "mime1/file1", null); |
| file1.contents = "fileone".getBytes(); |
| file1.flags = Document.FLAG_SUPPORTS_WRITE; |
| mLocalRoot.children.add(file1); |
| mCreateRoot.children.add(file1); |
| } |
| |
| { |
| Doc file2 = buildDoc("doc:file2", "FILE2", "mime2/file2", null); |
| file2.contents = "filetwo".getBytes(); |
| file2.flags = Document.FLAG_SUPPORTS_WRITE; |
| mLocalRoot.children.add(file2); |
| mCreateRoot.children.add(file2); |
| } |
| |
| { |
| Doc virtualFile = buildDoc("doc:virtual-file", "VIRTUAL_FILE", "application/icecream", |
| new String[] { "text/plain" }); |
| virtualFile.flags = Document.FLAG_VIRTUAL_DOCUMENT; |
| virtualFile.contents = "Converted contents.".getBytes(); |
| mLocalRoot.children.add(virtualFile); |
| mCreateRoot.children.add(virtualFile); |
| } |
| |
| { |
| Doc webLinkableFile = buildDoc("doc:web-linkable-file", "WEB_LINKABLE_FILE", |
| "application/icecream", new String[] { "text/plain" }); |
| webLinkableFile.flags = Document.FLAG_VIRTUAL_DOCUMENT | Document.FLAG_WEB_LINKABLE; |
| webLinkableFile.contents = "Fake contents.".getBytes(); |
| mLocalRoot.children.add(webLinkableFile); |
| mCreateRoot.children.add(webLinkableFile); |
| } |
| |
| Doc dir1 = buildDoc("doc:dir1", "DIR1", Document.MIME_TYPE_DIR, null); |
| mLocalRoot.children.add(dir1); |
| |
| { |
| Doc file3 = buildDoc("doc:file3", "FILE3", "mime3/file3", null); |
| file3.contents = "filethree".getBytes(); |
| file3.flags = Document.FLAG_SUPPORTS_WRITE; |
| dir1.children.add(file3); |
| } |
| |
| Doc dir2 = buildDoc("doc:dir2", "DIR2", Document.MIME_TYPE_DIR, null); |
| mCreateRoot.children.add(dir2); |
| |
| { |
| Doc file4 = buildDoc("doc:file4", "FILE4", "mime4/file4", null); |
| file4.contents = "filefour".getBytes(); |
| file4.flags = Document.FLAG_SUPPORTS_WRITE | |
| Document.FLAG_SUPPORTS_COPY | |
| Document.FLAG_SUPPORTS_MOVE | |
| Document.FLAG_SUPPORTS_REMOVE; |
| dir2.children.add(file4); |
| |
| Doc subDir2 = buildDoc("doc:sub_dir2", "SUB_DIR2", Document.MIME_TYPE_DIR, null); |
| dir2.children.add(subDir2); |
| } |
| } |
| |
| private static class Doc { |
| public String docId; |
| public int flags; |
| public String displayName; |
| public long size; |
| public String mimeType; |
| public String[] streamTypes; |
| public long lastModified; |
| public byte[] contents; |
| public List<Doc> children = new ArrayList<>(); |
| |
| public void include(MatrixCursor result) { |
| final RowBuilder row = result.newRow(); |
| row.add(Document.COLUMN_DOCUMENT_ID, docId); |
| row.add(Document.COLUMN_DISPLAY_NAME, displayName); |
| row.add(Document.COLUMN_SIZE, size); |
| row.add(Document.COLUMN_MIME_TYPE, mimeType); |
| row.add(Document.COLUMN_FLAGS, flags); |
| row.add(Document.COLUMN_LAST_MODIFIED, lastModified); |
| } |
| } |
| |
| @Override |
| public boolean isChildDocument(String parentDocumentId, String documentId) { |
| for (Doc doc : mDocs.get(parentDocumentId).children) { |
| if (doc.docId.equals(documentId)) { |
| return true; |
| } |
| if (Document.MIME_TYPE_DIR.equals(doc.mimeType)) { |
| if (isChildDocument(doc.docId, documentId)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public String createDocument(String parentDocumentId, String mimeType, String displayName) |
| throws FileNotFoundException { |
| final String docId = "doc:" + mNextDocId.getAndIncrement(); |
| final Doc doc = buildDoc(docId, displayName, mimeType, null); |
| doc.flags = Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_RENAME; |
| mDocs.get(parentDocumentId).children.add(doc); |
| return docId; |
| } |
| |
| @Override |
| public String renameDocument(String documentId, String displayName) |
| throws FileNotFoundException { |
| mDocs.get(documentId).displayName = displayName; |
| return null; |
| } |
| |
| @Override |
| public void deleteDocument(String documentId) throws FileNotFoundException { |
| final Doc doc = mDocs.get(documentId); |
| mDocs.remove(doc); |
| for (Doc parentDoc : mDocs.values()) { |
| parentDoc.children.remove(doc); |
| } |
| } |
| |
| @Override |
| public void removeDocument(String documentId, String parentDocumentId) |
| throws FileNotFoundException { |
| // There are no multi-parented documents in this provider, so it's safe to remove the |
| // document from mDocs. |
| final Doc doc = mDocs.get(documentId); |
| mDocs.remove(doc); |
| mDocs.get(parentDocumentId).children.remove(doc); |
| } |
| |
| @Override |
| public String copyDocument(String sourceDocumentId, String targetParentDocumentId) |
| throws FileNotFoundException { |
| final Doc doc = mDocs.get(sourceDocumentId); |
| if (doc.children.size() > 0) { |
| throw new UnsupportedOperationException("Recursive copy not supported for tests."); |
| } |
| |
| final Doc docCopy = buildDoc(doc.docId + "_copy", doc.displayName + "_COPY", doc.mimeType, |
| doc.streamTypes); |
| mDocs.get(targetParentDocumentId).children.add(docCopy); |
| return docCopy.docId; |
| } |
| |
| @Override |
| public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, |
| String targetParentDocumentId) |
| throws FileNotFoundException { |
| final Doc doc = mDocs.get(sourceDocumentId); |
| mDocs.get(sourceParentDocumentId).children.remove(doc); |
| mDocs.get(targetParentDocumentId).children.add(doc); |
| return doc.docId; |
| } |
| |
| @Override |
| public Path findDocumentPath(String parentDocumentId, String documentId) |
| throws FileNotFoundException { |
| if (!mDocs.containsKey(documentId)) { |
| throw new FileNotFoundException(documentId + " is not found."); |
| } |
| |
| final Map<String, String> parentMap = new HashMap<>(); |
| for (Doc doc : mDocs.values()) { |
| for (Doc childDoc : doc.children) { |
| parentMap.put(childDoc.docId, doc.docId); |
| } |
| } |
| |
| String currentDocId = documentId; |
| final LinkedList<String> path = new LinkedList<>(); |
| while (!currentDocId.equals(parentDocumentId) |
| && !currentDocId.equals(mLocalRoot.docId) |
| && !currentDocId.equals(mCreateRoot.docId)) { |
| path.addFirst(currentDocId); |
| currentDocId = parentMap.get(currentDocId); |
| } |
| |
| if (parentDocumentId != null && !currentDocId.equals(parentDocumentId)) { |
| throw new FileNotFoundException(documentId + " is not found under " + parentDocumentId); |
| } |
| |
| // Add the root doc / parent doc |
| path.addFirst(currentDocId); |
| |
| String rootId = null; |
| if (parentDocumentId == null) { |
| rootId = currentDocId.equals(mLocalRoot.docId) ? "local" : "create"; |
| } |
| return new Path(rootId, path); |
| } |
| |
| @Override |
| public Cursor queryDocument(String documentId, String[] projection) |
| throws FileNotFoundException { |
| final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); |
| mDocs.get(documentId).include(result); |
| return result; |
| } |
| |
| @Override |
| public Cursor queryChildDocuments(String parentDocumentId, String[] projection, |
| String sortOrder) throws FileNotFoundException { |
| final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); |
| for (Doc doc : mDocs.get(parentDocumentId).children) { |
| doc.include(result); |
| } |
| return result; |
| } |
| |
| @Override |
| public ParcelFileDescriptor openDocument(String documentId, String mode, |
| CancellationSignal signal) throws FileNotFoundException { |
| final Doc doc = mDocs.get(documentId); |
| if (doc == null) { |
| throw new FileNotFoundException(); |
| } |
| if ((doc.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0) { |
| throw new IllegalArgumentException("Tried to open a virtual file."); |
| } |
| return openDocumentUnchecked(doc, mode, signal); |
| } |
| |
| private ParcelFileDescriptor openDocumentUnchecked(final Doc doc, String mode, |
| CancellationSignal signal) throws FileNotFoundException { |
| final ParcelFileDescriptor[] pipe; |
| try { |
| pipe = ParcelFileDescriptor.createPipe(); |
| } catch (IOException e) { |
| throw new IllegalStateException(e); |
| } |
| if (mode.contains("w")) { |
| new AsyncTask<Void, Void, Void>() { |
| @Override |
| protected Void doInBackground(Void... params) { |
| synchronized (doc) { |
| try { |
| final InputStream is = new ParcelFileDescriptor.AutoCloseInputStream( |
| pipe[0]); |
| doc.contents = readFullyNoClose(is); |
| is.close(); |
| doc.notifyAll(); |
| } catch (IOException e) { |
| Log.w(TAG, "Failed to stream", e); |
| } |
| } |
| return null; |
| } |
| }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); |
| return pipe[1]; |
| } else { |
| new AsyncTask<Void, Void, Void>() { |
| @Override |
| protected Void doInBackground(Void... params) { |
| synchronized (doc) { |
| try { |
| final OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream( |
| pipe[1]); |
| while (doc.contents == null) { |
| doc.wait(); |
| } |
| os.write(doc.contents); |
| os.close(); |
| } catch (IOException e) { |
| Log.w(TAG, "Failed to stream", e); |
| } catch (InterruptedException e) { |
| Log.w(TAG, "Interuppted", e); |
| } |
| } |
| return null; |
| } |
| }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); |
| return pipe[0]; |
| } |
| } |
| |
| @Override |
| public String[] getStreamTypes(Uri documentUri, String mimeTypeFilter) { |
| // TODO: Add enforceTree(uri); b/27156282 |
| final String documentId = DocumentsContract.getDocumentId(documentUri); |
| |
| if (!"*/*".equals(mimeTypeFilter)) { |
| throw new UnsupportedOperationException( |
| "Unsupported MIME type filter supported for tests."); |
| } |
| |
| final Doc doc = mDocs.get(documentId); |
| if (doc == null) { |
| return null; |
| } |
| |
| return doc.streamTypes; |
| } |
| |
| @Override |
| public AssetFileDescriptor openTypedDocument( |
| String documentId, String mimeTypeFilter, Bundle opts, CancellationSignal signal) |
| throws FileNotFoundException { |
| final Doc doc = mDocs.get(documentId); |
| if (doc == null) { |
| throw new FileNotFoundException(); |
| } |
| |
| if (mimeTypeFilter.contains("*")) { |
| throw new UnsupportedOperationException( |
| "MIME type filters with Wildcards not supported for tests."); |
| } |
| |
| for (String streamType : doc.streamTypes) { |
| if (streamType.equals(mimeTypeFilter)) { |
| return new AssetFileDescriptor(openDocumentUnchecked( |
| doc, "r", signal), 0, doc.contents.length); |
| } |
| } |
| |
| throw new UnsupportedOperationException("Unsupported MIME type filter for tests."); |
| } |
| |
| @Override |
| public IntentSender createWebLinkIntent(String documentId, Bundle options) |
| throws FileNotFoundException { |
| final Doc doc = mDocs.get(documentId); |
| if (doc == null) { |
| throw new FileNotFoundException(); |
| } |
| if ((doc.flags & Document.FLAG_WEB_LINKABLE) == 0) { |
| throw new IllegalArgumentException("The file is not web linkable"); |
| } |
| |
| final Intent intent = new Intent(getContext(), WebLinkActivity.class); |
| intent.putExtra(WebLinkActivity.EXTRA_DOCUMENT_ID, documentId); |
| if (options != null) { |
| intent.putExtras(options); |
| } |
| |
| final PendingIntent pendingIntent = PendingIntent.getActivity( |
| getContext(), WEB_LINK_REQUEST_CODE, intent, |
| PendingIntent.FLAG_ONE_SHOT); |
| return pendingIntent.getIntentSender(); |
| } |
| |
| @Override |
| public void ejectRoot(String rootId) { |
| if ("eject".equals(rootId)) { |
| mEjected = true; |
| getContext().getContentResolver() |
| .notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null); |
| } |
| |
| throw new IllegalStateException("Root " + rootId + " doesn't support ejection."); |
| } |
| |
| private static byte[] readFullyNoClose(InputStream in) throws IOException { |
| ByteArrayOutputStream bytes = new ByteArrayOutputStream(); |
| byte[] buffer = new byte[1024]; |
| int count; |
| while ((count = in.read(buffer)) != -1) { |
| bytes.write(buffer, 0, count); |
| } |
| return bytes.toByteArray(); |
| } |
| } |