OSDN Git Service

Use StubProvider for functional tests.
authorSteve McKay <smckay@google.com>
Tue, 27 Oct 2015 00:03:55 +0000 (17:03 -0700)
committerSteve McKay <smckay@google.com>
Tue, 3 Nov 2015 21:18:00 +0000 (13:18 -0800)
Add a "UiBot" class for driving the UI from tests.
Add a "DocumentsProviderHelper" class for convenient test doc setup.
Update FilesActivityUiTest to use "TargetContext" which is
    necessary in order to perform ContentProvider
    operations.
Fix a bug where CopyTest relied on implicit order of roots.
Don't include guava in tests...since it breaks functional tests (incompatible class def).
Add test coverage for:
- basic roots list.
- basic files list.
- Live updates to files list.
- basic delete operations.

Bug: 24988170
Change-Id: I2ec01a5e1a474314cb33efb6e92df0f61dfcc1da
NOTE: This is currently broken at the point I try to init files in the stub roots.

packages/DocumentsUI/tests/Android.mk
packages/DocumentsUI/tests/src/com/android/documentsui/CopyTest.java
packages/DocumentsUI/tests/src/com/android/documentsui/DocumentsProviderHelper.java [new file with mode: 0644]
packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java
packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java
packages/DocumentsUI/tests/src/com/android/documentsui/UiBot.java [new file with mode: 0644]

index 2a540d4..b65ac98 100644 (file)
@@ -8,7 +8,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 ub-uiautomator
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 mockito-target ub-uiautomator
 
 LOCAL_PACKAGE_NAME := DocumentsUITests
 LOCAL_INSTRUMENTATION_FOR := DocumentsUI
index fc42c3b..6f1a89b 100644 (file)
@@ -28,6 +28,7 @@ import android.net.Uri;
 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;
@@ -36,6 +37,7 @@ 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;
@@ -52,70 +54,19 @@ 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 file operations to complete.");
-            }
-        }
-
-        @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 AUTHORITY = "com.android.documentsui.stubprovider";
-    private static String DST = "sd1";
-    private static String SRC = "sd0";
+    private static String SRC_ROOT = StubProvider.ROOT_0_ID;
+    private static String DST_ROOT = StubProvider.ROOT_1_ID;
     private static String TAG = "CopyTest";
-    private List<RootInfo> mRoots;
+
     private Context mContext;
     private TestContentResolver mResolver;
     private ContentProviderClient mClient;
+    private DocumentsProviderHelper mDocHelper;
     private StubProvider mStorage;
     private Context mSystemContext;
 
@@ -129,18 +80,7 @@ public class CopyTest extends ServiceTestCase<CopyService> {
         // Reset the stub provider's storage.
         mStorage.clearCacheAndBuildRoots();
 
-        mRoots = Lists.newArrayList();
-        Uri queryUri = DocumentsContract.buildRootsUri(AUTHORITY);
-        Cursor cursor = null;
-        try {
-            cursor = mClient.query(queryUri, null, null, null, null);
-            while (cursor.moveToNext()) {
-                mRoots.add(RootInfo.fromRootsCursor(AUTHORITY, cursor));
-            }
-        } finally {
-            IoUtils.closeQuietly(cursor);
-        }
-
+        mDocHelper = new DocumentsProviderHelper(AUTHORITY, mClient);
     }
 
     @Override
