widget/android/EventDispatcher.cpp
author Andrew Halberstadt <ahalberstadt@mozilla.com>
Wed, 04 Sep 2019 17:32:42 +0000
changeset 555705 0100c63fd8222d60b6bab50a401659239d324ef5
parent 547860 388e7bb0eb074d278a4cd0c668f3b03678a13aef
permissions -rw-r--r--
Bug 1572089 - [ci] Migrate Win10-64 virtual-with-gpu tasks to a cheaper workertype, r=jmaher a=tomprince Differential Revision: https://phabricator.services.mozilla.com/D43820

/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
 * vim: set sw=2 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 "js/Warnings.h"  // JS::WarnUTF8
#include "xpcpublic.h"

#include "mozilla/ScopeExit.h"
#include "mozilla/dom/ScriptSettings.h"

// Disable the C++ 2a warning. See bug #1509926
#if defined(__clang__)
#  pragma clang diagnostic push
#  pragma clang diagnostic ignored "-Wc++2a-compat"
#endif

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::WarnUTF8(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::RootedVector<JS::Value> 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;
}

nsresult UnboxValue(JSContext* aCx, const jni::Object::LocalRef& aData,
                    JS::MutableHandleValue aOut) {
  using jni::Java2Native;

  if (!aData) {
    aOut.setNull();
  } else if (aData.IsInstanceOf<jni::Boolean>()) {
    aOut.setBoolean(Java2Native<bool>(aData, aData.Env()));
  } else if (aData.IsInstanceOf<jni::Integer>()) {
    aOut.setInt32(Java2Native<int>(aData, aData.Env()));
  } 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>()) {
    aOut.setNumber(Java2Native<double>(aData, aData.Env()));
  } 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::WarnUTF8(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(JSContext* aCx, JS::HandleValue aData,
                  void (java::EventCallback::*aCall)(jni::Object::Param)
                      const) {
    MOZ_ASSERT(NS_IsMainThread());

    jni::Object::LocalRef data(jni::GetGeckoThreadEnv());
    nsresult rv = BoxData(NS_LITERAL_STRING("callback"), aCx, aData, data,
                          /* ObjectOnly */ false);
    NS_ENSURE_SUCCESS(rv, 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, JSContext* aCx) override {
    return Call(aCx, aData, &java::EventCallback::SendSuccess);
  }

  NS_IMETHOD OnError(JS::HandleValue aData, JSContext* aCx) override {
    return Call(aCx, 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<nsIGlobalObject> mGlobalObject;

  void Call(jni::Object::Param aData,
            nsresult (nsIAndroidEventCallback::*aCall)(JS::HandleValue,
                                                       JSContext*)) {
    MOZ_ASSERT(NS_IsMainThread());

    // Use either the attached window's realm or a default realm.

    dom::AutoJSAPI jsapi;
    NS_ENSURE_TRUE_VOID(jsapi.Init(mGlobalObject));

    JS::RootedValue data(jsapi.cx());
    nsresult rv = UnboxData(NS_LITERAL_STRING("callback"), jsapi.cx(), aData,
                            &data, /* BundleOnly */ false);
    NS_ENSURE_SUCCESS_VOID(rv);

    rv = (mCallback->*aCall)(data, jsapi.cx());
    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,
                                nsIGlobalObject* globalObject)
      : mCallback(callback),
        mFinalizer(finalizer),
        mGlobalObject(globalObject) {}

  ~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)

nsIGlobalObject* EventDispatcher::GetGlobalObject() {
  if (mDOMWindow) {
    return nsGlobalWindowInner::Cast(mDOMWindow->GetCurrentInnerWindow());
  }
  return xpc::NativeGlobal(xpc::PrivilegedJunkScope());
}

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,
                                                          GetGlobalObject()));
  return callback;
}

bool EventDispatcher::HasListener(const char16_t* aEvent) {
  java::EventDispatcher::LocalRef dispatcher(mDispatcher);
  if (!dispatcher) {
    return false;
  }

  nsDependentString event(aEvent);
  return dispatcher->HasListener(event);
}

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);
  }

  java::EventDispatcher::LocalRef dispatcher(mDispatcher);
  if (!dispatcher) {
    return NS_OK;
  }

  jni::Object::LocalRef data(jni::GetGeckoThreadEnv());
  nsresult rv = BoxData(event, aCx, aData, data, /* ObjectOnly */ true);
  // Keep XPConnect from overriding the JSContext exception with one
  // based on the nsresult.
  //
  // XXXbz Does xpconnect still do that?  Needs to be checked/tested.
  NS_ENSURE_SUCCESS(rv, JS_IsExceptionPending(aCx) ? NS_OK : rv);

  dom::AutoNoJSAPI nojsapi;
  dispatcher->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;
    NS_ENSURE_TRUE(jsapi.Init(GetGlobalObject()), 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);
  }

  java::EventDispatcher::LocalRef dispatcher(mDispatcher);
  if (!dispatcher) {
    return NS_OK;
  }

  dispatcher->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);

  java::EventDispatcher::LocalRef dispatcher(mDispatcher);

  if (dispatcher) {
    if (dispatcher == aDispatcher) {
      // Only need to update the window.
      mDOMWindow = aDOMWindow;
      return;
    }
    dispatcher->SetAttachedToGecko(java::EventDispatcher::REATTACHING);
  }

  dispatcher = java::EventDispatcher::LocalRef(aDispatcher);
  NativesBase::AttachNative(dispatcher, this);
  mDispatcher = dispatcher;
  mDOMWindow = aDOMWindow;

  dispatcher->SetAttachedToGecko(java::EventDispatcher::ATTACHED);
}

void EventDispatcher::Detach() {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(mDispatcher);

  java::EventDispatcher::GlobalRef dispatcher(mDispatcher);

  // SetAttachedToGecko will call disposeNative for us later on the Gecko
  // thread to make sure all pending dispatchToGecko calls have completed.
  if (dispatcher) {
    dispatcher->SetAttachedToGecko(java::EventDispatcher::DETACHED);
  }

  mDispatcher = nullptr;
  mDOMWindow = nullptr;
}

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;
  NS_ENSURE_TRUE_VOID(jsapi.Init(GetGlobalObject()));

  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

#if defined(__clang__)
#  pragma clang diagnostic pop
#endif