/****************************************************************************
**
** Copyright (C) 2014 BogDan Vatra <bogdan@kde.org>
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL21$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see http://www.qt.io/terms-conditions. For further
** information use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file. Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** As a special exception, The Qt Company gives you certain additional
** rights. These rights are described in The Qt Company LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include <dlfcn.h>
#include <pthread.h>
#include <semaphore.h>
#include <qplugin.h>
#include <qdebug.h>

#include "androidjnimain.h"
#include "androidjniaccessibility.h"
#include "androidjniinput.h"
#include "androidjniclipboard.h"
#include "androidjnimenu.h"
#include "androiddeadlockprotector.h"
#include "qandroidplatformdialoghelpers.h"
#include "qandroidplatformintegration.h"
#include "qandroidassetsfileenginehandler.h"

#include <android/bitmap.h>
#include <android/asset_manager_jni.h>
#include "qandroideventdispatcher.h"
#include <android/api-level.h>

#include <QtCore/private/qjnihelpers_p.h>
#include <QtCore/private/qjni_p.h>
#include <QtGui/private/qguiapplication_p.h>
#include <QtGui/private/qhighdpiscaling_p.h>

#include <qpa/qwindowsysteminterface.h>

Q_IMPORT_PLUGIN(QAndroidPlatformIntegrationPlugin)

QT_BEGIN_NAMESPACE

static JavaVM *m_javaVM = nullptr;
static jclass m_applicationClass  = nullptr;
static jobject m_classLoaderObject = nullptr;
static jmethodID m_loadClassMethodID = nullptr;
static AAssetManager *m_assetManager = nullptr;
static jobject m_resourcesObj = nullptr;
static jobject m_activityObject = nullptr;
static jmethodID m_createSurfaceMethodID = nullptr;
static jmethodID m_setSurfaceGeometryMethodID = nullptr;
static jmethodID m_destroySurfaceMethodID = nullptr;

static bool m_activityActive = true; // defaults to true because when the platform plugin is
                                     // initialized, QtActivity::onResume() has already been called

static jclass m_bitmapClass  = nullptr;
static jmethodID m_createBitmapMethodID = nullptr;
static jobject m_ARGB_8888_BitmapConfigValue = nullptr;
static jobject m_RGB_565_BitmapConfigValue = nullptr;

static bool m_statusBarShowing = true;

static jclass m_bitmapDrawableClass = nullptr;
static jmethodID m_bitmapDrawableConstructorMethodID = nullptr;

extern "C" typedef int (*Main)(int, char **); //use the standard main method to start the application
static Main m_main = nullptr;
static void *m_mainLibraryHnd = nullptr;
static QList<QByteArray> m_applicationParams;
pthread_t m_qtAppThread = 0;
static sem_t m_exitSemaphore, m_terminateSemaphore;

struct SurfaceData
{
    ~SurfaceData() { delete surface; }
    QJNIObjectPrivate *surface = nullptr;
    AndroidSurfaceClient *client = nullptr;
};

QHash<int, AndroidSurfaceClient *> m_surfaces;

static QMutex m_surfacesMutex;
static int m_surfaceId = 1;


static QAndroidPlatformIntegration *m_androidPlatformIntegration = nullptr;

static int m_desktopWidthPixels  = 0;
static int m_desktopHeightPixels = 0;
static double m_scaledDensity = 0;
static double m_density = 1.0;

static volatile bool m_pauseApplication;

static AndroidAssetsFileEngineHandler *m_androidAssetsFileEngineHandler = nullptr;



static const char m_qtTag[] = "Qt";
static const char m_classErrorMsg[] = "Can't find class \"%s\"";
static const char m_methodErrorMsg[] = "Can't find method \"%s%s\"";

namespace QtAndroid
{
    void setAndroidPlatformIntegration(QAndroidPlatformIntegration *androidPlatformIntegration)
    {
        m_surfacesMutex.lock();
        m_androidPlatformIntegration = androidPlatformIntegration;
        m_surfacesMutex.unlock();
    }

    QAndroidPlatformIntegration *androidPlatformIntegration()
    {
        QMutexLocker locker(&m_surfacesMutex);
        return m_androidPlatformIntegration;
    }

    QWindow *topLevelWindowAt(const QPoint &globalPos)
    {
        return m_androidPlatformIntegration
               ? m_androidPlatformIntegration->screen()->topLevelAt(globalPos)
               : 0;
    }

    int desktopWidthPixels()
    {
        return m_desktopWidthPixels;
    }

    int desktopHeightPixels()
    {
        return m_desktopHeightPixels;
    }

    double scaledDensity()
    {
        return m_scaledDensity;
    }

    double pixelDensity()
    {
        return m_density;
    }

    JavaVM *javaVM()
    {
        return m_javaVM;
    }

    AAssetManager *assetManager()
    {
        return m_assetManager;
    }

    jclass applicationClass()
    {
        return m_applicationClass;
    }

    jobject activity()
    {
        return m_activityObject;
    }

    void showStatusBar()
    {
        if (m_statusBarShowing)
            return;

        QJNIObjectPrivate::callStaticMethod<void>(m_applicationClass, "setFullScreen", "(Z)V", false);
        m_statusBarShowing = true;
    }

    void hideStatusBar()
    {
        if (!m_statusBarShowing)
            return;

        QJNIObjectPrivate::callStaticMethod<void>(m_applicationClass, "setFullScreen", "(Z)V", true);
        m_statusBarShowing = false;
    }

    void setApplicationActive()
    {
        if (m_activityActive)
            QWindowSystemInterface::handleApplicationStateChanged(Qt::ApplicationActive);
    }

    jobject createBitmap(QImage img, JNIEnv *env)
    {
        if (!m_bitmapClass)
            return 0;

        if (img.format() != QImage::Format_RGBA8888 && img.format() != QImage::Format_RGB16)
            img = img.convertToFormat(QImage::Format_RGBA8888);

        jobject bitmap = env->CallStaticObjectMethod(m_bitmapClass,
                                                     m_createBitmapMethodID,
                                                     img.width(),
                                                     img.height(),
                                                     img.format() == QImage::Format_RGBA8888
                                                        ? m_ARGB_8888_BitmapConfigValue
                                                        : m_RGB_565_BitmapConfigValue);
        if (!bitmap)
            return 0;

        AndroidBitmapInfo info;
        if (AndroidBitmap_getInfo(env, bitmap, &info) < 0) {
            env->DeleteLocalRef(bitmap);
            return 0;
        }

        void *pixels;
        if (AndroidBitmap_lockPixels(env, bitmap, &pixels) < 0) {
            env->DeleteLocalRef(bitmap);
            return 0;
        }

        if (info.stride == uint(img.bytesPerLine())
                && info.width == uint(img.width())
                && info.height == uint(img.height())) {
            memcpy(pixels, img.constBits(), info.stride * info.height);
        } else {
            uchar *bmpPtr = static_cast<uchar *>(pixels);
            const unsigned width = qMin(info.width, (uint)img.width()); //should be the same
            const unsigned height = qMin(info.height, (uint)img.height()); //should be the same
            for (unsigned y = 0; y < height; y++, bmpPtr += info.stride)
                memcpy(bmpPtr, img.constScanLine(y), width);
        }
        AndroidBitmap_unlockPixels(env, bitmap);
        return bitmap;
    }

    jobject createBitmap(int width, int height, QImage::Format format, JNIEnv *env)
    {
        if (format != QImage::Format_RGBA8888
                && format != QImage::Format_RGB16)
            return 0;

        return env->CallStaticObjectMethod(m_bitmapClass,
                                                     m_createBitmapMethodID,
                                                     width,
                                                     height,
                                                     format == QImage::Format_RGB16
                                                        ? m_RGB_565_BitmapConfigValue
                                                        : m_ARGB_8888_BitmapConfigValue);
    }

    jobject createBitmapDrawable(jobject bitmap, JNIEnv *env)
    {
        if (!bitmap || !m_bitmapDrawableClass || !m_resourcesObj)
            return 0;

        return env->NewObject(m_bitmapDrawableClass,
                              m_bitmapDrawableConstructorMethodID,
                              m_resourcesObj,
                              bitmap);
    }

    const char *classErrorMsgFmt()
    {
        return m_classErrorMsg;
    }

    const char *methodErrorMsgFmt()
    {
        return m_methodErrorMsg;
    }

    const char *qtTagText()
    {
        return m_qtTag;
    }

    QString deviceName()
    {
        QString manufacturer = QJNIObjectPrivate::getStaticObjectField("android/os/Build", "MANUFACTURER", "Ljava/lang/String;").toString();
        QString model = QJNIObjectPrivate::getStaticObjectField("android/os/Build", "MODEL", "Ljava/lang/String;").toString();

        return manufacturer + QLatin1Char(' ') + model;
    }

    int createSurface(AndroidSurfaceClient *client, const QRect &geometry, bool onTop, int imageDepth)
    {
        QJNIEnvironmentPrivate env;
        if (!env)
            return -1;

        m_surfacesMutex.lock();
        int surfaceId = m_surfaceId++;
        m_surfaces[surfaceId] = client;
        m_surfacesMutex.unlock();

        jint x = 0, y = 0, w = -1, h = -1;
        if (!geometry.isNull()) {
            x = geometry.x();
            y = geometry.y();
            w = std::max(geometry.width(), 1);
            h = std::max(geometry.height(), 1);
        }
        env->CallStaticVoidMethod(m_applicationClass,
                                     m_createSurfaceMethodID,
                                     surfaceId,
                                     jboolean(onTop),
                                     x, y, w, h,
                                     imageDepth);
        return surfaceId;
    }

    int insertNativeView(jobject view, const QRect &geometry)
    {
        m_surfacesMutex.lock();
        const int surfaceId = m_surfaceId++;
        m_surfaces[surfaceId] = nullptr; // dummy
        m_surfacesMutex.unlock();

        jint x = 0, y = 0, w = -1, h = -1;
        if (!geometry.isNull())
            geometry.getRect(&x, &y, &w, &h);

        QJNIObjectPrivate::callStaticMethod<void>(m_applicationClass,
                                                  "insertNativeView",
                                                  "(ILandroid/view/View;IIII)V",
                                                  surfaceId,
                                                  view,
                                                  x,
                                                  y,
                                                  qMax(w, 1),
                                                  qMax(h, 1));

        return surfaceId;
    }

    void setViewVisibility(jobject view, bool visible)
    {
        QJNIObjectPrivate::callStaticMethod<void>(m_applicationClass,
                                                  "setViewVisibility",
                                                  "(Landroid/view/View;Z)V",
                                                  view,
                                                  visible);
    }

    void setSurfaceGeometry(int surfaceId, const QRect &geometry)
    {
        if (surfaceId == -1)
            return;

        QJNIEnvironmentPrivate env;
        if (!env)
            return;
        jint x = 0, y = 0, w = -1, h = -1;
        if (!geometry.isNull()) {
            x = geometry.x();
            y = geometry.y();
            w = geometry.width();
            h = geometry.height();
        }
        env->CallStaticVoidMethod(m_applicationClass,
                                     m_setSurfaceGeometryMethodID,
                                     surfaceId,
                                     x, y, w, h);
    }


    void destroySurface(int surfaceId)
    {
        if (surfaceId == -1)
            return;

        QMutexLocker lock(&m_surfacesMutex);
        const auto &it = m_surfaces.find(surfaceId);
        if (it != m_surfaces.end())
            m_surfaces.remove(surfaceId);

        QJNIEnvironmentPrivate env;
        if (!env)
            return;

        env->CallStaticVoidMethod(m_applicationClass,
                                     m_destroySurfaceMethodID,
                                     surfaceId);
    }

    void bringChildToFront(int surfaceId)
    {
        if (surfaceId == -1)
            return;

        QJNIObjectPrivate::callStaticMethod<void>(m_applicationClass,
                                                  "bringChildToFront",
                                                  "(I)V",
                                                  surfaceId);
    }

    void bringChildToBack(int surfaceId)
    {
        if (surfaceId == -1)
            return;

        QJNIObjectPrivate::callStaticMethod<void>(m_applicationClass,
                                                  "bringChildToBack",
                                                  "(I)V",
                                                  surfaceId);
    }

    bool blockEventLoopsWhenSuspended()
    {
        static bool block = qEnvironmentVariableIntValue("QT_BLOCK_EVENT_LOOPS_WHEN_SUSPENDED");
        return block;
    }

} // namespace QtAndroid


static jboolean startQtAndroidPlugin(JNIEnv* /*env*/, jobject /*object*//*, jobject applicationAssetManager*/)
{
    m_androidPlatformIntegration = nullptr;
    m_androidAssetsFileEngineHandler = new AndroidAssetsFileEngineHandler();
    return true;
}

