Fix chrome upload with content uri

For android, the upload file dialog returns files with content uri scheme(content://).
This CL makes it possible for upload to work with this new file type.
It fixes both the form and fileapi based uploads.

BUG=278640

Review URL: https://codereview.chromium.org/75533002

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@236192 0039d316-1c4b-4281-b951-d872f2087c98


CrOS-Libchrome-Original-Commit: f12d1e1552d00cc2f4f38a37460dedd4737c5b05
diff --git a/base/android/base_jni_registrar.cc b/base/android/base_jni_registrar.cc
index 86275b2..d4a6ee3 100644
--- a/base/android/base_jni_registrar.cc
+++ b/base/android/base_jni_registrar.cc
@@ -7,6 +7,7 @@
 #include "base/android/activity_status.h"
 #include "base/android/build_info.h"
 #include "base/android/command_line.h"
+#include "base/android/content_uri_utils.h"
 #include "base/android/cpu_features.h"
 #include "base/android/important_file_writer_android.h"
 #include "base/android/java_handler_thread.h"
@@ -36,6 +37,7 @@
 #if defined(GOOGLE_TV)
   { "ContextTypes", base::android::RegisterContextTypes },
 #endif
+  { "ContentUriUtils", base::RegisterContentUriUtils },
   { "CpuFeatures", base::android::RegisterCpuFeatures },
   { "ImportantFileWriterAndroid",
     base::android::RegisterImportantFileWriterAndroid },
diff --git a/base/android/content_uri_utils.cc b/base/android/content_uri_utils.cc
new file mode 100644
index 0000000..64d6ad2
--- /dev/null
+++ b/base/android/content_uri_utils.cc
@@ -0,0 +1,39 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/android/content_uri_utils.h"
+
+#include "base/android/jni_android.h"
+#include "base/android/jni_string.h"
+#include "base/platform_file.h"
+#include "jni/ContentUriUtils_jni.h"
+
+using base::android::ConvertUTF8ToJavaString;
+
+namespace base {
+
+bool RegisterContentUriUtils(JNIEnv* env) {
+  return RegisterNativesImpl(env);
+}
+
+bool ContentUriExists(const FilePath& content_uri) {
+  JNIEnv* env = base::android::AttachCurrentThread();
+  ScopedJavaLocalRef<jstring> j_uri =
+      ConvertUTF8ToJavaString(env, content_uri.value());
+  return Java_ContentUriUtils_contentUriExists(
+      env, base::android::GetApplicationContext(), j_uri.obj());
+}
+
+int OpenContentUriForRead(const FilePath& content_uri) {
+  JNIEnv* env = base::android::AttachCurrentThread();
+  ScopedJavaLocalRef<jstring> j_uri =
+      ConvertUTF8ToJavaString(env, content_uri.value());
+  jint fd = Java_ContentUriUtils_openContentUriForRead(
+      env, base::android::GetApplicationContext(), j_uri.obj());
+  if (fd < 0)
+    return base::kInvalidPlatformFileValue;
+  return fd;
+}
+
+}  // namespace base
diff --git a/base/android/content_uri_utils.h b/base/android/content_uri_utils.h
new file mode 100644
index 0000000..ec820ef
--- /dev/null
+++ b/base/android/content_uri_utils.h
@@ -0,0 +1,27 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef BASE_ANDROID_CONTENT_URI_UTILS_H_
+#define BASE_ANDROID_CONTENT_URI_UTILS_H_
+
+#include <jni.h>
+
+#include "base/base_export.h"
+#include "base/basictypes.h"
+#include "base/files/file_path.h"
+
+namespace base {
+
+bool RegisterContentUriUtils(JNIEnv* env);
+
+// Opens a content uri for read and returns the file descriptor to the caller.
+// Returns -1 if the uri is invalid.
+BASE_EXPORT int OpenContentUriForRead(const FilePath& content_uri);
+
+// Check whether a content uri exists.
+BASE_EXPORT bool ContentUriExists(const FilePath& content_uri);
+
+}  // namespace base
+
+#endif  // BASE_ANDROID_CONTENT_URI_UTILS_H_
diff --git a/base/android/java/src/org/chromium/base/ContentUriUtils.java b/base/android/java/src/org/chromium/base/ContentUriUtils.java
new file mode 100644
index 0000000..0203595
--- /dev/null
+++ b/base/android/java/src/org/chromium/base/ContentUriUtils.java
@@ -0,0 +1,76 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import org.chromium.base.CalledByNative;
+
+/**
+ * This class provides methods to access content URI schemes.
+ */
+abstract class ContentUriUtils {
+    private static final String TAG = "ContentUriUtils";
+
+    // Prevent instantiation.
+    private ContentUriUtils() {}
+
+    /**
+     * Opens the content URI for reading, and returns the file descriptor to
+     * the caller. The caller is responsible for closing the file desciptor.
+     *
+     * @param context {@link Context} in interest
+     * @param uriString the content URI to open
+     * @returns file desciptor upon sucess, or -1 otherwise.
+     */
+    @CalledByNative
+    public static int openContentUriForRead(Context context, String uriString) {
+        ParcelFileDescriptor pfd = getParcelFileDescriptor(context, uriString);
+        if (pfd != null) {
+            return pfd.detachFd();
+        }
+        return -1;
+    }
+
+    /**
+     * Check whether a content URI exists.
+     *
+     * @param context {@link Context} in interest.
+     * @param uriString the content URI to query.
+     * @returns true if the uri exists, or false otherwise.
+     */
+    @CalledByNative
+    public static boolean contentUriExists(Context context, String uriString) {
+        ParcelFileDescriptor pfd = getParcelFileDescriptor(context, uriString);
+        if (pfd == null) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Helper method to open a content URI and return the ParcelFileDescriptor.
+     *
+     * @param context {@link Context} in interest.
+     * @param uriString the content URI to open.
+     * @returns ParcelFileDescriptor of the content URI, or NULL if the file does not exist.
+     */
+    private static ParcelFileDescriptor getParcelFileDescriptor(Context context, String uriString) {
+        ContentResolver resolver = context.getContentResolver();
+        Uri uri = Uri.parse(uriString);
+
+        ParcelFileDescriptor pfd = null;
+        try {
+            pfd = resolver.openFileDescriptor(uri, "r");
+        } catch (java.io.FileNotFoundException e) {
+            Log.w(TAG, "Cannot find content uri: " + uriString, e);
+        }
+        return pfd;
+    }
+}
diff --git a/base/base.gyp b/base/base.gyp
index d0ca483..10c6577 100644
--- a/base/base.gyp
+++ b/base/base.gyp
@@ -896,6 +896,15 @@
             'test/test_file_util_linux.cc',
           ],
         }],
