Vendor dependencies for 0.3.0 release

This commit is contained in:
2025-09-27 10:29:08 -05:00
parent 0c8d39d483
commit 82ab7f317b
26803 changed files with 16134934 additions and 0 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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 <android/asset_manager.h>
#include <android/input.h>
#include <android/native_window.h>
#include <android/rect.h>
#include <jni.h>
#include <stdbool.h>
#include <stdint.h>
#include <sys/types.h>
#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

View File

@@ -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 <sys/system_properties.h>
#include <string>
#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)
};
}

View File

@@ -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 <android/input.h>
#include <jni.h>
#include <stdbool.h>
#include <stdint.h>
#include <sys/types.h>
#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

View File

@@ -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 <android/log.h>
#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_

View File

@@ -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 <android/log.h>
#include <assert.h>
#include <errno.h>
#include <jni.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#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);
}

View File

@@ -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 <android/configuration.h>
#include <android/looper.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include "game-activity/GameActivity.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* The GameActivity interface provided by <game-activity/GameActivity.h>
* 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
/** @} */

View File

@@ -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;

View File

@@ -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 <android/log.h>
#include <jni.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <memory>
#include <vector>
#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<char> stateStringBuffer_;
};
std::unique_ptr<GameTextInput> 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<GameTextInput>(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<uint32_t>(state.text_length + 1),
static_cast<uint32_t>(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<GameTextInput *>(context);
if (state != nullptr) thiz->setStateInner(*state);
}
void GameTextInput::processEvent(jobject textInputEvent) {
stateFromJava(textInputEvent, processCallback, this);
if (eventCallback_) {
eventCallback_(eventCallbackContext_, &currentState_);
}
}
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_, &currentInsets_);
}
}
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_, "<init>",
"(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);
}

View File

@@ -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 <android/rect.h>
#include <jni.h>
#include <stdint.h>
#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
/** @} */