diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8079524b..08f0cde1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,6 +22,7 @@ + + { + mGLSurfaceView?.enableControllerEvents() + true + } else -> false } } diff --git a/app/src/main/java/com/geode/launcher/utils/GeodeUtils.kt b/app/src/main/java/com/geode/launcher/utils/GeodeUtils.kt index b077b35f..cb0dcb49 100644 --- a/app/src/main/java/com/geode/launcher/utils/GeodeUtils.kt +++ b/app/src/main/java/com/geode/launcher/utils/GeodeUtils.kt @@ -7,6 +7,9 @@ import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.hardware.lights.Light +import android.hardware.lights.LightState +import android.hardware.lights.LightsRequest import android.net.Uri import android.os.Build import android.os.Environment @@ -16,6 +19,7 @@ import android.os.VibratorManager import android.provider.DocumentsContract import android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION import android.util.Log +import android.view.InputDevice import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -547,6 +551,7 @@ object GeodeUtils { const val CAPABILITY_EXTENDED_INPUT = "extended_input" const val CAPABILITY_TIMESTAMP_INPUT = "timestamp_inputs" const val CAPABILITY_INTERNAL_CALLBACKS = "internal_callbacks_v1" + const val CAPABILITY_CONTROLLER_DATA = "controller_data" private var capabilityListener: WeakReference = WeakReference(null) @@ -576,6 +581,188 @@ object GeodeUtils { .launchUrl(activity, url.toUri()) } + /** + * Determines if any gamepad or joystick input devices are connected. + * This is useful during start, but tracking inputDeviceAdded events is better during runtime. + */ + @JvmStatic + fun controllersConnected(): Int { + val deviceIds = InputDevice.getDeviceIds() + return deviceIds.count { + InputDevice.getDevice(it)?.run { + sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD || sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK + } ?: false + } + } + + @JvmStatic + fun getConnectedDevices(): IntArray = InputDevice.getDeviceIds() + + @JvmStatic + fun getDevice(deviceId: Int): InputDevice? = InputDevice.getDevice(deviceId) + + @JvmStatic + fun getDeviceBatteryCapacity(deviceId: Int): Float { + val device = InputDevice.getDevice(deviceId) ?: return 0.0f + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + device.batteryState.capacity + } else { + 0.0f + } + } + + @JvmStatic + fun deviceHasBattery(deviceId: Int): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + return false + } + + val device = InputDevice.getDevice(deviceId) ?: return false + return device.batteryState.isPresent + } + + @JvmStatic + fun getDeviceBatteryStatus(deviceId: Int): Int { + val device = InputDevice.getDevice(deviceId) ?: return 0 + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + device.batteryState.status + } else { + 0 + } + } + + @JvmStatic + fun getDeviceLightsCount(deviceId: Int): Int { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + return 0 + } + + val device = InputDevice.getDevice(deviceId) ?: return 0 + val lightsManager = device.lightsManager + + return lightsManager.lights.size + } + + // this is based on paddleboat's api + @JvmStatic + fun getLightType(deviceId: Int): Int { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + return 0 + } + + val device = InputDevice.getDevice(deviceId) ?: return 0 + val lightsManager = device.lightsManager + + var flags = 0 + lightsManager.lights.forEach { light -> + if (light.type == Light.LIGHT_TYPE_PLAYER_ID) { + flags = flags or 1 + } else if (light.hasRgbControl()) { + flags = flags or 2 + } + } + + return flags + } + + /** + * Sets the color for a device. + * @return false if the given device does not exist or setting lights was otherwise unsuccessful + */ + @JvmStatic + fun setDeviceLightColor(deviceId: Int, color: Int, type: Int): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + return false + } + + val device = InputDevice.getDevice(deviceId) ?: return false + val lightsManager = device.lightsManager + + val lights = lightsManager.lights + if (lights.isEmpty()) { + return false + } + + val actedLights = lights.count { light -> + val state = LightState.Builder() + + if (light.type == Light.LIGHT_TYPE_PLAYER_ID && (type and 1 == 1)) { + state.setPlayerId(color) + } else if (light.hasRgbControl() && (type and 2 == 2)) { + state.setColor(color) + } else { + return@count false + } + + val request = LightsRequest.Builder() + .addLight(light, state.build()) + .build() + + lightsManager.openSession() + .requestLights(request) + + true + } + + return actedLights > 0 + } + + @JvmStatic + fun getDeviceHapticsCount(deviceId: Int): Int { + val device = InputDevice.getDevice(deviceId) ?: return 0 + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + return if (device.vibrator.hasVibrator()) 1 else 0 + } + + val vibratorManager = device.vibratorManager + val vibratorIds = vibratorManager.vibratorIds + return vibratorIds.size + } + + private fun performVibration(vibrator: Vibrator?, durationMs: Long, intensity: Int) { + vibrator?.apply { + if (intensity == 0) { + cancel() + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val vibrationEffect = VibrationEffect.createOneShot(durationMs, intensity) + vibrate(vibrationEffect) + } else { + vibrate(durationMs) + } + } + } + } + + /** + * Vibrates a given device. Set motorIdx == -1 to vibrate all available motors on a device. + */ + @JvmStatic + fun vibrateDevice(deviceId: Int, durationMs: Long, intensity: Int, motorIdx: Int): Boolean { + val device = InputDevice.getDevice(deviceId) ?: return false + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val vibratorManager = device.vibratorManager + val vibratorIds = vibratorManager.vibratorIds + + if (motorIdx == -1) { + vibratorIds.forEach { + val vibrator = vibratorManager.getVibrator(it) + performVibration(vibrator, durationMs, intensity) + } + } else { + val deviceId = vibratorIds.getOrNull(motorIdx) ?: return false + val vibrator = vibratorManager.getVibrator(deviceId) + performVibration(vibrator, durationMs, intensity) + } + } else { + val vibrator = device.vibrator + performVibration(vibrator, durationMs, intensity) + } + + return true + } + external fun nativeKeyUp(keyCode: Int, modifiers: Int) external fun nativeKeyDown(keyCode: Int, modifiers: Int, isRepeating: Boolean) external fun nativeActionScroll(scrollX: Float, scrollY: Float) @@ -588,4 +775,15 @@ object GeodeUtils { */ external fun setNextInputTimestamp(timestamp: Long) external fun setNextInputTimestampInternal(timestamp: Long) + + external fun setNextInputDevice(deviceId: Int, eventSource: Int) + external fun inputDeviceAdded(deviceId: Int) + external fun inputDeviceChanged(deviceId: Int) + external fun inputDeviceRemoved(deviceId: Int) + + /** + * Sends all batched joystick events. The final position in each array represents the current position of that axis. + * Have fun! + */ + external fun onJoystickEvent(leftX: FloatArray, leftY: FloatArray, rightX: FloatArray, rightY: FloatArray, hatX: FloatArray, hatY: FloatArray, leftTrigger: FloatArray, rightTrigger: FloatArray) } \ No newline at end of file diff --git a/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt b/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt index 480cb597..eb4a0d5f 100644 --- a/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt +++ b/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt @@ -2,12 +2,14 @@ package org.cocos2dx.lib import android.app.Activity import android.content.Context +import android.hardware.input.InputManager import android.opengl.GLSurfaceView import android.os.Build import android.os.Handler import android.os.Looper import android.os.Message import android.util.Log +import android.view.InputDevice import android.view.KeyEvent import android.view.MotionEvent import android.view.Surface @@ -16,6 +18,7 @@ import androidx.annotation.RequiresApi import androidx.appcompat.widget.AppCompatEditText import com.customRobTop.BaseRobTopActivity import com.geode.launcher.utils.GeodeUtils +import kotlin.math.abs private const val HANDLER_OPEN_IME_KEYBOARD = 2 private const val HANDLER_CLOSE_IME_KEYBOARD = 3 @@ -58,6 +61,8 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { var sendTimestampEvents = false var sendInternalTimestampEvents = false var manualBackEvents = false + var controllerEventsEnabled = false + private set var cocos2dxEditText: AppCompatEditText? = null set(value) { @@ -159,6 +164,16 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { GeodeUtils.setNextInputTimestampInternal(timestamp) } + private fun sendNextEventDetails(event: KeyEvent) { + if (controllerEventsEnabled) + GeodeUtils.setNextInputDevice(event.deviceId, event.source) + } + + private fun sendNextEventDetails(event: MotionEvent) { + if (controllerEventsEnabled) + GeodeUtils.setNextInputDevice(event.deviceId, event.source) + } + override fun onTouchEvent(motionEvent: MotionEvent): Boolean { val pointerNumber = motionEvent.pointerCount val ids = IntArray(pointerNumber) @@ -178,6 +193,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { val xDown = xs[0] val f = ys[0] queueEvent { + sendNextEventDetails(motionEvent) sendNextEventTimestamp(timestamp) cocos2dxRenderer.handleActionDown(idDown, xDown, f) } @@ -188,6 +204,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { val f2 = xs[0] val f3 = ys[0] queueEvent { + sendNextEventDetails(motionEvent) sendNextEventTimestamp(timestamp) cocos2dxRenderer.handleActionUp(idUp, f2, f3) } @@ -195,6 +212,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { } MotionEvent.ACTION_MOVE -> { queueEvent { + sendNextEventDetails(motionEvent) sendNextEventTimestamp(timestamp) cocos2dxRenderer.handleActionMove(ids, xs, ys) } @@ -202,6 +220,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { } MotionEvent.ACTION_CANCEL -> { queueEvent { + sendNextEventDetails(motionEvent) sendNextEventTimestamp(timestamp) cocos2dxRenderer.handleActionCancel(ids, xs, ys) } @@ -213,6 +232,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { val xPointerDown = motionEvent.getX(indexPointerDown) val y = motionEvent.getY(indexPointerDown) queueEvent { + sendNextEventDetails(motionEvent) sendNextEventTimestamp(timestamp) cocos2dxRenderer.handleActionDown(idPointerDown, xPointerDown, y) } @@ -224,6 +244,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { val xPointerUp = motionEvent.getX(indexPointUp) val y2 = motionEvent.getY(indexPointUp) queueEvent { + sendNextEventDetails(motionEvent) sendNextEventTimestamp(timestamp) cocos2dxRenderer.handleActionUp(idPointerUp, xPointerUp, y2) } @@ -273,6 +294,9 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { } else { val currentTime = System.nanoTime() queueEvent { + if (controllerEventsEnabled) + GeodeUtils.setNextInputDevice(-1, InputDevice.SOURCE_ANY) + sendNextEventTimestamp(currentTime) GeodeUtils.nativeKeyDown(KeyEvent.KEYCODE_BACK, 0, false) } @@ -295,6 +319,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { } queueEvent { + sendNextEventDetails(event) sendNextEventTimestamp(event.eventTime * MS_TO_NS) GeodeUtils.nativeKeyDown(keyCode, event.modifiers, event.repeatCount != 0) } @@ -318,6 +343,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { } queueEvent { + sendNextEventDetails(event) sendNextEventTimestamp(event.eventTime * MS_TO_NS) GeodeUtils.nativeKeyUp(keyCode, event.modifiers) } @@ -327,6 +353,65 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { } } + // i'm lazy. + // https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input + private fun getCenteredAxis( + event: MotionEvent, + device: InputDevice, + axis: Int, + historyPos: Int + ): Float { + device.getMotionRange(axis, event.source)?.apply { + val value: Float = if (historyPos < 0) { + event.getAxisValue(axis) + } else { + event.getHistoricalAxisValue(axis, historyPos) + } + + if (abs(value) > flat) { + return value + } + } + return 0.0f + } + + private fun getJoystickMotion(event: MotionEvent, axisHorizontal: Int, axisVertical: Int): Pair { + val positionChangesX = ArrayList() + val positionChangesY = ArrayList() + + val inputDevice = event.device + + (0 until event.historySize).forEach { i -> + val x = getCenteredAxis(event, inputDevice, axisHorizontal, i) + val y = getCenteredAxis(event, inputDevice, axisVertical, i) + positionChangesX.add(x) + positionChangesY.add(y) + } + + val x = getCenteredAxis(event, inputDevice, axisHorizontal, -1) + val y = getCenteredAxis(event, inputDevice, axisVertical, -1) + positionChangesX.add(x) + positionChangesY.add(y) + + return Pair(positionChangesX.toFloatArray(), positionChangesY.toFloatArray()) + } + + private fun getJoystickMotion(event: MotionEvent, axis: Int): FloatArray { + val positionChanges = ArrayList() + + val inputDevice = event.device + + (0 until event.historySize).forEach { i -> + val x = getCenteredAxis(event, inputDevice, axis, i) + positionChanges.add(x) + } + + val x = getCenteredAxis(event, inputDevice, axis, -1) + positionChanges.add(x) + + return positionChanges.toFloatArray() + } + override fun onGenericMotionEvent(event: MotionEvent?): Boolean { if (!useKeyboardEvents) { return super.onGenericMotionEvent(event) @@ -339,6 +424,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { event.eventTimeNanos else event.eventTime * MS_TO_NS queueEvent { + sendNextEventDetails(event) sendNextEventTimestamp(timestamp) GeodeUtils.nativeActionScroll(scrollX, scrollY) } @@ -346,6 +432,33 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { return true } + if (controllerEventsEnabled && event?.action == MotionEvent.ACTION_MOVE && event.source and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK) { + val (motionLeftX, motionLeftY) = getJoystickMotion(event, MotionEvent.AXIS_X, MotionEvent.AXIS_Y) + val (motionRightX, motionRightY) = getJoystickMotion(event, MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ) + val (motionDPadX, motionDPadY) = getJoystickMotion(event, MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y) + + val motionLTrigger = getJoystickMotion(event, MotionEvent.AXIS_LTRIGGER) + val motionRTrigger = getJoystickMotion(event, MotionEvent.AXIS_RTRIGGER) + + // i'm sure this is great for performance + val timestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + event.eventTimeNanos else event.eventTime * MS_TO_NS + + queueEvent { + sendNextEventDetails(event) + sendNextEventTimestamp(timestamp) + + GeodeUtils.onJoystickEvent( + motionLeftX, motionLeftY, + motionRightX, motionRightY, + motionDPadX, motionDPadY, + motionLTrigger, motionRTrigger + ) + } + + return true + } + return super.onGenericMotionEvent(event) } @@ -365,4 +478,40 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { fun deleteBackward() { queueEvent { cocos2dxRenderer.handleDeleteBackward() } } + + fun enableControllerEvents() { + if (controllerEventsEnabled) { + return + } + + controllerEventsEnabled = true + + val inputManager = context.getSystemService(Context.INPUT_SERVICE) as InputManager + inputManager.registerInputDeviceListener(InputListener, null) + } + + fun disableControllerEvents() { + if (!controllerEventsEnabled) { + return + } + + controllerEventsEnabled = true + + val inputManager = context.getSystemService(Context.INPUT_SERVICE) as InputManager + inputManager.unregisterInputDeviceListener(InputListener) + } + + object InputListener : InputManager.InputDeviceListener { + override fun onInputDeviceAdded(deviceId: Int) { + GeodeUtils.inputDeviceAdded(deviceId) + } + + override fun onInputDeviceChanged(deviceId: Int) { + GeodeUtils.inputDeviceChanged(deviceId) + } + + override fun onInputDeviceRemoved(deviceId: Int) { + GeodeUtils.inputDeviceRemoved(deviceId) + } + } } \ No newline at end of file