package com.warren.iwanttoheal; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; 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 com.getcapacitor.BridgeActivity; import java.io.File; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; public abstract class ControllerBridgeActivity extends BridgeActivity { public static final String EXTRA_INITIAL_URL = "com.warren.iwanttoheal.INITIAL_URL"; private static final String PREFS_NAME = "com.warren.iwanttoheal.performance"; private static final String CACHE_CLEARED_VERSION_KEY = "webview_cache_cleared_version"; private static final float AXIS_DEAD_ZONE = 0.45f; private static final long DPAD_THROTTLE_MS = 55; 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) { boolean shouldClearWebViewCache = shouldClearWebViewCacheForCurrentVersion(); if (shouldClearWebViewCache) clearWebViewServiceWorkers(); super.onCreate(savedInstanceState); AndroidDisplayPerformance.preferBatteryRefreshRate(getWindow(), getWindowManager().getDefaultDisplay()); if (bridge != null) { if (shouldClearWebViewCache) { bridge.getWebView().clearCache(true); markWebViewCacheClearedForCurrentVersion(); } } loadIntentUrl(); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); loadIntentUrl(); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { enableImmersiveMode(); AndroidDisplayPerformance.preferBatteryRefreshRate( getWindow(), getWindowManager().getDefaultDisplay() ); } } @Override public boolean dispatchTouchEvent(MotionEvent event) { if ( event.getActionMasked() == MotionEvent.ACTION_DOWN && bridge != null && bridge.getWebView() != null ) { bridge.getWebView().requestFocus(); } return super.dispatchTouchEvent(event); } private void loadIntentUrl() { if (bridge == null || getIntent() == null) return; String initialUrl = getIntent().getStringExtra(EXTRA_INITIAL_URL); if (initialUrl == null || initialUrl.isEmpty()) return; bridge.getWebView().post(() -> bridge.getWebView().loadUrl(initialUrl)); } private void enableImmersiveMode() { getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE ); } private void clearWebViewServiceWorkers() { File webViewData = new File(getApplicationInfo().dataDir, "app_webview"); deleteIfExists(new File(webViewData, "Default/Service Worker")); deleteIfExists(new File(webViewData, "Service Worker")); } private boolean shouldClearWebViewCacheForCurrentVersion() { return performancePreferences().getLong(CACHE_CLEARED_VERSION_KEY, -1L) != currentVersionCode(); } private void markWebViewCacheClearedForCurrentVersion() { performancePreferences() .edit() .putLong(CACHE_CLEARED_VERSION_KEY, currentVersionCode()) .apply(); } private SharedPreferences performancePreferences() { return getSharedPreferences(PREFS_NAME, MODE_PRIVATE); } private long currentVersionCode() { try { PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { return packageInfo.getLongVersionCode(); } return packageInfo.versionCode; } catch (PackageManager.NameNotFoundException exception) { return -1L; } } private void deleteIfExists(File file) { if (!file.exists()) return; if (file.isDirectory()) { File[] children = file.listFiles(); if (children != null) { for (File child : children) { deleteIfExists(child); } } } file.delete(); } @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); } Set currentTokens = new HashSet<>(); float leftStickX = event.getAxisValue(MotionEvent.AXIS_X); float leftStickY = event.getAxisValue(MotionEvent.AXIS_Y); dispatchNativeControllerMotion(leftStickX, leftStickY); 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+" ); boolean hadMotionTokens = !activeMotionTokens.isEmpty(); long now = SystemClock.uptimeMillis(); for (String token : currentTokens) { boolean repeat = activeMotionTokens.contains(token); long lastDispatchAt = lastMotionDispatchAt.containsKey(token) ? lastMotionDispatchAt.get(token) : 0L; if (!repeat || now - lastDispatchAt >= DPAD_THROTTLE_MS) { dispatchNativeControllerToken(token, repeat); lastMotionDispatchAt.put(token, now); } } activeMotionTokens.clear(); activeMotionTokens.addAll(currentTokens); if (currentTokens.isEmpty() && !hadMotionTokens) { return super.dispatchGenericMotionEvent(event); } return true; } 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 negativeToken, String positiveToken ) { if (value <= -AXIS_DEAD_ZONE) tokens.add(negativeToken); if (value >= AXIS_DEAD_ZONE) tokens.add(positiveToken); } private void dispatchNativeControllerToken(String token, boolean repeat) { if (bridge == null || bridge.getWebView() == null) return; String script = "window.dispatchEvent(new CustomEvent('ashen-halls-native-controller'," + "{detail:{token:'" + token + "',repeat:" + repeat + "}}));"; bridge.getWebView().post( () -> { bridge.getWebView().requestFocus(); bridge.getWebView().evaluateJavascript(script, null); } ); } private void dispatchNativeControllerMotion(float x, float y) { if (bridge == null || bridge.getWebView() == null) return; String script = "window.dispatchEvent(new CustomEvent('ashen-halls-native-controller-motion'," + "{detail:{x:" + x + ",y:" + y + "}}));"; bridge.getWebView().post( () -> { bridge.getWebView().requestFocus(); bridge.getWebView().evaluateJavascript(script, null); } ); } private boolean shouldThrottleDpad(String token) { int buttonIndex = Integer.parseInt(token.substring("Button".length())); long now = SystemClock.uptimeMillis(); if (now - lastDpadDispatchAt[buttonIndex] < DPAD_THROTTLE_MS) return true; lastDpadDispatchAt[buttonIndex] = 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_BACK: 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; } } }