+        ['OS == "android"', {
+          'dependencies': [
+            'base_unittests_jni_headers',
+            'base_java_unittest_support',
+          ],
+          'include_dirs': [
+            '<(SHARED_INTERMEDIATE_DIR)/base',
+          ],
+        }],
       ],
       'sources': [
         'test/expectations/expectation.cc',
@@ -948,6 +957,7 @@
         'test/task_runner_test_template.h',
         'test/test_file_util.cc',
         'test/test_file_util.h',
+        'test/test_file_util_android.cc',
         'test/test_file_util_linux.cc',
         'test/test_file_util_mac.cc',
         'test/test_file_util_posix.cc',
@@ -1215,6 +1225,7 @@
             'android/java/src/org/chromium/base/ActivityStatus.java',
             'android/java/src/org/chromium/base/BuildInfo.java',
             'android/java/src/org/chromium/base/CommandLine.java',
+            'android/java/src/org/chromium/base/ContentUriUtils.java',
             'android/java/src/org/chromium/base/CpuFeatures.java',
             'android/java/src/org/chromium/base/ImportantFileWriterAndroid.java',
             'android/java/src/org/chromium/base/MemoryPressureListener.java',
@@ -1240,6 +1251,17 @@
           'includes': [ '../build/jni_generator.gypi' ],
         },
         {
+          'target_name': 'base_unittests_jni_headers',
+          'type': 'none',
+          'sources': [
+            'test/android/java/src/org/chromium/base/ContentUriTestUtils.java',
+          ],
+          'variables': {
+            'jni_gen_package': 'base',
+          },
+          'includes': [ '../build/jni_generator.gypi' ],
+        },
+        {
           'target_name': 'base_java',
           'type': 'none',
           'variables': {
@@ -1259,6 +1281,17 @@
           ],
         },
         {
+          'target_name': 'base_java_unittest_support',
+          'type': 'none',
+          'dependencies': [
+            'base_java',
+          ],
+          'variables': {
+            'java_in_dir': '../base/test/android/java',
+          },
+          'includes': [ '../build/java.gypi' ],
+        },
+        {
           'target_name': 'base_java_activity_state',
           'type': 'none',
           # This target is used to auto-generate ActivityState.java
diff --git a/base/base.gypi b/base/base.gypi
index a0dd681..74d83c0 100644
--- a/base/base.gypi
+++ b/base/base.gypi
@@ -41,6 +41,8 @@
           'android/build_info.h',
           'android/command_line.cc',
           'android/command_line.h',
+          'android/content_uri_utils.cc',
+          'android/content_uri_utils.h',
           'android/cpu_features.cc',
           'android/fifo_utils.cc',
           'android/fifo_utils.h',
diff --git a/base/file_util_posix.cc b/base/file_util_posix.cc
index 134935c..721f858 100644
--- a/base/file_util_posix.cc
+++ b/base/file_util_posix.cc
@@ -48,6 +48,7 @@
 #include "base/time/time.h"
 
 #if defined(OS_ANDROID)
+#include "base/android/content_uri_utils.h"
 #include "base/os_compat_android.h"
 #endif
 
@@ -79,6 +80,12 @@
   ThreadRestrictions::AssertIOAllowed();
   return lstat64(path, sb);
 }
