diff options
author | mnaganov@chromium.org <mnaganov@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-12 08:36:08 +0000 |
---|---|---|
committer | mnaganov@chromium.org <mnaganov@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-12 08:36:08 +0000 |
commit | 869365302661dbf642e6be8e8265862496dbae5c (patch) | |
tree | 8d865e7ffce75687c89d4add9a06b435cff05ab4 | |
parent | 62115ba4d4bbeadcd491cd3cfdacd292d4bf34f8 (diff) | |
download | chromium_src-869365302661dbf642e6be8e8265862496dbae5c.zip chromium_src-869365302661dbf642e6be8e8265862496dbae5c.tar.gz chromium_src-869365302661dbf642e6be8e8265862496dbae5c.tar.bz2 |
[Android] Java Bridge with Gin: implement Java methods invocation
This patch adds GinJavaMethodInvocationHelper class which serves
for coercion of arguments and return values of Java methods to / from
base::Value and GinJavaBridgeValue.
The coercion code is taken from the existing implementation
(JavaBoundObject) with required conversions to use base::Value and
GinJavaBridgeValue instead of NPVARIANT and friends.
There are extensive Java tests for coercion, so here we only add some
unit tests for edge cases.
This patch also adds a trivial EventLog class for writing into Android
Event Log from C++ code.
BUG=355644
R=bulach@chromium.org, torne@chromium.org
TBR=brettw@chromium.org
Review URL: https://codereview.chromium.org/302173006
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@276586 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | base/BUILD.gn | 3 | ||||
-rw-r--r-- | base/android/base_jni_registrar.cc | 2 | ||||
-rw-r--r-- | base/android/event_log.cc | 20 | ||||
-rw-r--r-- | base/android/event_log.h | 22 | ||||
-rw-r--r-- | base/android/java/src/org/chromium/base/EventLog.java | 17 | ||||
-rw-r--r-- | base/android/jni_weak_ref.h | 2 | ||||
-rw-r--r-- | base/base.gyp | 1 | ||||
-rw-r--r-- | base/base.gypi | 2 | ||||
-rw-r--r-- | content/browser/renderer_host/java/gin_java_bound_object.h | 27 | ||||
-rw-r--r-- | content/browser/renderer_host/java/gin_java_method_invocation_helper.cc | 310 | ||||
-rw-r--r-- | content/browser/renderer_host/java/gin_java_method_invocation_helper.h | 113 | ||||
-rw-r--r-- | content/browser/renderer_host/java/gin_java_method_invocation_helper_unittest.cc | 280 | ||||
-rw-r--r-- | content/browser/renderer_host/java/gin_java_script_to_java_types_coercion.cc | 705 | ||||
-rw-r--r-- | content/browser/renderer_host/java/gin_java_script_to_java_types_coercion.h | 33 | ||||
-rw-r--r-- | content/content_browser.gypi | 5 | ||||
-rw-r--r-- | content/content_tests.gypi | 1 |
16 files changed, 1543 insertions, 0 deletions
diff --git a/base/BUILD.gn b/base/BUILD.gn index 7b11288..53084d5 100644 --- a/base/BUILD.gn +++ b/base/BUILD.gn @@ -28,6 +28,8 @@ component("base") { "android/content_uri_utils.cc", "android/content_uri_utils.h", "android/cpu_features.cc", + "android/event_log.cc", + "android/event_log.h", "android/fifo_utils.cc", "android/fifo_utils.h", "android/important_file_writer_android.cc", @@ -1317,6 +1319,7 @@ if (is_android) { "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/EventLog.java", "android/java/src/org/chromium/base/ImportantFileWriterAndroid.java", "android/java/src/org/chromium/base/library_loader/LibraryLoader.java", "android/java/src/org/chromium/base/MemoryPressureListener.java", diff --git a/base/android/base_jni_registrar.cc b/base/android/base_jni_registrar.cc index e14fd44..97c8fcf 100644 --- a/base/android/base_jni_registrar.cc +++ b/base/android/base_jni_registrar.cc @@ -9,6 +9,7 @@ #include "base/android/command_line_android.h" #include "base/android/content_uri_utils.h" #include "base/android/cpu_features.h" +#include "base/android/event_log.h" #include "base/android/important_file_writer_android.h" #include "base/android/java_handler_thread.h" #include "base/android/jni_android.h" @@ -34,6 +35,7 @@ static RegistrationMethod kBaseRegisteredMethods[] = { { "CommandLine", base::android::RegisterCommandLine }, { "ContentUriUtils", base::RegisterContentUriUtils }, { "CpuFeatures", base::android::RegisterCpuFeatures }, + { "EventLog", base::android::RegisterEventLog }, { "ImportantFileWriterAndroid", base::android::RegisterImportantFileWriterAndroid }, { "MemoryPressureListenerAndroid", diff --git a/base/android/event_log.cc b/base/android/event_log.cc new file mode 100644 index 0000000..a4b1dd1 --- /dev/null +++ b/base/android/event_log.cc @@ -0,0 +1,20 @@ +// 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/event_log.h" +#include "jni/EventLog_jni.h" + +namespace base { +namespace android { + +void EventLogWriteInt(int tag, int value) { + Java_EventLog_writeEvent(AttachCurrentThread(), tag, value); +} + +bool RegisterEventLog(JNIEnv* env) { + return RegisterNativesImpl(env); +} + +} // namespace android +} // namespace base diff --git a/base/android/event_log.h b/base/android/event_log.h new file mode 100644 index 0000000..dad4e4c --- /dev/null +++ b/base/android/event_log.h @@ -0,0 +1,22 @@ +// Copyright 2014 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_EVENT_LOG_H_ +#define BASE_ANDROID_EVENT_LOG_H_ + +#include <jni.h> + +#include "base/base_export.h" + +namespace base { +namespace android { + +void BASE_EXPORT EventLogWriteInt(int tag, int value); + +bool RegisterEventLog(JNIEnv* env); + +} // namespace android +} // namespace base + +#endif // BASE_ANDROID_EVENT_LOG_H_ diff --git a/base/android/java/src/org/chromium/base/EventLog.java b/base/android/java/src/org/chromium/base/EventLog.java new file mode 100644 index 0000000..894de15 --- /dev/null +++ b/base/android/java/src/org/chromium/base/EventLog.java @@ -0,0 +1,17 @@ +// Copyright 2014 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; + +/** + * A simple interface to Android's EventLog to be used by native code. + */ +@JNINamespace("base::android") +public class EventLog { + + @CalledByNative + public static void writeEvent(int tag, int value) { + android.util.EventLog.writeEvent(tag, value); + } +} diff --git a/base/android/jni_weak_ref.h b/base/android/jni_weak_ref.h index ef39b8a..c851046 100644 --- a/base/android/jni_weak_ref.h +++ b/base/android/jni_weak_ref.h @@ -25,6 +25,8 @@ class BASE_EXPORT JavaObjectWeakGlobalRef { base::android::ScopedJavaLocalRef<jobject> get(JNIEnv* env) const; + bool is_empty() const { return obj_ == NULL; } + void reset(); private: diff --git a/base/base.gyp b/base/base.gyp index cf00752..4353509 100644 --- a/base/base.gyp +++ b/base/base.gyp @@ -1282,6 +1282,7 @@ '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/EventLog.java', 'android/java/src/org/chromium/base/ImportantFileWriterAndroid.java', 'android/java/src/org/chromium/base/library_loader/LibraryLoader.java', 'android/java/src/org/chromium/base/MemoryPressureListener.java', diff --git a/base/base.gypi b/base/base.gypi index 87bab04..274dc3d 100644 --- a/base/base.gypi +++ b/base/base.gypi @@ -37,6 +37,8 @@ 'android/content_uri_utils.cc', 'android/content_uri_utils.h', 'android/cpu_features.cc', + 'android/event_log.cc', + 'android/event_log.h', 'android/fifo_utils.cc', 'android/fifo_utils.h', 'android/important_file_writer_android.cc', diff --git a/content/browser/renderer_host/java/gin_java_bound_object.h b/content/browser/renderer_host/java/gin_java_bound_object.h new file mode 100644 index 0000000..522a48a --- /dev/null +++ b/content/browser/renderer_host/java/gin_java_bound_object.h @@ -0,0 +1,27 @@ +// Copyright 2014 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 CONTENT_BROWSER_RENDERER_HOST_JAVA_GIN_JAVA_BOUND_OBJECT_H_ +#define CONTENT_BROWSER_RENDERER_HOST_JAVA_GIN_JAVA_BOUND_OBJECT_H_ + +#include "base/id_map.h" +#include "base/memory/ref_counted.h" + +namespace content { + +class GinJavaBoundObject + : public base::RefCountedThreadSafe<GinJavaBoundObject> { + public: + typedef IDMap<scoped_refptr<GinJavaBoundObject>, IDMapOwnPointer> ObjectMap; + typedef ObjectMap::KeyType ObjectID; + + private: + friend class base::RefCountedThreadSafe<GinJavaBoundObject>; + GinJavaBoundObject() {} + ~GinJavaBoundObject() {} +}; + +} // namespace content + +#endif // CONTENT_BROWSER_RENDERER_HOST_JAVA_GIN_JAVA_BOUND_OBJECT_H_ diff --git a/content/browser/renderer_host/java/gin_java_method_invocation_helper.cc b/content/browser/renderer_host/java/gin_java_method_invocation_helper.cc new file mode 100644 index 0000000..5c8abb6 --- /dev/null +++ b/content/browser/renderer_host/java/gin_java_method_invocation_helper.cc @@ -0,0 +1,310 @@ +// Copyright 2014 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 "content/browser/renderer_host/java/gin_java_method_invocation_helper.h" + +#include <unistd.h> + +#include "base/android/event_log.h" +#include "base/android/jni_android.h" +#include "base/android/jni_string.h" +#include "base/float_util.h" +#include "content/browser/renderer_host/java/gin_java_script_to_java_types_coercion.h" +#include "content/browser/renderer_host/java/java_method.h" +#include "content/browser/renderer_host/java/jni_helper.h" +#include "content/common/android/gin_java_bridge_value.h" +#include "content/public/browser/browser_thread.h" + +using base::android::AttachCurrentThread; +using base::android::ScopedJavaLocalRef; + +namespace content { + +namespace { + +const char kObjectIsGone[] = "Java object is gone"; +const char kMethodNotFound[] = "Method not found"; +const char kAccessToObjectGetClassIsBlocked[] = + "Access to java.lang.Object.getClass is blocked"; +const char kJavaExceptionRaised[] = + "Java exception has been raised during method invocation"; + +// See frameworks/base/core/java/android/webkit/EventLogTags.logtags +const int kObjectGetClassInvocationAttemptLogTag = 70151; + +} // namespace + +GinJavaMethodInvocationHelper::GinJavaMethodInvocationHelper( + scoped_ptr<ObjectDelegate> object, + const std::string& method_name, + const base::ListValue& arguments) + : object_(object.Pass()), + method_name_(method_name), + arguments_(arguments.DeepCopy()) { +} + +GinJavaMethodInvocationHelper::~GinJavaMethodInvocationHelper() {} + +void GinJavaMethodInvocationHelper::Init(DispatcherDelegate* dispatcher) { + // Build on the UI thread a map of object_id -> WeakRef for Java objects from + // |arguments_|. Then we can use this map on the background thread without + // accessing |dispatcher|. + BuildObjectRefsFromListValue(dispatcher, arguments_.get()); +} + +// As V8ValueConverter has finite recursion depth when serializing +// JavaScript values, we don't bother about having a recursion threshold here. +void GinJavaMethodInvocationHelper::BuildObjectRefsFromListValue( + DispatcherDelegate* dispatcher, + const base::Value* list_value) { + DCHECK(list_value->IsType(base::Value::TYPE_LIST)); + const base::ListValue* list; + list_value->GetAsList(&list); + for (base::ListValue::const_iterator iter = list->begin(); + iter != list->end(); + ++iter) { + if (AppendObjectRef(dispatcher, *iter)) + continue; + if ((*iter)->IsType(base::Value::TYPE_LIST)) { + BuildObjectRefsFromListValue(dispatcher, *iter); + } else if ((*iter)->IsType(base::Value::TYPE_DICTIONARY)) { + BuildObjectRefsFromDictionaryValue(dispatcher, *iter); + } + } +} + +void GinJavaMethodInvocationHelper::BuildObjectRefsFromDictionaryValue( + DispatcherDelegate* dispatcher, + const base::Value* dict_value) { + DCHECK(dict_value->IsType(base::Value::TYPE_DICTIONARY)); + const base::DictionaryValue* dict; + dict_value->GetAsDictionary(&dict); + for (base::DictionaryValue::Iterator iter(*dict); + !iter.IsAtEnd(); + iter.Advance()) { + if (AppendObjectRef(dispatcher, &iter.value())) + continue; + if (iter.value().IsType(base::Value::TYPE_LIST)) { + BuildObjectRefsFromListValue(dispatcher, &iter.value()); + } else if (iter.value().IsType(base::Value::TYPE_DICTIONARY)) { + BuildObjectRefsFromDictionaryValue(dispatcher, &iter.value()); + } + } +} + +bool GinJavaMethodInvocationHelper::AppendObjectRef( + DispatcherDelegate* dispatcher, + const base::Value* raw_value) { + if (!GinJavaBridgeValue::ContainsGinJavaBridgeValue(raw_value)) + return false; + scoped_ptr<const GinJavaBridgeValue> value( + GinJavaBridgeValue::FromValue(raw_value)); + if (!value->IsType(GinJavaBridgeValue::TYPE_OBJECT_ID)) + return false; + GinJavaBoundObject::ObjectID object_id; + if (value->GetAsObjectID(&object_id)) { + ObjectRefs::iterator iter = object_refs_.find(object_id); + if (iter == object_refs_.end()) { + JavaObjectWeakGlobalRef object_ref( + dispatcher->GetObjectWeakRef(object_id)); + if (!object_ref.is_empty()) { + object_refs_.insert(std::make_pair(object_id, object_ref)); + } + } + } + return true; +} + +void GinJavaMethodInvocationHelper::Invoke() { + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef<jobject> obj(object_->GetLocalRef(env)); + if (obj.is_null()) { + SetInvocationFailure(kObjectIsGone); + return; + } + const JavaMethod* method = + object_->FindMethod(method_name_, arguments_->GetSize()); + if (!method) { + SetInvocationFailure(kMethodNotFound); + return; + } + + if (object_->IsObjectGetClassMethod(method)) { + base::android::EventLogWriteInt(kObjectGetClassInvocationAttemptLogTag, + getuid()); + SetInvocationFailure(kAccessToObjectGetClassIsBlocked); + return; + } + + std::vector<jvalue> parameters(method->num_parameters()); + for (size_t i = 0; i < method->num_parameters(); ++i) { + const base::Value* argument; + arguments_->Get(i, &argument); + parameters[i] = CoerceJavaScriptValueToJavaValue( + env, argument, method->parameter_type(i), true, object_refs_); + } + InvokeMethod(obj.obj(), method->return_type(), method->id(), ¶meters[0]); + + // Now that we're done with the jvalue, release any local references created + // by CoerceJavaScriptValueToJavaValue(). + for (size_t i = 0; i < method->num_parameters(); ++i) { + ReleaseJavaValueIfRequired(env, ¶meters[i], method->parameter_type(i)); + } +} + +void GinJavaMethodInvocationHelper::SetInvocationFailure( + const char* error_message) { + holds_primitive_result_ = true; + primitive_result_.reset(new base::ListValue()); + error_message_ = error_message; +} + +void GinJavaMethodInvocationHelper::SetPrimitiveResult( + const base::ListValue& result_wrapper) { + holds_primitive_result_ = true; + primitive_result_.reset(result_wrapper.DeepCopy()); +} + +void GinJavaMethodInvocationHelper::SetObjectResult( + const base::android::JavaRef<jobject>& object, + const base::android::JavaRef<jclass>& safe_annotation_clazz) { + holds_primitive_result_ = false; + object_result_.Reset(object); + safe_annotation_clazz_.Reset(safe_annotation_clazz); +} + +bool GinJavaMethodInvocationHelper::HoldsPrimitiveResult() { + return holds_primitive_result_; +} + +const base::ListValue& GinJavaMethodInvocationHelper::GetPrimitiveResult() { + return *primitive_result_.get(); +} + +const base::android::JavaRef<jobject>& +GinJavaMethodInvocationHelper::GetObjectResult() { + return object_result_; +} + +const base::android::JavaRef<jclass>& +GinJavaMethodInvocationHelper::GetSafeAnnotationClass() { + return safe_annotation_clazz_; +} + +const std::string& GinJavaMethodInvocationHelper::GetErrorMessage() { + return error_message_; +} + +void GinJavaMethodInvocationHelper::InvokeMethod(jobject object, + const JavaType& return_type, + jmethodID id, + jvalue* parameters) { + JNIEnv* env = AttachCurrentThread(); + base::ListValue result_wrapper; + switch (return_type.type) { + case JavaType::TypeBoolean: + result_wrapper.AppendBoolean( + env->CallBooleanMethodA(object, id, parameters)); + break; + case JavaType::TypeByte: + result_wrapper.AppendInteger( + env->CallByteMethodA(object, id, parameters)); + break; + case JavaType::TypeChar: + result_wrapper.AppendInteger( + env->CallCharMethodA(object, id, parameters)); + break; + case JavaType::TypeShort: + result_wrapper.AppendInteger( + env->CallShortMethodA(object, id, parameters)); + break; + case JavaType::TypeInt: + result_wrapper.AppendInteger( + env->CallIntMethodA(object, id, parameters)); + break; + case JavaType::TypeLong: + result_wrapper.AppendDouble( + env->CallLongMethodA(object, id, parameters)); + break; + case JavaType::TypeFloat: { + float result = env->CallFloatMethodA(object, id, parameters); + if (base::IsFinite(result)) { + result_wrapper.AppendDouble(result); + } else { + result_wrapper.Append( + GinJavaBridgeValue::CreateNonFiniteValue(result).release()); + } + break; + } + case JavaType::TypeDouble: { + double result = env->CallDoubleMethodA(object, id, parameters); + if (base::IsFinite(result)) { + result_wrapper.AppendDouble(result); + } else { + result_wrapper.Append( + GinJavaBridgeValue::CreateNonFiniteValue(result).release()); + } + break; + } + case JavaType::TypeVoid: + env->CallVoidMethodA(object, id, parameters); + result_wrapper.Append( + GinJavaBridgeValue::CreateUndefinedValue().release()); + break; + case JavaType::TypeArray: + // LIVECONNECT_COMPLIANCE: Existing behavior is to not call methods that + // return arrays. Spec requires calling the method and converting the + // result to a JavaScript array. + result_wrapper.Append( + GinJavaBridgeValue::CreateUndefinedValue().release()); + break; + case JavaType::TypeString: { + jstring java_string = static_cast<jstring>( + env->CallObjectMethodA(object, id, parameters)); + // If an exception was raised, we must clear it before calling most JNI + // methods. ScopedJavaLocalRef is liable to make such calls, so we test + // first. + if (base::android::ClearException(env)) { + SetInvocationFailure(kJavaExceptionRaised); + return; + } + ScopedJavaLocalRef<jstring> scoped_java_string(env, java_string); + if (!scoped_java_string.obj()) { + // LIVECONNECT_COMPLIANCE: Existing behavior is to return undefined. + // Spec requires returning a null string. + result_wrapper.Append( + GinJavaBridgeValue::CreateUndefinedValue().release()); + break; + } + result_wrapper.AppendString( + base::android::ConvertJavaStringToUTF8(scoped_java_string)); + break; + } + case JavaType::TypeObject: { + // If an exception was raised, we must clear it before calling most JNI + // methods. ScopedJavaLocalRef is liable to make such calls, so we test + // first. + jobject java_object = env->CallObjectMethodA(object, id, parameters); + if (base::android::ClearException(env)) { + SetInvocationFailure(kJavaExceptionRaised); + return; + } + ScopedJavaLocalRef<jobject> scoped_java_object(env, java_object); + if (!scoped_java_object.obj()) { + result_wrapper.Append(base::Value::CreateNullValue()); + break; + } + SetObjectResult(scoped_java_object, object_->GetSafeAnnotationClass()); + return; + } + } + // This is for all cases except JavaType::TypeObject. + if (!base::android::ClearException(env)) { + SetPrimitiveResult(result_wrapper); + } else { + SetInvocationFailure(kJavaExceptionRaised); + } +} + +} // namespace content diff --git a/content/browser/renderer_host/java/gin_java_method_invocation_helper.h b/content/browser/renderer_host/java/gin_java_method_invocation_helper.h new file mode 100644 index 0000000..5498fda --- /dev/null +++ b/content/browser/renderer_host/java/gin_java_method_invocation_helper.h @@ -0,0 +1,113 @@ +// Copyright 2014 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 CONTENT_BROWSER_RENDERER_HOST_JAVA_GIN_JAVA_METHOD_INVOCATION_HELPER_H_ +#define CONTENT_BROWSER_RENDERER_HOST_JAVA_GIN_JAVA_METHOD_INVOCATION_HELPER_H_ + +#include <map> + +#include "base/android/jni_weak_ref.h" +#include "base/android/scoped_java_ref.h" +#include "base/memory/ref_counted.h" +#include "base/values.h" +#include "content/browser/renderer_host/java/gin_java_bound_object.h" +#include "content/browser/renderer_host/java/java_type.h" +#include "content/common/content_export.h" + +namespace content { + +class JavaMethod; + +// Instances of this class are created and initialized on the UI thread, do +// their work on the background thread, and then again examined on the UI +// thread. They don't work on both threads simultaneously, though. +class CONTENT_EXPORT GinJavaMethodInvocationHelper + : public base::RefCountedThreadSafe<GinJavaMethodInvocationHelper> { + public: + // DispatcherDelegate is used on the UI thread + class DispatcherDelegate { + public: + DispatcherDelegate() {} + virtual ~DispatcherDelegate() {} + virtual JavaObjectWeakGlobalRef GetObjectWeakRef( + GinJavaBoundObject::ObjectID object_id) = 0; + + private: + DISALLOW_COPY_AND_ASSIGN(DispatcherDelegate); + }; + + // ObjectDelegate is used in the background thread + class ObjectDelegate { + public: + ObjectDelegate() {} + virtual ~ObjectDelegate() {} + virtual base::android::ScopedJavaLocalRef<jobject> GetLocalRef( + JNIEnv* env) = 0; + virtual const JavaMethod* FindMethod(const std::string& method_name, + size_t num_parameters) = 0; + virtual bool IsObjectGetClassMethod(const JavaMethod* method) = 0; + virtual const base::android::JavaRef<jclass>& GetSafeAnnotationClass() = 0; + + private: + DISALLOW_COPY_AND_ASSIGN(ObjectDelegate); + }; + + GinJavaMethodInvocationHelper(scoped_ptr<ObjectDelegate> object, + const std::string& method_name, + const base::ListValue& arguments); + void Init(DispatcherDelegate* dispatcher); + + // Called on the background thread + void Invoke(); + + // Called on the UI thread + bool HoldsPrimitiveResult(); + const base::ListValue& GetPrimitiveResult(); + const base::android::JavaRef<jobject>& GetObjectResult(); + const base::android::JavaRef<jclass>& GetSafeAnnotationClass(); + const std::string& GetErrorMessage(); + + private: + friend class base::RefCountedThreadSafe<GinJavaMethodInvocationHelper>; + ~GinJavaMethodInvocationHelper(); + + // Called on the UI thread + void BuildObjectRefsFromListValue(DispatcherDelegate* dispatcher, + const base::Value* list_value); + void BuildObjectRefsFromDictionaryValue(DispatcherDelegate* dispatcher, + const base::Value* dict_value); + + bool AppendObjectRef(DispatcherDelegate* dispatcher, + const base::Value* raw_value); + + // Called on the background thread. + void InvokeMethod(jobject object, + const JavaType& return_type, + jmethodID id, + jvalue* parameters); + void SetInvocationFailure(const char* error_message); + void SetPrimitiveResult(const base::ListValue& result_wrapper); + void SetObjectResult( + const base::android::JavaRef<jobject>& object, + const base::android::JavaRef<jclass>& safe_annotation_clazz); + + typedef std::map<GinJavaBoundObject::ObjectID, + JavaObjectWeakGlobalRef> ObjectRefs; + + scoped_ptr<ObjectDelegate> object_; + const std::string method_name_; + scoped_ptr<base::ListValue> arguments_; + ObjectRefs object_refs_; + bool holds_primitive_result_; + scoped_ptr<base::ListValue> primitive_result_; + std::string error_message_; + base::android::ScopedJavaGlobalRef<jobject> object_result_; + base::android::ScopedJavaGlobalRef<jclass> safe_annotation_clazz_; + + DISALLOW_COPY_AND_ASSIGN(GinJavaMethodInvocationHelper); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_RENDERER_HOST_JAVA_GIN_JAVA_METHOD_INVOCATION_HELPER_H_ diff --git a/content/browser/renderer_host/java/gin_java_method_invocation_helper_unittest.cc b/content/browser/renderer_host/java/gin_java_method_invocation_helper_unittest.cc new file mode 100644 index 0000000..82267d8 --- /dev/null +++ b/content/browser/renderer_host/java/gin_java_method_invocation_helper_unittest.cc @@ -0,0 +1,280 @@ +// Copyright 2014 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 "content/browser/renderer_host/java/gin_java_method_invocation_helper.h" + +#include "base/android/jni_android.h" +#include "content/common/android/gin_java_bridge_value.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace content { + +namespace { + +class NullObjectDelegate + : public GinJavaMethodInvocationHelper::ObjectDelegate { + public: + NullObjectDelegate() {} + + virtual ~NullObjectDelegate() {} + + virtual base::android::ScopedJavaLocalRef<jobject> GetLocalRef( + JNIEnv* env) OVERRIDE { + return base::android::ScopedJavaLocalRef<jobject>(); + } + + virtual const JavaMethod* FindMethod(const std::string& method_name, + size_t num_parameters) OVERRIDE { + return NULL; + } + + virtual bool IsObjectGetClassMethod(const JavaMethod* method) OVERRIDE { + return false; + } + + virtual const base::android::JavaRef<jclass>& GetSafeAnnotationClass() + OVERRIDE { + return safe_annotation_class_; + } + + private: + base::android::ScopedJavaLocalRef<jclass> safe_annotation_class_; + + DISALLOW_COPY_AND_ASSIGN(NullObjectDelegate); +}; + +class NullDispatcherDelegate + : public GinJavaMethodInvocationHelper::DispatcherDelegate { + public: + NullDispatcherDelegate() {} + + virtual ~NullDispatcherDelegate() {} + + virtual JavaObjectWeakGlobalRef GetObjectWeakRef( + GinJavaBoundObject::ObjectID object_id) OVERRIDE { + return JavaObjectWeakGlobalRef(); + } + + DISALLOW_COPY_AND_ASSIGN(NullDispatcherDelegate); +}; + +} // namespace + +class GinJavaMethodInvocationHelperTest : public testing::Test { +}; + +namespace { + +class CountingDispatcherDelegate + : public GinJavaMethodInvocationHelper::DispatcherDelegate { + public: + CountingDispatcherDelegate() {} + + virtual ~CountingDispatcherDelegate() {} + + virtual JavaObjectWeakGlobalRef GetObjectWeakRef( + GinJavaBoundObject::ObjectID object_id) OVERRIDE { + counters_[object_id]++; + return JavaObjectWeakGlobalRef(); + } + + void AssertInvocationsCount(GinJavaBoundObject::ObjectID begin_object_id, + GinJavaBoundObject::ObjectID end_object_id) { + EXPECT_EQ(end_object_id - begin_object_id, + static_cast<int>(counters_.size())); + for (GinJavaBoundObject::ObjectID i = begin_object_id; + i < end_object_id; ++i) { + EXPECT_LT(0, counters_[i]) << "ObjectID: " << i; + } + } + + private: + typedef std::map<GinJavaBoundObject::ObjectID, int> Counters; + Counters counters_; + + DISALLOW_COPY_AND_ASSIGN(CountingDispatcherDelegate); +}; + +} // namespace + +TEST_F(GinJavaMethodInvocationHelperTest, RetrievalOfObjectsNoObjects) { + base::ListValue no_objects; + for (int i = 0; i < 10; ++i) { + no_objects.AppendInteger(i); + } + + scoped_refptr<GinJavaMethodInvocationHelper> helper = + new GinJavaMethodInvocationHelper( + scoped_ptr<GinJavaMethodInvocationHelper::ObjectDelegate>( + new NullObjectDelegate()), + "foo", + no_objects); + CountingDispatcherDelegate counter; + helper->Init(&counter); + counter.AssertInvocationsCount(0, 0); +} + +TEST_F(GinJavaMethodInvocationHelperTest, RetrievalOfObjectsHaveObjects) { + base::ListValue objects; + objects.AppendInteger(100); + objects.Append(GinJavaBridgeValue::CreateObjectIDValue(1).release()); + base::ListValue* sub_list = new base::ListValue(); + sub_list->AppendInteger(200); + sub_list->Append(GinJavaBridgeValue::CreateObjectIDValue(2).release()); + objects.Append(sub_list); + base::DictionaryValue* sub_dict = new base::DictionaryValue(); + sub_dict->SetInteger("1", 300); + sub_dict->Set("2", GinJavaBridgeValue::CreateObjectIDValue(3).release()); + objects.Append(sub_dict); + base::ListValue* sub_list_with_dict = new base::ListValue(); + base::DictionaryValue* sub_sub_dict = new base::DictionaryValue(); + sub_sub_dict->Set("1", GinJavaBridgeValue::CreateObjectIDValue(4).release()); + sub_list_with_dict->Append(sub_sub_dict); + objects.Append(sub_list_with_dict); + base::DictionaryValue* sub_dict_with_list = new base::DictionaryValue(); + base::ListValue* sub_sub_list = new base::ListValue(); + sub_sub_list->Append(GinJavaBridgeValue::CreateObjectIDValue(5).release()); + sub_dict_with_list->Set("1", sub_sub_list); + objects.Append(sub_dict_with_list); + + scoped_refptr<GinJavaMethodInvocationHelper> helper = + new GinJavaMethodInvocationHelper( + scoped_ptr<GinJavaMethodInvocationHelper::ObjectDelegate>( + new NullObjectDelegate()), + "foo", + objects); + CountingDispatcherDelegate counter; + helper->Init(&counter); + counter.AssertInvocationsCount(1, 6); +} + +TEST_F(GinJavaMethodInvocationHelperTest, HandleObjectIsGone) { + base::ListValue no_objects; + scoped_refptr<GinJavaMethodInvocationHelper> helper = + new GinJavaMethodInvocationHelper( + scoped_ptr<GinJavaMethodInvocationHelper::ObjectDelegate>( + new NullObjectDelegate()), + "foo", + no_objects); + NullDispatcherDelegate dispatcher; + helper->Init(&dispatcher); + EXPECT_TRUE(helper->GetErrorMessage().empty()); + helper->Invoke(); + EXPECT_TRUE(helper->HoldsPrimitiveResult()); + EXPECT_TRUE(helper->GetPrimitiveResult().empty()); + EXPECT_FALSE(helper->GetErrorMessage().empty()); +} + +namespace { + +class MethodNotFoundObjectDelegate : public NullObjectDelegate { + public: + MethodNotFoundObjectDelegate() : find_method_called_(false) {} + + virtual ~MethodNotFoundObjectDelegate() {} + + virtual base::android::ScopedJavaLocalRef<jobject> GetLocalRef( + JNIEnv* env) OVERRIDE { + return base::android::ScopedJavaLocalRef<jobject>( + env, static_cast<jobject>(env->FindClass("java/lang/String"))); + } + + virtual const JavaMethod* FindMethod(const std::string& method_name, + size_t num_parameters) OVERRIDE { + find_method_called_ = true; + return NULL; + } + + bool find_method_called() const { return find_method_called_; } + + protected: + bool find_method_called_; + + private: + DISALLOW_COPY_AND_ASSIGN(MethodNotFoundObjectDelegate); +}; + +} // namespace + +TEST_F(GinJavaMethodInvocationHelperTest, HandleMethodNotFound) { + base::ListValue no_objects; + MethodNotFoundObjectDelegate* object_delegate = + new MethodNotFoundObjectDelegate(); + scoped_refptr<GinJavaMethodInvocationHelper> helper = + new GinJavaMethodInvocationHelper( + scoped_ptr<GinJavaMethodInvocationHelper::ObjectDelegate>( + object_delegate), + "foo", + no_objects); + NullDispatcherDelegate dispatcher; + helper->Init(&dispatcher); + EXPECT_FALSE(object_delegate->find_method_called()); + EXPECT_TRUE(helper->GetErrorMessage().empty()); + helper->Invoke(); + EXPECT_TRUE(object_delegate->find_method_called()); + EXPECT_TRUE(helper->HoldsPrimitiveResult()); + EXPECT_TRUE(helper->GetPrimitiveResult().empty()); + EXPECT_FALSE(helper->GetErrorMessage().empty()); +} + +namespace { + +class GetClassObjectDelegate : public MethodNotFoundObjectDelegate { + public: + GetClassObjectDelegate() : get_class_called_(false) {} + + virtual ~GetClassObjectDelegate() {} + + virtual const JavaMethod* FindMethod(const std::string& method_name, + size_t num_parameters) OVERRIDE { + find_method_called_ = true; + return kFakeGetClass; + } + + virtual bool IsObjectGetClassMethod(const JavaMethod* method) OVERRIDE { + get_class_called_ = true; + return kFakeGetClass == method; + } + + bool get_class_called() const { return get_class_called_; } + + private: + static const JavaMethod* kFakeGetClass; + bool get_class_called_; + + DISALLOW_COPY_AND_ASSIGN(GetClassObjectDelegate); +}; + +// We don't expect GinJavaMethodInvocationHelper to actually invoke the +// method, since the point of the test is to verify whether calls to +// 'getClass' get blocked. +const JavaMethod* GetClassObjectDelegate::kFakeGetClass = + (JavaMethod*)0xdeadbeef; + +} // namespace + +TEST_F(GinJavaMethodInvocationHelperTest, HandleGetClassInvocation) { + base::ListValue no_objects; + GetClassObjectDelegate* object_delegate = + new GetClassObjectDelegate(); + scoped_refptr<GinJavaMethodInvocationHelper> helper = + new GinJavaMethodInvocationHelper( + scoped_ptr<GinJavaMethodInvocationHelper::ObjectDelegate>( + object_delegate), + "foo", + no_objects); + NullDispatcherDelegate dispatcher; + helper->Init(&dispatcher); + EXPECT_FALSE(object_delegate->find_method_called()); + EXPECT_FALSE(object_delegate->get_class_called()); + EXPECT_TRUE(helper->GetErrorMessage().empty()); + helper->Invoke(); + EXPECT_TRUE(object_delegate->find_method_called()); + EXPECT_TRUE(object_delegate->get_class_called()); + EXPECT_TRUE(helper->HoldsPrimitiveResult()); + EXPECT_TRUE(helper->GetPrimitiveResult().empty()); + EXPECT_FALSE(helper->GetErrorMessage().empty()); +} + +} // namespace content diff --git a/content/browser/renderer_host/java/gin_java_script_to_java_types_coercion.cc b/content/browser/renderer_host/java/gin_java_script_to_java_types_coercion.cc new file mode 100644 index 0000000..8f7c29f --- /dev/null +++ b/content/browser/renderer_host/java/gin_java_script_to_java_types_coercion.cc @@ -0,0 +1,705 @@ +// Copyright 2014 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 "content/browser/renderer_host/java/gin_java_script_to_java_types_coercion.h" + +#include <unistd.h> + +#include "base/android/jni_android.h" +#include "base/android/jni_string.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "content/common/android/gin_java_bridge_value.h" + +using base::android::ConvertUTF8ToJavaString; + +namespace content { + +namespace { + +const char kJavaLangString[] = "java/lang/String"; +const char kUndefined[] = "undefined"; + +double RoundDoubleTowardsZero(const double& x) { + if (std::isnan(x)) { + return 0.0; + } + return x > 0.0 ? floor(x) : ceil(x); +} + +// Rounds to jlong using Java's type conversion rules. +jlong RoundDoubleToLong(const double& x) { + double intermediate = RoundDoubleTowardsZero(x); + // The int64 limits can not be converted exactly to double values, so we + // compare to custom constants. kint64max is 2^63 - 1, but the spacing + // between double values in the the range 2^62 to 2^63 is 2^10. The cast is + // required to silence a spurious gcc warning for integer overflow. + const int64 kLimit = (GG_INT64_C(1) << 63) - static_cast<uint64>(1 << 10); + DCHECK(kLimit > 0); + const double kLargestDoubleLessThanInt64Max = kLimit; + const double kSmallestDoubleGreaterThanInt64Min = -kLimit; + if (intermediate > kLargestDoubleLessThanInt64Max) { + return kint64max; + } + if (intermediate < kSmallestDoubleGreaterThanInt64Min) { + return kint64min; + } + return static_cast<jlong>(intermediate); +} + +// Rounds to jint using Java's type conversion rules. +jint RoundDoubleToInt(const double& x) { + double intermediate = RoundDoubleTowardsZero(x); + // The int32 limits cast exactly to double values. + intermediate = std::min(intermediate, static_cast<double>(kint32max)); + intermediate = std::max(intermediate, static_cast<double>(kint32min)); + return static_cast<jint>(intermediate); +} + +jvalue CoerceJavaScriptIntegerToJavaValue(JNIEnv* env, + const base::Value* value, + const JavaType& target_type, + bool coerce_to_string) { + // See http://jdk6.java.net/plugin2/liveconnect/#JS_NUMBER_VALUES. + + // For conversion to numeric types, we need to replicate Java's type + // conversion rules. This requires that for integer values, we simply discard + // all but the lowest n buts, where n is the number of bits in the target + // type. + jvalue result; + int int_value; + value->GetAsInteger(&int_value); + switch (target_type.type) { + case JavaType::TypeByte: + result.b = static_cast<jbyte>(int_value); + break; + case JavaType::TypeChar: + result.c = static_cast<jchar>(int_value); + break; + case JavaType::TypeShort: + result.s = static_cast<jshort>(int_value); + break; + case JavaType::TypeInt: + result.i = int_value; + break; + case JavaType::TypeLong: + result.j = int_value; + break; + case JavaType::TypeFloat: + result.f = int_value; + break; + case JavaType::TypeDouble: + result.d = int_value; + break; + case JavaType::TypeObject: + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert to null. Spec + // requires handling object equivalents of primitive types. + result.l = NULL; + break; + case JavaType::TypeString: + result.l = coerce_to_string + ? ConvertUTF8ToJavaString( + env, base::Int64ToString(int_value)).Release() + : NULL; + break; + case JavaType::TypeBoolean: + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert to false. Spec + // requires converting to false for 0 or NaN, true otherwise. + result.z = JNI_FALSE; + break; + case JavaType::TypeArray: + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert to null. Spec + // requires raising a JavaScript exception. + result.l = NULL; + break; + case JavaType::TypeVoid: + // Conversion to void must never happen. + NOTREACHED(); + break; + } + return result; +} + +jvalue CoerceJavaScriptDoubleToJavaValue(JNIEnv* env, + double double_value, + const JavaType& target_type, + bool coerce_to_string) { + // See http://jdk6.java.net/plugin2/liveconnect/#JS_NUMBER_VALUES. + // For conversion to numeric types, we need to replicate Java's type + // conversion rules. + jvalue result; + switch (target_type.type) { + case JavaType::TypeByte: + result.b = static_cast<jbyte>(RoundDoubleToInt(double_value)); + break; + case JavaType::TypeChar: + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert double to 0. + // Spec requires converting doubles similarly to how we convert doubles to + // other numeric types. + result.c = 0; + break; + case JavaType::TypeShort: + result.s = static_cast<jshort>(RoundDoubleToInt(double_value)); + break; + case JavaType::TypeInt: + result.i = RoundDoubleToInt(double_value); + break; + case JavaType::TypeLong: + result.j = RoundDoubleToLong(double_value); + break; + case JavaType::TypeFloat: + result.f = static_cast<jfloat>(double_value); + break; + case JavaType::TypeDouble: + result.d = double_value; + break; + case JavaType::TypeObject: + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert to null. Spec + // requires handling object equivalents of primitive types. + result.l = NULL; + break; + case JavaType::TypeString: + result.l = + coerce_to_string + ? ConvertUTF8ToJavaString( + env, base::StringPrintf("%.6lg", double_value)).Release() + : NULL; + break; + case JavaType::TypeBoolean: + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert to false. Spec + // requires converting to false for 0 or NaN, true otherwise. + result.z = JNI_FALSE; + break; + case JavaType::TypeArray: + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert to null. Spec + // requires raising a JavaScript exception. + result.l = NULL; + break; + case JavaType::TypeVoid: + // Conversion to void must never happen. + NOTREACHED(); + break; + } + return result; +} + +jvalue CoerceJavaScriptBooleanToJavaValue(JNIEnv* env, + const base::Value* value, + const JavaType& target_type, + bool coerce_to_string) { + // See http://jdk6.java.net/plugin2/liveconnect/#JS_BOOLEAN_VALUES. + bool boolean_value; + value->GetAsBoolean(&boolean_value); + jvalue result; + switch (target_type.type) { + case JavaType::TypeBoolean: + result.z = boolean_value ? JNI_TRUE : JNI_FALSE; + break; + case JavaType::TypeObject: + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert to NULL. Spec + // requires handling java.lang.Boolean and java.lang.Object. + result.l = NULL; + break; + case JavaType::TypeString: + result.l = coerce_to_string + ? ConvertUTF8ToJavaString( + env, boolean_value ? "true" : "false").Release() + : NULL; + break; + case JavaType::TypeByte: + case JavaType::TypeChar: + case JavaType::TypeShort: + case JavaType::TypeInt: + case JavaType::TypeLong: + case JavaType::TypeFloat: + case JavaType::TypeDouble: { + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert to 0. Spec + // requires converting to 0 or 1. + jvalue null_value = {0}; + result = null_value; + break; + } + case JavaType::TypeArray: + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert to NULL. Spec + // requires raising a JavaScript exception. + result.l = NULL; + break; + case JavaType::TypeVoid: + // Conversion to void must never happen. + NOTREACHED(); + break; + } + return result; +} + +jvalue CoerceJavaScriptStringToJavaValue(JNIEnv* env, + const base::Value* value, + const JavaType& target_type) { + // See http://jdk6.java.net/plugin2/liveconnect/#JS_STRING_VALUES. + jvalue result; + switch (target_type.type) { + case JavaType::TypeString: { + std::string string_result; + value->GetAsString(&string_result); + result.l = ConvertUTF8ToJavaString(env, string_result).Release(); + break; + } + case JavaType::TypeObject: + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert to NULL. Spec + // requires handling java.lang.Object. + result.l = NULL; + break; + case JavaType::TypeByte: + case JavaType::TypeShort: + case JavaType::TypeInt: + case JavaType::TypeLong: + case JavaType::TypeFloat: + case JavaType::TypeDouble: { + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert to 0. Spec + // requires using valueOf() method of corresponding object type. + jvalue null_value = {0}; + result = null_value; + break; + } + case JavaType::TypeChar: + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert to 0. Spec + // requires using java.lang.Short.decode(). + result.c = 0; + break; + case JavaType::TypeBoolean: + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert to false. Spec + // requires converting the empty string to false, otherwise true. + result.z = JNI_FALSE; + break; + case JavaType::TypeArray: + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert to NULL. Spec + // requires raising a JavaScript exception. + result.l = NULL; + break; + case JavaType::TypeVoid: + // Conversion to void must never happen. + NOTREACHED(); + break; + } + return result; +} + +// Note that this only handles primitive types and strings. +jobject CreateJavaArray(JNIEnv* env, const JavaType& type, jsize length) { + switch (type.type) { + case JavaType::TypeBoolean: + return env->NewBooleanArray(length); + case JavaType::TypeByte: + return env->NewByteArray(length); + case JavaType::TypeChar: + return env->NewCharArray(length); + case JavaType::TypeShort: + return env->NewShortArray(length); + case JavaType::TypeInt: + return env->NewIntArray(length); + case JavaType::TypeLong: + return env->NewLongArray(length); + case JavaType::TypeFloat: + return env->NewFloatArray(length); + case JavaType::TypeDouble: + return env->NewDoubleArray(length); + case JavaType::TypeString: { + base::android::ScopedJavaLocalRef<jclass> clazz( + base::android::GetClass(env, kJavaLangString)); + return env->NewObjectArray(length, clazz.obj(), NULL); + } + case JavaType::TypeVoid: + // Conversion to void must never happen. + case JavaType::TypeArray: + case JavaType::TypeObject: + // Not handled. + NOTREACHED(); + } + return NULL; +} + +// Sets the specified element of the supplied array to the value of the +// supplied jvalue. Requires that the type of the array matches that of the +// jvalue. Handles only primitive types and strings. Note that in the case of a +// string, the array takes a new reference to the string object. +void SetArrayElement(JNIEnv* env, + jobject array, + const JavaType& type, + jsize index, + const jvalue& value) { + switch (type.type) { + case JavaType::TypeBoolean: + env->SetBooleanArrayRegion(static_cast<jbooleanArray>(array), index, 1, + &value.z); + break; + case JavaType::TypeByte: + env->SetByteArrayRegion(static_cast<jbyteArray>(array), index, 1, + &value.b); + break; + case JavaType::TypeChar: + env->SetCharArrayRegion(static_cast<jcharArray>(array), index, 1, + &value.c); + break; + case JavaType::TypeShort: + env->SetShortArrayRegion(static_cast<jshortArray>(array), index, 1, + &value.s); + break; + case JavaType::TypeInt: + env->SetIntArrayRegion(static_cast<jintArray>(array), index, 1, + &value.i); + break; + case JavaType::TypeLong: + env->SetLongArrayRegion(static_cast<jlongArray>(array), index, 1, + &value.j); + break; + case JavaType::TypeFloat: + env->SetFloatArrayRegion(static_cast<jfloatArray>(array), index, 1, + &value.f); + break; + case JavaType::TypeDouble: + env->SetDoubleArrayRegion(static_cast<jdoubleArray>(array), index, 1, + &value.d); + break; + case JavaType::TypeString: + env->SetObjectArrayElement(static_cast<jobjectArray>(array), index, + value.l); + break; + case JavaType::TypeVoid: + // Conversion to void must never happen. + case JavaType::TypeArray: + case JavaType::TypeObject: + // Not handled. + NOTREACHED(); + } + base::android::CheckException(env); +} + +jvalue CoerceJavaScriptNullOrUndefinedToJavaValue(JNIEnv* env, + const base::Value* value, + const JavaType& target_type, + bool coerce_to_string) { + bool is_undefined = false; + scoped_ptr<const GinJavaBridgeValue> gin_value; + if (GinJavaBridgeValue::ContainsGinJavaBridgeValue(value)) { + gin_value = GinJavaBridgeValue::FromValue(value); + if (gin_value->IsType(GinJavaBridgeValue::TYPE_UNDEFINED)) { + is_undefined = true; + } + } + jvalue result; + switch (target_type.type) { + case JavaType::TypeObject: + result.l = NULL; + break; + case JavaType::TypeString: + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert undefined to + // "undefined". Spec requires converting undefined to NULL. + result.l = (coerce_to_string && is_undefined) + ? ConvertUTF8ToJavaString(env, kUndefined).Release() + : NULL; + break; + case JavaType::TypeByte: + case JavaType::TypeChar: + case JavaType::TypeShort: + case JavaType::TypeInt: + case JavaType::TypeLong: + case JavaType::TypeFloat: + case JavaType::TypeDouble: { + jvalue null_value = {0}; + result = null_value; + break; + } + case JavaType::TypeBoolean: + result.z = JNI_FALSE; + break; + case JavaType::TypeArray: + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert to NULL. Spec + // requires raising a JavaScript exception. + result.l = NULL; + break; + case JavaType::TypeVoid: + // Conversion to void must never happen. + NOTREACHED(); + break; + } + return result; +} + +jobject CoerceJavaScriptListToArray(JNIEnv* env, + const base::Value* value, + const JavaType& target_type, + const ObjectRefs& object_refs) { + DCHECK_EQ(JavaType::TypeArray, target_type.type); + const JavaType& target_inner_type = *target_type.inner_type.get(); + // LIVECONNECT_COMPLIANCE: Existing behavior is to return null for + // multi-dimensional arrays. Spec requires handling multi-demensional arrays. + if (target_inner_type.type == JavaType::TypeArray) { + return NULL; + } + + // LIVECONNECT_COMPLIANCE: Existing behavior is to return null for object + // arrays. Spec requires handling object arrays. + if (target_inner_type.type == JavaType::TypeObject) { + return NULL; + } + + const base::ListValue* list_value; + value->GetAsList(&list_value); + // Create the Java array. + jsize length = static_cast<jsize>(list_value->GetSize()); + jobject result = CreateJavaArray(env, target_inner_type, length); + if (!result) { + return NULL; + } + scoped_ptr<base::Value> null_value(base::Value::CreateNullValue()); + for (jsize i = 0; i < length; ++i) { + const base::Value* value_element = null_value.get(); + list_value->Get(i, &value_element); + jvalue element = CoerceJavaScriptValueToJavaValue( + env, value_element, target_inner_type, false, object_refs); + SetArrayElement(env, result, target_inner_type, i, element); + // CoerceJavaScriptValueToJavaValue() creates new local references to + // strings, objects and arrays. Of these, only strings can occur here. + // SetArrayElement() causes the array to take its own reference to the + // string, so we can now release the local reference. + DCHECK_NE(JavaType::TypeObject, target_inner_type.type); + DCHECK_NE(JavaType::TypeArray, target_inner_type.type); + ReleaseJavaValueIfRequired(env, &element, target_inner_type); + } + + return result; +} + +jobject CoerceJavaScriptDictionaryToArray(JNIEnv* env, + const base::Value* value, + const JavaType& target_type, + const ObjectRefs& object_refs) { + DCHECK_EQ(JavaType::TypeArray, target_type.type); + + const JavaType& target_inner_type = *target_type.inner_type.get(); + // LIVECONNECT_COMPLIANCE: Existing behavior is to return null for + // multi-dimensional arrays. Spec requires handling multi-demensional arrays. + if (target_inner_type.type == JavaType::TypeArray) { + return NULL; + } + + // LIVECONNECT_COMPLIANCE: Existing behavior is to return null for object + // arrays. Spec requires handling object arrays. + if (target_inner_type.type == JavaType::TypeObject) { + return NULL; + } + + const base::DictionaryValue* dictionary_value; + value->GetAsDictionary(&dictionary_value); + const base::Value* length_value; + // If the object does not have a length property, return null. + if (!dictionary_value->Get("length", &length_value)) { + return NULL; + } + + // If the length property does not have numeric type, or is outside the valid + // range for a Java array length, return null. + jsize length = -1; + if (length_value->IsType(base::Value::TYPE_INTEGER)) { + int int_length; + length_value->GetAsInteger(&int_length); + if (int_length >= 0 && int_length <= kint32max) { + length = static_cast<jsize>(int_length); + } + } else if (length_value->IsType(base::Value::TYPE_DOUBLE)) { + double double_length; + length_value->GetAsDouble(&double_length); + if (double_length >= 0.0 && double_length <= kint32max) { + length = static_cast<jsize>(double_length); + } + } + if (length == -1) { + return NULL; + } + + jobject result = CreateJavaArray(env, target_inner_type, length); + if (!result) { + return NULL; + } + scoped_ptr<base::Value> null_value(base::Value::CreateNullValue()); + for (jsize i = 0; i < length; ++i) { + const std::string key(base::IntToString(i)); + const base::Value* value_element = null_value.get(); + if (dictionary_value->HasKey(key)) { + dictionary_value->Get(key, &value_element); + } + jvalue element = CoerceJavaScriptValueToJavaValue( + env, value_element, target_inner_type, false, object_refs); + SetArrayElement(env, result, target_inner_type, i, element); + // CoerceJavaScriptValueToJavaValue() creates new local references to + // strings, objects and arrays. Of these, only strings can occur here. + // SetArrayElement() causes the array to take its own reference to the + // string, so we can now release the local reference. + DCHECK_NE(JavaType::TypeObject, target_inner_type.type); + DCHECK_NE(JavaType::TypeArray, target_inner_type.type); + ReleaseJavaValueIfRequired(env, &element, target_inner_type); + } + + return result; +} + +jvalue CoerceJavaScriptObjectToJavaValue(JNIEnv* env, + const base::Value* value, + const JavaType& target_type, + bool coerce_to_string, + const ObjectRefs& object_refs) { + // This covers both JavaScript objects (including arrays) and Java objects. + // See http://jdk6.java.net/plugin2/liveconnect/#JS_OTHER_OBJECTS, + // http://jdk6.java.net/plugin2/liveconnect/#JS_ARRAY_VALUES and + // http://jdk6.java.net/plugin2/liveconnect/#JS_JAVA_OBJECTS + jvalue result; + switch (target_type.type) { + case JavaType::TypeObject: { + if (GinJavaBridgeValue::ContainsGinJavaBridgeValue(value)) { + scoped_ptr<const GinJavaBridgeValue> gin_value( + GinJavaBridgeValue::FromValue(value)); + DCHECK(gin_value); + DCHECK(gin_value->IsType(GinJavaBridgeValue::TYPE_OBJECT_ID)); + base::android::ScopedJavaLocalRef<jobject> obj; + GinJavaBoundObject::ObjectID object_id; + if (gin_value->GetAsObjectID(&object_id)) { + ObjectRefs::const_iterator iter = object_refs.find(object_id); + if (iter != object_refs.end()) { + obj.Reset(iter->second.get(env)); + } + } + result.l = obj.Release(); + } else { + // LIVECONNECT_COMPLIANCE: Existing behavior is to pass null. Spec + // requires converting if the target type is + // netscape.javascript.JSObject, otherwise raising a JavaScript + // exception. + result.l = NULL; + } + break; + } + case JavaType::TypeString: + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert to + // "undefined". Spec requires calling toString() on the Java object. + result.l = coerce_to_string + ? ConvertUTF8ToJavaString(env, kUndefined).Release() + : NULL; + break; + case JavaType::TypeByte: + case JavaType::TypeShort: + case JavaType::TypeInt: + case JavaType::TypeLong: + case JavaType::TypeFloat: + case JavaType::TypeDouble: + case JavaType::TypeChar: { + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert to 0. Spec + // requires raising a JavaScript exception. + jvalue null_value = {0}; + result = null_value; + break; + } + case JavaType::TypeBoolean: + // LIVECONNECT_COMPLIANCE: Existing behavior is to convert to false. Spec + // requires raising a JavaScript exception. + result.z = JNI_FALSE; + break; + case JavaType::TypeArray: + if (value->IsType(base::Value::TYPE_DICTIONARY)) { + result.l = CoerceJavaScriptDictionaryToArray( + env, value, target_type, object_refs); + } else if (value->IsType(base::Value::TYPE_LIST)) { + result.l = + CoerceJavaScriptListToArray(env, value, target_type, object_refs); + } else { + result.l = NULL; + } + break; + case JavaType::TypeVoid: + // Conversion to void must never happen. + NOTREACHED(); + break; + } + return result; +} + +jvalue CoerceGinJavaBridgeValueToJavaValue(JNIEnv* env, + const base::Value* value, + const JavaType& target_type, + bool coerce_to_string, + const ObjectRefs& object_refs) { + DCHECK(GinJavaBridgeValue::ContainsGinJavaBridgeValue(value)); + scoped_ptr<const GinJavaBridgeValue> gin_value( + GinJavaBridgeValue::FromValue(value)); + switch (gin_value->GetType()) { + case GinJavaBridgeValue::TYPE_UNDEFINED: + return CoerceJavaScriptNullOrUndefinedToJavaValue( + env, value, target_type, coerce_to_string); + case GinJavaBridgeValue::TYPE_NONFINITE: { + float float_value; + gin_value->GetAsNonFinite(&float_value); + return CoerceJavaScriptDoubleToJavaValue( + env, float_value, target_type, coerce_to_string); + } + case GinJavaBridgeValue::TYPE_OBJECT_ID: + return CoerceJavaScriptObjectToJavaValue( + env, value, target_type, coerce_to_string, object_refs); + default: + NOTREACHED(); + } + return jvalue(); +} + +} // namespace + + +void ReleaseJavaValueIfRequired(JNIEnv* env, + jvalue* value, + const JavaType& type) { + if (type.type == JavaType::TypeString || type.type == JavaType::TypeObject || + type.type == JavaType::TypeArray) { + env->DeleteLocalRef(value->l); + value->l = NULL; + } +} + +jvalue CoerceJavaScriptValueToJavaValue(JNIEnv* env, + const base::Value* value, + const JavaType& target_type, + bool coerce_to_string, + const ObjectRefs& object_refs) { + // Note that in all these conversions, the relevant field of the jvalue must + // always be explicitly set, as jvalue does not initialize its fields. + + switch (value->GetType()) { + case base::Value::TYPE_INTEGER: + return CoerceJavaScriptIntegerToJavaValue( + env, value, target_type, coerce_to_string); + case base::Value::TYPE_DOUBLE: { + double double_value; + value->GetAsDouble(&double_value); + return CoerceJavaScriptDoubleToJavaValue( + env, double_value, target_type, coerce_to_string); + } + case base::Value::TYPE_BOOLEAN: + return CoerceJavaScriptBooleanToJavaValue( + env, value, target_type, coerce_to_string); + case base::Value::TYPE_STRING: + return CoerceJavaScriptStringToJavaValue(env, value, target_type); + case base::Value::TYPE_DICTIONARY: + case base::Value::TYPE_LIST: + return CoerceJavaScriptObjectToJavaValue( + env, value, target_type, coerce_to_string, object_refs); + case base::Value::TYPE_NULL: + return CoerceJavaScriptNullOrUndefinedToJavaValue( + env, value, target_type, coerce_to_string); + case base::Value::TYPE_BINARY: + return CoerceGinJavaBridgeValueToJavaValue( + env, value, target_type, coerce_to_string, object_refs); + } + NOTREACHED(); + return jvalue(); +} + +} // namespace content diff --git a/content/browser/renderer_host/java/gin_java_script_to_java_types_coercion.h b/content/browser/renderer_host/java/gin_java_script_to_java_types_coercion.h new file mode 100644 index 0000000..1df7345 --- /dev/null +++ b/content/browser/renderer_host/java/gin_java_script_to_java_types_coercion.h @@ -0,0 +1,33 @@ +// Copyright 2014 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 CONTENT_BROWSER_RENDERER_HOST_JAVA_GIN_JAVA_SCRIPT_TO_JAVA_TYPES_COERCION_H_ +#define CONTENT_BROWSER_RENDERER_HOST_JAVA_GIN_JAVA_SCRIPT_TO_JAVA_TYPES_COERCION_H_ + +#include <map> + +#include "base/android/jni_weak_ref.h" +#include "base/values.h" +#include "content/browser/renderer_host/java/gin_java_bound_object.h" +#include "content/browser/renderer_host/java/java_type.h" + +namespace content { + +typedef std::map<GinJavaBoundObject::ObjectID, JavaObjectWeakGlobalRef> + ObjectRefs; + +jvalue CoerceJavaScriptValueToJavaValue( + JNIEnv* env, + const base::Value* value, + const JavaType& target_type, + bool coerce_to_string, + const ObjectRefs& object_refs); + +void ReleaseJavaValueIfRequired(JNIEnv* env, + jvalue* value, + const JavaType& type); + +} // namespace content + +#endif // CONTENT_BROWSER_RENDERER_HOST_JAVA_GIN_JAVA_SCRIPT_TO_JAVA_TYPES_COERCION_H_ diff --git a/content/content_browser.gypi b/content/content_browser.gypi index 7144e6b..23a57fe 100644 --- a/content/content_browser.gypi +++ b/content/content_browser.gypi @@ -964,6 +964,11 @@ 'browser/renderer_host/input/web_input_event_util.h', 'browser/renderer_host/input/web_input_event_util_posix.cc', 'browser/renderer_host/input/web_input_event_util_posix.h', + 'browser/renderer_host/java/gin_java_bound_object.h', + 'browser/renderer_host/java/gin_java_method_invocation_helper.cc', + 'browser/renderer_host/java/gin_java_method_invocation_helper.h', + 'browser/renderer_host/java/gin_java_script_to_java_types_coercion.cc', + 'browser/renderer_host/java/gin_java_script_to_java_types_coercion.h', 'browser/renderer_host/java/java_bound_object.cc', 'browser/renderer_host/java/java_bound_object.h', 'browser/renderer_host/java/java_bridge_channel_host.cc', diff --git a/content/content_tests.gypi b/content/content_tests.gypi index 8ffe45e..48fb198 100644 --- a/content/content_tests.gypi +++ b/content/content_tests.gypi @@ -875,6 +875,7 @@ }], ['OS == "android"', { 'sources': [ + 'browser/renderer_host/java/gin_java_method_invocation_helper_unittest.cc', 'browser/renderer_host/java/jni_helper_unittest.cc', 'renderer/java/gin_java_bridge_value_converter_unittest.cc', ], |