Merge "Snap for 5742267 from 562202cf1e9899b55c457947810c62f97f896b18 to pie-cts-release" into pie-cts-release
diff --git a/res/apks/contentprovider/MODULE_LICENSE_APL b/res/apks/contentprovider/MODULE_LICENSE_APL
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/res/apks/contentprovider/MODULE_LICENSE_APL
diff --git a/res/apks/contentprovider/PREBUILT b/res/apks/contentprovider/PREBUILT
new file mode 100644
index 0000000..84ae85c
--- /dev/null
+++ b/res/apks/contentprovider/PREBUILT
@@ -0,0 +1,4 @@
+This apk can be rebuilt from
+        platform/tools/tradefederation/core/util-apps/ContentProvider
+
+By running `m TradefedContentProvider` on revision b2b676a778864281572ec0c252623562be9c3c2b
diff --git a/res/apks/contentprovider/TradefedContentProvider.apk b/res/apks/contentprovider/TradefedContentProvider.apk
new file mode 100644
index 0000000..5bad779
--- /dev/null
+++ b/res/apks/contentprovider/TradefedContentProvider.apk
Binary files differ
diff --git a/src/com/android/tradefed/device/INativeDevice.java b/src/com/android/tradefed/device/INativeDevice.java
index ff4071b..6f624a3 100644
--- a/src/com/android/tradefed/device/INativeDevice.java
+++ b/src/com/android/tradefed/device/INativeDevice.java
@@ -36,6 +36,7 @@
 
 import java.io.File;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.Collection;
 import java.util.Date;
 import java.util.List;
