зеркало из https://github.com/mozilla/gecko-dev.git
1115 строки
35 KiB
C++
1115 строки
35 KiB
C++
/* -*- Mode: c++; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
|
* vim: set sw=4 ts=4 expandtab:
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "EventDispatcher.h"
|
|
|
|
#include "JavaBuiltins.h"
|
|
#include "nsAppShell.h"
|
|
#include "nsIXPConnect.h"
|
|
#include "nsJSUtils.h"
|
|
#include "xpcpublic.h"
|
|
|
|
#include "mozilla/ScopeExit.h"
|
|
#include "mozilla/dom/ScriptSettings.h"
|
|
|
|
namespace mozilla {
|
|
namespace widget {
|
|
|
|
namespace detail {
|
|
|
|
bool
|
|
CheckJS(JSContext* aCx, bool aResult)
|
|
{
|
|
if (!aResult) {
|
|
JS_ClearPendingException(aCx);
|
|
}
|
|
return aResult;
|
|
}
|
|
|
|
nsresult
|
|
BoxString(JSContext* aCx, JS::HandleValue aData, jni::Object::LocalRef& aOut)
|
|
{
|
|
if (aData.isNullOrUndefined()) {
|
|
aOut = nullptr;
|
|
return NS_OK;
|
|
}
|
|
|
|
MOZ_ASSERT(aData.isString());
|
|
|
|
JS::RootedString str(aCx, aData.toString());
|
|
|
|
if (JS_StringHasLatin1Chars(str)) {
|
|
nsAutoJSString autoStr;
|
|
NS_ENSURE_TRUE(CheckJS(aCx, autoStr.init(aCx, str)), NS_ERROR_FAILURE);
|
|
|
|
// StringParam can automatically convert a nsString to jstring.
|
|
aOut = jni::StringParam(autoStr, aOut.Env());
|
|
return NS_OK;
|
|
}
|
|
|
|
// Two-byte string
|
|
JNIEnv* const env = aOut.Env();
|
|
const char16_t* chars;
|
|
{
|
|
JS::AutoCheckCannotGC nogc;
|
|
size_t len = 0;
|
|
chars = JS_GetTwoByteStringCharsAndLength(aCx, nogc, str, &len);
|
|
if (chars) {
|
|
aOut = jni::String::LocalRef::Adopt(env,
|
|
env->NewString(reinterpret_cast<const jchar*>(chars), len));
|
|
}
|
|
}
|
|
if (NS_WARN_IF(!CheckJS(aCx, !!chars) || !aOut)) {
|
|
env->ExceptionClear();
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
BoxObject(JSContext* aCx, JS::HandleValue aData, jni::Object::LocalRef& aOut);
|
|
|
|
template<typename Type,
|
|
bool (JS::Value::*IsType)() const,
|
|
Type (JS::Value::*ToType)() const,
|
|
class ArrayType,
|
|
typename ArrayType::LocalRef (*NewArray)(const Type*, size_t)> nsresult
|
|
BoxArrayPrimitive(JSContext* aCx, JS::HandleObject aData,
|
|
jni::Object::LocalRef& aOut, size_t aLength,
|
|
JS::HandleValue aElement)
|
|
{
|
|
JS::RootedValue element(aCx);
|
|
auto data = MakeUnique<Type[]>(aLength);
|
|
data[0] = (aElement.get().*ToType)();
|
|
|
|
for (size_t i = 1; i < aLength; i++) {
|
|
NS_ENSURE_TRUE(CheckJS(aCx, JS_GetElement(aCx, aData, i, &element)),
|
|
NS_ERROR_FAILURE);
|
|
NS_ENSURE_TRUE((element.get().*IsType)(), NS_ERROR_INVALID_ARG);
|
|
|
|
data[i] = (element.get().*ToType)();
|
|
}
|
|
aOut = (*NewArray)(data.get(), aLength);
|
|
return NS_OK;
|
|
}
|
|
|
|
template<class Type,
|
|
nsresult (*Box)(JSContext*, JS::HandleValue, jni::Object::LocalRef&),
|
|
typename IsType>
|
|
nsresult
|
|
BoxArrayObject(JSContext* aCx, JS::HandleObject aData,
|
|
jni::Object::LocalRef& aOut, size_t aLength,
|
|
JS::HandleValue aElement,
|
|
IsType&& aIsType)
|
|
{
|
|
auto out = jni::ObjectArray::New<Type>(aLength);
|
|
JS::RootedValue element(aCx);
|
|
jni::Object::LocalRef jniElement(aOut.Env());
|
|
|
|
nsresult rv = (*Box)(aCx, aElement, jniElement);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
out->SetElement(0, jniElement);
|
|
|
|
for (size_t i = 1; i < aLength; i++) {
|
|
NS_ENSURE_TRUE(CheckJS(aCx, JS_GetElement(aCx, aData, i, &element)),
|
|
NS_ERROR_FAILURE);
|
|
NS_ENSURE_TRUE(element.isNullOrUndefined() || aIsType(element),
|
|
NS_ERROR_INVALID_ARG);
|
|
|
|
rv = (*Box)(aCx, element, jniElement);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
out->SetElement(i, jniElement);
|
|
}
|
|
aOut = out;
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
BoxArray(JSContext* aCx, JS::HandleObject aData, jni::Object::LocalRef& aOut)
|
|
{
|
|
uint32_t length = 0;
|
|
NS_ENSURE_TRUE(CheckJS(aCx, JS_GetArrayLength(aCx, aData, &length)),
|
|
NS_ERROR_FAILURE);
|
|
|
|
if (!length) {
|
|
// Always represent empty arrays as an empty boolean array.
|
|
aOut = java::GeckoBundle::EMPTY_BOOLEAN_ARRAY();
|
|
return NS_OK;
|
|
}
|
|
|
|
// We only check the first element's type. If the array has mixed types,
|
|
// we'll throw an error during actual conversion.
|
|
JS::RootedValue element(aCx);
|
|
NS_ENSURE_TRUE(CheckJS(aCx, JS_GetElement(aCx, aData, 0, &element)),
|
|
NS_ERROR_FAILURE);
|
|
|
|
if (element.isBoolean()) {
|
|
return BoxArrayPrimitive<bool, &JS::Value::isBoolean, &JS::Value::toBoolean,
|
|
jni::BooleanArray, &jni::BooleanArray::New>(
|
|
aCx, aData, aOut, length, element);
|
|
}
|
|
|
|
if (element.isInt32()) {
|
|
nsresult rv = BoxArrayPrimitive<
|
|
int32_t, &JS::Value::isInt32, &JS::Value::toInt32,
|
|
jni::IntArray, &jni::IntArray::New>(aCx, aData, aOut,
|
|
length, element);
|
|
if (rv != NS_ERROR_INVALID_ARG) {
|
|
return rv;
|
|
}
|
|
// Not int32, but we can still try a double array.
|
|
}
|
|
|
|
if (element.isNumber()) {
|
|
return BoxArrayPrimitive<
|
|
double, &JS::Value::isNumber, &JS::Value::toNumber,
|
|
jni::DoubleArray, &jni::DoubleArray::New>(aCx, aData, aOut,
|
|
length, element);
|
|
}
|
|
|
|
if (element.isNullOrUndefined() || element.isString()) {
|
|
const auto isString = [] (JS::HandleValue val) -> bool {
|
|
return val.isString();
|
|
};
|
|
nsresult rv = BoxArrayObject<jni::String, &BoxString>(
|
|
aCx, aData, aOut, length, element, isString);
|
|
if (element.isString() || rv != NS_ERROR_INVALID_ARG) {
|
|
return rv;
|
|
}
|
|
// First element was null/undefined, so it may still be an object array.
|
|
}
|
|
|
|
const auto isObject = [aCx] (JS::HandleValue val) -> bool {
|
|
if (!val.isObject()) {
|
|
return false;
|
|
}
|
|
bool array = false;
|
|
JS::RootedObject obj(aCx, &val.toObject());
|
|
// We don't support array of arrays.
|
|
return CheckJS(aCx, JS_IsArrayObject(aCx, obj, &array)) && !array;
|
|
};
|
|
|
|
if (element.isNullOrUndefined() || isObject(element)) {
|
|
return BoxArrayObject<java::GeckoBundle, &BoxObject>(
|
|
aCx, aData, aOut, length, element, isObject);
|
|
}
|
|
|
|
NS_WARNING("Unknown type");
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
|
|
nsresult
|
|
BoxValue(JSContext* aCx, JS::HandleValue aData, jni::Object::LocalRef& aOut);
|
|
|
|
nsresult
|
|
BoxObject(JSContext* aCx, JS::HandleValue aData, jni::Object::LocalRef& aOut)
|
|
{
|
|
if (aData.isNullOrUndefined()) {
|
|
aOut = nullptr;
|
|
return NS_OK;
|
|
}
|
|
|
|
MOZ_ASSERT(aData.isObject());
|
|
|
|
JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx));
|
|
JS::RootedObject obj(aCx, &aData.toObject());
|
|
|
|
bool isArray = false;
|
|
if (CheckJS(aCx, JS_IsArrayObject(aCx, obj, &isArray)) && isArray) {
|
|
return BoxArray(aCx, obj, aOut);
|
|
}
|
|
|
|
NS_ENSURE_TRUE(CheckJS(aCx, JS_Enumerate(aCx, obj, &ids)),
|
|
NS_ERROR_FAILURE);
|
|
|
|
const size_t length = ids.length();
|
|
auto keys = jni::ObjectArray::New<jni::String>(length);
|
|
auto values = jni::ObjectArray::New<jni::Object>(length);
|
|
|
|
// Iterate through each property of the JS object.
|
|
for (size_t i = 0; i < ids.length(); i++) {
|
|
const JS::RootedId id(aCx, ids[i]);
|
|
JS::RootedValue idVal(aCx);
|
|
JS::RootedValue val(aCx);
|
|
jni::Object::LocalRef key(aOut.Env());
|
|
jni::Object::LocalRef value(aOut.Env());
|
|
|
|
NS_ENSURE_TRUE(CheckJS(aCx, JS_IdToValue(aCx, id, &idVal)),
|
|
NS_ERROR_FAILURE);
|
|
|
|
JS::RootedString idStr(aCx, JS::ToString(aCx, idVal));
|
|
NS_ENSURE_TRUE(CheckJS(aCx, !!idStr), NS_ERROR_FAILURE);
|
|
|
|
idVal.setString(idStr);
|
|
NS_ENSURE_SUCCESS(BoxString(aCx, idVal, key), NS_ERROR_FAILURE);
|
|
NS_ENSURE_TRUE(CheckJS(aCx, JS_GetPropertyById(aCx, obj, id, &val)),
|
|
NS_ERROR_FAILURE);
|
|
|
|
nsresult rv = BoxValue(aCx, val, value);
|
|
if (rv == NS_ERROR_INVALID_ARG && !JS_IsExceptionPending(aCx)) {
|
|
nsAutoJSString autoStr;
|
|
if (CheckJS(aCx, autoStr.init(aCx, idVal.toString()))) {
|
|
JS_ReportErrorUTF8(
|
|
aCx, u8"Invalid event data property %s",
|
|
NS_ConvertUTF16toUTF8(autoStr).get());
|
|
}
|
|
}
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
keys->SetElement(i, key);
|
|
values->SetElement(i, value);
|
|
}
|
|
|
|
aOut = java::GeckoBundle::New(keys, values);
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
BoxValue(JSContext* aCx, JS::HandleValue aData, jni::Object::LocalRef& aOut)
|
|
{
|
|
if (aData.isNullOrUndefined()) {
|
|
aOut = nullptr;
|
|
} else if (aData.isBoolean()) {
|
|
aOut = aData.toBoolean() ? java::sdk::Boolean::TRUE()
|
|
: java::sdk::Boolean::FALSE();
|
|
} else if (aData.isInt32()) {
|
|
aOut = java::sdk::Integer::ValueOf(aData.toInt32());
|
|
} else if (aData.isNumber()) {
|
|
aOut = java::sdk::Double::New(aData.toNumber());
|
|
} else if (aData.isString()) {
|
|
return BoxString(aCx, aData, aOut);
|
|
} else if (aData.isObject()) {
|
|
return BoxObject(aCx, aData, aOut);
|
|
} else {
|
|
NS_WARNING("Unknown type");
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
BoxData(const nsAString& aEvent, JSContext* aCx, JS::HandleValue aData,
|
|
jni::Object::LocalRef& aOut, bool aObjectOnly)
|
|
{
|
|
nsresult rv = NS_ERROR_INVALID_ARG;
|
|
|
|
if (!aObjectOnly) {
|
|
rv = BoxValue(aCx, aData, aOut);
|
|
} else if (aData.isObject() || aData.isNullOrUndefined()) {
|
|
rv = BoxObject(aCx, aData, aOut);
|
|
}
|
|
if (rv != NS_ERROR_INVALID_ARG) {
|
|
return rv;
|
|
}
|
|
|
|
NS_ConvertUTF16toUTF8 event(aEvent);
|
|
if (JS_IsExceptionPending(aCx)) {
|
|
JS_ReportWarningUTF8(aCx, "Error dispatching %s", event.get());
|
|
} else {
|
|
JS_ReportErrorUTF8(aCx, "Invalid event data for %s", event.get());
|
|
}
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
|
|
nsresult
|
|
UnboxString(JSContext* aCx, const jni::Object::LocalRef& aData,
|
|
JS::MutableHandleValue aOut)
|
|
{
|
|
if (!aData) {
|
|
aOut.setNull();
|
|
return NS_OK;
|
|
}
|
|
|
|
MOZ_ASSERT(aData.IsInstanceOf<jni::String>());
|
|
|
|
JNIEnv* const env = aData.Env();
|
|
const jstring jstr = jstring(aData.Get());
|
|
const size_t len = env->GetStringLength(jstr);
|
|
const jchar* const jchars = env->GetStringChars(jstr, nullptr);
|
|
|
|
if (NS_WARN_IF(!jchars)) {
|
|
env->ExceptionClear();
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
auto releaseStr = MakeScopeExit([env, jstr, jchars] {
|
|
env->ReleaseStringChars(jstr, jchars);
|
|
env->ExceptionClear();
|
|
});
|
|
|
|
JS::RootedString str(aCx, JS_NewUCStringCopyN(
|
|
aCx, reinterpret_cast<const char16_t*>(jchars), len));
|
|
NS_ENSURE_TRUE(CheckJS(aCx, !!str), NS_ERROR_FAILURE);
|
|
|
|
aOut.setString(str);
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
UnboxValue(JSContext* aCx, const jni::Object::LocalRef& aData,
|
|
JS::MutableHandleValue aOut);
|
|
|
|
nsresult
|
|
UnboxBundle(JSContext* aCx, const jni::Object::LocalRef& aData,
|
|
JS::MutableHandleValue aOut)
|
|
{
|
|
if (!aData) {
|
|
aOut.setNull();
|
|
return NS_OK;
|
|
}
|
|
|
|
MOZ_ASSERT(aData.IsInstanceOf<java::GeckoBundle>());
|
|
|
|
JNIEnv* const env = aData.Env();
|
|
const auto& bundle = java::GeckoBundle::Ref::From(aData);
|
|
jni::ObjectArray::LocalRef keys = bundle->Keys();
|
|
jni::ObjectArray::LocalRef values = bundle->Values();
|
|
const size_t len = keys->Length();
|
|
JS::RootedObject obj(aCx, JS_NewPlainObject(aCx));
|
|
|
|
NS_ENSURE_TRUE(CheckJS(aCx, !!obj), NS_ERROR_FAILURE);
|
|
NS_ENSURE_TRUE(values->Length() == len, NS_ERROR_FAILURE);
|
|
|
|
for (size_t i = 0; i < len; i++) {
|
|
jni::String::LocalRef key = keys->GetElement(i);
|
|
const size_t keyLen = env->GetStringLength(key.Get());
|
|
const jchar* const keyChars = env->GetStringChars(key.Get(), nullptr);
|
|
if (NS_WARN_IF(!keyChars)) {
|
|
env->ExceptionClear();
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
auto releaseKeyChars = MakeScopeExit([env, &key, keyChars] {
|
|
env->ReleaseStringChars(key.Get(), keyChars);
|
|
env->ExceptionClear();
|
|
});
|
|
|
|
JS::RootedValue value(aCx);
|
|
nsresult rv = UnboxValue(aCx, values->GetElement(i), &value);
|
|
if (rv == NS_ERROR_INVALID_ARG && !JS_IsExceptionPending(aCx)) {
|
|
JS_ReportErrorUTF8(
|
|
aCx, u8"Invalid event data property %s",
|
|
NS_ConvertUTF16toUTF8(nsString(reinterpret_cast<
|
|
const char16_t*>(keyChars), keyLen)).get());
|
|
}
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
NS_ENSURE_TRUE(CheckJS(aCx, JS_SetUCProperty(aCx, obj, reinterpret_cast<
|
|
const char16_t*>(keyChars), keyLen, value)),
|
|
NS_ERROR_FAILURE);
|
|
}
|
|
|
|
aOut.setObject(*obj);
|
|
return NS_OK;
|
|
}
|
|
|
|
template<typename Type, typename JNIType, typename ArrayType,
|
|
JNIType* (JNIEnv::*GetElements)(ArrayType, jboolean*),
|
|
void (JNIEnv::*ReleaseElements)(ArrayType, JNIType*, jint),
|
|
JS::Value (*ToValue)(Type)> nsresult
|
|
UnboxArrayPrimitive(JSContext* aCx, const jni::Object::LocalRef& aData,
|
|
JS::MutableHandleValue aOut)
|
|
{
|
|
JNIEnv* const env = aData.Env();
|
|
const ArrayType jarray = ArrayType(aData.Get());
|
|
JNIType* const array = (env->*GetElements)(jarray, nullptr);
|
|
JS::AutoValueVector elements(aCx);
|
|
|
|
if (NS_WARN_IF(!array)) {
|
|
env->ExceptionClear();
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
auto releaseArray = MakeScopeExit([env, jarray, array] {
|
|
(env->*ReleaseElements)(jarray, array, JNI_ABORT);
|
|
env->ExceptionClear();
|
|
});
|
|
|
|
const size_t len = env->GetArrayLength(jarray);
|
|
NS_ENSURE_TRUE(elements.initCapacity(len), NS_ERROR_FAILURE);
|
|
|
|
for (size_t i = 0; i < len; i++) {
|
|
NS_ENSURE_TRUE(elements.append((*ToValue)(Type(array[i]))),
|
|
NS_ERROR_FAILURE);
|
|
}
|
|
|
|
JS::RootedObject obj(aCx, JS_NewArrayObject(
|
|
aCx, JS::HandleValueArray(elements)));
|
|
NS_ENSURE_TRUE(CheckJS(aCx, !!obj), NS_ERROR_FAILURE);
|
|
|
|
aOut.setObject(*obj);
|
|
return NS_OK;
|
|
}
|
|
|
|
struct StringArray : jni::ObjectBase<StringArray>
|
|
{
|
|
static const char name[];
|
|
};
|
|
|
|
struct GeckoBundleArray : jni::ObjectBase<GeckoBundleArray>
|
|
{
|
|
static const char name[];
|
|
};
|
|
|
|
const char StringArray::name[] = "[Ljava/lang/String;";
|
|
const char GeckoBundleArray::name[] = "[Lorg/mozilla/gecko/util/GeckoBundle;";
|
|
|
|
template<nsresult (*Unbox)(JSContext*, const jni::Object::LocalRef&,
|
|
JS::MutableHandleValue)> nsresult
|
|
UnboxArrayObject(JSContext* aCx, const jni::Object::LocalRef& aData,
|
|
JS::MutableHandleValue aOut)
|
|
{
|
|
jni::ObjectArray::LocalRef array(
|
|
aData.Env(), jni::ObjectArray::Ref::From(aData));
|
|
const size_t len = array->Length();
|
|
JS::RootedObject obj(aCx, JS_NewArrayObject(aCx, len));
|
|
NS_ENSURE_TRUE(CheckJS(aCx, !!obj), NS_ERROR_FAILURE);
|
|
|
|
for (size_t i = 0; i < len; i++) {
|
|
jni::Object::LocalRef element = array->GetElement(i);
|
|
JS::RootedValue value(aCx);
|
|
nsresult rv = (*Unbox)(aCx, element, &value);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
NS_ENSURE_TRUE(CheckJS(aCx, JS_SetElement(aCx, obj, i, value)),
|
|
NS_ERROR_FAILURE);
|
|
}
|
|
|
|
aOut.setObject(*obj);
|
|
return NS_OK;
|
|
}
|
|
|
|
template<class T> jfieldID
|
|
GetValueFieldID(const char* aType)
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
JNIEnv* const env = jni::GetGeckoThreadEnv();
|
|
const jfieldID id = env->GetFieldID(
|
|
typename T::Context(env, nullptr).ClassRef(), "value", aType);
|
|
env->ExceptionClear();
|
|
return id;
|
|
}
|
|
|
|
nsresult
|
|
UnboxValue(JSContext* aCx, const jni::Object::LocalRef& aData,
|
|
JS::MutableHandleValue aOut)
|
|
{
|
|
static jfieldID booleanValueField =
|
|
GetValueFieldID<java::sdk::Boolean>("Z");
|
|
static jfieldID intValueField = GetValueFieldID<java::sdk::Integer>("I");
|
|
static jfieldID doubleValueField = GetValueFieldID<java::sdk::Double>("D");
|
|
|
|
if (!aData) {
|
|
aOut.setNull();
|
|
} else if (aData.IsInstanceOf<jni::Boolean>()) {
|
|
if (booleanValueField) {
|
|
aOut.setBoolean(aData.Env()->GetBooleanField(
|
|
aData.Get(), booleanValueField) != JNI_FALSE);
|
|
MOZ_CATCH_JNI_EXCEPTION(aData.Env());
|
|
} else {
|
|
aOut.setBoolean(
|
|
java::sdk::Boolean::Ref::From(aData)->BooleanValue());
|
|
}
|
|
} else if (aData.IsInstanceOf<jni::Integer>()) {
|
|
if (intValueField) {
|
|
aOut.setInt32(aData.Env()->GetIntField(aData.Get(), intValueField));
|
|
MOZ_CATCH_JNI_EXCEPTION(aData.Env());
|
|
} else {
|
|
aOut.setInt32(java::sdk::Number::Ref::From(aData)->IntValue());
|
|
}
|
|
} else if (aData.IsInstanceOf<jni::Byte>() ||
|
|
aData.IsInstanceOf<jni::Short>()) {
|
|
aOut.setInt32(java::sdk::Number::Ref::From(aData)->IntValue());
|
|
} else if (aData.IsInstanceOf<jni::Double>()) {
|
|
if (doubleValueField) {
|
|
aOut.setNumber(aData.Env()->GetDoubleField(
|
|
aData.Get(), doubleValueField));
|
|
} else {
|
|
aOut.setNumber(java::sdk::Number::Ref::From(aData)->DoubleValue());
|
|
}
|
|
} else if (aData.IsInstanceOf<jni::Float>() ||
|
|
aData.IsInstanceOf<jni::Long>()) {
|
|
aOut.setNumber(java::sdk::Number::Ref::From(aData)->DoubleValue());
|
|
} else if (aData.IsInstanceOf<jni::String>()) {
|
|
return UnboxString(aCx, aData, aOut);
|
|
} else if (aData.IsInstanceOf<jni::Character>()) {
|
|
return UnboxString(aCx, java::sdk::String::ValueOf(aData), aOut);
|
|
} else if (aData.IsInstanceOf<java::GeckoBundle>()) {
|
|
return UnboxBundle(aCx, aData, aOut);
|
|
|
|
} else if (aData.IsInstanceOf<jni::BooleanArray>()) {
|
|
return UnboxArrayPrimitive<bool, jboolean, jbooleanArray,
|
|
&JNIEnv::GetBooleanArrayElements,
|
|
&JNIEnv::ReleaseBooleanArrayElements,
|
|
&JS::BooleanValue>(aCx, aData, aOut);
|
|
|
|
} else if (aData.IsInstanceOf<jni::IntArray>()) {
|
|
return UnboxArrayPrimitive<int32_t, jint, jintArray,
|
|
&JNIEnv::GetIntArrayElements,
|
|
&JNIEnv::ReleaseIntArrayElements,
|
|
&JS::Int32Value>(aCx, aData, aOut);
|
|
|
|
} else if (aData.IsInstanceOf<jni::DoubleArray>()) {
|
|
return UnboxArrayPrimitive<double, jdouble, jdoubleArray,
|
|
&JNIEnv::GetDoubleArrayElements,
|
|
&JNIEnv::ReleaseDoubleArrayElements,
|
|
&JS::DoubleValue>(aCx, aData, aOut);
|
|
|
|
} else if (aData.IsInstanceOf<StringArray>()) {
|
|
return UnboxArrayObject<&UnboxString>(aCx, aData, aOut);
|
|
} else if (aData.IsInstanceOf<GeckoBundleArray>()) {
|
|
return UnboxArrayObject<&UnboxBundle>(aCx, aData, aOut);
|
|
} else {
|
|
NS_WARNING("Invalid type");
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
UnboxData(jni::String::Param aEvent, JSContext* aCx, jni::Object::Param aData,
|
|
JS::MutableHandleValue aOut, bool aBundleOnly)
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
|
|
jni::Object::LocalRef jniData(jni::GetGeckoThreadEnv(), aData);
|
|
nsresult rv = NS_ERROR_INVALID_ARG;
|
|
|
|
if (!aBundleOnly) {
|
|
rv = UnboxValue(aCx, jniData, aOut);
|
|
} else if (!jniData || jniData.IsInstanceOf<java::GeckoBundle>()) {
|
|
rv = UnboxBundle(aCx, jniData, aOut);
|
|
}
|
|
if (rv != NS_ERROR_INVALID_ARG || !aEvent) {
|
|
return rv;
|
|
}
|
|
|
|
nsCString event = aEvent->ToCString();
|
|
if (JS_IsExceptionPending(aCx)) {
|
|
JS_ReportWarningUTF8(aCx, "Error dispatching %s", event.get());
|
|
} else {
|
|
JS_ReportErrorUTF8(aCx, "Invalid event data for %s", event.get());
|
|
}
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
|
|
class JavaCallbackDelegate final : public nsIAndroidEventCallback
|
|
{
|
|
const java::EventCallback::GlobalRef mCallback;
|
|
|
|
virtual ~JavaCallbackDelegate() {}
|
|
|
|
NS_IMETHOD Call(JS::HandleValue aData,
|
|
void (java::EventCallback::*aCall)(
|
|
jni::Object::Param) const)
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
AutoJSContext cx;
|
|
|
|
jni::Object::LocalRef data(jni::GetGeckoThreadEnv());
|
|
nsresult rv = BoxData(NS_LITERAL_STRING("callback"), cx, aData, data,
|
|
/* ObjectOnly */ false);
|
|
NS_ENSURE_SUCCESS(rv, JS_IsExceptionPending(cx) ? NS_OK : rv);
|
|
|
|
dom::AutoNoJSAPI nojsapi;
|
|
(java::EventCallback(*mCallback).*aCall)(data);
|
|
return NS_OK;
|
|
}
|
|
|
|
public:
|
|
explicit JavaCallbackDelegate(java::EventCallback::Param aCallback)
|
|
: mCallback(jni::GetGeckoThreadEnv(), aCallback)
|
|
{}
|
|
|
|
NS_DECL_ISUPPORTS
|
|
|
|
NS_IMETHOD OnSuccess(JS::HandleValue aData) override
|
|
{
|
|
return Call(aData, &java::EventCallback::SendSuccess);
|
|
}
|
|
|
|
NS_IMETHOD OnError(JS::HandleValue aData) override
|
|
{
|
|
return Call(aData, &java::EventCallback::SendError);
|
|
}
|
|
};
|
|
|
|
NS_IMPL_ISUPPORTS(JavaCallbackDelegate, nsIAndroidEventCallback)
|
|
|
|
class NativeCallbackDelegateSupport final :
|
|
public java::EventDispatcher::NativeCallbackDelegate
|
|
::Natives<NativeCallbackDelegateSupport>
|
|
{
|
|
using CallbackDelegate = java::EventDispatcher::NativeCallbackDelegate;
|
|
using Base = CallbackDelegate::Natives<NativeCallbackDelegateSupport>;
|
|
|
|
const nsCOMPtr<nsIAndroidEventCallback> mCallback;
|
|
const nsCOMPtr<nsIAndroidEventFinalizer> mFinalizer;
|
|
const nsCOMPtr<nsPIDOMWindowOuter> mWindow;
|
|
|
|
void Call(jni::Object::Param aData,
|
|
nsresult (nsIAndroidEventCallback::*aCall)(JS::HandleValue))
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
|
|
// Use the same compartment as the wrapped JS object if possible,
|
|
// otherwise use either the attached window's compartment or a default
|
|
// compartment.
|
|
nsCOMPtr<nsIXPConnectWrappedJS> wrappedJS(do_QueryInterface(mCallback));
|
|
|
|
dom::AutoJSAPI jsapi;
|
|
if (!wrappedJS && mWindow) {
|
|
NS_ENSURE_TRUE_VOID(jsapi.Init(mWindow->GetCurrentInnerWindow()));
|
|
} else {
|
|
NS_ENSURE_TRUE_VOID(jsapi.Init(wrappedJS ?
|
|
wrappedJS->GetJSObject() : xpc::PrivilegedJunkScope()));
|
|
}
|
|
|
|
JS::RootedValue data(jsapi.cx());
|
|
nsresult rv = UnboxData(NS_LITERAL_STRING("callback"), jsapi.cx(),
|
|
aData, &data, /* BundleOnly */ false);
|
|
NS_ENSURE_SUCCESS_VOID(rv);
|
|
|
|
dom::AutoNoJSAPI nojsapi;
|
|
rv = (mCallback->*aCall)(data);
|
|
NS_ENSURE_SUCCESS_VOID(rv);
|
|
}
|
|
|
|
public:
|
|
using Base::AttachNative;
|
|
|
|
template<typename Functor>
|
|
static void OnNativeCall(Functor&& aCall)
|
|
{
|
|
if (NS_IsMainThread()) {
|
|
// Invoke callbacks synchronously if we're already on Gecko thread.
|
|
return aCall();
|
|
}
|
|
NS_DispatchToMainThread(NS_NewRunnableFunction("OnNativeCall", std::move(aCall)));
|
|
}
|
|
|
|
static void Finalize(const CallbackDelegate::LocalRef& aInstance)
|
|
{
|
|
DisposeNative(aInstance);
|
|
}
|
|
|
|
NativeCallbackDelegateSupport(nsIAndroidEventCallback* callback,
|
|
nsIAndroidEventFinalizer* finalizer,
|
|
nsPIDOMWindowOuter* domWindow)
|
|
: mCallback(callback)
|
|
, mFinalizer(finalizer)
|
|
, mWindow(domWindow)
|
|
{}
|
|
|
|
~NativeCallbackDelegateSupport()
|
|
{
|
|
if (mFinalizer) {
|
|
mFinalizer->OnFinalize();
|
|
}
|
|
}
|
|
|
|
void SendSuccess(jni::Object::Param aData)
|
|
{
|
|
Call(aData, &nsIAndroidEventCallback::OnSuccess);
|
|
}
|
|
|
|
void SendError(jni::Object::Param aData)
|
|
{
|
|
Call(aData, &nsIAndroidEventCallback::OnError);
|
|
}
|
|
};
|
|
|
|
class FinalizingCallbackDelegate final : public nsIAndroidEventCallback
|
|
{
|
|
const nsCOMPtr<nsIAndroidEventCallback> mCallback;
|
|
const nsCOMPtr<nsIAndroidEventFinalizer> mFinalizer;
|
|
|
|
virtual ~FinalizingCallbackDelegate()
|
|
{
|
|
if (mFinalizer) {
|
|
mFinalizer->OnFinalize();
|
|
}
|
|
}
|
|
|
|
public:
|
|
FinalizingCallbackDelegate(nsIAndroidEventCallback* aCallback,
|
|
nsIAndroidEventFinalizer* aFinalizer)
|
|
: mCallback(aCallback)
|
|
, mFinalizer(aFinalizer)
|
|
{}
|
|
|
|
NS_DECL_ISUPPORTS
|
|
NS_FORWARD_NSIANDROIDEVENTCALLBACK(mCallback->);
|
|
};
|
|
|
|
NS_IMPL_ISUPPORTS(FinalizingCallbackDelegate, nsIAndroidEventCallback)
|
|
|
|
} // namespace detail
|
|
|
|
using namespace detail;
|
|
|
|
NS_IMPL_ISUPPORTS(EventDispatcher, nsIAndroidEventDispatcher)
|
|
|
|
nsresult
|
|
EventDispatcher::DispatchOnGecko(ListenersList* list, const nsAString& aEvent,
|
|
JS::HandleValue aData,
|
|
nsIAndroidEventCallback* aCallback)
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
dom::AutoNoJSAPI nojsapi;
|
|
|
|
list->lockCount++;
|
|
|
|
auto iteratingScope = MakeScopeExit([list] {
|
|
list->lockCount--;
|
|
if (list->lockCount || !list->unregistering) {
|
|
return;
|
|
}
|
|
|
|
list->unregistering = false;
|
|
for (ssize_t i = list->listeners.Count() - 1; i >= 0; i--) {
|
|
if (list->listeners[i]) {
|
|
continue;
|
|
}
|
|
list->listeners.RemoveObjectAt(i);
|
|
}
|
|
});
|
|
|
|
const size_t count = list->listeners.Count();
|
|
for (size_t i = 0; i < count; i++) {
|
|
if (!list->listeners[i]) {
|
|
// Unregistered.
|
|
continue;
|
|
}
|
|
const nsresult rv = list->listeners[i]->OnEvent(
|
|
aEvent, aData, aCallback);
|
|
Unused << NS_WARN_IF(NS_FAILED(rv));
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
java::EventDispatcher::NativeCallbackDelegate::LocalRef
|
|
EventDispatcher::WrapCallback(nsIAndroidEventCallback* aCallback,
|
|
nsIAndroidEventFinalizer* aFinalizer)
|
|
{
|
|
if (!aCallback) {
|
|
return java::EventDispatcher::NativeCallbackDelegate::LocalRef(
|
|
jni::GetGeckoThreadEnv());
|
|
}
|
|
|
|
java::EventDispatcher::NativeCallbackDelegate::LocalRef
|
|
callback = java::EventDispatcher::NativeCallbackDelegate::New();
|
|
NativeCallbackDelegateSupport::AttachNative(
|
|
callback,
|
|
MakeUnique<NativeCallbackDelegateSupport>(
|
|
aCallback, aFinalizer, mDOMWindow));
|
|
return callback;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
EventDispatcher::Dispatch(JS::HandleValue aEvent, JS::HandleValue aData,
|
|
nsIAndroidEventCallback* aCallback,
|
|
nsIAndroidEventFinalizer* aFinalizer, JSContext* aCx)
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
|
|
if (!aEvent.isString()) {
|
|
NS_WARNING("Invalid event name");
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
|
|
nsAutoJSString event;
|
|
NS_ENSURE_TRUE(CheckJS(aCx, event.init(aCx, aEvent.toString())),
|
|
NS_ERROR_OUT_OF_MEMORY);
|
|
|
|
// Don't need to lock here because we're on the main thread, and we can't
|
|
// race against Register/UnregisterListener.
|
|
|
|
ListenersList* list = mListenersMap.Get(event);
|
|
if (list) {
|
|
if (!aCallback || !aFinalizer) {
|
|
return DispatchOnGecko(list, event, aData, aCallback);
|
|
}
|
|
nsCOMPtr<nsIAndroidEventCallback> callback(
|
|
new FinalizingCallbackDelegate(aCallback, aFinalizer));
|
|
return DispatchOnGecko(list, event, aData, callback);
|
|
}
|
|
|
|
if (!mDispatcher) {
|
|
return NS_OK;
|
|
}
|
|
|
|
jni::Object::LocalRef data(jni::GetGeckoThreadEnv());
|
|
nsresult rv = BoxData(event, aCx, aData, data, /* ObjectOnly */ true);
|
|
NS_ENSURE_SUCCESS(rv, JS_IsExceptionPending(aCx) ? NS_OK : rv);
|
|
|
|
dom::AutoNoJSAPI nojsapi;
|
|
mDispatcher->DispatchToThreads(event, data,
|
|
WrapCallback(aCallback, aFinalizer));
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
EventDispatcher::Dispatch(const char16_t* aEvent,
|
|
java::GeckoBundle::Param aData,
|
|
nsIAndroidEventCallback* aCallback)
|
|
{
|
|
nsDependentString event(aEvent);
|
|
|
|
ListenersList* list = mListenersMap.Get(event);
|
|
if (list) {
|
|
dom::AutoJSAPI jsapi;
|
|
if (mDOMWindow) {
|
|
NS_ENSURE_TRUE(jsapi.Init(mDOMWindow->GetCurrentInnerWindow()),
|
|
NS_ERROR_FAILURE);
|
|
} else {
|
|
NS_ENSURE_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()),
|
|
NS_ERROR_FAILURE);
|
|
}
|
|
JS::RootedValue data(jsapi.cx());
|
|
nsresult rv = UnboxData(/* Event */ nullptr, jsapi.cx(), aData, &data,
|
|
/* BundleOnly */ true);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
return DispatchOnGecko(list, event, data, aCallback);
|
|
}
|
|
|
|
if (!mDispatcher) {
|
|
return NS_OK;
|
|
}
|
|
|
|
mDispatcher->DispatchToThreads(event, aData, WrapCallback(aCallback));
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
EventDispatcher::IterateEvents(JSContext* aCx, JS::HandleValue aEvents,
|
|
IterateEventsCallback aCallback,
|
|
nsIAndroidEventListener* aListener)
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
|
|
MutexAutoLock lock(mLock);
|
|
|
|
auto processEvent = [this, aCx, aCallback, aListener]
|
|
(JS::HandleValue event) -> nsresult {
|
|
nsAutoJSString str;
|
|
NS_ENSURE_TRUE(CheckJS(aCx, str.init(aCx, event.toString())),
|
|
NS_ERROR_OUT_OF_MEMORY);
|
|
return (this->*aCallback)(str, aListener);
|
|
};
|
|
|
|
if (aEvents.isString()) {
|
|
return processEvent(aEvents);
|
|
}
|
|
|
|
bool isArray = false;
|
|
NS_ENSURE_TRUE(aEvents.isObject(), NS_ERROR_INVALID_ARG);
|
|
NS_ENSURE_TRUE(CheckJS(aCx, JS_IsArrayObject(aCx, aEvents, &isArray)),
|
|
NS_ERROR_INVALID_ARG);
|
|
NS_ENSURE_TRUE(isArray, NS_ERROR_INVALID_ARG);
|
|
|
|
JS::RootedObject events(aCx, &aEvents.toObject());
|
|
uint32_t length = 0;
|
|
NS_ENSURE_TRUE(CheckJS(aCx, JS_GetArrayLength(aCx, events, &length)),
|
|
NS_ERROR_INVALID_ARG);
|
|
NS_ENSURE_TRUE(length, NS_ERROR_INVALID_ARG);
|
|
|
|
for (size_t i = 0; i < length; i++) {
|
|
JS::RootedValue event(aCx);
|
|
NS_ENSURE_TRUE(CheckJS(aCx, JS_GetElement(aCx, events, i, &event)),
|
|
NS_ERROR_INVALID_ARG);
|
|
NS_ENSURE_TRUE(event.isString(), NS_ERROR_INVALID_ARG);
|
|
|
|
const nsresult rv = processEvent(event);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
EventDispatcher::RegisterEventLocked(const nsAString& aEvent,
|
|
nsIAndroidEventListener* aListener)
|
|
{
|
|
ListenersList* list = mListenersMap.Get(aEvent);
|
|
if (!list) {
|
|
list = new ListenersList();
|
|
mListenersMap.Put(aEvent, list);
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
for (ssize_t i = 0; i < list->listeners.Count(); i++) {
|
|
NS_ENSURE_TRUE(list->listeners[i] != aListener,
|
|
NS_ERROR_ALREADY_INITIALIZED);
|
|
}
|
|
#endif
|
|
|
|
list->listeners.AppendObject(aListener);
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
EventDispatcher::RegisterListener(nsIAndroidEventListener* aListener,
|
|
JS::HandleValue aEvents, JSContext *aCx)
|
|
{
|
|
return IterateEvents(aCx, aEvents, &EventDispatcher::RegisterEventLocked,
|
|
aListener);
|
|
}
|
|
|
|
nsresult
|
|
EventDispatcher::UnregisterEventLocked(const nsAString& aEvent,
|
|
nsIAndroidEventListener* aListener)
|
|
{
|
|
ListenersList* list = mListenersMap.Get(aEvent);
|
|
#ifdef DEBUG
|
|
NS_ENSURE_TRUE(list, NS_ERROR_NOT_INITIALIZED);
|
|
#else
|
|
NS_ENSURE_TRUE(list, NS_OK);
|
|
#endif
|
|
|
|
DebugOnly<bool> found = false;
|
|
for (ssize_t i = list->listeners.Count() - 1; i >= 0; i--) {
|
|
if (list->listeners[i] != aListener) {
|
|
continue;
|
|
}
|
|
if (list->lockCount) {
|
|
// Only mark for removal when list is locked.
|
|
list->listeners.ReplaceObjectAt(nullptr, i);
|
|
list->unregistering = true;
|
|
} else {
|
|
list->listeners.RemoveObjectAt(i);
|
|
}
|
|
found = true;
|
|
}
|
|
#ifdef DEBUG
|
|
return found ? NS_OK : NS_ERROR_NOT_INITIALIZED;
|
|
#else
|
|
return NS_OK;
|
|
#endif
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
EventDispatcher::UnregisterListener(nsIAndroidEventListener* aListener,
|
|
JS::HandleValue aEvents, JSContext *aCx)
|
|
{
|
|
return IterateEvents(aCx, aEvents, &EventDispatcher::UnregisterEventLocked,
|
|
aListener);
|
|
}
|
|
|
|
void
|
|
EventDispatcher::Attach(java::EventDispatcher::Param aDispatcher,
|
|
nsPIDOMWindowOuter* aDOMWindow)
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
MOZ_ASSERT(aDispatcher);
|
|
|
|
if (mDispatcher) {
|
|
if (mDispatcher == aDispatcher) {
|
|
// Only need to update the window.
|
|
mDOMWindow = aDOMWindow;
|
|
return;
|
|
}
|
|
mAttachCount--;
|
|
mDispatcher->SetAttachedToGecko(java::EventDispatcher::REATTACHING);
|
|
}
|
|
|
|
java::EventDispatcher::LocalRef dispatcher(aDispatcher);
|
|
|
|
NativesBase::AttachNative(dispatcher, this);
|
|
mDispatcher = dispatcher;
|
|
mDOMWindow = aDOMWindow;
|
|
|
|
dispatcher->SetAttachedToGecko(java::EventDispatcher::ATTACHED);
|
|
mAttachCount++;
|
|
}
|
|
|
|
void
|
|
EventDispatcher::Detach()
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
MOZ_ASSERT(mDispatcher);
|
|
|
|
// SetAttachedToGecko will call disposeNative for us. disposeNative will be
|
|
// called later on the Gecko thread to make sure all pending
|
|
// dispatchToGecko calls have completed.
|
|
mAttachCount--;
|
|
mDispatcher->SetAttachedToGecko(java::EventDispatcher::DETACHED);
|
|
mDispatcher = nullptr;
|
|
mDOMWindow = nullptr;
|
|
}
|
|
|
|
void
|
|
EventDispatcher::DisposeNative(const java::EventDispatcher::LocalRef& aInstance)
|
|
{
|
|
JNIEnv* const env = jni::GetGeckoThreadEnv();
|
|
const auto natives = reinterpret_cast<RefPtr<EventDispatcher>*>(
|
|
jni::GetNativeHandle(env, aInstance.Get()));
|
|
MOZ_CATCH_JNI_EXCEPTION(env);
|
|
|
|
if (!(*natives)->mAttachCount) {
|
|
// Only actually dispose if we haven't attached again between calling
|
|
// Detach() and calling DisposeNative().
|
|
NativesBase::DisposeNative(aInstance);
|
|
}
|
|
}
|
|
|
|
bool
|
|
EventDispatcher::HasGeckoListener(jni::String::Param aEvent)
|
|
{
|
|
// Can be called from any thread.
|
|
MutexAutoLock lock(mLock);
|
|
return !!mListenersMap.Get(aEvent->ToString());
|
|
}
|
|
|
|
void
|
|
EventDispatcher::DispatchToGecko(jni::String::Param aEvent,
|
|
jni::Object::Param aData,
|
|
jni::Object::Param aCallback)
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
|
|
// Don't need to lock here because we're on the main thread, and we can't
|
|
// race against Register/UnregisterListener.
|
|
|
|
nsString event = aEvent->ToString();
|
|
ListenersList* list = mListenersMap.Get(event);
|
|
if (!list || list->listeners.IsEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// Use the same compartment as the attached window if possible, otherwise
|
|
// use a default compartment.
|
|
dom::AutoJSAPI jsapi;
|
|
if (mDOMWindow) {
|
|
NS_ENSURE_TRUE_VOID(jsapi.Init(mDOMWindow->GetCurrentInnerWindow()));
|
|
} else {
|
|
NS_ENSURE_TRUE_VOID(jsapi.Init(xpc::PrivilegedJunkScope()));
|
|
}
|
|
|
|
JS::RootedValue data(jsapi.cx());
|
|
nsresult rv = UnboxData(aEvent, jsapi.cx(), aData, &data,
|
|
/* BundleOnly */ true);
|
|
NS_ENSURE_SUCCESS_VOID(rv);
|
|
|
|
nsCOMPtr<nsIAndroidEventCallback> callback;
|
|
if (aCallback) {
|
|
callback = new JavaCallbackDelegate(
|
|
java::EventCallback::Ref::From(aCallback));
|
|
}
|
|
|
|
DispatchOnGecko(list, event, data, callback);
|
|
}
|
|
|
|
/* static */
|
|
nsresult
|
|
EventDispatcher::UnboxBundle(JSContext* aCx, jni::Object::Param aData,
|
|
JS::MutableHandleValue aOut)
|
|
{
|
|
return detail::UnboxBundle(aCx, aData, aOut);
|
|
}
|
|
|
|
} // namespace widget
|
|
} // namespace mozilla
|