static void *startMainMethod(void */*data*/)
{
    QVarLengthArray<const char *> params(m_applicationParams.size());
    for (int i = 0; i < m_applicationParams.size(); i++)
        params[i] = static_cast<const char *>(m_applicationParams[i].constData());

    int ret = m_main(m_applicationParams.length(), const_cast<char **>(params.data()));

    if (m_mainLibraryHnd) {
        int res = dlclose(m_mainLibraryHnd);
        if (res < 0)
            qWarning() << "dlclose failed:" << dlerror();
    }

    if (m_applicationClass)
        QJNIObjectPrivate::callStaticMethod<void>(m_applicationClass, "quitApp", "()V");

    // All attached threads should be detached before returning from this function.
    JavaVM *vm = QtAndroidPrivate::javaVM();
    if (vm != 0)
        vm->DetachCurrentThread();

    sem_post(&m_terminateSemaphore);
    sem_wait(&m_exitSemaphore);
    sem_destroy(&m_exitSemaphore);

    // We must call exit() to ensure that all global objects will be destructed
    exit(ret);
    return 0;
}

static jboolean startQtApplication(JNIEnv *env, jobject /*object*/, jstring paramsString, jstring environmentString)
{
    m_mainLibraryHnd = nullptr;
    { // Set env. vars
        const char *nativeString = env->GetStringUTFChars(environmentString, 0);
        const QList<QByteArray> envVars = QByteArray(nativeString).split('\t');
        env->ReleaseStringUTFChars(environmentString, nativeString);
        foreach (const QByteArray &envVar, envVars) {
            const QList<QByteArray> envVarPair = envVar.split('=');
            if (envVarPair.size() == 2 && ::setenv(envVarPair[0], envVarPair[1], 1) != 0)
                qWarning() << "Can't set environment" << envVarPair;
        }
    }

    const char *nativeString = env->GetStringUTFChars(paramsString, 0);
    QByteArray string = nativeString;
    env->ReleaseStringUTFChars(paramsString, nativeString);

    m_applicationParams=string.split('\t');

    // Go home
    QDir::setCurrent(QDir::homePath());

    //look for main()
    if (m_applicationParams.length()) {
        // Obtain a handle to the main library (the library that contains the main() function).
        // This library should already be loaded, and calling dlopen() will just return a reference to it.
        m_mainLibraryHnd = dlopen(m_applicationParams.first().data(), 0);
        if (m_mainLibraryHnd == nullptr) {
            qCritical() << "dlopen failed:" << dlerror();
            return false;
        }
        m_main = (Main)dlsym(m_mainLibraryHnd, "main");
    } else {
        qWarning() << "No main library was specified; searching entire process (this is slow!)";
        m_main = (Main)dlsym(RTLD_DEFAULT, "main");
    }

    if (!m_main) {
        qCritical() << "dlsym failed:" << dlerror() << endl
                    << "Could not find main method";
        return false;
    }

    if (sem_init(&m_exitSemaphore, 0, 0) == -1)
        return false;

    if (sem_init(&m_terminateSemaphore, 0, 0) == -1)
        return false;

    return pthread_create(&m_qtAppThread, nullptr, startMainMethod, nullptr) == 0;
}


