blob: edf6f15e5c74e836e5f17c575e7f3e15a43a9ddf [file] [log] [blame]
/*
* 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();
}
}