diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index 6777781ca..1bdf02513 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -126,6 +126,22 @@
+
+
+
+
+
+
+
+
+
+
ExecuteImpl(config));
+ final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
+ if (emulationActivity instanceof VrActivity) {
+ ((VrActivity)emulationActivity).mVrKeyboardLauncher.launch(config);
+ } else {
+ NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config));
+ }
synchronized (finishLock) {
try {
@@ -256,11 +297,11 @@ public static KeyboardData Execute(KeyboardConfig config) {
public static void ShowError(String error) {
NativeLibrary.displayAlertMsg(
- CitraApplication.getAppContext().getResources().getString(R.string.software_keyboard),
- error, false);
+ CitraApplication.getAppContext().getResources().getString(R.string.software_keyboard),
+ error, false);
}
- private static native ValidationError ValidateFilters(String text);
+ public static native ValidationError ValidateFilters(String text);
- private static native ValidationError ValidateInput(String text);
+ public static native ValidationError ValidateInput(String text);
}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/vr/ErrorMessageLayer.java b/src/android/app/src/main/java/org/citra/citra_emu/vr/ErrorMessageLayer.java
index 0abc14b78..2123b2277 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/vr/ErrorMessageLayer.java
+++ b/src/android/app/src/main/java/org/citra/citra_emu/vr/ErrorMessageLayer.java
@@ -1,19 +1,12 @@
package org.citra.citra_emu.vr;
-public class ErrorMessageLayer
-{
+public class ErrorMessageLayer {
public static ErrorMessageLayer instance = null;
- public static void showErrorWindow(final String titleStr,
- final String mainMessageStr)
- {
- }
+ public static void showErrorWindow(final String titleStr, final String mainMessageStr) {}
- public void _showErrorWindow(final String titleStr,
- final String mainMessageStr)
- {
- }
+ public void _showErrorWindow(final String titleStr, final String mainMessageStr) {}
public void hideErrorWindow() {}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/vr/GameSurfaceLayer.java b/src/android/app/src/main/java/org/citra/citra_emu/vr/GameSurfaceLayer.java
index fbdede56d..176428802 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/vr/GameSurfaceLayer.java
+++ b/src/android/app/src/main/java/org/citra/citra_emu/vr/GameSurfaceLayer.java
@@ -14,13 +14,11 @@
* Note: this is set up to require the min number of changes possible to
*existing Citra code, in case an upstream merge is desired.
**/
-public class GameSurfaceLayer
-{
- public static void setSurface(VrActivity activity, Surface surface)
- {
+public class GameSurfaceLayer {
+ public static void setSurface(VrActivity activity, Surface surface) {
assert activity != null;
- ((EmulationFragment)activity.getSupportFragmentManager()
- .findFragmentById(R.id.frame_emulation_fragment))
+ ((EmulationFragment)activity.getSupportFragmentManager().findFragmentById(
+ R.id.frame_emulation_fragment))
.surfaceCreated(surface);
}
}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/vr/VrActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/vr/VrActivity.java
index a8693992d..682ebd8dd 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/vr/VrActivity.java
+++ b/src/android/app/src/main/java/org/citra/citra_emu/vr/VrActivity.java
@@ -10,52 +10,53 @@
import android.view.Display;
import android.view.InputDevice;
import android.view.KeyEvent;
+import androidx.activity.result.ActivityResultLauncher;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.activities.EmulationActivity;
+import org.citra.citra_emu.applets.SoftwareKeyboard;
import org.citra.citra_emu.features.settings.ui.SettingsActivity;
import org.citra.citra_emu.features.settings.utils.SettingsFile;
import org.citra.citra_emu.utils.Log;
-
-public class VrActivity extends EmulationActivity
-{
+public class VrActivity extends EmulationActivity {
private long mHandle = 0;
public static boolean hasRun = false;
public static VrActivity currentActivity = null;
ClickRunnable clickRunnable = new ClickRunnable();
- static { System.loadLibrary("openxr_forwardloader.oculus"); }
- public static void launch(Context context, final String gamePath,
- final String gameTitle)
- {
+ static {
+ System.loadLibrary("openxr_forwardloader.oculus");
+ }
+
+ public final ActivityResultLauncher mVrKeyboardLauncher =
+ registerForActivityResult(new VrKeyboardActivity.Contract(),
+ result -> VrKeyboardActivity.onFinishResult(result));
+
+ public static void launch(Context context, final String gamePath, final String gameTitle) {
Intent intent = new Intent(context, VrActivity.class);
final int mainDisplayId = getMainDisplay(context);
- if (mainDisplayId < 0)
- {
+ if (mainDisplayId < 0) {
// TODO handle error
throw new RuntimeException("Could not find main display");
}
- ActivityOptions options =
- ActivityOptions.makeBasic().setLaunchDisplayId(mainDisplayId);
- intent.setFlags(
- Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
- Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ ActivityOptions options = ActivityOptions.makeBasic().setLaunchDisplayId(mainDisplayId);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
+ Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(EmulationActivity.EXTRA_SELECTED_GAME, gamePath);
intent.putExtra(EmulationActivity.EXTRA_SELECTED_TITLE, gameTitle);
- if (context instanceof ContextWrapper)
- {
+ if (context instanceof ContextWrapper) {
ContextWrapper contextWrapper = (ContextWrapper)context;
Context baseContext = contextWrapper.getBaseContext();
baseContext.startActivity(intent, options.toBundle());
+ } else {
+ context.startActivity(intent, options.toBundle());
}
- else { context.startActivity(intent, options.toBundle()); }
((Activity)(context)).finish();
}
- @Override protected void onCreate(Bundle savedInstanceState)
- {
- if (hasRun)
- {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ if (hasRun) {
Log.info("VRActivity already existed");
finish();
}
@@ -67,35 +68,37 @@ public static void launch(Context context, final String gamePath,
// TODO assert mHandle != null
}
- @Override protected void onDestroy()
- {
+ @Override
+ protected void onDestroy() {
Log.info("VR [Java] onDestroy");
currentActivity = null;
- if (mHandle != 0) { nativeOnDestroy(mHandle); }
+ if (mHandle != 0) {
+ nativeOnDestroy(mHandle);
+ }
super.onDestroy();
}
- @Override public void onStart()
- {
+ @Override
+ public void onStart() {
Log.info("VR [Java] onStart");
System.gc();
super.onStart();
}
- @Override public void onResume()
- {
+ @Override
+ public void onResume() {
Log.info("VR [Java] onResume");
super.onResume();
}
- @Override public void onPause()
- {
+ @Override
+ public void onPause() {
Log.info("VR [Java] onPause");
super.onPause();
}
- @Override public void onStop()
- {
+ @Override
+ public void onStop() {
Log.info("VR [Java] onStop");
super.onStop();
}
@@ -103,97 +106,90 @@ public static void launch(Context context, final String gamePath,
private native long nativeOnCreate();
private native void nativeOnDestroy(final long handle);
- private static int getMainDisplay(Context context)
- {
+ private static int getMainDisplay(Context context) {
final DisplayManager displayManager =
(DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
Display[] displays = displayManager.getDisplays();
- for (int i = 0; i < displays.length; i++)
- {
- if (displays[i].getDisplayId() == Display.DEFAULT_DISPLAY)
- {
+ for (int i = 0; i < displays.length; i++) {
+ if (displays[i].getDisplayId() == Display.DEFAULT_DISPLAY) {
return displays[i].getDisplayId();
}
}
return -1;
}
- public void finishActivity()
- {
- if (!isFinishing()) { finish(); }
+ public void finishActivity() {
+ if (!isFinishing()) {
+ finish();
+ }
}
- void forwardVRInput(final int keycode, final boolean isPressed)
- {
- KeyEvent event = new KeyEvent(
- isPressed ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP, keycode);
+ void forwardVRInput(final int keycode, final boolean isPressed) {
+ KeyEvent event =
+ new KeyEvent(isPressed ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP, keycode);
event.setSource(InputDevice.SOURCE_GAMEPAD);
dispatchKeyEvent(event);
}
- void forwardVRJoystick(final float x, final float y, final int joystickType)
- {
+ void forwardVRJoystick(final float x, final float y, final int joystickType) {
// dispatch joystick input as gamepad joystick input
- NativeLibrary.onGamePadMoveEvent(
- "Quest controller",
- joystickType == 0 ? NativeLibrary.ButtonType.STICK_C
- : NativeLibrary.ButtonType.STICK_LEFT,
- x, -y);
+ NativeLibrary.onGamePadMoveEvent("Quest controller",
+ joystickType == 0 ? NativeLibrary.ButtonType.STICK_C
+ : NativeLibrary.ButtonType.STICK_LEFT,
+ x, -y);
}
- void openSettingsMenu()
- {
+ void openSettingsMenu() {
SettingsActivity.launch(this, SettingsFile.FILE_NAME_CONFIG, "");
}
- public void sendClickToWindow(final float x, final float y,
- final int motionType)
- {
+ public void sendClickToWindow(final float x, final float y, final int motionType) {
clickRunnable.updateState((int)x, (int)y, motionType);
runOnUiThread(clickRunnable);
}
- public void pauseGame()
- {
+ public void pauseGame() {
Log.info("VR [Java] pauseGame");
- if (NativeLibrary.IsRunning()) { NativeLibrary.PauseEmulation(); }
+ if (NativeLibrary.IsRunning()) {
+ NativeLibrary.PauseEmulation();
+ }
}
- public void resumeGame()
- {
+ public void resumeGame() {
Log.info("VR [Java] resumeGame");
// this checks to make sure the emulation has started and pausing it is
// safe -- not whether it's paused/resumed
- if (NativeLibrary.IsRunning()) { NativeLibrary.UnPauseEmulation(); }
+ if (NativeLibrary.IsRunning()) {
+ NativeLibrary.UnPauseEmulation();
+ }
}
- class ClickRunnable implements Runnable
- {
+ class ClickRunnable implements Runnable {
private int xPosition;
private int yPosition;
private int motionType;
- public void updateState(int x, int y, int motionType)
- {
+ public void updateState(int x, int y, int motionType) {
this.xPosition = x;
this.yPosition = y;
this.motionType = motionType;
}
- @Override public void run()
- {
- switch (motionType)
- {
- case 0: NativeLibrary.onTouchEvent(0, 0, false); break;
- case 1:
- NativeLibrary.onTouchEvent(xPosition, yPosition, true);
- break;
- case 2: NativeLibrary.onTouchMoved(xPosition, yPosition); break;
- default:
- Log.error(
- "VR [Java] sendClickToWindow: unknown motionType: " +
- motionType);
- break;
+ @Override
+ public void run() {
+ switch (motionType) {
+ case 0:
+ NativeLibrary.onTouchEvent(0, 0, false);
+ break;
+ case 1:
+ NativeLibrary.onTouchEvent(xPosition, yPosition, true);
+ break;
+ case 2:
+ NativeLibrary.onTouchMoved(xPosition, yPosition);
+ break;
+ default:
+ Log.error("VR [Java] sendClickToWindow: unknown motionType: " + motionType);
+ break;
}
}
}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/vr/VrKeyboardActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/vr/VrKeyboardActivity.java
new file mode 100644
index 000000000..00aa880f5
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/vr/VrKeyboardActivity.java
@@ -0,0 +1,404 @@
+package org.citra.citra_emu.vr;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.Spanned;
+import android.text.TextWatcher;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContract;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.view.WindowCompat;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.fragment.app.DialogFragment;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import java.io.Serializable;
+import java.util.Objects;
+import org.citra.citra_emu.CitraApplication;
+import org.citra.citra_emu.R;
+import org.citra.citra_emu.applets.SoftwareKeyboard;
+import org.citra.citra_emu.utils.Log;
+
+public class VrKeyboardActivity extends android.app.Activity {
+
+ private static final String EXTRA_KEYBOARD_INPUT_CONFIG =
+ "org.citra.citra_emu.vr.KEYBOARD_INPUT_CONFIG";
+ private static final String EXTRA_KEYBOARD_RESULT = "org.citra.citra_emu.vr.KEYBOARD_RESULT";
+
+ public static class Result implements Serializable {
+ public static enum Type { None, Positive, Neutral, Negative }
+ ;
+ public Result() {
+ text = "";
+ type = Type.None;
+ config = null;
+ }
+ public Result(final String text, final Type type,
+ final SoftwareKeyboard.KeyboardConfig config) {
+ this.text = text;
+ this.type = type;
+ this.config = config;
+ }
+
+ public Result(final Type type) {
+ this.text = "";
+ this.type = type;
+ this.config = null;
+ }
+
+ public String text;
+ public Type type;
+ public SoftwareKeyboard.KeyboardConfig config;
+ }
+
+ public static class Contract
+ extends ActivityResultContract {
+ @Override
+ public Intent createIntent(Context context, final SoftwareKeyboard.KeyboardConfig config) {
+ Intent intent = new Intent(context, VrKeyboardActivity.class);
+ intent.putExtra(EXTRA_KEYBOARD_INPUT_CONFIG, config);
+ return intent;
+ }
+
+ @Override
+ public Result parseResult(int resultCode, Intent intent) {
+ if (resultCode != Activity.RESULT_OK) {
+ Log.warning("parseResult(): Unexpected result code: " + resultCode);
+ return new Result();
+ }
+ if (intent != null) {
+ final Result result = (Result)intent.getSerializableExtra(EXTRA_KEYBOARD_RESULT);
+ if (result != null) {
+ return result;
+ }
+ }
+ Log.warning("parseResult(): finished with OK, but no result. Intent: " + intent);
+ return new Result();
+ }
+ }
+
+ public static void onFinishResult(final Result result) {
+ switch (result.type) {
+ case Positive:
+ SoftwareKeyboard.onFinishVrKeyboardPositive(result.text, result.config);
+ break;
+ case Neutral:
+ SoftwareKeyboard.onFinishVrKeyboardNeutral();
+ break;
+ case Negative:
+ case None:
+ SoftwareKeyboard.onFinishVrKeyboardNegative();
+ break;
+ }
+ }
+
+ private static enum KeyboardType { None, Abc, Num }
+
+ private EditText mEditText = null;
+ private boolean mIsShifted = false;
+ private KeyboardType mKeyboardTypeCur = KeyboardType.None;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Bundle extras = getIntent().getExtras();
+ SoftwareKeyboard.KeyboardConfig config = new SoftwareKeyboard.KeyboardConfig();
+ if (extras != null) {
+ config = (SoftwareKeyboard.KeyboardConfig)extras.getSerializable(
+ EXTRA_KEYBOARD_INPUT_CONFIG);
+ }
+
+ setContentView(R.layout.vr_keyboard);
+ mEditText = findViewById(R.id.vrKeyboardText);
+
+ {
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ params.leftMargin = params.rightMargin =
+ CitraApplication.getAppContext().getResources().getDimensionPixelSize(
+ R.dimen.dialog_margin);
+ mEditText.setHint(config.hint_text);
+ mEditText.setSingleLine(!config.multiline_mode);
+ mEditText.setLayoutParams(params);
+ mEditText.setFilters(
+ new InputFilter[] {new SoftwareKeyboard.Filter(),
+ new InputFilter.LengthFilter(config.max_text_length)});
+ }
+
+ // Needed to show cursor onscreen.
+ mEditText.requestFocus();
+ WindowCompat.getInsetsController(getWindow(), mEditText)
+ .show(WindowInsetsCompat.Type.ime());
+
+ setupResultButtons(config);
+ showKeyboardType(KeyboardType.Abc);
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ if (!hasFocus) {
+ finish(); // Finish the activity when it loses focus, like an AlertDialog.
+ }
+ }
+
+ private void setupResultButtons(final SoftwareKeyboard.KeyboardConfig config) {
+ // Configure the result buttons
+ findViewById(R.id.keyPositive).setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ Intent resultIntent = new Intent();
+ resultIntent.putExtra(
+ EXTRA_KEYBOARD_RESULT,
+ new Result(mEditText.getText().toString(), Result.Type.Positive, config));
+ setResult(Activity.RESULT_OK, resultIntent);
+ finish();
+ }
+ return false;
+ }
+ });
+
+ findViewById(R.id.keyNeutral).setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ Intent resultIntent = new Intent();
+ resultIntent.putExtra(EXTRA_KEYBOARD_RESULT, new Result(Result.Type.Neutral));
+ setResult(Activity.RESULT_OK, resultIntent);
+ finish();
+ }
+ return false;
+ }
+ });
+
+ findViewById(R.id.keyNegative).setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ Intent resultIntent = new Intent();
+ resultIntent.putExtra(EXTRA_KEYBOARD_RESULT, new Result(Result.Type.Negative));
+ setResult(Activity.RESULT_OK, resultIntent);
+ finish();
+ }
+ return false;
+ }
+ });
+
+ switch (config.button_config) {
+ case SoftwareKeyboard.ButtonConfig.Triple:
+ findViewById(R.id.keyNeutral).setVisibility(View.VISIBLE);
+ // fallthrough
+ case SoftwareKeyboard.ButtonConfig.Dual:
+ findViewById(R.id.keyNegative).setVisibility(View.VISIBLE);
+ // fallthrough
+ case SoftwareKeyboard.ButtonConfig.Single:
+ findViewById(R.id.keyPositive).setVisibility(View.VISIBLE);
+ // fallthrough
+ case SoftwareKeyboard.ButtonConfig.None:
+ break;
+ default:
+ Log.error("Unknown button config: " + config.button_config);
+ assert false;
+ }
+ }
+
+ private void showKeyboardType(final KeyboardType keyboardType) {
+ if (mKeyboardTypeCur == keyboardType) {
+ return;
+ }
+ mKeyboardTypeCur = keyboardType;
+ final ViewGroup keyboard = findViewById(R.id.vr_keyboard_keyboard);
+ keyboard.removeAllViews();
+ switch (keyboardType) {
+ case Abc:
+ getLayoutInflater().inflate(R.layout.vr_keyboard_abc, keyboard);
+ addLetterKeyHandlersForViewGroup(keyboard, mIsShifted);
+ break;
+ case Num:
+ getLayoutInflater().inflate(R.layout.vr_keyboard_123, keyboard);
+ addLetterKeyHandlersForViewGroup(keyboard, false);
+ break;
+ default:
+ assert false;
+ }
+ addModifierKeyHandlers();
+ }
+
+ private void addModifierKeyHandlers() {
+ findViewById(R.id.keyShift).setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ setKeyCase(!mIsShifted);
+ }
+ return false;
+ }
+ });
+ // Note: I prefer touch listeners over click listeners because they activate
+ // on the press instead of the release and therefore feel more responsive.
+ findViewById(R.id.keyBackspace).setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ final String text = mEditText.getText().toString();
+ if (text.length() > 0) {
+ // Delete character before cursor
+ final int position = mEditText.getSelectionStart();
+ if (position > 0) {
+ final String newText =
+ text.substring(0, position - 1) + text.substring(position);
+ mEditText.setText(newText);
+ mEditText.setSelection(position - 1);
+ }
+ }
+ }
+ return false;
+ }
+ });
+
+ findViewById(R.id.keySpace).setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ final int position = mEditText.getSelectionStart();
+ if (position < mEditText.getText().length()) {
+ final String newText =
+ mEditText.getText().toString().substring(0, position) + " " +
+ mEditText.getText().toString().substring(position);
+ mEditText.setText(newText);
+ mEditText.setSelection(position + 1);
+ } else {
+ mEditText.append(" ");
+ }
+ }
+ return false;
+ }
+ });
+
+ findViewById(R.id.keyLeft).setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ final int position = mEditText.getSelectionStart();
+ if (position > 0) {
+ mEditText.setSelection(position - 1);
+ }
+ }
+ return false;
+ }
+ });
+
+ findViewById(R.id.keyRight).setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ final int position = mEditText.getSelectionStart();
+ if (position < mEditText.getText().length()) {
+ mEditText.setSelection(position + 1);
+ }
+ }
+ return false;
+ }
+ });
+
+ if (findViewById(R.id.keyNumbers) != null) {
+ findViewById(R.id.keyNumbers).setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ showKeyboardType(KeyboardType.Num);
+ }
+ return false;
+ }
+ });
+ }
+ if (findViewById(R.id.keyAbc) != null) {
+ findViewById(R.id.keyAbc).setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ showKeyboardType(KeyboardType.Abc);
+ }
+ return false;
+ }
+ });
+ }
+ }
+
+ private void addLetterKeyHandlersForViewGroup(final ViewGroup viewGroup,
+ final boolean isShifted) {
+ for (int i = 0; i < viewGroup.getChildCount(); i++) {
+ final View child = viewGroup.getChildAt(i);
+ if (child instanceof ViewGroup) {
+ addLetterKeyHandlersForViewGroup((ViewGroup)child, isShifted);
+ } else if (child instanceof Button) {
+ if ("key_letter".equals(child.getTag())) {
+ final Button key = (Button)child;
+ key.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ final int position = mEditText.getSelectionStart();
+ if (position < mEditText.getText().length()) {
+ final String newText =
+ mEditText.getText().toString().substring(0, position) +
+ key.getText().toString() +
+ mEditText.getText().toString().substring(position);
+ mEditText.setText(newText);
+ mEditText.setSelection(position + 1);
+ } else {
+ mEditText.append(key.getText().toString());
+ }
+ }
+ return false;
+ }
+ });
+ setKeyCaseForButton(key, isShifted);
+ }
+ }
+ }
+ }
+
+ private void setKeyCase(final boolean isShifted) {
+ mIsShifted = isShifted;
+ final ViewGroup layout = findViewById(R.id.vr_keyboard);
+ setKeyCaseForViewGroup(layout, isShifted);
+ }
+
+ private static void setKeyCaseForViewGroup(ViewGroup viewGroup, final boolean isShifted) {
+ for (int i = 0; i < viewGroup.getChildCount(); i++) {
+ final View child = viewGroup.getChildAt(i);
+ if (child instanceof ViewGroup) {
+ setKeyCaseForViewGroup((ViewGroup)child, isShifted);
+ } else if (child instanceof Button && "key_letter".equals(child.getTag())) {
+ setKeyCaseForButton((Button)child, isShifted);
+ }
+ }
+ }
+
+ private static void setKeyCaseForButton(Button button, final boolean isShifted) {
+ final String text = button.getText().toString();
+ if (isShifted) {
+ button.setText(text.toUpperCase());
+ } else {
+ button.setText(text.toLowerCase());
+ }
+ }
+}
diff --git a/src/android/app/src/main/res/drawable/vr_keyboard_key_background.xml b/src/android/app/src/main/res/drawable/vr_keyboard_key_background.xml
new file mode 100644
index 000000000..5b0700394
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/vr_keyboard_key_background.xml
@@ -0,0 +1,16 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/vr_keyboard.xml b/src/android/app/src/main/res/layout/vr_keyboard.xml
new file mode 100644
index 000000000..78475d1ee
--- /dev/null
+++ b/src/android/app/src/main/res/layout/vr_keyboard.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/vr_keyboard_123.xml b/src/android/app/src/main/res/layout/vr_keyboard_123.xml
new file mode 100644
index 000000000..2e7f87bc0
--- /dev/null
+++ b/src/android/app/src/main/res/layout/vr_keyboard_123.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/layout/vr_keyboard_abc.xml b/src/android/app/src/main/res/layout/vr_keyboard_abc.xml
new file mode 100644
index 000000000..ffeab5714
--- /dev/null
+++ b/src/android/app/src/main/res/layout/vr_keyboard_abc.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index e6a62bb28..ba123ffae 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -290,7 +290,8 @@
Having trouble? Press @string/button_start + @string/button_select on the gamepad to toggle input modes
-
VR
VR Environment
+ <
+ >
diff --git a/src/android/app/src/main/res/values/styles.xml b/src/android/app/src/main/res/values/styles.xml
index 432392b57..0bc278d4a 100644
--- a/src/android/app/src/main/res/values/styles.xml
+++ b/src/android/app/src/main/res/values/styles.xml
@@ -51,4 +51,26 @@
+
+
+
+
+
+
+