static void quitQtAndroidPlugin(JNIEnv *env, jclass /*clazz*/)
{
    Q_UNUSED(env);
    m_androidPlatformIntegration = nullptr;
    delete m_androidAssetsFileEngineHandler;
    m_androidAssetsFileEngineHandler = nullptr;
}

static void terminateQt(JNIEnv *env, jclass /*clazz*/)
{
    sem_wait(&m_terminateSemaphore);
    sem_destroy(&m_terminateSemaphore);
    env->DeleteGlobalRef(m_applicationClass);
    env->DeleteGlobalRef(m_classLoaderObject);
    if (m_resourcesObj)
        env->DeleteGlobalRef(m_resourcesObj);
    if (m_activityObject)
        env->DeleteGlobalRef(m_activityObject);
    if (m_bitmapClass)
        env->DeleteGlobalRef(m_bitmapClass);
    if (m_ARGB_8888_BitmapConfigValue)
        env->DeleteGlobalRef(m_ARGB_8888_BitmapConfigValue);
    if (m_RGB_565_BitmapConfigValue)
        env->DeleteGlobalRef(m_RGB_565_BitmapConfigValue);
    if (m_bitmapDrawableClass)
        env->DeleteGlobalRef(m_bitmapDrawableClass);
    m_androidPlatformIntegration = nullptr;
    delete m_androidAssetsFileEngineHandler;
    m_androidAssetsFileEngineHandler = nullptr;
    sem_post(&m_exitSemaphore);
    pthread_join(m_qtAppThread, nullptr);
}

