Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.LIGHTS" />

<uses-feature
android:name="android.hardware.wifi"
Expand All @@ -30,6 +31,8 @@
android:glEsVersion="0x00020000"
android:required="true" />

<uses-feature android:name="android.hardware.gamepad" android:required="false" />

<application
android:allowBackup="true"
android:appCategory="game"
Expand Down
142 changes: 141 additions & 1 deletion app/src/main/java/com/geode/launcher/GeometryDashActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.hardware.input.InputManager
import android.opengl.GLSurfaceView
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.text.InputType
import android.util.Log
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.FrameLayout
Expand Down Expand Up @@ -64,7 +69,7 @@ fun ratioForPreference(value: String) = when (value) {
else -> 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
Expand All @@ -77,6 +82,9 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
private var mScreenZoom = 1.0f
private var mScreenZoomFit = false

private var mGamepads = mutableListOf<GeodeUtils.Gamepad>()
private lateinit var mInputManager: InputManager

override fun onCreate(savedInstanceState: Bundle?) {
setupUIState()
FMOD.init(this)
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -511,6 +523,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
override fun onDestroy() {
super.onDestroy()
FMOD.close()
mInputManager.unregisterInputDeviceListener(this)
}

private fun resumeGame() {
Expand Down Expand Up @@ -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
Expand Down
114 changes: 114 additions & 0 deletions app/src/main/java/com/geode/launcher/utils/GeodeUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 ->
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}