Skip to content

Commit

Permalink
android keyboard support
Browse files Browse the repository at this point in the history
  • Loading branch information
flyinghead committed Jun 26, 2022
1 parent 9a55797 commit b5b0875
Show file tree
Hide file tree
Showing 6 changed files with 868 additions and 8 deletions.
5 changes: 4 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1289,7 +1289,10 @@ if(NOT LIBRETRO)
if(ANDROID)
target_compile_definitions(${PROJECT_NAME} PRIVATE GLES GLES3)

target_sources(${PROJECT_NAME} PRIVATE shell/android-studio/flycast/src/main/jni/src/Android.cpp)
target_sources(${PROJECT_NAME} PRIVATE
shell/android-studio/flycast/src/main/jni/src/Android.cpp
shell/android-studio/flycast/src/main/jni/src/android_gamepad.h
shell/android-studio/flycast/src/main/jni/src/android_keyboard.h)

target_link_libraries(${PROJECT_NAME} PRIVATE android EGL GLESv2 log)
elseif(APPLE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
Expand Down Expand Up @@ -43,6 +43,7 @@

import tv.ouya.console.api.OuyaController;

import static android.content.res.Configuration.HARDKEYBOARDHIDDEN_NO;
import static android.view.View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
Expand All @@ -61,6 +62,7 @@ public abstract class BaseGLActivity extends Activity implements ActivityCompat.
private boolean paused = true;
private boolean resumedCalled = false;
private String pendingIntentUrl;
private boolean hasKeyboard = false;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
Expand Down Expand Up @@ -134,6 +136,8 @@ public void onClick(DialogInterface dialog,int id) {

audioBackend = new AudioBackend();

onConfigurationChanged(getResources().getConfiguration());

// When viewing a resource, pass its URI to the native code for opening
Intent intent = getIntent();
if (intent.getAction() != null) {
Expand Down Expand Up @@ -206,6 +210,12 @@ public void onWindowFocusChanged(boolean hasFocus) {
}
}

@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
hasKeyboard = newConfig.hardKeyboardHidden == HARDKEYBOARDHIDDEN_NO;
}

protected abstract void doPause();
protected abstract void doResume();
protected abstract boolean isSurfaceReady();
Expand Down Expand Up @@ -291,6 +301,8 @@ else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) == InputDevice.S
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (InputDeviceManager.getInstance().joystickButtonEvent(event.getDeviceId(), keyCode, false))
return true;
if (hasKeyboard && InputDeviceManager.getInstance().keyboardEvent(keyCode, false))
return true;
return super.onKeyUp(keyCode, event);
}

Expand All @@ -310,6 +322,12 @@ else if (JNIdc.guiIsContentBrowser()) {
if (InputDeviceManager.getInstance().joystickButtonEvent(event.getDeviceId(), keyCode, true))
return true;

if (hasKeyboard) {
InputDeviceManager.getInstance().keyboardEvent(keyCode, true);
if (!event.isCtrlPressed() && (event.isPrintingKey() || event.getKeyCode() == KeyEvent.KEYCODE_SPACE))
InputDeviceManager.getInstance().keyboardText(event.getUnicodeChar());
return true;
}
if (ViewConfiguration.get(this).hasPermanentMenuKey()) {
if (keyCode == KeyEvent.KEYCODE_MENU) {
return showMenu();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
package com.reicast.emulator;

import android.content.Context;
import android.os.Bundle;
import android.text.InputType;
import android.util.Log;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.RelativeLayout;

import androidx.annotation.Nullable;

import com.reicast.emulator.emu.JNIdc;
import com.reicast.emulator.emu.NativeGLView;
import com.reicast.emulator.periph.InputDeviceManager;
import com.reicast.emulator.periph.VJoy;

public final class NativeGLActivity extends BaseGLActivity {

private static ViewGroup mLayout; // used for text input
private ViewGroup mLayout; // used for text input
private NativeGLView mView;
View mTextEdit;
private boolean mScreenKeyboardShown;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
Expand Down Expand Up @@ -77,4 +88,215 @@ public void run() {
}
});
}

// On-screen keyboard borrowed from SDL core android code
class ShowTextInputTask implements Runnable {
/*
* This is used to regulate the pan&scan method to have some offset from
* the bottom edge of the input region and the top edge of an input
* method (soft keyboard)
*/
static final int HEIGHT_PADDING = 15;

public int x, y, w, h;

public ShowTextInputTask(int x, int y, int w, int h) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;

/* Minimum size of 1 pixel, so it takes focus. */
if (this.w <= 0) {
this.w = 1;
}
if (this.h + HEIGHT_PADDING <= 0) {
this.h = 1 - HEIGHT_PADDING;
}
}

@Override
public void run() {
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING);
params.leftMargin = x;
params.topMargin = y;
if (mTextEdit == null) {
mTextEdit = new DummyEdit(getApplicationContext(), NativeGLActivity.this);

mLayout.addView(mTextEdit, params);
} else {
mTextEdit.setLayoutParams(params);
}

mTextEdit.setVisibility(View.VISIBLE);
mTextEdit.requestFocus();

InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(mTextEdit, 0);

mScreenKeyboardShown = true;
Log.d("flycast", "ShowTextInputTask: run");
}
}