@@ -154,7 +94,7 @@ public class CopyTest extends ServiceTestCase<CopyService> {
      */
     public void testCopyFile() throws Exception {
         String srcPath = "/test0.txt";
-        Uri testFile = mStorage.createFile(SRC, srcPath, "text/plain",
+        Uri testFile = mStorage.createFile(SRC_ROOT, srcPath, "text/plain",
                 "The five boxing wizards jump quickly".getBytes());
 
         assertDstFileCountEquals(0);
@@ -172,7 +112,7 @@ public class CopyTest extends ServiceTestCase<CopyService> {
     public void testMoveFile() throws Exception {
         String srcPath = "/test0.txt";
         String testContent = "The five boxing wizards jump quickly";
-        Uri testFile = mStorage.createFile(SRC, srcPath, "text/plain", testContent.getBytes());
+        Uri testFile = mStorage.createFile(SRC_ROOT, srcPath, "text/plain", testContent.getBytes());
 
         assertDstFileCountEquals(0);
 
@@ -185,9 +125,9 @@ public class CopyTest extends ServiceTestCase<CopyService> {
 
         // Verify that one file was moved; check file contents.
         assertDstFileCountEquals(1);
-        assertDoesNotExist(SRC, srcPath);
+        assertDoesNotExist(SRC_ROOT, srcPath);
 
-        byte[] dstContent = readFile(DST, srcPath);
+        byte[] dstContent = readFile(DST_ROOT, srcPath);
         MoreAsserts.assertEquals("Moved file contents differ", testContent.getBytes(), dstContent);
     }
 
@@ -206,9 +146,9 @@ public class CopyTest extends ServiceTestCase<CopyService> {
                 "/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()));
+                mStorage.createFile(SRC_ROOT, srcPaths[0], "text/plain", testContent[0].getBytes()),
+                mStorage.createFile(SRC_ROOT, srcPaths[1], "text/plain", testContent[1].getBytes()),
+                mStorage.createFile(SRC_ROOT, srcPaths[2], "text/plain", testContent[2].getBytes()));
 
         assertDstFileCountEquals(0);
 
@@ -226,7 +166,7 @@ public class CopyTest extends ServiceTestCase<CopyService> {
 
     public void testCopyEmptyDir() throws Exception {
         String srcPath = "/emptyDir";
-        Uri testDir = mStorage.createFile(SRC, srcPath, DocumentsContract.Document.MIME_TYPE_DIR,
+        Uri testDir = mStorage.createFile(SRC_ROOT, srcPath, DocumentsContract.Document.MIME_TYPE_DIR,
                 null);
 
         assertDstFileCountEquals(0);
@@ -239,13 +179,13 @@ public class CopyTest extends ServiceTestCase<CopyService> {
         assertDstFileCountEquals(1);
 
         // Verify that the dst exists and is a directory.
-        File dst = mStorage.getFile(DST, srcPath);
+        File dst = mStorage.getFile(DST_ROOT, srcPath);
         assertTrue(dst.isDirectory());
     }
 
     public void testMoveEmptyDir() throws Exception {
         String srcPath = "/emptyDir";
-        Uri testDir = mStorage.createFile(SRC, srcPath, DocumentsContract.Document.MIME_TYPE_DIR,
+        Uri testDir = mStorage.createFile(SRC_ROOT, srcPath, DocumentsContract.Document.MIME_TYPE_DIR,
                 null);
 
         assertDstFileCountEquals(0);
@@ -260,11 +200,11 @@ public class CopyTest extends ServiceTestCase<CopyService> {
         assertDstFileCountEquals(1);
 
         // Verify that the dst exists and is a directory.
-        File dst = mStorage.getFile(DST, srcPath);
+        File dst = mStorage.getFile(DST_ROOT, srcPath);
         assertTrue(dst.isDirectory());
 
         // Verify that the src was cleaned up.
-        assertDoesNotExist(SRC, srcPath);
+        assertDoesNotExist(SRC_ROOT, srcPath);
     }
 
     public void testMovePopulatedDir() throws Exception {
@@ -280,11 +220,11 @@ public class CopyTest extends ServiceTestCase<CopyService> {
                 srcDir + "/test2.txt"
         };
         // Create test dir; put some files in it.
-        Uri testDir = mStorage.createFile(SRC, srcDir, DocumentsContract.Document.MIME_TYPE_DIR,
+        Uri testDir = mStorage.createFile(SRC_ROOT, srcDir, DocumentsContract.Document.MIME_TYPE_DIR,
                 null);
-        mStorage.createFile(SRC, srcFiles[0], "text/plain", testContent[0].getBytes());
-        mStorage.createFile(SRC, srcFiles[1], "text/plain", testContent[1].getBytes());
-        mStorage.createFile(SRC, srcFiles[2], "text/plain", testContent[2].getBytes());
+        mStorage.createFile(SRC_ROOT, srcFiles[0], "text/plain", testContent[0].getBytes());
+        mStorage.createFile(SRC_ROOT, srcFiles[1], "text/plain", testContent[1].getBytes());
+        mStorage.createFile(SRC_ROOT, srcFiles[2], "text/plain", testContent[2].getBytes());
 
         Intent moveIntent = createCopyIntent(Lists.newArrayList(testDir));
         moveIntent.putExtra(CopyService.EXTRA_TRANSFER_MODE, CopyService.TRANSFER_MODE_MOVE);
@@ -295,24 +235,24 @@ public class CopyTest extends ServiceTestCase<CopyService> {
         mResolver.waitForChanges(11);
 
         // Check the content of the moved files.
-        File dst = mStorage.getFile(DST, srcDir);
+        File dst = mStorage.getFile(DST_ROOT, srcDir);
         assertTrue(dst.isDirectory());
         for (int i = 0; i < testContent.length; ++i) {
-            byte[] dstContent = readFile(DST, srcFiles[i]);
+            byte[] dstContent = readFile(DST_ROOT, srcFiles[i]);
             MoreAsserts.assertEquals("Copied file contents differ", testContent[i].getBytes(),
                     dstContent);
         }
 
         // Check that the src files were removed.
-        assertDoesNotExist(SRC, srcDir);
+        assertDoesNotExist(SRC_ROOT, srcDir);
         for (String srcFile : srcFiles) {
-            assertDoesNotExist(SRC, srcFile);
+            assertDoesNotExist(SRC_ROOT, srcFile);
         }
     }
 
     public void testCopyFileWithReadErrors() throws Exception {
         String srcPath = "/test0.txt";
-        Uri testFile = mStorage.createFile(SRC, srcPath, "text/plain",
+        Uri testFile = mStorage.createFile(SRC_ROOT, srcPath, "text/plain",
                 "The five boxing wizards jump quickly".getBytes());
 
         assertDstFileCountEquals(0);
@@ -330,7 +270,7 @@ public class CopyTest extends ServiceTestCase<CopyService> {
 
     public void testMoveFileWithReadErrors() throws Exception {
         String srcPath = "/test0.txt";
-        Uri testFile = mStorage.createFile(SRC, srcPath, "text/plain",
+        Uri testFile = mStorage.createFile(SRC_ROOT, srcPath, "text/plain",
                 "The five boxing wizards jump quickly".getBytes());
 
         assertDstFileCountEquals(0);
@@ -352,7 +292,7 @@ public class CopyTest extends ServiceTestCase<CopyService> {
         } finally {
             // Verify that the failed copy was cleaned up, and the src file wasn't removed.
             assertDstFileCountEquals(0);
-            assertExists(SRC, srcPath);
+            assertExists(SRC_ROOT, srcPath);
         }
         // The asserts above didn't fail, but the CopyService did something unexpected.
         fail("Extra file operations were detected");
@@ -371,12 +311,12 @@ public class CopyTest extends ServiceTestCase<CopyService> {
                 srcDir + "/test2.txt"
         };
         // Create test dir; put some files in it.
-        Uri testDir = mStorage.createFile(SRC, srcDir, DocumentsContract.Document.MIME_TYPE_DIR,
+        Uri testDir = mStorage.createFile(SRC_ROOT, srcDir, DocumentsContract.Document.MIME_TYPE_DIR,
                 null);
-        mStorage.createFile(SRC, srcFiles[0], "text/plain", testContent[0].getBytes());
+        mStorage.createFile(SRC_ROOT, srcFiles[0], "text/plain", testContent[0].getBytes());
         Uri errFile = mStorage
-                .createFile(SRC, srcFiles[1], "text/plain", testContent[1].getBytes());
-        mStorage.createFile(SRC, srcFiles[2], "text/plain", testContent[2].getBytes());
+                .createFile(SRC_ROOT, srcFiles[1], "text/plain", testContent[1].getBytes());
+        mStorage.createFile(SRC_ROOT, srcFiles[2], "text/plain", testContent[2].getBytes());
 
         mStorage.simulateReadErrorsForFile(errFile);
 
@@ -391,22 +331,22 @@ public class CopyTest extends ServiceTestCase<CopyService> {
 
         // Check that both the src and dst dirs exist. The src dir shouldn't have been removed,
         // because it should contain the one errFile.
-        assertTrue(mStorage.getFile(SRC, srcDir).isDirectory());
-        assertTrue(mStorage.getFile(DST, srcDir).isDirectory());
+        assertTrue(mStorage.getFile(SRC_ROOT, srcDir).isDirectory());
+        assertTrue(mStorage.getFile(DST_ROOT, srcDir).isDirectory());
 
         // Check the content of the moved files.
         MoreAsserts.assertEquals("Copied file contents differ", testContent[0].getBytes(),
-                readFile(DST, srcFiles[0]));
+                readFile(DST_ROOT, srcFiles[0]));
         MoreAsserts.assertEquals("Copied file contents differ", testContent[2].getBytes(),
-                readFile(DST, srcFiles[2]));
+                readFile(DST_ROOT, srcFiles[2]));
 
         // Check that the src files were removed.
-        assertDoesNotExist(SRC, srcFiles[0]);
-        assertDoesNotExist(SRC, srcFiles[2]);
+        assertDoesNotExist(SRC_ROOT, srcFiles[0]);
+        assertDoesNotExist(SRC_ROOT, srcFiles[2]);
 
         // Check that the error file was not copied over.
-        assertDoesNotExist(DST, srcFiles[1]);
-        assertExists(SRC, srcFiles[1]);
+        assertDoesNotExist(DST_ROOT, srcFiles[1]);
+        assertExists(SRC_ROOT, srcFiles[1]);
     }
 
     /**
@@ -414,13 +354,14 @@ public class CopyTest extends ServiceTestCase<CopyService> {
      *
      * @throws FileNotFoundException
      */
-    private Intent createCopyIntent(List<Uri> srcs) throws FileNotFoundException {
+    private Intent createCopyIntent(List<Uri> srcs) throws Exception {
         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);
+        RootInfo root = mDocHelper.getRoot(DST_ROOT);
+        final Uri dst = DocumentsContract.buildDocumentUri(AUTHORITY, root.documentId);
         DocumentStack stack = new DocumentStack();
         stack.push(DocumentInfo.fromUri(mResolver, dst));
         final Intent copyIntent = new Intent(mContext, CopyService.class);
@@ -435,8 +376,9 @@ public class CopyTest extends ServiceTestCase<CopyService> {
      * Returns a count of the files in the given directory.
      */
     private void assertDstFileCountEquals(int expected) throws RemoteException {
+        RootInfo dest = mDocHelper.getRoot(DST_ROOT);
         final Uri queryUri = DocumentsContract.buildChildDocumentsUri(AUTHORITY,
-                mRoots.get(1).documentId);
+                dest.documentId);
         Cursor c = null;
         int count = 0;
         try {
@@ -474,8 +416,8 @@ public class CopyTest extends ServiceTestCase<CopyService> {
     }
 
     private void assertCopied(String path) throws Exception {
-        MoreAsserts.assertEquals("Copied file contents differ", readFile(SRC, path),
-                readFile(DST, path));
+        MoreAsserts.assertEquals("Copied file contents differ", readFile(SRC_ROOT, path),
+                readFile(DST_ROOT, path));
     }
 
     /**
@@ -509,4 +451,56 @@ public class CopyTest extends ServiceTestCase<CopyService> {
         mStorage.attachInfo(mContext, info);
         mResolver.addProvider(AUTHORITY, mStorage);
     }
+
+    /**
+     * 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 file operations to complete.");
+            }
+        }
+
+        @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();
+            }
+        }
+    };
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/DocumentsProviderHelper.java b/packages/DocumentsUI/tests/src/com/android/documentsui/DocumentsProviderHelper.java
new file mode 100644 (file)
index 0000000..7abc99c
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * 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.content.ContentProviderClient;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsContract.Root;
+
+import com.android.documentsui.model.RootInfo;
+
+import libcore.io.IoUtils;
+
+/**
+ * Provides support for creation of documents in a test settings.
+ */
+public class DocumentsProviderHelper {
+
+    private final ContentProviderClient mClient;
+    private final String mAuthority;
+
+    public DocumentsProviderHelper(String authority, ContentProviderClient client) {
+        mClient = client;
+        mAuthority = authority;
+    }
+
+    public RootInfo getRoot(String id) throws RemoteException {
+        final Uri rootsUri = DocumentsContract.buildRootsUri(mAuthority);
+
+        Cursor cursor = null;
+        try {
+            cursor = mClient.query(rootsUri, null, null, null, null);
+            while (cursor.moveToNext()) {
+                if (id.equals(getCursorString(cursor, Root.COLUMN_ROOT_ID))) {
+                    return RootInfo.fromRootsCursor(mAuthority, cursor);
+                }
+            }
+            throw new IllegalArgumentException("Can't find matching root for id=" + id);
+        } catch (Exception e) {
+            throw new RuntimeException("Can't load root for id=" + id , e);
+        } finally {
+            IoUtils.closeQuietly(cursor);
+        }
+    }
+
+    public Uri createDocument(Uri parentUri, String mimeType, String name) {
+        if (name.contains("/")) {
+            throw new IllegalArgumentException("Name and mimetype probably interposed.");
+        }
+        try {
+            return DocumentsContract.createDocument(mClient, parentUri, mimeType, name);
+        } catch (RemoteException e) {
+            throw new RuntimeException("Couldn't create document: " + name + " with mimetype " + mimeType, e);
+        }
+    }
+
+    public Uri createFolder(Uri parentUri, String name) {
+        return createDocument(parentUri, Document.MIME_TYPE_DIR, name);
+    }
+
+    public Uri createDocument(RootInfo root, String mimeType, String name) {
+        Uri rootUri = DocumentsContract.buildDocumentUri(mAuthority, root.documentId);
+        return createDocument(rootUri, mimeType, name);
+    }
+
+    public Uri createFolder(RootInfo root, String name) {
+        return createDocument(root, Document.MIME_TYPE_DIR, name);
+    }
+}
index 1f4b751..9060516 100644 (file)
 
 package com.android.documentsui;
 
+import static com.android.documentsui.StubProvider.DEFAULT_AUTHORITY;
+import static com.android.documentsui.StubProvider.ROOT_0_ID;
+import static com.android.documentsui.StubProvider.ROOT_1_ID;
+
+import android.app.Instrumentation;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
+import android.os.RemoteException;
 import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.Configurator;
 import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
 import android.test.InstrumentationTestCase;
+import android.util.Log;
 import android.view.MotionEvent;
 
-import java.util.concurrent.TimeoutException;
+import com.android.documentsui.model.RootInfo;
 
 public class FilesActivityUiTest extends InstrumentationTestCase {
 
+    private static final int TIMEOUT = 5000;
     private static final String TAG = "FilesActivityUiTest";
     private static final String TARGET_PKG = "com.android.documentsui";
     private static final String LAUNCHER_PKG = "com.android.launcher";
-    private static final int ONE_SECOND = 1000;
-    private static final int FIVE_SECONDS = 5 * ONE_SECOND;
 
-    private ActionBar mBar;
+    private UiBot mBot;
     private UiDevice mDevice;
     private Context mContext;
+    private ContentResolver mResolver;
+    private DocumentsProviderHelper mDocsHelper;
+    private ContentProviderClient mClient;
+    private RootInfo mRoot_0;
+    private RootInfo mRoot_1;
 
-    public void setUp() throws TimeoutException {
+    public void setUp() throws Exception {
         // Initialize UiDevice instance.
-        mDevice = UiDevice.getInstance(getInstrumentation());
+        Instrumentation instrumentation = getInstrumentation();
+
+        mDevice = UiDevice.getInstance(instrumentation);
 
         Configurator.getInstance().setToolType(MotionEvent.TOOL_TYPE_MOUSE);
 
         // Start from the home screen.
         mDevice.pressHome();
-        mDevice.wait(Until.hasObject(By.pkg(LAUNCHER_PKG).depth(0)), FIVE_SECONDS);
+        mDevice.wait(Until.hasObject(By.pkg(LAUNCHER_PKG).depth(0)), TIMEOUT);
+
+        // NOTE: Must be the "target" context, else security checks in content provider will fail.
+        mContext = instrumentation.getTargetContext();
+        mResolver = mContext.getContentResolver();
+
+        mClient = mResolver.acquireUnstableContentProviderClient(DEFAULT_AUTHORITY);
+        mDocsHelper = new DocumentsProviderHelper(DEFAULT_AUTHORITY, mClient);
 
         // Launch app.
-        mContext = getInstrumentation().getContext();
         Intent intent = mContext.getPackageManager().getLaunchIntentForPackage(TARGET_PKG);
         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
         mContext.startActivity(intent);
 
         // Wait for the app to appear.
-        mDevice.wait(Until.hasObject(By.pkg(TARGET_PKG).depth(0)), FIVE_SECONDS);
+        mDevice.wait(Until.hasObject(By.pkg(TARGET_PKG).depth(0)), TIMEOUT);
+        mDevice.waitForIdle();
+
+        mBot = new UiBot(mDevice, TIMEOUT);
+
+        resetStorage();  // Just incase a test failed and tearDown didn't happen.
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        Log.d(TAG, "Resetting storage from setUp");
+        resetStorage();
+        mClient.release();
+    }
+
+    private void resetStorage() throws RemoteException {
+        mClient.call("clear", null, null);
+        // TODO: Would be nice to have an event to wait on here.
         mDevice.waitForIdle();
+    }
+
+    private void initTestFiles() throws RemoteException {
+        mRoot_0 = mDocsHelper.getRoot(ROOT_0_ID);
+        mRoot_1 = mDocsHelper.getRoot(ROOT_1_ID);
+
+        mDocsHelper.createDocument(mRoot_0, "text/plain", "file0.log");
+        mDocsHelper.createDocument(mRoot_0, "image/png", "file1.png");
+        mDocsHelper.createDocument(mRoot_0, "text/csv", "file2.csv");
+
+        mDocsHelper.createDocument(mRoot_1, "text/plain", "anotherFile0.log");
+        mDocsHelper.createDocument(mRoot_1, "text/plain", "poodles.text");
+    }
 
-        mBar = new ActionBar();
+    public void testRootsListed() throws Exception {
+        initTestFiles();
+
+        mBot.openRoot(ROOT_0_ID);
+
+        // Should also have Drive, but that requires pre-configuration of devices
+        // We omit for now.
+        mBot.assertHasRoots(
+                "Images",
+                "Videos",
+                "Audio",
+                "Downloads",
+                ROOT_0_ID,
+                ROOT_1_ID);
+    }
+
+    public void testFilesListed() throws Exception {
+        initTestFiles();
+
+        mBot.openRoot(ROOT_0_ID);
+        mBot.assertHasDocuments("file0.log", "file1.png", "file2.csv");
     }
 
-    public void testSwitchMode() throws Exception {
-        UiObject2 mode = mBar.gridMode(100);
-        if (mode != null) {
-            mode.click();
-            assertNotNull(mBar.listMode(ONE_SECOND));
-        } else {
-            mBar.listMode(100).click();
-            assertNotNull(mBar.gridMode(ONE_SECOND));
-        }
+    public void testFilesList_LiveUpdate() throws Exception {
+        initTestFiles();
+
+        mBot.openRoot(ROOT_0_ID);
+        mDocsHelper.createDocument(mRoot_0, "yummers/sandwich", "Ham & Cheese.sandwich");
+        mBot.assertHasDocuments("file0.log", "file1.png", "file2.csv", "Ham & Cheese.sandwich");
     }
 
-    private class ActionBar {
-
-        public UiObject2 gridMode(int timeout) {
-            // Note that we're using By.desc rather than By.res, because of b/25285770
-            BySelector selector = By.desc("Grid view");
-            if (timeout > 0) {
-                mDevice.wait(Until.findObject(selector), timeout);
-            }
-            return mDevice.findObject(selector);
-        }
-
-        public UiObject2 listMode(int timeout) {
-            // Note that we're using By.desc rather than By.res, because of b/25285770
-            BySelector selector = By.desc("List view");
-            if (timeout > 0) {
-                mDevice.wait(Until.findObject(selector), timeout);
-            }
-            return mDevice.findObject(selector);
-        }
+    public void testDeleteDocument() throws Exception {
+        initTestFiles();
+
+        mBot.openRoot(ROOT_0_ID);
+
+        mBot.clickDocument("file1.png");
+        mDevice.waitForIdle();
+        mBot.menuDelete().click();
+
+        mBot.waitForDeleteSnackbar();
+        assertFalse(mBot.hasDocuments("file1.png"));
+
+        mBot.waitForDeleteSnackbarGone();
+        assertFalse(mBot.hasDocuments("file1.png"));
+
+        // Now delete from another root.
+        mBot.openRoot(ROOT_1_ID);
+
+        mBot.clickDocument("poodles.text");
+        mDevice.waitForIdle();
+        mBot.menuDelete().click();
+
+        mBot.waitForDeleteSnackbar();
+        assertFalse(mBot.hasDocuments("poodles.text"));
+
+        mBot.waitForDeleteSnackbarGone();
+        assertFalse(mBot.hasDocuments("poodles.text"));
     }
 }
index 6a2e03a..2d42ddc 100644 (file)
@@ -50,12 +50,17 @@ import java.util.HashMap;
 import java.util.Map;
 
 public class StubProvider extends DocumentsProvider {
+
+    public static final String DEFAULT_AUTHORITY = "com.android.documentsui.stubprovider";
+    public static final String ROOT_0_ID = "TEST_ROOT_0";
+    public static final String ROOT_1_ID = "TEST_ROOT_1";
+
+    private static final String TAG = "StubProvider";
     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 = "sd0";
+    private static int DEFAULT_ROOT_SIZE = 1024 * 1024 * 100; // 100 MB.
+
     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
@@ -65,11 +70,12 @@ public class StubProvider extends DocumentsProvider {
             Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
     };
 
-    private HashMap<String, StubDocument> mStorage = new HashMap<String, StubDocument>();
-    private Object mWriteLock = new Object();
-    private String mAuthority;
+    private final Map<String, StubDocument> mStorage = new HashMap<>();
+    private final Map<String, RootInfo> mRoots = new HashMap<>();
+    private final Object mWriteLock = new Object();
+
+    private String mAuthority = DEFAULT_AUTHORITY;
     private SharedPreferences mPrefs;
-    private Map<String, RootInfo> mRoots;
     private String mSimulateReadErrors;
 
     @Override
@@ -86,20 +92,18 @@ public class StubProvider extends DocumentsProvider {
 
     @VisibleForTesting
     public void clearCacheAndBuildRoots() {
-        final File cacheDir = getContext().getCacheDir();
-        removeRecursively(cacheDir);
+        Log.d(TAG, "Resetting storage.");
+        removeChildrenRecursively(getContext().getCacheDir());
         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"
-            });
+            rootIds = Arrays.asList(new String[] { ROOT_0_ID, ROOT_1_ID });
         }
-        // Create new roots.
-        mRoots = new HashMap<>();
+
+        mRoots.clear();
         for (String rootId : rootIds) {
             final RootInfo rootInfo = new RootInfo(rootId, getSize(rootId));
             mRoots.put(rootId, rootInfo);
@@ -111,7 +115,7 @@ public class StubProvider extends DocumentsProvider {
      */
     private long getSize(String rootId) {
         final String key = STORAGE_SIZE_KEY + "." + rootId;
-        return mPrefs.getLong(key, DEFAULT_SIZE);
+        return mPrefs.getLong(key, DEFAULT_ROOT_SIZE);
     }
 
     @Override
@@ -125,7 +129,7 @@ public class StubProvider extends DocumentsProvider {
             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_DOCUMENT_ID, info.document.documentId);
             row.add(Root.COLUMN_AVAILABLE_BYTES, info.getRemainingCapacity());
         }
         return result;
@@ -152,28 +156,48 @@ public class StubProvider extends DocumentsProvider {
     }
 
     @Override
-    public String createDocument(String parentDocumentId, String mimeType, String displayName)
+    public String createDocument(String parentId, String mimeType, String displayName)
             throws FileNotFoundException {
-        final StubDocument parentDocument = mStorage.get(parentDocumentId);
-        if (parentDocument == null || !parentDocument.file.isDirectory()) {
-            throw new FileNotFoundException();
+
+        final StubDocument parent = mStorage.get(parentId);
+        if (parent == null) {
+            throw new IllegalArgumentException(
+                    "Can't create file " + displayName + " in null parent.");
+        }
+        if (!parent.file.isDirectory()) {
+            throw new IllegalArgumentException(
+                    "Can't create file " + displayName + " inside non-directory parent "
+                    + parent.file.getName());
         }
-        final File file = new File(parentDocument.file, displayName);
+
+        final File file = new File(parent.file, displayName);
+        if (file.exists()) {
+            throw new FileNotFoundException(
+                    "Duplicate file names not supported for " + file);
+        }
+
         if (mimeType.equals(Document.MIME_TYPE_DIR)) {
             if (!file.mkdirs()) {
-                throw new FileNotFoundException();
+                throw new FileNotFoundException(
+                        "Failed to create directory(s): " + file);
             }
+            Log.i(TAG, "Created new directory: " + file);
         } else {
+            boolean created = false;
             try {
-                if (!file.createNewFile()) {
-                    throw new IllegalStateException("The file " + file.getPath() + " already exists");
-                }
+                created = file.createNewFile();
             } catch (IOException e) {
-                throw new FileNotFoundException();
+                // We'll throw an FNF exception later :)
+                Log.e(TAG, "createnewFile operation failed for file: " + file, e);
+            }
+            if (!created) {
+                throw new FileNotFoundException(
+                        "createNewFile operation failed for: " + file);
             }
+            Log.i(TAG, "Created new file: " + file);
         }
 
-        final StubDocument document = new StubDocument(file, mimeType, parentDocument);
+        final StubDocument document = new StubDocument(file, mimeType, parent);
         Log.d(TAG, "Created document " + document.documentId);
         notifyParentChanged(document.parentId);
         getContext().getContentResolver().notifyChange(
@@ -349,7 +373,7 @@ public class StubProvider extends DocumentsProvider {
 
     private void configure(String arg, Bundle extras) {
         Log.d(TAG, "Configure " + arg);
-        String rootName = extras.getString(EXTRA_ROOT, MY_ROOT_ID);
+        String rootName = extras.getString(EXTRA_ROOT, ROOT_0_ID);
         long rootSize = extras.getLong(EXTRA_SIZE, 1) * 1024 * 1024;
         setSize(rootName, rootSize);
     }
@@ -379,10 +403,10 @@ public class StubProvider extends DocumentsProvider {
         row.add(Document.COLUMN_LAST_MODIFIED, document.file.lastModified());
     }
 
-    private void removeRecursively(File file) {
+    private void removeChildrenRecursively(File file) {
         for (File childFile : file.listFiles()) {
             if (childFile.isDirectory()) {
-                removeRecursively(childFile);
+                removeChildrenRecursively(childFile);
             }
             childFile.delete();
         }
@@ -411,8 +435,8 @@ public class StubProvider extends DocumentsProvider {
     @VisibleForTesting
     public Uri createFile(String rootId, String path, String mimeType, byte[] content)
             throws FileNotFoundException, IOException {
-        Log.d(TAG, "Creating file " + rootId + ":" + path);
-        StubDocument root = mRoots.get(rootId).rootDocument;
+        Log.d(TAG, "Creating test file " + rootId + ":" + path);
+        StubDocument root = mRoots.get(rootId).document;
         if (root == null) {
             throw new FileNotFoundException("No roots with the ID " + rootId + " were found");
         }
@@ -445,7 +469,7 @@ public class StubProvider extends DocumentsProvider {
 
     @VisibleForTesting
     public File getFile(String rootId, String path) throws FileNotFoundException {
-        StubDocument root = mRoots.get(rootId).rootDocument;
+        StubDocument root = mRoots.get(rootId).document;
         if (root == null) {
             throw new FileNotFoundException("No roots with the ID " + rootId + " were found");
         }
@@ -461,7 +485,7 @@ public class StubProvider extends DocumentsProvider {
 
     final class RootInfo {
         public final String name;
-        public final StubDocument rootDocument;
+        public final StubDocument document;
         public long capacity;
         public long size;
 
@@ -469,9 +493,11 @@ public class StubProvider extends DocumentsProvider {
             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);
+            File file = new File(getContext().getCacheDir(), name);
+            if (file.mkdir()) {
+                Log.i(TAG, "Created new root directory @ " + file.getPath());
+            }
+            this.document = new StubDocument(file, Document.MIME_TYPE_DIR, this);
             this.capacity = capacity;
             this.size = 0;
         }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/UiBot.java b/packages/DocumentsUI/tests/src/com/android/documentsui/UiBot.java
new file mode 100644 (file)
index 0000000..5c09794
--- /dev/null
@@ -0,0 +1,170 @@
+/*
+ * 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 android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.UiScrollable;
+import android.support.test.uiautomator.UiSelector;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * A test helper class that provides support for controlling DocumentsUI activities
+ * programmatically, and making assertions against the state of the UI.
+ */
+class UiBot {
+
+    private static final String TAG = "UiBot";
+
+    private static final BySelector SNACK_DELETE =
+            By.desc(Pattern.compile("^Deleting [0-9]+ file.+"));
+
+    private UiDevice mDevice;
+    private int mTimeout;
+
+    public UiBot(UiDevice device, int timeout) {
+        mDevice = device;
+        mTimeout = timeout;
+    }
+
+    UiObject findRoot(String label) throws UiObjectNotFoundException {
+        final UiSelector rootsList = new UiSelector().resourceId(
+                "com.android.documentsui:id/container_roots").childSelector(
+                new UiSelector().resourceId("android:id/list"));
+
+        // We might need to expand drawer if not visible
+        if (!new UiObject(rootsList).waitForExists(mTimeout)) {
+            Log.d(TAG, "Failed to find roots list; trying to expand");
+            final UiSelector hamburger = new UiSelector().resourceId(
+                    "com.android.documentsui:id/toolbar").childSelector(
+                    new UiSelector().className("android.widget.ImageButton").clickable(true));
+            new UiObject(hamburger).click();
+        }
+
+        // Wait for the first list item to appear
+        new UiObject(rootsList.childSelector(new UiSelector())).waitForExists(mTimeout);
+
+        // Now scroll around to find our item
+        new UiScrollable(rootsList).scrollIntoView(new UiSelector().text(label));
+        return new UiObject(rootsList.childSelector(new UiSelector().text(label)));
+    }
+
+    void openRoot(String label) throws UiObjectNotFoundException {
+        findRoot(label).click();
+        mDevice.waitForIdle();
+    }
+
+    void assertHasRoots(String... labels) throws UiObjectNotFoundException {
+        List<String> missing = new ArrayList<>();
+        for (String label : labels) {
+            if (!findRoot(label).exists()) {
+                missing.add(label);
+            }
+        }
+        if (!missing.isEmpty()) {
+            Assert.fail(
+                    "Expected roots " + Arrays.asList(labels) + ", but missing " + missing);
+        }
+    }
+
+    UiObject findDocument(String label) throws UiObjectNotFoundException {
+        final UiSelector docList = new UiSelector().resourceId(
+                "com.android.documentsui:id/container_directory").childSelector(
+                        new UiSelector().resourceId("com.android.documentsui:id/list"));
+
+        // Wait for the first list item to appear
+        new UiObject(docList.childSelector(new UiSelector())).waitForExists(mTimeout);
+
+        // new UiScrollable(docList).scrollIntoView(new UiSelector().text(label));
+        return mDevice.findObject(docList.childSelector(new UiSelector().text(label)));
+    }
+
+    boolean hasDocuments(String... labels) throws UiObjectNotFoundException {
+        for (String label : labels) {
+            if (!findDocument(label).exists()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    void assertHasDocuments(String... labels) throws UiObjectNotFoundException {
+        List<String> missing = new ArrayList<>();
+        for (String label : labels) {
+            if (!findDocument(label).exists()) {
+                missing.add(label);
+            }
+        }
+        if (!missing.isEmpty()) {
+            Assert.fail(
+                    "Expected documents " + Arrays.asList(labels) + ", but missing " + missing);
+        }
+    }
+
+    void clickDocument(String label) throws UiObjectNotFoundException {
+        findDocument(label).click();
+    }
+
+    void waitForDeleteSnackbar() {
+        mDevice.wait(Until.findObject(SNACK_DELETE), mTimeout);
+    }
+
+    void waitForDeleteSnackbarGone() {
+        // wait a little longer for snackbar to go away, as it disappears after a timeout.
+        mDevice.wait(Until.gone(SNACK_DELETE), mTimeout * 2);
+    }
+
+    void switchViewMode() {
+        UiObject2 mode = menuGridMode();
+        if (mode != null) {
+            mode.click();
+        } else {
+            menuListMode().click();
+        }
+    }
+
+    UiObject2 menuGridMode() {
+        // Note that we're using By.desc rather than By.res, because of b/25285770
+        return find(By.desc("Grid view"));
+    }
+
+    UiObject2 menuListMode() {
+        // Note that we're using By.desc rather than By.res, because of b/25285770
+        return find(By.desc("List view"));
+    }
+
+    UiObject2 menuDelete() {
+        return find(By.res("com.android.documentsui:id/menu_delete"));
+    }
+
+    private UiObject2 find(BySelector selector) {
+        mDevice.wait(Until.findObject(selector), mTimeout);
+        return mDevice.findObject(selector);
+    }
+}