static void setSurface(JNIEnv *env, jobject /*thiz*/, jint id, jobject jSurface, jint w, jint h)
{
    QMutexLocker lock(&m_surfacesMutex);
    const auto &it = m_surfaces.find(id);
    if (it.value() == nullptr) // This should never happen...
        return;

    if (it == m_surfaces.end()) {
        qWarning()<<"Can't find surface" << id;
        return;
    }
    it.value()->surfaceChanged(env, jSurface, w, h);
}

static void setDisplayMetrics(JNIEnv */*env*/, jclass /*clazz*/,
                            jint widthPixels, jint heightPixels,
                            jint desktopWidthPixels, jint desktopHeightPixels,
                            jdouble xdpi, jdouble ydpi,
                            jdouble scaledDensity, jdouble density)
{
    // Android does not give us the correct screen size for immersive mode, but
    // the surface does have the right size

    widthPixels = qMax(widthPixels, desktopWidthPixels);
    heightPixels = qMax(heightPixels, desktopHeightPixels);

    m_desktopWidthPixels = desktopWidthPixels;
    m_desktopHeightPixels = desktopHeightPixels;
    m_scaledDensity = scaledDensity;
    m_density = density;

    if (!m_androidPlatformIntegration) {
        QAndroidPlatformIntegration::setDefaultDisplayMetrics(desktopWidthPixels,
                                                              desktopHeightPixels,
                                                              qRound(double(widthPixels)  / xdpi * 25.4),
                                                              qRound(double(heightPixels) / ydpi * 25.4),
                                                              widthPixels,
                                                              heightPixels);
    } else {
        m_androidPlatformIntegration->setDisplayMetrics(qRound(double(widthPixels)  / xdpi * 25.4),
                                                        qRound(double(heightPixels) / ydpi * 25.4));
        m_androidPlatformIntegration->setScreenSize(widthPixels, heightPixels);
        m_androidPlatformIntegration->setDesktopSize(desktopWidthPixels, desktopHeightPixels);
    }
}