// Called from native code
public void showTextInput(int x, int y, int w, int h) {
// Transfer the task to the main thread as a Runnable
handler.post(new ShowTextInputTask(x, y, w, h));
}

// Called from native code
public void hideTextInput() {
Log.d("flycast", "hideTextInput " + (mTextEdit != null ? "mTextEdit != null" : ""));
if (mTextEdit != null) {
mTextEdit.postDelayed(new Runnable() {
@Override
public void run() {
// Note: On some devices setting view to GONE creates a flicker in landscape.
// Setting the View's sizes to 0 is similar to GONE but without the flicker.
// The sizes will be set to useful values when the keyboard is shown again.
mTextEdit.setLayoutParams(new RelativeLayout.LayoutParams(0, 0));

InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0);

mScreenKeyboardShown = false;

mView.requestFocus();
}
}, 50);
}
}

// Called from native code
public boolean isScreenKeyboardShown() {
if (mTextEdit == null)
return false;

if (!mScreenKeyboardShown)
return false;

InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
return imm.isAcceptingText();
}
}

/* This is a fake invisible editor view that receives the input and defines the
+ * pan&scan region
+ */
class DummyEdit extends View implements View.OnKeyListener {
InputConnection ic;
NativeGLActivity activity;

class InputConnection extends BaseInputConnection {
public InputConnection(boolean fullEditor) {
super(DummyEdit.this, fullEditor);
}

@Override
public boolean sendKeyEvent(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)
activity.hideTextInput();
return super.sendKeyEvent(event);
}

@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
InputDeviceManager devManager = InputDeviceManager.getInstance();
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c == '\n') {
activity.hideTextInput();
return true;
}
devManager.keyboardText(c);
}
return super.commitText(text, newCursorPosition);
}

@Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
// Workaround to capture backspace key. Ref: http://stackoverflow.com/questions/14560344/android-backspace-in-webview-baseinputconnection
// and https://bugzilla.libsdl.org/show_bug.cgi?id=2265
if (beforeLength > 0 && afterLength == 0) {
Log.d("flycast", "deleteSurroundingText before=" + beforeLength);
KeyEvent downEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
downEvent.setSource(InputDevice.SOURCE_KEYBOARD);
KeyEvent upEvent = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL);
upEvent.setSource(InputDevice.SOURCE_KEYBOARD);
boolean ret = true;
// backspace(s)
while (beforeLength-- > 0) {
boolean ret_key = sendKeyEvent(downEvent);
sendKeyEvent(upEvent);
ret = ret && ret_key;
}
return ret;
}
return super.deleteSurroundingText(beforeLength, afterLength);
}
}

public DummyEdit(Context context, NativeGLActivity activity) {
super(context);
this.activity = activity;
setFocusableInTouchMode(true);
setFocusable(true);
setOnKeyListener(this);
}

@Override
public boolean onCheckIsTextEditor() {
return true;
}

@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
/*
* This handles the hardware keyboard input
*/
InputDeviceManager devManager = InputDeviceManager.getInstance();
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (!event.isCtrlPressed() && (event.isPrintingKey() || event.getKeyCode() == KeyEvent.KEYCODE_SPACE))
ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1);
else
devManager.keyboardEvent(event.getKeyCode(), true);
return true;
} else if (event.getAction() == KeyEvent.ACTION_UP) {
devManager.keyboardEvent(event.getKeyCode(), false);
return true;
}
return false;
}

@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
// As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event
// FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639
// FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not
// FIXME: A more effective solution would be to assume our Layout to be RelativeLayout or LinearLayout
// FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
// FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :)
if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
if (activity.mTextEdit != null && activity.mTextEdit.getVisibility() == View.VISIBLE) {
// activity.hideTextInput();
KeyEvent downEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER);
downEvent.setSource(InputDevice.SOURCE_KEYBOARD);
ic.sendKeyEvent(downEvent);
KeyEvent upEvent = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER);
upEvent.setSource(InputDevice.SOURCE_KEYBOARD);
ic.sendKeyEvent(upEvent);
}
}
return super.onKeyPreIme(keyCode, event);
}

@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
| EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */;
ic = new InputConnection(true);
return ic;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,6 @@ public static InputDeviceManager getInstance() {
public native void mouseScrollEvent(int scrollValue);
private native void joystickAdded(int id, String name, int maple_port, String uniqueId, int fullAxes[], int halfAxes[]);
private native void joystickRemoved(int id);
public native boolean keyboardEvent(int key, boolean pressed);
public native void keyboardText(int c);
}

0 comments on commit b5b0875

Please sign in to comment.