/****************************************************************************
**
** Copyright (C) 2016 BogDan Vatra <bogdan@kde.org>
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Android port of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** 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 https://www.qt.io/terms-conditions. For further
** information use the contact form at https://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 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

package org.qtproject.qt.android;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Objects;
import java.util.concurrent.Semaphore;
import java.io.IOException;
import java.util.HashMap;

import android.app.Activity;
import android.app.Service;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ApplicationInfo;
import android.content.UriPermission;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.content.ClipboardManager;
import android.content.ClipboardManager.OnPrimaryClipChangedListener;
import android.content.ClipData;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.View;
import android.view.InputDevice;
import android.database.Cursor;
import android.provider.DocumentsContract;

import java.lang.reflect.Method;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.Iterator;
import java.util.List;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

public class QtNative
{
    private static Activity m_activity = null;
    private static boolean m_activityPaused = false;
    private static Service m_service = null;
    private static QtActivityDelegate m_activityDelegate = null;
    private static QtServiceDelegate m_serviceDelegate = null;
    public static Object m_mainActivityMutex = new Object(); // mutex used to synchronize runnable operations

    public static final String QtTAG = "Qt JAVA"; // string used for Log.x
    private static ArrayList<Runnable> m_lostActions = new ArrayList<Runnable>(); // a list containing all actions which could not be performed (e.g. the main activity is destroyed, etc.)
    private static boolean m_started = false;
    private static int m_displayMetricsScreenWidthPixels = 0;
    private static int m_displayMetricsScreenHeightPixels = 0;
    private static int m_displayMetricsAvailableLeftPixels = 0;
    private static int m_displayMetricsAvailableTopPixels = 0;
    private static int m_displayMetricsAvailableWidthPixels = 0;
    private static int m_displayMetricsAvailableHeightPixels = 0;
    private static double m_displayMetricsXDpi = .0;
    private static double m_displayMetricsYDpi = .0;
    private static double m_displayMetricsScaledDensity = 1.0;
    private static double m_displayMetricsDensity = 1.0;
    private static int m_oldx, m_oldy;
    private static final int m_moveThreshold = 0;
    private static ClipboardManager m_clipboardManager = null;
    private static Method m_checkSelfPermissionMethod = null;
    private static Boolean m_tabletEventSupported = null;
    private static boolean m_usePrimaryClip = false;
    public static QtThread m_qtThread = new QtThread();
    private static HashMap<String, Uri> m_cachedUris = new HashMap<String, Uri>();
    private static ArrayList<String> m_knownDirs = new ArrayList<String>();

    private static final Runnable runPendingCppRunnablesRunnable = new Runnable() {
        @Override
        public void run() {
            runPendingCppRunnables();
        }
    };

    private static ClassLoader m_classLoader = null;
    public static ClassLoader classLoader()
    {
        return m_classLoader;
    }

    public static void setClassLoader(ClassLoader classLoader)
    {
            m_classLoader = classLoader;
    }

    public static Activity activity()
    {
        synchronized (m_mainActivityMutex) {
            return m_activity;
        }
    }

    public static Service service()
    {
        synchronized (m_mainActivityMutex) {
            return m_service;
        }
    }


    public static QtActivityDelegate activityDelegate()
    {
        synchronized (m_mainActivityMutex) {
            return m_activityDelegate;
        }
    }

    public static QtServiceDelegate serviceDelegate()
    {
        synchronized (m_mainActivityMutex) {
            return m_serviceDelegate;
        }
    }

    public static String[] getStringArray(String joinedString)
    {
        return joinedString.split(",");
    }

    private static Uri getUriWithValidPermission(Context context, String uri, String openMode)
    {
        try {
            Uri parsedUri = Uri.parse(uri);
            String scheme = parsedUri.getScheme();

            // We only want to check permissions for content Uris
            if (scheme.compareTo("content") != 0)
                return parsedUri;

            List<UriPermission> permissions = context.getContentResolver().getPersistedUriPermissions();
            String uriStr = parsedUri.getPath();

            for (int i = 0; i < permissions.size(); ++i) {
                Uri iterUri = permissions.get(i).getUri();
                boolean isRightPermission = permissions.get(i).isReadPermission();

                if (!openMode.equals("r"))
                   isRightPermission = permissions.get(i).isWritePermission();

                if (iterUri.getPath().equals(uriStr) && isRightPermission)
                    return iterUri;
            }

            // Android 6 and earlier could still manage to open the file so we can return the
            // parsed uri here
            if (Build.VERSION.SDK_INT < 24)
                return parsedUri;
            return null;
        } catch (SecurityException e) {
            e.printStackTrace();
            return null;
        }
    }

    public static boolean openURL(Context context, String url, String mime)
    {
        Uri uri = getUriWithValidPermission(context, url, "r");

        if (uri == null) {
            Log.e(QtTAG, "openURL(): No permissions to open Uri");
            return false;
        }

        try {
            Intent intent = new Intent(Intent.ACTION_VIEW, uri);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            if (!mime.isEmpty())
                intent.setDataAndType(uri, mime);

            activity().startActivity(intent);

            return true;
        } catch (IllegalArgumentException e) {
            Log.e(QtTAG, "openURL(): Invalid Uri");
            e.printStackTrace();
            return false;
        } catch (UnsupportedOperationException e) {
            Log.e(QtTAG, "openURL(): Unsupported operation for given Uri");
            e.printStackTrace();
            return false;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    public static int openFdForContentUrl(Context context, String contentUrl, String openMode)
    {
        Uri uri = m_cachedUris.get(contentUrl);
        if (uri == null)
            uri = getUriWithValidPermission(context, contentUrl, openMode);
        int error = -1;

        if (uri == null) {
            Log.e(QtTAG, "openFdForContentUrl(): No permissions to open Uri");
            return error;
        }

        try {
            ContentResolver resolver = context.getContentResolver();
            ParcelFileDescriptor fdDesc = resolver.openFileDescriptor(uri, openMode);
            return fdDesc.detachFd();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return error;
        } catch (IllegalArgumentException e) {
            Log.e(QtTAG, "openFdForContentUrl(): Invalid Uri");
            e.printStackTrace();
            return error;
        }
    }

    public static long getSize(Context context, String contentUrl)
    {
        long size = -1;
        Uri uri = m_cachedUris.get(contentUrl);
        if (uri == null)
            uri = getUriWithValidPermission(context, contentUrl, "r");

        if (uri == null) {
            Log.e(QtTAG, "getSize(): No permissions to open Uri");
            return size;
        } else if (!m_cachedUris.containsKey(contentUrl)) {
            m_cachedUris.put(contentUrl, uri);
        }

        try {
            ContentResolver resolver = context.getContentResolver();
            Cursor cur = resolver.query(uri, new String[] { DocumentsContract.Document.COLUMN_SIZE }, null, null, null);
            if (cur != null) {
                if (cur.moveToFirst())
                    size = cur.getLong(0);
                cur.close();
            }
            return size;
        } catch (IllegalArgumentException e) {
            Log.e(QtTAG, "getSize(): Invalid Uri");
            e.printStackTrace();
            return size;
        }  catch (UnsupportedOperationException e) {
            Log.e(QtTAG, "getSize(): Unsupported operation for given Uri");
            e.printStackTrace();
            return size;
        }
    }

    public static boolean checkFileExists(Context context, String contentUrl)
    {
        boolean exists = false;
        Uri uri = m_cachedUris.get(contentUrl);
        if (uri == null)
            uri = getUriWithValidPermission(context, contentUrl, "r");
        if (uri == null) {
            Log.e(QtTAG, "checkFileExists(): No permissions to open Uri");
            return exists;
        } else {
            if (!m_cachedUris.containsKey(contentUrl))
                m_cachedUris.put(contentUrl, uri);
        }

        try {
            ContentResolver resolver = context.getContentResolver();
            Cursor cur = resolver.query(uri, null, null, null, null);
            if (cur != null) {
                exists = true;
                cur.close();
            }
            return exists;
        } catch (IllegalArgumentException e) {
            Log.e(QtTAG, "checkFileExists(): Invalid Uri");
            e.printStackTrace();
            return exists;
        } catch (UnsupportedOperationException e) {
            Log.e(QtTAG, "checkFileExists(): Unsupported operation for given Uri");
            e.printStackTrace();
            return false;
        }
    }

    public static boolean checkIfWritable(Context context, String contentUrl)
    {
        return getUriWithValidPermission(context, contentUrl, "w") != null;
    }

    public static boolean checkIfDir(Context context, String contentUrl)
    {
        boolean isDir = false;
        Uri uri = m_cachedUris.get(contentUrl);
        if (m_knownDirs.contains(contentUrl))
            return true;
        if (uri == null) {
            uri = getUriWithValidPermission(context, contentUrl, "r");
        }
        if (uri == null) {
            Log.e(QtTAG, "isDir(): No permissions to open Uri");
            return isDir;
        } else {
            if (!m_cachedUris.containsKey(contentUrl))
                m_cachedUris.put(contentUrl, uri);
        }

        try {
            final List<String> paths = uri.getPathSegments();
            // getTreeDocumentId will throw an exception if it is not a directory so check manually
            if (!paths.get(0).equals("tree"))
                return false;
            ContentResolver resolver = context.getContentResolver();
            Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri));
            if (!docUri.toString().startsWith(uri.toString()))
                return false;
            Cursor cur = resolver.query(docUri, new String[] { DocumentsContract.Document.COLUMN_MIME_TYPE }, null, null, null);
            if (cur != null) {
                if (cur.moveToFirst()) {
                    final String dirStr = new String(DocumentsContract.Document.MIME_TYPE_DIR);
                    isDir = cur.getString(0).equals(dirStr);
                    if (isDir)
                        m_knownDirs.add(contentUrl);
                }
                cur.close();
            }
            return isDir;
        } catch (IllegalArgumentException e) {
            Log.e(QtTAG, "checkIfDir(): Invalid Uri");
            e.printStackTrace();
            return false;
        } catch (UnsupportedOperationException e) {
            Log.e(QtTAG, "checkIfDir(): Unsupported operation for given Uri");
            e.printStackTrace();
            return false;
        }
    }
    public static String[] listContentsFromTreeUri(Context context, String contentUrl)
    {
        Uri treeUri = Uri.parse(contentUrl);
        final ArrayList<String> results = new ArrayList<String>();
        if (treeUri == null) {
            Log.e(QtTAG, "listContentsFromTreeUri(): Invalid uri");
            return results.toArray(new String[results.size()]);
        }
        final ContentResolver resolver = context.getContentResolver();
        final Uri docUri = DocumentsContract.buildDocumentUriUsingTree(treeUri,
                DocumentsContract.getTreeDocumentId(treeUri));
        final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(docUri,
                                DocumentsContract.getDocumentId(docUri));
        Cursor c = null;
        final String dirStr = new String(DocumentsContract.Document.MIME_TYPE_DIR);
        try {
            c = resolver.query(childrenUri, new String[] {
                    DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_MIME_TYPE }, null, null, null);
            while (c.moveToNext()) {
                final String fileString = c.getString(1);
                if (!m_cachedUris.containsKey(contentUrl + "/" + fileString)) {
                    m_cachedUris.put(contentUrl + "/" + fileString,
                                     DocumentsContract.buildDocumentUriUsingTree(treeUri, c.getString(0)));
                }
                results.add(fileString);
                if (c.getString(2).equals(dirStr))
                    m_knownDirs.add(contentUrl + "/" + fileString);
            }
            c.close();
        } catch (Exception e) {
            Log.w(QtTAG, "Failed query: " + e);
            return results.toArray(new String[results.size()]);
        }
        return results.toArray(new String[results.size()]);
    }
    // this method loads full path libs
    public static void loadQtLibraries(final ArrayList<String> libraries)
    {
        m_qtThread.run(new Runnable() {
            @Override
            public void run() {
                if (libraries == null)
                    return;
                for (String libName : libraries) {
                    try {
                        File f = new File(libName);
                        if (f.exists())
                            System.load(libName);
                        else
                            Log.i(QtTAG, "Can't find '" + libName + "'");
                    } catch (SecurityException e) {
                        Log.i(QtTAG, "Can't load '" + libName + "'", e);
                    } catch (Exception e) {
                        Log.i(QtTAG, "Can't load '" + libName + "'", e);
                    }
                }
            }
        });
    }

    // this method loads bundled libs by name.
    public static void loadBundledLibraries(final ArrayList<String> libraries, final String nativeLibraryDir)
    {
        m_qtThread.run(new Runnable() {
            @Override
            public void run() {
                if (libraries == null)
                    return;

                for (String libName : libraries) {
                    try {
                        String libNameTemplate = "lib" + libName + ".so";
                        File f = new File(nativeLibraryDir + libNameTemplate);
                        if (!f.exists()) {
                            Log.i(QtTAG, "Can't find '" + f.getAbsolutePath());
                            try {
                                ApplicationInfo info = getContext().getApplicationContext().getPackageManager()
                                    .getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA);
                                String systemLibraryDir = QtNativeLibrariesDir.systemLibrariesDir;
                                if (info.metaData.containsKey("android.app.system_libs_prefix"))
                                    systemLibraryDir = info.metaData.getString("android.app.system_libs_prefix");
                                f = new File(systemLibraryDir + libNameTemplate);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                        if (f.exists())
                            System.load(f.getAbsolutePath());
                        else
                            Log.i(QtTAG, "Can't find '" + f.getAbsolutePath());
                    } catch (Exception e) {
                        Log.i(QtTAG, "Can't load '" + libName + "'", e);
                    }
                }
            }
        });
    }

    public static String loadMainLibrary(final String mainLibrary, final String nativeLibraryDir)
    {
        final String[] res = new String[1];
        res[0] = null;
        m_qtThread.run(new Runnable() {
            @Override
            public void run() {
                try {
                    String mainLibNameTemplate = "lib" + mainLibrary + ".so";
                    File f = new File(nativeLibraryDir + mainLibNameTemplate);
                    if (!f.exists()) {
                        try {
                            ApplicationInfo info = getContext().getApplicationContext().getPackageManager()
                                    .getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA);
                            String systemLibraryDir = QtNativeLibrariesDir.systemLibrariesDir;
                            if (info.metaData.containsKey("android.app.system_libs_prefix"))
                                systemLibraryDir = info.metaData.getString("android.app.system_libs_prefix");
                            f = new File(systemLibraryDir + mainLibNameTemplate);
                        } catch (Exception e) {
                            e.printStackTrace();
                            return;
                        }
                    }
                    if (!f.exists())
                        return;
                    System.load(f.getAbsolutePath());
                    res[0] = f.getAbsolutePath();
                } catch (Exception e) {
                    Log.e(QtTAG, "Can't load '" + mainLibrary + "'", e);
                }
            }
        });
        return res[0];
    }

    public static void setActivity(Activity qtMainActivity, QtActivityDelegate qtActivityDelegate)
    {
        synchronized (m_mainActivityMutex) {
            m_activity = qtMainActivity;
            m_activityDelegate = qtActivityDelegate;
        }
    }

    public static void setService(Service qtMainService, QtServiceDelegate qtServiceDelegate)
    {
        synchronized (m_mainActivityMutex) {
            m_service = qtMainService;
            m_serviceDelegate = qtServiceDelegate;
        }
    }

    public static void setApplicationState(int state)
    {
        synchronized (m_mainActivityMutex) {
            switch (state) {
                case QtActivityDelegate.ApplicationActive:
                    m_activityPaused = false;
                    Iterator<Runnable> itr = m_lostActions.iterator();
                    while (itr.hasNext())
                        runAction(itr.next());
                    m_lostActions.clear();
                    break;
                default:
                    m_activityPaused = true;
                    break;
            }
        }
        updateApplicationState(state);
    }

    private static void runAction(Runnable action)
    {
        synchronized (m_mainActivityMutex) {
            final Looper mainLooper = Looper.getMainLooper();
            final Handler handler = new Handler(mainLooper);
            final boolean actionIsQueued = !m_activityPaused && m_activity != null && mainLooper != null && handler.post(action);
            if (!actionIsQueued)
                m_lostActions.add(action);
        }
    }

    private static void runPendingCppRunnablesOnAndroidThread()
    {
        synchronized (m_mainActivityMutex) {
            if (m_activity != null) {
                if (!m_activityPaused)
                    m_activity.runOnUiThread(runPendingCppRunnablesRunnable);
                else
                    runAction(runPendingCppRunnablesRunnable);
            } else {
                final Looper mainLooper = Looper.getMainLooper();
                final Thread looperThread = mainLooper.getThread();
                if (looperThread.equals(Thread.currentThread())) {
                    runPendingCppRunnablesRunnable.run();
                } else {
                    final Handler handler = new Handler(mainLooper);
                    handler.post(runPendingCppRunnablesRunnable);
                }
            }
        }
    }

    private static void setViewVisibility(final View view, final boolean visible)
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                view.setVisibility(visible ? View.VISIBLE : View.GONE);
            }
        });
    }

    public static boolean startApplication(String params, final String environment, String mainLib) throws Exception
    {
        if (params == null)
            params = "-platform\tandroid";

        final boolean[] res = new boolean[1];
        res[0] = false;
        synchronized (m_mainActivityMutex) {
            if (params.length() > 0 && !params.startsWith("\t"))
                params = "\t" + params;
            final String qtParams = mainLib + params;
            m_qtThread.run(new Runnable() {
                @Override
                public void run() {
                    res[0] = startQtAndroidPlugin(qtParams, environment);
                    setDisplayMetrics(m_displayMetricsScreenWidthPixels,
                                      m_displayMetricsScreenHeightPixels,
                                      m_displayMetricsAvailableLeftPixels,
                                      m_displayMetricsAvailableTopPixels,
                                      m_displayMetricsAvailableWidthPixels,
                                      m_displayMetricsAvailableHeightPixels,
                                      m_displayMetricsXDpi,
                                      m_displayMetricsYDpi,
                                      m_displayMetricsScaledDensity,
                                      m_displayMetricsDensity);
                }
            });
            m_qtThread.post(new Runnable() {
                @Override
                public void run() {
                    startQtApplication();
                }
            });
            waitForServiceSetup();
            m_started = true;
        }
        return res[0];
    }

    public static void setApplicationDisplayMetrics(int screenWidthPixels,
                                                    int screenHeightPixels,
                                                    int availableLeftPixels,
                                                    int availableTopPixels,
                                                    int availableWidthPixels,
                                                    int availableHeightPixels,
                                                    double XDpi,
                                                    double YDpi,
                                                    double scaledDensity,
                                                    double density)
    {
        /* Fix buggy dpi report */
        if (XDpi < android.util.DisplayMetrics.DENSITY_LOW)
            XDpi = android.util.DisplayMetrics.DENSITY_LOW;
        if (YDpi < android.util.DisplayMetrics.DENSITY_LOW)
            YDpi = android.util.DisplayMetrics.DENSITY_LOW;

        synchronized (m_mainActivityMutex) {
            if (m_started) {
                setDisplayMetrics(screenWidthPixels,
                                  screenHeightPixels,
                                  availableLeftPixels,
                                  availableTopPixels,
                                  availableWidthPixels,
                                  availableHeightPixels,
                                  XDpi,
                                  YDpi,
                                  scaledDensity,
                                  density);
            } else {
                m_displayMetricsScreenWidthPixels = screenWidthPixels;
                m_displayMetricsScreenHeightPixels = screenHeightPixels;
                m_displayMetricsAvailableLeftPixels = availableLeftPixels;
                m_displayMetricsAvailableTopPixels = availableTopPixels;
                m_displayMetricsAvailableWidthPixels = availableWidthPixels;
                m_displayMetricsAvailableHeightPixels = availableHeightPixels;
                m_displayMetricsXDpi = XDpi;
                m_displayMetricsYDpi = YDpi;
                m_displayMetricsScaledDensity = scaledDensity;
                m_displayMetricsDensity = density;
            }
        }
    }



    // application methods
    public static native boolean startQtAndroidPlugin(String params, String env);
    public static native void startQtApplication();
    public static native void waitForServiceSetup();
    public static native void quitQtCoreApplication();
    public static native void quitQtAndroidPlugin();
    public static native void terminateQt();
    // application methods

    private static void quitApp()
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                quitQtAndroidPlugin();
                if (m_activity != null)
                     m_activity.finish();
                 if (m_service != null)
                     m_service.stopSelf();
            }
        });
    }

    //@ANDROID-9
    static private int getAction(int index, MotionEvent event)
    {
        int action = event.getActionMasked();
        if (action == MotionEvent.ACTION_MOVE) {
            int hsz = event.getHistorySize();
            if (hsz > 0) {
                float x = event.getX(index);
                float y = event.getY(index);
                for (int h = 0; h < hsz; ++h) {
                    if ( event.getHistoricalX(index, h) != x ||
                         event.getHistoricalY(index, h) != y )
                        return 1;
                }
                return 2;
            }
            return 1;
        }
        if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN && index == event.getActionIndex()) {
            return 0;
        } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_POINTER_UP && index == event.getActionIndex()) {
            return 3;
        }
        return 2;
    }
    //@ANDROID-9

    static public void sendTouchEvent(MotionEvent event, int id)
    {
        int pointerType = 0;

        if (m_tabletEventSupported == null)
            m_tabletEventSupported = isTabletEventSupported();

        switch (event.getToolType(0)) {
        case MotionEvent.TOOL_TYPE_STYLUS:
            pointerType = 1; // QTabletEvent::Pen
            break;
        case MotionEvent.TOOL_TYPE_ERASER:
            pointerType = 3; // QTabletEvent::Eraser
            break;
        }

        if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
            sendMouseEvent(event, id);
        } else if (m_tabletEventSupported && pointerType != 0) {
            tabletEvent(id, event.getDeviceId(), event.getEventTime(), event.getAction(), pointerType,
                event.getButtonState(), event.getX(), event.getY(), event.getPressure());
        } else {
            touchBegin(id);
            for (int i = 0; i < event.getPointerCount(); ++i) {
                    touchAdd(id,
                             event.getPointerId(i),
                             getAction(i, event),
                             i == 0,
                             (int)event.getX(i),
                             (int)event.getY(i),
                             event.getTouchMajor(i),
                             event.getTouchMinor(i),
                             event.getOrientation(i),
                             event.getPressure(i));
            }

            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    touchEnd(id, 0);
                    break;

                case MotionEvent.ACTION_UP:
                    touchEnd(id, 2);
                    break;

                default:
                    touchEnd(id, 1);
            }
        }
    }

    static public void sendTrackballEvent(MotionEvent event, int id)
    {
        sendMouseEvent(event,id);
    }

    static public boolean sendGenericMotionEvent(MotionEvent event, int id)
    {
        if (((event.getAction() & (MotionEvent.ACTION_SCROLL | MotionEvent.ACTION_HOVER_MOVE)) == 0)
                || (event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != InputDevice.SOURCE_CLASS_POINTER) {
            return false;
        }

        return sendMouseEvent(event, id);
    }

    static public boolean sendMouseEvent(MotionEvent event, int id)
    {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_UP:
                mouseUp(id, (int) event.getX(), (int) event.getY());
                break;

            case MotionEvent.ACTION_DOWN:
                mouseDown(id, (int) event.getX(), (int) event.getY());
                m_oldx = (int) event.getX();
                m_oldy = (int) event.getY();
                break;
            case MotionEvent.ACTION_HOVER_MOVE:
            case MotionEvent.ACTION_MOVE:
                if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
                    mouseMove(id, (int) event.getX(), (int) event.getY());
                } else {
                    int dx = (int) (event.getX() - m_oldx);
                    int dy = (int) (event.getY() - m_oldy);
                    if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
                        mouseMove(id, (int) event.getX(), (int) event.getY());
                        m_oldx = (int) event.getX();
                        m_oldy = (int) event.getY();
                    }
                }
                break;
            case MotionEvent.ACTION_SCROLL:
                mouseWheel(id, (int) event.getX(), (int) event.getY(),
                        event.getAxisValue(MotionEvent.AXIS_HSCROLL), event.getAxisValue(MotionEvent.AXIS_VSCROLL));
                break;
            default:
                return false;
        }
        return true;
    }

    public static Context getContext() {
        if (m_activity != null)
            return m_activity;
        return m_service;
    }

    public static int checkSelfPermission(String permission)
    {
        int perm = PackageManager.PERMISSION_DENIED;
        synchronized (m_mainActivityMutex) {
            Context context = getContext();
            try {
                if (m_checkSelfPermissionMethod == null)
                    m_checkSelfPermissionMethod = Context.class.getMethod("checkSelfPermission", String.class);
                perm = (Integer)m_checkSelfPermissionMethod.invoke(context, permission);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        return perm;
    }

    private static void updateSelection(final int selStart,
                                        final int selEnd,
                                        final int candidatesStart,
                                        final int candidatesEnd)
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activityDelegate != null)
                    m_activityDelegate.updateSelection(selStart, selEnd, candidatesStart, candidatesEnd);
            }
        });
    }

    private static void updateHandles(final int mode,
                                      final int editX,
                                      final int editY,
                                      final int editButtons,
                                      final int x1,
                                      final int y1,
                                      final int x2,
                                      final int y2,
                                      final boolean rtl)
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                m_activityDelegate.updateHandles(mode, editX, editY, editButtons, x1, y1, x2, y2, rtl);
            }
        });
    }

    private static void showSoftwareKeyboard(final int x,
                                             final int y,
                                             final int width,
                                             final int height,
                                             final int inputHints,
                                             final int enterKeyType)
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activityDelegate != null)
                    m_activityDelegate.showSoftwareKeyboard(x, y, width, height, inputHints, enterKeyType);
            }
        });
    }

    private static void resetSoftwareKeyboard()
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activityDelegate != null)
                    m_activityDelegate.resetSoftwareKeyboard();
            }
        });
    }

    private static void hideSoftwareKeyboard()
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activityDelegate != null)
                    m_activityDelegate.hideSoftwareKeyboard();
            }
        });
    }

    private static void setSystemUiVisibility(final int systemUiVisibility)
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activityDelegate != null) {
                    m_activityDelegate.setSystemUiVisibility(systemUiVisibility);
                }
                updateWindow();
            }
        });
    }

    private static void notifyAccessibilityLocationChange()
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activityDelegate != null) {
                    m_activityDelegate.notifyAccessibilityLocationChange();
                }
            }
        });
    }

    private static void notifyObjectHide(final int viewId)
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activityDelegate != null) {
                    m_activityDelegate.notifyObjectHide(viewId);
                }
            }
        });
    }

    private static void notifyObjectFocus(final int viewId)
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activityDelegate != null) {
                    m_activityDelegate.notifyObjectFocus(viewId);
                }
            }
        });
    }

    private static void registerClipboardManager()
    {
        if (m_service == null || m_activity != null) { // Avoid freezing if only service
            final Semaphore semaphore = new Semaphore(0);
            runAction(new Runnable() {
                @Override
                public void run() {
                    if (m_activity != null)
                        m_clipboardManager = (android.content.ClipboardManager) m_activity.getSystemService(Context.CLIPBOARD_SERVICE);
                    if (m_clipboardManager != null) {
                        m_clipboardManager.addPrimaryClipChangedListener(new ClipboardManager.OnPrimaryClipChangedListener() {
                            public void onPrimaryClipChanged() {
                                onClipboardDataChanged();
                            }
                        });
                    }
                    semaphore.release();
                }
            });
            try {
                semaphore.acquire();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private static void clearClipData()
    {
        if (Build.VERSION.SDK_INT >= 28 && m_clipboardManager != null)
            m_clipboardManager.clearPrimaryClip();
    }
    private static void setClipboardText(String text)
    {
        if (m_clipboardManager != null) {
            ClipData clipData = ClipData.newPlainText("text/plain", text);
            updatePrimaryClip(clipData);
        }
    }

    public static boolean hasClipboardText()
    {
        try {
            if (m_clipboardManager != null && m_clipboardManager.hasPrimaryClip()) {
                ClipData primaryClip = m_clipboardManager.getPrimaryClip();
                for (int i = 0; i < primaryClip.getItemCount(); ++i)
                    if (primaryClip.getItemAt(i).getText() != null)
                        return true;
            }
        } catch (Exception e) {
            Log.e(QtTAG, "Failed to get clipboard data", e);
        }
        return false;
    }

    private static String getClipboardText()
    {
        try {
            if (m_clipboardManager != null && m_clipboardManager.hasPrimaryClip()) {
                ClipData primaryClip = m_clipboardManager.getPrimaryClip();
                for (int i = 0; i < primaryClip.getItemCount(); ++i)
                    if (primaryClip.getItemAt(i).getText() != null)
                        return primaryClip.getItemAt(i).getText().toString();
            }
        } catch (Exception e) {
            Log.e(QtTAG, "Failed to get clipboard data", e);
        }
        return "";
    }

    private static void updatePrimaryClip(ClipData clipData)
    {
        try {
            if (m_usePrimaryClip) {
                ClipData clip = m_clipboardManager.getPrimaryClip();
                if (Build.VERSION.SDK_INT >= 26) {
                    Objects.requireNonNull(clip).addItem(m_activity.getContentResolver(), clipData.getItemAt(0));
                } else {
                    Objects.requireNonNull(clip).addItem(clipData.getItemAt(0));
                }
                m_clipboardManager.setPrimaryClip(clip);
            } else {
                m_clipboardManager.setPrimaryClip(clipData);
                m_usePrimaryClip = true;
            }
        } catch (Exception e) {
            Log.e(QtTAG, "Failed to set clipboard data", e);
        }
    }

    private static void setClipboardHtml(String text, String html)
    {
        if (m_clipboardManager != null) {
            ClipData clipData = ClipData.newHtmlText("text/html", text, html);
            updatePrimaryClip(clipData);
        }
    }

    public static boolean hasClipboardHtml()
    {
        try {
            if (m_clipboardManager != null && m_clipboardManager.hasPrimaryClip()) {
                ClipData primaryClip = m_clipboardManager.getPrimaryClip();
                for (int i = 0; i < Objects.requireNonNull(primaryClip).getItemCount(); ++i)
                    if (primaryClip.getItemAt(i).getHtmlText() != null)
                        return true;
            }
        } catch (Exception e) {
            Log.e(QtTAG, "Failed to get clipboard data", e);
        }
        return false;
    }

    private static String getClipboardHtml()
    {
        try {
            if (m_clipboardManager != null && m_clipboardManager.hasPrimaryClip()) {
                ClipData primaryClip = m_clipboardManager.getPrimaryClip();
                for (int i = 0; i < primaryClip.getItemCount(); ++i)
                    if (primaryClip.getItemAt(i).getHtmlText() != null)
                        return primaryClip.getItemAt(i).getHtmlText().toString();
            }
        } catch (Exception e) {
            Log.e(QtTAG, "Failed to get clipboard data", e);
        }
        return "";
    }

    private static void setClipboardUri(String uriString)
    {
        if (m_clipboardManager != null) {
            ClipData clipData = ClipData.newUri(m_activity.getContentResolver(), "text/uri-list",
                                                Uri.parse(uriString));
            updatePrimaryClip(clipData);
        }
    }

    public static boolean hasClipboardUri()
    {
        try {
            if (m_clipboardManager != null && m_clipboardManager.hasPrimaryClip()) {
                ClipData primaryClip = m_clipboardManager.getPrimaryClip();
                for (int i = 0; i < primaryClip.getItemCount(); ++i)
                    if (primaryClip.getItemAt(i).getUri() != null)
                        return true;
            }
        } catch (Exception e) {
            Log.e(QtTAG, "Failed to get clipboard data", e);
        }
        return false;
    }

    private static String[] getClipboardUris()
    {
        ArrayList<String> uris = new ArrayList<String>();
        try {
            if (m_clipboardManager != null && m_clipboardManager.hasPrimaryClip()) {
                ClipData primaryClip = m_clipboardManager.getPrimaryClip();
                for (int i = 0; i < primaryClip.getItemCount(); ++i)
                    if (primaryClip.getItemAt(i).getUri() != null)
                        uris.add(primaryClip.getItemAt(i).getUri().toString());
            }
        } catch (Exception e) {
            Log.e(QtTAG, "Failed to get clipboard data", e);
        }
        String[] strings = new String[uris.size()];
        strings = uris.toArray(strings);
        return strings;
    }

    private static void openContextMenu(final int x, final int y, final int w, final int h)
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activityDelegate != null)
                    m_activityDelegate.openContextMenu(x, y, w, h);
            }
        });
    }

    private static void closeContextMenu()
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activityDelegate != null)
                    m_activityDelegate.closeContextMenu();
            }
        });
    }

    private static void resetOptionsMenu()
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activityDelegate != null)
                    m_activityDelegate.resetOptionsMenu();
            }
        });
    }

    private static void openOptionsMenu()
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activity != null)
                    m_activity.openOptionsMenu();
            }
        });
    }

    private static byte[][] getSSLCertificates()
    {
        ArrayList<byte[]> certificateList = new ArrayList<byte[]>();

        try {
            TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            factory.init((KeyStore) null);

            for (TrustManager manager : factory.getTrustManagers()) {
                if (manager instanceof X509TrustManager) {
                    X509TrustManager trustManager = (X509TrustManager) manager;

                    for (X509Certificate certificate : trustManager.getAcceptedIssuers()) {
                        byte buffer[] = certificate.getEncoded();
                        certificateList.add(buffer);
                    }
                }
            }
        } catch (Exception e) {
            Log.e(QtTAG, "Failed to get certificates", e);
        }

        byte[][] certificateArray = new byte[certificateList.size()][];
        certificateArray = certificateList.toArray(certificateArray);
        return certificateArray;
    }

    private static void createSurface(final int id, final boolean onTop, final int x, final int y, final int w, final int h, final int imageDepth)
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activityDelegate != null)
                    m_activityDelegate.createSurface(id, onTop, x, y, w, h, imageDepth);
            }
        });
    }

    private static void insertNativeView(final int id, final View view, final int x, final int y, final int w, final int h)
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activityDelegate != null)
                    m_activityDelegate.insertNativeView(id, view, x, y, w, h);
            }
        });
    }

    private static void setSurfaceGeometry(final int id, final int x, final int y, final int w, final int h)
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activityDelegate != null)
                    m_activityDelegate.setSurfaceGeometry(id, x, y, w, h);
            }
        });
    }

    private static void bringChildToFront(final int id)
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activityDelegate != null)
                    m_activityDelegate.bringChildToFront(id);
            }
        });
    }

    private static void bringChildToBack(final int id)
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activityDelegate != null)
                    m_activityDelegate.bringChildToBack(id);
            }
        });
    }

    private static void destroySurface(final int id)
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activityDelegate != null)
                    m_activityDelegate.destroySurface(id);
            }
        });
    }

    private static void initializeAccessibility()
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                m_activityDelegate.initializeAccessibility();
            }
        });
    }

    private static void hideSplashScreen(final int duration)
    {
        runAction(new Runnable() {
            @Override
            public void run() {
                if (m_activityDelegate != null)
                    m_activityDelegate.hideSplashScreen(duration);
            }
        });
    }

    private static String[] listAssetContent(android.content.res.AssetManager asset, String path) {
        String [] list;
        ArrayList<String> res = new ArrayList<String>();
        try {
            list = asset.list(path);
            if (list.length > 0) {
                for (String file : list) {
                    try {
                        String[] isDir = asset.list(path.length() > 0 ? path + "/" + file : file);
                        if (isDir != null && isDir.length > 0)
                            file += "/";
                        res.add(file);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return res.toArray(new String[res.size()]);
    }

    // screen methods
    public static native void setDisplayMetrics(int screenWidthPixels,
                                                int screenHeightPixels,
                                                int availableLeftPixels,
                                                int availableTopPixels,
                                                int availableWidthPixels,
                                                int availableHeightPixels,
                                                double XDpi,
                                                double YDpi,
                                                double scaledDensity,
                                                double density);
    public static native void handleOrientationChanged(int newRotation, int nativeOrientation);
    // screen methods

    // pointer methods
    public static native void mouseDown(int winId, int x, int y);
    public static native void mouseUp(int winId, int x, int y);
    public static native void mouseMove(int winId, int x, int y);
    public static native void mouseWheel(int winId, int x, int y, float hdelta, float vdelta);
    public static native void touchBegin(int winId);
    public static native void touchAdd(int winId, int pointerId, int action, boolean primary, int x, int y, float major, float minor, float rotation, float pressure);
    public static native void touchEnd(int winId, int action);
    public static native void longPress(int winId, int x, int y);
    // pointer methods

    // tablet methods
    public static native boolean isTabletEventSupported();
    public static native void tabletEvent(int winId, int deviceId, long time, int action, int pointerType, int buttonState, float x, float y, float pressure);
    // tablet methods

    // keyboard methods
    public static native void keyDown(int key, int unicode, int modifier, boolean autoRepeat);
    public static native void keyUp(int key, int unicode, int modifier, boolean autoRepeat);
    public static native void keyboardVisibilityChanged(boolean visibility);
    public static native void keyboardGeometryChanged(int x, int y, int width, int height);
    // keyboard methods

    // handle methods
    public static final int IdCursorHandle = 1;
    public static final int IdLeftHandle = 2;
    public static final int IdRightHandle = 3;
    public static native void handleLocationChanged(int id, int x, int y);
    // handle methods

    // dispatch events methods
    public static native boolean dispatchGenericMotionEvent(MotionEvent ev);
    public static native boolean dispatchKeyEvent(KeyEvent event);
    // dispatch events methods

    // surface methods
    public static native void setSurface(int id, Object surface, int w, int h);
    // surface methods

    // window methods
    public static native void updateWindow();
    // window methods

    // application methods
    public static native void updateApplicationState(int state);

    // menu methods
    public static native boolean onPrepareOptionsMenu(Menu menu);
    public static native boolean onOptionsItemSelected(int itemId, boolean checked);
    public static native void onOptionsMenuClosed(Menu menu);

    public static native void onCreateContextMenu(ContextMenu menu);
    public static native void fillContextMenu(Menu menu);
    public static native boolean onContextItemSelected(int itemId, boolean checked);
    public static native void onContextMenuClosed(Menu menu);
    // menu methods

    // clipboard methods
    public static native void onClipboardDataChanged();
    // clipboard methods

    // activity methods
    public static native void onActivityResult(int requestCode, int resultCode, Intent data);
    public static native void onNewIntent(Intent data);

    public static native void runPendingCppRunnables();

    public static native void sendRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults);

    private static native void setNativeActivity(Activity activity);
    private static native void setNativeService(Service service);
    // activity methods

    // service methods
    public static native IBinder onBind(Intent intent);
    // service methods
}