@@ -276,6 +277,32 @@
     public CommandResult executeShellV2Command(String command) throws DeviceNotAvailableException;
 
     /**
+     * Helper method which executes an adb shell command and returns the results as a {@link
+     * CommandResult} properly populated with the command status output, stdout and stderr.
+     *
+     * @param command The command that should be run.
+     * @param pipeAsInput A {@link File} that will be piped as input to the command.
+     * @return The result in {@link CommandResult}.
+     * @throws DeviceNotAvailableException if connection with device is lost and cannot be
+     *     recovered.
+     */
+    public CommandResult executeShellV2Command(String command, File pipeAsInput)
+            throws DeviceNotAvailableException;
+
+    /**
+     * Helper method which executes an adb shell command and returns the results as a {@link
+     * CommandResult} properly populated with the command status output, stdout and stderr.
+     *
+     * @param command The command that should be run.
+     * @param pipeToOutput {@link OutputStream} where the std output will be redirected.
+     * @return The result in {@link CommandResult}.
+     * @throws DeviceNotAvailableException if connection with device is lost and cannot be
+     *     recovered.
+     */
+    public CommandResult executeShellV2Command(String command, OutputStream pipeToOutput)
+            throws DeviceNotAvailableException;
+
+    /**
      * Executes a adb shell command, with more parameters to control command behavior.
      *
      * @see #executeShellV2Command(String)
@@ -591,6 +618,14 @@
     public boolean doesFileExist(String deviceFilePath) throws DeviceNotAvailableException;
 
     /**
+     * Helper method to delete a file or directory on the device.
+     *
+     * @param deviceFilePath The absolute path of the file on the device.
+     * @throws DeviceNotAvailableException
+     */
+    public void deleteFile(String deviceFilePath) throws DeviceNotAvailableException;
+
+    /**
      * Retrieve a reference to a remote file on device.
      *
      * @param path the file path to retrieve. Can be an absolute path or path relative to '/'. (ie
diff --git a/src/com/android/tradefed/device/NativeDevice.java b/src/com/android/tradefed/device/NativeDevice.java
index cfbf3a1..d5bc048 100644
--- a/src/com/android/tradefed/device/NativeDevice.java
+++ b/src/com/android/tradefed/device/NativeDevice.java
@@ -35,6 +35,7 @@
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.command.remote.DeviceDescriptor;
 import com.android.tradefed.config.GlobalConfiguration;
+import com.android.tradefed.device.contentprovider.ContentProviderHandler;
 import com.android.tradefed.host.IHostOptions;
 import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -70,6 +71,7 @@
 import java.io.File;
 import java.io.FilenameFilter;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.time.Clock;
@@ -96,6 +98,7 @@
  */
 public class NativeDevice implements IManagedTestDevice {
 
+    protected static final String SD_CARD = "/sdcard/";
     /**
      * Allow pauses of up to 2 minutes while receiving bugreport.
      * <p/>
@@ -205,6 +208,9 @@
     private String mLastConnectedWifiPsk = null;
     private boolean mNetworkMonitorEnabled = false;
 
+    private ContentProviderHandler mContentProvider = null;
+    private boolean mShouldSkipContentProviderSetup = false;
+
     /**
      * Interface for a generic device communication attempt.
      */
@@ -258,15 +264,24 @@
 
         private String[] mCmd;
         private long mTimeout;
+        private File mPipeAsInput; // Used in pushFile, uses local file as input to "content write"
+        private OutputStream mPipeToOutput; // Used in pullFile, to pipe content from "content read"
 
-        AdbShellAction(String[] cmd, long timeout) {
+        AdbShellAction(String[] cmd, File pipeAsInput, OutputStream pipeToOutput, long timeout) {
             mCmd = cmd;
+            mPipeAsInput = pipeAsInput;
+            mPipeToOutput = pipeToOutput;
             mTimeout = timeout;
         }
 
         @Override
         public boolean run() throws TimeoutException, IOException {
-            mResult = getRunUtil().runTimedCmd(mTimeout, mCmd);
+            if (mPipeAsInput != null) {
+                mResult = getRunUtil().runTimedCmdWithInputRedirect(mTimeout, mPipeAsInput, mCmd);
+            } else {
+                mResult =
+                        getRunUtil().runTimedCmd(mTimeout, mPipeToOutput, /* stderr= */ null, mCmd);
+            }
             if (mResult.getStatus() == CommandStatus.EXCEPTION) {
                 throw new IOException(mResult.getStderr());
             } else if (mResult.getStatus() == CommandStatus.TIMED_OUT) {
@@ -685,10 +700,37 @@
 
     /** {@inheritDoc} */
     @Override
+    public CommandResult executeShellV2Command(String cmd, File pipeAsInput)
+            throws DeviceNotAvailableException {
+        return executeShellV2Command(
+                cmd,
+                pipeAsInput,
+                null,
+                getCommandTimeout(),
+                TimeUnit.MILLISECONDS,
+                MAX_RETRY_ATTEMPTS);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public CommandResult executeShellV2Command(String cmd, OutputStream pipeToOutput)
+            throws DeviceNotAvailableException {
+        return executeShellV2Command(
+                cmd,
+                null,
+                pipeToOutput,
+                getCommandTimeout(),
+                TimeUnit.MILLISECONDS,
+                MAX_RETRY_ATTEMPTS);
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public CommandResult executeShellV2Command(
             String cmd, final long maxTimeoutForCommand, final TimeUnit timeUnit)
             throws DeviceNotAvailableException {
-        return executeShellV2Command(cmd, maxTimeoutForCommand, timeUnit, MAX_RETRY_ATTEMPTS);
+        return executeShellV2Command(
+                cmd, null, null, maxTimeoutForCommand, timeUnit, MAX_RETRY_ATTEMPTS);
     }
 
     /** {@inheritDoc} */
@@ -696,9 +738,25 @@
     public CommandResult executeShellV2Command(
             String cmd, final long maxTimeoutForCommand, final TimeUnit timeUnit, int retryAttempts)
             throws DeviceNotAvailableException {
+        return executeShellV2Command(
+                cmd, null, null, maxTimeoutForCommand, timeUnit, retryAttempts);
+    }
+
+    private CommandResult executeShellV2Command(
+            String cmd,
+            File pipeAsInput,
+            OutputStream pipeToOutput,
+            final long maxTimeoutForCommand,
+            final TimeUnit timeUnit,
+            int retryAttempts)
+            throws DeviceNotAvailableException {
         final String[] fullCmd = buildAdbShellCommand(cmd);
         AdbShellAction adbActionV2 =
-                new AdbShellAction(fullCmd, timeUnit.toMillis(maxTimeoutForCommand));
+                new AdbShellAction(
+                        fullCmd,
+                        pipeAsInput,
+                        pipeToOutput,
+                        timeUnit.toMillis(maxTimeoutForCommand));
         performDeviceAction(String.format("adb %s", fullCmd[4]), adbActionV2, retryAttempts);
         return adbActionV2.mResult;
     }
@@ -893,6 +951,13 @@
     public boolean pullFile(final String remoteFilePath, final File localFile)
             throws DeviceNotAvailableException {
 
+        if (remoteFilePath.startsWith(SD_CARD)) {
+            ContentProviderHandler handler = getContentProvider();
+            if (handler != null) {
+                return handler.pullFile(remoteFilePath, localFile);
+            }
+        }
+
         DeviceAction pullAction = new DeviceAction() {
             @Override
             public boolean run() throws TimeoutException, IOException, AdbCommandRejectedException,
@@ -994,6 +1059,13 @@
     @Override
     public boolean pushFile(final File localFile, final String remoteFilePath)
             throws DeviceNotAvailableException {
+        if (remoteFilePath.startsWith(SD_CARD)) {
+            ContentProviderHandler handler = getContentProvider();
+            if (handler != null) {
+                return handler.pushFile(localFile, remoteFilePath);
+            }
+        }
+
         DeviceAction pushAction =
                 new DeviceAction() {
                     @Override
@@ -1059,15 +1131,28 @@
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
-    public boolean doesFileExist(String destPath) throws DeviceNotAvailableException {
-        String lsGrep = executeShellCommand(String.format("ls \"%s\"", destPath));
+    public boolean doesFileExist(String deviceFilePath) throws DeviceNotAvailableException {
+        String lsGrep = executeShellCommand(String.format("ls \"%s\"", deviceFilePath));
         return !lsGrep.contains("No such file or directory");
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void deleteFile(String deviceFilePath) throws DeviceNotAvailableException {
+        if (deviceFilePath.startsWith(SD_CARD)) {
+            ContentProviderHandler handler = getContentProvider();
+            if (handler != null) {
+                if (handler.deleteFile(deviceFilePath)) {
+                    return;
+                }
+            }
+        }
+        // Fallback to the direct command if content provider is unsuccessful
+        executeShellCommand(String.format("rm -rf \"%s\"", deviceFilePath));
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -1361,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;
@@ -1770,7 +1862,11 @@
     /** Builds the OS command for the given adb shell command session and args */
     private String[] buildAdbShellCommand(String command) {
         // TODO: implement the shell v2 support in ddmlib itself.
-        String[] commandArgs = QuotationAwareTokenizer.tokenizeLine(command);
+        String[] commandArgs =
+                QuotationAwareTokenizer.tokenizeLine(
+                        command,
+                        /** No logging */
+                        false);
         return ArrayUtil.buildArray(
                 new String[] {"adb", "-s", getSerialNumber(), "shell"}, commandArgs);
     }
@@ -2266,7 +2362,7 @@
                             remoteFilePath.substring(0, remoteFilePath.lastIndexOf('/'));
                     if (!bugreportDir.isEmpty()) {
                         // clean bugreport files directory on device
-                        executeShellCommand(String.format("rm %s/*", bugreportDir));
+                        deleteFile(String.format("%s/*", bugreportDir));
                     }
 
                     return zipFile;
@@ -2634,7 +2730,8 @@
         final String wifiSsid = mLastConnectedWifiSsid;
         if (!connectToWifiNetworkIfNeeded(mLastConnectedWifiSsid, mLastConnectedWifiPsk)) {
             throw new NetworkNotAvailableException(
-                    String.format("Failed to connect to wifi network %s on %s after reboot",
+                    String.format(
+                            "Failed to connect to wifi network %s on %s after reboot",
                             wifiSsid, getSerialNumber()));
         }
     }
@@ -2649,6 +2746,8 @@
             throw new UnsupportedOperationException(
                     "Fastboot is not available and cannot reboot into bootloader");
         }
+        // If we go to bootloader, it's probably for flashing so ensure we re-check the provider
+        mShouldSkipContentProviderSetup = false;
         CLog.i("Rebooting device %s in state %s into bootloader", getSerialNumber(),
                 getDeviceState());
         if (TestDeviceState.FASTBOOT.equals(getDeviceState())) {
@@ -3089,7 +3188,7 @@
             CLog.w("Property ro.crypto.state is null on device %s", getSerialNumber());
         }
 
-        return "encrypted".equals(output);
+        return "encrypted".equals(output.trim());
     }
 
     /**
@@ -3106,11 +3205,13 @@
             return mIsEncryptionSupported.booleanValue();
         }
         enableAdbRoot();
-        String output = executeShellCommand("vdc cryptfs enablecrypto").trim();
 
-        mIsEncryptionSupported =
-                (output != null
-                        && Pattern.matches("(500)(\\s+)(\\d+)(\\s+)(Usage)(.*)(:)(.*)", output));
+        String output = getProperty("ro.crypto.state");
+        if (output == null || "unsupported".equals(output.trim())) {
+            mIsEncryptionSupported = false;
+            return mIsEncryptionSupported;
+        }
+        mIsEncryptionSupported = true;
         return mIsEncryptionSupported;
     }
 
@@ -3837,15 +3938,28 @@
     @Override
     public void preInvocationSetup(IBuildInfo info)
             throws TargetSetupError, DeviceNotAvailableException {
-        // Default implementation empty on purpose
+        // Default implementation
+        mContentProvider = null;
+        mShouldSkipContentProviderSetup = false;
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
     public void postInvocationTearDown() {
-        // Default implementation empty on purpose
+        mIsEncryptionSupported = null;
+        // Default implementation
+        if (getIDevice() instanceof StubDevice) {
+            return;
+        }
+        try {
+            // If we never installed it, don't even bother checking for it during tear down.
+            if (mContentProvider == null) {
+                return;
+            }
+            mContentProvider.tearDown();
+        } catch (DeviceNotAvailableException e) {
+            CLog.e(e);
+        }
     }
 
     /**
@@ -4079,4 +4193,30 @@
     IHostOptions getHostOptions() {
         return GlobalConfiguration.getInstance().getHostOptions();
     }
+
+    /** Returns the {@link ContentProviderHandler} or null if not available. */
+    @VisibleForTesting
+    ContentProviderHandler getContentProvider() throws DeviceNotAvailableException {
+        // If disabled at the device level, don't attempt any checks.
+        if (!getOptions().shouldUseContentProvider()) {
+            return null;
+        }
+        // Prevent usage of content provider before API 28 as it would not work well since content
+        // tool is not working before P.
+        if (getApiLevel() < 28) {
+            return null;
+        }
+        if (mContentProvider == null) {
+            mContentProvider = new ContentProviderHandler(this);
+        }
+        if (!mShouldSkipContentProviderSetup) {
+            boolean res = mContentProvider.setUp();
+            if (!res) {
+                // TODO: once CP becomes a requirement, throw/fail the test if CP can't be found
+                return null;
+            }
+            mShouldSkipContentProviderSetup = true;
+        }
+        return mContentProvider;
+    }
 }
diff --git a/src/com/android/tradefed/device/TestDevice.java b/src/com/android/tradefed/device/TestDevice.java
index bc40082..f845b5a 100644
--- a/src/com/android/tradefed/device/TestDevice.java
+++ b/src/com/android/tradefed/device/TestDevice.java
@@ -763,6 +763,17 @@
         return packages;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public boolean doesFileExist(String deviceFilePath) throws DeviceNotAvailableException {
+        if (deviceFilePath.startsWith(SD_CARD)) {
+            deviceFilePath =
+                    deviceFilePath.replaceFirst(
+                            SD_CARD, String.format("/storage/emulated/%s/", getCurrentUser()));
+        }
+        return super.doesFileExist(deviceFilePath);
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -1089,9 +1100,9 @@
                 // disable keyguard if option is true
                 prePostBootSetup();
                 return true;
-            } else {
-                RunUtil.getDefault().sleep(getCheckNewUserSleep());
             }
+            RunUtil.getDefault().sleep(getCheckNewUserSleep());
+            executeShellCommand(String.format("am switch-user %d", userId));
         }
         CLog.e("User did not switch in the given %d timeout", timeout);
         return false;
@@ -1387,7 +1398,7 @@
         }
         File dump = dumpAndPullHeap(pid, devicePath);
         // Clean the device.
-        executeShellCommand(String.format("rm %s", devicePath));
+        deleteFile(devicePath);
         return dump;
     }
 
diff --git a/src/com/android/tradefed/device/TestDeviceOptions.java b/src/com/android/tradefed/device/TestDeviceOptions.java
index e4d9569..0f33b2e 100644
--- a/src/com/android/tradefed/device/TestDeviceOptions.java
+++ b/src/com/android/tradefed/device/TestDeviceOptions.java
@@ -115,6 +115,14 @@
             "the minimum battery level required to continue the invocation. Scale: 0-100")
     private Integer mCutoffBattery = null;
 
+    @Option(
+        name = "use-content-provider",
+        description =
+                "Allow to disable the use of the content provider at the device level. "
+                        + "This results in falling back to standard adb push/pull."
+    )
+    private boolean mUseContentProvider = true;
+
     /**
      * Check whether adb root should be enabled on boot for this device
      */
@@ -340,4 +348,9 @@
     public String getWifiUtilAPKPath() {
         return mWifiUtilAPKPath;
     }
+
+    /** Returns whether or not the Tradefed content provider can be used to push/pull files. */
+    public boolean shouldUseContentProvider() {
+        return mUseContentProvider;
+    }
 }
diff --git a/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java b/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
new file mode 100644
index 0000000..a250987
--- /dev/null
+++ b/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2019 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.tradefed.device.contentprovider;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.StreamUtil;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.net.UrlEscapers;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.Set;
+import java.util.StringJoiner;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Handler that abstract the content provider interactions and allow to use the device side content
+ * provider for different operations.
+ *
+ * <p>All implementation in this class should be mindful of the user currently running on the
+ * device.
+ */
+public class ContentProviderHandler {
+    public static final String COLUMN_NAME = "name";
+    public static final String COLUMN_ABSOLUTE_PATH = "absolute_path";
+    public static final String COLUMN_DIRECTORY = "is_directory";
+    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 =
+            new String[] {
+                COLUMN_NAME,
+                COLUMN_ABSOLUTE_PATH,
+                COLUMN_DIRECTORY,
+                COLUMN_MIME_TYPE,
+                COLUMN_METADATA
+            };
+
+    public static final String PACKAGE_NAME = "android.tradefed.contentprovider";
+    public static final String CONTENT_PROVIDER_URI = "content://android.tradefed.contentprovider";
+    private static final String APK_NAME = "TradefedContentProvider.apk";
+    private static final String CONTENT_PROVIDER_APK_RES = "/apks/contentprovider/" + APK_NAME;
+    private static final String ERROR_MESSAGE_TAG = "[ERROR]";
+
+    private ITestDevice mDevice;
+    private File mContentProviderApk = null;
+
+    /** Constructor. */
+    public ContentProviderHandler(ITestDevice device) {
+        mDevice = device;
+    }
+
+    /**
+     * Ensure the content provider helper apk is installed and ready to be used.
+     *
+     * @return True if ready to be used, False otherwise.
+     */
+    public boolean setUp() throws DeviceNotAvailableException {
+        Set<String> packageNames = mDevice.getInstalledPackageNames();
+        if (packageNames.contains(PACKAGE_NAME)) {
+            return true;
+        }
+        if (mContentProviderApk == null) {
+            try {
+                mContentProviderApk = extractResourceApk();
+            } catch (IOException e) {
+                CLog.e(e);
+                return false;
+            }
+        }
+        // Install package for all users
+        String output =
+                mDevice.installPackage(
+                        mContentProviderApk,
+                        /** reinstall */
+                        true,
+                        /** grant permission */
+                        true);
+        if (output != null) {
+            CLog.e("Something went wrong while installing the content provider apk: %s", output);
+            FileUtil.deleteFile(mContentProviderApk);
+            return false;
+        }
+        return true;
+    }
+
+    /** Clean the device from the content provider helper. */
+    public void tearDown() throws DeviceNotAvailableException {
+        FileUtil.deleteFile(mContentProviderApk);
+        mDevice.uninstallPackage(PACKAGE_NAME);
+    }
+
+    /**
+     * Content provider callback that delete a file at the URI location. File will be deleted from
+     * the device content.
+     *
+     * @param deviceFilePath The path on the device of the file to delete.
+     * @return True if successful, False otherwise
+     * @throws DeviceNotAvailableException
+     */
+    public boolean deleteFile(String deviceFilePath) throws DeviceNotAvailableException {
+        String contentUri = createEscapedContentUri(deviceFilePath);
+        String deleteCommand =
+                String.format(
+                        "content delete --user %d --uri %s", mDevice.getCurrentUser(), contentUri);
+        CommandResult deleteResult = mDevice.executeShellV2Command(deleteCommand);
+
+        if (isSuccessful(deleteResult)) {
+            return true;
+        }
+        CLog.e(
+                "Failed to remove a file at %s using content provider. Error: '%s'",
+                deviceFilePath, deleteResult.getStderr());
+        return false;
+    }
+
+    /**
+     * 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 {
+        return pullDirInternal(deviceFilePath, localDir, /* currentUser */ null);
+    }
+
+    /**
+     * 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.
+     * @param localFile The {@link File} to store the contents in. If non-empty, contents will be
+     *     replaced.
+     * @return True if successful, False otherwise
+     * @throws DeviceNotAvailableException
+     */
+    public boolean pullFile(String deviceFilePath, File localFile)
+            throws DeviceNotAvailableException {
+        return pullFileInternal(deviceFilePath, localFile, /* currentUser */ null);
+    }
+
+    /**
+     * Content provider callback that push a file to the URI location.
+     *
+     * @param fileToPush The {@link File} to be pushed to the device.
+     * @param deviceFilePath The path on the device where to push the file.
+     * @return True if successful, False otherwise
+     * @throws DeviceNotAvailableException
+     * @throws IllegalArgumentException
+     */
+    public boolean pushFile(File fileToPush, String deviceFilePath)
+            throws DeviceNotAvailableException, IllegalArgumentException {
+        if (fileToPush.isDirectory()) {
+            throw new IllegalArgumentException(
+                    String.format("File '%s' to push is a directory.", fileToPush));
+        }
+        if (!fileToPush.exists()) {
+            throw new IllegalArgumentException(
+                    String.format("File '%s' to push does not exist.", fileToPush));
+        }
+        String contentUri = createEscapedContentUri(deviceFilePath);
+        String pushCommand =
+                String.format(
+                        "content write --user %d --uri %s", mDevice.getCurrentUser(), contentUri);
+        CommandResult pushResult = mDevice.executeShellV2Command(pushCommand, fileToPush);
+
+        if (isSuccessful(pushResult)) {
+            return true;
+        }
+
+        CLog.e(
+                "Failed to push a file '%s' at %s using content provider. Error: '%s'",
+                fileToPush, deviceFilePath, pushResult.getStderr());
+        return false;
+    }
+
+    /** Returns true if {@link CommandStatus} is successful and there is no error message. */
+    private boolean isSuccessful(CommandResult result) {
+        if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
+            return false;
+        }
+        String stdout = result.getStdout();
+        if (stdout.contains(ERROR_MESSAGE_TAG)) {
+            return false;
+        }
+        String stderr = result.getStderr();
+        return Strings.isNullOrEmpty(stderr);
+    }
+
+    /** Helper method to extract the content provider apk. */
+    private File extractResourceApk() throws IOException {
+        File apkTempFile = FileUtil.createTempFile(APK_NAME, ".apk");
+        InputStream apkStream =
+                ContentProviderHandler.class.getResourceAsStream(CONTENT_PROVIDER_APK_RES);
+        FileUtil.writeToFile(apkStream, apkTempFile);
+        return apkTempFile;
+    }
+
+    /**
+     * Returns the full URI string for the given device path, escaped and encoded to avoid non-URL
+     * characters.
+     */
+    public static String createEscapedContentUri(String deviceFilePath) {
+        String escapedFilePath = deviceFilePath;
+        try {
+            // Escape the path then encode it.
+            String escaped = UrlEscapers.urlPathSegmentEscaper().escape(deviceFilePath);
+            escapedFilePath = URLEncoder.encode(escaped, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            CLog.e(e);
+        }
+        return String.format("\"%s/%s\"", CONTENT_PROVIDER_URI, escapedFilePath);
+    }
+
+    /**
+     * Parses the String output of "adb shell content query" for a single row.
+     *
+     * @param row The entire row representing a single file/directory returned by the "adb shell
+     *     content query" command.
+     * @return Key-value map of column name to column value.
+     */
+    @VisibleForTesting
+    final HashMap<String, String> parseQueryResultRow(String row) {
+        HashMap<String, String> columnValues = new HashMap<>();
+
+        StringJoiner pattern = new StringJoiner(", ");
+        for (int i = 0; i < COLUMNS.length; i++) {
+            pattern.add(String.format("(%s=.*)", COLUMNS[i]));
+        }
+
+        Pattern p = Pattern.compile(pattern.toString());
+        Matcher m = p.matcher(row);
+        if (m.find()) {
+            for (int i = 1; i <= m.groupCount(); i++) {
+                String[] keyValue = m.group(i).split("=");
+                if (keyValue.length == 2) {
+                    columnValues.put(keyValue[0], keyValue[1]);
+                }
+            }
+        }
+        return columnValues;
+    }
+
+    /**
+     * Internal method to actually do the pull directory but without re-querying the current user
+     * while doing the recursive pull.
+     */
+    private boolean pullDirInternal(String deviceFilePath, File localDir, Integer currentUser)
+            throws DeviceNotAvailableException {
+        if (!localDir.isDirectory()) {
+            CLog.e("Local path %s is not a directory", localDir.getAbsolutePath());
+            return false;
+        }
+
+        String contentUri = createEscapedContentUri(deviceFilePath);
+        if (currentUser == null) {
+            // Keep track of the user so if we recursively pull dir we don't re-query it.
+            currentUser = mDevice.getCurrentUser();
+        }
+        String queryContentCommand =
+                String.format("content query --user %d --uri %s", currentUser, contentUri);
+
+        String listCommandResult = mDevice.executeShellCommand(queryContentCommand);
+
+        if (NO_RESULTS_STRING.equals(listCommandResult.trim())) {
+            // 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 (!pullDirInternal(path, localChild, currentUser)) {
+                    CLog.w("Failed to pull sub directory %s from device, aborting", path);
+                    return false;
+                }
+            } else {
+                // handle regular file
+                if (!pullFileInternal(path, localChild, currentUser)) {
+                    CLog.w("Failed to pull file %s from device, aborting", path);
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    private boolean pullFileInternal(String deviceFilePath, File localFile, Integer currentUser)
+            throws DeviceNotAvailableException {
+        String contentUri = createEscapedContentUri(deviceFilePath);
+        if (currentUser == null) {
+            currentUser = mDevice.getCurrentUser();
+        }
+        String pullCommand =
+                String.format("content read --user %d --uri %s", currentUser, contentUri);
+
+        // Open the output stream to the local file.
+        OutputStream localFileStream;
+        try {
+            localFileStream = new FileOutputStream(localFile);
+        } catch (FileNotFoundException e) {
+            CLog.e("Failed to open OutputStream to the local file. Error: %s", e.getMessage());
+            return false;
+        }
+
+        try {
+            CommandResult pullResult = mDevice.executeShellV2Command(pullCommand, localFileStream);
+            if (isSuccessful(pullResult)) {
+                return true;
+            }
+
+            CLog.e(
+                    "Failed to pull a file at '%s' to %s using content provider. Error: '%s'",
+                    deviceFilePath, localFile, pullResult.getStderr());
+            return false;
+        } finally {
+            StreamUtil.close(localFileStream);
+        }
+    }
+}
diff --git a/src/com/android/tradefed/device/metric/AtraceCollector.java b/src/com/android/tradefed/device/metric/AtraceCollector.java
index 3b29f57..0199e4d 100644
--- a/src/com/android/tradefed/device/metric/AtraceCollector.java
+++ b/src/com/android/tradefed/device/metric/AtraceCollector.java
@@ -219,7 +219,7 @@
                 }
 
                 if (!mPreserveOndeviceLog) {
-                    device.executeShellCommand("rm -f " + fullLogPath());
+                    device.deleteFile(fullLogPath());
                 }
                 else {
                     CLog.w("preserving ondevice atrace log: %s", fullLogPath());
diff --git a/src/com/android/tradefed/targetprep/DeviceSetup.java b/src/com/android/tradefed/targetprep/DeviceSetup.java
index fd59eaa..880d2fd 100644
--- a/src/com/android/tradefed/targetprep/DeviceSetup.java
+++ b/src/com/android/tradefed/targetprep/DeviceSetup.java
@@ -483,7 +483,7 @@
             if (mPreviousProperties != null) {
                 device.pushFile(mPreviousProperties, "/data/local.prop");
             } else {
-                device.executeShellCommand("rm -f /data/local.prop");
+                device.deleteFile("/data/local.prop");
             }
             device.reboot();
         }
diff --git a/src/com/android/tradefed/targetprep/PushFilePreparer.java b/src/com/android/tradefed/targetprep/PushFilePreparer.java
index 81b684d..4732332 100644
--- a/src/com/android/tradefed/targetprep/PushFilePreparer.java
+++ b/src/com/android/tradefed/targetprep/PushFilePreparer.java
@@ -189,7 +189,7 @@
                 device.remountSystemWritable();
             }
             for (String devicePath : mFilesPushed) {
-                device.executeShellCommand("rm -r " + devicePath);
+                device.deleteFile(devicePath);
             }
         }
     }
diff --git a/src/com/android/tradefed/testtype/AndroidJUnitTest.java b/src/com/android/tradefed/testtype/AndroidJUnitTest.java
index 8e8ce81..8eb5cf0 100644
--- a/src/com/android/tradefed/testtype/AndroidJUnitTest.java
+++ b/src/com/android/tradefed/testtype/AndroidJUnitTest.java
@@ -240,16 +240,21 @@
         if (getDevice() == null) {
             throw new IllegalArgumentException("Device has not been set");
         }
+        boolean pushedFile = false;
         // if mIncludeTestFile is set, perform filtering with this file
-        if (mIncludeTestFile != null) {
+        if (mIncludeTestFile != null && mIncludeTestFile.length() > 0) {
             mDeviceIncludeFile = mTestFilterDir.replaceAll("/$", "") + "/" + INCLUDE_FILE;
             pushTestFile(mIncludeTestFile, mDeviceIncludeFile, listener);
+            pushedFile = true;
+            // If an explicit include file filter is provided, do not use the package
+            setTestPackageName(null);
         }
 
         // if mExcludeTestFile is set, perform filtering with this file
         if (mExcludeTestFile != null) {
             mDeviceExcludeFile = mTestFilterDir.replaceAll("/$", "") + "/" + EXCLUDE_FILE;
             pushTestFile(mExcludeTestFile, mDeviceExcludeFile, listener);
+            pushedFile = true;
         }
         if (mTotalShards > 0 && !isShardable() && mShardIndex != 0) {
             // If not shardable, only first shard can run.
@@ -257,11 +262,9 @@
             return;
         }
         super.run(listener);
-        if (mIncludeTestFile != null) {
-            removeTestFile(mDeviceIncludeFile);
-        }
-        if (mExcludeTestFile != null) {
-            removeTestFile(mDeviceExcludeFile);
+        if (pushedFile) {
+            // Remove the directory where the files where pushed
+            removeTestFilterDir();
         }
     }
 
@@ -354,6 +357,7 @@
         }
         ITestDevice device = getDevice();
         try {
+            CLog.d("Attempting to push filters to %s", destination);
             if (!device.pushFile(testFile, destination)) {
                 String message =
                         String.format(
@@ -370,9 +374,8 @@
         }
     }
 
-    private void removeTestFile(String deviceTestFile) throws DeviceNotAvailableException {
-        ITestDevice device = getDevice();
-        device.executeShellCommand(String.format("rm %s", deviceTestFile));
+    private void removeTestFilterDir() throws DeviceNotAvailableException {
+        getDevice().deleteFile(mTestFilterDir);
     }
 
     private void reportEarlyFailure(ITestInvocationListener listener, String errorMessage) {
diff --git a/src/com/android/tradefed/testtype/GTest.java b/src/com/android/tradefed/testtype/GTest.java
index aa23040..047e879 100644
--- a/src/com/android/tradefed/testtype/GTest.java
+++ b/src/com/android/tradefed/testtype/GTest.java
@@ -525,7 +525,7 @@
         testDevice.executeShellCommand(String.format("sh %s", tmpFileDevice),
                 resultParser, mMaxTestTimeMs /* maxTimeToShellOutputResponse */,
                 TimeUnit.MILLISECONDS, 0 /* retry attempts */);
-        testDevice.executeShellCommand(String.format("rm %s", tmpFileDevice));
+        testDevice.deleteFile(tmpFileDevice);
     }
 
     /**
@@ -605,7 +605,7 @@
             // Pull the result file, may not exist if issue with the test.
             testDevice.pullFile(tmpResName, tmpOutput);
             // Clean the file on the device
-            testDevice.executeShellCommand("rm " + tmpResName);
+            testDevice.deleteFile(tmpResName);
             GTestXmlResultParser parser = createXmlParser(testRunName, listener);
             // Attempt to parse the file, doesn't matter if the content is invalid.
             if (tmpOutput.exists()) {
diff --git a/src/com/android/tradefed/util/QuotationAwareTokenizer.java b/src/com/android/tradefed/util/QuotationAwareTokenizer.java
index e72afa7..eaf7377 100644
--- a/src/com/android/tradefed/util/QuotationAwareTokenizer.java
+++ b/src/com/android/tradefed/util/QuotationAwareTokenizer.java
@@ -25,34 +25,40 @@
     private static final String LOG_TAG = "TOKEN";
 
     /**
-     * Tokenizes the string, splitting on specified delimiter.  Does not split between consecutive,
+     * Tokenizes the string, splitting on specified delimiter. Does not split between consecutive,
      * unquoted double-quote marks.
-     * <p/>
-     * How the tokenizer works:
+     *
+     * <p>How the tokenizer works:
+     *
      * <ol>
-     *     <li> Split the string into "characters" where each "character" is either an escaped
-     *          character like \" (that is, "\\\"") or a single real character like f (just "f").
-     *     <li> For each "character"
-     *     <ol>
+     *   <li> Split the string into "characters" where each "character" is either an escaped
+     *       character like \" (that is, "\\\"") or a single real character like f (just "f").
+     *   <li> For each "character"
+     *       <ol>
      *         <li> If it's a space, finish a token unless we're being quoted
      *         <li> If it's a quotation mark, flip the "we're being quoted" bit
      *         <li> Otherwise, add it to the token being built
-     *     </ol>
-     *     <li> At EOL, we typically haven't added the final token to the (tokens) {@link ArrayList}
-     *     <ol>
+     *       </ol>
+     *
+     *   <li> At EOL, we typically haven't added the final token to the (tokens) {@link ArrayList}
+     *       <ol>
      *         <li> If the last "character" is an escape character, throw an exception; that's not
-     *              valid
+     *             valid
      *         <li> If we're in the middle of a quotation, throw an exception; that's not valid
      *         <li> Otherwise, add the final token to (tokens)
-     *     </ol>
-     *     <li> Return a String[] version of (tokens)
+     *       </ol>
+     *
+     *   <li> Return a String[] version of (tokens)
      * </ol>
      *
      * @param line A {@link String} to be tokenized
+     * @param delim the delimiter to split on
+     * @param logging whether or not to log operations
      * @return A tokenized version of the string
      * @throws IllegalArgumentException if the line cannot be parsed
      */
-    public static String[] tokenizeLine(String line, String delim) throws IllegalArgumentException {
+    public static String[] tokenizeLine(String line, String delim, boolean logging)
+            throws IllegalArgumentException {
         if (line == null) {
             throw new IllegalArgumentException("line is null");
         }
@@ -65,7 +71,7 @@
         String aChar = "";
         boolean quotation = false;
 
-        Log.d(LOG_TAG, String.format("Trying to tokenize the line '%s'", line));
+        log(String.format("Trying to tokenize the line '%s'", line), logging);
         while (charMatcher.find()) {
             aChar = charMatcher.group();
 
@@ -77,7 +83,7 @@
                     if (token.length() > 0) {
                         // this is the end of a non-empty token; dump it in our list of tokens,
                         // clear our temp storage, and keep rolling
-                        Log.d(LOG_TAG, String.format("Finished token '%s'", token.toString()));
+                        log(String.format("Finished token '%s'", token.toString()), logging);
                         tokens.add(token.toString());
                         token.delete(0, token.length());
                     }
@@ -85,7 +91,7 @@
                 }
             } else if ("\"".equals(aChar)) {
                 // unescaped quotation mark; flip quotation state
-                Log.v(LOG_TAG, "Flipped quotation state");
+                log("Flipped quotation state", logging);
                 quotation ^= true;
             } else {
                 // default case: add the character to the token being built
@@ -101,7 +107,7 @@
 
         // Add the final token to the tokens array.
         if (token.length() > 0) {
-            Log.v(LOG_TAG, String.format("Finished final token '%s'", token.toString()));
+            log(String.format("Finished final token '%s'", token.toString()), logging);
             tokens.add(token.toString());
             token.delete(0, token.length());
         }
@@ -117,7 +123,22 @@
      * See also {@link #tokenizeLine(String, String)}
      */
     public static String[] tokenizeLine(String line) throws IllegalArgumentException {
-        return tokenizeLine(line, " ");
+        return tokenizeLine(line, " ", true);
+    }
+
+    public static String[] tokenizeLine(String line, String delim) throws IllegalArgumentException {
+        return tokenizeLine(line, delim, true);
+    }
+
+    /**
+     * Tokenizes the string, splitting on spaces. Does not split between consecutive, unquoted
+     * double-quote marks.
+     *
+     * <p>See also {@link #tokenizeLine(String, String)}
+     */
+    public static String[] tokenizeLine(String line, boolean logging)
+            throws IllegalArgumentException {
+        return tokenizeLine(line, " ", logging);
     }
 
     /**
@@ -147,4 +168,10 @@
         }
         return sb.toString();
     }
+
+    private static void log(String message, boolean display) {
+        if (display) {
+            Log.v(LOG_TAG, message);
+        }
+    }
 }
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 51aa8d8..2429be3 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -67,6 +67,7 @@
 import com.android.tradefed.device.TopHelperTest;
 import com.android.tradefed.device.WaitDeviceRecoveryTest;
 import com.android.tradefed.device.WifiHelperTest;
+import com.android.tradefed.device.contentprovider.ContentProviderHandlerTest;
 import com.android.tradefed.device.metric.AtraceCollectorTest;
 import com.android.tradefed.device.metric.AtraceRunMetricCollectorTest;
 import com.android.tradefed.device.metric.BaseDeviceMetricCollectorTest;
@@ -367,6 +368,9 @@
     WaitDeviceRecoveryTest.class,
     WifiHelperTest.class,
 
+    // device.contentprovider
+    ContentProviderHandlerTest.class,
+
     // device.metric
     AtraceCollectorTest.class,
     AtraceRunMetricCollectorTest.class,
diff --git a/tests/src/com/android/tradefed/device/NativeDeviceTest.java b/tests/src/com/android/tradefed/device/NativeDeviceTest.java
index 23f0e94..5362af8 100644
--- a/tests/src/com/android/tradefed/device/NativeDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/NativeDeviceTest.java
@@ -54,6 +54,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.text.SimpleDateFormat;
 import java.time.Clock;
 import java.util.ArrayList;
@@ -310,7 +311,7 @@
         // Empty list of childen
         EasyMock.replay(fakeEntry);
         try {
-            boolean res = mTestDevice.pullDir("/sdcard/screenshots/", dir);
+            boolean res = mTestDevice.pullDir("/some_device_path/screenshots/", dir);
             assertTrue(res);
             assertTrue(dir.list().length == 0);
         } finally {
@@ -365,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);
@@ -378,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"));
@@ -438,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);
@@ -454,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);
@@ -1181,34 +1182,44 @@
      * Unit test for {@link NativeDevice#getBugreportz()}.
      */
     public void testGetBugreportz() throws IOException {
-        mTestDevice = new TestableAndroidNativeDevice() {
-            @Override
-            public void executeShellCommand(
-                    String command, IShellOutputReceiver receiver,
-                    long maxTimeToOutputShellResponse, TimeUnit timeUnit, int retryAttempts)
+        mTestDevice =
+                new TestableAndroidNativeDevice() {
+                    @Override
+                    public void executeShellCommand(
+                            String command,
+                            IShellOutputReceiver receiver,
+                            long maxTimeToOutputShellResponse,
+                            TimeUnit timeUnit,
+                            int retryAttempts)
                             throws DeviceNotAvailableException {
-                String fakeRep = "OK:/data/0/com.android.shell/bugreports/bugreport1970-10-27.zip";
-                receiver.addOutput(fakeRep.getBytes(), 0, fakeRep.getBytes().length);
-            }
-            @Override
-            public boolean doesFileExist(String destPath) throws DeviceNotAvailableException {
-                return true;
-            }
-            @Override
-            public boolean pullFile(String remoteFilePath, File localFile)
-                    throws DeviceNotAvailableException {
-                return true;
-            }
-            @Override
-            public String executeShellCommand(String command) throws DeviceNotAvailableException {
-                assertEquals("rm /data/0/com.android.shell/bugreports/*", command);
-                return null;
-            }
-            @Override
-            public int getApiLevel() throws DeviceNotAvailableException {
-                return 24;
-            }
-        };
+                        String fakeRep =
+                                "OK:/data/0/com.android.shell/bugreports/bugreport1970-10-27.zip";
+                        receiver.addOutput(fakeRep.getBytes(), 0, fakeRep.getBytes().length);
+                    }
+
+                    @Override
+                    public boolean doesFileExist(String destPath)
+                            throws DeviceNotAvailableException {
+                        return true;
+                    }
+
+                    @Override
+                    public boolean pullFile(String remoteFilePath, File localFile)
+                            throws DeviceNotAvailableException {
+                        return true;
+                    }
+
+                    @Override
+                    public void deleteFile(String deviceFilePath)
+                            throws DeviceNotAvailableException {
+                        assertEquals("/data/0/com.android.shell/bugreports/*", deviceFilePath);
+                    }
+
+                    @Override
+                    public int getApiLevel() throws DeviceNotAvailableException {
+                        return 24;
+                    }
+                };
         FileInputStreamSource f = null;
         try {
             f = (FileInputStreamSource) mTestDevice.getBugreportz();
@@ -2316,11 +2327,13 @@
 
     /** Test when {@link NativeDevice#executeShellV2Command(String)} returns a success. */
     public void testExecuteShellV2Command() throws Exception {
+        OutputStream stdout = null, stderr = null;
         CommandResult res = new CommandResult();
         res.setStatus(CommandStatus.SUCCESS);
         EasyMock.expect(
                         mMockRunUtil.runTimedCmd(
-                                100, "adb", "-s", "serial", "shell", "some", "command"))
+                                100, stdout, stderr, "adb", "-s", "serial", "shell", "some",
+                                "command"))
                 .andReturn(res);
         EasyMock.replay(mMockRunUtil, mMockIDevice);
         assertNotNull(mTestDevice.executeShellV2Command("some command"));
@@ -2332,12 +2345,14 @@
      * repeat because of a timeout.
      */
     public void testExecuteShellV2Command_timeout() throws Exception {
+        OutputStream stdout = null, stderr = null;
         CommandResult res = new CommandResult();
         res.setStatus(CommandStatus.TIMED_OUT);
         res.setStderr("timed out");
         EasyMock.expect(
                         mMockRunUtil.runTimedCmd(
-                                200L, "adb", "-s", "serial", "shell", "some", "command"))
+                                200L, stdout, stderr, "adb", "-s", "serial", "shell", "some",
+                                "command"))
                 .andReturn(res)
                 .times(2);
         EasyMock.replay(mMockRunUtil, mMockIDevice);
@@ -2355,12 +2370,14 @@
      * output.
      */
     public void testExecuteShellV2Command_fail() throws Exception {
+        OutputStream stdout = null, stderr = null;
         CommandResult res = new CommandResult();
         res.setStatus(CommandStatus.FAILED);
         res.setStderr("timed out");
         EasyMock.expect(
                         mMockRunUtil.runTimedCmd(
-                                200L, "adb", "-s", "serial", "shell", "some", "command"))
+                                200L, stdout, stderr, "adb", "-s", "serial", "shell", "some",
+                                "command"))
                 .andReturn(res)
                 .times(1);
         EasyMock.replay(mMockRunUtil, mMockIDevice);
diff --git a/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java b/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
index e14cf50..bce52a1 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
@@ -208,7 +208,7 @@
             assertNotNull(externalStorePath);
             deviceFilePath = String.format("%s/%s", externalStorePath, "tmp_testPushPull.txt");
             // ensure file does not already exist
-            mTestDevice.executeShellCommand(String.format("rm %s", deviceFilePath));
+            mTestDevice.deleteFile(deviceFilePath);
             assertFalse(String.format("%s exists", deviceFilePath),
                     mTestDevice.doesFileExist(deviceFilePath));
 
@@ -223,7 +223,7 @@
                 tmpDestFile.delete();
             }
             if (deviceFilePath != null) {
-                mTestDevice.executeShellCommand(String.format("rm %s", deviceFilePath));
+                mTestDevice.deleteFile(deviceFilePath);
             }
         }
     }
@@ -385,9 +385,9 @@
         final String extStore = "/data/local";
 
         // Clean up after potential failed run
-        mTestDevice.executeShellCommand(String.format("rm %s/testdir", extStore));
-        mTestDevice.executeShellCommand(String.format("rm %s/testdir2/foo.txt", extStore));
-        mTestDevice.executeShellCommand(String.format("rmdir %s/testdir2", extStore));
+        mTestDevice.deleteFile(String.format("%s/testdir", extStore));
+        mTestDevice.deleteFile(String.format("%s/testdir2/foo.txt", extStore));
+        mTestDevice.deleteFile(String.format("%s/testdir2", extStore));
 
         try {
             assertEquals("",
@@ -401,9 +401,9 @@
 
             assertNotNull(mTestDevice.getFileEntry(String.format("%s/testdir/foo.txt", extStore)));
         } finally {
-            mTestDevice.executeShellCommand(String.format("rm %s/testdir", extStore));
-            mTestDevice.executeShellCommand(String.format("rm %s/testdir2/foo.txt", extStore));
-            mTestDevice.executeShellCommand(String.format("rmdir %s/testdir2", extStore));
+            mTestDevice.deleteFile(String.format("%s/testdir", extStore));
+            mTestDevice.deleteFile(String.format("%s/testdir2/foo.txt", extStore));
+            mTestDevice.deleteFile(String.format("%s/testdir2", extStore));
         }
     }
 
@@ -469,7 +469,7 @@
         } finally {
             if (expectedDeviceFilePath != null && externalStorePath != null) {
                 // note that expectedDeviceFilePath has externalStorePath prepended at definition
-                mTestDevice.executeShellCommand(String.format("rm -r %s", expectedDeviceFilePath));
+                mTestDevice.deleteFile(expectedDeviceFilePath);
             }
             FileUtil.recursiveDelete(tmpDir);
         }
