Handle special named file in Content Provider

Encode the URI to make sure we can handle special named
files and push them to the right location.

Bug: 123529934
Test: unit tests
 atest TradefedContentProviderHostTest
Bug: 129840750

Change-Id: Ib01f5f0acdceb3164558acf1f5c90e2f7090b8f3
Merged-In: Ib01f5f0acdceb3164558acf1f5c90e2f7090b8f3
diff --git a/res/apks/contentprovider/TradefedContentProvider.apk b/res/apks/contentprovider/TradefedContentProvider.apk
index 22a4569..0f89d63 100644
--- a/res/apks/contentprovider/TradefedContentProvider.apk
+++ b/res/apks/contentprovider/TradefedContentProvider.apk
Binary files differ
diff --git a/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java b/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
index 09a4662..4c966a6 100644
--- a/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
+++ b/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
@@ -24,6 +24,7 @@
 import com.android.tradefed.util.StreamUtil;
 
 import com.google.common.base.Strings;
+import com.google.common.net.UrlEscapers;
 
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -31,6 +32,8 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
 import java.util.Set;
 
 /**
@@ -104,7 +107,7 @@
      * @throws DeviceNotAvailableException
      */
     public boolean deleteFile(String deviceFilePath) throws DeviceNotAvailableException {
-        String contentUri = createContentUri(deviceFilePath);
+        String contentUri = createEscapedContentUri(deviceFilePath);
         String deleteCommand =
                 String.format(
                         "content delete --user %d --uri %s", mDevice.getCurrentUser(), contentUri);
@@ -130,7 +133,7 @@
      */
     public boolean pullFile(String deviceFilePath, File localFile)
             throws DeviceNotAvailableException {
-        String contentUri = createContentUri(deviceFilePath);
+        String contentUri = createEscapedContentUri(deviceFilePath);
         String pullCommand =
                 String.format(
                         "content read --user %d --uri %s", mDevice.getCurrentUser(), contentUri);
@@ -178,7 +181,7 @@
             throw new IllegalArgumentException(
                     String.format("File '%s' to push does not exist.", fileToPush));
         }
-        String contentUri = createContentUri(deviceFilePath);
+        String contentUri = createEscapedContentUri(deviceFilePath);
         String pushCommand =
                 String.format(
                         "content write --user %d --uri %s", mDevice.getCurrentUser(), contentUri);
@@ -209,8 +212,19 @@
         return apkTempFile;
     }
 
-    /** Returns the full URI string for the given device path. */
-    private String createContentUri(String deviceFilePath) {
-        return String.format("%s/%s", CONTENT_PROVIDER_URI, deviceFilePath);
+    /**
+     * 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);
     }
 }
diff --git a/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java b/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
index 0b4e31a..6532a5f 100644
--- a/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
+++ b/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
@@ -99,9 +99,8 @@
                 .executeShellV2Command(
                         eq(
                                 "content delete --user 99 --uri "
-                                        + ContentProviderHandler.CONTENT_PROVIDER_URI
-                                        + "/"
-                                        + devicePath));
+                                        + ContentProviderHandler.createEscapedContentUri(
+                                                devicePath)));
         assertTrue(mProvider.deleteFile(devicePath));
     }
 
@@ -117,9 +116,8 @@
                 .executeShellV2Command(
                         eq(
                                 "content delete --user 99 --uri "
-                                        + ContentProviderHandler.CONTENT_PROVIDER_URI
-                                        + "/"
-                                        + devicePath));
+                                        + ContentProviderHandler.createEscapedContentUri(
+                                                devicePath)));
         assertFalse(mProvider.deleteFile(devicePath));
     }
 
@@ -135,9 +133,8 @@
                     .executeShellV2Command(
                             eq(
                                     "content write --user 99 --uri "
-                                            + ContentProviderHandler.CONTENT_PROVIDER_URI
-                                            + "/"
-                                            + devicePath),
+                                            + ContentProviderHandler.createEscapedContentUri(
+                                                    devicePath)),
                             eq(toPush));
             assertTrue(mProvider.pushFile(toPush, devicePath));
         } finally {
@@ -165,9 +162,7 @@
             assertEquals(
                     shellCommandCaptor.getValue(),
                     "content read --user 99 --uri "
-                            + ContentProviderHandler.CONTENT_PROVIDER_URI
-                            + "/"
-                            + devicePath);
+                            + ContentProviderHandler.createEscapedContentUri(devicePath));
         } finally {
             FileUtil.deleteFile(pullTo);
         }
@@ -203,6 +198,17 @@
         }
     }
 
+    @Test
+    public void testCreateUri() throws Exception {
+        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);
+    }
+
     private CommandResult mockSuccess() {
         CommandResult result = new CommandResult(CommandStatus.SUCCESS);
         result.setStderr("");
diff --git a/util-apps/ContentProvider/hostsidetests/src/com/android/tradefed/contentprovider/ContentProviderTest.java b/util-apps/ContentProvider/hostsidetests/src/com/android/tradefed/contentprovider/ContentProviderTest.java
index 8b56fc7..a20e713 100644
--- a/util-apps/ContentProvider/hostsidetests/src/com/android/tradefed/contentprovider/ContentProviderTest.java
+++ b/util-apps/ContentProvider/hostsidetests/src/com/android/tradefed/contentprovider/ContentProviderTest.java
@@ -61,6 +61,20 @@
         mToBeDeleted.clear();
     }
 
+    /** 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("/sdcard/" + tmpFile.getName()));
+        } finally {
+            FileUtil.deleteFile(tmpFile);
+        }
+    }
+
     @Test
     public void testPushFile() throws Exception {
         File tmpFile = FileUtil.createTempFile("tmpFileToPush", ".txt");
diff --git a/util-apps/ContentProvider/main/java/android/tradefed/contentprovider/ManagedFileContentProvider.java b/util-apps/ContentProvider/main/java/android/tradefed/contentprovider/ManagedFileContentProvider.java
index c82b736..2cb1c2e 100644
--- a/util-apps/ContentProvider/main/java/android/tradefed/contentprovider/ManagedFileContentProvider.java
+++ b/util-apps/ContentProvider/main/java/android/tradefed/contentprovider/ManagedFileContentProvider.java
@@ -29,6 +29,8 @@
 
 import java.io.File;
 import java.io.FileNotFoundException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -207,6 +209,11 @@
     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(