/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <android/api-level.h>
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>
#include <android/log.h>
#include <android/looper.h>
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <game-activity/GameActivity.h>
#include <game-activity/GameActivityLog.h>
#include <jni.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/system_properties.h>
#include <sys/types.h>
#include <system_utils.h>
#include <unistd.h>

#include <memory>
#include <mutex>
#include <string>
#include <vector>

#include "GameActivityEvents_internal.h"

namespace {

static std::atomic<bool> g_methodsRegistered(false);

struct OwnedGameTextInputState {
    OwnedGameTextInputState& operator=(const GameTextInputState& rhs) {
        inner = rhs;
        owned_string = std::string(rhs.text_UTF8, rhs.text_length);
        inner.text_UTF8 = owned_string.data();
        return *this;
    }
    GameTextInputState inner;
    std::string owned_string;
};

} // anonymous namespace

#ifndef NELEM
#define NELEM(x) ((int)(sizeof(x) / sizeof((x)[0])))
#endif

/*
 * JNI methods of the GameActivity Java class.
 */
static struct {
    jmethodID finish;
    jmethodID setWindowFlags;
    jmethodID getWindowInsets;
    jmethodID getWaterfallInsets;
    jmethodID setImeEditorInfoFields;
} gGameActivityClassInfo;

/*
 * JNI methods of the android.os.LocaleList class.
 */
static struct {
    jmethodID size;
    jmethodID get;
} gLocaleListClassInfo;

/*
 * JNI methods of the java.util.Locale class.
 */
static struct {
    jmethodID getLanguage;
    jmethodID getScript;
    jmethodID getCountry;
    jmethodID getVariant;
} gLocaleClassInfo;

/*
 * JNI fields of the androidx.core.graphics.Insets Java class.
 */
static struct {
    jfieldID left;
    jfieldID right;
    jfieldID top;
    jfieldID bottom;
} gInsetsClassInfo;

/*
 * JNI fields of the Configuration Java class.
 */
static struct ConfigurationClassInfo {
    jfieldID colorMode;
    jfieldID densityDpi;
    jfieldID fontScale;
    jfieldID fontWeightAdjustment;
    jfieldID hardKeyboardHidden;
    jfieldID keyboard;
    jfieldID keyboardHidden;
    jfieldID mcc;
    jfieldID mnc;
    jfieldID navigation;
    jfieldID navigationHidden;
    jfieldID orientation;
    jfieldID screenHeightDp;
    jfieldID screenLayout;
    jfieldID screenWidthDp;
    jfieldID smallestScreenWidthDp;
    jfieldID touchscreen;
    jfieldID uiMode;

    jmethodID getLocales;
} gConfigurationClassInfo;

/*
 * JNI methods of the WindowInsetsCompat.Type Java class.
 */
static struct {
    jmethodID methods[GAMECOMMON_INSETS_TYPE_COUNT];
    jclass clazz;
} gWindowInsetsCompatTypeClassInfo;

/*
 * Contains a command to be executed by the GameActivity
 * on the application main thread.
 */
struct ActivityWork {
    int32_t cmd;
    int64_t arg1;
    int64_t arg2;
    int64_t arg3;
};

/*
 * The type of commands that can be passed to the GameActivity and that
 * are executed on the application main thread.
 */
enum {
    CMD_FINISH = 1,
    CMD_SET_WINDOW_FORMAT,
    CMD_SET_WINDOW_FLAGS,
    CMD_SHOW_SOFT_INPUT,
    CMD_HIDE_SOFT_INPUT,
    CMD_SET_SOFT_INPUT_STATE,
    CMD_SET_IME_EDITOR_INFO,
    CMD_RESTART_INPUT
};

/*
 * A class for locale information, matching Android Java Locale class.
 */
struct Locale {
    std::string language;
    std::string script;
    std::string country;
    std::string variant;
};

/*
 * Last known Configuration values. They may be accessed from the different
 * thread, this is why they are made atomic.
 */
static struct Configuration {
    int colorMode;
    int densityDpi;
    float fontScale;
    int fontWeightAdjustment;
    int hardKeyboardHidden;
    int keyboard;
    int keyboardHidden;
    int mcc;
    int mnc;
    int navigation;
    int navigationHidden;
    int orientation;
    int screenHeightDp;
    int screenLayout;
    int screenWidthDp;
    int smallestScreenWidthDp;
    int touchscreen;
    int uiMode;
    std::vector<Locale> locales;
} gConfiguration;

static std::mutex gConfigMutex;

/*
 * Write a command to be executed by the GameActivity on the application main
 * thread.
 */
static void write_work(int fd, int32_t cmd, int64_t arg1 = 0, int64_t arg2 = 0, int64_t arg3 = 0) {
    ActivityWork work;
    work.cmd = cmd;
    work.arg1 = arg1;
    work.arg2 = arg2;
    work.arg3 = arg3;

    LOG_TRACE("write_work: cmd=%d", cmd);
restart:
    int res = write(fd, &work, sizeof(work));
    if (res < 0 && errno == EINTR) {
        goto restart;
    }

    if (res == sizeof(work)) return;

    if (res < 0) {
        ALOGW("Failed writing to work fd: %s", strerror(errno));
    } else {
        ALOGW("Truncated writing to work fd: %d", res);
    }
}

/*
 * Read commands to be executed by the GameActivity on the application main
 * thread.
 */
static bool read_work(int fd, ActivityWork* outWork) {
    int res = read(fd, outWork, sizeof(ActivityWork));
    // no need to worry about EINTR, poll loop will just come back again.
    if (res == sizeof(ActivityWork)) return true;

    if (res < 0) {
        ALOGW("Failed reading work fd: %s", strerror(errno));
    } else {
        ALOGW("Truncated reading work fd: %d", res);
    }
    return false;
}

/*
 * Native state for interacting with the GameActivity class.
 */
struct NativeCode : public GameActivity {
    NativeCode() {
        memset((GameActivity*)this, 0, sizeof(GameActivity));
        memset(&callbacks, 0, sizeof(callbacks));
        memset(&insetsState, 0, sizeof(insetsState));
        nativeWindow = NULL;
        mainWorkRead = mainWorkWrite = -1;
        gameTextInput = NULL;
        softwareKeyboardVisible = false;
        sdkVersion = gamesdk::GetSystemPropAsInt("ro.build.version.sdk");
        ALOGD("SDK version: %d", sdkVersion);
    }

