Adding pullDir to ContentProviderHandler.

pullDir obtains contents of the directory by calling "adb shell content query",
and then recursively calls pullDir or pullFile.

Bug: 123529934
Fixes: 123528227
Test: unit tests; atest TradefedContentProviderHostTest; running any cts module with Device Info collection
Change-Id: Ib941edd54688d03a782c25fcc01b781aeed9933f
Merged-In: Ib941edd54688d03a782c25fcc01b781aeed9933f
diff --git a/src/com/android/tradefed/device/NativeDevice.java b/src/com/android/tradefed/device/NativeDevice.java
index 8857952..4541f3a 100644
--- a/src/com/android/tradefed/device/NativeDevice.java
+++ b/src/com/android/tradefed/device/NativeDevice.java
@@ -1446,6 +1446,13 @@
     @Override
     public boolean pullDir(String deviceFilePath, File localDir)
             throws DeviceNotAvailableException {
+        if (deviceFilePath.startsWith(SD_CARD)) {
+            ContentProviderHandler handler = getContentProvider();
+            if (handler != null) {
+                return handler.pullDir(deviceFilePath, localDir);
+            }
+        }
+
         if (!localDir.isDirectory()) {
             CLog.e("Local path %s is not a directory", localDir.getAbsolutePath());
             return false;
diff --git a/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java b/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
index fb524f5..482b970 100644
--- a/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
+++ b/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
@@ -55,6 +55,7 @@
     public static final String COLUMN_MIME_TYPE = "mime_type";
     public static final String COLUMN_METADATA = "metadata";
     public static final String QUERY_INFO_VALUE = "INFO";
+    public static final String NO_RESULTS_STRING = "No result found.";
 
     // Has to be kept in sync with columns in ManagedFileContentProvider.java.
     public static final String[] COLUMNS =
@@ -145,6 +146,66 @@
     }
 
     /**
+     * Recursively pull directory contents from device using content provider.
+     *
+     * @param deviceFilePath the absolute file path of the remote source
+     * @param localDir the local directory to pull files into
+     * @return <code>true</code> if file was pulled successfully. <code>false</code> otherwise.
+     * @throws DeviceNotAvailableException if connection with device is lost and cannot be
+     *     recovered.
+     */
+    public boolean pullDir(String deviceFilePath, File localDir)
+            throws DeviceNotAvailableException {
+        if (!localDir.isDirectory()) {
+            CLog.e("Local path %s is not a directory", localDir.getAbsolutePath());
+            return false;
+        }
+
+        String contentUri = createEscapedContentUri(deviceFilePath);
+        String queryContentCommand =
+                String.format(
+                        "content query --user %d --uri %s", mDevice.getCurrentUser(), contentUri);
+
+        String listCommandResult = mDevice.executeShellCommand(queryContentCommand);
+
+        if (listCommandResult.equals(NO_RESULTS_STRING)) {
+            // Empty directory.
+            return true;
+        }
+
+        String[] listResult = listCommandResult.split("[\\r\\n]+");
+
+        for (String row : listResult) {
+            HashMap<String, String> columnValues = parseQueryResultRow(row);
+            boolean isDirectory = Boolean.valueOf(columnValues.get(COLUMN_DIRECTORY));
+            String name = columnValues.get(COLUMN_NAME);
+            String path = columnValues.get(COLUMN_ABSOLUTE_PATH);
+
+            File localChild = new File(localDir, name);
+            if (isDirectory) {
+                if (!localChild.mkdir()) {
+                    CLog.w(
+                            "Failed to create sub directory %s, aborting.",
+                            localChild.getAbsolutePath());
+                    return false;
+                }
+
+                if (!pullDir(path, localChild)) {
+                    CLog.w("Failed to pull sub directory %s from device, aborting", path);
+                    return false;
+                }
+            } else {
+                // handle regular file
+                if (!pullFile(path, localChild)) {
+                    CLog.w("Failed to pull file %s from device, aborting", path);
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
      * Content provider callback that pulls a file from the URI location into a local file.
      *
      * @param deviceFilePath The path on the device where to pull the file from.
diff --git a/tests/src/com/android/tradefed/device/NativeDeviceTest.java b/tests/src/com/android/tradefed/device/NativeDeviceTest.java
index c34c55e..f1410e2 100644
--- a/tests/src/com/android/tradefed/device/NativeDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/NativeDeviceTest.java
@@ -366,12 +366,12 @@
         children.add(fakeFile);
         EasyMock.expect(fakeFile.isDirectory()).andReturn(false);
         EasyMock.expect(fakeFile.getName()).andReturn("fakeFile");
-        EasyMock.expect(fakeFile.getFullPath()).andReturn("/sdcard/screenshots/fakeFile");
+        EasyMock.expect(fakeFile.getFullPath()).andReturn("/some_device_path/fakeFile");
 
         children.add(fakeDir);
         EasyMock.expect(fakeDir.isDirectory()).andReturn(true);
         EasyMock.expect(fakeDir.getName()).andReturn("fakeDir");
-        EasyMock.expect(fakeDir.getFullPath()).andReturn("/sdcard/screenshots/fakeDir");
+        EasyMock.expect(fakeDir.getFullPath()).andReturn("/some_device_path/fakeDir");
         // #pullDir is being called on dir fakeDir to pull everything recursively.
         Collection<IFileEntry> fakeDirChildren = new ArrayList<>();
         EasyMock.expect(fakeDir.getChildren(false)).andReturn(fakeDirChildren);
@@ -379,7 +379,7 @@
 
         EasyMock.replay(fakeEntry, fakeFile, fakeDir);
         try {
-            boolean res = mTestDevice.pullDir("/sdcard/screenshots/", dir);
+            boolean res = mTestDevice.pullDir("/some_device_path/", dir);
             assertTrue(res);
             assertEquals(2, dir.list().length);
             assertTrue(Arrays.asList(dir.list()).contains("fakeFile"));
@@ -439,12 +439,12 @@
         children.add(fakeFile);
         EasyMock.expect(fakeFile.isDirectory()).andReturn(false);
         EasyMock.expect(fakeFile.getName()).andReturn("fakeFile");
-        EasyMock.expect(fakeFile.getFullPath()).andReturn("/sdcard/screenshots/fakeFile");
+        EasyMock.expect(fakeFile.getFullPath()).andReturn("/some_device_path/fakeFile");
 
         children.add(fakeDir);
         EasyMock.expect(fakeDir.isDirectory()).andReturn(true);
         EasyMock.expect(fakeDir.getName()).andReturn("fakeDir");
-        EasyMock.expect(fakeDir.getFullPath()).andReturn("/sdcard/screenshots/fakeDir");
+        EasyMock.expect(fakeDir.getFullPath()).andReturn("/some_device_path/fakeDir");
         // #pullDir is being called on dir fakeDir to pull everything recursively.
         Collection<IFileEntry> fakeDirChildren = new ArrayList<>();
         IFileEntry secondLevelChildren = EasyMock.createMock(IFileEntry.class);
@@ -455,11 +455,11 @@
         EasyMock.expect(secondLevelChildren.isDirectory()).andReturn(false);
         EasyMock.expect(secondLevelChildren.getName()).andReturn("secondLevelChildren");
         EasyMock.expect(secondLevelChildren.getFullPath())
-                .andReturn("/sdcard/screenshots/fakeDir/secondLevelChildren");
+                .andReturn("/some_device_path/fakeDir/secondLevelChildren");
 
         EasyMock.replay(fakeEntry, fakeFile, fakeDir, secondLevelChildren);
         try {
-            boolean res = mTestDevice.pullDir("/sdcard/screenshots/", dir);
+            boolean res = mTestDevice.pullDir("/some_device_path/", dir);
             // If one of the pull fails, the full command is considered failed.
             assertFalse(res);
             assertEquals(2, dir.list().length);
@@ -2389,133 +2389,133 @@
         EasyMock.verify(mMockRunUtil, mMockIDevice);
     }
 
-// FIXME: delete this, not necessary
-// <<<<<<< HEAD
-// =======
-//
-//     /** Unit test for {@link INativeDevice#setProperty(String, String)}. */
-//     @Test
-//     public void testSetProperty() throws DeviceNotAvailableException {
-//         OutputStream stdout = null, stderr = null;
-//         mTestDevice =
-//                 new TestableAndroidNativeDevice() {
-//                     @Override
-//                     public boolean isAdbRoot() throws DeviceNotAvailableException {
-//                         return true;
-//                     }
-//                 };
-//         CommandResult res = new CommandResult();
-//         res.setStatus(CommandStatus.SUCCESS);
-//         EasyMock.expect(
-//                         mMockRunUtil.runTimedCmd(
-//                                 120000, stdout, stderr, "adb", "-s", "serial", "shell", "setprop",
-//                                 "test", "value"))
-//                 .andReturn(res);
-//         EasyMock.replay(mMockRunUtil, mMockIDevice);
-//         assertTrue(mTestDevice.setProperty("test", "value"));
-//         EasyMock.verify(mMockRunUtil, mMockIDevice);
-//     }
-//
-//     /** Unit test for {@link INativeDevice#setProperty(String, String)}. */
-//     @Test
-//     public void testSetProperty_notRoot() throws DeviceNotAvailableException {
-//         mTestDevice =
-//                 new TestableAndroidNativeDevice() {
-//                     @Override
-//                     public boolean isAdbRoot() throws DeviceNotAvailableException {
-//                         return false;
-//                     }
-//                 };
-//         EasyMock.replay(mMockRunUtil, mMockIDevice);
-//         assertFalse(mTestDevice.setProperty("test", "value"));
-//         EasyMock.verify(mMockRunUtil, mMockIDevice);
-//     }
-//
-//     /**
-//      * Verifies that {@link INativeDevice#isExecutable(String)} recognizes regular executable file
-//      *
-//      * @throws Exception
-//      */
-//     @Test
-//     public void testIsDeviceFileExecutable_executable_rwx() throws Exception {
-//         mTestDevice =
-//                 new TestableAndroidNativeDevice() {
-//                     @Override
-//                     public String executeShellCommand(String command)
-//                             throws DeviceNotAvailableException {
-//                         return "-rwxr-xr-x 1 root shell 42824 2009-01-01 00:00 /system/bin/ping";
-//                     }
-//                 };
-//         assertTrue(mTestDevice.isExecutable("/system/bin/ping"));
-//     }
-//
-//     /**
-//      * Verifies that {@link INativeDevice#isExecutable(String)} recognizes symlink'd executable file
-//      *
-//      * @throws Exception
-//      */
-//     @Test
-//     public void testIsDeviceFileExecutable_executable_lrwx() throws Exception {
-//         mTestDevice =
-//                 new TestableAndroidNativeDevice() {
-//                     @Override
-//                     public String executeShellCommand(String command)
-//                             throws DeviceNotAvailableException {
-//                         return "lrwxr-xr-x 1 root shell 7 2009-01-01 00:00 /system/bin/start -> toolbox";
-//                     }
-//                 };
-//         assertTrue(mTestDevice.isExecutable("/system/bin/start"));
-//     }
-//
-//     /**
-//      * Verifies that {@link INativeDevice#isExecutable(String)} recognizes non-executable file
-//      *
-//      * @throws Exception
-//      */
-//     @Test
-//     public void testIsDeviceFileExecutable_notExecutable() throws Exception {
-//         mTestDevice =
-//                 new TestableAndroidNativeDevice() {
-//                     @Override
-//                     public String executeShellCommand(String command)
-//                             throws DeviceNotAvailableException {
-//                         return "-rw-r--r-- 1 root root 5020 2009-01-01 00:00 /system/build.prop";
-//                     }
-//                 };
-//         assertFalse(mTestDevice.isExecutable("/system/build.prop"));
-//     }
-//
-//     /**
-//      * Verifies that {@link INativeDevice#isExecutable(String)} recognizes a directory listing
-//      *
-//      * @throws Exception
-//      */
-//     @Test
-//     public void testIsDeviceFileExecutable_directory() throws Exception {
-//         mTestDevice =
-//                 new TestableAndroidNativeDevice() {
-//                     @Override
-//                     public String executeShellCommand(String command)
-//                             throws DeviceNotAvailableException {
-//                         return "total 416\n"
-//                                 + "drwxr-xr-x 74 root root    4096 2009-01-01 00:00 app\n"
-//                                 + "drwxr-xr-x  3 root shell   8192 2009-01-01 00:00 bin\n"
-//                                 + "-rw-r--r--  1 root root    5020 2009-01-01 00:00 build.prop\n"
-//                                 + "drwxr-xr-x 15 root root    4096 2009-01-01 00:00 etc\n"
-//                                 + "drwxr-xr-x  2 root root    4096 2009-01-01 00:00 fake-libs\n"
-//                                 + "drwxr-xr-x  2 root root    8192 2009-01-01 00:00 fonts\n"
-//                                 + "drwxr-xr-x  4 root root    4096 2009-01-01 00:00 framework\n"
-//                                 + "drwxr-xr-x  6 root root    8192 2009-01-01 00:00 lib\n"
-//                                 + "drwx------  2 root root    4096 1970-01-01 00:00 lost+found\n"
-//                                 + "drwxr-xr-x  3 root root    4096 2009-01-01 00:00 media\n"
-//                                 + "drwxr-xr-x 68 root root    4096 2009-01-01 00:00 priv-app\n"
-//                                 + "-rw-r--r--  1 root root  137093 2009-01-01 00:00 recovery-from-boot.p\n"
-//                                 + "drwxr-xr-x  9 root root    4096 2009-01-01 00:00 usr\n"
-//                                 + "drwxr-xr-x  8 root shell   4096 2009-01-01 00:00 vendor\n"
-//                                 + "drwxr-xr-x  2 root shell   4096 2009-01-01 00:00 xbin\n";
-//                     }
-//                 };
-//         assertFalse(mTestDevice.isExecutable("/system"));
-//     }
-// >>>>>>> f39a78ced... Hook up pullFile to use content provider.
+    // FIXME: delete this, not necessary
+    // <<<<<<< HEAD
+    // =======
+    //
+    //     /** Unit test for {@link INativeDevice#setProperty(String, String)}. */
+    //     @Test
+    //     public void testSetProperty() throws DeviceNotAvailableException {
+    //         OutputStream stdout = null, stderr = null;
+    //         mTestDevice =
+    //                 new TestableAndroidNativeDevice() {
+    //                     @Override
+    //                     public boolean isAdbRoot() throws DeviceNotAvailableException {
+    //                         return true;
+    //                     }
+    //                 };
+    //         CommandResult res = new CommandResult();
+    //         res.setStatus(CommandStatus.SUCCESS);
+    //         EasyMock.expect(
+    //                         mMockRunUtil.runTimedCmd(
+    //                                 120000, stdout, stderr, "adb", "-s", "serial", "shell", "setprop",
+    //                                 "test", "value"))
+    //                 .andReturn(res);
+    //         EasyMock.replay(mMockRunUtil, mMockIDevice);
+    //         assertTrue(mTestDevice.setProperty("test", "value"));
+    //         EasyMock.verify(mMockRunUtil, mMockIDevice);
+    //     }
+    //
+    //     /** Unit test for {@link INativeDevice#setProperty(String, String)}. */
+    //     @Test
+    //     public void testSetProperty_notRoot() throws DeviceNotAvailableException {
+    //         mTestDevice =
+    //                 new TestableAndroidNativeDevice() {
+    //                     @Override
+    //                     public boolean isAdbRoot() throws DeviceNotAvailableException {
+    //                         return false;
+    //                     }
+    //                 };
+    //         EasyMock.replay(mMockRunUtil, mMockIDevice);
+    //         assertFalse(mTestDevice.setProperty("test", "value"));
+    //         EasyMock.verify(mMockRunUtil, mMockIDevice);
+    //     }
+    //
+    //     /**
+    //      * Verifies that {@link INativeDevice#isExecutable(String)} recognizes regular executable file
+    //      *
+    //      * @throws Exception
+    //      */
+    //     @Test
+    //     public void testIsDeviceFileExecutable_executable_rwx() throws Exception {
+    //         mTestDevice =
+    //                 new TestableAndroidNativeDevice() {
+    //                     @Override
+    //                     public String executeShellCommand(String command)
+    //                             throws DeviceNotAvailableException {
+    //                         return "-rwxr-xr-x 1 root shell 42824 2009-01-01 00:00 /system/bin/ping";
+    //                     }
+    //                 };
+    //         assertTrue(mTestDevice.isExecutable("/system/bin/ping"));
+    //     }
+    //
+    //     /**
+    //      * Verifies that {@link INativeDevice#isExecutable(String)} recognizes symlink'd executable file
+    //      *
+    //      * @throws Exception
+    //      */
+    //     @Test
+    //     public void testIsDeviceFileExecutable_executable_lrwx() throws Exception {
+    //         mTestDevice =
+    //                 new TestableAndroidNativeDevice() {
+    //                     @Override
+    //                     public String executeShellCommand(String command)
+    //                             throws DeviceNotAvailableException {
+    //                         return "lrwxr-xr-x 1 root shell 7 2009-01-01 00:00 /system/bin/start -> toolbox";
+    //                     }
+    //                 };
+    //         assertTrue(mTestDevice.isExecutable("/system/bin/start"));
+    //     }
+    //
+    //     /**
+    //      * Verifies that {@link INativeDevice#isExecutable(String)} recognizes non-executable file
+    //      *
+    //      * @throws Exception
+    //      */
+    //     @Test
+    //     public void testIsDeviceFileExecutable_notExecutable() throws Exception {
+    //         mTestDevice =
+    //                 new TestableAndroidNativeDevice() {
+    //                     @Override
+    //                     public String executeShellCommand(String command)
+    //                             throws DeviceNotAvailableException {
+    //                         return "-rw-r--r-- 1 root root 5020 2009-01-01 00:00 /system/build.prop";
+    //                     }
+    //                 };
+    //         assertFalse(mTestDevice.isExecutable("/system/build.prop"));
+    //     }
+    //
+    //     /**
+    //      * Verifies that {@link INativeDevice#isExecutable(String)} recognizes a directory listing
+    //      *
+    //      * @throws Exception
+    //      */
+    //     @Test
+    //     public void testIsDeviceFileExecutable_directory() throws Exception {
+    //         mTestDevice =
+    //                 new TestableAndroidNativeDevice() {
+    //                     @Override
+    //                     public String executeShellCommand(String command)
+    //                             throws DeviceNotAvailableException {
+    //                         return "total 416\n"
+    //                                 + "drwxr-xr-x 74 root root    4096 2009-01-01 00:00 app\n"
+    //                                 + "drwxr-xr-x  3 root shell   8192 2009-01-01 00:00 bin\n"
+    //                                 + "-rw-r--r--  1 root root    5020 2009-01-01 00:00 build.prop\n"
+    //                                 + "drwxr-xr-x 15 root root    4096 2009-01-01 00:00 etc\n"
+    //                                 + "drwxr-xr-x  2 root root    4096 2009-01-01 00:00 fake-libs\n"
+    //                                 + "drwxr-xr-x  2 root root    8192 2009-01-01 00:00 fonts\n"
+    //                                 + "drwxr-xr-x  4 root root    4096 2009-01-01 00:00 framework\n"
+    //                                 + "drwxr-xr-x  6 root root    8192 2009-01-01 00:00 lib\n"
+    //                                 + "drwx------  2 root root    4096 1970-01-01 00:00 lost+found\n"
+    //                                 + "drwxr-xr-x  3 root root    4096 2009-01-01 00:00 media\n"
+    //                                 + "drwxr-xr-x 68 root root    4096 2009-01-01 00:00 priv-app\n"
+    //                                 + "-rw-r--r--  1 root root  137093 2009-01-01 00:00 recovery-from-boot.p\n"
+    //                                 + "drwxr-xr-x  9 root root    4096 2009-01-01 00:00 usr\n"
+    //                                 + "drwxr-xr-x  8 root shell   4096 2009-01-01 00:00 vendor\n"
+    //                                 + "drwxr-xr-x  2 root shell   4096 2009-01-01 00:00 xbin\n";
+    //                     }
+    //                 };
+    //         assertFalse(mTestDevice.isExecutable("/system"));
+    //     }
+    // >>>>>>> f39a78ced... Hook up pullFile to use content provider.
 }
diff --git a/tests/src/com/android/tradefed/device/TestDeviceTest.java b/tests/src/com/android/tradefed/device/TestDeviceTest.java
index 3164167..d82415e 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceTest.java
@@ -62,7 +62,6 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -3779,37 +3778,37 @@
         verifyMocks();
     }
 
-// FIXME: Delete this, not necessary
-// <<<<<<< HEAD
-// =======
-//
-//     /** Test that displays can be collected. */
-//     public void testListDisplayId() throws Exception {
-//         OutputStream stdout = null, stderr = null;
-//         CommandResult res = new CommandResult(CommandStatus.SUCCESS);
-//         res.setStdout("Display 0 color modes:\nDisplay 5 color modes:\n");
-//         EasyMock.expect(
-//                         mMockRunUtil.runTimedCmd(
-//                                 100L,
-//                                 stdout,
-//                                 stderr,
-//                                 "adb",
-//                                 "-s",
-//                                 "serial",
-//                                 "shell",
-//                                 "dumpsys",
-//                                 "SurfaceFlinger",
-//                                 "|",
-//                                 "grep",
-//                                 "'color",
-//                                 "modes:'"))
-//                 .andReturn(res);
-//         replayMocks();
-//         Set<Integer> displays = mTestDevice.listDisplayIds();
-//         assertEquals(2, displays.size());
-//         assertTrue(displays.contains(0));
-//         assertTrue(displays.contains(5));
-//         verifyMocks();
-//     }
-// >>>>>>> f39a78ced... Hook up pullFile to use content provider.
+    // FIXME: Delete this, not necessary
+    // <<<<<<< HEAD
+    // =======
+    //
+    //     /** Test that displays can be collected. */
+    //     public void testListDisplayId() throws Exception {
+    //         OutputStream stdout = null, stderr = null;
+    //         CommandResult res = new CommandResult(CommandStatus.SUCCESS);
+    //         res.setStdout("Display 0 color modes:\nDisplay 5 color modes:\n");
+    //         EasyMock.expect(
+    //                         mMockRunUtil.runTimedCmd(
+    //                                 100L,
+    //                                 stdout,
+    //                                 stderr,
+    //                                 "adb",
+    //                                 "-s",
+    //                                 "serial",
+    //                                 "shell",
+    //                                 "dumpsys",
+    //                                 "SurfaceFlinger",
+    //                                 "|",
+    //                                 "grep",
+    //                                 "'color",
+    //                                 "modes:'"))
+    //                 .andReturn(res);
+    //         replayMocks();
+    //         Set<Integer> displays = mTestDevice.listDisplayIds();
+    //         assertEquals(2, displays.size());
+    //         assertTrue(displays.contains(0));
+    //         assertTrue(displays.contains(5));
+    //         verifyMocks();
+    //     }
+    // >>>>>>> f39a78ced... Hook up pullFile to use content provider.
 }
diff --git a/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java b/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
index d03d979..f03922f 100644
--- a/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
+++ b/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
@@ -217,8 +217,100 @@
         }
     }
 
+    /** Test {@link ContentProviderHandler#pullDir(String, File)}. */
     @Test
-    public void testCreateUri() throws Exception {
+    public void testPullDir_EmptyDirectory() throws Exception {
+        File pullTo = FileUtil.createTempDir("content-provider-test");
+
+        doReturn("No result found.").when(mMockDevice).executeShellCommand(anyString());
+
+        try {
+            assertTrue(mProvider.pullDir("path/somewhere", pullTo));
+        } finally {
+            FileUtil.recursiveDelete(pullTo);
+        }
+    }
+
+    /**
+     * Test {@link ContentProviderHandler#pullDir(String, File)} to pull a directory that contains
+     * one text file.
+     */
+    @Test
+    public void testPullDir_OneFile() throws Exception {
+        File pullTo = FileUtil.createTempDir("content-provider-test");
+
+        String devicePath = "path/somewhere";
+        String fileName = "content-provider-file.txt";
+
+        doReturn(createMockFileRow(fileName, devicePath + "/" + fileName, "text/plain"))
+                .when(mMockDevice)
+                .executeShellCommand(anyString());
+        mockPullFileSuccess();
+
+        try {
+            // Assert that local directory is empty.
+            assertEquals(pullTo.listFiles().length, 0);
+            mProvider.pullDir(devicePath, pullTo);
+
+            // Assert that a file has been pulled inside the directory.
+            assertEquals(pullTo.listFiles().length, 1);
+            assertEquals(pullTo.listFiles()[0].getName(), fileName);
+        } finally {
+            FileUtil.recursiveDelete(pullTo);
+        }
+    }
+
+    /**
+     * Test {@link ContentProviderHandler#pullDir(String, File)} to pull a directory that contains
+     * another directory.
+     */
+    @Test
+    public void testPullDir_RecursiveSubDir() throws Exception {
+        File pullTo = FileUtil.createTempDir("content-provider-test");
+
+        String devicePath = "path/somewhere";
+        String subDirName = "test-subdir";
+        String subDirPath = devicePath + "/" + subDirName;
+        String fileName = "test-file.txt";
+
+        doReturn(99).when(mMockDevice).getCurrentUser();
+        // Mock the result for the directory.
+        doReturn(createMockDirRow(subDirName, subDirPath))
+                .when(mMockDevice)
+                .executeShellCommand(
+                        "content query --user 99 --uri "
+                                + ContentProviderHandler.createEscapedContentUri(devicePath));
+
+        // Mock the result for the subdir.
+        doReturn(createMockFileRow(fileName, subDirPath + "/" + fileName, "text/plain"))
+                .when(mMockDevice)
+                .executeShellCommand(
+                        "content query --user 99 --uri "
+                                + ContentProviderHandler.createEscapedContentUri(
+                                        devicePath + "/" + subDirName));
+
+        mockPullFileSuccess();
+
+        try {
+            // Assert that local directory is empty.
+            assertEquals(pullTo.listFiles().length, 0);
+            mProvider.pullDir(devicePath, pullTo);
+
+            // Assert that a subdirectory has been created.
+            assertEquals(pullTo.listFiles().length, 1);
+            assertEquals(pullTo.listFiles()[0].getName(), subDirName);
+            assertTrue(pullTo.listFiles()[0].isDirectory());
+
+            // Assert that a file has been pulled inside the subdirectory.
+            assertEquals(pullTo.listFiles()[0].listFiles().length, 1);
+            assertEquals(pullTo.listFiles()[0].listFiles()[0].getName(), fileName);
+        } finally {
+            FileUtil.recursiveDelete(pullTo);
+        }
+    }
+
+    @Test
+    public void testCreateUri() {
         String espacedUrl =
                 ContentProviderHandler.createEscapedContentUri("filepath/file name spaced (data)");
         // We expect the full url to be quoted to avoid space issues and the URL to be encoded.
@@ -259,4 +351,16 @@
                 .when(mMockDevice)
                 .executeShellV2Command(anyString(), any(OutputStream.class));
     }
+
+    private String createMockDirRow(String name, String path) {
+        return String.format(
+                "Row: 1 name=%s, absolute_path=%s, is_directory=%b, mime_type=NULL, metadata=NULL",
+                name, path, true);
+    }
+
+    private String createMockFileRow(String name, String path, String mimeType) {
+        return String.format(
+                "Row: 1 name=%s, absolute_path=%s, is_directory=%b, mime_type=%s, metadata=NULL",
+                name, path, false, mimeType);
+    }
 }
diff --git a/util-apps/ContentProvider/hostsidetests/src/com/android/tradefed/contentprovider/ContentProviderTest.java b/util-apps/ContentProvider/hostsidetests/src/com/android/tradefed/contentprovider/ContentProviderTest.java
index a20e713..0e6a655 100644
--- a/util-apps/ContentProvider/hostsidetests/src/com/android/tradefed/contentprovider/ContentProviderTest.java
+++ b/util-apps/ContentProvider/hostsidetests/src/com/android/tradefed/contentprovider/ContentProviderTest.java
@@ -52,13 +52,13 @@
 
     @After
     public void tearDown() throws Exception {
-        if (mHandler != null) {
-            mHandler.tearDown();
-        }
         for (String delete : mToBeDeleted) {
             getDevice().deleteFile(delete);
         }
         mToBeDeleted.clear();
+        if (mHandler != null) {
+            mHandler.tearDown();
+        }
     }
 
     /** Test pushing a file with special characters in the name. */
@@ -69,7 +69,7 @@
         try {
             boolean res = mHandler.pushFile(tmpFile, "/sdcard/" + tmpFile.getName());
             assertTrue(res);
-            assertTrue(getDevice().doesFileExist("/sdcard/" + tmpFile.getName()));
+            assertTrue(getDevice().doesFileExist(mCurrentUserStoragePath + tmpFile.getName()));
         } finally {
             FileUtil.deleteFile(tmpFile);
         }
@@ -114,6 +114,7 @@
         File tmpFile = FileUtil.createTempFile("tmpFileToPush", ".txt");
         FileUtil.writeToFile(fileContent, tmpFile);
         mHandler.pushFile(tmpFile, "/sdcard/" + tmpFile.getName());
+        mToBeDeleted.add("/sdcard/" + tmpFile.getName());
 
         File tmpPullFile = new File("fileToPullTo.txt");
         // Local file does not exist before we pull the content from the device.
@@ -129,4 +130,33 @@
             FileUtil.deleteFile(tmpPullFile);
         }
     }
