Loading packages/DocumentsUI/src/com/android/documentsui/CopyService.java +21 −10 Original line number Diff line number Diff line Loading @@ -449,7 +449,7 @@ public class CopyService extends IntentService { InputStream src = null; OutputStream dst = null; boolean errorOccurred = false; IOException copyError = null; try { srcFile = mSrcClient.openFile(srcUri, "r", canceller); dstFile = mDstClient.openFile(dstUri, "w", canceller); Loading @@ -462,24 +462,35 @@ public class CopyService extends IntentService { dst.write(buffer, 0, len); makeProgress(len); } srcFile.checkError(); dstFile.checkError(); } catch (IOException e) { errorOccurred = true; Log.e(TAG, "Error while copying " + srcUri.toString(), e); copyError = e; } finally { if (copyError != null) { try { mFailedFiles.add(DocumentInfo.fromUri(getContentResolver(), srcUri)); } catch (FileNotFoundException ignore) { Log.w(TAG, "Source file gone: " + srcUri, e); // The source file is gone. dstFile.closeWithError(copyError.getMessage()); } catch (IOException e) { Log.e(TAG, "Error closing destination", e); } } } finally { // This also ensures the file descriptors are closed. IoUtils.closeQuietly(src); IoUtils.closeQuietly(dst); } if (errorOccurred || mIsCancelled) { if (copyError != null) { // Log errors. Log.e(TAG, "Error while copying " + srcUri.toString(), copyError); try { mFailedFiles.add(DocumentInfo.fromUri(getContentResolver(), srcUri)); } catch (FileNotFoundException ignore) { Log.w(TAG, "Source file gone: " + srcUri, copyError); // The source file is gone. } } if (copyError != null || mIsCancelled) { // Clean up half-copied files. canceller.cancel(); try { Loading packages/DocumentsUI/tests/src/com/android/documentsui/CopyTest.java +169 −121 Original line number Diff line number Diff line Loading @@ -16,23 +16,21 @@ 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.content.pm.ProviderInfo; import android.database.ContentObserver; 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.test.mock.MockContentResolver; import android.util.Log; import com.android.documentsui.model.DocumentInfo; Loading @@ -43,40 +41,93 @@ import com.google.common.collect.Lists; import libcore.io.IoUtils; import libcore.io.Streams; import org.mockito.Mockito; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; public class CopyTest extends ServiceTestCase<CopyService> { /** * A test resolver that enables this test suite to listen for notifications that mark when copy * operations are done. */ class TestContentResolver extends MockContentResolver { private CountDownLatch mReadySignal; private CountDownLatch mNotificationSignal; public TestContentResolver() { mReadySignal = new CountDownLatch(1); } /** * Wait for the given number of files to be copied to destination. Times out after 1 sec. */ public void waitForChanges(int count) throws Exception { // Wait for no more than 1 second by default. waitForChanges(count, 1000); } /** * Wait for files to be copied to destination. * * @param count Number of files to wait for. * @param timeOut Timeout in ms. TimeoutException will be thrown if this function times out. */ public void waitForChanges(int count, int timeOut) throws Exception { mNotificationSignal = new CountDownLatch(count); // Signal that the test is now waiting for files. mReadySignal.countDown(); if (!mNotificationSignal.await(timeOut, TimeUnit.MILLISECONDS)) { throw new TimeoutException("Timed out waiting for files to be copied."); } } @Override public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) { // Wait until the test is ready to receive file notifications. try { mReadySignal.await(); } catch (InterruptedException e) { Log.d(TAG, "Interrupted while waiting for file copy readiness"); Thread.currentThread().interrupt(); } if (DocumentsContract.isDocumentUri(mContext, uri)) { Log.d(TAG, "Notification: " + uri); // Watch for document URI change notifications - this signifies the end of a copy. mNotificationSignal.countDown(); } } }; 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 static String DST = "sd1"; private static String SRC = "sd0"; private static String TAG = "CopyTest"; private List<RootInfo> mRoots; private Context mContext; private ContentResolver mResolver; private TestContentResolver mResolver; private ContentProviderClient mClient; private NotificationManager mNotificationManager; private StubProvider mStorage; private Context mSystemContext; @Override protected void setUp() throws Exception { super.setUp(); setupTestContext(); mResolver = mContext.getContentResolver(); setupTestContext(); mClient = mResolver.acquireContentProviderClient(AUTHORITY); // Reset the stub provider's storage. mClient.call("clear", "", null); mStorage.clearCacheAndBuildRoots(); mRoots = Lists.newArrayList(); Uri queryUri = DocumentsContract.buildRootsUri(AUTHORITY); Loading @@ -84,9 +135,7 @@ public class CopyTest extends ServiceTestCase<CopyService> { 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); mRoots.add(RootInfo.fromRootsCursor(AUTHORITY, cursor)); } } finally { IoUtils.closeQuietly(cursor); Loading @@ -100,68 +149,94 @@ public class CopyTest extends ServiceTestCase<CopyService> { 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); String srcPath = "/test0.txt"; Uri testFile = mStorage.createFile(SRC, srcPath, "text/plain", "The five boxing wizards jump quickly".getBytes()); assertDstFileCountEquals(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()); // 2 operations: file creation, then writing data. mResolver.waitForChanges(2); // Verify that one file was copied; check file contents. assertDstFileCountEquals(1); assertCopied(testFile); assertCopied(srcPath); } /** * Test copying multiple files. */ public void testCopyMultipleFiles() throws Exception { List<Uri> testFiles = setupTestFiles(); String testContent[] = { "The five boxing wizards jump quickly", "The quick brown fox jumps over the lazy dog", "Jackdaws love my big sphinx of quartz" }; String srcPaths[] = { "/test0.txt", "/test1.txt", "/test2.txt" }; List<Uri> testFiles = Lists.newArrayList( mStorage.createFile(SRC, srcPaths[0], "text/plain", testContent[0].getBytes()), mStorage.createFile(SRC, srcPaths[1], "text/plain", testContent[1].getBytes()), mStorage.createFile(SRC, srcPaths[2], "text/plain", testContent[2].getBytes())); assertDstFileCountEquals(0); // 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()); // 3 file creations, 3 file writes. mResolver.waitForChanges(6); assertDstFileCountEquals(3); for (Uri testFile : testFiles) { assertCopied(testFile); for (String path : srcPaths) { assertCopied(path); } } public void testCopyEmptyDir() throws Exception { String srcPath = "/emptyDir"; Uri testDir = mStorage.createFile(SRC, srcPath, DocumentsContract.Document.MIME_TYPE_DIR, null); assertDstFileCountEquals(0); copyToDestination(Lists.newArrayList(testDir)); // Just 1 operation: Directory creation. mResolver.waitForChanges(1); assertDstFileCountEquals(1); File dst = mStorage.getFile(DST, srcPath); assertTrue(dst.isDirectory()); } public void testReadErrors() throws Exception { String srcPath = "/test0.txt"; Uri testFile = mStorage.createFile(SRC, srcPath, "text/plain", "The five boxing wizards jump quickly".getBytes()); assertDstFileCountEquals(0); mStorage.simulateReadErrors(true); copyToDestination(Lists.newArrayList(testFile)); // 3 operations: file creation, writing, then deletion (due to failed copy). mResolver.waitForChanges(3); assertDstFileCountEquals(0); } /** * Copies the given files to a pre-determined destination. * Loading Loading @@ -200,82 +275,55 @@ public class CopyTest extends ServiceTestCase<CopyService> { 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); private void assertCopied(String path) throws Exception { File srcFile = mStorage.getFile(SRC, path); File dstFile = mStorage.getFile(DST, path); assertNotNull(dstFile); InputStream in0 = null; InputStream in1 = null; FileInputStream src = null; FileInputStream dst = 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); src = new FileInputStream(srcFile); dst = new FileInputStream(dstFile); byte[] srcbuf = Streams.readFully(src); byte[] dstbuf = Streams.readFully(dst); MoreAsserts.assertEquals(buffer0, buffer1); MoreAsserts.assertEquals(srcbuf, dstbuf); } finally { IoUtils.closeQuietly(in0); IoUtils.closeQuietly(in1); IoUtils.closeQuietly(src); IoUtils.closeQuietly(dst); } } /** * 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. * * @throws FileNotFoundException */ private void setupTestContext() { mContext = getSystemContext(); System.setProperty("dexmaker.dexcache", mContext.getCacheDir().getPath()); mNotificationManager = Mockito.spy((NotificationManager) mContext .getSystemService(Context.NOTIFICATION_SERVICE)); private void setupTestContext() throws FileNotFoundException { mSystemContext = getSystemContext(); // Insert a stub NotificationManager that enables us to listen for when copying is complete. setContext(new ContextWrapper(mContext) { // Set up the context with the test content resolver. mResolver = new TestContentResolver(); mContext = new ContextWrapper(mSystemContext) { @Override public Object getSystemService(String name) { if (Context.NOTIFICATION_SERVICE.equals(name)) { return mNotificationManager; } else { return super.getSystemService(name); public ContentResolver getContentResolver() { return mResolver; } } }); }; setContext(mContext); // Create a local stub provider and add it to the content resolver. ProviderInfo info = new ProviderInfo(); info.authority = AUTHORITY; info.exported = true; info.grantUriPermissions = true; info.readPermission = android.Manifest.permission.MANAGE_DOCUMENTS; info.writePermission = android.Manifest.permission.MANAGE_DOCUMENTS; mStorage = new StubProvider(); mStorage.attachInfo(mContext, info); mResolver.addProvider(AUTHORITY, mStorage); } } packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java +75 −25 Original line number Diff line number Diff line Loading @@ -21,9 +21,10 @@ 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.database.MatrixCursor.RowBuilder; import android.graphics.Point; import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; import android.os.FileUtils; Loading @@ -32,15 +33,16 @@ import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Root; import android.provider.DocumentsProvider; import android.support.annotation.VisibleForTesting; import android.util.Log; import com.google.android.collect.Maps; import libcore.io.IoUtils; import java.io.FileOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; Loading Loading @@ -70,6 +72,7 @@ public class StubProvider extends DocumentsProvider { private String mAuthority; private SharedPreferences mPrefs; private Map<String, RootInfo> mRoots; private boolean mSimulateReadErrors; @Override public void attachInfo(Context context, ProviderInfo info) { Loading @@ -83,7 +86,8 @@ public class StubProvider extends DocumentsProvider { return true; } private void clearCacheAndBuildRoots() { @VisibleForTesting public void clearCacheAndBuildRoots() { final File cacheDir = getContext().getCacheDir(); removeRecursively(cacheDir); mStorage.clear(); Loading Loading @@ -164,7 +168,7 @@ public class StubProvider extends DocumentsProvider { } else { try { if (!file.createNewFile()) { throw new FileNotFoundException(); throw new IllegalStateException("The file " + file.getPath() + " already exists"); } } catch (IOException e) { throw new FileNotFoundException(); Loading @@ -173,6 +177,10 @@ public class StubProvider extends DocumentsProvider { final StubDocument document = new StubDocument(file, mimeType, parentDocument); notifyParentChanged(document.parentId); getContext().getContentResolver().notifyChange( DocumentsContract.buildDocumentUri(mAuthority, document.documentId), null, false); return document.documentId; } Loading @@ -187,6 +195,9 @@ public class StubProvider extends DocumentsProvider { document.rootInfo.size -= fileSize; } notifyParentChanged(document.parentId); getContext().getContentResolver().notifyChange( DocumentsContract.buildDocumentUri(mAuthority, document.documentId), null, false); } @Override Loading Loading @@ -226,7 +237,17 @@ public class StubProvider extends DocumentsProvider { throw new FileNotFoundException(); if ("r".equals(mode)) { return ParcelFileDescriptor.open(document.file, ParcelFileDescriptor.MODE_READ_ONLY); ParcelFileDescriptor pfd = ParcelFileDescriptor.open(document.file, ParcelFileDescriptor.MODE_READ_ONLY); if (mSimulateReadErrors) { pfd = new ParcelFileDescriptor(pfd) { @Override public void checkError() throws IOException { throw new IOException("Test error"); } }; } return pfd; } if ("w".equals(mode)) { return startWrite(document); Loading @@ -235,6 +256,11 @@ public class StubProvider extends DocumentsProvider { throw new FileNotFoundException(); } @VisibleForTesting public void simulateReadErrors(boolean b) { mSimulateReadErrors = b; } @Override public AssetFileDescriptor openDocumentThumbnail( String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { Loading Loading @@ -281,11 +307,15 @@ public class StubProvider extends DocumentsProvider { } } } catch (IOException e) { Log.e(TAG, "Error on close", e); closePipeWithErrorSilently(readPipe, e.getMessage()); } finally { IoUtils.closeQuietly(inputStream); IoUtils.closeQuietly(outputStream); notifyParentChanged(document.parentId); getContext().getContentResolver().notifyChange( DocumentsContract.buildDocumentUri(mAuthority, document.documentId), null, false); } } }.start(); Loading @@ -302,7 +332,6 @@ public class StubProvider extends DocumentsProvider { @Override public Bundle call(String method, String arg, Bundle extras) { Log.d(TAG, "call: " + method + arg); switch (method) { case "clear": clearCacheAndBuildRoots(); Loading Loading @@ -376,30 +405,51 @@ public class StubProvider extends DocumentsProvider { } } public File createFile(String rootId, File parent, String mimeType, String name) throws IOException { StubDocument parentDoc = null; @VisibleForTesting public Uri createFile(String rootId, String path, String mimeType, byte[] content) throws FileNotFoundException, IOException { StubDocument root = mRoots.get(rootId).rootDocument; if (root == null) { throw new FileNotFoundException("No roots with the ID " + rootId + " were found"); } File file = new File(root.file, path.substring(1)); StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile())); if (parent == null) { // Use the root dir as the parent, if one wasn't specified. parentDoc = mRoots.get(rootId).rootDocument; parent = mStorage.get(createFile(rootId, file.getParentFile().getPath(), DocumentsContract.Document.MIME_TYPE_DIR, null)); } if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) { if (!file.mkdirs()) { throw new FileNotFoundException("Couldn't create directory " + file.getPath()); } } 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 (!file.createNewFile()) { throw new FileNotFoundException("Couldn't create file " + file.getPath()); } if (!Document.MIME_TYPE_DIR.equals(parentDoc.mimeType)) { throw new IllegalArgumentException("Parent file must be a directory."); // Add content to the file. FileOutputStream fout = new FileOutputStream(file); fout.write(content); fout.close(); } final StubDocument document = new StubDocument(file, mimeType, parent); return DocumentsContract.buildDocumentUri(mAuthority, document.documentId); } File file = new File(parentDoc.file, name); if (Document.MIME_TYPE_DIR.equals(mimeType)) { file.mkdir(); } else { file.createNewFile(); @VisibleForTesting public File getFile(String rootId, String path) throws FileNotFoundException { StubDocument root = mRoots.get(rootId).rootDocument; if (root == null) { throw new FileNotFoundException("No roots with the ID " + rootId + " were found"); } // Convert the path string into a path that's relative to the root. File needle = new File(root.file, path.substring(1)); StubDocument found = mStorage.get(getDocumentIdForFile(needle)); if (found == null) { return null; } new StubDocument(file, mimeType, parentDoc); return file; return found.file; } final class RootInfo { Loading Loading
packages/DocumentsUI/src/com/android/documentsui/CopyService.java +21 −10 Original line number Diff line number Diff line Loading @@ -449,7 +449,7 @@ public class CopyService extends IntentService { InputStream src = null; OutputStream dst = null; boolean errorOccurred = false; IOException copyError = null; try { srcFile = mSrcClient.openFile(srcUri, "r", canceller); dstFile = mDstClient.openFile(dstUri, "w", canceller); Loading @@ -462,24 +462,35 @@ public class CopyService extends IntentService { dst.write(buffer, 0, len); makeProgress(len); } srcFile.checkError(); dstFile.checkError(); } catch (IOException e) { errorOccurred = true; Log.e(TAG, "Error while copying " + srcUri.toString(), e); copyError = e; } finally { if (copyError != null) { try { mFailedFiles.add(DocumentInfo.fromUri(getContentResolver(), srcUri)); } catch (FileNotFoundException ignore) { Log.w(TAG, "Source file gone: " + srcUri, e); // The source file is gone. dstFile.closeWithError(copyError.getMessage()); } catch (IOException e) { Log.e(TAG, "Error closing destination", e); } } } finally { // This also ensures the file descriptors are closed. IoUtils.closeQuietly(src); IoUtils.closeQuietly(dst); } if (errorOccurred || mIsCancelled) { if (copyError != null) { // Log errors. Log.e(TAG, "Error while copying " + srcUri.toString(), copyError); try { mFailedFiles.add(DocumentInfo.fromUri(getContentResolver(), srcUri)); } catch (FileNotFoundException ignore) { Log.w(TAG, "Source file gone: " + srcUri, copyError); // The source file is gone. } } if (copyError != null || mIsCancelled) { // Clean up half-copied files. canceller.cancel(); try { Loading
packages/DocumentsUI/tests/src/com/android/documentsui/CopyTest.java +169 −121 Original line number Diff line number Diff line Loading @@ -16,23 +16,21 @@ 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.content.pm.ProviderInfo; import android.database.ContentObserver; 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.test.mock.MockContentResolver; import android.util.Log; import com.android.documentsui.model.DocumentInfo; Loading @@ -43,40 +41,93 @@ import com.google.common.collect.Lists; import libcore.io.IoUtils; import libcore.io.Streams; import org.mockito.Mockito; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; public class CopyTest extends ServiceTestCase<CopyService> { /** * A test resolver that enables this test suite to listen for notifications that mark when copy * operations are done. */ class TestContentResolver extends MockContentResolver { private CountDownLatch mReadySignal; private CountDownLatch mNotificationSignal; public TestContentResolver() { mReadySignal = new CountDownLatch(1); } /** * Wait for the given number of files to be copied to destination. Times out after 1 sec. */ public void waitForChanges(int count) throws Exception { // Wait for no more than 1 second by default. waitForChanges(count, 1000); } /** * Wait for files to be copied to destination. * * @param count Number of files to wait for. * @param timeOut Timeout in ms. TimeoutException will be thrown if this function times out. */ public void waitForChanges(int count, int timeOut) throws Exception { mNotificationSignal = new CountDownLatch(count); // Signal that the test is now waiting for files. mReadySignal.countDown(); if (!mNotificationSignal.await(timeOut, TimeUnit.MILLISECONDS)) { throw new TimeoutException("Timed out waiting for files to be copied."); } } @Override public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) { // Wait until the test is ready to receive file notifications. try { mReadySignal.await(); } catch (InterruptedException e) { Log.d(TAG, "Interrupted while waiting for file copy readiness"); Thread.currentThread().interrupt(); } if (DocumentsContract.isDocumentUri(mContext, uri)) { Log.d(TAG, "Notification: " + uri); // Watch for document URI change notifications - this signifies the end of a copy. mNotificationSignal.countDown(); } } }; 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 static String DST = "sd1"; private static String SRC = "sd0"; private static String TAG = "CopyTest"; private List<RootInfo> mRoots; private Context mContext; private ContentResolver mResolver; private TestContentResolver mResolver; private ContentProviderClient mClient; private NotificationManager mNotificationManager; private StubProvider mStorage; private Context mSystemContext; @Override protected void setUp() throws Exception { super.setUp(); setupTestContext(); mResolver = mContext.getContentResolver(); setupTestContext(); mClient = mResolver.acquireContentProviderClient(AUTHORITY); // Reset the stub provider's storage. mClient.call("clear", "", null); mStorage.clearCacheAndBuildRoots(); mRoots = Lists.newArrayList(); Uri queryUri = DocumentsContract.buildRootsUri(AUTHORITY); Loading @@ -84,9 +135,7 @@ public class CopyTest extends ServiceTestCase<CopyService> { 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); mRoots.add(RootInfo.fromRootsCursor(AUTHORITY, cursor)); } } finally { IoUtils.closeQuietly(cursor); Loading @@ -100,68 +149,94 @@ public class CopyTest extends ServiceTestCase<CopyService> { 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); String srcPath = "/test0.txt"; Uri testFile = mStorage.createFile(SRC, srcPath, "text/plain", "The five boxing wizards jump quickly".getBytes()); assertDstFileCountEquals(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()); // 2 operations: file creation, then writing data. mResolver.waitForChanges(2); // Verify that one file was copied; check file contents. assertDstFileCountEquals(1); assertCopied(testFile); assertCopied(srcPath); } /** * Test copying multiple files. */ public void testCopyMultipleFiles() throws Exception { List<Uri> testFiles = setupTestFiles(); String testContent[] = { "The five boxing wizards jump quickly", "The quick brown fox jumps over the lazy dog", "Jackdaws love my big sphinx of quartz" }; String srcPaths[] = { "/test0.txt", "/test1.txt", "/test2.txt" }; List<Uri> testFiles = Lists.newArrayList( mStorage.createFile(SRC, srcPaths[0], "text/plain", testContent[0].getBytes()), mStorage.createFile(SRC, srcPaths[1], "text/plain", testContent[1].getBytes()), mStorage.createFile(SRC, srcPaths[2], "text/plain", testContent[2].getBytes())); assertDstFileCountEquals(0); // 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()); // 3 file creations, 3 file writes. mResolver.waitForChanges(6); assertDstFileCountEquals(3); for (Uri testFile : testFiles) { assertCopied(testFile); for (String path : srcPaths) { assertCopied(path); } } public void testCopyEmptyDir() throws Exception { String srcPath = "/emptyDir"; Uri testDir = mStorage.createFile(SRC, srcPath, DocumentsContract.Document.MIME_TYPE_DIR, null); assertDstFileCountEquals(0); copyToDestination(Lists.newArrayList(testDir)); // Just 1 operation: Directory creation. mResolver.waitForChanges(1); assertDstFileCountEquals(1); File dst = mStorage.getFile(DST, srcPath); assertTrue(dst.isDirectory()); } public void testReadErrors() throws Exception { String srcPath = "/test0.txt"; Uri testFile = mStorage.createFile(SRC, srcPath, "text/plain", "The five boxing wizards jump quickly".getBytes()); assertDstFileCountEquals(0); mStorage.simulateReadErrors(true); copyToDestination(Lists.newArrayList(testFile)); // 3 operations: file creation, writing, then deletion (due to failed copy). mResolver.waitForChanges(3); assertDstFileCountEquals(0); } /** * Copies the given files to a pre-determined destination. * Loading Loading @@ -200,82 +275,55 @@ public class CopyTest extends ServiceTestCase<CopyService> { 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); private void assertCopied(String path) throws Exception { File srcFile = mStorage.getFile(SRC, path); File dstFile = mStorage.getFile(DST, path); assertNotNull(dstFile); InputStream in0 = null; InputStream in1 = null; FileInputStream src = null; FileInputStream dst = 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); src = new FileInputStream(srcFile); dst = new FileInputStream(dstFile); byte[] srcbuf = Streams.readFully(src); byte[] dstbuf = Streams.readFully(dst); MoreAsserts.assertEquals(buffer0, buffer1); MoreAsserts.assertEquals(srcbuf, dstbuf); } finally { IoUtils.closeQuietly(in0); IoUtils.closeQuietly(in1); IoUtils.closeQuietly(src); IoUtils.closeQuietly(dst); } } /** * 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. * * @throws FileNotFoundException */ private void setupTestContext() { mContext = getSystemContext(); System.setProperty("dexmaker.dexcache", mContext.getCacheDir().getPath()); mNotificationManager = Mockito.spy((NotificationManager) mContext .getSystemService(Context.NOTIFICATION_SERVICE)); private void setupTestContext() throws FileNotFoundException { mSystemContext = getSystemContext(); // Insert a stub NotificationManager that enables us to listen for when copying is complete. setContext(new ContextWrapper(mContext) { // Set up the context with the test content resolver. mResolver = new TestContentResolver(); mContext = new ContextWrapper(mSystemContext) { @Override public Object getSystemService(String name) { if (Context.NOTIFICATION_SERVICE.equals(name)) { return mNotificationManager; } else { return super.getSystemService(name); public ContentResolver getContentResolver() { return mResolver; } } }); }; setContext(mContext); // Create a local stub provider and add it to the content resolver. ProviderInfo info = new ProviderInfo(); info.authority = AUTHORITY; info.exported = true; info.grantUriPermissions = true; info.readPermission = android.Manifest.permission.MANAGE_DOCUMENTS; info.writePermission = android.Manifest.permission.MANAGE_DOCUMENTS; mStorage = new StubProvider(); mStorage.attachInfo(mContext, info); mResolver.addProvider(AUTHORITY, mStorage); } }
packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java +75 −25 Original line number Diff line number Diff line Loading @@ -21,9 +21,10 @@ 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.database.MatrixCursor.RowBuilder; import android.graphics.Point; import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; import android.os.FileUtils; Loading @@ -32,15 +33,16 @@ import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Root; import android.provider.DocumentsProvider; import android.support.annotation.VisibleForTesting; import android.util.Log; import com.google.android.collect.Maps; import libcore.io.IoUtils; import java.io.FileOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; Loading Loading @@ -70,6 +72,7 @@ public class StubProvider extends DocumentsProvider { private String mAuthority; private SharedPreferences mPrefs; private Map<String, RootInfo> mRoots; private boolean mSimulateReadErrors; @Override public void attachInfo(Context context, ProviderInfo info) { Loading @@ -83,7 +86,8 @@ public class StubProvider extends DocumentsProvider { return true; } private void clearCacheAndBuildRoots() { @VisibleForTesting public void clearCacheAndBuildRoots() { final File cacheDir = getContext().getCacheDir(); removeRecursively(cacheDir); mStorage.clear(); Loading Loading @@ -164,7 +168,7 @@ public class StubProvider extends DocumentsProvider { } else { try { if (!file.createNewFile()) { throw new FileNotFoundException(); throw new IllegalStateException("The file " + file.getPath() + " already exists"); } } catch (IOException e) { throw new FileNotFoundException(); Loading @@ -173,6 +177,10 @@ public class StubProvider extends DocumentsProvider { final StubDocument document = new StubDocument(file, mimeType, parentDocument); notifyParentChanged(document.parentId); getContext().getContentResolver().notifyChange( DocumentsContract.buildDocumentUri(mAuthority, document.documentId), null, false); return document.documentId; } Loading @@ -187,6 +195,9 @@ public class StubProvider extends DocumentsProvider { document.rootInfo.size -= fileSize; } notifyParentChanged(document.parentId); getContext().getContentResolver().notifyChange( DocumentsContract.buildDocumentUri(mAuthority, document.documentId), null, false); } @Override Loading Loading @@ -226,7 +237,17 @@ public class StubProvider extends DocumentsProvider { throw new FileNotFoundException(); if ("r".equals(mode)) { return ParcelFileDescriptor.open(document.file, ParcelFileDescriptor.MODE_READ_ONLY); ParcelFileDescriptor pfd = ParcelFileDescriptor.open(document.file, ParcelFileDescriptor.MODE_READ_ONLY); if (mSimulateReadErrors) { pfd = new ParcelFileDescriptor(pfd) { @Override public void checkError() throws IOException { throw new IOException("Test error"); } }; } return pfd; } if ("w".equals(mode)) { return startWrite(document); Loading @@ -235,6 +256,11 @@ public class StubProvider extends DocumentsProvider { throw new FileNotFoundException(); } @VisibleForTesting public void simulateReadErrors(boolean b) { mSimulateReadErrors = b; } @Override public AssetFileDescriptor openDocumentThumbnail( String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { Loading Loading @@ -281,11 +307,15 @@ public class StubProvider extends DocumentsProvider { } } } catch (IOException e) { Log.e(TAG, "Error on close", e); closePipeWithErrorSilently(readPipe, e.getMessage()); } finally { IoUtils.closeQuietly(inputStream); IoUtils.closeQuietly(outputStream); notifyParentChanged(document.parentId); getContext().getContentResolver().notifyChange( DocumentsContract.buildDocumentUri(mAuthority, document.documentId), null, false); } } }.start(); Loading @@ -302,7 +332,6 @@ public class StubProvider extends DocumentsProvider { @Override public Bundle call(String method, String arg, Bundle extras) { Log.d(TAG, "call: " + method + arg); switch (method) { case "clear": clearCacheAndBuildRoots(); Loading Loading @@ -376,30 +405,51 @@ public class StubProvider extends DocumentsProvider { } } public File createFile(String rootId, File parent, String mimeType, String name) throws IOException { StubDocument parentDoc = null; @VisibleForTesting public Uri createFile(String rootId, String path, String mimeType, byte[] content) throws FileNotFoundException, IOException { StubDocument root = mRoots.get(rootId).rootDocument; if (root == null) { throw new FileNotFoundException("No roots with the ID " + rootId + " were found"); } File file = new File(root.file, path.substring(1)); StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile())); if (parent == null) { // Use the root dir as the parent, if one wasn't specified. parentDoc = mRoots.get(rootId).rootDocument; parent = mStorage.get(createFile(rootId, file.getParentFile().getPath(), DocumentsContract.Document.MIME_TYPE_DIR, null)); } if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) { if (!file.mkdirs()) { throw new FileNotFoundException("Couldn't create directory " + file.getPath()); } } 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 (!file.createNewFile()) { throw new FileNotFoundException("Couldn't create file " + file.getPath()); } if (!Document.MIME_TYPE_DIR.equals(parentDoc.mimeType)) { throw new IllegalArgumentException("Parent file must be a directory."); // Add content to the file. FileOutputStream fout = new FileOutputStream(file); fout.write(content); fout.close(); } final StubDocument document = new StubDocument(file, mimeType, parent); return DocumentsContract.buildDocumentUri(mAuthority, document.documentId); } File file = new File(parentDoc.file, name); if (Document.MIME_TYPE_DIR.equals(mimeType)) { file.mkdir(); } else { file.createNewFile(); @VisibleForTesting public File getFile(String rootId, String path) throws FileNotFoundException { StubDocument root = mRoots.get(rootId).rootDocument; if (root == null) { throw new FileNotFoundException("No roots with the ID " + rootId + " were found"); } // Convert the path string into a path that's relative to the root. File needle = new File(root.file, path.substring(1)); StubDocument found = mStorage.get(getDocumentIdForFile(needle)); if (found == null) { return null; } new StubDocument(file, mimeType, parentDoc); return file; return found.file; } final class RootInfo { Loading