Implement some tests for com.android.documentsui.CopyService.

- Add a unit test for CopyService.
- Make some changes to StubProvider to make it more configurable, for
  testing.

Change-Id: I3d726099feaf6b7a3fdd40bf2449f4ee3e848d77
This commit is contained in:
Ben Kwa
2015-04-16 18:14:35 -07:00
parent 5c1e306502
commit 448dbbbf0e
3 changed files with 481 additions and 59 deletions

View File

@@ -7,6 +7,7 @@ LOCAL_MODULE_TAGS := tests
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_JAVA_LIBRARIES := android.test.runner
LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 mockito-target guava
LOCAL_PACKAGE_NAME := DocumentsUITests
LOCAL_INSTRUMENTATION_FOR := DocumentsUI

View File

@@ -0,0 +1,281 @@
/*
* Copyright (C) 2015 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.documentsui;
import static com.android.documentsui.model.DocumentInfo.getCursorString;
import android.app.NotificationManager;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import android.os.RemoteException;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.test.MoreAsserts;
import android.test.ServiceTestCase;
import android.util.Log;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.RootInfo;
import com.google.common.collect.Lists;
import libcore.io.IoUtils;
import libcore.io.Streams;
import org.mockito.Mockito;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class CopyTest extends ServiceTestCase<CopyService> {
public CopyTest() {
super(CopyService.class);
}
private static String TAG = "CopyTest";
// This must match the authority for the StubProvider.
private static String AUTHORITY = "com.android.documentsui.stubprovider";
private List<RootInfo> mRoots;
private Context mContext;
private ContentResolver mResolver;
private ContentProviderClient mClient;
private NotificationManager mNotificationManager;
@Override
protected void setUp() throws Exception {
super.setUp();
setupTestContext();
mResolver = mContext.getContentResolver();
mClient = mResolver.acquireContentProviderClient(AUTHORITY);
// Reset the stub provider's storage.
mClient.call("clear", "", null);
mRoots = Lists.newArrayList();
Uri queryUri = DocumentsContract.buildRootsUri(AUTHORITY);
Cursor cursor = null;
try {
cursor = mClient.query(queryUri, null, null, null, null);
while (cursor.moveToNext()) {
final RootInfo root = RootInfo.fromRootsCursor(AUTHORITY, cursor);
final String id = root.rootId;
mRoots.add(root);
}
} finally {
IoUtils.closeQuietly(cursor);
}
}
@Override
protected void tearDown() throws Exception {
mClient.release();
super.tearDown();
}
public List<Uri> setupTestFiles() throws Exception {
Uri rootUri = DocumentsContract.buildDocumentUri(AUTHORITY, mRoots.get(0).documentId);
List<Uri> testFiles = Lists.newArrayList(
DocumentsContract.createDocument(mClient, rootUri, "text/plain", "test0.txt"),
DocumentsContract.createDocument(mClient, rootUri, "text/plain", "test1.txt"),
DocumentsContract.createDocument(mClient, rootUri, "text/plain", "test2.txt")
);
String testContent[] = {
"The five boxing wizards jump quickly",
"The quick brown fox jumps over the lazy dog",
"Jackdaws love my big sphinx of quartz"
};
for (int i = 0; i < testFiles.size(); ++i) {
ParcelFileDescriptor pfd = null;
OutputStream out = null;
try {
pfd = mClient.openFile(testFiles.get(i), "w");
out = new ParcelFileDescriptor.AutoCloseOutputStream(pfd);
out.write(testContent[i].getBytes());
} finally {
IoUtils.closeQuietly(out);
}
}
return testFiles;
}
/**
* Test copying a single file.
*/
public void testCopyFile() throws Exception {
Uri testFile = setupTestFiles().get(0);
// Just copy one file.
copyToDestination(Lists.newArrayList(testFile));
// A call to NotificationManager.cancel marks the end of the copy operation.
Mockito.verify(mNotificationManager, Mockito.timeout(1000)).cancel(Mockito.anyString(),
Mockito.anyInt());
// Verify that one file was copied; check file contents.
assertDstFileCountEquals(1);
assertCopied(testFile);
}
/**
* Test copying multiple files.
*/
public void testCopyMultipleFiles() throws Exception {
List<Uri> testFiles = setupTestFiles();
// Copy all the test files.
copyToDestination(testFiles);
// A call to NotificationManager.cancel marks the end of the copy operation.
Mockito.verify(mNotificationManager, Mockito.timeout(1000)).cancel(Mockito.anyString(),
Mockito.anyInt());
assertDstFileCountEquals(3);
for (Uri testFile : testFiles) {
assertCopied(testFile);
}
}
/**
* Copies the given files to a pre-determined destination.
*
* @throws FileNotFoundException
*/
private void copyToDestination(List<Uri> srcs) throws FileNotFoundException {
final ArrayList<DocumentInfo> srcDocs = Lists.newArrayList();
for (Uri src : srcs) {
srcDocs.add(DocumentInfo.fromUri(mResolver, src));
}
final Uri dst = DocumentsContract.buildDocumentUri(AUTHORITY, mRoots.get(1).documentId);
DocumentStack stack = new DocumentStack();
stack.push(DocumentInfo.fromUri(mResolver, dst));
final Intent copyIntent = new Intent(mContext, CopyService.class);
copyIntent.putParcelableArrayListExtra(CopyService.EXTRA_SRC_LIST, srcDocs);
copyIntent.putExtra(CopyService.EXTRA_STACK, (Parcelable) stack);
startService(copyIntent);
}
/**
* Returns a count of the files in the given directory.
*/
private void assertDstFileCountEquals(int expected) throws RemoteException {
final Uri queryUri = DocumentsContract.buildChildDocumentsUri(AUTHORITY,
mRoots.get(1).documentId);
Cursor c = null;
int count = 0;
try {
c = mClient.query(queryUri, null, null, null, null);
count = c.getCount();
} finally {
IoUtils.closeQuietly(c);
}
assertEquals("Incorrect file count after copy", expected, count);
}
/**
* Verifies that the file pointed to by the given URI was correctly copied to the destination.
*/
private void assertCopied(Uri src) throws Exception {
Cursor cursor = null;
String srcName = null;
try {
cursor = mClient.query(src, null, null, null, null);
if (cursor.moveToFirst()) {
srcName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
}
} finally {
IoUtils.closeQuietly(cursor);
}
Uri dst = getDstFileUri(srcName);
InputStream in0 = null;
InputStream in1 = null;
try {
in0 = new ParcelFileDescriptor.AutoCloseInputStream(mClient.openFile(src, "r"));
in1 = new ParcelFileDescriptor.AutoCloseInputStream(mClient.openFile(dst, "r"));
byte[] buffer0 = Streams.readFully(in0);
byte[] buffer1 = Streams.readFully(in1);
MoreAsserts.assertEquals(buffer0, buffer1);
} finally {
IoUtils.closeQuietly(in0);
IoUtils.closeQuietly(in1);
}
}
/**
* Generates a file URI from a given filename. This assumes the file already exists in the
* destination root.
*/
private Uri getDstFileUri(String filename) throws RemoteException {
final Uri dstFileQuery = DocumentsContract.buildChildDocumentsUri(AUTHORITY,
mRoots.get(1).documentId);
Cursor cursor = null;
try {
// StubProvider doesn't seem to support query strings; filter the results manually.
cursor = mClient.query(dstFileQuery, null, null, null, null);
while (cursor.moveToNext()) {
if (filename.equals(getCursorString(cursor, Document.COLUMN_DISPLAY_NAME))) {
return DocumentsContract.buildDocumentUri(AUTHORITY,
getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
}
}
} finally {
IoUtils.closeQuietly(cursor);
}
return null;
}
/**
* Sets up a ContextWrapper that substitutes a stub NotificationManager. This allows the test to
* listen for notification events, to gauge copy progress.
*/
private void setupTestContext() {
mContext = getSystemContext();
System.setProperty("dexmaker.dexcache", mContext.getCacheDir().getPath());
mNotificationManager = Mockito.spy((NotificationManager) mContext
.getSystemService(Context.NOTIFICATION_SERVICE));
// Insert a stub NotificationManager that enables us to listen for when copying is complete.
setContext(new ContextWrapper(mContext) {
@Override
public Object getSystemService(String name) {
if (Context.NOTIFICATION_SERVICE.equals(name)) {
return mNotificationManager;
} else {
return super.getSystemService(name);
}
}
});
}
}

