/* -*- 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(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 nsresult BoxArrayPrimitive(JSContext* aCx, JS::HandleObject aData, jni::Object::LocalRef& aOut, size_t aLength, JS::HandleValue aElement) { JS::RootedValue element(aCx); auto data = MakeUnique(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 nsresult BoxArrayObject(JSContext* aCx, JS::HandleObject aData, jni::Object::LocalRef& aOut, size_t aLength, JS::HandleValue aElement, IsType&& aIsType) { auto out = jni::ObjectArray::New(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( 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( 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( 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 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(length); auto values = jni::ObjectArray::New(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()); 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(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()); 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 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 { static const char name[]; }; struct GeckoBundleArray : jni::ObjectBase { static const char name[]; }; const char StringArray::name[] = "[Ljava/lang/String;"; const char GeckoBundleArray::name[] = "[Lorg/mozilla/gecko/util/GeckoBundle;"; template 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 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("Z"); static jfieldID intValueField = GetValueFieldID("I"); static jfieldID doubleValueField = GetValueFieldID("D"); if (!aData) { aOut.setNull(); } else if (aData.IsInstanceOf()) { 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()) { 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() || aData.IsInstanceOf()) { aOut.setInt32(java::sdk::Number::Ref::From(aData)->IntValue()); } else if (aData.IsInstanceOf()) { if (doubleValueField) { aOut.setNumber(aData.Env()->GetDoubleField( aData.Get(), doubleValueField)); } else { aOut.setNumber(java::sdk::Number::Ref::From(aData)->DoubleValue()); } } else if (aData.IsInstanceOf() || aData.IsInstanceOf()) { aOut.setNumber(java::sdk::Number::Ref::From(aData)->DoubleValue()); } else if (aData.IsInstanceOf()) { return UnboxString(aCx, aData, aOut); } else if (aData.IsInstanceOf()) { return UnboxString(aCx, java::sdk::String::ValueOf(aData), aOut); } else if (aData.IsInstanceOf()) { return UnboxBundle(aCx, aData, aOut); } else if (aData.IsInstanceOf()) { return UnboxArrayPrimitive(aCx, aData, aOut); } else if (aData.IsInstanceOf()) { return UnboxArrayPrimitive(aCx, aData, aOut); } else if (aData.IsInstanceOf()) { return UnboxArrayPrimitive(aCx, aData, aOut); } else if (aData.IsInstanceOf()) { return UnboxArrayObject<&UnboxString>(aCx, aData, aOut); } else if (aData.IsInstanceOf()) { 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()) { 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 { using CallbackDelegate = java::EventDispatcher::NativeCallbackDelegate; using Base = CallbackDelegate::Natives; const nsCOMPtr mCallback; const nsCOMPtr mFinalizer; const nsCOMPtr 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 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 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 mCallback; const nsCOMPtr 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( 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 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 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*>( 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 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