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_, &currentState_);
   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_, &currentInsets_);
   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  }