    ~NativeCode() {
        if (callbacks.onDestroy != NULL) {
            callbacks.onDestroy(this);
        }
        if (env != NULL) {
            if (javaGameActivity != NULL) {
                env->DeleteGlobalRef(javaGameActivity);
            }
            if (javaAssetManager != NULL) {
                env->DeleteGlobalRef(javaAssetManager);
            }
        }
        GameTextInput_destroy(gameTextInput);
        if (looper != NULL && mainWorkRead >= 0) {
            ALooper_removeFd(looper, mainWorkRead);
        }
        ALooper_release(looper);
        looper = NULL;

        setSurface(NULL);
        if (mainWorkRead >= 0) close(mainWorkRead);
        if (mainWorkWrite >= 0) close(mainWorkWrite);
    }

    void setSurface(jobject _surface) {
        if (nativeWindow != NULL) {
            ANativeWindow_release(nativeWindow);
        }
        if (_surface != NULL) {
            nativeWindow = ANativeWindow_fromSurface(env, _surface);
        } else {
            nativeWindow = NULL;
        }
    }

    GameActivityCallbacks callbacks;

    std::string internalDataPathObj;
    std::string externalDataPathObj;
    std::string obbPathObj;

    ANativeWindow* nativeWindow;
    int32_t lastWindowWidth;
    int32_t lastWindowHeight;

    // These are used to wake up the main thread to process work.
    int mainWorkRead;
    int mainWorkWrite;
    ALooper* looper;

    // Need to hold on to a reference here in case the upper layers destroy our
    // AssetManager.
    jobject javaAssetManager;

    GameTextInput* gameTextInput;
    // Set by users in GameActivity_setTextInputState, then passed to
    // GameTextInput.
    OwnedGameTextInputState gameTextInputState;
    std::mutex gameTextInputStateMutex;

    ARect insetsState[GAMECOMMON_INSETS_TYPE_COUNT];
    bool softwareKeyboardVisible;
};

static void readConfigurationValues(NativeCode* code, jobject javaConfig);

extern "C" void GameActivity_finish(GameActivity* activity) {
    NativeCode* code = static_cast<NativeCode*>(activity);
    write_work(code->mainWorkWrite, CMD_FINISH, 0);
}

extern "C" void GameActivity_setWindowFlags(GameActivity* activity, uint32_t values,
                                            uint32_t mask) {
    NativeCode* code = static_cast<NativeCode*>(activity);
    write_work(code->mainWorkWrite, CMD_SET_WINDOW_FLAGS, values, mask);
}

extern "C" void GameActivity_showSoftInput(GameActivity* activity, uint32_t flags) {
    NativeCode* code = static_cast<NativeCode*>(activity);
    write_work(code->mainWorkWrite, CMD_SHOW_SOFT_INPUT, flags);
}

extern "C" void GameActivity_restartInput(GameActivity* activity) {
    NativeCode* code = static_cast<NativeCode*>(activity);
    write_work(code->mainWorkWrite, CMD_RESTART_INPUT);
}

extern "C" void GameActivity_setTextInputState(GameActivity* activity,
                                               const GameTextInputState* state) {
    NativeCode* code = static_cast<NativeCode*>(activity);
    std::lock_guard<std::mutex> lock(code->gameTextInputStateMutex);
    code->gameTextInputState = *state;
    write_work(code->mainWorkWrite, CMD_SET_SOFT_INPUT_STATE);
}

extern "C" void GameActivity_getTextInputState(GameActivity* activity,
                                               GameTextInputGetStateCallback callback,
                                               void* context) {
    NativeCode* code = static_cast<NativeCode*>(activity);
    return GameTextInput_getState(code->gameTextInput, callback, context);
}

extern "C" void GameActivity_hideSoftInput(GameActivity* activity, uint32_t flags) {
    NativeCode* code = static_cast<NativeCode*>(activity);
    write_work(code->mainWorkWrite, CMD_HIDE_SOFT_INPUT, flags);
}

extern "C" void GameActivity_getWindowInsets(GameActivity* activity, GameCommonInsetsType type,
                                             ARect* insets) {
    if (type < 0 || type >= GAMECOMMON_INSETS_TYPE_COUNT) return;
    NativeCode* code = static_cast<NativeCode*>(activity);
    *insets = code->insetsState[type];
}

extern "C" bool GameActivity_isSoftwareKeyboardVisible(GameActivity* activity) {
    NativeCode* code = static_cast<NativeCode*>(activity);
    return code->softwareKeyboardVisible;
}

extern "C" GameTextInput* GameActivity_getTextInput(const GameActivity* activity) {
    const NativeCode* code = static_cast<const NativeCode*>(activity);
    return code->gameTextInput;
}

/*
 * Log the JNI exception, if any.
 */
static void checkAndClearException(JNIEnv* env, const char* methodName) {
    if (env->ExceptionCheck()) {
        ALOGE("Exception while running %s", methodName);
        env->ExceptionDescribe();
        env->ExceptionClear();
    }
}

/*
 * Callback for handling native events on the application's main thread.
 */
static int mainWorkCallback(int fd, int events, void* data) {
    ALOGD("************** mainWorkCallback *********");
    NativeCode* code = (NativeCode*)data;
    if ((events & POLLIN) == 0) {
        return 1;
    }

    ActivityWork work;
    if (!read_work(code->mainWorkRead, &work)) {
        return 1;
    }
    LOG_TRACE("mainWorkCallback: cmd=%d", work.cmd);
    switch (work.cmd) {
        case CMD_FINISH: {
            code->env->CallVoidMethod(code->javaGameActivity, gGameActivityClassInfo.finish);
            checkAndClearException(code->env, "finish");
        } break;
        case CMD_SET_WINDOW_FLAGS: {
            code->env->CallVoidMethod(code->javaGameActivity, gGameActivityClassInfo.setWindowFlags,
                                      static_cast<jint>(work.arg1), static_cast<jint>(work.arg2));
            checkAndClearException(code->env, "setWindowFlags");
        } break;
        case CMD_SHOW_SOFT_INPUT: {
            GameTextInput_showIme(code->gameTextInput, work.arg1);
        } break;
        case CMD_SET_SOFT_INPUT_STATE: {
            std::lock_guard<std::mutex> lock(code->gameTextInputStateMutex);
            GameTextInput_setState(code->gameTextInput, &code->gameTextInputState.inner);
            checkAndClearException(code->env, "setTextInputState");
        } break;
        case CMD_HIDE_SOFT_INPUT: {
            GameTextInput_hideIme(code->gameTextInput, work.arg1);
        } break;
        case CMD_SET_IME_EDITOR_INFO: {
            code->env->CallVoidMethod(code->javaGameActivity,
                                      gGameActivityClassInfo.setImeEditorInfoFields,
                                      static_cast<jint>(work.arg1), static_cast<jint>(work.arg2),
                                      static_cast<jint>(work.arg3));
            checkAndClearException(code->env, "setImeEditorInfo");
        } break;
        case CMD_RESTART_INPUT: {
            GameTextInput_restartInput(code->gameTextInput);
        } break;
        default:
            ALOGW("Unknown work command: %d", work.cmd);
            break;
    }

    return 1;
}