View File

@@ -17,34 +17,45 @@
package com.android.documentsui;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ProviderInfo;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor.RowBuilder;
import android.database.MatrixCursor;
import android.graphics.Point;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.util.Log;
import com.google.android.collect.Maps;
import libcore.io.IoUtils;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class StubProvider extends DocumentsProvider {
private static int STORAGE_SIZE = 1024 * 1024; // 1 MB.
private static final String EXTRA_SIZE = "com.android.documentsui.stubprovider.SIZE";
private static final String EXTRA_ROOT = "com.android.documentsui.stubprovider.ROOT";
private static final String STORAGE_SIZE_KEY = "documentsui.stubprovider.size";
private static int DEFAULT_SIZE = 1024 * 1024; // 1 MB.
private static final String TAG = "StubProvider";
private static final String MY_ROOT_ID = "myRoot";
private static final String MY_ROOT_ID = "sd0";
private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_AVAILABLE_BYTES
@@ -54,11 +65,11 @@ public class StubProvider extends DocumentsProvider {
Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
};
private String mRootDocumentId;
private HashMap<String, StubDocument> mStorage = new HashMap<String, StubDocument>();
private int mStorageUsedBytes;
private Object mWriteLock = new Object();
private String mAuthority;
private SharedPreferences mPrefs;
private Map<String, RootInfo> mRoots;
@Override
public void attachInfo(Context context, ProviderInfo info) {
@@ -68,29 +79,61 @@ public class StubProvider extends DocumentsProvider {
@Override
public boolean onCreate() {
clearCacheAndBuildRoots();
return true;
}
private void clearCacheAndBuildRoots() {
final File cacheDir = getContext().getCacheDir();
removeRecursively(cacheDir);
final StubDocument document = new StubDocument(cacheDir, Document.MIME_TYPE_DIR, null);
mRootDocumentId = document.documentId;
mStorage.put(mRootDocumentId, document);
return true;
mStorage.clear();
mPrefs = getContext().getSharedPreferences(
"com.android.documentsui.stubprovider.preferences", Context.MODE_PRIVATE);
Collection<String> rootIds = mPrefs.getStringSet("roots", null);
if (rootIds == null) {
rootIds = Arrays.asList(new String[] {
"sd0", "sd1"
});
}
// Create new roots.
mRoots = Maps.newHashMap();
for (String rootId : rootIds) {
final RootInfo rootInfo = new RootInfo(rootId, getSize(rootId));
mRoots.put(rootId, rootInfo);
}
}
/**
* @return Storage size, in bytes.
*/
private long getSize(String rootId) {
final String key = STORAGE_SIZE_KEY + "." + rootId;
return mPrefs.getLong(key, DEFAULT_SIZE);
}
@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
final RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, MY_ROOT_ID);
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD);
row.add(Root.COLUMN_TITLE, "Foobar SD 4GB");
row.add(Root.COLUMN_DOCUMENT_ID, mRootDocumentId);
row.add(Root.COLUMN_AVAILABLE_BYTES, STORAGE_SIZE - mStorageUsedBytes);
final MatrixCursor result = new MatrixCursor(projection != null ? projection
: DEFAULT_ROOT_PROJECTION);
for (Map.Entry<String, RootInfo> entry : mRoots.entrySet()) {
final String id = entry.getKey();
final RootInfo info = entry.getValue();
final RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, id);
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD);
row.add(Root.COLUMN_TITLE, id);
row.add(Root.COLUMN_DOCUMENT_ID, info.rootDocument.documentId);
row.add(Root.COLUMN_AVAILABLE_BYTES, info.getRemainingCapacity());
}
return result;
}
@Override
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
public Cursor queryDocument(String documentId, String[] projection)
throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection
: DEFAULT_DOCUMENT_PROJECTION);
final StubDocument file = mStorage.get(documentId);
if (file == null) {
throw new FileNotFoundException();
@@ -123,14 +166,12 @@ public class StubProvider extends DocumentsProvider {
if (!file.createNewFile()) {
throw new FileNotFoundException();
}
}
catch (IOException e) {
} catch (IOException e) {
throw new FileNotFoundException();
}
}
final StubDocument document = new StubDocument(file, mimeType, parentDocument);
mStorage.put(document.documentId, document);
notifyParentChanged(document.parentId);
return document.documentId;
}
@@ -143,7 +184,7 @@ public class StubProvider extends DocumentsProvider {
if (document == null || !document.file.delete())
throw new FileNotFoundException();
synchronized (mWriteLock) {
mStorageUsedBytes -= fileSize;
document.rootInfo.size -= fileSize;
}
notifyParentChanged(document.parentId);
}
@@ -155,12 +196,13 @@ public class StubProvider extends DocumentsProvider {
if (parentDocument == null || parentDocument.file.isFile()) {
throw new FileNotFoundException();
}
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final MatrixCursor result = new MatrixCursor(projection != null ? projection
: DEFAULT_DOCUMENT_PROJECTION);
result.setNotificationUri(getContext().getContentResolver(),
DocumentsContract.buildChildDocumentsUri(mAuthority, parentDocumentId));
StubDocument document;
for (File file : parentDocument.file.listFiles()) {
document = mStorage.get(StubDocument.getDocumentIdForFile(file));
document = mStorage.get(getDocumentIdForFile(file));
if (document != null) {
includeDocument(result, document);
}
@@ -171,7 +213,9 @@ public class StubProvider extends DocumentsProvider {
@Override
public Cursor queryRecentDocuments(String rootId, String[] projection)
throws FileNotFoundException {
throw new FileNotFoundException();
final MatrixCursor result = new MatrixCursor(projection != null ? projection
: DEFAULT_DOCUMENT_PROJECTION);
return result;
}
@Override
@@ -202,8 +246,7 @@ public class StubProvider extends DocumentsProvider {
ParcelFileDescriptor[] pipe;
try {
pipe = ParcelFileDescriptor.createReliablePipe();
}
catch (IOException exception) {
} catch (IOException exception) {
throw new FileNotFoundException();
}
final ParcelFileDescriptor readPipe = pipe[0];
@@ -212,15 +255,19 @@ public class StubProvider extends DocumentsProvider {
new Thread() {
@Override
public void run() {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
final FileInputStream inputStream = new FileInputStream(readPipe.getFileDescriptor());
final FileOutputStream outputStream = new FileOutputStream(document.file);
inputStream = new ParcelFileDescriptor.AutoCloseInputStream(readPipe);
outputStream = new FileOutputStream(document.file);
byte[] buffer = new byte[32 * 1024];
int bytesToRead;
int bytesRead = 0;
while (bytesRead != -1) {
synchronized (mWriteLock) {
bytesToRead = Math.min(STORAGE_SIZE - mStorageUsedBytes, buffer.length);
// This cast is safe because the max possible value is buffer.length.
bytesToRead = (int) Math.min(document.rootInfo.getRemainingCapacity(),
buffer.length);
if (bytesToRead == 0) {
closePipeWithErrorSilently(readPipe, "Not enough space.");
break;
@@ -230,15 +277,14 @@ public class StubProvider extends DocumentsProvider {
break;
}
outputStream.write(buffer, 0, bytesRead);
mStorageUsedBytes += bytesRead;
document.rootInfo.size += bytesRead;
}
}
}
catch (IOException e) {
} catch (IOException e) {
closePipeWithErrorSilently(readPipe, e.getMessage());
}
finally {
closePipeSilently(readPipe);
} finally {
IoUtils.closeQuietly(inputStream);
IoUtils.closeQuietly(outputStream);
notifyParentChanged(document.parentId);
}
}
@@ -250,24 +296,38 @@ public class StubProvider extends DocumentsProvider {
private void closePipeWithErrorSilently(ParcelFileDescriptor pipe, String error) {
try {
pipe.closeWithError(error);
}
catch (IOException ignore) {
} catch (IOException ignore) {
}
}
private void closePipeSilently(ParcelFileDescriptor pipe) {
try {
pipe.close();
}
catch (IOException ignore) {
@Override
public Bundle call(String method, String arg, Bundle extras) {
Log.d(TAG, "call: " + method + arg);
switch (method) {
case "clear":
clearCacheAndBuildRoots();
return null;
case "configure":
configure(arg, extras);
return null;
default:
return super.call(method, arg, extras);
}
}
private void configure(String arg, Bundle extras) {
Log.d(TAG, "Configure " + arg);
String rootName = extras.getString(EXTRA_ROOT, MY_ROOT_ID);
long rootSize = extras.getLong(EXTRA_SIZE, 1) * 1024 * 1024;
setSize(rootName, rootSize);
}
private void notifyParentChanged(String parentId) {
getContext().getContentResolver().notifyChange(
DocumentsContract.buildChildDocumentsUri(mAuthority, parentId), null, false);
// Notify also about possible change in remaining space on the root.
getContext().getContentResolver().notifyChange(DocumentsContract.buildRootsUri(mAuthority), null, false);
getContext().getContentResolver().notifyChange(DocumentsContract.buildRootsUri(mAuthority),
null, false);
}
private void includeDocument(MatrixCursor result, StubDocument document) {
@@ -295,22 +355,102 @@ public class StubProvider extends DocumentsProvider {
childFile.delete();
}
}
}
class StubDocument {
public final File file;
public final String mimeType;
public final String documentId;
public final String parentId;
public void setSize(String rootId, long rootSize) {
RootInfo root = mRoots.get(rootId);
if (root != null) {
final String key = STORAGE_SIZE_KEY + "." + rootId;
Log.d(TAG, "Set size of " + key + " : " + rootSize);
StubDocument(File file, String mimeType, StubDocument parent) {
this.file = file;
this.mimeType = mimeType;
this.documentId = getDocumentIdForFile(file);
this.parentId = parent != null ? parent.documentId : null;
// Persist the size.
SharedPreferences.Editor editor = mPrefs.edit();
editor.putLong(key, rootSize);
editor.apply();
// Apply the size in the current instance of this provider.
root.capacity = rootSize;
getContext().getContentResolver().notifyChange(
DocumentsContract.buildRootsUri(mAuthority),
null, false);
} else {
Log.e(TAG, "Attempt to configure non-existent root: " + rootId);
}
}
public static String getDocumentIdForFile(File file) {
public File createFile(String rootId, File parent, String mimeType, String name)
throws IOException {
StubDocument parentDoc = null;
if (parent == null) {
// Use the root dir as the parent, if one wasn't specified.
parentDoc = mRoots.get(rootId).rootDocument;
} else {
// Verify that the parent exists and is a directory.
parentDoc = mStorage.get(getDocumentIdForFile(parent));
if (parentDoc == null) {
throw new IllegalArgumentException("Parent file not found.");
}
if (!Document.MIME_TYPE_DIR.equals(parentDoc.mimeType)) {
throw new IllegalArgumentException("Parent file must be a directory.");
}
}
File file = new File(parentDoc.file, name);
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
file.mkdir();
} else {
file.createNewFile();
}
new StubDocument(file, mimeType, parentDoc);
return file;
}
final class RootInfo {
public final String name;
public final StubDocument rootDocument;
public long capacity;
public long size;
RootInfo(String name, long capacity) {
this.name = name;
this.capacity = 1024 * 1024;
// Make a subdir in the cache dir for each root.
File rootDir = new File(getContext().getCacheDir(), name);
rootDir.mkdir();
this.rootDocument = new StubDocument(rootDir, Document.MIME_TYPE_DIR, this);
this.capacity = capacity;
this.size = 0;
}
public long getRemainingCapacity() {
return capacity - size;
}
}
final class StubDocument {
public final File file;
public final String mimeType;
public final String documentId;
public final String parentId;
public final RootInfo rootInfo;
StubDocument(File file, String mimeType, StubDocument parent) {
this.file = file;
this.mimeType = mimeType;
this.documentId = getDocumentIdForFile(file);
this.parentId = parent.documentId;
this.rootInfo = parent.rootInfo;
mStorage.put(this.documentId, this);
}
StubDocument(File file, String mimeType, RootInfo rootInfo) {
this.file = file;
this.mimeType = mimeType;
this.documentId = getDocumentIdForFile(file);
this.parentId = null;
this.rootInfo = rootInfo;
mStorage.put(this.documentId, this);
}
}
private static String getDocumentIdForFile(File file) {
return file.getAbsolutePath();
}
}