github.com/rajveermalviya/gamen@v0.1.2-0.20220930195403-9be15877c1aa/internal/android/game-activity/include/game-text-input/gametextinput.cpp (about) 1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 #include "game-text-input/gametextinput.h" 17 18 #include <android/log.h> 19 #include <jni.h> 20 #include <stdlib.h> 21 #include <string.h> 22 23 #include <algorithm> 24 #include <memory> 25 #include <vector> 26 27 #define LOG_TAG "GameTextInput" 28 29 static constexpr int32_t DEFAULT_MAX_STRING_SIZE = 1 << 16; 30 31 // Cache of field ids in the Java GameTextInputState class 32 struct StateClassInfo { 33 jfieldID text; 34 jfieldID selectionStart; 35 jfieldID selectionEnd; 36 jfieldID composingRegionStart; 37 jfieldID composingRegionEnd; 38 }; 39 40 // Main GameTextInput object. 41 struct GameTextInput { 42 public: 43 GameTextInput(JNIEnv *env, uint32_t max_string_size); 44 ~GameTextInput(); 45 void setState(const GameTextInputState &state); 46 const GameTextInputState &getState() const { return currentState_; } 47 void setInputConnection(jobject inputConnection); 48 void processEvent(jobject textInputEvent); 49 void showIme(uint32_t flags); 50 void hideIme(uint32_t flags); 51 void setEventCallback(GameTextInputEventCallback callback, void *context); 52 jobject stateToJava(const GameTextInputState &state) const; 53 void stateFromJava(jobject textInputEvent, 54 GameTextInputGetStateCallback callback, 55 void *context) const; 56 void setImeInsetsCallback(GameTextInputImeInsetsCallback callback, 57 void *context); 58 void processImeInsets(const ARect *insets); 59 const ARect &getImeInsets() const { return currentInsets_; } 60 61 private: 62 // Copy string and set other fields 63 void setStateInner(const GameTextInputState &state); 64 static void processCallback(void *context, const GameTextInputState *state); 65 JNIEnv *env_ = nullptr; 66 // Cached at initialization from 67 // com/google/androidgamesdk/gametextinput/State. 68 jclass stateJavaClass_ = nullptr; 69 // The latest text input update. 70 GameTextInputState currentState_ = {}; 71 // An instance of gametextinput.InputConnection. 72 jclass inputConnectionClass_ = nullptr; 73 jobject inputConnection_ = nullptr; 74 jmethodID inputConnectionSetStateMethod_; 75 jmethodID setSoftKeyboardActiveMethod_; 76 void (*eventCallback_)(void *context, 77 const struct GameTextInputState *state) = nullptr; 78 void *eventCallbackContext_ = nullptr; 79 void (*insetsCallback_)(void *context, 80 const struct ARect *insets) = nullptr; 81 ARect currentInsets_ = {}; 82 void *insetsCallbackContext_ = nullptr; 83 StateClassInfo stateClassInfo_ = {}; 84 // Constant-sized buffer used to store state text. 85 std::vector<char> stateStringBuffer_; 86 }; 87 88 std::unique_ptr<GameTextInput> s_gameTextInput; 89 90 extern "C" { 91 92 /////////////////////////////////////////////////////////// 93 /// GameTextInputState C Functions 94 /////////////////////////////////////////////////////////// 95 96 // Convert to a Java structure. 97 jobject currentState_toJava(const GameTextInput *gameTextInput, 98 const GameTextInputState *state) { 99 if (state == nullptr) return NULL; 100 return gameTextInput->stateToJava(*state); 101 } 102 103 // Convert from Java structure. 104 void currentState_fromJava(const GameTextInput *gameTextInput, 105 jobject textInputEvent, 106 GameTextInputGetStateCallback callback, 107 void *context) { 108 gameTextInput->stateFromJava(textInputEvent, callback, context); 109 } 110 111 /////////////////////////////////////////////////////////// 112 /// GameTextInput C Functions 113 /////////////////////////////////////////////////////////// 114 115 struct GameTextInput *GameTextInput_init(JNIEnv *env, 116 uint32_t max_string_size) { 117 if (s_gameTextInput.get() != nullptr) { 118 __android_log_print(ANDROID_LOG_WARN, LOG_TAG, 119 "Warning: called GameTextInput_init twice without " 120 "calling GameTextInput_destroy"); 121 return s_gameTextInput.get(); 122 } 123 // Don't use make_unique, for C++11 compatibility 124 s_gameTextInput = 125 std::unique_ptr<GameTextInput>(new GameTextInput(env, max_string_size)); 126 return s_gameTextInput.get(); 127 } 128 129 void GameTextInput_destroy(GameTextInput *input) { 130 if (input == nullptr || s_gameTextInput.get() == nullptr) return; 131 s_gameTextInput.reset(); 132 } 133 134 void GameTextInput_setState(GameTextInput *input, 135 const GameTextInputState *state) { 136 if (state == nullptr) return; 137 input->setState(*state); 138 } 139 140 void GameTextInput_getState(GameTextInput *input, 141 GameTextInputGetStateCallback callback, 142 void *context) { 143 callback(context, &input->getState()); 144 } 145 146 void GameTextInput_setInputConnection(GameTextInput *input, 147 jobject inputConnection) { 148 input->setInputConnection(inputConnection); 149 } 150 151 void GameTextInput_processEvent(GameTextInput *input, jobject textInputEvent) { 152 input->processEvent(textInputEvent); 153 } 154 155 void GameTextInput_processImeInsets(GameTextInput *input, const ARect *insets) { 156 input->processImeInsets(insets); 157 } 158 159 void GameTextInput_showIme(struct GameTextInput *input, uint32_t flags) { 160 input->showIme(flags); 161 } 162 163 void GameTextInput_hideIme(struct GameTextInput *input, uint32_t flags) { 164 input->hideIme(flags); 165 } 166 167 void GameTextInput_setEventCallback(struct GameTextInput *input, 168 GameTextInputEventCallback callback, 169 void *context) { 170 input->setEventCallback(callback, context); 171 } 172 173 void GameTextInput_setImeInsetsCallback(struct GameTextInput *input, 174 GameTextInputImeInsetsCallback callback, 175 void *context) { 176 input->setImeInsetsCallback(callback, context); 177 } 178 179 void GameTextInput_getImeInsets(const GameTextInput *input, ARect *insets) { 180 *insets = input->getImeInsets(); 181 } 182 183 } // extern "C" 184 185 /////////////////////////////////////////////////////////// 186 /// GameTextInput C++ class Implementation 187 /////////////////////////////////////////////////////////// 188 189 GameTextInput::GameTextInput(JNIEnv *env, uint32_t max_string_size) 190 : env_(env), 191 stateStringBuffer_(max_string_size == 0 ? DEFAULT_MAX_STRING_SIZE 192 : max_string_size) { 193 stateJavaClass_ = (jclass)env_->NewGlobalRef( 194 env_->FindClass("com/google/androidgamesdk/gametextinput/State")); 195 inputConnectionClass_ = (jclass)env_->NewGlobalRef(env_->FindClass( 196 "com/google/androidgamesdk/gametextinput/InputConnection")); 197 inputConnectionSetStateMethod_ = 198 env_->GetMethodID(inputConnectionClass_, "setState", 199 "(Lcom/google/androidgamesdk/gametextinput/State;)V"); 200 setSoftKeyboardActiveMethod_ = env_->GetMethodID( 201 inputConnectionClass_, "setSoftKeyboardActive", "(ZI)V"); 202 203 stateClassInfo_.text = 204 env_->GetFieldID(stateJavaClass_, "text", "Ljava/lang/String;"); 205 stateClassInfo_.selectionStart = 206 env_->GetFieldID(stateJavaClass_, "selectionStart", "I"); 207 stateClassInfo_.selectionEnd = 208 env_->GetFieldID(stateJavaClass_, "selectionEnd", "I"); 209 stateClassInfo_.composingRegionStart = 210 env_->GetFieldID(stateJavaClass_, "composingRegionStart", "I"); 211 stateClassInfo_.composingRegionEnd = 212 env_->GetFieldID(stateJavaClass_, "composingRegionEnd", "I"); 213 } 214 215 GameTextInput::~GameTextInput() { 216 if (stateJavaClass_ != NULL) { 217 env_->DeleteGlobalRef(stateJavaClass_); 218 stateJavaClass_ = NULL; 219 } 220 if (inputConnectionClass_ != NULL) { 221 env_->DeleteGlobalRef(inputConnectionClass_); 222 inputConnectionClass_ = NULL; 223 } 224 if (inputConnection_ != NULL) { 225 env_->DeleteGlobalRef(inputConnection_); 226 inputConnection_ = NULL; 227 } 228 } 229 230 void GameTextInput::setState(const GameTextInputState &state) { 231 if (inputConnection_ == nullptr) return; 232 jobject jstate = stateToJava(state); 233 env_->CallVoidMethod(inputConnection_, inputConnectionSetStateMethod_, 234 jstate); 235 env_->DeleteLocalRef(jstate); 236 setStateInner(state); 237 } 238 239 void GameTextInput::setStateInner(const GameTextInputState &state) { 240 // Check if we're setting using our own string (other parts may be 241 // different) 242 if (state.text_UTF8 == currentState_.text_UTF8) { 243 currentState_ = state; 244 return; 245 } 246 // Otherwise, copy across the string. 247 auto bytes_needed = 248 std::min(static_cast<uint32_t>(state.text_length + 1), 249 static_cast<uint32_t>(stateStringBuffer_.size())); 250 currentState_.text_UTF8 = stateStringBuffer_.data(); 251 std::copy(state.text_UTF8, state.text_UTF8 + bytes_needed - 1, 252 stateStringBuffer_.data()); 253 currentState_.text_length = state.text_length; 254 currentState_.selection = state.selection; 255 currentState_.composingRegion = state.composingRegion; 256 stateStringBuffer_[bytes_needed - 1] = 0; 257 } 258 259 void GameTextInput::setInputConnection(jobject inputConnection) { 260 if (inputConnection_ != NULL) { 261 env_->DeleteGlobalRef(inputConnection_); 262 } 263 inputConnection_ = env_->NewGlobalRef(inputConnection); 264 } 265 266 /*static*/ void GameTextInput::processCallback( 267 void *context, const GameTextInputState *state) { 268 auto thiz = static_cast<GameTextInput *>(context); 269 if (state != nullptr) thiz->setStateInner(*state); 270 } 271 272 void GameTextInput::processEvent(jobject textInputEvent) { 273 stateFromJava(textInputEvent, processCallback, this); 274 if (eventCallback_) { 275 eventCallback_(eventCallbackContext_, ¤tState_); 276 } 277 } 278 279 void GameTextInput::showIme(uint32_t flags) { 280 if (inputConnection_ == nullptr) return; 281 env_->CallVoidMethod(inputConnection_, setSoftKeyboardActiveMethod_, true, 282 flags); 283 } 284 285 void GameTextInput::setEventCallback(GameTextInputEventCallback callback, 286 void *context) { 287 eventCallback_ = callback; 288 eventCallbackContext_ = context; 289 } 290 291 void GameTextInput::setImeInsetsCallback( 292 GameTextInputImeInsetsCallback callback, void *context) { 293 insetsCallback_ = callback; 294 insetsCallbackContext_ = context; 295 } 296 297 void GameTextInput::processImeInsets(const ARect *insets) { 298 currentInsets_ = *insets; 299 if (insetsCallback_) { 300 insetsCallback_(insetsCallbackContext_, ¤tInsets_); 301 } 302 } 303 304 void GameTextInput::hideIme(uint32_t flags) { 305 if (inputConnection_ == nullptr) return; 306 env_->CallVoidMethod(inputConnection_, setSoftKeyboardActiveMethod_, false, 307 flags); 308 } 309 310 jobject GameTextInput::stateToJava(const GameTextInputState &state) const { 311 static jmethodID constructor = nullptr; 312 if (constructor == nullptr) { 313 constructor = env_->GetMethodID(stateJavaClass_, "<init>", 314 "(Ljava/lang/String;IIII)V"); 315 if (constructor == nullptr) { 316 __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, 317 "Can't find gametextinput.State constructor"); 318 return nullptr; 319 } 320 } 321 const char *text = state.text_UTF8; 322 if (text == nullptr) { 323 static char empty_string[] = ""; 324 text = empty_string; 325 } 326 // Note that this expects 'modified' UTF-8 which is not the same as UTF-8 327 // https://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8 328 jstring jtext = env_->NewStringUTF(text); 329 jobject jobj = 330 env_->NewObject(stateJavaClass_, constructor, jtext, 331 state.selection.start, state.selection.end, 332 state.composingRegion.start, state.composingRegion.end); 333 env_->DeleteLocalRef(jtext); 334 return jobj; 335 } 336 337 void GameTextInput::stateFromJava(jobject textInputEvent, 338 GameTextInputGetStateCallback callback, 339 void *context) const { 340 jstring text = 341 (jstring)env_->GetObjectField(textInputEvent, stateClassInfo_.text); 342 // Note this is 'modified' UTF-8, not true UTF-8. It has no NULLs in it, 343 // except at the end. It's actually not specified whether the value returned 344 // by GetStringUTFChars includes a null at the end, but it *seems to* on 345 // Android. 346 const char *text_chars = env_->GetStringUTFChars(text, NULL); 347 int text_len = env_->GetStringUTFLength( 348 text); // Length in bytes, *not* including the null. 349 int selectionStart = 350 env_->GetIntField(textInputEvent, stateClassInfo_.selectionStart); 351 int selectionEnd = 352 env_->GetIntField(textInputEvent, stateClassInfo_.selectionEnd); 353 int composingRegionStart = 354 env_->GetIntField(textInputEvent, stateClassInfo_.composingRegionStart); 355 int composingRegionEnd = 356 env_->GetIntField(textInputEvent, stateClassInfo_.composingRegionEnd); 357 GameTextInputState state{text_chars, 358 text_len, 359 {selectionStart, selectionEnd}, 360 {composingRegionStart, composingRegionEnd}}; 361 callback(context, &state); 362 env_->ReleaseStringUTFChars(text, text_chars); 363 env_->DeleteLocalRef(text); 364 }