// ------------------------------------------------------------------------
static thread_local std::string g_error_msg;

static jlong initializeNativeCode_native(JNIEnv* env, jobject javaGameActivity,
                                         jstring internalDataDir, jstring obbDir,
                                         jstring externalDataDir, jobject jAssetMgr,
                                         jbyteArray savedState, jobject javaConfig) {
    LOG_TRACE("initializeNativeCode_native");
    NativeCode* code = NULL;

    code = new NativeCode();

    code->looper = ALooper_forThread();
    if (code->looper == nullptr) {
        g_error_msg = "Unable to retrieve native ALooper";
        ALOGW("%s", g_error_msg.c_str());
        delete code;
        return 0;
    }
    ALooper_acquire(code->looper);

    int msgpipe[2];
    if (pipe(msgpipe)) {
        g_error_msg = "could not create pipe: ";
        g_error_msg += strerror(errno);

        ALOGW("%s", g_error_msg.c_str());
        delete code;
        return 0;
    }
    code->mainWorkRead = msgpipe[0];
    code->mainWorkWrite = msgpipe[1];
    int result = fcntl(code->mainWorkRead, F_SETFL, O_NONBLOCK);
    SLOGW_IF(result != 0,
             "Could not make main work read pipe "
             "non-blocking: %s",
             strerror(errno));
    result = fcntl(code->mainWorkWrite, F_SETFL, O_NONBLOCK);
    SLOGW_IF(result != 0,
             "Could not make main work write pipe "
             "non-blocking: %s",
             strerror(errno));
    ALooper_addFd(code->looper, code->mainWorkRead, 0, ALOOPER_EVENT_INPUT, mainWorkCallback, code);

    code->GameActivity::callbacks = &code->callbacks;
    if (env->GetJavaVM(&code->vm) < 0) {
        ALOGW("GameActivity GetJavaVM failed");
        delete code;
        return 0;
    }
    code->env = env;
    code->javaGameActivity = env->NewGlobalRef(javaGameActivity);

    const char* dirStr = internalDataDir ? env->GetStringUTFChars(internalDataDir, NULL) : "";
    code->internalDataPathObj = dirStr;
    code->internalDataPath = code->internalDataPathObj.c_str();
    if (internalDataDir) env->ReleaseStringUTFChars(internalDataDir, dirStr);

    dirStr = externalDataDir ? env->GetStringUTFChars(externalDataDir, NULL) : "";
    code->externalDataPathObj = dirStr;
    code->externalDataPath = code->externalDataPathObj.c_str();
    if (externalDataDir) env->ReleaseStringUTFChars(externalDataDir, dirStr);

    code->javaAssetManager = env->NewGlobalRef(jAssetMgr);
    code->assetManager = AAssetManager_fromJava(env, jAssetMgr);

    dirStr = obbDir ? env->GetStringUTFChars(obbDir, NULL) : "";
    code->obbPathObj = dirStr;
    code->obbPath = code->obbPathObj.c_str();
    if (obbDir) env->ReleaseStringUTFChars(obbDir, dirStr);

    jbyte* rawSavedState = NULL;
    jsize rawSavedSize = 0;
    if (savedState != NULL) {
        rawSavedState = env->GetByteArrayElements(savedState, NULL);
        rawSavedSize = env->GetArrayLength(savedState);
    }

    // read configuration for the first time
    readConfigurationValues(code, javaConfig);

    GameActivity_onCreate(code, rawSavedState, rawSavedSize);

    code->gameTextInput = GameTextInput_init(env, 0);
    GameTextInput_setEventCallback(code->gameTextInput,
                                   reinterpret_cast<GameTextInputEventCallback>(
                                           code->callbacks.onTextInputEvent),
                                   code);

    if (rawSavedState != NULL) {
        env->ReleaseByteArrayElements(savedState, rawSavedState, 0);
    }

    return reinterpret_cast<jlong>(code);
}

static jstring getDlError_native(JNIEnv* env, jobject javaGameActivity) {
    jstring result = env->NewStringUTF(g_error_msg.c_str());
    g_error_msg.clear();
    return result;
}

static void terminateNativeCode_native(JNIEnv* env, jobject javaGameActivity, jlong handle) {
    LOG_TRACE("terminateNativeCode_native");
    if (handle != 0) {
        NativeCode* code = (NativeCode*)handle;
        delete code;
    }
}

static void onStart_native(JNIEnv* env, jobject javaGameActivity, jlong handle) {
    ALOGV("onStart_native");
    if (handle != 0) {
        NativeCode* code = (NativeCode*)handle;
        if (code->callbacks.onStart != NULL) {
            code->callbacks.onStart(code);
        }
    }
}

static void onResume_native(JNIEnv* env, jobject javaGameActivity, jlong handle) {
    LOG_TRACE("onResume_native");
    if (handle != 0) {
        NativeCode* code = (NativeCode*)handle;
        if (code->callbacks.onResume != NULL) {
            code->callbacks.onResume(code);
        }
    }
}

struct SaveInstanceLocals {
    JNIEnv* env;
    jbyteArray array;
};

static jbyteArray onSaveInstanceState_native(JNIEnv* env, jobject javaGameActivity, jlong handle) {
    LOG_TRACE("onSaveInstanceState_native");

    SaveInstanceLocals locals{env, NULL}; // Passed through the user's state prep function.

    if (handle != 0) {
        NativeCode* code = (NativeCode*)handle;
        if (code->callbacks.onSaveInstanceState != NULL) {
            code->callbacks.onSaveInstanceState(
                    code,
                    [](const char* bytes, int len, void* context) {
                        auto locals = static_cast<SaveInstanceLocals*>(context);
                        if (len > 0) {
                            locals->array = locals->env->NewByteArray(len);
                            if (locals->array != NULL) {
                                locals->env->SetByteArrayRegion(locals->array, 0, len,
                                                                (const jbyte*)bytes);
                            }
                        }
                    },
                    &locals);
        }
    }
    return locals.array;
}