+#if defined(OS_ANDROID)
+static int CallFstat(int fd, stat_wrapper_t *sb) {
+  ThreadRestrictions::AssertIOAllowed();
+  return fstat64(fd, sb);
+}
+#endif
 #endif
 
 // Helper for NormalizeFilePath(), defined below.
@@ -308,6 +315,11 @@
 
 bool PathExists(const FilePath& path) {
   ThreadRestrictions::AssertIOAllowed();
+#if defined(OS_ANDROID)
+  if (path.IsContentUri()) {
+    return ContentUriExists(path);
+  }
+#endif
   return access(path.value().c_str(), F_OK) == 0;
 }
 
@@ -569,8 +581,21 @@
 
 bool GetFileInfo(const FilePath& file_path, base::PlatformFileInfo* results) {
   stat_wrapper_t file_info;
-  if (CallStat(file_path.value().c_str(), &file_info) != 0)
-    return false;
+#if defined(OS_ANDROID)
+  if (file_path.IsContentUri()) {
+    int fd = OpenContentUriForRead(file_path);
+    if (fd < 0)
+      return false;
+    ScopedFD scoped_fd(&fd);
+    if (base::CallFstat(fd, &file_info) != 0)
+      return false;
+  } else {
+#endif  // defined(OS_ANDROID)
+    if (CallStat(file_path.value().c_str(), &file_info) != 0)
+      return false;
+#if defined(OS_ANDROID)
+  }
+#endif  // defined(OS_ANDROID)
   results->is_directory = S_ISDIR(file_info.st_mode);
   results->size = file_info.st_size;
 #if defined(OS_MACOSX)
diff --git a/base/file_util_unittest.cc b/base/file_util_unittest.cc
index 1ca70b4..3594749 100644
--- a/base/file_util_unittest.cc
+++ b/base/file_util_unittest.cc
@@ -33,6 +33,10 @@
 #include "base/win/windows_version.h"
 #endif
 
+#if defined(OS_ANDROID)
+#include "base/android/content_uri_utils.h"
+#endif
+
 // This macro helps avoid wrapped lines in the test structs.
 #define FPL(x) FILE_PATH_LITERAL(x)
 
@@ -2319,6 +2323,52 @@
           sub_dir_, text_file_, uid_, ok_gids_));
 }
 
+#if defined(OS_ANDROID)
+TEST_F(FileUtilTest, ValidContentUriTest) {
+  // Get the test image path.
+  FilePath data_dir;
+  ASSERT_TRUE(PathService::Get(base::DIR_TEST_DATA, &data_dir));
+  data_dir = data_dir.AppendASCII("file_util");
+  ASSERT_TRUE(base::PathExists(data_dir));
+  FilePath image_file = data_dir.Append(FILE_PATH_LITERAL("red.png"));
+  int64 image_size;
+  file_util::GetFileSize(image_file, &image_size);
+  EXPECT_LT(0, image_size);
+
+  // Insert the image into MediaStore. MediaStore will do some conversions, and
+  // return the content URI.
+  base::FilePath path = file_util::InsertImageIntoMediaStore(image_file);
+  EXPECT_TRUE(path.IsContentUri());
+  EXPECT_TRUE(base::PathExists(path));
+  // The file size may not equal to the input image as MediaStore may convert
+  // the image.
+  int64 content_uri_size;
+  file_util::GetFileSize(path, &content_uri_size);
+  EXPECT_EQ(image_size, content_uri_size);
+
+  // We should be able to read the file.
+  char* buffer = new char[image_size];
+  int fd = base::OpenContentUriForRead(path);
+  EXPECT_LT(0, fd);
+  EXPECT_TRUE(file_util::ReadFromFD(fd, buffer, image_size));
+  delete[] buffer;
+}
+
+TEST_F(FileUtilTest, NonExistentContentUriTest) {
+  base::FilePath path("content://foo.bar");
+  EXPECT_TRUE(path.IsContentUri());
+  EXPECT_FALSE(base::PathExists(path));
+  // Size should be smaller than 0.
+  int64 size;
+  file_util::GetFileSize(path, &size);
+  EXPECT_GT(0, size);
+
+  // We should not be able to read the file.
+  int fd = base::OpenContentUriForRead(path);
+  EXPECT_EQ(-1, fd);
+}
+#endif
+
 #endif  // defined(OS_POSIX)
 
 }  // namespace
