package com.phenomrom.iwanttoheal; import android.os.Bundle; import android.os.SystemClock; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import androidx.activity.OnBackPressedCallback; import androidx.core.view.WindowCompat; import androidx.core.view.WindowInsetsCompat; import androidx.core.view.WindowInsetsControllerCompat; import com.getcapacitor.BridgeActivity; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** Routes every Thor controller event into one JavaScript input service without WebView focus. */ public abstract class ControllerBridgeActivity extends BridgeActivity { private static final float AXIS_DEAD_ZONE = 0.45f; private static final long REPEAT_THROTTLE_MS = 55L; private final long[] lastDpadDispatchAt = new long[16]; private final Set activeMotionTokens = new HashSet<>(); private final Map lastMotionDispatchAt = new HashMap<>(); @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { dispatchNativeControllerToken("Button1", false); } }); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); WindowManager.LayoutParams attributes = getWindow().getAttributes(); attributes.preferredRefreshRate = 60.0f; getWindow().setAttributes(attributes); if (bridge != null && bridge.getWebView() != null) { bridge.getWebView().setOverScrollMode(View.OVER_SCROLL_NEVER); } enterImmersiveMode(); } @Override public void onPause() { clearHeldControllerState(); super.onPause(); } @Override public void onResume() { super.onResume(); enterImmersiveMode(); } @Override public boolean dispatchKeyEvent(KeyEvent event) { String token = controllerToken(event.getKeyCode()); if (token == null || bridge == null) return super.dispatchKeyEvent(event); if (event.getAction() == KeyEvent.ACTION_DOWN) { boolean repeat = event.getRepeatCount() > 0; if (isDpadToken(token) && shouldThrottleDpad(token)) return true; dispatchNativeControllerToken(token, repeat); } return true; } @Override public boolean dispatchGenericMotionEvent(MotionEvent event) { if ( bridge == null || event.getActionMasked() != MotionEvent.ACTION_MOVE || !isControllerMotionEvent(event) ) { return super.dispatchGenericMotionEvent(event); } float leftStickX = event.getAxisValue(MotionEvent.AXIS_X); float leftStickY = event.getAxisValue(MotionEvent.AXIS_Y); float rightStickX = controllerAxisValue(event, MotionEvent.AXIS_Z, MotionEvent.AXIS_RX); float rightStickY = controllerAxisValue(event, MotionEvent.AXIS_RZ, MotionEvent.AXIS_RY); dispatchNativeControllerMotion(leftStickX, leftStickY, rightStickX, rightStickY); Set currentTokens = new HashSet<>(); addAxisTokens(currentTokens, event.getAxisValue(MotionEvent.AXIS_HAT_X), "Button14", "Button15"); addAxisTokens(currentTokens, event.getAxisValue(MotionEvent.AXIS_HAT_Y), "Button12", "Button13"); addAxisTokens(currentTokens, leftStickX, "Axis0-", "Axis0+"); addAxisTokens(currentTokens, leftStickY, "Axis1-", "Axis1+"); long now = SystemClock.uptimeMillis(); for (String token : currentTokens) { boolean repeat = activeMotionTokens.contains(token); long lastDispatch = lastMotionDispatchAt.containsKey(token) ? lastMotionDispatchAt.get(token) : 0L; if (!repeat || now - lastDispatch >= REPEAT_THROTTLE_MS) { dispatchNativeControllerToken(token, repeat); lastMotionDispatchAt.put(token, now); } } activeMotionTokens.clear(); activeMotionTokens.addAll(currentTokens); return true; } private void enterImmersiveMode() { WindowCompat.setDecorFitsSystemWindows(getWindow(), false); WindowInsetsControllerCompat controller = WindowCompat.getInsetsController( getWindow(), getWindow().getDecorView() ); controller.hide(WindowInsetsCompat.Type.systemBars()); controller.setSystemBarsBehavior( WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE ); } private boolean isControllerMotionEvent(MotionEvent event) { int source = event.getSource(); return (source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK || (source & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD || (source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD; } private void addAxisTokens(Set tokens, float value, String negative, String positive) { if (value <= -AXIS_DEAD_ZONE) tokens.add(negative); if (value >= AXIS_DEAD_ZONE) tokens.add(positive); } private float controllerAxisValue(MotionEvent event, int primaryAxis, int fallbackAxis) { InputDevice device = event.getDevice(); if (device != null && device.getMotionRange(primaryAxis) != null) { return event.getAxisValue(primaryAxis); } return event.getAxisValue(fallbackAxis); } private void dispatchNativeControllerToken(String token, boolean repeat) { if (bridge == null || bridge.getWebView() == null) return; String script = "window.dispatchEvent(new CustomEvent('iwt-native-controller'," + "{detail:{token:'" + token + "',repeat:" + repeat + "}}));"; bridge.getWebView().post(() -> bridge.getWebView().evaluateJavascript(script, null)); } private void dispatchNativeControllerMotion(float moveX, float moveY, float lookX, float lookY) { if (bridge == null || bridge.getWebView() == null) return; String script = "window.dispatchEvent(new CustomEvent('iwt-native-controller-motion'," + "{detail:{moveX:" + moveX + ",moveY:" + moveY + ",lookX:" + lookX + ",lookY:" + lookY + "}}));"; bridge.getWebView().post(() -> bridge.getWebView().evaluateJavascript(script, null)); } private void clearHeldControllerState() { activeMotionTokens.clear(); lastMotionDispatchAt.clear(); dispatchNativeControllerMotion(0.0f, 0.0f, 0.0f, 0.0f); if (bridge == null || bridge.getWebView() == null) return; bridge.getWebView().post(() -> bridge.getWebView().evaluateJavascript( "window.dispatchEvent(new Event('iwt-native-controller-reset'));", null )); } private boolean shouldThrottleDpad(String token) { int index = Integer.parseInt(token.substring("Button".length())); long now = SystemClock.uptimeMillis(); if (now - lastDpadDispatchAt[index] < REPEAT_THROTTLE_MS) return true; lastDpadDispatchAt[index] = now; return false; } private boolean isDpadToken(String token) { return token.equals("Button12") || token.equals("Button13") || token.equals("Button14") || token.equals("Button15"); } private String controllerToken(int keyCode) { switch (keyCode) { case KeyEvent.KEYCODE_BUTTON_A: case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: case KeyEvent.KEYCODE_NUMPAD_ENTER: return "Button0"; case KeyEvent.KEYCODE_BUTTON_B: case KeyEvent.KEYCODE_ESCAPE: return "Button1"; case KeyEvent.KEYCODE_BUTTON_X: return "Button2"; case KeyEvent.KEYCODE_BUTTON_Y: return "Button3"; case KeyEvent.KEYCODE_BUTTON_L1: return "Button4"; case KeyEvent.KEYCODE_BUTTON_R1: return "Button5"; case KeyEvent.KEYCODE_BUTTON_L2: return "Button6"; case KeyEvent.KEYCODE_BUTTON_R2: return "Button7"; case KeyEvent.KEYCODE_BUTTON_SELECT: return "Button8"; case KeyEvent.KEYCODE_BUTTON_START: return "Button9"; case KeyEvent.KEYCODE_BUTTON_THUMBL: return "Button10"; case KeyEvent.KEYCODE_BUTTON_THUMBR: return "Button11"; case KeyEvent.KEYCODE_DPAD_UP: return "Button12"; case KeyEvent.KEYCODE_DPAD_DOWN: return "Button13"; case KeyEvent.KEYCODE_DPAD_LEFT: return "Button14"; case KeyEvent.KEYCODE_DPAD_RIGHT: return "Button15"; default: return null; } } }