static void onPause_native(JNIEnv* env, jobject javaGameActivity, jlong handle) {
    LOG_TRACE("onPause_native");
    if (handle != 0) {
        NativeCode* code = (NativeCode*)handle;
        if (code->callbacks.onPause != NULL) {
            code->callbacks.onPause(code);
        }
    }
}

static void onStop_native(JNIEnv* env, jobject javaGameActivity, jlong handle) {
    LOG_TRACE("onStop_native");
    if (handle != 0) {
        NativeCode* code = (NativeCode*)handle;
        if (code->callbacks.onStop != NULL) {
            code->callbacks.onStop(code);
        }
    }
}

static std::string getStringField(JNIEnv* env, jobject obj, jmethodID method) {
    jstring str = (jstring)env->CallObjectMethod(obj, method);
    const char* chars = env->GetStringUTFChars(str, NULL);
    std::string res(chars);
    env->ReleaseStringUTFChars(str, chars);
    checkAndClearException(env, "getStringField");
    return res;
}

static void readConfigurationValues(NativeCode* code, jobject javaConfig) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);

    if (gConfigurationClassInfo.colorMode != NULL) {
        gConfiguration.colorMode =
                code->env->GetIntField(javaConfig, gConfigurationClassInfo.colorMode);
    }

    gConfiguration.densityDpi =
            code->env->GetIntField(javaConfig, gConfigurationClassInfo.densityDpi);
    gConfiguration.fontScale =
            code->env->GetFloatField(javaConfig, gConfigurationClassInfo.fontScale);

    if (gConfigurationClassInfo.fontWeightAdjustment != NULL) {
        gConfiguration.fontWeightAdjustment =
                code->env->GetIntField(javaConfig, gConfigurationClassInfo.fontWeightAdjustment);
    }

    gConfiguration.hardKeyboardHidden =
            code->env->GetIntField(javaConfig, gConfigurationClassInfo.hardKeyboardHidden);
    gConfiguration.keyboard = code->env->GetIntField(javaConfig, gConfigurationClassInfo.keyboard);
    gConfiguration.keyboardHidden =
            code->env->GetIntField(javaConfig, gConfigurationClassInfo.keyboardHidden);
    gConfiguration.mcc = code->env->GetIntField(javaConfig, gConfigurationClassInfo.mcc);
    gConfiguration.mnc = code->env->GetIntField(javaConfig, gConfigurationClassInfo.mnc);
    gConfiguration.navigation =
            code->env->GetIntField(javaConfig, gConfigurationClassInfo.navigation);
    gConfiguration.navigationHidden =
            code->env->GetIntField(javaConfig, gConfigurationClassInfo.navigationHidden);
    gConfiguration.orientation =
            code->env->GetIntField(javaConfig, gConfigurationClassInfo.orientation);
    gConfiguration.screenHeightDp =
            code->env->GetIntField(javaConfig, gConfigurationClassInfo.screenHeightDp);
    gConfiguration.screenLayout =
            code->env->GetIntField(javaConfig, gConfigurationClassInfo.screenLayout);
    gConfiguration.screenWidthDp =
            code->env->GetIntField(javaConfig, gConfigurationClassInfo.screenWidthDp);
    gConfiguration.smallestScreenWidthDp =
            code->env->GetIntField(javaConfig, gConfigurationClassInfo.smallestScreenWidthDp);
    gConfiguration.touchscreen =
            code->env->GetIntField(javaConfig, gConfigurationClassInfo.touchscreen);
    gConfiguration.uiMode = code->env->GetIntField(javaConfig, gConfigurationClassInfo.uiMode);

    checkAndClearException(code->env, "Configuration.get");

    jobject locales = code->env->CallObjectMethod(javaConfig, gConfigurationClassInfo.getLocales);
    checkAndClearException(code->env, "getLocales");

    int localesCount = code->env->CallIntMethod(locales, gLocaleListClassInfo.size);
    checkAndClearException(code->env, "size");
    gConfiguration.locales.resize(localesCount);

    // extract the data for every locale
    for (int i = 0; i < localesCount; ++i) {
        Locale& locale = gConfiguration.locales[i];

        // get locale object from the array
        jobject jniLocale = code->env->CallObjectMethod(locales, gLocaleListClassInfo.get, i);
        checkAndClearException(code->env, "GetObjectArrayElement");

        // get data strings for this locale
        locale.language = getStringField(code->env, jniLocale, gLocaleClassInfo.getLanguage);
        locale.script = getStringField(code->env, jniLocale, gLocaleClassInfo.getScript);
        locale.country = getStringField(code->env, jniLocale, gLocaleClassInfo.getCountry);
        locale.variant = getStringField(code->env, jniLocale, gLocaleClassInfo.getVariant);
    }
}

static void onConfigurationChanged_native(JNIEnv* env, jobject javaGameActivity, jlong handle,
                                          jobject javaNewConfig) {
    LOG_TRACE("onConfigurationChanged_native");
    if (handle != 0) {
        NativeCode* code = (NativeCode*)handle;
        readConfigurationValues(code, javaNewConfig);

        if (code->callbacks.onConfigurationChanged != NULL) {
            code->callbacks.onConfigurationChanged(code);
        }
    }
}

static void onTrimMemory_native(JNIEnv* env, jobject javaGameActivity, jlong handle, jint level) {
    LOG_TRACE("onTrimMemory_native");
    if (handle != 0) {
        NativeCode* code = (NativeCode*)handle;
        if (code->callbacks.onTrimMemory != NULL) {
            code->callbacks.onTrimMemory(code, level);
        }
    }
}

static void onWindowFocusChanged_native(JNIEnv* env, jobject javaGameActivity, jlong handle,
                                        jboolean focused) {
    LOG_TRACE("onWindowFocusChanged_native");
    if (handle != 0) {
        NativeCode* code = (NativeCode*)handle;
        if (code->callbacks.onWindowFocusChanged != NULL) {
            code->callbacks.onWindowFocusChanged(code, focused ? 1 : 0);
        }
    }
}