diff --git a/base/files/file_path.cc b/base/files/file_path.cc
index cfae3a5..4cfa2e6 100644
--- a/base/files/file_path.cc
+++ b/base/files/file_path.cc
@@ -1280,6 +1280,12 @@
 #endif
 }
 
+#if defined(OS_ANDROID)
+bool FilePath::IsContentUri() const {
+  return StartsWithASCII(path_, "content://", false /*case_sensitive*/);
+}
+#endif
+
 }  // namespace base
 
 void PrintTo(const base::FilePath& path, std::ostream* out) {
diff --git a/base/files/file_path.h b/base/files/file_path.h
index 4d03da4..33beb0b 100644
--- a/base/files/file_path.h
+++ b/base/files/file_path.h
@@ -387,6 +387,15 @@
                                    const StringType& string2);
 #endif
 
+#if defined(OS_ANDROID)
+  // On android, file selection dialog can return a file with content uri
+  // scheme(starting with content://). Content uri needs to be opened with
+  // ContentResolver to guarantee that the app has appropriate permissions
+  // to access it.
+  // Returns true if the path is a content uri, or false otherwise.
+  bool IsContentUri() const;
+#endif
+
  private:
   // Remove trailing separators from this object.  If the path is absolute, it
   // will never be stripped any more than to refer to the absolute root
diff --git a/base/files/file_path_unittest.cc b/base/files/file_path_unittest.cc
index 8b2fcf5..1b6e465 100644
--- a/base/files/file_path_unittest.cc
+++ b/base/files/file_path_unittest.cc
@@ -1228,4 +1228,33 @@
   }
 }
 
+#if defined(OS_ANDROID)
+TEST_F(FilePathTest, ContentUriTest) {
+  const struct UnaryBooleanTestData cases[] = {
+    { FPL("content://foo.bar"),    true },
+    { FPL("content://foo.bar/"),   true },
+    { FPL("content://foo/bar"),    true },
+    { FPL("CoNTenT://foo.bar"),    true },
+    { FPL("content://"),           true },
+    { FPL("content:///foo.bar"),   true },
+    { FPL("content://3foo/bar"),   true },
+    { FPL("content://_foo/bar"),   true },
+    { FPL(".. "),                  false },
+    { FPL("foo.bar"),              false },
+    { FPL("content:foo.bar"),      false },
+    { FPL("content:/foo.ba"),      false },
+    { FPL("content:/dir/foo.bar"), false },
+    { FPL("content: //foo.bar"),   false },
+    { FPL("content%2a%2f%2f"),     false },
+  };
+
+  for (size_t i = 0; i < arraysize(cases); ++i) {
+    FilePath input(cases[i].input);
+    bool observed = input.IsContentUri();
+    EXPECT_EQ(cases[i].expected, observed) <<
+              "i: " << i << ", input: " << input.value();
+  }
+}
+#endif
+
 }  // namespace base
