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