From 49f2b864247c887b4f894ecef1372e74c5975ff6 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Thu, 13 Mar 2025 17:28:19 -0700 Subject: [PATCH] switch to android-game-sdk-rs grafted repo at v2.0.2 Also includes our patches on top (branch android-activity-2.0.2). This is mainly to test to make sure everything is still working. We will switch to the android-activity-4.0.0 branch when we're done Signed-off-by: William Casarin --- android-activity/android-games-sdk/README.md | 40 + android-activity/android-games-sdk/copy-files | 57 + .../android-games-sdk/file_list.txt | 11 + .../include/game-activity/GameActivity.cpp | 1147 +++++++++++++++++ .../include/game-activity/GameActivity.h | 636 +++++++++ .../game-activity/GameActivityEvents.cpp | 414 ++++++ .../game-activity/GameActivityEvents.h | 336 +++++ .../include/game-activity/GameActivityLog.h | 109 ++ .../native_app_glue/android_native_app_glue.c | 732 +++++++++++ .../native_app_glue/android_native_app_glue.h | 507 ++++++++ .../include/game-text-input/gamecommon.h | 41 + .../include/game-text-input/gametextinput.cpp | 377 ++++++ .../include/game-text-input/gametextinput.h | 305 +++++ .../include/common/gamesdk_common.h | 41 + android-activity/build.rs | 31 +- 15 files changed, 4773 insertions(+), 11 deletions(-) create mode 100644 android-activity/android-games-sdk/README.md create mode 100755 android-activity/android-games-sdk/copy-files create mode 100644 android-activity/android-games-sdk/file_list.txt create mode 100644 android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivity.cpp create mode 100644 android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivity.h create mode 100644 android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivityEvents.cpp create mode 100644 android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivityEvents.h create mode 100644 android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivityLog.h create mode 100644 android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/native_app_glue/android_native_app_glue.c create mode 100644 android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/native_app_glue/android_native_app_glue.h create mode 100644 android-activity/android-games-sdk/game-text-input/prefab-src/modules/game-text-input/include/game-text-input/gamecommon.h create mode 100644 android-activity/android-games-sdk/game-text-input/prefab-src/modules/game-text-input/include/game-text-input/gametextinput.cpp create mode 100644 android-activity/android-games-sdk/game-text-input/prefab-src/modules/game-text-input/include/game-text-input/gametextinput.h create mode 100644 android-activity/android-games-sdk/include/common/gamesdk_common.h diff --git a/android-activity/android-games-sdk/README.md b/android-activity/android-games-sdk/README.md new file mode 100644 index 0000000..bfe6452 --- /dev/null +++ b/android-activity/android-games-sdk/README.md @@ -0,0 +1,40 @@ +# android-games-sdk-rs + +This is a grafted version of [android-games-sdk-rs][agdk]. + +The purpose of android-games-sdk-rs is to track our patches on top of the Android Game Development Kit (AGDK) so that it is easier to update to new upstream versions. + +## Updating to new agdk version checklist + +This is a basic checklist for things that need to be done before updating to a new agdk version: + +- [ ] Ensure all patches applied to the local repo have been backported (see section below on backporting patches) +- [ ] Rebase patches on top of a new *release* version of agdk. If you're not sure which version you should be rebasing on, open an issue. +- [ ] Copy any new files over from the new version using the [copy_files](./copy_files) script (see section below on copying files) +- [ ] Update [build.rs](../build.rs) with any new includes and src files +- [ ] Regenerate ffi bindings using [generate_bindings.sh](./generate_bindings.sh) + +## Backporting patches + +Changes made to these files must be backported to [android-games-sdk-rs][agdk], otherwise they will be lost when updating to newer upstream versions. This can be done like so (running from the project root): + +```bash +git format-patch -o ~/agdk-patches last-import.. -- android-activity/android-games-sdk +``` + +When applying on the [android-games-sdk-rs][agdk] side: + +```bash +git checkout android-activity-4.0.0 +git am -p3 ~/agdk-patches/*.patch +``` + +Once these are applied on top of the current imported agdk version (in this example 4.0.0). Then they can be rebased onto future agdk versions easily. + +## Copying files from agdk + +When updating to a new version of upstream agdk properly, you can use the [copy_files](./copy_files) script. + +This script takes in a list of files from [file_list.txt](./file_list.txt), and grafts those files into the local directory. This ensures that pathes are aligned so that patching is easier. + +[agdk]: https://github.com/rust-mobile/android-games-sdk-rs diff --git a/android-activity/android-games-sdk/copy-files b/android-activity/android-games-sdk/copy-files new file mode 100755 index 0000000..5e21578 --- /dev/null +++ b/android-activity/android-games-sdk/copy-files @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +# Copies files from android-games-sdk-rs so that we don't have to use +# it has a submodule (see https://github.com/rust-mobile/android-activity/issues/190) + +# Usage: ./copy_files source_dir dest_dir file_list.txt +# file_list.txt should contain one filename or path pattern per line + +# Check if correct number of arguments +if [ $# -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +SOURCE_DIR="$1" +DEST_DIR="${2:-.}" +FILE_LIST="${3:-file_list.txt}" + +# Check if source directory exists +if [ ! -d "$SOURCE_DIR" ]; then + echo "Error: Source directory '$SOURCE_DIR' does not exist." + exit 1 +fi + +# Check if file list exists +if [ ! -f "$FILE_LIST" ]; then + echo "Error: File list '$FILE_LIST' does not exist." + exit 1 +fi + +# Create destination directory if it doesn't exist +mkdir -p "$DEST_DIR" + +# Read file list and copy each file +while IFS= read -r file || [ -n "$file" ]; do + # Skip empty lines and comments + if [ -z "$file" ] || [[ "$file" =~ ^# ]]; then + continue + fi + + # Find matching files + find "$SOURCE_DIR" -name "$file" -type f | while read -r file; do + # Get the relative path + rel_path="${file#$SOURCE_DIR/}" + dest_file="$DEST_DIR/$rel_path" + dest_dir=$(dirname "$dest_file") + + # Create destination directory structure + mkdir -p "$dest_dir" + + # Copy the file + cp "$file" "$dest_file" + echo "Copied: $rel_path" + done +done < "$FILE_LIST" + +echo "Copy operation completed." diff --git a/android-activity/android-games-sdk/file_list.txt b/android-activity/android-games-sdk/file_list.txt new file mode 100644 index 0000000..503ef41 --- /dev/null +++ b/android-activity/android-games-sdk/file_list.txt @@ -0,0 +1,11 @@ +GameActivity.h +GameActivity.cpp +GameActivityEvents.h +GameActivityEvents.cpp +GameActivityLog.h +gametextinput.cpp +gametextinput.h +gamecommon.h +gamesdk_common.h +android_native_app_glue.h +android_native_app_glue.c diff --git a/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivity.cpp b/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivity.cpp new file mode 100644 index 0000000..4dbc9e8 --- /dev/null +++ b/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivity.cpp @@ -0,0 +1,1147 @@ +/* + * 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 "GameActivity.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "GameActivityLog.h" + +namespace { + +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 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; +} 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 +}; + +/* + * Last known Configuration values. They may be accessed from the different + * thread, this is why they are made atomic. + */ +static struct Configuration { + std::atomic_int colorMode; + std::atomic_int densityDpi; + std::atomic fontScale; + std::atomic_int fontWeightAdjustment; + std::atomic_int hardKeyboardHidden; + std::atomic_int keyboard; + std::atomic_int keyboardHidden; + std::atomic_int mcc; + std::atomic_int mnc; + std::atomic_int navigation; + std::atomic_int navigationHidden; + std::atomic_int orientation; + std::atomic_int screenHeightDp; + std::atomic_int screenLayout; + std::atomic_int screenWidthDp; + std::atomic_int smallestScreenWidthDp; + std::atomic_int touchscreen; + std::atomic_int uiMode; +} gConfiguration; + +/* + * 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; + } + + ~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]; +}; + +static void readConfigurationValues(NativeCode *code, jobject javaConfig); + +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; + case CMD_SET_IME_EDITOR_INFO: { + code->env->CallVoidMethod( + code->javaGameActivity, + gGameActivityClassInfo.setImeEditorInfoFields, work.arg1, + work.arg2, work.arg3); + checkAndClearException(code->env, "setImeEditorInfo"); + } 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); + } + + readConfigurationValues(code, javaConfig); + + GameActivity_onCreate_C(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 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(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 readConfigurationValues(NativeCode *code, jobject javaConfig) { + 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.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"); +} + +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, + int inputType, int actionId, + int imeOptions) { + NativeCode *code = static_cast(activity); + write_work(code->mainWorkWrite, CMD_SET_IME_EDITOR_INFO, inputType, + actionId, imeOptions); +} + +extern "C" int GameActivity_getColorMode(GameActivity *) { + return gConfiguration.colorMode; +} + +extern "C" int GameActivity_getDensityDpi(GameActivity *) { + return gConfiguration.densityDpi; +} + +extern "C" float GameActivity_getFontScale(GameActivity *) { + return gConfiguration.fontScale; +} + +extern "C" int GameActivity_getFontWeightAdjustment(GameActivity *) { + return gConfiguration.fontWeightAdjustment; +} + +extern "C" int GameActivity_getHardKeyboardHidden(GameActivity *) { + return gConfiguration.hardKeyboardHidden; +} + +extern "C" int GameActivity_getKeyboard(GameActivity *) { + return gConfiguration.keyboard; +} + +extern "C" int GameActivity_getKeyboardHidden(GameActivity *) { + return gConfiguration.keyboardHidden; +} + +extern "C" int GameActivity_getMcc(GameActivity *) { + return gConfiguration.mcc; +} + +extern "C" int GameActivity_getMnc(GameActivity *) { + return gConfiguration.mnc; +} + +extern "C" int GameActivity_getNavigation(GameActivity *) { + return gConfiguration.navigation; +} + +extern "C" int GameActivity_getNavigationHidden(GameActivity *) { + return gConfiguration.navigationHidden; +} + +extern "C" int GameActivity_getOrientation(GameActivity *) { + return gConfiguration.orientation; +} + +extern "C" int GameActivity_getScreenHeightDp(GameActivity *) { + return gConfiguration.screenHeightDp; +} + +extern "C" int GameActivity_getScreenLayout(GameActivity *) { + return gConfiguration.screenLayout; +} + +extern "C" int GameActivity_getScreenWidthDp(GameActivity *) { + return gConfiguration.screenWidthDp; +} + +extern "C" int GameActivity_getSmallestScreenWidthDp(GameActivity *) { + return gConfiguration.smallestScreenWidthDp; +} + +extern "C" int GameActivity_getTouchscreen(GameActivity *) { + return gConfiguration.touchscreen; +} + +extern "C" int GameActivity_getUIMode(GameActivity *) { + return gConfiguration.uiMode; +} + +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; + GameActivityMotionEvent_fromJava(env, motionEvent, &c_event); + 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 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;)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}, +}; + +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"; + +#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 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"); + + 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 +// +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; +} diff --git a/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivity.h b/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivity.h new file mode 100644 index 0000000..b945069 --- /dev/null +++ b/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivity.h @@ -0,0 +1,636 @@ +/* + * Copyright (C) 2021 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. + */ + +/** + * @addtogroup GameActivity Game Activity + * The interface to use GameActivity. + * @{ + */ + +/** + * @file GameActivity.h + */ + +#ifndef ANDROID_GAME_SDK_GAME_ACTIVITY_H +#define ANDROID_GAME_SDK_GAME_ACTIVITY_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/gamesdk_common.h" +#include "game-activity/GameActivityEvents.h" +#include "game-text-input/gametextinput.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define GAMEACTIVITY_MAJOR_VERSION 2 +#define GAMEACTIVITY_MINOR_VERSION 0 +#define GAMEACTIVITY_BUGFIX_VERSION 2 +#define GAMEACTIVITY_PACKED_VERSION \ + ANDROID_GAMESDK_PACKED_VERSION(GAMEACTIVITY_MAJOR_VERSION, \ + GAMEACTIVITY_MINOR_VERSION, \ + GAMEACTIVITY_BUGFIX_VERSION) + +/** + * {@link GameActivityCallbacks} + */ +struct GameActivityCallbacks; + +/** + * This structure defines the native side of an android.app.GameActivity. + * It is created by the framework, and handed to the application's native + * code as it is being launched. + */ +typedef struct GameActivity { + /** + * Pointer to the callback function table of the native application. + * You can set the functions here to your own callbacks. The callbacks + * pointer itself here should not be changed; it is allocated and managed + * for you by the framework. + */ + struct GameActivityCallbacks* callbacks; + + /** + * The global handle on the process's Java VM. + */ + JavaVM* vm; + + /** + * JNI context for the main thread of the app. Note that this field + * can ONLY be used from the main thread of the process; that is, the + * thread that calls into the GameActivityCallbacks. + */ + JNIEnv* env; + + /** + * The GameActivity object handle. + */ + jobject javaGameActivity; + + /** + * Path to this application's internal data directory. + */ + const char* internalDataPath; + + /** + * Path to this application's external (removable/mountable) data directory. + */ + const char* externalDataPath; + + /** + * The platform's SDK version code. + */ + int32_t sdkVersion; + + /** + * This is the native instance of the application. It is not used by + * the framework, but can be set by the application to its own instance + * state. + */ + void* instance; + + /** + * Pointer to the Asset Manager instance for the application. The + * application uses this to access binary assets bundled inside its own .apk + * file. + */ + AAssetManager* assetManager; + + /** + * Available starting with Honeycomb: path to the directory containing + * the application's OBB files (if any). If the app doesn't have any + * OBB files, this directory may not exist. + */ + const char* obbPath; +} GameActivity; + +/** + * A function the user should call from their callback with the data, its length + * and the library- supplied context. + */ +typedef void (*SaveInstanceStateRecallback)(const char* bytes, int len, + void* context); + +/** + * These are the callbacks the framework makes into a native application. + * All of these callbacks happen on the main thread of the application. + * By default, all callbacks are NULL; set to a pointer to your own function + * to have it called. + */ +typedef struct GameActivityCallbacks { + /** + * GameActivity has started. See Java documentation for Activity.onStart() + * for more information. + */ + void (*onStart)(GameActivity* activity); + + /** + * GameActivity has resumed. See Java documentation for Activity.onResume() + * for more information. + */ + void (*onResume)(GameActivity* activity); + + /** + * The framework is asking GameActivity to save its current instance state. + * See the Java documentation for Activity.onSaveInstanceState() for more + * information. The user should call the recallback with their data, its + * length and the provided context; they retain ownership of the data. Note + * that the saved state will be persisted, so it can not contain any active + * entities (pointers to memory, file descriptors, etc). + */ + void (*onSaveInstanceState)(GameActivity* activity, + SaveInstanceStateRecallback recallback, + void* context); + + /** + * GameActivity has paused. See Java documentation for Activity.onPause() + * for more information. + */ + void (*onPause)(GameActivity* activity); + + /** + * GameActivity has stopped. See Java documentation for Activity.onStop() + * for more information. + */ + void (*onStop)(GameActivity* activity); + + /** + * GameActivity is being destroyed. See Java documentation for + * Activity.onDestroy() for more information. + */ + void (*onDestroy)(GameActivity* activity); + + /** + * Focus has changed in this GameActivity's window. This is often used, + * for example, to pause a game when it loses input focus. + */ + void (*onWindowFocusChanged)(GameActivity* activity, bool hasFocus); + + /** + * The drawing window for this native activity has been created. You + * can use the given native window object to start drawing. + */ + void (*onNativeWindowCreated)(GameActivity* activity, + ANativeWindow* window); + + /** + * The drawing window for this native activity has been resized. You should + * retrieve the new size from the window and ensure that your rendering in + * it now matches. + */ + void (*onNativeWindowResized)(GameActivity* activity, ANativeWindow* window, + int32_t newWidth, int32_t newHeight); + + /** + * The drawing window for this native activity needs to be redrawn. To + * avoid transient artifacts during screen changes (such resizing after + * rotation), applications should not return from this function until they + * have finished drawing their window in its current state. + */ + void (*onNativeWindowRedrawNeeded)(GameActivity* activity, + ANativeWindow* window); + + /** + * The drawing window for this native activity is going to be destroyed. + * You MUST ensure that you do not touch the window object after returning + * from this function: in the common case of drawing to the window from + * another thread, that means the implementation of this callback must + * properly synchronize with the other thread to stop its drawing before + * returning from here. + */ + void (*onNativeWindowDestroyed)(GameActivity* activity, + ANativeWindow* window); + + /** + * The current device AConfiguration has changed. The new configuration can + * be retrieved from assetManager. + */ + void (*onConfigurationChanged)(GameActivity* activity); + + /** + * The system is running low on memory. Use this callback to release + * resources you do not need, to help the system avoid killing more + * important processes. + */ + void (*onTrimMemory)(GameActivity* activity, int level); + + /** + * Callback called for every MotionEvent done on the GameActivity + * SurfaceView. Ownership of `event` is maintained by the library and it is + * only valid during the callback. + */ + bool (*onTouchEvent)(GameActivity* activity, + const GameActivityMotionEvent* event); + + /** + * Callback called for every key down event on the GameActivity SurfaceView. + * Ownership of `event` is maintained by the library and it is only valid + * during the callback. + */ + bool (*onKeyDown)(GameActivity* activity, + const GameActivityKeyEvent* event); + + /** + * Callback called for every key up event on the GameActivity SurfaceView. + * Ownership of `event` is maintained by the library and it is only valid + * during the callback. + */ + bool (*onKeyUp)(GameActivity* activity, const GameActivityKeyEvent* event); + + /** + * Callback called for every soft-keyboard text input event. + * Ownership of `state` is maintained by the library and it is only valid + * during the callback. + */ + void (*onTextInputEvent)(GameActivity* activity, + const GameTextInputState* state); + + /** + * Callback called when WindowInsets of the main app window have changed. + * Call GameActivity_getWindowInsets to retrieve the insets themselves. + */ + void (*onWindowInsetsChanged)(GameActivity* activity); + + /** + * Callback called when the rectangle in the window where the content + * should be placed has changed. + */ + void (*onContentRectChanged)(GameActivity *activity, const ARect *rect); +} GameActivityCallbacks; + +/** + * This is the function that must be in the native code to instantiate the + * application's native activity. It is called with the activity instance (see + * above); if the code is being instantiated from a previously saved instance, + * the savedState will be non-NULL and point to the saved data. You must make + * any copy of this data you need -- it will be released after you return from + * this function. + */ +typedef void GameActivity_createFunc(GameActivity* activity, void* savedState, + size_t savedStateSize); + +/** + * The name of the function that NativeInstance looks for when launching its + * native code. This is the default function that is used, you can specify + * "android.app.func_name" string meta-data in your manifest to use a different + * function. + */ +extern GameActivity_createFunc GameActivity_onCreate_C; + +/** + * Finish the given activity. Its finish() method will be called, causing it + * to be stopped and destroyed. Note that this method can be called from + * *any* thread; it will send a message to the main thread of the process + * where the Java finish call will take place. + */ +void GameActivity_finish(GameActivity* activity); + +/** + * Flags for GameActivity_setWindowFlags, + * as per the Java API at android.view.WindowManager.LayoutParams. + */ +enum GameActivitySetWindowFlags { + /** + * As long as this window is visible to the user, allow the lock + * screen to activate while the screen is on. This can be used + * independently, or in combination with {@link + * GAMEACTIVITY_FLAG_KEEP_SCREEN_ON} and/or {@link + * GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED} + */ + GAMEACTIVITY_FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 0x00000001, + /** Everything behind this window will be dimmed. */ + GAMEACTIVITY_FLAG_DIM_BEHIND = 0x00000002, + /** + * Blur everything behind this window. + * @deprecated Blurring is no longer supported. + */ + GAMEACTIVITY_FLAG_BLUR_BEHIND = 0x00000004, + /** + * This window won't ever get key input focus, so the + * user can not send key or other button events to it. Those will + * instead go to whatever focusable window is behind it. This flag + * will also enable {@link GAMEACTIVITY_FLAG_NOT_TOUCH_MODAL} whether or not + * that is explicitly set. + * + * Setting this flag also implies that the window will not need to + * interact with + * a soft input method, so it will be Z-ordered and positioned + * independently of any active input method (typically this means it + * gets Z-ordered on top of the input method, so it can use the full + * screen for its content and cover the input method if needed. You + * can use {@link GAMEACTIVITY_FLAG_ALT_FOCUSABLE_IM} to modify this + * behavior. + */ + GAMEACTIVITY_FLAG_NOT_FOCUSABLE = 0x00000008, + /** This window can never receive touch events. */ + GAMEACTIVITY_FLAG_NOT_TOUCHABLE = 0x00000010, + /** + * Even when this window is focusable (its + * {@link GAMEACTIVITY_FLAG_NOT_FOCUSABLE} is not set), allow any pointer + * events outside of the window to be sent to the windows behind it. + * Otherwise it will consume all pointer events itself, regardless of + * whether they are inside of the window. + */ + GAMEACTIVITY_FLAG_NOT_TOUCH_MODAL = 0x00000020, + /** + * When set, if the device is asleep when the touch + * screen is pressed, you will receive this first touch event. Usually + * the first touch event is consumed by the system since the user can + * not see what they are pressing on. + * + * @deprecated This flag has no effect. + */ + GAMEACTIVITY_FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040, + /** + * As long as this window is visible to the user, keep + * the device's screen turned on and bright. + */ + GAMEACTIVITY_FLAG_KEEP_SCREEN_ON = 0x00000080, + /** + * Place the window within the entire screen, ignoring + * decorations around the border (such as the status bar). The + * window must correctly position its contents to take the screen + * decoration into account. + */ + GAMEACTIVITY_FLAG_LAYOUT_IN_SCREEN = 0x00000100, + /** Allows the window to extend outside of the screen. */ + GAMEACTIVITY_FLAG_LAYOUT_NO_LIMITS = 0x00000200, + /** + * Hide all screen decorations (such as the status + * bar) while this window is displayed. This allows the window to + * use the entire display space for itself -- the status bar will + * be hidden when an app window with this flag set is on the top + * layer. A fullscreen window will ignore a value of {@link + * GAMEACTIVITY_SOFT_INPUT_ADJUST_RESIZE}; the window will stay + * fullscreen and will not resize. + */ + GAMEACTIVITY_FLAG_FULLSCREEN = 0x00000400, + /** + * Override {@link GAMEACTIVITY_FLAG_FULLSCREEN} and force the + * screen decorations (such as the status bar) to be shown. + */ + GAMEACTIVITY_FLAG_FORCE_NOT_FULLSCREEN = 0x00000800, + /** + * Turn on dithering when compositing this window to + * the screen. + * @deprecated This flag is no longer used. + */ + GAMEACTIVITY_FLAG_DITHER = 0x00001000, + /** + * Treat the content of the window as secure, preventing + * it from appearing in screenshots or from being viewed on non-secure + * displays. + */ + GAMEACTIVITY_FLAG_SECURE = 0x00002000, + /** + * A special mode where the layout parameters are used + * to perform scaling of the surface when it is composited to the + * screen. + */ + GAMEACTIVITY_FLAG_SCALED = 0x00004000, + /** + * Intended for windows that will often be used when the user is + * holding the screen against their face, it will aggressively + * filter the event stream to prevent unintended presses in this + * situation that may not be desired for a particular window, when + * such an event stream is detected, the application will receive + * a {@link AMOTION_EVENT_ACTION_CANCEL} to indicate this so + * applications can handle this accordingly by taking no action on + * the event until the finger is released. + */ + GAMEACTIVITY_FLAG_IGNORE_CHEEK_PRESSES = 0x00008000, + /** + * A special option only for use in combination with + * {@link GAMEACTIVITY_FLAG_LAYOUT_IN_SCREEN}. When requesting layout in + * the screen your window may appear on top of or behind screen decorations + * such as the status bar. By also including this flag, the window + * manager will report the inset rectangle needed to ensure your + * content is not covered by screen decorations. + */ + GAMEACTIVITY_FLAG_LAYOUT_INSET_DECOR = 0x00010000, + /** + * Invert the state of {@link GAMEACTIVITY_FLAG_NOT_FOCUSABLE} with + * respect to how this window interacts with the current method. + * That is, if FLAG_NOT_FOCUSABLE is set and this flag is set, + * then the window will behave as if it needs to interact with the + * input method and thus be placed behind/away from it; if {@link + * GAMEACTIVITY_FLAG_NOT_FOCUSABLE} is not set and this flag is set, + * then the window will behave as if it doesn't need to interact + * with the input method and can be placed to use more space and + * cover the input method. + */ + GAMEACTIVITY_FLAG_ALT_FOCUSABLE_IM = 0x00020000, + /** + * If you have set {@link GAMEACTIVITY_FLAG_NOT_TOUCH_MODAL}, you + * can set this flag to receive a single special MotionEvent with + * the action + * {@link AMOTION_EVENT_ACTION_OUTSIDE} for + * touches that occur outside of your window. Note that you will not + * receive the full down/move/up gesture, only the location of the + * first down as an {@link AMOTION_EVENT_ACTION_OUTSIDE}. + */ + GAMEACTIVITY_FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000, + /** + * Special flag to let windows be shown when the screen + * is locked. This will let application windows take precedence over + * key guard or any other lock screens. Can be used with + * {@link GAMEACTIVITY_FLAG_KEEP_SCREEN_ON} to turn screen on and display + * windows directly before showing the key guard window. Can be used with + * {@link GAMEACTIVITY_FLAG_DISMISS_KEYGUARD} to automatically fully + * dismisss non-secure keyguards. This flag only applies to the top-most + * full-screen window. + */ + GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED = 0x00080000, + /** + * Ask that the system wallpaper be shown behind + * your window. The window surface must be translucent to be able + * to actually see the wallpaper behind it; this flag just ensures + * that the wallpaper surface will be there if this window actually + * has translucent regions. + */ + GAMEACTIVITY_FLAG_SHOW_WALLPAPER = 0x00100000, + /** + * When set as a window is being added or made + * visible, once the window has been shown then the system will + * poke the power manager's user activity (as if the user had woken + * up the device) to turn the screen on. + */ + GAMEACTIVITY_FLAG_TURN_SCREEN_ON = 0x00200000, + /** + * When set the window will cause the keyguard to + * be dismissed, only if it is not a secure lock keyguard. Because such + * a keyguard is not needed for security, it will never re-appear if + * the user navigates to another window (in contrast to + * {@link GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED}, which will only temporarily + * hide both secure and non-secure keyguards but ensure they reappear + * when the user moves to another UI that doesn't hide them). + * If the keyguard is currently active and is secure (requires an + * unlock pattern) than the user will still need to confirm it before + * seeing this window, unless {@link GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED} has + * also been set. + */ + GAMEACTIVITY_FLAG_DISMISS_KEYGUARD = 0x00400000, +}; + +/** + * Change the window flags of the given activity. Calls getWindow().setFlags() + * of the given activity. + * Note that some flags must be set before the window decoration is created, + * see + * https://developer.android.com/reference/android/view/Window#setFlags(int,%20int). + * Note also that this method can be called from + * *any* thread; it will send a message to the main thread of the process + * where the Java finish call will take place. + */ +void GameActivity_setWindowFlags(GameActivity* activity, uint32_t addFlags, + uint32_t removeFlags); + +/** + * Flags for GameActivity_showSoftInput; see the Java InputMethodManager + * API for documentation. + */ +enum GameActivityShowSoftInputFlags { + /** + * Implicit request to show the input window, not as the result + * of a direct request by the user. + */ + GAMEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT = 0x0001, + + /** + * The user has forced the input method open (such as by + * long-pressing menu) so it should not be closed until they + * explicitly do so. + */ + GAMEACTIVITY_SHOW_SOFT_INPUT_FORCED = 0x0002, +}; + +/** + * Show the IME while in the given activity. Calls + * InputMethodManager.showSoftInput() for the given activity. Note that this + * method can be called from *any* thread; it will send a message to the main + * thread of the process where the Java call will take place. + */ +void GameActivity_showSoftInput(GameActivity* activity, uint32_t flags); + +/** + * Set the text entry state (see documentation of the GameTextInputState struct + * in the Game Text Input library reference). + * + * Ownership of the state is maintained by the caller. + */ +void GameActivity_setTextInputState(GameActivity* activity, + const GameTextInputState* state); + +/** + * Get the last-received text entry state (see documentation of the + * GameTextInputState struct in the Game Text Input library reference). + * + */ +void GameActivity_getTextInputState(GameActivity* activity, + GameTextInputGetStateCallback callback, + void* context); + +/** + * Get a pointer to the GameTextInput library instance. + */ +GameTextInput* GameActivity_getTextInput(const GameActivity* activity); + +/** + * Flags for GameActivity_hideSoftInput; see the Java InputMethodManager + * API for documentation. + */ +enum GameActivityHideSoftInputFlags { + /** + * The soft input window should only be hidden if it was not + * explicitly shown by the user. + */ + GAMEACTIVITY_HIDE_SOFT_INPUT_IMPLICIT_ONLY = 0x0001, + /** + * The soft input window should normally be hidden, unless it was + * originally shown with {@link GAMEACTIVITY_SHOW_SOFT_INPUT_FORCED}. + */ + GAMEACTIVITY_HIDE_SOFT_INPUT_NOT_ALWAYS = 0x0002, +}; + +/** + * Hide the IME while in the given activity. Calls + * InputMethodManager.hideSoftInput() for the given activity. Note that this + * method can be called from *any* thread; it will send a message to the main + * thread of the process where the Java finish call will take place. + */ +void GameActivity_hideSoftInput(GameActivity* activity, uint32_t flags); + +/** + * Get the current window insets of the particular component. See + * https://developer.android.com/reference/androidx/core/view/WindowInsetsCompat.Type + * for more details. + * You can use these insets to influence what you show on the screen. + */ +void GameActivity_getWindowInsets(GameActivity* activity, + GameCommonInsetsType type, ARect* insets); + +/** + * Set options on how the IME behaves when it is requested for text input. + * See + * https://developer.android.com/reference/android/view/inputmethod/EditorInfo + * for the meaning of inputType, actionId and imeOptions. + * + * Note that this function will attach the current thread to the JVM if it is + * not already attached, so the caller must detach the thread from the JVM + * before the thread is destroyed using DetachCurrentThread. + */ +void GameActivity_setImeEditorInfo(GameActivity* activity, int inputType, + int actionId, int imeOptions); + +/** + * These are getters for Configuration class members. They may be called from + * any thread. + */ +int GameActivity_getOrientation(GameActivity* activity); +int GameActivity_getColorMode(GameActivity* activity); +int GameActivity_getDensityDpi(GameActivity* activity); +float GameActivity_getFontScale(GameActivity* activity); +int GameActivity_getFontWeightAdjustment(GameActivity* activity); +int GameActivity_getHardKeyboardHidden(GameActivity* activity); +int GameActivity_getKeyboard(GameActivity* activity); +int GameActivity_getKeyboardHidden(GameActivity* activity); +int GameActivity_getMcc(GameActivity* activity); +int GameActivity_getMnc(GameActivity* activity); +int GameActivity_getNavigation(GameActivity* activity); +int GameActivity_getNavigationHidden(GameActivity* activity); +int GameActivity_getOrientation(GameActivity* activity); +int GameActivity_getScreenHeightDp(GameActivity* activity); +int GameActivity_getScreenLayout(GameActivity* activity); +int GameActivity_getScreenWidthDp(GameActivity* activity); +int GameActivity_getSmallestScreenWidthDp(GameActivity* activity); +int GameActivity_getTouchscreen(GameActivity* activity); +int GameActivity_getUIMode(GameActivity* activity); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif // ANDROID_GAME_SDK_GAME_ACTIVITY_H diff --git a/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivityEvents.cpp b/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivityEvents.cpp new file mode 100644 index 0000000..028b55e --- /dev/null +++ b/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivityEvents.cpp @@ -0,0 +1,414 @@ +/* + * Copyright (C) 2022 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 "GameActivityEvents.h" + +#include + +#include + +#include "GameActivityLog.h" + +// 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 { + +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 ""; +} + +std::string GetSystemProp(const char *key, const char *default_value = "") { + return getSystemPropViaGet(key, default_value); +} + +int GetSystemPropAsInt(const char *key, int default_value = 0) { + std::string prop = GetSystemProp(key); + return prop == "" ? default_value : strtoll(prop.c_str(), nullptr, 10); +} + +} // anonymous namespace + +#ifndef NELEM +#define NELEM(x) ((int)(sizeof(x) / sizeof((x)[0]))) +#endif + +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; +} + +float GameActivityPointerAxes_getAxisValue( + const GameActivityPointerAxes *pointerInfo, int32_t axis) { + if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) { + return 0; + } + + if (!enabledAxes[axis]) { + ALOGW("Axis %d must be enabled before it can be accessed.", axis); + return 0; + } + + return pointerInfo->axisValues[axis]; +} + +extern "C" void GameActivityPointerAxes_disableAxis(int32_t axis) { + if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) { + return; + } + + enabledAxes[axis] = false; +} + +float GameActivityMotionEvent_getHistoricalAxisValue( + const GameActivityMotionEvent *event, int axis, int pointerIndex, + int historyPos) { + if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) { + ALOGE("Invalid axis %d", axis); + return -1; + } + if (pointerIndex < 0 || pointerIndex >= event->pointerCount) { + ALOGE("Invalid pointer index %d", pointerIndex); + return -1; + } + if (historyPos < 0 || historyPos >= event->historySize) { + ALOGE("Invalid history index %d", historyPos); + return -1; + } + if (!enabledAxes[axis]) { + ALOGW("Axis %d must be enabled before it can be accessed.", axis); + return 0; + } + + int pointerOffset = pointerIndex * GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT; + int historyValuesOffset = historyPos * event->pointerCount * + GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT; + return event + ->historicalAxisValues[historyValuesOffset + pointerOffset + axis]; +} + +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 getHistorySize; + jmethodID getHistoricalEventTime; + + jmethodID getPointerCount; + jmethodID getPointerId; + + jmethodID getToolType; + + jmethodID getRawX; + jmethodID getRawY; + jmethodID getXPrecision; + jmethodID getYPrecision; + jmethodID getAxisValue; + + jmethodID getHistoricalAxisValue; +} gMotionEventClassInfo; + +extern "C" void GameActivityMotionEvent_destroy( + GameActivityMotionEvent *c_event) { + delete c_event->historicalAxisValues; + delete c_event->historicalEventTimesMillis; + delete c_event->historicalEventTimesNanos; +} + +extern "C" void GameActivityMotionEvent_fromJava( + JNIEnv *env, jobject motionEvent, GameActivityMotionEvent *out_event) { + 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.getHistorySize = + env->GetMethodID(motionEventClass, "getHistorySize", "()I"); + gMotionEventClassInfo.getHistoricalEventTime = env->GetMethodID( + motionEventClass, "getHistoricalEventTime", "(I)J"); + + 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.getHistoricalAxisValue = env->GetMethodID( + motionEventClass, "getHistoricalAxisValue", "(III)F"); + gMotionEventClassInfoInitialized = true; + } + + 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); + } + } + } + + int historySize = + env->CallIntMethod(motionEvent, gMotionEventClassInfo.getHistorySize); + out_event->historySize = historySize; + out_event->historicalAxisValues = + new float[historySize * pointerCount * + GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT]; + out_event->historicalEventTimesMillis = new int64_t[historySize]; + out_event->historicalEventTimesNanos = new int64_t[historySize]; + + for (int historyIndex = 0; historyIndex < historySize; historyIndex++) { + out_event->historicalEventTimesMillis[historyIndex] = + env->CallLongMethod(motionEvent, + gMotionEventClassInfo.getHistoricalEventTime, + historyIndex); + out_event->historicalEventTimesNanos[historyIndex] = + out_event->historicalEventTimesMillis[historyIndex] * 1000000; + for (int i = 0; i < pointerCount; ++i) { + int pointerOffset = i * GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT; + int historyAxisOffset = historyIndex * pointerCount * + GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT; + float *axisValues = + &out_event + ->historicalAxisValues[historyAxisOffset + pointerOffset]; + for (int axisIndex = 0; + axisIndex < GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT; + ++axisIndex) { + if (enabledAxes[axisIndex]) { + axisValues[axisIndex] = env->CallFloatMethod( + motionEvent, + gMotionEventClassInfo.getHistoricalAxisValue, axisIndex, + i, historyIndex); + } + } + } + } + + 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); +} + +static struct { + jmethodID getDeviceId; + jmethodID getSource; + jmethodID getAction; + + jmethodID getEventTime; + jmethodID getDownTime; + + jmethodID getFlags; + jmethodID getMetaState; + + jmethodID getModifiers; + jmethodID getRepeatCount; + jmethodID getKeyCode; + jmethodID getScanCode; + //jmethodID getUnicodeChar; +} 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"); + //gKeyEventClassInfo.getUnicodeChar = + // env->GetMethodID(keyEventClass, "getUnicodeChar", "()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) + /*unicodeChar=*/ + //env->CallIntMethod(keyEvent, gKeyEventClassInfo.getUnicodeChar) + }; +} diff --git a/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivityEvents.h b/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivityEvents.h new file mode 100644 index 0000000..015ffbe --- /dev/null +++ b/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivityEvents.h @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2022 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. + */ + +/** + * @addtogroup GameActivity Game Activity Events + * The interface to use Game Activity Events. + * @{ + */ + +/** + * @file GameActivityEvents.h + */ +#ifndef ANDROID_GAME_SDK_GAME_ACTIVITY_EVENTS_H +#define ANDROID_GAME_SDK_GAME_ACTIVITY_EVENTS_H + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * The maximum number of axes supported in an Android MotionEvent. + * See https://developer.android.com/ndk/reference/group/input. + */ +#define GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT 48 + +/** + * \brief Describe information about a pointer, found in a + * GameActivityMotionEvent. + * + * You can read values directly from this structure, or use helper functions + * (`GameActivityPointerAxes_getX`, `GameActivityPointerAxes_getY` and + * `GameActivityPointerAxes_getAxisValue`). + * + * The X axis and Y axis are enabled by default but any other axis that you want + * to read **must** be enabled first, using + * `GameActivityPointerAxes_enableAxis`. + * + * \see GameActivityMotionEvent + */ +typedef struct GameActivityPointerAxes { + int32_t id; + int32_t toolType; + float axisValues[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT]; + float rawX; + float rawY; +} GameActivityPointerAxes; + +/** \brief Get the toolType of the pointer. */ +inline int32_t GameActivityPointerAxes_getToolType( + const GameActivityPointerAxes* pointerInfo) { + return pointerInfo->toolType; +} + +/** \brief Get the current X coordinate of the pointer. */ +inline float GameActivityPointerAxes_getX( + const GameActivityPointerAxes* pointerInfo) { + return pointerInfo->axisValues[AMOTION_EVENT_AXIS_X]; +} + +/** \brief Get the current Y coordinate of the pointer. */ +inline float GameActivityPointerAxes_getY( + const GameActivityPointerAxes* pointerInfo) { + return pointerInfo->axisValues[AMOTION_EVENT_AXIS_Y]; +} + +/** + * \brief Enable the specified axis, so that its value is reported in the + * GameActivityPointerAxes structures stored in a motion event. + * + * You must enable any axis that you want to read, apart from + * `AMOTION_EVENT_AXIS_X` and `AMOTION_EVENT_AXIS_Y` that are enabled by + * default. + * + * If the axis index is out of range, nothing is done. + */ +void GameActivityPointerAxes_enableAxis(int32_t axis); + +/** + * \brief Disable the specified axis. Its value won't be reported in the + * GameActivityPointerAxes structures stored in a motion event anymore. + * + * Apart from X and Y, any axis that you want to read **must** be enabled first, + * using `GameActivityPointerAxes_enableAxis`. + * + * If the axis index is out of range, nothing is done. + */ +void GameActivityPointerAxes_disableAxis(int32_t axis); + +/** + * \brief Get the value of the requested axis. + * + * Apart from X and Y, any axis that you want to read **must** be enabled first, + * using `GameActivityPointerAxes_enableAxis`. + * + * Find the valid enums for the axis (`AMOTION_EVENT_AXIS_X`, + * `AMOTION_EVENT_AXIS_Y`, `AMOTION_EVENT_AXIS_PRESSURE`...) + * in https://developer.android.com/ndk/reference/group/input. + * + * @param pointerInfo The structure containing information about the pointer, + * obtained from GameActivityMotionEvent. + * @param axis The axis to get the value from + * @return The value of the axis, or 0 if the axis is invalid or was not + * enabled. + */ +float GameActivityPointerAxes_getAxisValue( + const GameActivityPointerAxes* pointerInfo, int32_t axis); + +inline float GameActivityPointerAxes_getPressure( + const GameActivityPointerAxes* pointerInfo) { + return GameActivityPointerAxes_getAxisValue(pointerInfo, + AMOTION_EVENT_AXIS_PRESSURE); +} + +inline float GameActivityPointerAxes_getSize( + const GameActivityPointerAxes* pointerInfo) { + return GameActivityPointerAxes_getAxisValue(pointerInfo, + AMOTION_EVENT_AXIS_SIZE); +} + +inline float GameActivityPointerAxes_getTouchMajor( + const GameActivityPointerAxes* pointerInfo) { + return GameActivityPointerAxes_getAxisValue(pointerInfo, + AMOTION_EVENT_AXIS_TOUCH_MAJOR); +} + +inline float GameActivityPointerAxes_getTouchMinor( + const GameActivityPointerAxes* pointerInfo) { + return GameActivityPointerAxes_getAxisValue(pointerInfo, + AMOTION_EVENT_AXIS_TOUCH_MINOR); +} + +inline float GameActivityPointerAxes_getToolMajor( + const GameActivityPointerAxes* pointerInfo) { + return GameActivityPointerAxes_getAxisValue(pointerInfo, + AMOTION_EVENT_AXIS_TOOL_MAJOR); +} + +inline float GameActivityPointerAxes_getToolMinor( + const GameActivityPointerAxes* pointerInfo) { + return GameActivityPointerAxes_getAxisValue(pointerInfo, + AMOTION_EVENT_AXIS_TOOL_MINOR); +} + +inline float GameActivityPointerAxes_getOrientation( + const GameActivityPointerAxes* pointerInfo) { + return GameActivityPointerAxes_getAxisValue(pointerInfo, + AMOTION_EVENT_AXIS_ORIENTATION); +} + +/** + * The maximum number of pointers returned inside a motion event. + */ +#if (defined GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT_OVERRIDE) +#define GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT \ + GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT_OVERRIDE +#else +#define GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT 8 +#endif + +/** + * \brief Describe a motion event that happened on the GameActivity SurfaceView. + * + * This is 1:1 mapping to the information contained in a Java `MotionEvent` + * (see https://developer.android.com/reference/android/view/MotionEvent). + */ +typedef struct GameActivityMotionEvent { + int32_t deviceId; + int32_t source; + int32_t action; + + int64_t eventTime; + int64_t downTime; + + int32_t flags; + int32_t metaState; + + int32_t actionButton; + int32_t buttonState; + int32_t classification; + int32_t edgeFlags; + + uint32_t pointerCount; + GameActivityPointerAxes + pointers[GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT]; + + int historySize; + int64_t* historicalEventTimesMillis; + int64_t* historicalEventTimesNanos; + float* historicalAxisValues; + + float precisionX; + float precisionY; +} GameActivityMotionEvent; + +float GameActivityMotionEvent_getHistoricalAxisValue( + const GameActivityMotionEvent* event, int axis, int pointerIndex, + int historyPos); + +inline int GameActivityMotionEvent_getHistorySize( + const GameActivityMotionEvent* event) { + return event->historySize; +} + +inline float GameActivityMotionEvent_getHistoricalX( + const GameActivityMotionEvent* event, int pointerIndex, int historyPos) { + return GameActivityMotionEvent_getHistoricalAxisValue( + event, AMOTION_EVENT_AXIS_X, pointerIndex, historyPos); +} + +inline float GameActivityMotionEvent_getHistoricalY( + const GameActivityMotionEvent* event, int pointerIndex, int historyPos) { + return GameActivityMotionEvent_getHistoricalAxisValue( + event, AMOTION_EVENT_AXIS_Y, pointerIndex, historyPos); +} + +inline float GameActivityMotionEvent_getHistoricalPressure( + const GameActivityMotionEvent* event, int pointerIndex, int historyPos) { + return GameActivityMotionEvent_getHistoricalAxisValue( + event, AMOTION_EVENT_AXIS_PRESSURE, pointerIndex, historyPos); +} + +inline float GameActivityMotionEvent_getHistoricalSize( + const GameActivityMotionEvent* event, int pointerIndex, int historyPos) { + return GameActivityMotionEvent_getHistoricalAxisValue( + event, AMOTION_EVENT_AXIS_SIZE, pointerIndex, historyPos); +} + +inline float GameActivityMotionEvent_getHistoricalTouchMajor( + const GameActivityMotionEvent* event, int pointerIndex, int historyPos) { + return GameActivityMotionEvent_getHistoricalAxisValue( + event, AMOTION_EVENT_AXIS_TOUCH_MAJOR, pointerIndex, historyPos); +} + +inline float GameActivityMotionEvent_getHistoricalTouchMinor( + const GameActivityMotionEvent* event, int pointerIndex, int historyPos) { + return GameActivityMotionEvent_getHistoricalAxisValue( + event, AMOTION_EVENT_AXIS_TOUCH_MINOR, pointerIndex, historyPos); +} + +inline float GameActivityMotionEvent_getHistoricalToolMajor( + const GameActivityMotionEvent* event, int pointerIndex, int historyPos) { + return GameActivityMotionEvent_getHistoricalAxisValue( + event, AMOTION_EVENT_AXIS_TOOL_MAJOR, pointerIndex, historyPos); +} + +inline float GameActivityMotionEvent_getHistoricalToolMinor( + const GameActivityMotionEvent* event, int pointerIndex, int historyPos) { + return GameActivityMotionEvent_getHistoricalAxisValue( + event, AMOTION_EVENT_AXIS_TOOL_MINOR, pointerIndex, historyPos); +} + +inline float GameActivityMotionEvent_getHistoricalOrientation( + const GameActivityMotionEvent* event, int pointerIndex, int historyPos) { + return GameActivityMotionEvent_getHistoricalAxisValue( + event, AMOTION_EVENT_AXIS_ORIENTATION, pointerIndex, historyPos); +} + +/** \brief Handle the freeing of the GameActivityMotionEvent struct. */ +void GameActivityMotionEvent_destroy(GameActivityMotionEvent* c_event); + +/** + * \brief Convert a Java `MotionEvent` to a `GameActivityMotionEvent`. + * + * This is done automatically by the GameActivity: see `onTouchEvent` to set + * a callback to consume the received events. + * This function can be used if you re-implement events handling in your own + * activity. + * Ownership of out_event is maintained by the caller. + */ +void GameActivityMotionEvent_fromJava(JNIEnv* env, jobject motionEvent, + GameActivityMotionEvent* out_event); + +/** + * \brief Describe a key event that happened on the GameActivity SurfaceView. + * + * This is 1:1 mapping to the information contained in a Java `KeyEvent` + * (see https://developer.android.com/reference/android/view/KeyEvent). + * The only exception is the event times, which are reported as + * nanoseconds in this struct. + */ +typedef struct GameActivityKeyEvent { + int32_t deviceId; + int32_t source; + int32_t action; + + int64_t eventTime; + int64_t downTime; + + int32_t flags; + int32_t metaState; + + int32_t modifiers; + int32_t repeatCount; + int32_t keyCode; + int32_t scanCode; + //int32_t unicodeChar; +} GameActivityKeyEvent; + +/** + * \brief Convert a Java `KeyEvent` to a `GameActivityKeyEvent`. + * + * This is done automatically by the GameActivity: see `onKeyUp` and `onKeyDown` + * to set a callback to consume the received events. + * This function can be used if you re-implement events handling in your own + * activity. + * Ownership of out_event is maintained by the caller. + */ +void GameActivityKeyEvent_fromJava(JNIEnv* env, jobject motionEvent, + GameActivityKeyEvent* out_event); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif // ANDROID_GAME_SDK_GAME_ACTIVITY_EVENTS_H diff --git a/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivityLog.h b/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivityLog.h new file mode 100644 index 0000000..ba9a9e9 --- /dev/null +++ b/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/GameActivityLog.h @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 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. + */ +#ifndef ANDROID_GAME_SDK_GAME_ACTIVITY_LOG_H_ +#define ANDROID_GAME_SDK_GAME_ACTIVITY_LOG_H_ + +#define LOG_TAG "GameActivity" +#include + +#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(...) + +#endif // ANDROID_GAME_SDK_GAME_ACTIVITY_LOG_H_ diff --git a/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/native_app_glue/android_native_app_glue.c b/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/native_app_glue/android_native_app_glue.c new file mode 100644 index 0000000..a7bcb85 --- /dev/null +++ b/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/native_app_glue/android_native_app_glue.c @@ -0,0 +1,732 @@ +/* + * Copyright (C) 2021 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_native_app_glue.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#define NATIVE_APP_GLUE_MOTION_EVENTS_DEFAULT_BUF_SIZE 16 +#define NATIVE_APP_GLUE_KEY_EVENTS_DEFAULT_BUF_SIZE 4 + +#define LOGI(...) \ + ((void)__android_log_print(ANDROID_LOG_INFO, "threaded_app", __VA_ARGS__)) +#define LOGE(...) \ + ((void)__android_log_print(ANDROID_LOG_ERROR, "threaded_app", __VA_ARGS__)) +#define LOGW(...) \ + ((void)__android_log_print(ANDROID_LOG_WARN, "threaded_app", __VA_ARGS__)) +#define LOGW_ONCE(...) \ + do { \ + static bool alogw_once##__FILE__##__LINE__##__ = true; \ + if (alogw_once##__FILE__##__LINE__##__) { \ + alogw_once##__FILE__##__LINE__##__ = false; \ + LOGW(__VA_ARGS__); \ + } \ + } while (0) + +/* For debug builds, always enable the debug traces in this library */ +#ifndef NDEBUG +#define LOGV(...) \ + ((void)__android_log_print(ANDROID_LOG_VERBOSE, "threaded_app", \ + __VA_ARGS__)) +#else +#define LOGV(...) ((void)0) +#endif + +static void free_saved_state(struct android_app* android_app) { + pthread_mutex_lock(&android_app->mutex); + if (android_app->savedState != NULL) { + free(android_app->savedState); + android_app->savedState = NULL; + android_app->savedStateSize = 0; + } + pthread_mutex_unlock(&android_app->mutex); +} + +int8_t android_app_read_cmd(struct android_app* android_app) { + int8_t cmd; + if (read(android_app->msgread, &cmd, sizeof(cmd)) != sizeof(cmd)) { + LOGE("No data on command pipe!"); + return -1; + } + if (cmd == APP_CMD_SAVE_STATE) free_saved_state(android_app); + return cmd; +} + +static void print_cur_config(struct android_app* android_app) { + char lang[2], country[2]; + AConfiguration_getLanguage(android_app->config, lang); + AConfiguration_getCountry(android_app->config, country); + + LOGV( + "Config: mcc=%d mnc=%d lang=%c%c cnt=%c%c orien=%d touch=%d dens=%d " + "keys=%d nav=%d keysHid=%d navHid=%d sdk=%d size=%d long=%d " + "modetype=%d modenight=%d", + AConfiguration_getMcc(android_app->config), + AConfiguration_getMnc(android_app->config), lang[0], lang[1], + country[0], country[1], + AConfiguration_getOrientation(android_app->config), + AConfiguration_getTouchscreen(android_app->config), + AConfiguration_getDensity(android_app->config), + AConfiguration_getKeyboard(android_app->config), + AConfiguration_getNavigation(android_app->config), + AConfiguration_getKeysHidden(android_app->config), + AConfiguration_getNavHidden(android_app->config), + AConfiguration_getSdkVersion(android_app->config), + AConfiguration_getScreenSize(android_app->config), + AConfiguration_getScreenLong(android_app->config), + AConfiguration_getUiModeType(android_app->config), + AConfiguration_getUiModeNight(android_app->config)); +} + +void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) { + switch (cmd) { + case UNUSED_APP_CMD_INPUT_CHANGED: + LOGV("UNUSED_APP_CMD_INPUT_CHANGED"); + // Do nothing. This can be used in the future to handle AInputQueue + // natively, like done in NativeActivity. + break; + + case APP_CMD_INIT_WINDOW: + LOGV("APP_CMD_INIT_WINDOW"); + pthread_mutex_lock(&android_app->mutex); + android_app->window = android_app->pendingWindow; + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + break; + + case APP_CMD_TERM_WINDOW: + LOGV("APP_CMD_TERM_WINDOW"); + pthread_cond_broadcast(&android_app->cond); + break; + + case APP_CMD_RESUME: + case APP_CMD_START: + case APP_CMD_PAUSE: + case APP_CMD_STOP: + LOGV("activityState=%d", cmd); + pthread_mutex_lock(&android_app->mutex); + android_app->activityState = cmd; + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + break; + + case APP_CMD_CONFIG_CHANGED: + LOGV("APP_CMD_CONFIG_CHANGED"); + AConfiguration_fromAssetManager( + android_app->config, android_app->activity->assetManager); + print_cur_config(android_app); + break; + + case APP_CMD_DESTROY: + LOGV("APP_CMD_DESTROY"); + android_app->destroyRequested = 1; + break; + } +} + +void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd) { + switch (cmd) { + case APP_CMD_TERM_WINDOW: + LOGV("APP_CMD_TERM_WINDOW"); + pthread_mutex_lock(&android_app->mutex); + android_app->window = NULL; + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + break; + + case APP_CMD_SAVE_STATE: + LOGV("APP_CMD_SAVE_STATE"); + pthread_mutex_lock(&android_app->mutex); + android_app->stateSaved = 1; + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + break; + + case APP_CMD_RESUME: + free_saved_state(android_app); + break; + } +} + +void app_dummy() {} + +static void android_app_destroy(struct android_app* android_app) { + LOGV("android_app_destroy!"); + free_saved_state(android_app); + pthread_mutex_lock(&android_app->mutex); + + AConfiguration_delete(android_app->config); + android_app->destroyed = 1; + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + // Can't touch android_app object after this. +} + +static void process_cmd(struct android_app* app, + struct android_poll_source* source) { + int8_t cmd = android_app_read_cmd(app); + android_app_pre_exec_cmd(app, cmd); + if (app->onAppCmd != NULL) app->onAppCmd(app, cmd); + android_app_post_exec_cmd(app, cmd); +} + +// This is run on a separate thread (i.e: not the main thread). +static void* android_app_entry(void* param) { + struct android_app* android_app = (struct android_app*)param; + int input_buf_idx = 0; + + LOGV("android_app_entry called"); + android_app->config = AConfiguration_new(); + LOGV("android_app = %p", android_app); + LOGV("config = %p", android_app->config); + LOGV("activity = %p", android_app->activity); + LOGV("assetmanager = %p", android_app->activity->assetManager); + AConfiguration_fromAssetManager(android_app->config, + android_app->activity->assetManager); + + print_cur_config(android_app); + + /* initialize event buffers */ + for (input_buf_idx = 0; input_buf_idx < NATIVE_APP_GLUE_MAX_INPUT_BUFFERS; input_buf_idx++) { + struct android_input_buffer *buf = &android_app->inputBuffers[input_buf_idx]; + + buf->motionEventsBufferSize = NATIVE_APP_GLUE_MOTION_EVENTS_DEFAULT_BUF_SIZE; + buf->motionEvents = (GameActivityMotionEvent *) malloc(sizeof(GameActivityMotionEvent) * + buf->motionEventsBufferSize); + + buf->keyEventsBufferSize = NATIVE_APP_GLUE_KEY_EVENTS_DEFAULT_BUF_SIZE; + buf->keyEvents = (GameActivityKeyEvent *) malloc(sizeof(GameActivityKeyEvent) * + buf->keyEventsBufferSize); + } + + android_app->cmdPollSource.id = LOOPER_ID_MAIN; + android_app->cmdPollSource.app = android_app; + android_app->cmdPollSource.process = process_cmd; + + ALooper* looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS); + ALooper_addFd(looper, android_app->msgread, LOOPER_ID_MAIN, + ALOOPER_EVENT_INPUT, NULL, &android_app->cmdPollSource); + android_app->looper = looper; + + pthread_mutex_lock(&android_app->mutex); + android_app->running = 1; + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + + _rust_glue_entry(android_app); + + android_app_destroy(android_app); + return NULL; +} + +// Codes from https://developer.android.com/reference/android/view/KeyEvent +#define KEY_EVENT_KEYCODE_VOLUME_DOWN 25 +#define KEY_EVENT_KEYCODE_VOLUME_MUTE 164 +#define KEY_EVENT_KEYCODE_VOLUME_UP 24 +#define KEY_EVENT_KEYCODE_CAMERA 27 +#define KEY_EVENT_KEYCODE_ZOOM_IN 168 +#define KEY_EVENT_KEYCODE_ZOOM_OUT 169 + +// Double-buffer the key event filter to avoid race condition. +static bool default_key_filter(const GameActivityKeyEvent* event) { + // Ignore camera, volume, etc. buttons + return !(event->keyCode == KEY_EVENT_KEYCODE_VOLUME_DOWN || + event->keyCode == KEY_EVENT_KEYCODE_VOLUME_MUTE || + event->keyCode == KEY_EVENT_KEYCODE_VOLUME_UP || + event->keyCode == KEY_EVENT_KEYCODE_CAMERA || + event->keyCode == KEY_EVENT_KEYCODE_ZOOM_IN || + event->keyCode == KEY_EVENT_KEYCODE_ZOOM_OUT); +} + +// See +// https://developer.android.com/reference/android/view/InputDevice#SOURCE_TOUCHSCREEN +#define SOURCE_TOUCHSCREEN 0x00001002 + +static bool default_motion_filter(const GameActivityMotionEvent* event) { + // Ignore any non-touch events. + return event->source == SOURCE_TOUCHSCREEN; +} + +// -------------------------------------------------------------------- +// Native activity interaction (called from main thread) +// -------------------------------------------------------------------- + +static struct android_app* android_app_create(GameActivity* activity, + void* savedState, + size_t savedStateSize) { + // struct android_app* android_app = calloc(1, sizeof(struct android_app)); + struct android_app* android_app = + (struct android_app*)malloc(sizeof(struct android_app)); + memset(android_app, 0, sizeof(struct android_app)); + android_app->activity = activity; + + pthread_mutex_init(&android_app->mutex, NULL); + pthread_cond_init(&android_app->cond, NULL); + + if (savedState != NULL) { + android_app->savedState = malloc(savedStateSize); + android_app->savedStateSize = savedStateSize; + memcpy(android_app->savedState, savedState, savedStateSize); + } + + int msgpipe[2]; + if (pipe(msgpipe)) { + LOGE("could not create pipe: %s", strerror(errno)); + return NULL; + } + android_app->msgread = msgpipe[0]; + android_app->msgwrite = msgpipe[1]; + + android_app->keyEventFilter = default_key_filter; + android_app->motionEventFilter = default_motion_filter; + + LOGV("Launching android_app_entry in a thread"); + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + pthread_create(&android_app->thread, &attr, android_app_entry, android_app); + + // Wait for thread to start. + pthread_mutex_lock(&android_app->mutex); + while (!android_app->running) { + pthread_cond_wait(&android_app->cond, &android_app->mutex); + } + pthread_mutex_unlock(&android_app->mutex); + + return android_app; +} + +static void android_app_write_cmd(struct android_app* android_app, int8_t cmd) { + if (write(android_app->msgwrite, &cmd, sizeof(cmd)) != sizeof(cmd)) { + LOGE("Failure writing android_app cmd: %s", strerror(errno)); + } +} + +static void android_app_set_window(struct android_app* android_app, + ANativeWindow* window) { + LOGV("android_app_set_window called"); + pthread_mutex_lock(&android_app->mutex); + + // NB: we have to consider that the native thread could have already + // (gracefully) exit (setting android_app->destroyed) and so we need + // to be careful to avoid a deadlock waiting for a thread that's + // already exit. + if (android_app->destroyed) { + pthread_mutex_unlock(&android_app->mutex); + return; + } + if (android_app->pendingWindow != NULL) { + android_app_write_cmd(android_app, APP_CMD_TERM_WINDOW); + } + android_app->pendingWindow = window; + if (window != NULL) { + android_app_write_cmd(android_app, APP_CMD_INIT_WINDOW); + } + while (android_app->window != android_app->pendingWindow) { + pthread_cond_wait(&android_app->cond, &android_app->mutex); + } + pthread_mutex_unlock(&android_app->mutex); +} + +static void android_app_set_activity_state(struct android_app* android_app, + int8_t cmd) { + pthread_mutex_lock(&android_app->mutex); + + // NB: we have to consider that the native thread could have already + // (gracefully) exit (setting android_app->destroyed) and so we need + // to be careful to avoid a deadlock waiting for a thread that's + // already exit. + if (!android_app->destroyed) { + android_app_write_cmd(android_app, cmd); + while (android_app->activityState != cmd) { + pthread_cond_wait(&android_app->cond, &android_app->mutex); + } + } + pthread_mutex_unlock(&android_app->mutex); +} + +static void android_app_free(struct android_app* android_app) { + int input_buf_idx = 0; + + pthread_mutex_lock(&android_app->mutex); + + // It's possible that onDestroy is called after we have already 'destroyed' + // the app (via `android_app_destroy` due to `android_main` returning. + // + // In this case `->destroyed` will already be set (so we won't deadlock in + // the loop below) but we still need to close the messaging fds and finish + // freeing the android_app + + android_app_write_cmd(android_app, APP_CMD_DESTROY); + while (!android_app->destroyed) { + pthread_cond_wait(&android_app->cond, &android_app->mutex); + } + pthread_mutex_unlock(&android_app->mutex); + + for (input_buf_idx = 0; input_buf_idx < NATIVE_APP_GLUE_MAX_INPUT_BUFFERS; input_buf_idx++) { + struct android_input_buffer *buf = &android_app->inputBuffers[input_buf_idx]; + + android_app_clear_motion_events(buf); + free(buf->motionEvents); + free(buf->keyEvents); + } + + close(android_app->msgread); + close(android_app->msgwrite); + pthread_cond_destroy(&android_app->cond); + pthread_mutex_destroy(&android_app->mutex); + free(android_app); +} + +static inline struct android_app* ToApp(GameActivity* activity) { + return (struct android_app*)activity->instance; +} + +static void onDestroy(GameActivity* activity) { + LOGV("Destroy: %p", activity); + android_app_free(ToApp(activity)); +} + +static void onStart(GameActivity* activity) { + LOGV("Start: %p", activity); + android_app_set_activity_state(ToApp(activity), APP_CMD_START); +} + +static void onResume(GameActivity* activity) { + LOGV("Resume: %p", activity); + android_app_set_activity_state(ToApp(activity), APP_CMD_RESUME); +} + +static void onSaveInstanceState(GameActivity* activity, + SaveInstanceStateRecallback recallback, + void* context) { + LOGV("SaveInstanceState: %p", activity); + + struct android_app* android_app = ToApp(activity); + pthread_mutex_lock(&android_app->mutex); + + // NB: we have to consider that the native thread could have already + // (gracefully) exit (setting android_app->destroyed) and so we need + // to be careful to avoid a deadlock waiting for a thread that's + // already exit. + if (android_app->destroyed) { + pthread_mutex_unlock(&android_app->mutex); + return; + } + + android_app->stateSaved = 0; + android_app_write_cmd(android_app, APP_CMD_SAVE_STATE); + while (!android_app->stateSaved) { + pthread_cond_wait(&android_app->cond, &android_app->mutex); + } + + if (android_app->savedState != NULL) { + // Tell the Java side about our state. + recallback((const char*)android_app->savedState, + android_app->savedStateSize, context); + // Now we can free it. + free(android_app->savedState); + android_app->savedState = NULL; + android_app->savedStateSize = 0; + } + + pthread_mutex_unlock(&android_app->mutex); +} + +static void onPause(GameActivity* activity) { + LOGV("Pause: %p", activity); + android_app_set_activity_state(ToApp(activity), APP_CMD_PAUSE); +} + +static void onStop(GameActivity* activity) { + LOGV("Stop: %p", activity); + android_app_set_activity_state(ToApp(activity), APP_CMD_STOP); +} + +static void onConfigurationChanged(GameActivity* activity) { + LOGV("ConfigurationChanged: %p", activity); + android_app_write_cmd(ToApp(activity), APP_CMD_CONFIG_CHANGED); +} + +static void onTrimMemory(GameActivity* activity, int level) { + LOGV("TrimMemory: %p %d", activity, level); + android_app_write_cmd(ToApp(activity), APP_CMD_LOW_MEMORY); +} + +static void onWindowFocusChanged(GameActivity* activity, bool focused) { + LOGV("WindowFocusChanged: %p -- %d", activity, focused); + android_app_write_cmd(ToApp(activity), + focused ? APP_CMD_GAINED_FOCUS : APP_CMD_LOST_FOCUS); +} + +static void onNativeWindowCreated(GameActivity* activity, + ANativeWindow* window) { + LOGV("NativeWindowCreated: %p -- %p", activity, window); + android_app_set_window(ToApp(activity), window); +} + +static void onNativeWindowDestroyed(GameActivity* activity, + ANativeWindow* window) { + LOGV("NativeWindowDestroyed: %p -- %p", activity, window); + android_app_set_window(ToApp(activity), NULL); +} + +static void onNativeWindowRedrawNeeded(GameActivity* activity, + ANativeWindow* window) { + LOGV("NativeWindowRedrawNeeded: %p -- %p", activity, window); + android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_REDRAW_NEEDED); +} + +static void onNativeWindowResized(GameActivity* activity, ANativeWindow* window, + int32_t width, int32_t height) { + LOGV("NativeWindowResized: %p -- %p ( %d x %d )", activity, window, width, + height); + android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_RESIZED); +} + +void android_app_set_motion_event_filter(struct android_app* app, + android_motion_event_filter filter) { + pthread_mutex_lock(&app->mutex); + app->motionEventFilter = filter; + pthread_mutex_unlock(&app->mutex); +} + +bool android_app_input_available_wake_up(struct android_app* app) { + pthread_mutex_lock(&app->mutex); + bool available = app->inputAvailableWakeUp; + app->inputAvailableWakeUp = false; + pthread_mutex_unlock(&app->mutex); + return available; +} + +// NB: should be called with the android_app->mutex held already +static void notifyInput(struct android_app* android_app) { + // Don't spam the mainloop with wake ups if we've already sent one + if (android_app->inputSwapPending) { + return; + } + + if (android_app->looper != NULL) { + // for the app thread to know why it received the wake() up + android_app->inputAvailableWakeUp = true; + android_app->inputSwapPending = true; + ALooper_wake(android_app->looper); + } +} + +static bool onTouchEvent(GameActivity* activity, + const GameActivityMotionEvent* event) { + struct android_app* android_app = ToApp(activity); + pthread_mutex_lock(&android_app->mutex); + + // NB: we have to consider that the native thread could have already + // (gracefully) exit (setting android_app->destroyed) and so we need + // to be careful to avoid a deadlock waiting for a thread that's + // already exit. + if (android_app->destroyed) { + pthread_mutex_unlock(&android_app->mutex); + return false; + } + + if (android_app->motionEventFilter != NULL && + !android_app->motionEventFilter(event)) { + pthread_mutex_unlock(&android_app->mutex); + return false; + } + + struct android_input_buffer* inputBuffer = + &android_app->inputBuffers[android_app->currentInputBuffer]; + + // Add to the list of active motion events + if (inputBuffer->motionEventsCount >= inputBuffer->motionEventsBufferSize) { + inputBuffer->motionEventsBufferSize *= 2; + inputBuffer->motionEvents = (GameActivityMotionEvent *) realloc(inputBuffer->motionEvents, + sizeof(GameActivityMotionEvent) * inputBuffer->motionEventsBufferSize); + + if (inputBuffer->motionEvents == NULL) { + LOGE("onTouchEvent: out of memory"); + abort(); + } + } + + int new_ix = inputBuffer->motionEventsCount; + memcpy(&inputBuffer->motionEvents[new_ix], event, sizeof(GameActivityMotionEvent)); + ++inputBuffer->motionEventsCount; + notifyInput(android_app); + + pthread_mutex_unlock(&android_app->mutex); + return true; +} + +struct android_input_buffer* android_app_swap_input_buffers( + struct android_app* android_app) { + pthread_mutex_lock(&android_app->mutex); + + struct android_input_buffer* inputBuffer = + &android_app->inputBuffers[android_app->currentInputBuffer]; + + if (inputBuffer->motionEventsCount == 0 && + inputBuffer->keyEventsCount == 0) { + inputBuffer = NULL; + } else { + android_app->currentInputBuffer = + (android_app->currentInputBuffer + 1) % + NATIVE_APP_GLUE_MAX_INPUT_BUFFERS; + } + + android_app->inputSwapPending = false; + android_app->inputAvailableWakeUp = false; + + pthread_mutex_unlock(&android_app->mutex); + + return inputBuffer; +} + +void android_app_clear_motion_events(struct android_input_buffer* inputBuffer) { + // We do not need to lock here if the inputBuffer has already been swapped + // as is handled by the game loop thread + while (inputBuffer->motionEventsCount > 0) { + GameActivityMotionEvent_destroy( + &inputBuffer->motionEvents[inputBuffer->motionEventsCount - 1]); + + inputBuffer->motionEventsCount--; + } + assert(inputBuffer->motionEventsCount == 0); +} + +void android_app_set_key_event_filter(struct android_app* app, + android_key_event_filter filter) { + pthread_mutex_lock(&app->mutex); + app->keyEventFilter = filter; + pthread_mutex_unlock(&app->mutex); +} + +static bool onKey(GameActivity* activity, const GameActivityKeyEvent* event) { + struct android_app* android_app = ToApp(activity); + pthread_mutex_lock(&android_app->mutex); + + // NB: we have to consider that the native thread could have already + // (gracefully) exit (setting android_app->destroyed) and so we need + // to be careful to avoid a deadlock waiting for a thread that's + // already exit. + if (android_app->destroyed) { + pthread_mutex_unlock(&android_app->mutex); + return false; + } + + if (android_app->keyEventFilter != NULL && + !android_app->keyEventFilter(event)) { + pthread_mutex_unlock(&android_app->mutex); + return false; + } + + struct android_input_buffer* inputBuffer = + &android_app->inputBuffers[android_app->currentInputBuffer]; + + // Add to the list of active key down events + if (inputBuffer->keyEventsCount >= inputBuffer->keyEventsBufferSize) { + inputBuffer->keyEventsBufferSize = inputBuffer->keyEventsBufferSize * 2; + inputBuffer->keyEvents = (GameActivityKeyEvent *) realloc(inputBuffer->keyEvents, + sizeof(GameActivityKeyEvent) * inputBuffer->keyEventsBufferSize); + + if (inputBuffer->keyEvents == NULL) { + LOGE("onKey: out of memory"); + abort(); + } + } + + int new_ix = inputBuffer->keyEventsCount; + memcpy(&inputBuffer->keyEvents[new_ix], event, sizeof(GameActivityKeyEvent)); + ++inputBuffer->keyEventsCount; + notifyInput(android_app); + + pthread_mutex_unlock(&android_app->mutex); + return true; +} + +void android_app_clear_key_events(struct android_input_buffer* inputBuffer) { + inputBuffer->keyEventsCount = 0; +} + +static void onTextInputEvent(GameActivity* activity, + const GameTextInputState* state) { + struct android_app* android_app = ToApp(activity); + pthread_mutex_lock(&android_app->mutex); + if (!android_app->destroyed) { + android_app->textInputState = 1; + notifyInput(android_app); + } + pthread_mutex_unlock(&android_app->mutex); +} + +static void onWindowInsetsChanged(GameActivity* activity) { + LOGV("WindowInsetsChanged: %p", activity); + android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_INSETS_CHANGED); +} + +static void onContentRectChanged(GameActivity* activity, const ARect *rect) { + LOGV("ContentRectChanged: %p -- (%d %d) (%d %d)", activity, rect->left, rect->top, + rect->right, rect->bottom); + + struct android_app* android_app = ToApp(activity); + + pthread_mutex_lock(&android_app->mutex); + android_app->contentRect = *rect; + + android_app_write_cmd(android_app, APP_CMD_CONTENT_RECT_CHANGED); + pthread_mutex_unlock(&android_app->mutex); +} + +// 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 +// +JNIEXPORT +void GameActivity_onCreate_C(GameActivity* activity, void* savedState, + size_t savedStateSize) { + LOGV("Creating: %p", activity); + activity->callbacks->onDestroy = onDestroy; + activity->callbacks->onStart = onStart; + activity->callbacks->onResume = onResume; + activity->callbacks->onSaveInstanceState = onSaveInstanceState; + activity->callbacks->onPause = onPause; + activity->callbacks->onStop = onStop; + activity->callbacks->onTouchEvent = onTouchEvent; + activity->callbacks->onKeyDown = onKey; + activity->callbacks->onKeyUp = onKey; + activity->callbacks->onTextInputEvent = onTextInputEvent; + activity->callbacks->onConfigurationChanged = onConfigurationChanged; + activity->callbacks->onTrimMemory = onTrimMemory; + activity->callbacks->onWindowFocusChanged = onWindowFocusChanged; + activity->callbacks->onNativeWindowCreated = onNativeWindowCreated; + activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed; + activity->callbacks->onNativeWindowRedrawNeeded = + onNativeWindowRedrawNeeded; + activity->callbacks->onNativeWindowResized = onNativeWindowResized; + activity->callbacks->onWindowInsetsChanged = onWindowInsetsChanged; + activity->callbacks->onContentRectChanged = onContentRectChanged; + LOGV("Callbacks set: %p", activity->callbacks); + + activity->instance = + android_app_create(activity, savedState, savedStateSize); +} diff --git a/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/native_app_glue/android_native_app_glue.h b/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/native_app_glue/android_native_app_glue.h new file mode 100644 index 0000000..6fa5721 --- /dev/null +++ b/android-activity/android-games-sdk/game-activity/prefab-src/modules/game-activity/include/game-activity/native_app_glue/android_native_app_glue.h @@ -0,0 +1,507 @@ +/* + * Copyright (C) 2021 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. + */ + +#pragma once + +/** + * @addtogroup android_native_app_glue Native App Glue library + * The glue library to interface your game loop with GameActivity. + * @{ + */ + +#include +#include +#include +#include +#include + +#include "game-activity/GameActivity.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * The GameActivity interface provided by + * is based on a set of application-provided callbacks that will be called + * by the Activity's main thread when certain events occur. + * + * This means that each one of this callbacks _should_ _not_ block, or they + * risk having the system force-close the application. This programming + * model is direct, lightweight, but constraining. + * + * The 'android_native_app_glue' static library is used to provide a different + * execution model where the application can implement its own main event + * loop in a different thread instead. Here's how it works: + * + * 1/ The application must provide a function named "android_main()" that + * will be called when the activity is created, in a new thread that is + * distinct from the activity's main thread. + * + * 2/ android_main() receives a pointer to a valid "android_app" structure + * that contains references to other important objects, e.g. the + * GameActivity obejct instance the application is running in. + * + * 3/ the "android_app" object holds an ALooper instance that already + * listens to activity lifecycle events (e.g. "pause", "resume"). + * See APP_CMD_XXX declarations below. + * + * This corresponds to an ALooper identifier returned by + * ALooper_pollOnce with value LOOPER_ID_MAIN. + * + * Your application can use the same ALooper to listen to additional + * file-descriptors. They can either be callback based, or with return + * identifiers starting with LOOPER_ID_USER. + * + * 4/ Whenever you receive a LOOPER_ID_MAIN event, + * the returned data will point to an android_poll_source structure. You + * can call the process() function on it, and fill in android_app->onAppCmd + * to be called for your own processing of the event. + * + * Alternatively, you can call the low-level functions to read and process + * the data directly... look at the process_cmd() and process_input() + * implementations in the glue to see how to do this. + * + * See the sample named "native-activity" that comes with the NDK with a + * full usage example. Also look at the documentation of GameActivity. + */ + +struct android_app; + +/** + * Data associated with an ALooper fd that will be returned as the "outData" + * when that source has data ready. + */ +struct android_poll_source { + /** + * The identifier of this source. May be LOOPER_ID_MAIN or + * LOOPER_ID_INPUT. + */ + int32_t id; + + /** The android_app this ident is associated with. */ + struct android_app* app; + + /** + * Function to call to perform the standard processing of data from + * this source. + */ + void (*process)(struct android_app* app, + struct android_poll_source* source); +}; + +struct android_input_buffer { + /** + * Pointer to a read-only array of GameActivityMotionEvent. + * Only the first motionEventsCount events are valid. + */ + GameActivityMotionEvent *motionEvents; + + /** + * The number of valid motion events in `motionEvents`. + */ + uint64_t motionEventsCount; + + /** + * The size of the `motionEvents` buffer. + */ + uint64_t motionEventsBufferSize; + + /** + * Pointer to a read-only array of GameActivityKeyEvent. + * Only the first keyEventsCount events are valid. + */ + GameActivityKeyEvent *keyEvents; + + /** + * The number of valid "Key" events in `keyEvents`. + */ + uint64_t keyEventsCount; + + /** + * The size of the `keyEvents` buffer. + */ + uint64_t keyEventsBufferSize; +}; + +/** + * Function pointer declaration for the filtering of key events. + * A function with this signature should be passed to + * android_app_set_key_event_filter and return false for any events that should + * not be handled by android_native_app_glue. These events will be handled by + * the system instead. + */ +typedef bool (*android_key_event_filter)(const GameActivityKeyEvent*); + +/** + * Function pointer definition for the filtering of motion events. + * A function with this signature should be passed to + * android_app_set_motion_event_filter and return false for any events that + * should not be handled by android_native_app_glue. These events will be + * handled by the system instead. + */ +typedef bool (*android_motion_event_filter)(const GameActivityMotionEvent*); + +/** + * This is the interface for the standard glue code of a threaded + * application. In this model, the application's code is running + * in its own thread separate from the main thread of the process. + * It is not required that this thread be associated with the Java + * VM, although it will need to be in order to make JNI calls any + * Java objects. + */ +struct android_app { + /** + * An optional pointer to application-defined state. + */ + void* userData; + + /** + * A required callback for processing main app commands (`APP_CMD_*`). + * This is called each frame if there are app commands that need processing. + */ + void (*onAppCmd)(struct android_app* app, int32_t cmd); + + /** The GameActivity object instance that this app is running in. */ + GameActivity* activity; + + /** The current configuration the app is running in. */ + AConfiguration* config; + + /** + * The last activity saved state, as provided at creation time. + * It is NULL if there was no state. You can use this as you need; the + * memory will remain around until you call android_app_exec_cmd() for + * APP_CMD_RESUME, at which point it will be freed and savedState set to + * NULL. These variables should only be changed when processing a + * APP_CMD_SAVE_STATE, at which point they will be initialized to NULL and + * you can malloc your state and place the information here. In that case + * the memory will be freed for you later. + */ + void* savedState; + + /** + * The size of the activity saved state. It is 0 if `savedState` is NULL. + */ + size_t savedStateSize; + + /** The ALooper associated with the app's thread. */ + ALooper* looper; + + /** When non-NULL, this is the window surface that the app can draw in. */ + ANativeWindow* window; + + /** + * Current content rectangle of the window; this is the area where the + * window's content should be placed to be seen by the user. + */ + ARect contentRect; + + /** + * Current state of the app's activity. May be either APP_CMD_START, + * APP_CMD_RESUME, APP_CMD_PAUSE, or APP_CMD_STOP. + */ + int activityState; + + /** + * This is non-zero when the application's GameActivity is being + * destroyed and waiting for the app thread to complete. + */ + int destroyRequested; + +#define NATIVE_APP_GLUE_MAX_INPUT_BUFFERS 2 + + /** + * This is used for buffering input from GameActivity. Once ready, the + * application thread switches the buffers and processes what was + * accumulated. + */ + struct android_input_buffer inputBuffers[NATIVE_APP_GLUE_MAX_INPUT_BUFFERS]; + + int currentInputBuffer; + + /** + * 0 if no text input event is outstanding, 1 if it is. + * Use `GameActivity_getTextInputState` to get information + * about the text entered by the user. + */ + int textInputState; + + // Below are "private" implementation of the glue code. + /** @cond INTERNAL */ + + pthread_mutex_t mutex; + pthread_cond_t cond; + + int msgread; + int msgwrite; + + pthread_t thread; + + struct android_poll_source cmdPollSource; + + int running; + int stateSaved; + int destroyed; + int redrawNeeded; + ANativeWindow* pendingWindow; + ARect pendingContentRect; + + android_key_event_filter keyEventFilter; + android_motion_event_filter motionEventFilter; + + // When new input is received we set both of these flags and use the looper to + // wake up the application mainloop. + // + // To avoid spamming the mainloop with wake ups from lots of input though we + // don't sent a wake up if the inputSwapPending flag is already set. (i.e. + // we already expect input to be processed in a finite amount of time due to + // our previous wake up) + // + // When a wake up is received then we will check this flag (clearing it + // at the same time). If it was set then an InputAvailable event is sent to + // the application - which should lead to all input being processed within + // a finite amount of time. + // + // The next time android_app_swap_input_buffers is called, both flags will be + // cleared. + // + // NB: both of these should only be read with the app mutex held + bool inputAvailableWakeUp; + bool inputSwapPending; + + /** @endcond */ +}; + +/** + * Looper ID of commands coming from the app's main thread, an AInputQueue or + * user-defined sources. + */ +enum NativeAppGlueLooperId { + /** + * Looper data ID of commands coming from the app's main thread, which + * is returned as an identifier from ALooper_pollOnce(). The data for this + * identifier is a pointer to an android_poll_source structure. + * These can be retrieved and processed with android_app_read_cmd() + * and android_app_exec_cmd(). + */ + LOOPER_ID_MAIN = 1, + + /** + * Unused. Reserved for future use when usage of AInputQueue will be + * supported. + */ + LOOPER_ID_INPUT = 2, + + /** + * Start of user-defined ALooper identifiers. + */ + LOOPER_ID_USER = 3, +}; + +/** + * Commands passed from the application's main Java thread to the game's thread. + */ +enum NativeAppGlueAppCmd { + /** + * Unused. Reserved for future use when usage of AInputQueue will be + * supported. + */ + UNUSED_APP_CMD_INPUT_CHANGED, + + /** + * Command from main thread: a new ANativeWindow is ready for use. Upon + * receiving this command, android_app->window will contain the new window + * surface. + */ + APP_CMD_INIT_WINDOW, + + /** + * Command from main thread: the existing ANativeWindow needs to be + * terminated. Upon receiving this command, android_app->window still + * contains the existing window; after calling android_app_exec_cmd + * it will be set to NULL. + */ + APP_CMD_TERM_WINDOW, + + /** + * Command from main thread: the current ANativeWindow has been resized. + * Please redraw with its new size. + */ + APP_CMD_WINDOW_RESIZED, + + /** + * Command from main thread: the system needs that the current ANativeWindow + * be redrawn. You should redraw the window before handing this to + * android_app_exec_cmd() in order to avoid transient drawing glitches. + */ + APP_CMD_WINDOW_REDRAW_NEEDED, + + /** + * Command from main thread: the content area of the window has changed, + * such as from the soft input window being shown or hidden. You can + * find the new content rect in android_app::contentRect. + */ + APP_CMD_CONTENT_RECT_CHANGED, + + /** + * Command from main thread: the app's activity window has gained + * input focus. + */ + APP_CMD_GAINED_FOCUS, + + /** + * Command from main thread: the app's activity window has lost + * input focus. + */ + APP_CMD_LOST_FOCUS, + + /** + * Command from main thread: the current device configuration has changed. + */ + APP_CMD_CONFIG_CHANGED, + + /** + * Command from main thread: the system is running low on memory. + * Try to reduce your memory use. + */ + APP_CMD_LOW_MEMORY, + + /** + * Command from main thread: the app's activity has been started. + */ + APP_CMD_START, + + /** + * Command from main thread: the app's activity has been resumed. + */ + APP_CMD_RESUME, + + /** + * Command from main thread: the app should generate a new saved state + * for itself, to restore from later if needed. If you have saved state, + * allocate it with malloc and place it in android_app.savedState with + * the size in android_app.savedStateSize. The will be freed for you + * later. + */ + APP_CMD_SAVE_STATE, + + /** + * Command from main thread: the app's activity has been paused. + */ + APP_CMD_PAUSE, + + /** + * Command from main thread: the app's activity has been stopped. + */ + APP_CMD_STOP, + + /** + * Command from main thread: the app's activity is being destroyed, + * and waiting for the app thread to clean up and exit before proceeding. + */ + APP_CMD_DESTROY, + + /** + * Command from main thread: the app's insets have changed. + */ + APP_CMD_WINDOW_INSETS_CHANGED, + +}; + +/** + * Call when ALooper_pollAll() returns LOOPER_ID_MAIN, reading the next + * app command message. + */ +int8_t android_app_read_cmd(struct android_app* android_app); + +/** + * Call with the command returned by android_app_read_cmd() to do the + * initial pre-processing of the given command. You can perform your own + * actions for the command after calling this function. + */ +void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd); + +/** + * Call with the command returned by android_app_read_cmd() to do the + * final post-processing of the given command. You must have done your own + * actions for the command before calling this function. + */ +void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd); + +/** + * Call this before processing input events to get the events buffer. + * The function returns NULL if there are no events to process. + */ +struct android_input_buffer* android_app_swap_input_buffers( + struct android_app* android_app); + +/** + * Clear the array of motion events that were waiting to be handled, and release + * each of them. + * + * This method should be called after you have processed the motion events in + * your game loop. You should handle events at each iteration of your game loop. + */ +void android_app_clear_motion_events(struct android_input_buffer* inputBuffer); + +/** + * Clear the array of key events that were waiting to be handled, and release + * each of them. + * + * This method should be called after you have processed the key up events in + * your game loop. You should handle events at each iteration of your game loop. + */ +void android_app_clear_key_events(struct android_input_buffer* inputBuffer); + +/** + * This is a springboard into the Rust glue layer that wraps calling the + * main entry for the app itself. + */ +extern void _rust_glue_entry(struct android_app* app); + +/** + * Set the filter to use when processing key events. + * Any events for which the filter returns false will be ignored by + * android_native_app_glue. If filter is set to NULL, no filtering is done. + * + * The default key filter will filter out volume and camera button presses. + */ +void android_app_set_key_event_filter(struct android_app* app, + android_key_event_filter filter); + +/** + * Set the filter to use when processing touch and motion events. + * Any events for which the filter returns false will be ignored by + * android_native_app_glue. If filter is set to NULL, no filtering is done. + * + * Note that the default motion event filter will only allow touchscreen events + * through, in order to mimic NativeActivity's behaviour, so for controller + * events to be passed to the app, set the filter to NULL. + */ +void android_app_set_motion_event_filter(struct android_app* app, + android_motion_event_filter filter); + +/** + * Determines if a looper wake up was due to new input becoming available + */ +bool android_app_input_available_wake_up(struct android_app* app); + +#ifdef __cplusplus +} +#endif + +/** @} */ diff --git a/android-activity/android-games-sdk/game-text-input/prefab-src/modules/game-text-input/include/game-text-input/gamecommon.h b/android-activity/android-games-sdk/game-text-input/prefab-src/modules/game-text-input/include/game-text-input/gamecommon.h new file mode 100644 index 0000000..38bffff --- /dev/null +++ b/android-activity/android-games-sdk/game-text-input/prefab-src/modules/game-text-input/include/game-text-input/gamecommon.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2021 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. + */ + +/** + * @defgroup game_common Game Common + * Common structures and functions used within AGDK + * @{ + */ + +#pragma once + +/** + * The type of a component for which to retrieve insets. See + * https://developer.android.com/reference/androidx/core/view/WindowInsetsCompat.Type + */ +typedef enum GameCommonInsetsType { + GAMECOMMON_INSETS_TYPE_CAPTION_BAR = 0, + GAMECOMMON_INSETS_TYPE_DISPLAY_CUTOUT, + GAMECOMMON_INSETS_TYPE_IME, + GAMECOMMON_INSETS_TYPE_MANDATORY_SYSTEM_GESTURES, + GAMECOMMON_INSETS_TYPE_NAVIGATION_BARS, + GAMECOMMON_INSETS_TYPE_STATUS_BARS, + GAMECOMMON_INSETS_TYPE_SYSTEM_BARS, + GAMECOMMON_INSETS_TYPE_SYSTEM_GESTURES, + GAMECOMMON_INSETS_TYPE_TAPABLE_ELEMENT, + GAMECOMMON_INSETS_TYPE_WATERFALL, + GAMECOMMON_INSETS_TYPE_COUNT +} GameCommonInsetsType; diff --git a/android-activity/android-games-sdk/game-text-input/prefab-src/modules/game-text-input/include/game-text-input/gametextinput.cpp b/android-activity/android-games-sdk/game-text-input/prefab-src/modules/game-text-input/include/game-text-input/gametextinput.cpp new file mode 100644 index 0000000..b8244fb --- /dev/null +++ b/android-activity/android-games-sdk/game-text-input/prefab-src/modules/game-text-input/include/game-text-input/gametextinput.cpp @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2021 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 "game-text-input/gametextinput.h" + +#include +#include +#include +#include + +#include +#include +#include + +#define LOG_TAG "GameTextInput" + +static constexpr int32_t DEFAULT_MAX_STRING_SIZE = 1 << 16; + +// Cache of field ids in the Java GameTextInputState class +struct StateClassInfo { + jfieldID text; + jfieldID selectionStart; + jfieldID selectionEnd; + jfieldID composingRegionStart; + jfieldID composingRegionEnd; +}; + +// Main GameTextInput object. +struct GameTextInput { + public: + GameTextInput(JNIEnv *env, uint32_t max_string_size); + ~GameTextInput(); + void setState(const GameTextInputState &state); + const GameTextInputState &getState() const { return currentState_; } + void setInputConnection(jobject inputConnection); + void processEvent(jobject textInputEvent); + void showIme(uint32_t flags); + void hideIme(uint32_t flags); + void restartInput(); + void setEventCallback(GameTextInputEventCallback callback, void *context); + jobject stateToJava(const GameTextInputState &state) const; + void stateFromJava(jobject textInputEvent, + GameTextInputGetStateCallback callback, + void *context) const; + void setImeInsetsCallback(GameTextInputImeInsetsCallback callback, + void *context); + void processImeInsets(const ARect *insets); + const ARect &getImeInsets() const { return currentInsets_; } + + private: + // Copy string and set other fields + void setStateInner(const GameTextInputState &state); + static void processCallback(void *context, const GameTextInputState *state); + JNIEnv *env_ = nullptr; + // Cached at initialization from + // com/google/androidgamesdk/gametextinput/State. + jclass stateJavaClass_ = nullptr; + // The latest text input update. + GameTextInputState currentState_ = {}; + // An instance of gametextinput.InputConnection. + jclass inputConnectionClass_ = nullptr; + jobject inputConnection_ = nullptr; + jmethodID inputConnectionSetStateMethod_; + jmethodID setSoftKeyboardActiveMethod_; + jmethodID restartInputMethod_; + void (*eventCallback_)(void *context, + const struct GameTextInputState *state) = nullptr; + void *eventCallbackContext_ = nullptr; + void (*insetsCallback_)(void *context, + const struct ARect *insets) = nullptr; + ARect currentInsets_ = {}; + void *insetsCallbackContext_ = nullptr; + StateClassInfo stateClassInfo_ = {}; + // Constant-sized buffer used to store state text. + std::vector stateStringBuffer_; +}; + +std::unique_ptr s_gameTextInput; + +extern "C" { + +/////////////////////////////////////////////////////////// +/// GameTextInputState C Functions +/////////////////////////////////////////////////////////// + +// Convert to a Java structure. +jobject currentState_toJava(const GameTextInput *gameTextInput, + const GameTextInputState *state) { + if (state == nullptr) return NULL; + return gameTextInput->stateToJava(*state); +} + +// Convert from Java structure. +void currentState_fromJava(const GameTextInput *gameTextInput, + jobject textInputEvent, + GameTextInputGetStateCallback callback, + void *context) { + gameTextInput->stateFromJava(textInputEvent, callback, context); +} + +/////////////////////////////////////////////////////////// +/// GameTextInput C Functions +/////////////////////////////////////////////////////////// + +struct GameTextInput *GameTextInput_init(JNIEnv *env, + uint32_t max_string_size) { + if (s_gameTextInput.get() != nullptr) { + __android_log_print(ANDROID_LOG_WARN, LOG_TAG, + "Warning: called GameTextInput_init twice without " + "calling GameTextInput_destroy"); + return s_gameTextInput.get(); + } + // Don't use make_unique, for C++11 compatibility + s_gameTextInput = + std::unique_ptr(new GameTextInput(env, max_string_size)); + return s_gameTextInput.get(); +} + +void GameTextInput_destroy(GameTextInput *input) { + if (input == nullptr || s_gameTextInput.get() == nullptr) return; + s_gameTextInput.reset(); +} + +void GameTextInput_setState(GameTextInput *input, + const GameTextInputState *state) { + if (state == nullptr) return; + input->setState(*state); +} + +void GameTextInput_getState(GameTextInput *input, + GameTextInputGetStateCallback callback, + void *context) { + callback(context, &input->getState()); +} + +void GameTextInput_setInputConnection(GameTextInput *input, + jobject inputConnection) { + input->setInputConnection(inputConnection); +} + +void GameTextInput_processEvent(GameTextInput *input, jobject textInputEvent) { + input->processEvent(textInputEvent); +} + +void GameTextInput_processImeInsets(GameTextInput *input, const ARect *insets) { + input->processImeInsets(insets); +} + +void GameTextInput_showIme(struct GameTextInput *input, uint32_t flags) { + input->showIme(flags); +} + +void GameTextInput_hideIme(struct GameTextInput *input, uint32_t flags) { + input->hideIme(flags); +} + +void GameTextInput_restartInput(struct GameTextInput *input) { + input->restartInput(); +} + +void GameTextInput_setEventCallback(struct GameTextInput *input, + GameTextInputEventCallback callback, + void *context) { + input->setEventCallback(callback, context); +} + +void GameTextInput_setImeInsetsCallback(struct GameTextInput *input, + GameTextInputImeInsetsCallback callback, + void *context) { + input->setImeInsetsCallback(callback, context); +} + +void GameTextInput_getImeInsets(const GameTextInput *input, ARect *insets) { + *insets = input->getImeInsets(); +} + +} // extern "C" + +/////////////////////////////////////////////////////////// +/// GameTextInput C++ class Implementation +/////////////////////////////////////////////////////////// + +GameTextInput::GameTextInput(JNIEnv *env, uint32_t max_string_size) + : env_(env), + stateStringBuffer_(max_string_size == 0 ? DEFAULT_MAX_STRING_SIZE + : max_string_size) { + stateJavaClass_ = (jclass)env_->NewGlobalRef( + env_->FindClass("com/google/androidgamesdk/gametextinput/State")); + inputConnectionClass_ = (jclass)env_->NewGlobalRef(env_->FindClass( + "com/google/androidgamesdk/gametextinput/InputConnection")); + inputConnectionSetStateMethod_ = + env_->GetMethodID(inputConnectionClass_, "setState", + "(Lcom/google/androidgamesdk/gametextinput/State;)V"); + setSoftKeyboardActiveMethod_ = env_->GetMethodID( + inputConnectionClass_, "setSoftKeyboardActive", "(ZI)V"); + restartInputMethod_ = + env_->GetMethodID(inputConnectionClass_, "restartInput", "()V"); + + stateClassInfo_.text = + env_->GetFieldID(stateJavaClass_, "text", "Ljava/lang/String;"); + stateClassInfo_.selectionStart = + env_->GetFieldID(stateJavaClass_, "selectionStart", "I"); + stateClassInfo_.selectionEnd = + env_->GetFieldID(stateJavaClass_, "selectionEnd", "I"); + stateClassInfo_.composingRegionStart = + env_->GetFieldID(stateJavaClass_, "composingRegionStart", "I"); + stateClassInfo_.composingRegionEnd = + env_->GetFieldID(stateJavaClass_, "composingRegionEnd", "I"); +} + +GameTextInput::~GameTextInput() { + if (stateJavaClass_ != NULL) { + env_->DeleteGlobalRef(stateJavaClass_); + stateJavaClass_ = NULL; + } + if (inputConnectionClass_ != NULL) { + env_->DeleteGlobalRef(inputConnectionClass_); + inputConnectionClass_ = NULL; + } + if (inputConnection_ != NULL) { + env_->DeleteGlobalRef(inputConnection_); + inputConnection_ = NULL; + } +} + +void GameTextInput::setState(const GameTextInputState &state) { + if (inputConnection_ == nullptr) return; + jobject jstate = stateToJava(state); + env_->CallVoidMethod(inputConnection_, inputConnectionSetStateMethod_, + jstate); + env_->DeleteLocalRef(jstate); + setStateInner(state); +} + +void GameTextInput::setStateInner(const GameTextInputState &state) { + // Check if we're setting using our own string (other parts may be + // different) + if (state.text_UTF8 == currentState_.text_UTF8) { + currentState_ = state; + return; + } + // Otherwise, copy across the string. + auto bytes_needed = + std::min(static_cast(state.text_length + 1), + static_cast(stateStringBuffer_.size())); + currentState_.text_UTF8 = stateStringBuffer_.data(); + std::copy(state.text_UTF8, state.text_UTF8 + bytes_needed - 1, + stateStringBuffer_.data()); + currentState_.text_length = state.text_length; + currentState_.selection = state.selection; + currentState_.composingRegion = state.composingRegion; + stateStringBuffer_[bytes_needed - 1] = 0; +} + +void GameTextInput::setInputConnection(jobject inputConnection) { + if (inputConnection_ != NULL) { + env_->DeleteGlobalRef(inputConnection_); + } + inputConnection_ = env_->NewGlobalRef(inputConnection); +} + +/*static*/ void GameTextInput::processCallback( + void *context, const GameTextInputState *state) { + auto thiz = static_cast(context); + if (state != nullptr) thiz->setStateInner(*state); +} + +void GameTextInput::processEvent(jobject textInputEvent) { + stateFromJava(textInputEvent, processCallback, this); + if (eventCallback_) { + eventCallback_(eventCallbackContext_, ¤tState_); + } +} + +void GameTextInput::showIme(uint32_t flags) { + if (inputConnection_ == nullptr) return; + env_->CallVoidMethod(inputConnection_, setSoftKeyboardActiveMethod_, true, + flags); +} + +void GameTextInput::setEventCallback(GameTextInputEventCallback callback, + void *context) { + eventCallback_ = callback; + eventCallbackContext_ = context; +} + +void GameTextInput::setImeInsetsCallback( + GameTextInputImeInsetsCallback callback, void *context) { + insetsCallback_ = callback; + insetsCallbackContext_ = context; +} + +void GameTextInput::processImeInsets(const ARect *insets) { + currentInsets_ = *insets; + if (insetsCallback_) { + insetsCallback_(insetsCallbackContext_, ¤tInsets_); + } +} + +void GameTextInput::hideIme(uint32_t flags) { + if (inputConnection_ == nullptr) return; + env_->CallVoidMethod(inputConnection_, setSoftKeyboardActiveMethod_, false, + flags); +} + +void GameTextInput::restartInput() { + if (inputConnection_ == nullptr) return; + env_->CallVoidMethod(inputConnection_, restartInputMethod_, false); +} + +jobject GameTextInput::stateToJava(const GameTextInputState &state) const { + static jmethodID constructor = nullptr; + if (constructor == nullptr) { + constructor = env_->GetMethodID(stateJavaClass_, "", + "(Ljava/lang/String;IIII)V"); + if (constructor == nullptr) { + __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, + "Can't find gametextinput.State constructor"); + return nullptr; + } + } + const char *text = state.text_UTF8; + if (text == nullptr) { + static char empty_string[] = ""; + text = empty_string; + } + // Note that this expects 'modified' UTF-8 which is not the same as UTF-8 + // https://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8 + jstring jtext = env_->NewStringUTF(text); + jobject jobj = + env_->NewObject(stateJavaClass_, constructor, jtext, + state.selection.start, state.selection.end, + state.composingRegion.start, state.composingRegion.end); + env_->DeleteLocalRef(jtext); + return jobj; +} + +void GameTextInput::stateFromJava(jobject textInputEvent, + GameTextInputGetStateCallback callback, + void *context) const { + jstring text = + (jstring)env_->GetObjectField(textInputEvent, stateClassInfo_.text); + // Note this is 'modified' UTF-8, not true UTF-8. It has no NULLs in it, + // except at the end. It's actually not specified whether the value returned + // by GetStringUTFChars includes a null at the end, but it *seems to* on + // Android. + const char *text_chars = env_->GetStringUTFChars(text, NULL); + int text_len = env_->GetStringUTFLength( + text); // Length in bytes, *not* including the null. + int selectionStart = + env_->GetIntField(textInputEvent, stateClassInfo_.selectionStart); + int selectionEnd = + env_->GetIntField(textInputEvent, stateClassInfo_.selectionEnd); + int composingRegionStart = + env_->GetIntField(textInputEvent, stateClassInfo_.composingRegionStart); + int composingRegionEnd = + env_->GetIntField(textInputEvent, stateClassInfo_.composingRegionEnd); + GameTextInputState state{text_chars, + text_len, + {selectionStart, selectionEnd}, + {composingRegionStart, composingRegionEnd}}; + callback(context, &state); + env_->ReleaseStringUTFChars(text, text_chars); + env_->DeleteLocalRef(text); +} diff --git a/android-activity/android-games-sdk/game-text-input/prefab-src/modules/game-text-input/include/game-text-input/gametextinput.h b/android-activity/android-games-sdk/game-text-input/prefab-src/modules/game-text-input/include/game-text-input/gametextinput.h new file mode 100644 index 0000000..8c33da5 --- /dev/null +++ b/android-activity/android-games-sdk/game-text-input/prefab-src/modules/game-text-input/include/game-text-input/gametextinput.h @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2021 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. + */ + +/** + * @defgroup game_text_input Game Text Input + * The interface to use GameTextInput. + * @{ + */ + +#pragma once + +#include +#include +#include + +#include "common/gamesdk_common.h" +#include "gamecommon.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define GAMETEXTINPUT_MAJOR_VERSION 2 +#define GAMETEXTINPUT_MINOR_VERSION 0 +#define GAMETEXTINPUT_BUGFIX_VERSION 0 +#define GAMETEXTINPUT_PACKED_VERSION \ + ANDROID_GAMESDK_PACKED_VERSION(GAMETEXTINPUT_MAJOR_VERSION, \ + GAMETEXTINPUT_MINOR_VERSION, \ + GAMETEXTINPUT_BUGFIX_VERSION) + +/** + * This struct holds a span within a region of text from start (inclusive) to + * end (exclusive). An empty span or cursor position is specified with + * start==end. An undefined span is specified with start = end = SPAN_UNDEFINED. + */ +typedef struct GameTextInputSpan { + /** The start of the region (inclusive). */ + int32_t start; + /** The end of the region (exclusive). */ + int32_t end; +} GameTextInputSpan; + +/** + * Values with special meaning in a GameTextInputSpan. + */ +enum GameTextInputSpanFlag { SPAN_UNDEFINED = -1 }; + +/** + * This struct holds the state of an editable section of text. + * The text can have a selection and a composing region defined on it. + * A composing region is used by IMEs that allow input using multiple steps to + * compose a glyph or word. Use functions GameTextInput_getState and + * GameTextInput_setState to read and modify the state that an IME is editing. + */ +typedef struct GameTextInputState { + /** + * Text owned by the state, as a modified UTF-8 string. Null-terminated. + * https://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8 + */ + const char *text_UTF8; + /** + * Length in bytes of text_UTF8, *not* including the null at end. + */ + int32_t text_length; + /** + * A selection defined on the text. + */ + GameTextInputSpan selection; + /** + * A composing region defined on the text. + */ + GameTextInputSpan composingRegion; +} GameTextInputState; + +/** + * A callback called by GameTextInput_getState. + * @param context User-defined context. + * @param state State, owned by the library, that will be valid for the duration + * of the callback. + */ +typedef void (*GameTextInputGetStateCallback)( + void *context, const struct GameTextInputState *state); + +/** + * Opaque handle to the GameTextInput API. + */ +typedef struct GameTextInput GameTextInput; + +/** + * Initialize the GameTextInput library. + * If called twice without GameTextInput_destroy being called, the same pointer + * will be returned and a warning will be issued. + * @param env A JNI env valid on the calling thread. + * @param max_string_size The maximum length of a string that can be edited. If + * zero, the maximum defaults to 65536 bytes. A buffer of this size is allocated + * at initialization. + * @return A handle to the library. + */ +GameTextInput *GameTextInput_init(JNIEnv *env, uint32_t max_string_size); + +/** + * When using GameTextInput, you need to create a gametextinput.InputConnection + * on the Java side and pass it using this function to the library, unless using + * GameActivity in which case this will be done for you. See the GameActivity + * source code or GameTextInput samples for examples of usage. + * @param input A valid GameTextInput library handle. + * @param inputConnection A gametextinput.InputConnection object. + */ +void GameTextInput_setInputConnection(GameTextInput *input, + jobject inputConnection); + +/** + * Unless using GameActivity, it is required to call this function from your + * Java gametextinput.Listener.stateChanged method to convert eventState and + * trigger any event callbacks. When using GameActivity, this does not need to + * be called as event processing is handled by the Activity. + * @param input A valid GameTextInput library handle. + * @param eventState A Java gametextinput.State object. + */ +void GameTextInput_processEvent(GameTextInput *input, jobject eventState); + +/** + * Free any resources owned by the GameTextInput library. + * Any subsequent calls to the library will fail until GameTextInput_init is + * called again. + * @param input A valid GameTextInput library handle. + */ +void GameTextInput_destroy(GameTextInput *input); + +/** + * Flags to be passed to GameTextInput_showIme. + */ +enum ShowImeFlags { + SHOW_IME_UNDEFINED = 0, // Default value. + SHOW_IMPLICIT = + 1, // Indicates that the user has forced the input method open so it + // should not be closed until they explicitly do so. + SHOW_FORCED = 2 // Indicates that this is an implicit request to show the + // input window, not as the result of a direct request by + // the user. The window may not be shown in this case. +}; + +/** + * Show the IME. Calls InputMethodManager.showSoftInput(). + * @param input A valid GameTextInput library handle. + * @param flags Defined in ShowImeFlags above. For more information see: + * https://developer.android.com/reference/android/view/inputmethod/InputMethodManager + */ +void GameTextInput_showIme(GameTextInput *input, uint32_t flags); + +/** + * Flags to be passed to GameTextInput_hideIme. + */ +enum HideImeFlags { + HIDE_IME_UNDEFINED = 0, // Default value. + HIDE_IMPLICIT_ONLY = + 1, // Indicates that the soft input window should only be hidden if it + // was not explicitly shown by the user. + HIDE_NOT_ALWAYS = + 2, // Indicates that the soft input window should normally be hidden, + // unless it was originally shown with SHOW_FORCED. +}; + +/** + * Show the IME. Calls InputMethodManager.hideSoftInputFromWindow(). + * @param input A valid GameTextInput library handle. + * @param flags Defined in HideImeFlags above. For more information see: + * https://developer.android.com/reference/android/view/inputmethod/InputMethodManager + */ +void GameTextInput_hideIme(GameTextInput *input, uint32_t flags); + +/** + * Restarts the input method. Calls InputMethodManager.restartInput(). + * @param input A valid GameTextInput library handle. + */ +void GameTextInput_restartInput(GameTextInput *input); + +/** + * Call a callback with the current GameTextInput state, which may have been + * modified by changes in the IME and calls to GameTextInput_setState. We use a + * callback rather than returning the state in order to simplify ownership of + * text_UTF8 strings. These strings are only valid during the calling of the + * callback. + * @param input A valid GameTextInput library handle. + * @param callback A function that will be called with valid state. + * @param context Context used by the callback. + */ +void GameTextInput_getState(GameTextInput *input, + GameTextInputGetStateCallback callback, + void *context); + +/** + * Set the current GameTextInput state. This state is reflected to any active + * IME. + * @param input A valid GameTextInput library handle. + * @param state The state to set. Ownership is maintained by the caller and must + * remain valid for the duration of the call. + */ +void GameTextInput_setState(GameTextInput *input, + const GameTextInputState *state); + +/** + * Type of the callback needed by GameTextInput_setEventCallback that will be + * called every time the IME state changes. + * @param context User-defined context set in GameTextInput_setEventCallback. + * @param current_state Current IME state, owned by the library and valid during + * the callback. + */ +typedef void (*GameTextInputEventCallback)( + void *context, const GameTextInputState *current_state); + +/** + * Optionally set a callback to be called whenever the IME state changes. + * Not necessary if you are using GameActivity, which handles these callbacks + * for you. + * @param input A valid GameTextInput library handle. + * @param callback Called by the library when the IME state changes. + * @param context Context passed as first argument to the callback. + */ +void GameTextInput_setEventCallback(GameTextInput *input, + GameTextInputEventCallback callback, + void *context); + +/** + * Type of the callback needed by GameTextInput_setImeInsetsCallback that will + * be called every time the IME window insets change. + * @param context User-defined context set in + * GameTextInput_setImeWIndowInsetsCallback. + * @param current_insets Current IME insets, owned by the library and valid + * during the callback. + */ +typedef void (*GameTextInputImeInsetsCallback)(void *context, + const ARect *current_insets); + +/** + * Optionally set a callback to be called whenever the IME insets change. + * Not necessary if you are using GameActivity, which handles these callbacks + * for you. + * @param input A valid GameTextInput library handle. + * @param callback Called by the library when the IME insets change. + * @param context Context passed as first argument to the callback. + */ +void GameTextInput_setImeInsetsCallback(GameTextInput *input, + GameTextInputImeInsetsCallback callback, + void *context); + +/** + * Get the current window insets for the IME. + * @param input A valid GameTextInput library handle. + * @param insets Filled with the current insets by this function. + */ +void GameTextInput_getImeInsets(const GameTextInput *input, ARect *insets); + +/** + * Unless using GameActivity, it is required to call this function from your + * Java gametextinput.Listener.onImeInsetsChanged method to + * trigger any event callbacks. When using GameActivity, this does not need to + * be called as insets processing is handled by the Activity. + * @param input A valid GameTextInput library handle. + * @param eventState A Java gametextinput.State object. + */ +void GameTextInput_processImeInsets(GameTextInput *input, const ARect *insets); + +/** + * Convert a GameTextInputState struct to a Java gametextinput.State object. + * Don't forget to delete the returned Java local ref when you're done. + * @param input A valid GameTextInput library handle. + * @param state Input state to convert. + * @return A Java object of class gametextinput.State. The caller is required to + * delete this local reference. + */ +jobject GameTextInputState_toJava(const GameTextInput *input, + const GameTextInputState *state); + +/** + * Convert from a Java gametextinput.State object into a C GameTextInputState + * struct. + * @param input A valid GameTextInput library handle. + * @param state A Java gametextinput.State object. + * @param callback A function called with the C struct, valid for the duration + * of the call. + * @param context Context passed to the callback. + */ +void GameTextInputState_fromJava(const GameTextInput *input, jobject state, + GameTextInputGetStateCallback callback, + void *context); + +#ifdef __cplusplus +} +#endif + +/** @} */ diff --git a/android-activity/android-games-sdk/include/common/gamesdk_common.h b/android-activity/android-games-sdk/include/common/gamesdk_common.h new file mode 100644 index 0000000..d29ac01 --- /dev/null +++ b/android-activity/android-games-sdk/include/common/gamesdk_common.h @@ -0,0 +1,41 @@ +/* + * Copyright 2020 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. + */ + +/* + * This is the main interface to the Android Performance Tuner library, also + * known as Tuning Fork. + * + * It is part of the Android Games SDK and produces best results when integrated + * with the Swappy Frame Pacing Library. + * + * See the documentation at + * https://developer.android.com/games/sdk/performance-tuner/custom-engine for + * more information on using this library in a native Android game. + * + */ + +#pragma once + +// There are separate versions for each GameSDK component that use this format: +#define ANDROID_GAMESDK_PACKED_VERSION(MAJOR, MINOR, BUGFIX) \ + ((MAJOR << 16) | (MINOR << 8) | (BUGFIX)) +// Accessors +#define ANDROID_GAMESDK_MAJOR_VERSION(PACKED) ((PACKED) >> 16) +#define ANDROID_GAMESDK_MINOR_VERSION(PACKED) (((PACKED) >> 8) & 0xff) +#define ANDROID_GAMESDK_BUGFIX_VERSION(PACKED) ((PACKED) & 0xff) + +#define AGDK_STRING_VERSION(MAJOR, MINOR, BUGFIX, GIT) \ +#MAJOR "." #MINOR "." #BUGFIX "." #GIT diff --git a/android-activity/build.rs b/android-activity/build.rs index 7fc92e7..42288f9 100644 --- a/android-activity/build.rs +++ b/android-activity/build.rs @@ -1,6 +1,9 @@ #![allow(dead_code)] fn build_glue_for_game_activity() { + let activity_basepath = "android-games-sdk/game-activity/prefab-src/modules/game-activity/include"; + let textinput_basepath = "android-games-sdk/game-text-input/prefab-src/modules/game-text-input/include"; + for f in [ "GameActivity.h", "GameActivity.cpp", @@ -8,34 +11,40 @@ fn build_glue_for_game_activity() { "GameActivityEvents.cpp", "GameActivityLog.h", ] { - println!("cargo:rerun-if-changed=game-activity-csrc/game-activity/{f}"); + println!("cargo:rerun-if-changed={activity_basepath}/game-activity/{f}"); } cc::Build::new() .cpp(true) - .include("game-activity-csrc") - .file("game-activity-csrc/game-activity/GameActivity.cpp") - .file("game-activity-csrc/game-activity/GameActivityEvents.cpp") + .include("android-games-sdk/include") + .include(activity_basepath) + .include(textinput_basepath) + .file(format!("{activity_basepath}/game-activity/GameActivity.cpp")) + .file(format!("{activity_basepath}/game-activity/GameActivityEvents.cpp")) .extra_warnings(false) .cpp_link_stdlib("c++_static") .compile("libgame_activity.a"); for f in ["gamecommon.h", "gametextinput.h", "gametextinput.cpp"] { - println!("cargo:rerun-if-changed=game-activity-csrc/game-text-input/{f}"); + println!("cargo:rerun-if-changed={textinput_basepath}/game-text-input/{f}"); } cc::Build::new() .cpp(true) - .include("game-activity-csrc") - .file("game-activity-csrc/game-text-input/gametextinput.cpp") + .include("android-games-sdk/include") + .include(textinput_basepath) + .file(format!("{textinput_basepath}/game-text-input/gametextinput.cpp")) .cpp_link_stdlib("c++_static") .compile("libgame_text_input.a"); for f in ["android_native_app_glue.h", "android_native_app_glue.c"] { - println!("cargo:rerun-if-changed=game-activity-csrc/native_app_glue/{f}"); + println!("cargo:rerun-if-changed={activity_basepath}/game-activity/native_app_glue/{f}"); } + cc::Build::new() - .include("game-activity-csrc") - .include("game-activity-csrc/game-activity/native_app_glue") - .file("game-activity-csrc/game-activity/native_app_glue/android_native_app_glue.c") + .include("android-games-sdk/include") + .include(activity_basepath) + .include(textinput_basepath) + .include(format!("{activity_basepath}/game-activity/native_app_glue")) + .file(format!("{activity_basepath}/game-activity/native_app_glue/android_native_app_glue.c")) .extra_warnings(false) .cpp_link_stdlib("c++_static") .compile("libnative_app_glue.a");