static void onSurfaceCreated_native(JNIEnv* env, jobject javaGameActivity, jlong handle,
                                    jobject surface) {
    ALOGV("onSurfaceCreated_native");
    LOG_TRACE("onSurfaceCreated_native");
    if (handle != 0) {
        NativeCode* code = (NativeCode*)handle;
        code->setSurface(surface);

        if (code->nativeWindow != NULL && code->callbacks.onNativeWindowCreated != NULL) {
            code->callbacks.onNativeWindowCreated(code, code->nativeWindow);
        }
    }
}

static void onSurfaceChanged_native(JNIEnv* env, jobject javaGameActivity, jlong handle,
                                    jobject surface, jint format, jint width, jint height) {
    LOG_TRACE("onSurfaceChanged_native");
    if (handle != 0) {
        NativeCode* code = (NativeCode*)handle;
        ANativeWindow* oldNativeWindow = code->nativeWindow;
        // Fix for window being destroyed behind the scenes on older Android
        // versions.
        if (oldNativeWindow != NULL) {
            ANativeWindow_acquire(oldNativeWindow);
        }
        code->setSurface(surface);
        if (oldNativeWindow != code->nativeWindow) {
            if (oldNativeWindow != NULL && code->callbacks.onNativeWindowDestroyed != NULL) {
                code->callbacks.onNativeWindowDestroyed(code, oldNativeWindow);
            }
            if (code->nativeWindow != NULL) {
                if (code->callbacks.onNativeWindowCreated != NULL) {
                    code->callbacks.onNativeWindowCreated(code, code->nativeWindow);
                }

                code->lastWindowWidth = ANativeWindow_getWidth(code->nativeWindow);
                code->lastWindowHeight = ANativeWindow_getHeight(code->nativeWindow);
            }
        } else {
            // Maybe it was resized?
            int32_t newWidth = ANativeWindow_getWidth(code->nativeWindow);
            int32_t newHeight = ANativeWindow_getHeight(code->nativeWindow);

            if (newWidth != code->lastWindowWidth || newHeight != code->lastWindowHeight) {
                code->lastWindowWidth = newWidth;
                code->lastWindowHeight = newHeight;

                if (code->callbacks.onNativeWindowResized != NULL) {
                    code->callbacks.onNativeWindowResized(code, code->nativeWindow, newWidth,
                                                          newHeight);
                }
            }
        }
        // Release the window we acquired earlier.
        if (oldNativeWindow != NULL) {
            ANativeWindow_release(oldNativeWindow);
        }
    }
}

static void onSurfaceRedrawNeeded_native(JNIEnv* env, jobject javaGameActivity, jlong handle) {
    LOG_TRACE("onSurfaceRedrawNeeded_native");
    if (handle != 0) {
        NativeCode* code = (NativeCode*)handle;
        if (code->nativeWindow != NULL && code->callbacks.onNativeWindowRedrawNeeded != NULL) {
            code->callbacks.onNativeWindowRedrawNeeded(code, code->nativeWindow);
        }
    }
}

static void onSurfaceDestroyed_native(JNIEnv* env, jobject javaGameActivity, jlong handle) {
    LOG_TRACE("onSurfaceDestroyed_native");
    if (handle != 0) {
        NativeCode* code = (NativeCode*)handle;
        if (code->nativeWindow != NULL && code->callbacks.onNativeWindowDestroyed != NULL) {
            code->callbacks.onNativeWindowDestroyed(code, code->nativeWindow);
        }
        code->setSurface(NULL);
    }
}

extern "C" void GameActivity_setImeEditorInfo(GameActivity* activity, GameTextInputType inputType,
                                              GameTextInputActionType actionId,
                                              GameTextInputImeOptions imeOptions) {
    NativeCode* code = static_cast<NativeCode*>(activity);
    write_work(code->mainWorkWrite, CMD_SET_IME_EDITOR_INFO, inputType, actionId, imeOptions);
}

extern "C" int GameActivity_getColorMode(GameActivity*) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.colorMode;
}

extern "C" int GameActivity_getDensityDpi(GameActivity*) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.densityDpi;
}

extern "C" float GameActivity_getFontScale(GameActivity*) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.fontScale;
}

extern "C" int GameActivity_getFontWeightAdjustment(GameActivity*) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.fontWeightAdjustment;
}

extern "C" int GameActivity_getHardKeyboardHidden(GameActivity*) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.hardKeyboardHidden;
}

extern "C" int GameActivity_getKeyboard(GameActivity*) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.keyboard;
}

extern "C" int GameActivity_getKeyboardHidden(GameActivity*) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.keyboardHidden;
}

extern "C" int GameActivity_getLocalesCount(GameActivity* activity) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.locales.size();
}

extern "C" int GameActivity_getMcc(GameActivity*) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.mcc;
}

extern "C" int GameActivity_getMnc(GameActivity*) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.mnc;
}

extern "C" int GameActivity_getNavigation(GameActivity*) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.navigation;
}

extern "C" int GameActivity_getNavigationHidden(GameActivity*) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.navigationHidden;
}

extern "C" int GameActivity_getOrientation(GameActivity*) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.orientation;
}

extern "C" int GameActivity_getScreenHeightDp(GameActivity*) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.screenHeightDp;
}

extern "C" int GameActivity_getScreenLayout(GameActivity*) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.screenLayout;
}

extern "C" int GameActivity_getScreenWidthDp(GameActivity*) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.screenWidthDp;
}

extern "C" int GameActivity_getSmallestScreenWidthDp(GameActivity*) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.smallestScreenWidthDp;
}

extern "C" int GameActivity_getTouchscreen(GameActivity*) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.touchscreen;
}

extern "C" int GameActivity_getUIMode(GameActivity*) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);
    return gConfiguration.uiMode;
}

static int copyStringIfFits(char* dst, size_t dst_size, const std::string& s) {
    if (dst_size <= s.size()) {
        return ENOBUFS;
    }

    strcpy(dst, s.c_str());
    return 0;
}

extern "C" int GameActivity_getLocaleLanguage(char* dst, size_t dst_size, GameActivity* activity,
                                              size_t localeIdx) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);

    if (localeIdx >= gConfiguration.locales.size()) {
        return EINVAL;
    }

    return copyStringIfFits(dst, dst_size, gConfiguration.locales[localeIdx].language);
}