static void updateWindow(JNIEnv */*env*/, jobject /*thiz*/)
{
    if (!m_androidPlatformIntegration)
        return;

    if (QGuiApplication::instance() != nullptr) {
        foreach (QWindow *w, QGuiApplication::topLevelWindows()) {
            QRect availableGeometry = w->screen()->availableGeometry();
            if (w->geometry().width() > 0 && w->geometry().height() > 0 && availableGeometry.width() > 0 && availableGeometry.height() > 0)
                QWindowSystemInterface::handleExposeEvent(w, QRegion(QRect(QPoint(), w->geometry().size())));
        }
    }

    QAndroidPlatformScreen *screen = static_cast<QAndroidPlatformScreen *>(m_androidPlatformIntegration->screen());
    if (screen->rasterSurfaces())
        QMetaObject::invokeMethod(screen, "setDirty", Qt::QueuedConnection, Q_ARG(QRect,screen->geometry()));
}

static void updateApplicationState(JNIEnv */*env*/, jobject /*thiz*/, jint state)
{
    m_activityActive = (state == Qt::ApplicationActive);

    if (!m_main || !m_androidPlatformIntegration || !QGuiApplicationPrivate::platformIntegration()) {
        QAndroidPlatformIntegration::setDefaultApplicationState(Qt::ApplicationState(state));
        return;
    }

    if (state == Qt::ApplicationActive)
        QtAndroidPrivate::handleResume();
    else if (state == Qt::ApplicationInactive)
        QtAndroidPrivate::handlePause();

    if (state <= Qt::ApplicationInactive) {
        // NOTE: sometimes we will receive two consecutive suspended notifications,
        // In the second suspended notification, QWindowSystemInterface::flushWindowSystemEvents()
        // will deadlock since the dispatcher has been stopped in the first suspended notification.
        // To avoid the deadlock we simply return if we found the event dispatcher has been stopped.
        if (QAndroidEventDispatcherStopper::instance()->stopped())
            return;

        // Don't send timers and sockets events anymore if we are going to hide all windows
        QAndroidEventDispatcherStopper::instance()->goingToStop(true);
        QCoreApplication::processEvents();
        QWindowSystemInterface::handleApplicationStateChanged(Qt::ApplicationState(state));
        {
            AndroidDeadlockProtector protector;
            if (protector.acquire())
                QWindowSystemInterface::flushWindowSystemEvents();
        }
        if (state == Qt::ApplicationSuspended)
            QAndroidEventDispatcherStopper::instance()->stopAll();
    } else {
        QAndroidEventDispatcherStopper::instance()->startAll();
        QWindowSystemInterface::handleApplicationStateChanged(Qt::ApplicationState(state));
        QAndroidEventDispatcherStopper::instance()->goingToStop(false);
    }
}

