diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2606c146..86d9b279 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,6 +22,7 @@ + + + 1.77f } -class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperListener, GeodeUtils.CapabilityListener { +class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperListener, GeodeUtils.CapabilityListener, InputManager.InputDeviceListener { private var mGLSurfaceView: Cocos2dxGLSurfaceView? = null private var mIsRunning = false private var mIsOnPause = false @@ -77,6 +82,9 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL private var mScreenZoom = 1.0f private var mScreenZoomFit = false + private var mGamepads = mutableListOf() + private lateinit var mInputManager: InputManager + override fun onCreate(savedInstanceState: Bundle?) { setupUIState() FMOD.init(this) @@ -129,6 +137,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL } }) mGLSurfaceView?.manualBackEvents = true + + mInputManager = getSystemService(INPUT_SERVICE) as InputManager + mInputManager.registerInputDeviceListener(this, null) + updateControllerDeviceIDs() } private fun createVersionFile() { @@ -511,6 +523,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL override fun onDestroy() { super.onDestroy() FMOD.close() + mInputManager.unregisterInputDeviceListener(this) } private fun resumeGame() { @@ -628,6 +641,133 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL } } + private fun updateControllerDeviceIDs() { + mGamepads.clear() + + // basically taken from documentation + val deviceIDs = InputDevice.getDeviceIds() + for (id in deviceIDs) { + val device = InputDevice.getDevice(id) ?: continue + + if (device.sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD + || device.sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK) { + mGamepads.add(GeodeUtils.Gamepad(device.id)) + } + } + } + + override fun onInputDeviceAdded(deviceID: Int) { + updateControllerDeviceIDs() + if (GeodeUtils.controllerCallbacksEnabled()) { + val index = mGamepads.indexOfFirst { it.mDeviceID == deviceID } + Handler(mainLooper).post { GeodeUtils.setControllerConnected(index, true) } + } + } + + override fun onInputDeviceRemoved(deviceID: Int) { + if (GeodeUtils.controllerCallbacksEnabled()) { + val index = mGamepads.indexOfFirst { it.mDeviceID == deviceID } + Handler(mainLooper).post { GeodeUtils.setControllerConnected(index, false) } + } + updateControllerDeviceIDs() + } + + override fun onInputDeviceChanged(deviceID: Int) {} + + override fun dispatchGenericMotionEvent(event: MotionEvent?): Boolean { + event ?: return super.dispatchGenericMotionEvent(null) + + val index = mGamepads.indexOfFirst { it.mDeviceID == event.deviceId } + if (index == -1) return super.dispatchGenericMotionEvent(null) + val gamepad = mGamepads[index] + + fun processJoystick(event: MotionEvent, index: Int) { + if (index < 0) { + gamepad.mJoyLeftX = event.getAxisValue(MotionEvent.AXIS_X) + gamepad.mJoyLeftY = -event.getAxisValue(MotionEvent.AXIS_Y) + gamepad.mJoyRightX = event.getAxisValue(MotionEvent.AXIS_Z) // wtf is axis z and rz + gamepad.mJoyRightY = -event.getAxisValue(MotionEvent.AXIS_RZ) + gamepad.mTriggerZL = event.getAxisValue(MotionEvent.AXIS_LTRIGGER) + gamepad.mTriggerZR = event.getAxisValue(MotionEvent.AXIS_RTRIGGER) + gamepad.mButtonUp = event.getAxisValue(MotionEvent.AXIS_HAT_Y) < 0.0f + gamepad.mButtonDown = event.getAxisValue(MotionEvent.AXIS_HAT_Y) > 0.0f + gamepad.mButtonLeft = event.getAxisValue(MotionEvent.AXIS_HAT_X) < 0.0f + gamepad.mButtonRight = event.getAxisValue(MotionEvent.AXIS_HAT_X) > 0.0f + } else { + gamepad.mJoyLeftX = event.getHistoricalAxisValue(MotionEvent.AXIS_X, index) + gamepad.mJoyLeftY = -event.getHistoricalAxisValue(MotionEvent.AXIS_Y, index) + gamepad.mJoyRightX = event.getHistoricalAxisValue(MotionEvent.AXIS_Z, index) + gamepad.mJoyRightY = -event.getHistoricalAxisValue(MotionEvent.AXIS_RZ, index) + gamepad.mTriggerZL = event.getHistoricalAxisValue(MotionEvent.AXIS_LTRIGGER, index) + gamepad.mTriggerZR = event.getHistoricalAxisValue(MotionEvent.AXIS_RTRIGGER, index) + gamepad.mButtonUp = event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_Y, index) < 0.0f + gamepad.mButtonDown = event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_Y, index) > 0.0f + gamepad.mButtonLeft = event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_X, index) < 0.0f + gamepad.mButtonRight = event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_X, index) > 0.0f + } + } + + // taken from documentation - android batches joystick events for efficiency + + // Process the movements starting from the + // earliest historical position in the batch + (0 until event.historySize).forEach { i -> + // Process the event at historical position i + processJoystick(event, i) + } + + // Process the current movement sample in the batch (position -1) + processJoystick(event, -1) + + // call callback in main thread + Handler(mainLooper).post { + if (GeodeUtils.controllerCallbacksEnabled()) GeodeUtils.setControllerState(index, gamepad) + } + + return true; + } + + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + val index = mGamepads.indexOfFirst { it.mDeviceID == event.deviceId } + if (index == -1) return super.dispatchKeyEvent(event) + val gamepad = mGamepads[index] + + val changed = when (event.keyCode) { + KeyEvent.KEYCODE_BUTTON_A -> { gamepad.mButtonA = event.action == KeyEvent.ACTION_DOWN; true } + KeyEvent.KEYCODE_BUTTON_B -> { gamepad.mButtonB = event.action == KeyEvent.ACTION_DOWN; true } + KeyEvent.KEYCODE_BUTTON_X -> { gamepad.mButtonX = event.action == KeyEvent.ACTION_DOWN; true } + KeyEvent.KEYCODE_BUTTON_Y -> { gamepad.mButtonY = event.action == KeyEvent.ACTION_DOWN; true } + KeyEvent.KEYCODE_BUTTON_START -> { gamepad.mButtonStart = event.action == KeyEvent.ACTION_DOWN; true } + KeyEvent.KEYCODE_BUTTON_SELECT -> { gamepad.mButtonSelect = event.action == KeyEvent.ACTION_DOWN; true } + KeyEvent.KEYCODE_BUTTON_L1 -> { gamepad.mButtonL = event.action == KeyEvent.ACTION_DOWN; true } + KeyEvent.KEYCODE_BUTTON_R1 -> { gamepad.mButtonR = event.action == KeyEvent.ACTION_DOWN; true } + // zl/zr/d-pad don't actually function as a button on my controllers but android documentation says to keep it in for compatibility + KeyEvent.KEYCODE_BUTTON_L2 -> { gamepad.mTriggerZL = if (event.action == KeyEvent.ACTION_DOWN) 1.0f else 0.0f; true } + KeyEvent.KEYCODE_BUTTON_R2 -> { gamepad.mTriggerZR = if (event.action == KeyEvent.ACTION_DOWN) 1.0f else 0.0f; true } + KeyEvent.KEYCODE_DPAD_UP -> { gamepad.mButtonUp = event.action == KeyEvent.ACTION_DOWN; true } + KeyEvent.KEYCODE_DPAD_DOWN -> { gamepad.mButtonDown = event.action == KeyEvent.ACTION_DOWN; true } + KeyEvent.KEYCODE_DPAD_LEFT -> { gamepad.mButtonLeft = event.action == KeyEvent.ACTION_DOWN; true } + KeyEvent.KEYCODE_DPAD_RIGHT -> { gamepad.mButtonRight = event.action == KeyEvent.ACTION_DOWN; true } + KeyEvent.KEYCODE_BUTTON_THUMBL -> { gamepad.mButtonJoyLeft = event.action == KeyEvent.ACTION_DOWN; true } + KeyEvent.KEYCODE_BUTTON_THUMBR -> { gamepad.mButtonJoyRight = event.action == KeyEvent.ACTION_DOWN; true } + else -> false + } + + // call callback in main thread + if (changed) { + Handler(mainLooper).post { + if (GeodeUtils.controllerCallbacksEnabled()) GeodeUtils.setControllerState(index, gamepad) + } + + return true + } + + return super.dispatchKeyEvent(event) + } + + fun getGamepad(id: Int) = mGamepads.getOrNull(id) + fun getGamepadCount() = mGamepads.size + class EGLConfigChooser : GLSurfaceView.EGLConfigChooser { // this comes from EGL14, but is unavailable on EGL10 // also EGL14 is incompatible with EGL10. so whatever 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 12578802..fe348712 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,8 @@ import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.hardware.lights.LightState +import android.hardware.lights.LightsRequest import android.net.Uri import android.os.Build import android.os.Environment @@ -16,6 +18,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 @@ -26,6 +29,7 @@ import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import com.geode.launcher.BuildConfig +import com.geode.launcher.GeometryDashActivity import com.geode.launcher.R import com.geode.launcher.UserDirectoryProvider import com.geode.launcher.activityresult.GeodeOpenFileActivityResult @@ -52,6 +56,8 @@ object GeodeUtils { private var afterRequestPermissions: (() -> Unit)? = null private var afterRequestPermissionsFailure: (() -> Unit)? = null + private var mControllerCallbacksEnabled: Boolean = false + fun setContext(activity: AppCompatActivity) { this.activity = WeakReference(activity) openFileResultLauncher = activity.registerForActivityResult(GeodeOpenFileActivityResult()) { uri -> @@ -572,6 +578,106 @@ object GeodeUtils { .launchUrl(activity, url.toUri()) } + @JvmStatic + fun getControllerCount(): Int { + val act = activity.get() + return if (act is GeometryDashActivity) act.getGamepadCount() else 0 + } + + /** + * Enables calling of the controller callbacks - they **must** be defined in native functions before this is called + * @see setControllerState + * @see setControllerConnected + */ + @JvmStatic + fun enableControllerCallbacks() { mControllerCallbacksEnabled = true } + @JvmStatic + fun controllerCallbacksEnabled() = mControllerCallbacksEnabled + + /** + * Whether the **device** supports vibration or lighting effects - not necessarily if the controller can or not. + */ + @JvmStatic + fun supportsControllerExtendedFeatures(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + + @JvmStatic + fun setControllerVibration(id: Int, duration: Long, left: Int, right: Int) { + val act = activity.get() + if (act is GeometryDashActivity) { + act.getGamepad(id)?.setVibration(duration, left, right) + } + } + + @JvmStatic + fun setControllerColor(id: Int, color: Int) { + val act = activity.get() + if (act is GeometryDashActivity) { + act.getGamepad(id)?.setColor(color) + } + } + + class Gamepad(deviceID: Int) { + var mButtonA: Boolean = false + var mButtonB: Boolean = false + var mButtonX: Boolean = false + var mButtonY: Boolean = false + var mButtonStart: Boolean = false + var mButtonSelect: Boolean = false + var mButtonL: Boolean = false + var mButtonR: Boolean = false + var mTriggerZL: Float = 0.0f + var mTriggerZR: Float = 0.0f + var mButtonUp: Boolean = false + var mButtonDown: Boolean = false + var mButtonLeft: Boolean = false + var mButtonRight: Boolean = false + var mButtonJoyLeft: Boolean = false + var mButtonJoyRight: Boolean = false + + var mJoyLeftX: Float = 0.0f + var mJoyLeftY: Float = 0.0f + var mJoyRightX: Float = 0.0f + var mJoyRightY: Float = 0.0f + + var mDeviceID: Int = deviceID + + fun setVibration(duration: Long, left: Int, right: Int) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return + + val device = InputDevice.getDevice(mDeviceID) ?: return + val manager = device.vibratorManager + val ids = manager.vibratorIds + + if (ids.size == 1) { + manager.getVibrator(ids[0]).vibrate(VibrationEffect.createOneShot(duration, (left + right) / 2)) + } + + if (ids.size == 2) { + manager.getVibrator(ids[0]).vibrate(VibrationEffect.createOneShot(duration, left)) + manager.getVibrator(ids[1]).vibrate(VibrationEffect.createOneShot(duration, right)) + } + } + + fun setColor(color: Int) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return + + val device = InputDevice.getDevice(mDeviceID) ?: return + val manager = device.lightsManager + val request = LightsRequest.Builder() + + for (light in manager.lights) { + request.addLight( + light, + LightState.Builder() + .setColor(color) + .build() + ) + } + + manager.openSession().requestLights(request.build()) + } + } + external fun nativeKeyUp(keyCode: Int, modifiers: Int) external fun nativeKeyDown(keyCode: Int, modifiers: Int, isRepeating: Boolean) external fun nativeActionScroll(scrollX: Float, scrollY: Float) @@ -583,5 +689,13 @@ object GeodeUtils { * @see reportPlatformCapability */ external fun setNextInputTimestamp(timestamp: Long) + + /** + * Gives the state of the current controller at the index, whenever it updates. + * @see enableControllerCallbacks + */ + external fun setControllerState(index: Int, gamepad: Gamepad) + external fun setControllerConnected(index: Int, connected: Boolean) + external fun setNextInputTimestampInternal(timestamp: Long) } \ No newline at end of file