extern "C" int GameActivity_getLocaleScript(char* dst, size_t dst_size, GameActivity* activity,
                                            size_t localeIdx) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);

    if (localeIdx >= gConfiguration.locales.size()) {
        return EINVAL;
    }

    return copyStringIfFits(dst, dst_size, gConfiguration.locales[localeIdx].script);
}

extern "C" int GameActivity_getLocaleCountry(char* dst, size_t dst_size, GameActivity* activity,
                                             size_t localeIdx) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);

    if (localeIdx >= gConfiguration.locales.size()) {
        return EINVAL;
    }

    return copyStringIfFits(dst, dst_size, gConfiguration.locales[localeIdx].country);
}

extern "C" int GameActivity_getLocaleVariant(char* dst, size_t dst_size, GameActivity* activity,
                                             size_t localeIdx) {
    const std::lock_guard<std::mutex> lock(gConfigMutex);

    if (localeIdx >= gConfiguration.locales.size()) {
        return EINVAL;
    }

    return copyStringIfFits(dst, dst_size, gConfiguration.locales[localeIdx].variant);
}

static bool onTouchEvent_native(JNIEnv* env, jobject javaGameActivity, jlong handle,
                                jobject motionEvent, int pointerCount, int historySize,
                                int deviceId, int source, int action, int64_t eventTime,
                                int64_t downTime, int flags, int metaState, int actionButton,
                                int buttonState, int classification, int edgeFlags,
                                float precisionX, float precisionY) {
    if (handle == 0) return false;
    NativeCode* code = (NativeCode*)handle;
    if (code->callbacks.onTouchEvent == nullptr) return false;

    static GameActivityMotionEvent c_event;

    c_event.deviceId = deviceId;
    c_event.source = source;
    c_event.action = action;

    c_event.eventTime = eventTime;
    c_event.downTime = downTime;

    c_event.flags = flags;
    c_event.metaState = metaState;

    c_event.actionButton = actionButton;
    c_event.buttonState = buttonState;
    c_event.classification = classification;
    c_event.edgeFlags = edgeFlags;

    c_event.precisionX = precisionX;
    c_event.precisionY = precisionY;

    GameActivityMotionEvent_fromJava(env, motionEvent, &c_event, pointerCount, historySize);
    return code->callbacks.onTouchEvent(code, &c_event);
}

static bool onKeyUp_native(JNIEnv* env, jobject javaGameActivity, jlong handle, jobject keyEvent) {
    if (handle == 0) return false;
    NativeCode* code = (NativeCode*)handle;
    if (code->callbacks.onKeyUp == nullptr) return false;

    static GameActivityKeyEvent c_event;
    GameActivityKeyEvent_fromJava(env, keyEvent, &c_event);
    return code->callbacks.onKeyUp(code, &c_event);
}

static bool onKeyDown_native(JNIEnv* env, jobject javaGameActivity, jlong handle,
                             jobject keyEvent) {
    if (handle == 0) return false;
    NativeCode* code = (NativeCode*)handle;
    if (code->callbacks.onKeyDown == nullptr) return false;

    static GameActivityKeyEvent c_event;
    GameActivityKeyEvent_fromJava(env, keyEvent, &c_event);
    return code->callbacks.onKeyDown(code, &c_event);
}

static void onTextInput_native(JNIEnv* env, jobject activity, jlong handle,
                               jobject textInputEvent) {
    if (handle == 0) return;
    NativeCode* code = (NativeCode*)handle;
    GameTextInput_processEvent(code->gameTextInput, textInputEvent);
}

static void onWindowInsetsChanged_native(JNIEnv* env, jobject activity, jlong handle) {
    if (handle == 0) return;
    NativeCode* code = (NativeCode*)handle;
    if (code->callbacks.onWindowInsetsChanged == nullptr) return;
    for (int type = 0; type < GAMECOMMON_INSETS_TYPE_COUNT; ++type) {
        jobject jinsets;
        // Note that waterfall insets are handled differently on the Java side.
        if (type == GAMECOMMON_INSETS_TYPE_WATERFALL) {
            jinsets = env->CallObjectMethod(code->javaGameActivity,
                                            gGameActivityClassInfo.getWaterfallInsets);
        } else {
            jint jtype = env->CallStaticIntMethod(gWindowInsetsCompatTypeClassInfo.clazz,
                                                  gWindowInsetsCompatTypeClassInfo.methods[type]);
            jinsets = env->CallObjectMethod(code->javaGameActivity,
                                            gGameActivityClassInfo.getWindowInsets, jtype);
        }
        ARect& insets = code->insetsState[type];
        if (jinsets == nullptr) {
            insets.left = 0;
            insets.right = 0;
            insets.top = 0;
            insets.bottom = 0;
        } else {
            insets.left = env->GetIntField(jinsets, gInsetsClassInfo.left);
            insets.right = env->GetIntField(jinsets, gInsetsClassInfo.right);
            insets.top = env->GetIntField(jinsets, gInsetsClassInfo.top);
            insets.bottom = env->GetIntField(jinsets, gInsetsClassInfo.bottom);
        }
    }
    GameTextInput_processImeInsets(code->gameTextInput,
                                   &code->insetsState[GAMECOMMON_INSETS_TYPE_IME]);
    code->callbacks.onWindowInsetsChanged(code);
}

static void setInputConnection_native(JNIEnv* env, jobject activity, jlong handle,
                                      jobject inputConnection) {
    NativeCode* code = (NativeCode*)handle;
    GameTextInput_setInputConnection(code->gameTextInput, inputConnection);
}

static void onContentRectChangedNative_native(JNIEnv* env, jobject activity, jlong handle, jint x,
                                              jint y, jint w, jint h) {
    if (handle != 0) {
        NativeCode* code = (NativeCode*)handle;

        if (code->callbacks.onContentRectChanged != nullptr) {
            ARect rect;
            rect.left = x;
            rect.top = y;
            rect.right = x + w;
            rect.bottom = y + h;
            code->callbacks.onContentRectChanged(code, &rect);
        }
    }
}

static void onSoftwareKeyboardVisibilityChangedNative_native(JNIEnv* env, jobject activity,
                                                             jlong handle, bool visible) {
    if (handle != 0) {
        NativeCode* code = (NativeCode*)handle;
        code->softwareKeyboardVisible = visible;

        if (code->callbacks.onSoftwareKeyboardVisibilityChanged != nullptr) {
            code->callbacks.onSoftwareKeyboardVisibilityChanged(code, visible);
        }
    }
}

