/* * 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. */ #define LOG_TAG "GameActivity" #include "GameActivity.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // TODO(b/187147166): these functions were extracted from the Game SDK // (gamesdk/src/common/system_utils.h). system_utils.h/cpp should be used // instead. namespace { #if __ANDROID_API__ >= 26 std::string getSystemPropViaCallback(const char *key, const char *default_value = "") { const prop_info *prop = __system_property_find(key); if (prop == nullptr) { return default_value; } std::string return_value; auto thunk = [](void *cookie, const char * /*name*/, const char *value, uint32_t /*serial*/) { if (value != nullptr) { std::string *r = static_cast(cookie); *r = value; } }; __system_property_read_callback(prop, thunk, &return_value); return return_value; } #else std::string getSystemPropViaGet(const char *key, const char *default_value = "") { char buffer[PROP_VALUE_MAX + 1] = ""; // +1 for terminator int bufferLen = __system_property_get(key, buffer); if (bufferLen > 0) return buffer; else return ""; } #endif std::string GetSystemProp(const char *key, const char *default_value = "") { #if __ANDROID_API__ >= 26 return getSystemPropViaCallback(key, default_value); #else return getSystemPropViaGet(key, default_value); #endif } int GetSystemPropAsInt(const char *key, int default_value = 0) { std::string prop = GetSystemProp(key); return prop == "" ? default_value : strtoll(prop.c_str(), nullptr, 10); } 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 #define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__); #define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__); #define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__); #ifdef NDEBUG #define ALOGV(...) #else #define ALOGV(...) \ __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__); #endif /* Returns 2nd arg. Used to substitute default value if caller's vararg list * is empty. */ #define __android_second(first, second, ...) second /* If passed multiple args, returns ',' followed by all but 1st arg, otherwise * returns nothing. */ #define __android_rest(first, ...) , ##__VA_ARGS__ #define android_printAssert(cond, tag, fmt...) \ __android_log_assert(cond, tag, \ __android_second(0, ##fmt, NULL) __android_rest(fmt)) #define CONDITION(cond) (__builtin_expect((cond) != 0, 0)) #ifndef LOG_ALWAYS_FATAL_IF #define LOG_ALWAYS_FATAL_IF(cond, ...) \ ((CONDITION(cond)) \ ? ((void)android_printAssert(#cond, LOG_TAG, ##__VA_ARGS__)) \ : (void)0) #endif #ifndef LOG_ALWAYS_FATAL #define LOG_ALWAYS_FATAL(...) \ (((void)android_printAssert(NULL, LOG_TAG, ##__VA_ARGS__))) #endif /* * Simplified macro to send a warning system log message using current LOG_TAG. */ #ifndef SLOGW #define SLOGW(...) \ ((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)) #endif #ifndef SLOGW_IF #define SLOGW_IF(cond, ...) \ ((__predict_false(cond)) \ ? ((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)) \ : (void)0) #endif /* * Versions of LOG_ALWAYS_FATAL_IF and LOG_ALWAYS_FATAL that * are stripped out of release builds. */ #if LOG_NDEBUG #ifndef LOG_FATAL_IF #define LOG_FATAL_IF(cond, ...) ((void)0) #endif #ifndef LOG_FATAL #define LOG_FATAL(...) ((void)0) #endif #else #ifndef LOG_FATAL_IF #define LOG_FATAL_IF(cond, ...) LOG_ALWAYS_FATAL_IF(cond, ##__VA_ARGS__) #endif #ifndef LOG_FATAL #define LOG_FATAL(...) LOG_ALWAYS_FATAL(__VA_ARGS__) #endif #endif /* * Assertion that generates a log message when the assertion fails. * Stripped out of release builds. Uses the current LOG_TAG. */ #ifndef ALOG_ASSERT #define ALOG_ASSERT(cond, ...) LOG_FATAL_IF(!(cond), ##__VA_ARGS__) #endif #define LOG_TRACE(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) #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 fields of the androidx.core.graphics.Insets Java class. */ static struct { jfieldID left; jfieldID right; jfieldID top; jfieldID bottom; } gInsetsClassInfo; /* * 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; }; /* * 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 }; /* * 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) { ActivityWork work; work.cmd = cmd; work.arg1 = arg1; work.arg2 = arg2; 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(void *_dlhandle, GameActivity_createFunc *_createFunc) { memset((GameActivity *)this, 0, sizeof(GameActivity)); memset(&callbacks, 0, sizeof(callbacks)); memset(&insetsState, 0, sizeof(insetsState)); dlhandle = _dlhandle; createActivityFunc = _createFunc; nativeWindow = NULL; mainWorkRead = mainWorkWrite = -1; gameTextInput = NULL; } ~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); if (dlhandle != NULL) { // for now don't unload... we probably should clean this // up and only keep one open dlhandle per proc, since there // is really no benefit to unloading the code. // dlclose(dlhandle); } } void setSurface(jobject _surface) { if (nativeWindow != NULL) { ANativeWindow_release(nativeWindow); } if (_surface != NULL) { nativeWindow = ANativeWindow_fromSurface(env, _surface); } else { nativeWindow = NULL; } } GameActivityCallbacks callbacks; void *dlhandle; GameActivity_createFunc *createActivityFunc; 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]; }; extern "C" void GameActivity_finish(GameActivity *activity) { NativeCode *code = static_cast(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(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(activity); write_work(code->mainWorkWrite, CMD_SHOW_SOFT_INPUT, flags); } extern "C" void GameActivity_setTextInputState( GameActivity *activity, const GameTextInputState *state) { NativeCode *code = static_cast(activity); std::lock_guard 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(activity); return GameTextInput_getState(code->gameTextInput, callback, context); } extern "C" void GameActivity_hideSoftInput(GameActivity *activity, uint32_t flags) { NativeCode *code = static_cast(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(activity); *insets = code->insetsState[type]; } extern "C" GameTextInput *GameActivity_getTextInput( const GameActivity *activity) { const NativeCode *code = static_cast(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, work.arg1, 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 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; default: ALOGW("Unknown work command: %d", work.cmd); break; } return 1; } // ------------------------------------------------------------------------ static thread_local std::string g_error_msg; static jlong loadNativeCode_native(JNIEnv *env, jobject javaGameActivity, jstring path, jstring funcName, jstring internalDataDir, jstring obbDir, jstring externalDataDir, jobject jAssetMgr, jbyteArray savedState) { LOG_TRACE("loadNativeCode_native"); const char *pathStr = env->GetStringUTFChars(path, NULL); NativeCode *code = NULL; void *handle = dlopen(pathStr, RTLD_LAZY); env->ReleaseStringUTFChars(path, pathStr); if (handle == nullptr) { g_error_msg = dlerror(); ALOGE("GameActivity dlopen(\"%s\") failed: %s", pathStr, g_error_msg.c_str()); return 0; } const char *funcStr = env->GetStringUTFChars(funcName, NULL); code = new NativeCode(handle, (GameActivity_createFunc *)dlsym(handle, funcStr)); env->ReleaseStringUTFChars(funcName, funcStr); if (code->createActivityFunc == nullptr) { g_error_msg = dlerror(); ALOGW("GameActivity_onCreate not found: %s", g_error_msg.c_str()); delete code; return 0; } 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); } code->createActivityFunc(code, rawSavedState, rawSavedSize); code->gameTextInput = GameTextInput_init(env, 0); GameTextInput_setEventCallback(code->gameTextInput, reinterpret_cast( code->callbacks.onTextInputEvent), code); if (rawSavedState != NULL) { env->ReleaseByteArrayElements(savedState, rawSavedState, 0); } return reinterpret_cast(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 unloadNativeCode_native(JNIEnv *env, jobject javaGameActivity, jlong handle) { LOG_TRACE("unloadNativeCode_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(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 void onConfigurationChanged_native(JNIEnv *env, jobject javaGameActivity, jlong handle) { LOG_TRACE("onConfigurationChanged_native"); if (handle != 0) { NativeCode *code = (NativeCode *)handle; 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) { 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); } } static bool enabledAxes[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT] = { /* AMOTION_EVENT_AXIS_X */ true, /* AMOTION_EVENT_AXIS_Y */ true, // Disable all other axes by default (they can be enabled using // `GameActivityPointerAxes_enableAxis`). false}; extern "C" void GameActivityPointerAxes_enableAxis(int32_t axis) { if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) { return; } enabledAxes[axis] = true; } extern "C" void GameActivityPointerAxes_disableAxis(int32_t axis) { if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) { return; } enabledAxes[axis] = false; } static bool enabledHistoricalAxes[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT] = { // Disable all axes by default (they can be enabled using // `GameActivityPointerAxes_enableHistoricalAxis`). false}; extern "C" void GameActivityHistoricalPointerAxes_enableAxis(int32_t axis) { if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) { return; } enabledHistoricalAxes[axis] = true; } extern "C" void GameActivityHistoricalPointerAxes_disableAxis(int32_t axis) { if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) { return; } enabledHistoricalAxes[axis] = false; } extern "C" void GameActivity_setImeEditorInfo(GameActivity *activity, int inputType, int actionId, int imeOptions) { JNIEnv *env; if (activity->vm->AttachCurrentThread(&env, NULL) == JNI_OK) { env->CallVoidMethod(activity->javaGameActivity, gGameActivityClassInfo.setImeEditorInfoFields, inputType, actionId, imeOptions); } } static struct { jmethodID getDeviceId; jmethodID getSource; jmethodID getAction; jmethodID getEventTime; jmethodID getDownTime; jmethodID getFlags; jmethodID getMetaState; jmethodID getActionButton; jmethodID getButtonState; jmethodID getClassification; jmethodID getEdgeFlags; jmethodID getPointerCount; jmethodID getPointerId; jmethodID getToolType; jmethodID getRawX; jmethodID getRawY; jmethodID getXPrecision; jmethodID getYPrecision; jmethodID getAxisValue; jmethodID getHistorySize; jmethodID getHistoricalEventTime; jmethodID getHistoricalAxisValue; } gMotionEventClassInfo; extern "C" int GameActivityMotionEvent_fromJava( JNIEnv *env, jobject motionEvent, GameActivityMotionEvent *out_event, GameActivityHistoricalPointerAxes *out_historical) { static bool gMotionEventClassInfoInitialized = false; if (!gMotionEventClassInfoInitialized) { int sdkVersion = GetSystemPropAsInt("ro.build.version.sdk"); gMotionEventClassInfo = {0}; jclass motionEventClass = env->FindClass("android/view/MotionEvent"); gMotionEventClassInfo.getDeviceId = env->GetMethodID(motionEventClass, "getDeviceId", "()I"); gMotionEventClassInfo.getSource = env->GetMethodID(motionEventClass, "getSource", "()I"); gMotionEventClassInfo.getAction = env->GetMethodID(motionEventClass, "getAction", "()I"); gMotionEventClassInfo.getEventTime = env->GetMethodID(motionEventClass, "getEventTime", "()J"); gMotionEventClassInfo.getDownTime = env->GetMethodID(motionEventClass, "getDownTime", "()J"); gMotionEventClassInfo.getFlags = env->GetMethodID(motionEventClass, "getFlags", "()I"); gMotionEventClassInfo.getMetaState = env->GetMethodID(motionEventClass, "getMetaState", "()I"); if (sdkVersion >= 23) { gMotionEventClassInfo.getActionButton = env->GetMethodID(motionEventClass, "getActionButton", "()I"); } if (sdkVersion >= 14) { gMotionEventClassInfo.getButtonState = env->GetMethodID(motionEventClass, "getButtonState", "()I"); } if (sdkVersion >= 29) { gMotionEventClassInfo.getClassification = env->GetMethodID(motionEventClass, "getClassification", "()I"); } gMotionEventClassInfo.getEdgeFlags = env->GetMethodID(motionEventClass, "getEdgeFlags", "()I"); gMotionEventClassInfo.getPointerCount = env->GetMethodID(motionEventClass, "getPointerCount", "()I"); gMotionEventClassInfo.getPointerId = env->GetMethodID(motionEventClass, "getPointerId", "(I)I"); gMotionEventClassInfo.getToolType = env->GetMethodID(motionEventClass, "getToolType", "(I)I"); if (sdkVersion >= 29) { gMotionEventClassInfo.getRawX = env->GetMethodID(motionEventClass, "getRawX", "(I)F"); gMotionEventClassInfo.getRawY = env->GetMethodID(motionEventClass, "getRawY", "(I)F"); } gMotionEventClassInfo.getXPrecision = env->GetMethodID(motionEventClass, "getXPrecision", "()F"); gMotionEventClassInfo.getYPrecision = env->GetMethodID(motionEventClass, "getYPrecision", "()F"); gMotionEventClassInfo.getAxisValue = env->GetMethodID(motionEventClass, "getAxisValue", "(II)F"); gMotionEventClassInfo.getHistorySize = env->GetMethodID(motionEventClass, "getHistorySize", "()I"); gMotionEventClassInfo.getHistoricalEventTime = env->GetMethodID(motionEventClass, "getHistoricalEventTime", "(I)J"); gMotionEventClassInfo.getHistoricalAxisValue = env->GetMethodID(motionEventClass, "getHistoricalAxisValue", "(III)F"); gMotionEventClassInfoInitialized = true; } int historySize = env->CallIntMethod(motionEvent, gMotionEventClassInfo.getHistorySize); historySize = std::min(historySize, GAMEACTIVITY_MAX_NUM_HISTORICAL_IN_MOTION_EVENT); int localEnabledHistoricalAxis[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT]; int enabledHistoricalAxisCount = 0; for (int axisIndex = 0; axisIndex < GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT; ++axisIndex) { if (enabledHistoricalAxes[axisIndex]) { localEnabledHistoricalAxis[enabledHistoricalAxisCount++] = axisIndex; } } out_event->historicalCount = enabledHistoricalAxisCount == 0 ? 0 : historySize; out_event->historicalStart = 0; // Free for caller to use // The historical event times aren't unique per-pointer but for simplicity // we output a per-pointer event time, copied from here... int64_t historicalEventTimes[GAMEACTIVITY_MAX_NUM_HISTORICAL_IN_MOTION_EVENT]; for (int histIndex = 0; histIndex < historySize; ++histIndex) { historicalEventTimes[histIndex] = env->CallLongMethod(motionEvent, gMotionEventClassInfo.getHistoricalEventTime, histIndex) * 1000000; } int pointerCount = env->CallIntMethod(motionEvent, gMotionEventClassInfo.getPointerCount); pointerCount = std::min(pointerCount, GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT); out_event->pointerCount = pointerCount; for (int i = 0; i < pointerCount; ++i) { out_event->pointers[i] = { /*id=*/env->CallIntMethod(motionEvent, gMotionEventClassInfo.getPointerId, i), /*toolType=*/env->CallIntMethod(motionEvent, gMotionEventClassInfo.getToolType, i), /*axisValues=*/{0}, /*rawX=*/gMotionEventClassInfo.getRawX ? env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getRawX, i) : 0, /*rawY=*/gMotionEventClassInfo.getRawY ? env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getRawY, i) : 0, }; for (int axisIndex = 0; axisIndex < GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT; ++axisIndex) { if (enabledAxes[axisIndex]) { out_event->pointers[i].axisValues[axisIndex] = env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getAxisValue, axisIndex, i); } } if (enabledHistoricalAxisCount > 0) { for (int histIndex = 0; histIndex < historySize; ++histIndex) { int pointerHistIndex = historySize * i; out_historical[pointerHistIndex].eventTime = historicalEventTimes[histIndex]; for (int c = 0; c < enabledHistoricalAxisCount; ++c) { int axisIndex = localEnabledHistoricalAxis[c]; out_historical[pointerHistIndex].axisValues[axisIndex] = env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getHistoricalAxisValue, axisIndex, i, histIndex); } } } } out_event->deviceId = env->CallIntMethod(motionEvent, gMotionEventClassInfo.getDeviceId); out_event->source = env->CallIntMethod(motionEvent, gMotionEventClassInfo.getSource); out_event->action = env->CallIntMethod(motionEvent, gMotionEventClassInfo.getAction); out_event->eventTime = env->CallLongMethod(motionEvent, gMotionEventClassInfo.getEventTime) * 1000000; out_event->downTime = env->CallLongMethod(motionEvent, gMotionEventClassInfo.getDownTime) * 1000000; out_event->flags = env->CallIntMethod(motionEvent, gMotionEventClassInfo.getFlags); out_event->metaState = env->CallIntMethod(motionEvent, gMotionEventClassInfo.getMetaState); out_event->actionButton = gMotionEventClassInfo.getActionButton ? env->CallIntMethod(motionEvent, gMotionEventClassInfo.getActionButton) : 0; out_event->buttonState = gMotionEventClassInfo.getButtonState ? env->CallIntMethod(motionEvent, gMotionEventClassInfo.getButtonState) : 0; out_event->classification = gMotionEventClassInfo.getClassification ? env->CallIntMethod(motionEvent, gMotionEventClassInfo.getClassification) : 0; out_event->edgeFlags = env->CallIntMethod(motionEvent, gMotionEventClassInfo.getEdgeFlags); out_event->precisionX = env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getXPrecision); out_event->precisionY = env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getYPrecision); return out_event->pointerCount * out_event->historicalCount; } static struct { jmethodID getDeviceId; jmethodID getSource; jmethodID getAction; jmethodID getEventTime; jmethodID getDownTime; jmethodID getFlags; jmethodID getMetaState; jmethodID getModifiers; jmethodID getRepeatCount; jmethodID getKeyCode; jmethodID getScanCode; } gKeyEventClassInfo; extern "C" void GameActivityKeyEvent_fromJava(JNIEnv *env, jobject keyEvent, GameActivityKeyEvent *out_event) { static bool gKeyEventClassInfoInitialized = false; if (!gKeyEventClassInfoInitialized) { int sdkVersion = GetSystemPropAsInt("ro.build.version.sdk"); gKeyEventClassInfo = {0}; jclass keyEventClass = env->FindClass("android/view/KeyEvent"); gKeyEventClassInfo.getDeviceId = env->GetMethodID(keyEventClass, "getDeviceId", "()I"); gKeyEventClassInfo.getSource = env->GetMethodID(keyEventClass, "getSource", "()I"); gKeyEventClassInfo.getAction = env->GetMethodID(keyEventClass, "getAction", "()I"); gKeyEventClassInfo.getEventTime = env->GetMethodID(keyEventClass, "getEventTime", "()J"); gKeyEventClassInfo.getDownTime = env->GetMethodID(keyEventClass, "getDownTime", "()J"); gKeyEventClassInfo.getFlags = env->GetMethodID(keyEventClass, "getFlags", "()I"); gKeyEventClassInfo.getMetaState = env->GetMethodID(keyEventClass, "getMetaState", "()I"); if (sdkVersion >= 13) { gKeyEventClassInfo.getModifiers = env->GetMethodID(keyEventClass, "getModifiers", "()I"); } gKeyEventClassInfo.getRepeatCount = env->GetMethodID(keyEventClass, "getRepeatCount", "()I"); gKeyEventClassInfo.getKeyCode = env->GetMethodID(keyEventClass, "getKeyCode", "()I"); gKeyEventClassInfo.getScanCode = env->GetMethodID(keyEventClass, "getScanCode", "()I"); gKeyEventClassInfoInitialized = true; } *out_event = { /*deviceId=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getDeviceId), /*source=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getSource), /*action=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getAction), // TODO: introduce a millisecondsToNanoseconds helper: /*eventTime=*/ env->CallLongMethod(keyEvent, gKeyEventClassInfo.getEventTime) * 1000000, /*downTime=*/ env->CallLongMethod(keyEvent, gKeyEventClassInfo.getDownTime) * 1000000, /*flags=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getFlags), /*metaState=*/ env->CallIntMethod(keyEvent, gKeyEventClassInfo.getMetaState), /*modifiers=*/gKeyEventClassInfo.getModifiers ? env->CallIntMethod(keyEvent, gKeyEventClassInfo.getModifiers) : 0, /*repeatCount=*/ env->CallIntMethod(keyEvent, gKeyEventClassInfo.getRepeatCount), /*keyCode=*/ env->CallIntMethod(keyEvent, gKeyEventClassInfo.getKeyCode), /*scanCode=*/ env->CallIntMethod(keyEvent, gKeyEventClassInfo.getScanCode)}; } static bool onTouchEvent_native(JNIEnv *env, jobject javaGameActivity, jlong handle, jobject motionEvent) { if (handle == 0) return false; NativeCode *code = (NativeCode *)handle; if (code->callbacks.onTouchEvent == nullptr) return false; static GameActivityMotionEvent c_event; // Note the actual data is written contiguously as numPointers x historySize // entries. static GameActivityHistoricalPointerAxes historical[GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT * GAMEACTIVITY_MAX_NUM_HISTORICAL_IN_MOTION_EVENT]; int historicalLen = GameActivityMotionEvent_fromJava(env, motionEvent, &c_event, historical); return code->callbacks.onTouchEvent(code, &c_event, historical, historicalLen); } 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 const JNINativeMethod g_methods[] = { {"loadNativeCode", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/" "String;Ljava/lang/String;Landroid/content/res/AssetManager;[B)J", (void *)loadNativeCode_native}, {"getDlError", "()Ljava/lang/String;", (void *)getDlError_native}, {"unloadNativeCode", "(J)V", (void *)unloadNativeCode_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", "(J)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;)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}, }; static const char *const kGameActivityPathName = "com/google/androidgamesdk/GameActivity"; static const char *const kInsetsPathName = "androidx/core/graphics/Insets"; static const char *const kWindowInsetsCompatTypePathName = "androidx/core/view/WindowInsetsCompat$Type"; #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) { 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 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"); } 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 // // Register this method so that GameActiviy_register does not need to be called // manually. extern "C" jlong Java_com_google_androidgamesdk_GameActivity_loadNativeCode_C( JNIEnv *env, jobject javaGameActivity, jstring path, jstring funcName, jstring internalDataDir, jstring obbDir, jstring externalDataDir, jobject jAssetMgr, jbyteArray savedState) { GameActivity_register(env); jlong nativeCode = loadNativeCode_native( env, javaGameActivity, path, funcName, internalDataDir, obbDir, externalDataDir, jAssetMgr, savedState); return nativeCode; }