@@ -495,8 +495,8 @@
 
         } finally {
             if (expectedDeviceFilePath != null && externalStorePath != null) {
-                mTestDevice.executeShellCommand(String.format("rm -r %s/%s", externalStorePath,
-                        expectedDeviceFilePath));
+                mTestDevice.deleteFile(
+                        String.format("%s/%s", externalStorePath, expectedDeviceFilePath));
             }
             FileUtil.recursiveDelete(rootDir);
         }
diff --git a/tests/src/com/android/tradefed/device/TestDeviceStressTest.java b/tests/src/com/android/tradefed/device/TestDeviceStressTest.java
index 080dc2c..2588889 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceStressTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceStressTest.java
@@ -120,7 +120,7 @@
             // device before the test start.
             tmpDir = createTempTestFiles();
             for (int i = 0; i < mIterations; i++) {
-                mTestDevice.executeShellCommand(String.format("rm -r %s", deviceFilePath));
+                mTestDevice.deleteFile(deviceFilePath);
                 assertFalse(String.format("%s exists", deviceFilePath),
                         mTestDevice.doesFileExist(deviceFilePath));
                 assertTrue(mTestDevice.pushDir(tmpDir, deviceFilePath));
@@ -130,7 +130,7 @@
             if (tmpDir != null) {
                 FileUtil.recursiveDelete(tmpDir);
             }
-            mTestDevice.executeShellCommand(String.format("rm -r %s", deviceFilePath));
+            mTestDevice.deleteFile(deviceFilePath);
             assertFalse(String.format("%s exists", deviceFilePath),
                     mTestDevice.doesFileExist(deviceFilePath));
         }