static void onEditorActionNative_native(JNIEnv* env, jobject activity, jlong handle, int action) {
    if (handle != 0) {
        NativeCode* code = (NativeCode*)handle;

        if (code->callbacks.onEditorAction != nullptr) {
            code->callbacks.onEditorAction(code, action);
        }
    }
}

static const JNINativeMethod g_methods[] = {
        {"initializeNativeCode",
         "(Ljava/lang/String;Ljava/lang/String;"
         "Ljava/lang/String;Landroid/content/res/AssetManager;"
         "[BLandroid/content/res/Configuration;)J",
         (void*)initializeNativeCode_native},
        {"getDlError", "()Ljava/lang/String;", (void*)getDlError_native},
        {"terminateNativeCode", "(J)V", (void*)terminateNativeCode_native},
        {"onStartNative", "(J)V", (void*)onStart_native},
        {"onResumeNative", "(J)V", (void*)onResume_native},
        {"onSaveInstanceStateNative", "(J)[B", (void*)onSaveInstanceState_native},
        {"onPauseNative", "(J)V", (void*)onPause_native},
        {"onStopNative", "(J)V", (void*)onStop_native},
        {"onConfigurationChangedNative", "(JLandroid/content/res/Configuration;)V",
         (void*)onConfigurationChanged_native},
        {"onTrimMemoryNative", "(JI)V", (void*)onTrimMemory_native},
        {"onWindowFocusChangedNative", "(JZ)V", (void*)onWindowFocusChanged_native},
        {"onSurfaceCreatedNative", "(JLandroid/view/Surface;)V", (void*)onSurfaceCreated_native},
        {"onSurfaceChangedNative", "(JLandroid/view/Surface;III)V", (void*)onSurfaceChanged_native},
        {"onSurfaceRedrawNeededNative", "(JLandroid/view/Surface;)V",
         (void*)onSurfaceRedrawNeeded_native},
        {"onSurfaceDestroyedNative", "(J)V", (void*)onSurfaceDestroyed_native},
        {"onTouchEventNative", "(JLandroid/view/MotionEvent;IIIIIJJIIIIIIFF)Z",
         (void*)onTouchEvent_native},
        {"onKeyDownNative", "(JLandroid/view/KeyEvent;)Z", (void*)onKeyDown_native},
        {"onKeyUpNative", "(JLandroid/view/KeyEvent;)Z", (void*)onKeyUp_native},
        {"onTextInputEventNative", "(JLcom/google/androidgamesdk/gametextinput/State;)V",
         (void*)onTextInput_native},
        {"onWindowInsetsChangedNative", "(J)V", (void*)onWindowInsetsChanged_native},
        {"setInputConnectionNative",
         "(JLcom/google/androidgamesdk/gametextinput/InputConnection;)V",
         (void*)setInputConnection_native},
        {"onContentRectChangedNative", "(JIIII)V", (void*)onContentRectChangedNative_native},
        {"onSoftwareKeyboardVisibilityChangedNative", "(JZ)V",
         (void*)onSoftwareKeyboardVisibilityChangedNative_native},
        {"onEditorActionNative", "(JI)V", (void*)onEditorActionNative_native},
};

static const char* const kGameActivityPathName = "com/google/androidgamesdk/GameActivity";

static const char* const kInsetsPathName = "androidx/core/graphics/Insets";
static const char* const kConfigurationPathName = "android/content/res/Configuration";
static const char* const kWindowInsetsCompatTypePathName =
        "androidx/core/view/WindowInsetsCompat$Type";
static const char* const kLocaleListPathName = "android/os/LocaleList";
static const char* const kLocalePathName = "java/util/Locale";

#define FIND_CLASS(var, className)   \
    var = env->FindClass(className); \
    LOG_FATAL_IF(!var, "Unable to find class %s", className);

#define GET_METHOD_ID(var, clazz, methodName, fieldDescriptor)  \
    var = env->GetMethodID(clazz, methodName, fieldDescriptor); \
    LOG_FATAL_IF(!var, "Unable to find method %s", methodName);

#define GET_STATIC_METHOD_ID(var, clazz, methodName, fieldDescriptor) \
    var = env->GetStaticMethodID(clazz, methodName, fieldDescriptor); \
    LOG_FATAL_IF(!var, "Unable to find static method %s", methodName);

#define GET_FIELD_ID(var, clazz, fieldName, fieldDescriptor)  \
    var = env->GetFieldID(clazz, fieldName, fieldDescriptor); \
    LOG_FATAL_IF(!var, "Unable to find field %s", fieldName);

static int jniRegisterNativeMethods(JNIEnv* env, const char* className,
                                    const JNINativeMethod* methods, int numMethods) {
    ALOGV("Registering %s's %d native methods...", className, numMethods);
    jclass clazz = env->FindClass(className);
    LOG_FATAL_IF(clazz == nullptr, "Native registration unable to find class '%s'; aborting...",
                 className);
    int result = env->RegisterNatives(clazz, methods, numMethods);
    env->DeleteLocalRef(clazz);
    if (result == 0) {
        return 0;
    }

    // Failure to register natives is fatal. Try to report the corresponding
    // exception, otherwise abort with generic failure message.
    jthrowable thrown = env->ExceptionOccurred();
    if (thrown != NULL) {
        env->ExceptionDescribe();
        env->DeleteLocalRef(thrown);
    }
    LOG_FATAL("RegisterNatives failed for '%s'; aborting...", className);
}