+
+    @Test
+    public void testPullDir() throws Exception {
+        String fileContent = "some test content";
+        String dirName = "test_dir_path";
+        String subDirName = "test_subdir_path";
+        String subDirPath = dirName + "/" + subDirName;
+
+        // First, push the file onto a device to create nested structure= dirName->subDirName->file
+        File tmpFile = FileUtil.createTempFile("tmpFileToPush", ".txt");
+        FileUtil.writeToFile(fileContent, tmpFile);
+        mHandler.pushFile(tmpFile, "/sdcard/" + subDirPath + "/" + tmpFile.getName());
+        mToBeDeleted.add("/sdcard/" + dirName);
+
+        File tmpPullDir = FileUtil.createTempDir("dirToPullTo");
+
+        try {
+            boolean res = mHandler.pullDir("/sdcard/" + dirName, tmpPullDir);
+            assertTrue(res);
+            assertEquals(tmpPullDir.listFiles()[0].getName(), subDirName); // Verify subDir
+            assertEquals(tmpPullDir.listFiles()[0].listFiles().length, 1); // Verify subDir content.
+            assertEquals(
+                    FileUtil.readStringFromFile(tmpPullDir.listFiles()[0].listFiles()[0]),
+                    fileContent); // Verify content.
+        } finally {
+            FileUtil.recursiveDelete(tmpPullDir);
+            FileUtil.deleteFile(tmpFile);
+        }
+    }
 }