static void handleOrientationChanged(JNIEnv */*env*/, jobject /*thiz*/, jint newRotation, jint nativeOrientation)
{
    // Array of orientations rotated in 90 degree increments, counterclockwise
    // (same direction as Android measures angles)
    static const Qt::ScreenOrientation orientations[] = {
        Qt::PortraitOrientation,
        Qt::LandscapeOrientation,
        Qt::InvertedPortraitOrientation,
        Qt::InvertedLandscapeOrientation
    };

    // The Android API defines the following constants:
    // ROTATION_0 :   0
    // ROTATION_90 :  1
    // ROTATION_180 : 2
    // ROTATION_270 : 3
    // ORIENTATION_PORTRAIT :  1
    // ORIENTATION_LANDSCAPE : 2

    // and newRotation is how much the current orientation is rotated relative to nativeOrientation

    // which means that we can be really clever here :)
    Qt::ScreenOrientation screenOrientation = orientations[(nativeOrientation - 1 + newRotation) % 4];
    Qt::ScreenOrientation native = orientations[nativeOrientation - 1];

    QAndroidPlatformIntegration::setScreenOrientation(screenOrientation, native);
    if (m_androidPlatformIntegration) {
        QPlatformScreen *screen = m_androidPlatformIntegration->screen();
        QWindowSystemInterface::handleScreenOrientationChange(screen->screen(),
                                                              screenOrientation);
    }
}