extern "C" int GameActivity_register(JNIEnv* env) {
    if (g_methodsRegistered.exchange(true)) {
        // Already registered, do nothing.
        return JNI_OK;
    }

    ALOGD("GameActivity_register");
    jclass activity_class;
    FIND_CLASS(activity_class, kGameActivityPathName);
    GET_METHOD_ID(gGameActivityClassInfo.finish, activity_class, "finish", "()V");
    GET_METHOD_ID(gGameActivityClassInfo.setWindowFlags, activity_class, "setWindowFlags", "(II)V");
    GET_METHOD_ID(gGameActivityClassInfo.getWindowInsets, activity_class, "getWindowInsets",
                  "(I)Landroidx/core/graphics/Insets;");
    GET_METHOD_ID(gGameActivityClassInfo.getWaterfallInsets, activity_class, "getWaterfallInsets",
                  "()Landroidx/core/graphics/Insets;");
    GET_METHOD_ID(gGameActivityClassInfo.setImeEditorInfoFields, activity_class,
                  "setImeEditorInfoFields", "(III)V");

    jclass insets_class;
    FIND_CLASS(insets_class, kInsetsPathName);
    GET_FIELD_ID(gInsetsClassInfo.left, insets_class, "left", "I");
    GET_FIELD_ID(gInsetsClassInfo.right, insets_class, "right", "I");
    GET_FIELD_ID(gInsetsClassInfo.top, insets_class, "top", "I");
    GET_FIELD_ID(gInsetsClassInfo.bottom, insets_class, "bottom", "I");

    jclass configuration_class;
    FIND_CLASS(configuration_class, kConfigurationPathName);

    if (android_get_device_api_level() >= 26) {
        GET_FIELD_ID(gConfigurationClassInfo.colorMode, configuration_class, "colorMode", "I");
    }

    GET_FIELD_ID(gConfigurationClassInfo.densityDpi, configuration_class, "densityDpi", "I");
    GET_FIELD_ID(gConfigurationClassInfo.fontScale, configuration_class, "fontScale", "F");

    if (android_get_device_api_level() >= 31) {
        GET_FIELD_ID(gConfigurationClassInfo.fontWeightAdjustment, configuration_class,
                     "fontWeightAdjustment", "I");
    }

    GET_FIELD_ID(gConfigurationClassInfo.hardKeyboardHidden, configuration_class,
                 "hardKeyboardHidden", "I");
    GET_FIELD_ID(gConfigurationClassInfo.keyboard, configuration_class, "keyboard", "I");
    GET_FIELD_ID(gConfigurationClassInfo.keyboardHidden, configuration_class, "keyboardHidden",
                 "I");
    GET_FIELD_ID(gConfigurationClassInfo.mcc, configuration_class, "mcc", "I");
    GET_FIELD_ID(gConfigurationClassInfo.mnc, configuration_class, "mnc", "I");
    GET_FIELD_ID(gConfigurationClassInfo.navigation, configuration_class, "navigation", "I");
    GET_FIELD_ID(gConfigurationClassInfo.navigationHidden, configuration_class, "navigationHidden",
                 "I");
    GET_FIELD_ID(gConfigurationClassInfo.orientation, configuration_class, "orientation", "I");
    GET_FIELD_ID(gConfigurationClassInfo.screenHeightDp, configuration_class, "screenHeightDp",
                 "I");
    GET_FIELD_ID(gConfigurationClassInfo.screenLayout, configuration_class, "screenLayout", "I");
    GET_FIELD_ID(gConfigurationClassInfo.screenWidthDp, configuration_class, "screenWidthDp", "I");
    GET_FIELD_ID(gConfigurationClassInfo.smallestScreenWidthDp, configuration_class,
                 "smallestScreenWidthDp", "I");
    GET_FIELD_ID(gConfigurationClassInfo.touchscreen, configuration_class, "touchscreen", "I");
    GET_FIELD_ID(gConfigurationClassInfo.uiMode, configuration_class, "uiMode", "I");

    GET_METHOD_ID(gConfigurationClassInfo.getLocales, configuration_class, "getLocales",
                  "()Landroid/os/LocaleList;");

    jclass localeListClass;
    FIND_CLASS(localeListClass, kLocaleListPathName);
    GET_METHOD_ID(gLocaleListClassInfo.size, localeListClass, "size", "()I");
    GET_METHOD_ID(gLocaleListClassInfo.get, localeListClass, "get", "(I)Ljava/util/Locale;");

    jclass localeClass;
    FIND_CLASS(localeClass, kLocalePathName);
    GET_METHOD_ID(gLocaleClassInfo.getLanguage, localeClass, "getLanguage", "()Ljava/lang/String;");
    GET_METHOD_ID(gLocaleClassInfo.getScript, localeClass, "getScript", "()Ljava/lang/String;");
    GET_METHOD_ID(gLocaleClassInfo.getCountry, localeClass, "getCountry", "()Ljava/lang/String;");
    GET_METHOD_ID(gLocaleClassInfo.getVariant, localeClass, "getVariant", "()Ljava/lang/String;");

    jclass windowInsetsCompatType_class;
    FIND_CLASS(windowInsetsCompatType_class, kWindowInsetsCompatTypePathName);
    gWindowInsetsCompatTypeClassInfo.clazz =
            (jclass)env->NewGlobalRef(windowInsetsCompatType_class);
    // These names must match, in order, the GameCommonInsetsType enum fields
    // Note that waterfall is handled differently by the insets API, so we
    // exclude it here.
    const char* methodNames[GAMECOMMON_INSETS_TYPE_WATERFALL] =
            {"captionBar",     "displayCutout", "ime",        "mandatorySystemGestures",
             "navigationBars", "statusBars",    "systemBars", "systemGestures",
             "tappableElement"};
    for (int i = 0; i < GAMECOMMON_INSETS_TYPE_WATERFALL; ++i) {
        GET_STATIC_METHOD_ID(gWindowInsetsCompatTypeClassInfo.methods[i],
                             windowInsetsCompatType_class, methodNames[i], "()I");
    }

    GameActivityEventsInit(env);

    return jniRegisterNativeMethods(env, kGameActivityPathName, g_methods, NELEM(g_methods));
}

// XXX: This symbol is renamed with a _C suffix and then re-exported from
// Rust because Rust/Cargo don't give us a way to directly export symbols
// from C/C++ code: https://github.com/rust-lang/rfcs/issues/2771
//
extern "C" JNIEXPORT jlong JNICALL
Java_com_google_androidgamesdk_GameActivity_initializeNativeCode_C(
        JNIEnv* env, jobject javaGameActivity, jstring internalDataDir, jstring obbDir,
        jstring externalDataDir, jobject jAssetMgr, jbyteArray savedState, jobject javaConfig) {
    GameActivity_register(env);
    jlong nativeCode =
            initializeNativeCode_native(env, javaGameActivity, internalDataDir, obbDir,
                                        externalDataDir, jAssetMgr, savedState, javaConfig);
    return nativeCode;
}

extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) __attribute__((weak));

extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }
    int result = GameActivity_register(env);
    return (result == 0) ? JNI_VERSION_1_6 : -1;
}
