blob: f7d129ee4cecaf65193a92089b052146712bed83 [file] [log] [blame]
// 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/android/java/java_bound_object.h"
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/memory/singleton.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "content/browser/android/java/java_bridge_dispatcher_host_manager.h"
#include "content/browser/android/java/java_type.h"
#include "content/browser/android/java/jni_helper.h"
#include "content/public/browser/browser_thread.h"
#include "third_party/WebKit/public/web/WebBindings.h"
using base::StringPrintf;
using base::android::AttachCurrentThread;
using base::android::ConvertUTF8ToJavaString;
using base::android::GetClass;
using base::android::JavaRef;
using base::android::ScopedJavaGlobalRef;
using base::android::ScopedJavaLocalRef;
using blink::WebBindings;
// The conversion between JavaScript and Java types is based on the Live
// Connect 2 spec. See
// http://jdk6.java.net/plugin2/liveconnect/#JS_JAVA_CONVERSIONS.
// Note that in some cases, we differ from from the spec in order to maintain
// existing behavior. These areas are marked LIVECONNECT_COMPLIANCE. We may
// revisit this decision in the future.
namespace content {
namespace {
const char kJavaLangClass[] = "java/lang/Class";
const char kJavaLangObject[] = "java/lang/Object";
const char kJavaLangReflectMethod[] = "java/lang/reflect/Method";
const char kJavaLangSecurityExceptionClass[] = "java/lang/SecurityException";
const char kGetClass[] = "getClass";
const char kGetMethods[] = "getMethods";
const char kIsAnnotationPresent[] = "isAnnotationPresent";
const char kReturningJavaLangClass[] = "()Ljava/lang/Class;";
const char kReturningJavaLangReflectMethodArray[] =
"()[Ljava/lang/reflect/Method;";
const char kTakesJavaLangClassReturningBoolean[] = "(Ljava/lang/Class;)Z";
// This is an exception message, so no need to localize.
const char kAccessToObjectGetClassIsBlocked[] =
"Access to java.lang.Object.getClass is blocked";
// Our special NPObject type. We extend an NPObject with a pointer to a
// JavaBoundObject. We also add static methods for each of the NPObject
// callbacks, which are registered by our NPClass. These methods simply
// delegate to the private implementation methods of JavaBoundObject.
struct JavaNPObject : public NPObject {
JavaBoundObject* bound_object;
static const NPClass kNPClass;
static NPObject* Allocate(NPP npp, NPClass* np_class);
static void Deallocate(NPObject* np_object);
static bool HasMethod(NPObject* np_object, NPIdentifier np_identifier);
static bool Invoke(NPObject* np_object, NPIdentifier np_identifier,
const NPVariant *args, uint32_t arg_count,
NPVariant *result);
static bool HasProperty(NPObject* np_object, NPIdentifier np_identifier);
static bool GetProperty(NPObject* np_object, NPIdentifier np_identifier,
NPVariant *result);
static bool Enumerate(NPObject* object, NPIdentifier** values,
uint32_t* count);
};
const NPClass JavaNPObject::kNPClass = {
NP_CLASS_STRUCT_VERSION,
JavaNPObject::Allocate,
JavaNPObject::Deallocate,
NULL, // NPInvalidate
JavaNPObject::HasMethod,
JavaNPObject::Invoke,
NULL, // NPInvokeDefault
JavaNPObject::HasProperty,
JavaNPObject::GetProperty,
NULL, // NPSetProperty,
NULL, // NPRemoveProperty
JavaNPObject::Enumerate,
NULL,
};
NPObject* JavaNPObject::Allocate(NPP npp, NPClass* np_class) {
JavaNPObject* obj = new JavaNPObject();
return obj;
}
void JavaNPObject::Deallocate(NPObject* np_object) {
JavaNPObject* obj = reinterpret_cast<JavaNPObject*>(np_object);
delete obj->bound_object;
delete obj;
}
bool JavaNPObject::HasMethod(NPObject* np_object, NPIdentifier np_identifier) {
std::string name(WebBindings::utf8FromIdentifier(np_identifier));
JavaNPObject* obj = reinterpret_cast<JavaNPObject*>(np_object);
return obj->bound_object->HasMethod(name);
}
bool JavaNPObject::Invoke(NPObject* np_object, NPIdentifier np_identifier,
const NPVariant* args, uint32_t arg_count,
NPVariant* result) {
std::string name(WebBindings::utf8FromIdentifier(np_identifier));
JavaNPObject* obj = reinterpret_cast<JavaNPObject*>(np_object);
return obj->bound_object->Invoke(name, args, arg_count, result);
}
bool JavaNPObject::HasProperty(NPObject* np_object,
NPIdentifier np_identifier) {
// LIVECONNECT_COMPLIANCE: Existing behavior is to return false to indicate
// that the property is not present. Spec requires supporting this correctly.
return false;
}
bool JavaNPObject::GetProperty(NPObject* np_object,
NPIdentifier np_identifier,
NPVariant* result) {
// LIVECONNECT_COMPLIANCE: Existing behavior is to return false to indicate
// that the property is undefined. Spec requires supporting this correctly.
return false;
}
bool JavaNPObject::Enumerate(NPObject* np_object, NPIdentifier** values,
uint32_t* count) {
JavaNPObject* obj = reinterpret_cast<JavaNPObject*>(np_object);
if (!obj->bound_object->CanEnumerateMethods()) return false;
std::vector<std::string> method_names = obj->bound_object->GetMethodNames();
*count = base::saturated_cast<uint32_t>(method_names.size());
*values = static_cast<NPIdentifier*>(calloc(*count, sizeof(NPIdentifier)));
for (uint32_t i = 0; i < *count; ++i) {
(*values)[i] = WebBindings::getStringIdentifier(method_names[i].c_str());
}
return true;
}
// Calls a Java method through JNI. If the Java method raises an uncaught
// exception, it is cleared and this method returns false. Otherwise, this
// method returns true and the Java method's return value is provided as an
// NPVariant. Note that this method does not do any type coercion. The Java
// return value is simply converted to the corresponding NPAPI type.
bool CallJNIMethod(
jobject object,
jclass clazz,
const JavaType& return_type,
jmethodID id,
jvalue* parameters,
NPVariant* result,
const JavaRef<jclass>& safe_annotation_clazz,
const base::WeakPtr<JavaBridgeDispatcherHostManager>& manager,
bool can_enumerate_methods) {
DCHECK(object || clazz);
JNIEnv* env = AttachCurrentThread();
switch (return_type.type) {
case JavaType::TypeBoolean:
BOOLEAN_TO_NPVARIANT(
object ? env->CallBooleanMethodA(object, id, parameters)
: env->CallStaticBooleanMethodA(clazz, id, parameters),
*result);
break;
case JavaType::TypeByte:
INT32_TO_NPVARIANT(
object ? env->CallByteMethodA(object, id, parameters)
: env->CallStaticByteMethodA(clazz, id, parameters),
*result);
break;
case JavaType::TypeChar:
INT32_TO_NPVARIANT(
object ? env->CallCharMethodA(object, id, parameters)
: env->CallStaticCharMethodA(clazz, id, parameters),
*result);
break;
case JavaType::TypeShort:
INT32_TO_NPVARIANT(
object ? env->CallShortMethodA(object, id, parameters)
: env->CallStaticShortMethodA(clazz, id, parameters),
*result);
break;
case JavaType::TypeInt:
INT32_TO_NPVARIANT(object
? env->CallIntMethodA(object, id, parameters)
: env->CallStaticIntMethodA(clazz, id, parameters),
*result);
break;
case JavaType::TypeLong:
DOUBLE_TO_NPVARIANT(
object ? env->CallLongMethodA(object, id, parameters)
: env->CallStaticLongMethodA(clazz, id, parameters),
*result);
break;
case JavaType::TypeFloat:
DOUBLE_TO_NPVARIANT(
object ? env->CallFloatMethodA(object, id, parameters)
: env->CallStaticFloatMethodA(clazz, id, parameters),
*result);
break;
case JavaType::TypeDouble:
DOUBLE_TO_NPVARIANT(
object ? env->CallDoubleMethodA(object, id, parameters)
: env->CallStaticDoubleMethodA(clazz, id, parameters),
*result);
break;
case JavaType::TypeVoid:
if (object)
env->CallVoidMethodA(object, id, parameters);
else
env->CallStaticVoidMethodA(clazz, id, parameters);
VOID_TO_NPVARIANT(*result);
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.
VOID_TO_NPVARIANT(*result);
break;
case JavaType::TypeString: {
jstring java_string = static_cast<jstring>(
object ? env->CallObjectMethodA(object, id, parameters)
: env->CallStaticObjectMethodA(clazz, 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)) {
return false;
}
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.
VOID_TO_NPVARIANT(*result);
break;
}
std::string str =
base::android::ConvertJavaStringToUTF8(scoped_java_string);
size_t length = str.length();
// This pointer is freed in _NPN_ReleaseVariantValue in
// third_party/WebKit/Source/WebCore/bindings/v8/npruntime.cpp.
char* buffer = static_cast<char*>(malloc(length));
str.copy(buffer, length, 0);
STRINGN_TO_NPVARIANT(buffer, length, *result);
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 =
object ? env->CallObjectMethodA(object, id, parameters)
: env->CallStaticObjectMethodA(clazz, id, parameters);
if (base::android::ClearException(env)) {
return false;
}
ScopedJavaLocalRef<jobject> scoped_java_object(env, java_object);
if (!scoped_java_object.obj()) {
NULL_TO_NPVARIANT(*result);
break;
}
OBJECT_TO_NPVARIANT(JavaBoundObject::Create(scoped_java_object,
safe_annotation_clazz,
manager,
can_enumerate_methods),
*result);
break;
}
}
return !base::android::ClearException(env);
}
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 limit = (GG_INT64_C(1) << 63) - static_cast<uint64>(1 << 10);
DCHECK(limit > 0);
const double kLargestDoubleLessThanInt64Max = limit;
const double kSmallestDoubleGreaterThanInt64Min = -limit;
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 CoerceJavaScriptNumberToJavaValue(const NPVariant& variant,
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. For double values, the logic is more involved.
jvalue result;
DCHECK(variant.type == NPVariantType_Int32 ||
variant.type == NPVariantType_Double);
bool is_double = variant.type == NPVariantType_Double;
switch (target_type.type) {
case JavaType::TypeByte:
result.b = is_double ?
static_cast<jbyte>(RoundDoubleToInt(NPVARIANT_TO_DOUBLE(variant))) :
static_cast<jbyte>(NPVARIANT_TO_INT32(variant));
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 = is_double ? 0 :
static_cast<jchar>(NPVARIANT_TO_INT32(variant));
break;
case JavaType::TypeShort:
result.s = is_double ?
static_cast<jshort>(RoundDoubleToInt(NPVARIANT_TO_DOUBLE(variant))) :
static_cast<jshort>(NPVARIANT_TO_INT32(variant));
break;
case JavaType::TypeInt:
result.i = is_double ? RoundDoubleToInt(NPVARIANT_TO_DOUBLE(variant)) :
NPVARIANT_TO_INT32(variant);
break;
case JavaType::TypeLong:
result.j = is_double ? RoundDoubleToLong(NPVARIANT_TO_DOUBLE(variant)) :
NPVARIANT_TO_INT32(variant);
break;
case JavaType::TypeFloat:
result.f = is_double ? static_cast<jfloat>(NPVARIANT_TO_DOUBLE(variant)) :
NPVARIANT_TO_INT32(variant);
break;
case JavaType::TypeDouble:
result.d = is_double ? NPVARIANT_TO_DOUBLE(variant) :
NPVARIANT_TO_INT32(variant);
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(
AttachCurrentThread(),
is_double ?
base::StringPrintf("%.6lg", NPVARIANT_TO_DOUBLE(variant)) :
base::Int64ToString(NPVARIANT_TO_INT32(variant))).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(const NPVariant& variant,
const JavaType& target_type,
bool coerce_to_string) {
// See http://jdk6.java.net/plugin2/liveconnect/#JS_BOOLEAN_VALUES.
DCHECK_EQ(NPVariantType_Bool, variant.type);
bool boolean_value = NPVARIANT_TO_BOOLEAN(variant);
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(AttachCurrentThread(),
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(const NPVariant& variant,
const JavaType& target_type) {
// See http://jdk6.java.net/plugin2/liveconnect/#JS_STRING_VALUES.
DCHECK_EQ(NPVariantType_String, variant.type);
jvalue result;
switch (target_type.type) {
case JavaType::TypeString:
result.l = ConvertUTF8ToJavaString(
AttachCurrentThread(),
base::StringPiece(NPVARIANT_TO_STRING(variant).UTF8Characters,
NPVARIANT_TO_STRING(variant).UTF8Length)).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(const JavaType& type, jsize length) {
JNIEnv* env = AttachCurrentThread();
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: {
ScopedJavaLocalRef<jclass> clazz(GetClass(env, "java/lang/String"));
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(jobject array,
const JavaType& type,
jsize index,
const jvalue& value) {
JNIEnv* env = AttachCurrentThread();
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);
}
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(const NPVariant& variant,
const JavaType& target_type,
bool coerce_to_string);
// Returns a new local reference to a Java array.
jobject CoerceJavaScriptObjectToArray(const NPVariant& variant,
const JavaType& target_type) {
DCHECK_EQ(JavaType::TypeArray, target_type.type);
NPObject* object = NPVARIANT_TO_OBJECT(variant);
DCHECK_NE(&JavaNPObject::kNPClass, object->_class);
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;
}
// If the object does not have a length property, return null.
NPVariant length_variant;
if (!WebBindings::getProperty(0, object,
WebBindings::getStringIdentifier("length"),
&length_variant)) {
WebBindings::releaseVariantValue(&length_variant);
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 (NPVARIANT_IS_INT32(length_variant)
&& NPVARIANT_TO_INT32(length_variant) >= 0) {
length = NPVARIANT_TO_INT32(length_variant);
} else if (NPVARIANT_IS_DOUBLE(length_variant)
&& NPVARIANT_TO_DOUBLE(length_variant) >= 0.0
&& NPVARIANT_TO_DOUBLE(length_variant) <= kint32max) {
length = static_cast<jsize>(NPVARIANT_TO_DOUBLE(length_variant));
}
WebBindings::releaseVariantValue(&length_variant);
if (length == -1) {
return NULL;
}
// Create the Java array.
// TODO(steveblock): Handle failure to create the array.
jobject result = CreateJavaArray(target_inner_type, length);
NPVariant value_variant;
JNIEnv* env = AttachCurrentThread();
for (jsize i = 0; i < length; ++i) {
// It seems that getProperty() will set the variant to type void on failure,
// but this doesn't seem to be documented, so do it explicitly here for
// safety.
VOID_TO_NPVARIANT(value_variant);
// If this fails, for example due to a missing element, we simply treat the
// value as JavaScript undefined.
WebBindings::getProperty(0, object, WebBindings::getIntIdentifier(i),
&value_variant);
jvalue element = CoerceJavaScriptValueToJavaValue(value_variant,
target_inner_type,
false);
SetArrayElement(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);
WebBindings::releaseVariantValue(&value_variant);
}
return result;
}
jvalue CoerceJavaScriptObjectToJavaValue(const NPVariant& variant,
const JavaType& target_type,
bool coerce_to_string) {
// 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
DCHECK_EQ(NPVariantType_Object, variant.type);
NPObject* object = NPVARIANT_TO_OBJECT(variant);
bool is_java_object = &JavaNPObject::kNPClass == object->_class;
jvalue result;
switch (target_type.type) {
case JavaType::TypeObject:
if (is_java_object) {
// LIVECONNECT_COMPLIANCE: Existing behavior is to pass all Java
// objects. Spec requires passing only Java objects which are
// assignment-compatibile.
result.l = AttachCurrentThread()->NewLocalRef(
JavaBoundObject::GetJavaObject(object).obj());
} 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(AttachCurrentThread(), "undefined").
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 (is_java_object) {
// LIVECONNECT_COMPLIANCE: Existing behavior is to convert to NULL. Spec
// requires raising a JavaScript exception.
result.l = NULL;
} else {
result.l = CoerceJavaScriptObjectToArray(variant, target_type);
}
break;
case JavaType::TypeVoid:
// Conversion to void must never happen.
NOTREACHED();
break;
}
return result;
}
jvalue CoerceJavaScriptNullOrUndefinedToJavaValue(const NPVariant& variant,
const JavaType& target_type,
bool coerce_to_string) {
// See http://jdk6.java.net/plugin2/liveconnect/#JS_NULL.
DCHECK(variant.type == NPVariantType_Null ||
variant.type == NPVariantType_Void);
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 && variant.type == NPVariantType_Void) ?
ConvertUTF8ToJavaString(AttachCurrentThread(), "undefined").
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;
}
// coerce_to_string means that we should try to coerce all JavaScript values to
// strings when required, rather than simply converting to NULL. This is used
// to maintain current behaviour, which differs slightly depending upon whether
// or not the coercion in question is for an array element.
//
// Note that the jvalue returned by this method may contain a new local
// reference to an object (string, object or array). This must be released by
// the caller.
jvalue CoerceJavaScriptValueToJavaValue(const NPVariant& variant,
const JavaType& target_type,
bool coerce_to_string) {
// 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 (variant.type) {
case NPVariantType_Int32:
case NPVariantType_Double:
return CoerceJavaScriptNumberToJavaValue(variant, target_type,
coerce_to_string);
case NPVariantType_Bool:
return CoerceJavaScriptBooleanToJavaValue(variant, target_type,
coerce_to_string);
case NPVariantType_String:
return CoerceJavaScriptStringToJavaValue(variant, target_type);
case NPVariantType_Object:
return CoerceJavaScriptObjectToJavaValue(variant, target_type,
coerce_to_string);
case NPVariantType_Null:
case NPVariantType_Void:
return CoerceJavaScriptNullOrUndefinedToJavaValue(variant, target_type,
coerce_to_string);
}
NOTREACHED();
return jvalue();
}
} // namespace
NPObject* JavaBoundObject::Create(
const JavaRef<jobject>& object,
const JavaRef<jclass>& safe_annotation_clazz,
const base::WeakPtr<JavaBridgeDispatcherHostManager>& manager,
bool can_enumerate_methods) {
// The first argument (a plugin's instance handle) is passed through to the
// allocate function directly, and we don't use it, so it's ok to be 0.
// The object is created with a ref count of one.
NPObject* np_object = WebBindings::createObject(0, const_cast<NPClass*>(
&JavaNPObject::kNPClass));
// The NPObject takes ownership of the JavaBoundObject.
reinterpret_cast<JavaNPObject*>(np_object)->bound_object =
new JavaBoundObject(
object, safe_annotation_clazz, manager, can_enumerate_methods);
return np_object;
}
JavaBoundObject::JavaBoundObject(
const JavaRef<jobject>& object,
const JavaRef<jclass>& safe_annotation_clazz,
const base::WeakPtr<JavaBridgeDispatcherHostManager>& manager,
bool can_enumerate_methods)
: java_object_(AttachCurrentThread(), object.obj()),
manager_(manager),
are_methods_set_up_(false),
object_get_class_method_id_(NULL),
can_enumerate_methods_(can_enumerate_methods),
safe_annotation_clazz_(safe_annotation_clazz) {
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::Bind(&JavaBridgeDispatcherHostManager::JavaBoundObjectCreated,
manager_,
base::android::ScopedJavaGlobalRef<jobject>(object)));
// Other than informing the JavaBridgeDispatcherHostManager that a java bound
// object has been created (above), we don't do anything else with our Java
// object when first created. We do it all lazily when a method is first
// invoked.
}
JavaBoundObject::~JavaBoundObject() {
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::Bind(&JavaBridgeDispatcherHostManager::JavaBoundObjectDestroyed,
manager_,
base::android::ScopedJavaGlobalRef<jobject>(
java_object_.get(AttachCurrentThread()))));
}
ScopedJavaLocalRef<jobject> JavaBoundObject::GetJavaObject(NPObject* object) {
DCHECK_EQ(&JavaNPObject::kNPClass, object->_class);
JavaBoundObject* jbo = reinterpret_cast<JavaNPObject*>(object)->bound_object;
return jbo->java_object_.get(AttachCurrentThread());
}
std::vector<std::string> JavaBoundObject::GetMethodNames() const {
EnsureMethodsAreSetUp();
std::vector<std::string> result;
for (JavaMethodMap::const_iterator it = methods_.begin();
it != methods_.end();
it = methods_.upper_bound(it->first)) {
result.push_back(it->first);
}
return result;
}
bool JavaBoundObject::HasMethod(const std::string& name) const {
EnsureMethodsAreSetUp();
return methods_.find(name) != methods_.end();
}
bool JavaBoundObject::Invoke(const std::string& name, const NPVariant* args,
size_t arg_count, NPVariant* result) {
EnsureMethodsAreSetUp();
// Get all methods with the correct name.
std::pair<JavaMethodMap::const_iterator, JavaMethodMap::const_iterator>
iters = methods_.equal_range(name);
if (iters.first == iters.second) {
return false;
}
// Take the first method with the correct number of arguments.
JavaMethod* method = NULL;
for (JavaMethodMap::const_iterator iter = iters.first; iter != iters.second;
++iter) {
if (iter->second->num_parameters() == arg_count) {
method = iter->second.get();
break;
}
}
if (!method) {
return false;
}
// Block access to java.lang.Object.getClass.
// As it is declared to be final, it is sufficient to compare methodIDs.
if (method->id() == object_get_class_method_id_) {
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::Bind(&JavaBoundObject::ThrowSecurityException,
kAccessToObjectGetClassIsBlocked));
return false;
}
// Coerce
std::vector<jvalue> parameters(arg_count);
for (size_t i = 0; i < arg_count; ++i) {
parameters[i] = CoerceJavaScriptValueToJavaValue(args[i],
method->parameter_type(i),
true);
}
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> obj;
ScopedJavaLocalRef<jclass> cls;
bool ok = false;
if (method->is_static()) {
cls = GetLocalClassRef(env);
} else {
obj = java_object_.get(env);
}
if (!obj.is_null() || !cls.is_null()) {
// Call
ok = CallJNIMethod(obj.obj(), cls.obj(), method->return_type(),
method->id(), &parameters[0], result,
safe_annotation_clazz_,
manager_,
can_enumerate_methods_);
}
// Now that we're done with the jvalue, release any local references created
// by CoerceJavaScriptValueToJavaValue().
for (size_t i = 0; i < arg_count; ++i) {
ReleaseJavaValueIfRequired(env, &parameters[i], method->parameter_type(i));
}
return ok;
}
ScopedJavaLocalRef<jclass> JavaBoundObject::GetLocalClassRef(
JNIEnv* env) const {
if (!object_get_class_method_id_) {
object_get_class_method_id_ = GetMethodIDFromClassName(
env, kJavaLangObject, kGetClass, kReturningJavaLangClass);
}
ScopedJavaLocalRef<jobject> obj = java_object_.get(env);
if (!obj.is_null()) {
return ScopedJavaLocalRef<jclass>(env, static_cast<jclass>(
env->CallObjectMethod(obj.obj(), object_get_class_method_id_)));
} else {
return ScopedJavaLocalRef<jclass>();
}
}
void JavaBoundObject::EnsureMethodsAreSetUp() const {
if (are_methods_set_up_)
return;
are_methods_set_up_ = true;
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jclass> clazz = GetLocalClassRef(env);
if (clazz.is_null()) {
return;
}
ScopedJavaLocalRef<jobjectArray> methods(env, static_cast<jobjectArray>(
env->CallObjectMethod(clazz.obj(), GetMethodIDFromClassName(
env,
kJavaLangClass,
kGetMethods,
kReturningJavaLangReflectMethodArray))));
size_t num_methods = env->GetArrayLength(methods.obj());
// Java objects always have public methods.
DCHECK(num_methods);
for (size_t i = 0; i < num_methods; ++i) {
ScopedJavaLocalRef<jobject> java_method(
env,
env->GetObjectArrayElement(methods.obj(), i));
if (!safe_annotation_clazz_.is_null()) {
jboolean safe = env->CallBooleanMethod(java_method.obj(),
GetMethodIDFromClassName(
env,
kJavaLangReflectMethod,
kIsAnnotationPresent,
kTakesJavaLangClassReturningBoolean),
safe_annotation_clazz_.obj());
if (!safe)
continue;
}
JavaMethod* method = new JavaMethod(java_method);
methods_.insert(std::make_pair(method->name(), method));
}
}
// static
void JavaBoundObject::ThrowSecurityException(const char* message) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
JNIEnv* env = AttachCurrentThread();
base::android::ScopedJavaLocalRef<jclass> clazz(
env, env->FindClass(kJavaLangSecurityExceptionClass));
env->ThrowNew(clazz.obj(), message);
}
} // namespace content