@@ -159,7 +159,7 @@
             if (tmpDir != null) {
                 FileUtil.recursiveDelete(tmpDir);
             }
-            mTestDevice.executeShellCommand(String.format("rm -r %s", deviceFilePath));
+            mTestDevice.deleteFile(deviceFilePath);
             assertFalse(String.format("%s exists", deviceFilePath),
                     mTestDevice.doesFileExist(deviceFilePath));
         }
diff --git a/tests/src/com/android/tradefed/device/TestDeviceTest.java b/tests/src/com/android/tradefed/device/TestDeviceTest.java
index 9ca9df7..80a35d1 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceTest.java
@@ -29,6 +29,7 @@
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.ITestDevice.MountPointInfo;
 import com.android.tradefed.device.ITestDevice.RecoveryMode;
+import com.android.tradefed.device.contentprovider.ContentProviderHandler;
 import com.android.tradefed.host.HostOptions;
 import com.android.tradefed.host.IHostOptions;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -1169,7 +1170,7 @@
      */
     private void setEncryptedUnsupportedExpectations() throws Exception {
         setEnableAdbRootExpectations();
-        injectShellResponse("vdc cryptfs enablecrypto", "\r\n");
+        injectSystemProperty("ro.crypto.state", "unsupported");
     }
 
     /**
@@ -1177,9 +1178,7 @@
      */
     private void setEncryptedSupported() throws Exception {
         setEnableAdbRootExpectations();
-        injectShellResponse("vdc cryptfs enablecrypto",
-                "500 29805 Usage: cryptfs enablecrypto <wipe|inplace> "
-                + "default|password|pin|pattern [passwd] [noui]\r\n");
+        injectSystemProperty("ro.crypto.state", "encrypted");
     }
 
     /**
@@ -2652,8 +2651,12 @@
                     @Override
                     public String executeShellCommand(String command)
                             throws DeviceNotAvailableException {
-                        test.setName(getClass().getCanonicalName() + "#testSwitchUser_delay");
-                        test.start();
+                        if (!started) {
+                            started = true;
+                            test.setDaemon(true);
+                            test.setName(getClass().getCanonicalName() + "#testSwitchUser_delay");
+                            test.start();
+                        }
                         return "";
                     }
 
@@ -2677,6 +2680,7 @@
                         return 100;
                     }
 
+                    boolean started = false;
                     Thread test =
                             new Thread(
                                     new Runnable() {
@@ -3632,9 +3636,7 @@
                         return true;
                     }
                 };
-        injectShellResponse(
-                "vdc cryptfs enablecrypto",
-                "500 8674 Usage with ext4crypt: cryptfs enablecrypto inplace default noui\r\n");
+        injectSystemProperty("ro.crypto.state", "encrypted");
         EasyMock.replay(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
         assertTrue(mTestDevice.isEncryptionSupported());
         EasyMock.verify(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
@@ -3654,7 +3656,7 @@
                         return true;
                     }
                 };
-        injectShellResponse("vdc cryptfs enablecrypto", "500 8674 Command not recognized\r\n");
+        injectSystemProperty("ro.crypto.state", "unsupported");
         EasyMock.replay(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
         assertFalse(mTestDevice.isEncryptionSupported());
         EasyMock.verify(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
@@ -3672,7 +3674,8 @@
         injectShellResponse("pidof system_server", "929");
         injectShellResponse("am dumpheap 929 /data/dump.hprof", "");
         injectShellResponse("ls \"/data/dump.hprof\"", "/data/dump.hprof");
-        injectShellResponse("rm /data/dump.hprof", "");
+        injectShellResponse("rm -rf \"/data/dump.hprof\"", "");
+
         EasyMock.replay(mMockIDevice, mMockRunUtil);
         File res = mTestDevice.dumpHeap("system_server", "/data/dump.hprof");
         assertNotNull(res);
@@ -3756,6 +3759,11 @@
                             throws DeviceNotAvailableException {
                         return mMockWifi;
                     }
+
+                    @Override
+                    ContentProviderHandler getContentProvider() throws DeviceNotAvailableException {
+                        return null;
+                    }
                 };
         mMockIDevice.executeShellCommand(
                 EasyMock.eq("dumpsys package com.android.tradefed.utils.wifi"),
@@ -3770,4 +3778,75 @@
         mTestDevice.postInvocationTearDown();
         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.
+
+    /** Test {@link TestDevice#doesFileExist(String)}. */
+    public void testDoesFileExists() throws Exception {
+        injectShellResponse("ls \"/data/local/tmp/file\"", "file");
+        EasyMock.replay(mMockIDevice);
+        assertTrue(mTestDevice.doesFileExist("/data/local/tmp/file"));
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /** Test {@link TestDevice#doesFileExist(String)} when the file does not exists. */
+    public void testDoesFileExists_notExists() throws Exception {
+        injectShellResponse(
+                "ls \"/data/local/tmp/file\"",
+                "ls: cannot access 'file': No such file or directory\n");
+        EasyMock.replay(mMockIDevice);
+        assertFalse(mTestDevice.doesFileExist("/data/local/tmp/file"));
+        EasyMock.verify(mMockIDevice);
+    }
+
+    /**
+     * Test {@link TestDevice#doesFileExist(String)} when the file exists on an sdcard from another
+     * user.
+     */
+    public void testDoesFileExists_sdcard() throws Exception {
+        mTestDevice =
+                new TestableTestDevice() {
+                    @Override
+                    public int getCurrentUser()
+                            throws DeviceNotAvailableException, DeviceRuntimeException {
+                        return 10;
+                    }
+                };
+        injectShellResponse("ls \"/storage/emulated/10/file\"", "file");
+        EasyMock.replay(mMockIDevice);
+        assertTrue(mTestDevice.doesFileExist("/sdcard/file"));
+        EasyMock.verify(mMockIDevice);
+    }
 }