diff --git a/base/test/android/java/src/org/chromium/base/ContentUriTestUtils.java b/base/test/android/java/src/org/chromium/base/ContentUriTestUtils.java
new file mode 100644
index 0000000..6b94635
--- /dev/null
+++ b/base/test/android/java/src/org/chromium/base/ContentUriTestUtils.java
@@ -0,0 +1,51 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore;
+
+import org.chromium.base.CalledByNative;
+
+/**
+ * Utilities for testing operations on content URI.
+ */
+public class ContentUriTestUtils {
+    /**
+     * Insert an image into the MediaStore, and return the content URI. If the
+     * image already exists in the MediaStore, just retrieve the URI.
+     *
+     * @param context Application context.
+     * @param path Path to the image file.
+     * @return Content URI of the image.
+     */
+    @CalledByNative
+    private static String insertImageIntoMediaStore(Context context, String path) {
+        // Check whether the content URI exists.
+        Cursor c = context.getContentResolver().query(
+                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+                new String[] { MediaStore.Video.VideoColumns._ID },
+                MediaStore.Images.Media.DATA + " LIKE ?",
+                new String[] { path },
+                null);
+        if (c != null && c.getCount() > 0) {
+            c.moveToFirst();
+            int id = c.getInt(0);
+            return Uri.withAppendedPath(
+                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "" + id).toString();
+        }
+
+        // Insert the content URI into MediaStore.
+        ContentValues values = new ContentValues();
+        values.put(MediaStore.MediaColumns.DATA, path);
+        Uri uri = context.getContentResolver().insert(
+                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
+        return uri.toString();
+    }
+}
diff --git a/base/test/data/file_util/red.png b/base/test/data/file_util/red.png
new file mode 100644
index 0000000..0806141
--- /dev/null
+++ b/base/test/data/file_util/red.png
Binary files differ
diff --git a/base/test/run_all_unittests.cc b/base/test/run_all_unittests.cc
index e561f0e..3b5ebfe 100644
--- a/base/test/run_all_unittests.cc
+++ b/base/test/run_all_unittests.cc
@@ -7,6 +7,11 @@
 #include "base/test/launcher/unit_test_launcher.h"
 #include "base/test/test_suite.h"
 
+#if defined(OS_ANDROID)
+#include "base/android/jni_android.h"
+#include "base/test/test_file_util.h"
+#endif
+
 namespace {
 
 class NoAtExitBaseTestSuite : public base::TestSuite {
@@ -23,7 +28,10 @@
 }  // namespace
 
 int main(int argc, char** argv) {
-#if !defined(OS_ANDROID)
+#if defined(OS_ANDROID)
+  JNIEnv* env = base::android::AttachCurrentThread();
+  file_util::RegisterContentUriTestUtils(env);
+#else
   base::AtExitManager at_exit;
 #endif
   return base::LaunchUnitTests(argc,
diff --git a/base/test/test_file_util.h b/base/test/test_file_util.h
index cf20221..656babd 100644
--- a/base/test/test_file_util.h
+++ b/base/test/test_file_util.h
@@ -12,6 +12,11 @@
 #include "base/compiler_specific.h"
 #include "base/files/file_path.h"
 
+#if defined(OS_ANDROID)
+#include <jni.h>
+#include "base/basictypes.h"
+#endif
+
 namespace base {
 
 class FilePath;
@@ -58,6 +63,15 @@
 bool MakeFileUnreadable(const base::FilePath& path) WARN_UNUSED_RESULT;
 bool MakeFileUnwritable(const base::FilePath& path) WARN_UNUSED_RESULT;
 
+#if defined(OS_ANDROID)
+// Register the ContentUriTestUrils JNI bindings.
+bool RegisterContentUriTestUtils(JNIEnv* env);
+
+// Insert an image file into the MediaStore, and retrieve the content URI for
+// testing purpose.
+base::FilePath InsertImageIntoMediaStore(const base::FilePath& path);
+#endif  // defined(OS_ANDROID)
+
 // Saves the current permissions for a path, and restores it on destruction.
 class PermissionRestorer {
  public:
diff --git a/base/test/test_file_util_android.cc b/base/test/test_file_util_android.cc
new file mode 100644
index 0000000..c17f669
--- /dev/null
+++ b/base/test/test_file_util_android.cc
@@ -0,0 +1,29 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/test/test_file_util.h"
+
+#include "base/android/jni_android.h"
+#include "base/android/jni_string.h"
+#include "base/files/file_path.h"
+#include "jni/ContentUriTestUtils_jni.h"
+
+namespace file_util {
+
+bool RegisterContentUriTestUtils(JNIEnv* env) {
+  return RegisterNativesImpl(env);
+}
+
+base::FilePath InsertImageIntoMediaStore(const base::FilePath& path) {
+  JNIEnv* env = base::android::AttachCurrentThread();
+  ScopedJavaLocalRef<jstring> j_path =
+      base::android::ConvertUTF8ToJavaString(env, path.value());
+  ScopedJavaLocalRef<jstring> j_uri =
+      Java_ContentUriTestUtils_insertImageIntoMediaStore(
+          env, base::android::GetApplicationContext(), j_path.obj());
+  std::string uri = base::android::ConvertJavaStringToUTF8(j_uri);
+  return base::FilePath(uri);
+}
+
+}  // namespace file_util