static void onActivityResult(JNIEnv */*env*/, jclass /*cls*/,
                             jint requestCode,
                             jint resultCode,
                             jobject data)
{
    QtAndroidPrivate::handleActivityResult(requestCode, resultCode, data);
}

static void onNewIntent(JNIEnv *env, jclass /*cls*/, jobject data)
{
    QtAndroidPrivate::handleNewIntent(env, data);
}

static JNINativeMethod methods[] = {
    {"startQtAndroidPlugin", "()Z", (void *)startQtAndroidPlugin},
    {"startQtApplication", "(Ljava/lang/String;Ljava/lang/String;)V", (void *)startQtApplication},
    {"quitQtAndroidPlugin", "()V", (void *)quitQtAndroidPlugin},
    {"terminateQt", "()V", (void *)terminateQt},
    {"setDisplayMetrics", "(IIIIDDDD)V", (void *)setDisplayMetrics},
    {"setSurface", "(ILjava/lang/Object;II)V", (void *)setSurface},
    {"updateWindow", "()V", (void *)updateWindow},
    {"updateApplicationState", "(I)V", (void *)updateApplicationState},
    {"handleOrientationChanged", "(II)V", (void *)handleOrientationChanged},
    {"onActivityResult", "(IILandroid/content/Intent;)V", (void *)onActivityResult},
    {"onNewIntent", "(Landroid/content/Intent;)V", (void *)onNewIntent}
};

#define FIND_AND_CHECK_CLASS(CLASS_NAME) \
clazz = env->FindClass(CLASS_NAME); \
if (!clazz) { \
    __android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_classErrorMsg, CLASS_NAME); \
    return JNI_FALSE; \
}

#define GET_AND_CHECK_METHOD(VAR, CLASS, METHOD_NAME, METHOD_SIGNATURE) \
VAR = env->GetMethodID(CLASS, METHOD_NAME, METHOD_SIGNATURE); \
if (!VAR) { \
    __android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_methodErrorMsg, METHOD_NAME, METHOD_SIGNATURE); \
    return JNI_FALSE; \
}

#define GET_AND_CHECK_STATIC_METHOD(VAR, CLASS, METHOD_NAME, METHOD_SIGNATURE) \
VAR = env->GetStaticMethodID(CLASS, METHOD_NAME, METHOD_SIGNATURE); \
if (!VAR) { \
    __android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_methodErrorMsg, METHOD_NAME, METHOD_SIGNATURE); \
    return JNI_FALSE; \
}

#define GET_AND_CHECK_FIELD(VAR, CLASS, FIELD_NAME, FIELD_SIGNATURE) \
VAR = env->GetFieldID(CLASS, FIELD_NAME, FIELD_SIGNATURE); \
if (!VAR) { \
    __android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_methodErrorMsg, FIELD_NAME, FIELD_SIGNATURE); \
    return JNI_FALSE; \
}

#define GET_AND_CHECK_STATIC_FIELD(VAR, CLASS, FIELD_NAME, FIELD_SIGNATURE) \
VAR = env->GetStaticFieldID(CLASS, FIELD_NAME, FIELD_SIGNATURE); \
if (!VAR) { \
    __android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_methodErrorMsg, FIELD_NAME, FIELD_SIGNATURE); \
    return JNI_FALSE; \
}