diff --git a/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java b/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
new file mode 100644
index 0000000..a9f4533
--- /dev/null
+++ b/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
@@ -0,0 +1,366 @@
+/*
+ * Copyright (C) 2019 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.tradefed.device.contentprovider;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+/** Run unit tests for {@link ContentProviderHandler}. */
+@RunWith(JUnit4.class)
+public class ContentProviderHandlerTest {
+
+    private ContentProviderHandler mProvider;
+    private ITestDevice mMockDevice;
+
+    @Before
+    public void setUp() {
+        mMockDevice = Mockito.mock(ITestDevice.class);
+        mProvider = new ContentProviderHandler(mMockDevice);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mProvider.tearDown();
+    }
+
+    /** Test the install flow. */
+    @Test
+    public void testSetUp_install() throws Exception {
+        Set<String> set = new HashSet<>();
+        doReturn(set).when(mMockDevice).getInstalledPackageNames();
+        doReturn(1).when(mMockDevice).getCurrentUser();
+        doReturn(null).when(mMockDevice).installPackage(any(), eq(true), eq(true));
+        assertTrue(mProvider.setUp());
+    }
+
+    @Test
+    public void testSetUp_alreadyInstalled() throws Exception {
+        Set<String> set = new HashSet<>();
+        set.add(ContentProviderHandler.PACKAGE_NAME);
+        doReturn(set).when(mMockDevice).getInstalledPackageNames();
+
+        assertTrue(mProvider.setUp());
+    }
+
+    @Test
+    public void testSetUp_installFail() throws Exception {
+        Set<String> set = new HashSet<>();
+        doReturn(set).when(mMockDevice).getInstalledPackageNames();
+        doReturn(1).when(mMockDevice).getCurrentUser();
+        doReturn("fail").when(mMockDevice).installPackage(any(), eq(true), eq(true));
+
+        assertFalse(mProvider.setUp());
+    }
+
+    /** Test {@link ContentProviderHandler#deleteFile(String)}. */
+    @Test
+    public void testDeleteFile() throws Exception {
+        String devicePath = "path/somewhere/file.txt";
+        doReturn(99).when(mMockDevice).getCurrentUser();
+        doReturn(mockSuccess())
+                .when(mMockDevice)
+                .executeShellV2Command(
+                        eq(
+                                "content delete --user 99 --uri "
+                                        + ContentProviderHandler.createEscapedContentUri(
+                                                devicePath)));
+        assertTrue(mProvider.deleteFile(devicePath));
+    }
+
+    /** Test {@link ContentProviderHandler#deleteFile(String)}. */
+    @Test
+    public void testDeleteFile_fail() throws Exception {
+        String devicePath = "path/somewhere/file.txt";
+        CommandResult result = new CommandResult(CommandStatus.FAILED);
+        result.setStdout("");
+        result.setStderr("couldn't find the file");
+        doReturn(99).when(mMockDevice).getCurrentUser();
+        doReturn(result)
+                .when(mMockDevice)
+                .executeShellV2Command(
+                        eq(
+                                "content delete --user 99 --uri "
+                                        + ContentProviderHandler.createEscapedContentUri(
+                                                devicePath)));
+        assertFalse(mProvider.deleteFile(devicePath));
+    }
+
+    /** Test {@link ContentProviderHandler#deleteFile(String)}. */
+    @Test
+    public void testError() throws Exception {
+        String devicePath = "path/somewhere/file.txt";
+        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+        result.setStdout("[ERROR] Unsupported operation: delete");
+        doReturn(99).when(mMockDevice).getCurrentUser();
+        doReturn(result)
+                .when(mMockDevice)
+                .executeShellV2Command(
+                        eq(
+                                "content delete --user 99 --uri "
+                                        + ContentProviderHandler.createEscapedContentUri(
+                                                devicePath)));
+        assertFalse(mProvider.deleteFile(devicePath));
+    }
+
+    /** Test {@link ContentProviderHandler#pushFile(File, String)}. */
+    @Test
+    public void testPushFile() throws Exception {
+        File toPush = FileUtil.createTempFile("content-provider-test", ".txt");
+        try {
+            String devicePath = "path/somewhere/file.txt";
+            doReturn(99).when(mMockDevice).getCurrentUser();
+            doReturn(mockSuccess())
+                    .when(mMockDevice)
+                    .executeShellV2Command(
+                            eq(
+                                    "content write --user 99 --uri "
+                                            + ContentProviderHandler.createEscapedContentUri(
+                                                    devicePath)),
+                            eq(toPush));
+            assertTrue(mProvider.pushFile(toPush, devicePath));
+        } finally {
+            FileUtil.deleteFile(toPush);
+        }
+    }
+
+    /** Test {@link ContentProviderHandler#pullFile(String, File)}. */
+    @Test
+    public void testPullFile_verifyShellCommand() throws Exception {
+        File pullTo = FileUtil.createTempFile("content-provider-test", ".txt");
+        String devicePath = "path/somewhere/file.txt";
+        doReturn(99).when(mMockDevice).getCurrentUser();
+        mockPullFileSuccess();
+
+        try {
+            mProvider.pullFile(devicePath, pullTo);
+
+            // Capture the shell command used by pullFile.
+            ArgumentCaptor<String> shellCommandCaptor = ArgumentCaptor.forClass(String.class);
+            verify(mMockDevice)
+                    .executeShellV2Command(shellCommandCaptor.capture(), any(OutputStream.class));
+
+            // Verify the command.
+            assertEquals(
+                    shellCommandCaptor.getValue(),
+                    "content read --user 99 --uri "
+                            + ContentProviderHandler.createEscapedContentUri(devicePath));
+        } finally {
+            FileUtil.deleteFile(pullTo);
+        }
+    }
+
+    /** Test {@link ContentProviderHandler#pullFile(String, File)}. */
+    @Test
+    public void testPullFile_createLocalFileIfNotExist() throws Exception {
+        File pullTo = new File("content-provider-test.txt");
+        String devicePath = "path/somewhere/file.txt";
+        mockPullFileSuccess();
+
+        try {
+            assertFalse(pullTo.exists());
+            mProvider.pullFile(devicePath, pullTo);
+            assertTrue(pullTo.exists());
+        } finally {
+            FileUtil.deleteFile(pullTo);
+        }
+    }
+
+    /** Test {@link ContentProviderHandler#pullFile(String, File)}. */
+    @Test
+    public void testPullFile_success() throws Exception {
+        File pullTo = new File("content-provider-test.txt");
+        String devicePath = "path/somewhere/file.txt";
+
+        try {
+            mockPullFileSuccess();
+            assertTrue(mProvider.pullFile(devicePath, pullTo));
+        } finally {
+            FileUtil.deleteFile(pullTo);
+        }
+    }
+
+    /** Test {@link ContentProviderHandler#pullDir(String, File)}. */
+    @Test
+    public void testPullDir_EmptyDirectory() throws Exception {
+        File pullTo = FileUtil.createTempDir("content-provider-test");
+
+        doReturn("No result found.\n").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.
+        assertEquals(
+                "\"content://android.tradefed.contentprovider/filepath%252Ffile%2520name"
+                        + "%2520spaced%2520%28data%29\"",
+                espacedUrl);
+    }
+
+    @Test
+    public void testParseQueryResultRow() {
+        String row =
+                "Row: 1 name=name spaced with , ,comma, "
+                        + "absolute_path=/storage/emulated/0/Alarms/name spaced with , ,comma, "
+                        + "is_directory=true, mime_type=NULL, metadata=NULL";
+
+        HashMap<String, String> columnValues = mProvider.parseQueryResultRow(row);
+
+        assertEquals(
+                columnValues.get(ContentProviderHandler.COLUMN_NAME), "name spaced with , ,comma");
+        assertEquals(
+                columnValues.get(ContentProviderHandler.COLUMN_ABSOLUTE_PATH),
+                "/storage/emulated/0/Alarms/name spaced with , ,comma");
+        assertEquals(columnValues.get(ContentProviderHandler.COLUMN_DIRECTORY), "true");
+        assertEquals(columnValues.get(ContentProviderHandler.COLUMN_MIME_TYPE), "NULL");
+        assertEquals(columnValues.get(ContentProviderHandler.COLUMN_METADATA), "NULL");
+    }
+
+    private CommandResult mockSuccess() {
+        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+        result.setStderr("");
+        result.setStdout("");
+        return result;
+    }
+
+    private void mockPullFileSuccess() throws Exception {
+        doReturn(mockSuccess())
+                .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/tests/src/com/android/tradefed/device/metric/AtraceCollectorTest.java b/tests/src/com/android/tradefed/device/metric/AtraceCollectorTest.java
index e7a6070..5ffd8d6 100644
--- a/tests/src/com/android/tradefed/device/metric/AtraceCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/AtraceCollectorTest.java
@@ -230,9 +230,7 @@
         EasyMock.expect(mMockDevice.pullFile(EasyMock.eq(M_DEFAULT_LOG_PATH)))
                 .andReturn(new File("/tmp/potato"))
                 .once();
-        EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("rm -f " + M_DEFAULT_LOG_PATH)))
-                .andReturn("")
-                .times(1);
+        mMockDevice.deleteFile(M_DEFAULT_LOG_PATH);
 
         EasyMock.replay(mMockDevice);
         mAtrace.onTestEnd(
diff --git a/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java b/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java
index 651371c..49fab4c 100644
--- a/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java
+++ b/tests/src/com/android/tradefed/targetprep/DeviceSetupTest.java
@@ -1037,9 +1037,7 @@
         doSetupExpectations();
         doCheckExternalStoreSpaceExpectations();
         EasyMock.expect(mMockDevice.pullFile("/data/local.prop")).andReturn(null).once();
-        EasyMock.expect(mMockDevice.executeShellCommand("rm -f /data/local.prop"))
-                .andReturn(null)
-                .once();
+        mMockDevice.deleteFile("/data/local.prop");
         mMockDevice.reboot();
         EasyMock.expectLastCall().once();
 
diff --git a/tests/src/com/android/tradefed/testtype/AndroidJUnitTestTest.java b/tests/src/com/android/tradefed/testtype/AndroidJUnitTestTest.java
index c291318..07a7096 100644
--- a/tests/src/com/android/tradefed/testtype/AndroidJUnitTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/AndroidJUnitTestTest.java
@@ -203,10 +203,12 @@
                 EasyMock.<File>anyObject(), EasyMock.<String>anyObject())).andReturn(Boolean.TRUE);
         EasyMock.expect(mMockTestDevice.executeShellCommand(EasyMock.<String>anyObject()))
                 .andReturn("")
