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());
+ }
+ });
+ }
+}