static int registerNatives(JNIEnv *env)
{
    jclass clazz;
    FIND_AND_CHECK_CLASS("org/qtproject/qt5/android/QtNative");
    m_applicationClass = static_cast<jclass>(env->NewGlobalRef(clazz));

    if (env->RegisterNatives(m_applicationClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) {
        __android_log_print(ANDROID_LOG_FATAL,"Qt", "RegisterNatives failed");
        return JNI_FALSE;
    }

    GET_AND_CHECK_STATIC_METHOD(m_createSurfaceMethodID, m_applicationClass, "createSurface", "(IZIIIII)V");
    GET_AND_CHECK_STATIC_METHOD(m_setSurfaceGeometryMethodID, m_applicationClass, "setSurfaceGeometry", "(IIIII)V");
    GET_AND_CHECK_STATIC_METHOD(m_destroySurfaceMethodID, m_applicationClass, "destroySurface", "(I)V");

    jmethodID methodID;
    GET_AND_CHECK_STATIC_METHOD(methodID, m_applicationClass, "activity", "()Landroid/app/Activity;");
    jobject activityObject = env->CallStaticObjectMethod(m_applicationClass, methodID);
    GET_AND_CHECK_STATIC_METHOD(methodID, m_applicationClass, "classLoader", "()Ljava/lang/ClassLoader;");
    m_classLoaderObject = env->NewGlobalRef(env->CallStaticObjectMethod(m_applicationClass, methodID));
    clazz = env->GetObjectClass(m_classLoaderObject);
    GET_AND_CHECK_METHOD(m_loadClassMethodID, clazz, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");

    if (activityObject) {
        m_activityObject = env->NewGlobalRef(activityObject);

        FIND_AND_CHECK_CLASS("android/content/ContextWrapper");
        GET_AND_CHECK_METHOD(methodID, clazz, "getAssets", "()Landroid/content/res/AssetManager;");
        m_assetManager = AAssetManager_fromJava(env, env->CallObjectMethod(activityObject, methodID));

        GET_AND_CHECK_METHOD(methodID, clazz, "getResources", "()Landroid/content/res/Resources;");
        m_resourcesObj = env->NewGlobalRef(env->CallObjectMethod(activityObject, methodID));

        FIND_AND_CHECK_CLASS("android/graphics/Bitmap");
        m_bitmapClass = static_cast<jclass>(env->NewGlobalRef(clazz));
        GET_AND_CHECK_STATIC_METHOD(m_createBitmapMethodID, m_bitmapClass
                                    , "createBitmap", "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
        FIND_AND_CHECK_CLASS("android/graphics/Bitmap$Config");
        jfieldID fieldId;
        GET_AND_CHECK_STATIC_FIELD(fieldId, clazz, "ARGB_8888", "Landroid/graphics/Bitmap$Config;");
        m_ARGB_8888_BitmapConfigValue = env->NewGlobalRef(env->GetStaticObjectField(clazz, fieldId));
        GET_AND_CHECK_STATIC_FIELD(fieldId, clazz, "RGB_565", "Landroid/graphics/Bitmap$Config;");
        m_RGB_565_BitmapConfigValue = env->NewGlobalRef(env->GetStaticObjectField(clazz, fieldId));

        FIND_AND_CHECK_CLASS("android/graphics/drawable/BitmapDrawable");
        m_bitmapDrawableClass = static_cast<jclass>(env->NewGlobalRef(clazz));
        GET_AND_CHECK_METHOD(m_bitmapDrawableConstructorMethodID,
                             m_bitmapDrawableClass,
                             "<init>",
                             "(Landroid/content/res/Resources;Landroid/graphics/Bitmap;)V");
    }



    return JNI_TRUE;
}

QT_END_NAMESPACE

Q_DECL_EXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void */*reserved*/)
{
    QT_USE_NAMESPACE
    typedef union {
        JNIEnv *nativeEnvironment;
        void *venv;
    } UnionJNIEnvToVoid;

    __android_log_print(ANDROID_LOG_INFO, "Qt", "qt start");
    UnionJNIEnvToVoid uenv;
    uenv.venv = nullptr;
    m_javaVM = nullptr;

    if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_4) != JNI_OK) {
        __android_log_print(ANDROID_LOG_FATAL, "Qt", "GetEnv failed");
        return -1;
    }

    JNIEnv *env = uenv.nativeEnvironment;
    if (!registerNatives(env)
            || !QtAndroidInput::registerNatives(env)
            || !QtAndroidMenu::registerNatives(env)
            || !QtAndroidAccessibility::registerNatives(env)
            || !QtAndroidDialogHelpers::registerNatives(env)) {
        __android_log_print(ANDROID_LOG_FATAL, "Qt", "registerNatives failed");
        return -1;
    }

    m_javaVM = vm;
    return JNI_VERSION_1_4;
}