-                .times(2);
+                .times(1);
+        mMockTestDevice.deleteFile("/data/local/tmp/ajur");
         EasyMock.replay(mMockRemoteRunner, mMockTestDevice);
 
         File tmpFile = FileUtil.createTempFile("testFile", ".txt");
+        FileUtil.writeToFile(TEST1.toString(), tmpFile);
         try {
             mAndroidJUnitTest.setIncludeTestFile(tmpFile);
             mAndroidJUnitTest.run(mMockListener);
@@ -228,7 +230,8 @@
                 EasyMock.<File>anyObject(), EasyMock.<String>anyObject())).andReturn(Boolean.TRUE);
         EasyMock.expect(mMockTestDevice.executeShellCommand(EasyMock.<String>anyObject()))
                 .andReturn("")
-                .times(2);
+                .times(1);
+        mMockTestDevice.deleteFile("/data/local/tmp/ajur");
         EasyMock.replay(mMockRemoteRunner, mMockTestDevice);
 
         File tmpFile = FileUtil.createTempFile("notTestFile", ".txt");
@@ -257,11 +260,14 @@
                 EasyMock.<String>anyObject())).andReturn(Boolean.TRUE).times(2);
         EasyMock.expect(mMockTestDevice.executeShellCommand(EasyMock.<String>anyObject()))
                 .andReturn("")
-                .times(4);
+                .times(2);
+        mMockTestDevice.deleteFile("/data/local/tmp/ajur");
         EasyMock.replay(mMockRemoteRunner, mMockTestDevice);
 
         File tmpFileInclude = FileUtil.createTempFile("includeFile", ".txt");
+        FileUtil.writeToFile(TEST1.toString(), tmpFileInclude);
         File tmpFileExclude = FileUtil.createTempFile("excludeFile", ".txt");
+        FileUtil.writeToFile(TEST2.toString(), tmpFileExclude);
         try {
             mAndroidJUnitTest.addIncludeFilter(TEST1.getClassName());
             mAndroidJUnitTest.addExcludeFilter(TEST2.toString());
@@ -325,11 +331,14 @@
                 .times(2);
         EasyMock.expect(mMockTestDevice.executeShellCommand(EasyMock.<String>anyObject()))
                 .andReturn("")
-                .times(4);
+                .times(2);
+        mMockTestDevice.deleteFile("/data/local/tmp/ajur");
         EasyMock.replay(mMockRemoteRunner, mMockTestDevice);
 
         File tmpFileInclude = FileUtil.createTempFile("includeFile", ".txt");
+        FileUtil.writeToFile(TEST1.toString(), tmpFileInclude);
         File tmpFileExclude = FileUtil.createTempFile("excludeFile", ".txt");
+        FileUtil.writeToFile(TEST2.toString(), tmpFileExclude);
         try {
             OptionSetter setter = new OptionSetter(mAndroidJUnitTest);
             setter.setOptionValue("test-file-include-filter", tmpFileInclude.getAbsolutePath());
diff --git a/tests/src/com/android/tradefed/testtype/GTestTest.java b/tests/src/com/android/tradefed/testtype/GTestTest.java
index 31018e0..af2c822 100644
--- a/tests/src/com/android/tradefed/testtype/GTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/GTestTest.java
@@ -316,8 +316,7 @@
                 EasyMock.same(mMockReceiver), EasyMock.anyLong(), (TimeUnit)EasyMock.anyObject(),
                 EasyMock.anyInt());
         // Expect deletion of file on device
-        EasyMock.expect(mMockITestDevice.executeShellCommand(
-                EasyMock.eq(String.format("rm %s", deviceScriptPath)))).andReturn("");
+        mMockITestDevice.deleteFile(deviceScriptPath);
         replayMocks();
         mGTest.run(mMockInvocationListener);
 
@@ -410,9 +409,8 @@
                 .andReturn("-rwxr-xr-x 1 root shell 7 2009-01-01 00:00 " + testPath2);
         String[] files = new String[] {"test1", "test2"};
         EasyMock.expect(mMockITestDevice.getChildren(nativeTestPath)).andReturn(files);
-        EasyMock.expect(mMockITestDevice.executeShellCommand(EasyMock.contains("rm")))
-                .andReturn("")
-                .times(2);
+        mMockITestDevice.deleteFile(testPath1 + "_res.xml");
+        mMockITestDevice.deleteFile(testPath2 + "_res.xml");
         EasyMock.expect(mMockITestDevice.pullFile((String)EasyMock.anyObject(),
                 (File)EasyMock.anyObject())).andStubReturn(true);
         mMockITestDevice.executeShellCommand(EasyMock.contains(test1),
diff --git a/util-apps/ContentProvider/Android.mk b/util-apps/ContentProvider/Android.mk
new file mode 100644
index 0000000..29a3d6b
--- /dev/null
+++ b/util-apps/ContentProvider/Android.mk
@@ -0,0 +1,20 @@
+# Copyright (C) 2012 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+# Build the test APKs using their own makefiles
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/util-apps/ContentProvider/androidTest/Android.mk b/util-apps/ContentProvider/androidTest/Android.mk
new file mode 100644
index 0000000..7373c0d
--- /dev/null
+++ b/util-apps/ContentProvider/androidTest/Android.mk
@@ -0,0 +1,31 @@
+
+# Copyright (C) 2012 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := TradefedContentProviderTest
+LOCAL_INSTRUMENTATION_FOR := TradefedContentProvider
+LOCAL_MODULE_TAGS := tests optional
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SDK_VERSION := 24
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    junit \
+    androidx.test.runner
+
+LOCAL_COMPATIBILITY_SUITE := general-tests
+
+include $(BUILD_PACKAGE)
diff --git a/util-apps/ContentProvider/androidTest/AndroidManifest.xml b/util-apps/ContentProvider/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..18e1b11
--- /dev/null
+++ b/util-apps/ContentProvider/androidTest/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2018 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.tradefed.contentprovider.test">
+    <uses-sdk android:minSdkVersion="24"
+          android:targetSdkVersion="26"/>
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.tradefed.contentprovider"
+                     android:label="Unit tests for Tradefed Content provider">
+    </instrumentation>
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+</manifest>
diff --git a/util-apps/ContentProvider/androidTest/AndroidTest.xml b/util-apps/ContentProvider/androidTest/AndroidTest.xml
new file mode 100644
index 0000000..325e7f9
--- /dev/null
+++ b/util-apps/ContentProvider/androidTest/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+<configuration description="Configuration for Tradefed Content Provider Tests">
+    <option name="test-suite-tag" value="tradefed_content_provider" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="TradefedContentProviderTest.apk" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="TradefedContentProvider.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="package" value="android.tradefed.contentprovider.test" />
+    </test>
+</configuration>
diff --git a/util-apps/ContentProvider/androidTest/src/android/tradefed/contentprovider/ManagedFileContentProviderTest.java b/util-apps/ContentProvider/androidTest/src/android/tradefed/contentprovider/ManagedFileContentProviderTest.java
new file mode 100644
index 0000000..8ba5559
--- /dev/null
+++ b/util-apps/ContentProvider/androidTest/src/android/tradefed/contentprovider/ManagedFileContentProviderTest.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2018 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 android.tradefed.contentprovider;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for {@link android.tradefed.contentprovider.ManagedFileContentProvider}. TODO: Complete the
+ * tests when automatic test setup is made.
+ */
+@RunWith(AndroidJUnit4.class)
+public class ManagedFileContentProviderTest {
+
+    public static final String CONTENT_PROVIDER_AUTHORITY = "android.tradefed.contentprovider";
+    private static final String TEST_FILE = "ManagedFileContentProviderTest.txt";
+    private static final String TEST_DIRECTORY = "ManagedFileContentProviderTestDir";
+    private static final String TEST_SUBDIRECTORY = "ManagedFileContentProviderTestSubDir";
+
+    private File mTestFile = null;
+    private File mTestDir = null;
+    private File mTestSubdir = null;
+
+    private Uri mTestFileUri;
+    private Uri mTestDirUri;
+    private Uri mTestSubdirUri;
+
+    private Context mAppContext;
+    private ContentResolver mResolver;
+    private List<Uri> mShouldBeCleaned = new ArrayList<>();
+    private ContentValues mCv;
+
+    @Before
+    public void setUp() throws Exception {
+        mCv = new ContentValues();
+
+        // Context of the app under test.
+        mAppContext = InstrumentationRegistry.getTargetContext();
+        mResolver = mAppContext.getContentResolver();
+        assertEquals("android.tradefed.contentprovider", mAppContext.getPackageName());
+    }
+
+    @After
+    public void tearDown() {
+        if (mTestFile != null) {
+            mTestFile.delete();
+        }
+        if (mTestDir != null) {
+            mTestDir.delete();
+        }
+        if (mTestSubdir != null) {
+            mTestSubdir.delete();
+        }
+        for (Uri uri : mShouldBeCleaned) {
+            mResolver.delete(
+                    uri,
+                    /** selection * */
+                    null,
+                    /** selectionArgs * */
+                    null);
+        }
+    }
+
+    private void createTestDirectories() throws Exception {
+        mTestDir = new File(Environment.getExternalStorageDirectory(), TEST_DIRECTORY);
+        mTestDir.mkdir();
+        mTestSubdir = new File(mTestDir, TEST_SUBDIRECTORY);
+        mTestSubdir.mkdir();
+        createTestFile(mTestDir);
+    }
+
+    private void createTestFile(File parentDir) throws Exception {
+        mTestFile = new File(parentDir, TEST_FILE);
+        mTestFile.createNewFile();
+
+        mTestFileUri = createContentUri(mTestFile.getAbsolutePath());
+    }
+
+    /** Test that we can delete a file from the content provider. */
+    @Test
+    public void testDelete() throws Exception {
+        createTestFile(Environment.getExternalStorageDirectory());
+        Uri uriResult = mResolver.insert(mTestFileUri, mCv);
+        mShouldBeCleaned.add(mTestFileUri);
+        // Insert is successful
+        assertEquals(mTestFileUri, uriResult);
+
+        // Trying to insert again is inop
+        Uri reInsert = mResolver.insert(mTestFileUri, mCv);
+        assertNull(reInsert);
+
+        // Now delete
+        int affected =
+                mResolver.delete(
+                        mTestFileUri,
+                        /** selection * */
+                        null,
+                        /* selectionArgs */
+                        null);
+        assertEquals(1, affected);
+        // File should have been deleted.
+        assertFalse(mTestFile.exists());
+
+        // We can now insert again
+        mTestFile.createNewFile();
+        uriResult = mResolver.insert(mTestFileUri, mCv);
+        assertEquals(mTestFileUri, uriResult);
+    }
+
+    /** Test that querying the content provider for a single File returns null. */
+    @Test
+    public void testQueryForFile() throws Exception {
+        createTestFile(Environment.getExternalStorageDirectory());
+        Cursor cursor =
+                mResolver.query(
+                        mTestFileUri,
+                        /** projection * */
+                        null,
+                        /* selection */
+                        null,
+                        /* selectionArgs */
+                        null,
+                        /* sortOrder */
+                        null);
+        try {
+            assertEquals(1, cursor.getCount());
+            String[] columns = cursor.getColumnNames();
+            assertEquals(ManagedFileContentProvider.COLUMNS, columns);
+            assertTrue(cursor.moveToNext());
+
+            // Test values in all columns and enforce column ordering.
+            // Name
+            assertEquals(TEST_FILE, cursor.getString(0));
+            // Absolute path
+            assertEquals(
+                    Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + TEST_FILE,
+                    cursor.getString(1));
+            // Is directory
+            assertEquals("false", cursor.getString(2));
+            // Type
+            assertEquals("text/plain", cursor.getString(3));
+            // Metadata
+            assertNull(cursor.getString(4));
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /** Test that querying the content provider for a file is working when abstracting the sdcard */
+    @Test
+    public void testQueryForFile_sdcard() throws Exception {
+        createTestFile(Environment.getExternalStorageDirectory());
+        Uri sdcardUri = createContentUri(String.format("sdcard/%s", mTestFile.getName()));
+
+        Cursor cursor =
+                mResolver.query(
+                        sdcardUri,
+                        /* projection */
+                        null,
+                        /* selection */
+                        null,
+                        /* selectionArgs */
+                        null,
+                        /* sortOrder */
+                        null);
+        try {
+            assertEquals(1, cursor.getCount());
+            String[] columns = cursor.getColumnNames();
+            assertEquals(ManagedFileContentProvider.COLUMNS, columns);
+            assertTrue(cursor.moveToNext());
+
+            // Test values in all columns and enforce column ordering.
+            // Name
+            assertEquals(TEST_FILE, cursor.getString(0));
+            // Absolute path
+            assertEquals(
+                    Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + TEST_FILE,
+                    cursor.getString(1));
+            // Is directory
+            assertEquals("false", cursor.getString(2));
+            // Type
+            assertEquals("text/plain", cursor.getString(3));
+            // Metadata
+            assertNull(cursor.getString(4));
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /**
+     * Test that querying the content provider for a directory returns content of the directory -
+     * one row per each subdirectory/file.
+     */
+    @Test
+    public void testQueryForDirectoryContent() throws Exception {
+        createTestDirectories();
+
+        mTestDirUri = createContentUri(mTestDir.getAbsolutePath());
+        Cursor cursor =
+                mResolver.query(
+                        mTestDirUri,
+                        /** projection * */
+                        null,
+                        /** selection * */
+                        null,
+                        /** selectionArgs* */
+                        null,
+                        /** sortOrder * */
+                        null);
+        try {
+            // One row for subdir, one row for a file.
+            assertEquals(2, cursor.getCount());
+            String[] columns = cursor.getColumnNames();
+            assertEquals(ManagedFileContentProvider.COLUMNS, columns);
+
+            // Test the file.
+            assertTrue(cursor.moveToNext());
+            // Test values in all columns and enforce column ordering.
+            // Name
+            assertEquals(TEST_FILE, cursor.getString(0));
+            // Absolute path
+            assertEquals(
+                    Environment.getExternalStorageDirectory().getAbsolutePath()
+                            + "/"
+                            + TEST_DIRECTORY
+                            + "/"
+                            + TEST_FILE,
+                    cursor.getString(1));
+            // Is directory
+            assertEquals("false", cursor.getString(2));
+            // Type
+            assertEquals("text/plain", cursor.getString(3));
+            // Metadata
+            assertNull(cursor.getString(4));
+
+            // Test the subdirectory.
+            assertTrue(cursor.moveToNext());
+            // Test values in all columns and enforce column ordering.
+            // Name
+            assertEquals(TEST_SUBDIRECTORY, cursor.getString(0));
+            // Absolute path
+            assertEquals(
+                    Environment.getExternalStorageDirectory().getAbsolutePath()
+                            + "/"
+                            + TEST_DIRECTORY
+                            + "/"
+                            + TEST_SUBDIRECTORY,
+                    cursor.getString(1));
+            // Is directory
+            assertEquals("true", cursor.getString(2));
+            // Type
+            assertNull(cursor.getString(3));
+            // Metadata
+            assertNull(cursor.getString(4));
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private Uri createContentUri(String path) {
+        Uri.Builder builder = new Uri.Builder();
+        return builder.scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(CONTENT_PROVIDER_AUTHORITY)
+                .appendPath(path)
+                .build();
+    }
+}
diff --git a/util-apps/ContentProvider/hostsidetests/.classpath b/util-apps/ContentProvider/hostsidetests/.classpath
new file mode 100644
index 0000000..60e2e17
--- /dev/null
+++ b/util-apps/ContentProvider/hostsidetests/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/tradefederation"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/util-apps/ContentProvider/hostsidetests/.project b/util-apps/ContentProvider/hostsidetests/.project
new file mode 100644
index 0000000..07bfc9a
--- /dev/null
+++ b/util-apps/ContentProvider/hostsidetests/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>TradefedContentProviderHostTest</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/util-apps/ContentProvider/hostsidetests/Android.mk b/util-apps/ContentProvider/hostsidetests/Android.mk
new file mode 100644
index 0000000..21f2915
--- /dev/null
+++ b/util-apps/ContentProvider/hostsidetests/Android.mk
@@ -0,0 +1,27 @@
+
+# Copyright (C) 2012 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE :=TradefedContentProviderHostTest
+LOCAL_MODULE_TAGS := tests
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SDK_VERSION := 24
+LOCAL_JAVA_LIBRARIES := tradefed
+LOCAL_STATIC_JAVA_LIBRARIES := junit objenesis
+LOCAL_COMPATIBILITY_SUITE := general-tests
+
+include $(BUILD_HOST_JAVA_LIBRARY)
diff --git a/util-apps/ContentProvider/hostsidetests/AndroidTest.xml b/util-apps/ContentProvider/hostsidetests/AndroidTest.xml
new file mode 100644
index 0000000..744b54d
--- /dev/null
+++ b/util-apps/ContentProvider/hostsidetests/AndroidTest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<configuration description="Config for Tradefed Content Provider host tests">
+    <option name="test-suite-tag" value="tradefed_content_provider" />
+    <test class="com.android.tradefed.testtype.HostTest">
+        <option name="class" value="com.android.tradefed.contentprovider.ContentProviderTest" />
+    </test>
+</configuration>
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
new file mode 100644
index 0000000..0e6a655
--- /dev/null
+++ b/util-apps/ContentProvider/hostsidetests/src/com/android/tradefed/contentprovider/ContentProviderTest.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2019 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.tradefed.contentprovider;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
+import com.android.tradefed.device.contentprovider.ContentProviderHandler;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.FileUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Host tests for the Tradefed Content Provider. */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class ContentProviderTest extends BaseHostJUnit4Test {
+    private static final String EXTERNAL_STORAGE_PATH = "/storage/emulated/%d/";
+
+    private ContentProviderHandler mHandler;
+    private String mCurrentUserStoragePath;
+    private List<String> mToBeDeleted = new ArrayList<>();
+
+    @Before
+    public void setUp() throws Exception {
+        mHandler = new ContentProviderHandler(getDevice());
+        mCurrentUserStoragePath =
+                String.format(EXTERNAL_STORAGE_PATH, getDevice().getCurrentUser());
+        assertTrue(mHandler.setUp());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        for (String delete : mToBeDeleted) {
+            getDevice().deleteFile(delete);
+        }
+        mToBeDeleted.clear();
+        if (mHandler != null) {
+            mHandler.tearDown();
+        }
+    }
+
+    /** Test pushing a file with special characters in the name. */
+    @Test
+    public void testPushFile_encode() throws Exception {
+        // Name with space and parenthesis
+        File tmpFile = FileUtil.createTempFile("tmpFileToPush (test)", ".txt");
+        try {
+            boolean res = mHandler.pushFile(tmpFile, "/sdcard/" + tmpFile.getName());
+            assertTrue(res);
+            assertTrue(getDevice().doesFileExist(mCurrentUserStoragePath + tmpFile.getName()));
+        } finally {
+            FileUtil.deleteFile(tmpFile);
+        }
+    }
+
+    @Test
+    public void testPushFile() throws Exception {
+        File tmpFile = FileUtil.createTempFile("tmpFileToPush", ".txt");
+        try {
+            String devicePath = "/sdcard/" + tmpFile.getName();
+            mToBeDeleted.add(devicePath);
+            boolean res = mHandler.pushFile(tmpFile, devicePath);
+            assertTrue(res);
+            assertTrue(getDevice().doesFileExist(mCurrentUserStoragePath + tmpFile.getName()));
+        } finally {
+            FileUtil.deleteFile(tmpFile);
+        }
+    }
+
+    /** Test that we can delete a file via Content Provider. */
+    @Test
+    public void testDeleteFile() throws Exception {
+        File tmpFile = FileUtil.createTempFile("tmpFileToPush", ".txt");
+        try {
+            String devicePath = "/sdcard/" + tmpFile.getName();
+            // Push the file first
+            boolean res = mHandler.pushFile(tmpFile, devicePath);
+            assertTrue(res);
+            assertTrue(getDevice().doesFileExist(mCurrentUserStoragePath + tmpFile.getName()));
+            // Attempt to delete it.
+            assertTrue(mHandler.deleteFile(devicePath));
+        } finally {
+            FileUtil.deleteFile(tmpFile);
+        }
+    }
+
+    @Test
+    public void testPullFile() throws Exception {
+        String fileContent = "some test content";
+
+        // First, push the file onto a device.
+        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.
+        assertFalse(tmpPullFile.exists());
+
+        try {
+            boolean res = mHandler.pullFile("/sdcard/" + tmpFile.getName(), tmpPullFile);
+            assertTrue(res);
+            assertTrue(tmpPullFile.exists()); // Verify existence.
+            assertEquals(FileUtil.readStringFromFile(tmpPullFile), fileContent); // Verify content.
+        } finally {
+            FileUtil.deleteFile(tmpFile);
+            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);
+        }
+    }
+}
diff --git a/util-apps/ContentProvider/main/Android.mk b/util-apps/ContentProvider/main/Android.mk
new file mode 100644
index 0000000..4add64f
--- /dev/null
+++ b/util-apps/ContentProvider/main/Android.mk
@@ -0,0 +1,26 @@
+
+# Copyright (C) 2012 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+LOCAL_SRC_FILES := $(call all-java-files-under, java)
+LOCAL_PACKAGE_NAME := TradefedContentProvider
+LOCAL_SDK_VERSION := 24
+LOCAL_COMPATIBILITY_SUITE := general-tests
+LOCAL_STATIC_JAVA_LIBRARIES := androidx.annotation_annotation
+
+include $(BUILD_PACKAGE)
diff --git a/util-apps/ContentProvider/main/AndroidManifest.xml b/util-apps/ContentProvider/main/AndroidManifest.xml
new file mode 100644
index 0000000..ac37e68
--- /dev/null
+++ b/util-apps/ContentProvider/main/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright (C) 2018 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.tradefed.contentprovider">
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
+    <application>
+        <provider
+            android:name="android.tradefed.contentprovider.ManagedFileContentProvider"
+            android:authorities="android.tradefed.contentprovider"
+            android:grantUriPermissions="true"
+            android:exported="true"
+            android:enabled="true"
+            />
+    </application>
+</manifest>
diff --git a/util-apps/ContentProvider/main/java/android/tradefed/contentprovider/ManagedFileContentProvider.java b/util-apps/ContentProvider/main/java/android/tradefed/contentprovider/ManagedFileContentProvider.java
new file mode 100644
index 0000000..f759512
--- /dev/null
+++ b/util-apps/ContentProvider/main/java/android/tradefed/contentprovider/ManagedFileContentProvider.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2018 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 android.tradefed.contentprovider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Content Provider implementation to hide sd card details away from host/device interactions, and
+ * that allows to abstract the host/device interactions more by allowing device and host to
+ * communicate files through the provider.
+ *
+ * <p>This implementation aims to be standard and work in all situations.
+ */
+public class ManagedFileContentProvider extends ContentProvider {
+    public static final String COLUMN_NAME = "name";
+    public static final String COLUMN_ABSOLUTE_PATH = "absolute_path";
+    public static final String COLUMN_DIRECTORY = "is_directory";
+    public static final String COLUMN_MIME_TYPE = "mime_type";
+    public static final String COLUMN_METADATA = "metadata";
+
+    // TODO: Complete the list of columns
+    public static final String[] COLUMNS =
+            new String[] {
+                COLUMN_NAME,
+                COLUMN_ABSOLUTE_PATH,
+                COLUMN_DIRECTORY,
+                COLUMN_MIME_TYPE,
+                COLUMN_METADATA
+            };
+
+    private static String TAG = "ManagedFileContentProvider";
+    private static MimeTypeMap sMimeMap = MimeTypeMap.getSingleton();
+
+    private Map<Uri, ContentValues> mFileTracker = new HashMap<>();
+
+    @Override
+    public boolean onCreate() {
+        mFileTracker = new HashMap<>();
+        return true;
+    }
+
+    /**
+     * Use a content URI with absolute device path embedded to get information about a file or a
+     * directory on the device.
+     *
+     * @param uri A content uri that contains the path to the desired file/directory.
+     * @param projection - not supported.
+     * @param selection - not supported.
+     * @param selectionArgs - not supported.
+     * @param sortOrder - not supported.
+     * @return A {@link Cursor} containing the results of the query. Cursor contains a single row
+     *     for files and for directories it returns one row for each {@link File} returned by {@link
+     *     File#listFiles()}.
+     */
+    @Nullable
+    @Override
+    public Cursor query(
+            @NonNull Uri uri,
+            @Nullable String[] projection,
+            @Nullable String selection,
+            @Nullable String[] selectionArgs,
+            @Nullable String sortOrder) {
+        File file = getFileForUri(uri);
+        if ("/".equals(file.getAbsolutePath())) {
+            // Querying the root will list all the known file (inserted)
+            final MatrixCursor cursor = new MatrixCursor(COLUMNS, mFileTracker.size());
+            for (Map.Entry<Uri, ContentValues> path : mFileTracker.entrySet()) {
+                String metadata = path.getValue().getAsString(COLUMN_METADATA);
+                cursor.addRow(getRow(COLUMNS, getFileForUri(path.getKey()), metadata));
+            }
+            return cursor;
+        }
+
+        if (!file.exists()) {
+            Log.e(TAG, String.format("Query - File from uri: '%s' does not exists.", uri));
+            return null;
+        }
+
+        if (!file.isDirectory()) {
+            // Just return the information about the file itself.
+            final MatrixCursor cursor = new MatrixCursor(COLUMNS, 1);
+            cursor.addRow(getRow(COLUMNS, file, /* metadata= */ null));
+            return cursor;
+        }
+
+        // Otherwise return the content of the directory - similar to doing ls command.
+        File[] files = file.listFiles();
+        sortFilesByAbsolutePath(files);
+        final MatrixCursor cursor = new MatrixCursor(COLUMNS, files.length + 1);
+        for (File child : files) {
+            cursor.addRow(getRow(COLUMNS, child, /* metadata= */ null));
+        }
+        return cursor;
+    }
+
+    @Nullable
+    @Override
+    public String getType(@NonNull Uri uri) {
+        return getType(getFileForUri(uri));
+    }
+
+    @Nullable
+    @Override
+    public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
+        String extra = "";
+        File file = getFileForUri(uri);
+        if (!file.exists()) {
+            Log.e(TAG, String.format("Insert - File from uri: '%s' does not exists.", uri));
+            return null;
+        }
+        if (mFileTracker.get(uri) != null) {
+            Log.e(
+                    TAG,
+                    String.format("Insert - File from uri: '%s' already exists, ignoring.", uri));
+            return null;
+        }
+        mFileTracker.put(uri, contentValues);
+        return uri;
+    }
+
+    @Override
+    public int delete(
+            @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
+        // Stop Tracking the File of directory if it was tracked and delete it from the disk
+        mFileTracker.remove(uri);
+        File file = getFileForUri(uri);
+        int num = recursiveDelete(file);
+        return num;
+    }
+
+    @Override
+    public int update(
+            @NonNull Uri uri,
+            @Nullable ContentValues values,
+            @Nullable String selection,
+            @Nullable String[] selectionArgs) {
+        File file = getFileForUri(uri);
+        if (!file.exists()) {
+            Log.e(TAG, String.format("Update - File from uri: '%s' does not exists.", uri));
+            return 0;
+        }
+        if (mFileTracker.get(uri) == null) {
+            Log.e(
+                    TAG,
+                    String.format(
+                            "Update - File from uri: '%s' is not tracked yet, use insert.", uri));
+            return 0;
+        }
+        mFileTracker.put(uri, values);
+        return 1;
+    }
+
+    @Override
+    public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
+            throws FileNotFoundException {
+        final File file = getFileForUri(uri);
+        final int fileMode = modeToMode(mode);
+
+        if ((fileMode & ParcelFileDescriptor.MODE_CREATE) == ParcelFileDescriptor.MODE_CREATE) {
+            // If the file is being created, create all its parent directories that don't already
+            // exist.
+            file.getParentFile().mkdirs();
+            if (!mFileTracker.containsKey(uri)) {
+                // Track the file, if not already tracked.
+                mFileTracker.put(uri, new ContentValues());
+            }
+        }
+        return ParcelFileDescriptor.open(file, fileMode);
+    }
+
+    private Object[] getRow(String[] columns, File file, String metadata) {
+        Object[] values = new Object[columns.length];
+        for (int i = 0; i < columns.length; i++) {
+            values[i] = getColumnValue(columns[i], file, metadata);
+        }
+        return values;
+    }
+
+    private Object getColumnValue(String columnName, File file, String metadata) {
+        Object value = null;
+        if (COLUMN_NAME.equals(columnName)) {
+            value = file.getName();
+        } else if (COLUMN_ABSOLUTE_PATH.equals(columnName)) {
+            value = file.getAbsolutePath();
+        } else if (COLUMN_DIRECTORY.equals(columnName)) {
+            value = file.isDirectory();
+        } else if (COLUMN_METADATA.equals(columnName)) {
+            value = metadata;
+        } else if (COLUMN_MIME_TYPE.equals(columnName)) {
+            value = file.isDirectory() ? null : getType(file);
+        }
+        return value;
+    }
+
+    private String getType(@NonNull File file) {
+        final int lastDot = file.getName().lastIndexOf('.');
+        if (lastDot >= 0) {
+            final String extension = file.getName().substring(lastDot + 1);
+            final String mime = sMimeMap.getMimeTypeFromExtension(extension);
+            if (mime != null) {
+                return mime;
+            }
+        }
+
+        return "application/octet-stream";
+    }
+
+    private File getFileForUri(@NonNull Uri uri) {
+        // TODO: apply the /sdcard resolution to query() too.
+        String uriPath = uri.getPath();
+        try {
+            uriPath = URLDecoder.decode(uriPath, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+        if (uriPath.startsWith("/sdcard/")) {
+            uriPath =
+                    uriPath.replaceAll(
+                            "/sdcard", Environment.getExternalStorageDirectory().getAbsolutePath());
+        }
+        return new File(uriPath);
+    }
+
+    /** Copied from FileProvider.java. */
+    private static int modeToMode(String mode) {
+        int modeBits;
+        if ("r".equals(mode)) {
+            modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
+        } else if ("w".equals(mode) || "wt".equals(mode)) {
+            modeBits =
+                    ParcelFileDescriptor.MODE_WRITE_ONLY
+                            | ParcelFileDescriptor.MODE_CREATE
+                            | ParcelFileDescriptor.MODE_TRUNCATE;
+        } else if ("wa".equals(mode)) {
+            modeBits =
+                    ParcelFileDescriptor.MODE_WRITE_ONLY
+                            | ParcelFileDescriptor.MODE_CREATE
+                            | ParcelFileDescriptor.MODE_APPEND;
+        } else if ("rw".equals(mode)) {
+            modeBits = ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE;
+        } else if ("rwt".equals(mode)) {
+            modeBits =
+                    ParcelFileDescriptor.MODE_READ_WRITE
+                            | ParcelFileDescriptor.MODE_CREATE
+                            | ParcelFileDescriptor.MODE_TRUNCATE;
+        } else {
+            throw new IllegalArgumentException("Invalid mode: " + mode);
+        }
+        return modeBits;
+    }
+
+    /**
+     * Recursively delete given file or directory and all its contents.
+     *
+     * @param rootDir the directory or file to be deleted; can be null
+     * @return The number of deleted files.
+     */
+    private int recursiveDelete(File rootDir) {
+        int count = 0;
+        if (rootDir != null) {
+            if (rootDir.isDirectory()) {
+                File[] childFiles = rootDir.listFiles();
+                if (childFiles != null) {
+                    for (File child : childFiles) {
+                        count += recursiveDelete(child);
+                    }
+                }
+            }
+            rootDir.delete();
+            count++;
+        }
+        return count;
+    }
+
+    private void sortFilesByAbsolutePath(File[] files) {
+        Arrays.sort(
+                files,
+                new Comparator<File>() {
+                    @Override
+                    public int compare(File f1, File f2) {
+                        return f1.getAbsolutePath().compareTo(f2.getAbsolutePath());
+                    }
+                });
+    }
+}