From 79c06ca6295f7f39499cd8067845fbf6eb318709 Mon Sep 17 00:00:00 2001 From: Chloe Date: Thu, 15 Oct 2026 22:16:23 -0500 Subject: [PATCH 1/5] ime and input changes --- app/src/main/AndroidManifest.xml | 2 +- .../com/customRobTop/BaseRobTopActivity.kt | 31 ++- .../geode/launcher/GeometryDashActivity.kt | 20 +- .../geode/launcher/utils/GamePackageUtils.kt | 1 + .../com/geode/launcher/utils/GeodeUtils.kt | 2 +- .../org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt | 226 +++++++++++++----- .../java/org/cocos2dx/lib/Cocos2dxRenderer.kt | 29 +++ 7 files changed, 248 insertions(+), 63 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6cf37d5..7cf366f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -125,7 +125,7 @@ 0) { + return clip.getItemAt(0).coerceToText(this).toString() + } + } + return "" + } + @JvmStatic fun sendMail(subject: String, body: String, to: String) { me.get()?.runOnUiThread { @@ -264,4 +284,9 @@ object BaseRobTopActivity { level = DeprecationLevel.HIDDEN ) fun logEvent(event: String) {} + + @JvmStatic + fun setKeyboardInputFlags(flags: Int) { + curKeyboardInputFlags = flags + } } \ No newline at end of file diff --git a/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt b/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt index 8c13687..bad8ff8 100644 --- a/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt +++ b/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt @@ -171,7 +171,8 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL tryLoadLibrary(gdPackageInfo, Constants.FMOD_LIB_NAME) tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME) - if (GamePackageUtils.getGameVersionCode(packageManager) >= 39L) { + val gameVersionCode = GamePackageUtils.getGameVersionCode(packageManager) + if (gameVersionCode >= 39L) { // this fix requires geode v3, which is 2.206+ // there is a short period in which 2.206 users will still have geode v2, but whatever. ig LauncherFix.enableExceptionsRenaming() @@ -182,6 +183,9 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL loadInternalMods() val baseView = createView() + if (gameVersionCode >= 42L) { + mGLSurfaceView?.sendNewCocosEvents = true + } if (!GeodeUtils.handleSafeArea && Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { val frameLayoutParams = ViewGroup.LayoutParams( @@ -205,12 +209,22 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL left = 0, top = 0, right = 0, bottom = 0 ) } + + Cocos2dxGLSurfaceView.onKeyboardInsets(insets) + WindowInsets.CONSUMED } frameLayout.addView(baseView) setContentView(frameLayout) } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + baseView.setOnApplyWindowInsetsListener { view, insets -> + Cocos2dxGLSurfaceView.onKeyboardInsets(insets) + return@setOnApplyWindowInsetsListener view.onApplyWindowInsets(insets) + } + } + setContentView(baseView) } @@ -497,6 +511,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS glSurfaceView.cocos2dxEditText = editText + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + glSurfaceView.defaultFocusHighlightEnabled = false + } + return frameLayout } diff --git a/app/src/main/java/com/geode/launcher/utils/GamePackageUtils.kt b/app/src/main/java/com/geode/launcher/utils/GamePackageUtils.kt index eb6ca8f..6b60b83 100644 --- a/app/src/main/java/com/geode/launcher/utils/GamePackageUtils.kt +++ b/app/src/main/java/com/geode/launcher/utils/GamePackageUtils.kt @@ -53,6 +53,7 @@ object GamePackageUtils { 39L to "2.206", 40L to "2.2074", // ok 41L to "2.2081", + 42L to "2.209", ) /** 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 4776fa6..44f0afa 100644 --- a/app/src/main/java/com/geode/launcher/utils/GeodeUtils.kt +++ b/app/src/main/java/com/geode/launcher/utils/GeodeUtils.kt @@ -794,7 +794,7 @@ object GeodeUtils { external fun internalKeyEvent(timestamp: Long, deviceId: Int, eventSource: Int, keyCode: Int, modifiers: Int, isDown: Boolean, repeatCount: Int) external fun internalTouchEvent(timestamp: Long, deviceId: Int, eventSource: Int, eventType: Int, ids: IntArray, xs: FloatArray, ys: FloatArray) - external fun internalScrollEvent(timestamp: Long, deviceId: Int, eventSource: Int, scrollX: Float, scrollY: Float) + external fun internalScrollEvent(timestamp: Long, deviceId: Int, eventSource: Int, scrollX: Float, scrollY: Float, mouseX: Float, mouseY: Float) external fun internalJoystickEvent(timestamp: Long, deviceId: Int, eventSource: Int, leftX: FloatArray, leftY: FloatArray, rightX: FloatArray, rightY: FloatArray, hatX: FloatArray, hatY: FloatArray, leftTrigger: FloatArray, rightTrigger: FloatArray) external fun inputDeviceAdded(deviceId: Int) diff --git a/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt b/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt index 381029d..93bd501 100644 --- a/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt +++ b/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt @@ -8,11 +8,13 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.os.Message -import android.util.Log +import android.text.InputType import android.view.InputDevice import android.view.KeyEvent import android.view.MotionEvent import android.view.Surface +import android.view.WindowInsets +import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import androidx.annotation.RequiresApi import androidx.appcompat.widget.AppCompatEditText @@ -25,12 +27,54 @@ private const val HANDLER_CLOSE_IME_KEYBOARD = 3 private const val MS_TO_NS = 1_000_000 private const val SEC_TO_NS = 1_000_000_000.0 +fun inputTypeForFlags(i: Int): Int { + if ((i and BaseRobTopActivity.KEYBOARD_INPUT_NUMERIC) != 0) { + return InputType.TYPE_NUMBER_FLAG_DECIMAL or InputType.TYPE_NUMBER_FLAG_SIGNED or InputType.TYPE_CLASS_NUMBER + } + + if ((i and BaseRobTopActivity.KEYBOARD_INPUT_PASSWORD) != 0) { + return InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT + } + + if ((i and BaseRobTopActivity.KEYBOARD_INPUT_MULTILINE) != 0) { + InputType.TYPE_TEXT_FLAG_MULTI_LINE or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or InputType.TYPE_CLASS_TEXT + } + + return InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_CLASS_TEXT +} + +fun usesInlineKeyboard(): Boolean { + return Build.VERSION.SDK_INT >= 30 +} + +fun imeOptionsForFlags(i: Int): Int { + val i2 = if ((i and BaseRobTopActivity.KEYBOARD_INPUT_MULTILINE) == 0 || !usesInlineKeyboard()) + EditorInfo.IME_ACTION_DONE else EditorInfo.IME_ACTION_NONE + + return if (usesInlineKeyboard()) + EditorInfo.IME_FLAG_NO_FULLSCREEN or EditorInfo.IME_FLAG_NO_EXTRACT_UI or i2 + else i2 +} + class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { companion object { lateinit var cocos2dxGLSurfaceView: Cocos2dxGLSurfaceView private lateinit var handler: Handler private lateinit var cocos2dxTextInputWrapper: Cocos2dxTextInputWrapper + private var keyboardVisible = false + private var keyboardHeight = 0 + + private var keyboardHideCheck = Runnable { + if (keyboardVisible) { + return@Runnable + } + + cocos2dxGLSurfaceView.queueEvent { + cocos2dxGLSurfaceView.cocos2dxRenderer.handleTextClosed() + } + } + @JvmStatic fun openIMEKeyboard() { val msg = Message() @@ -50,6 +94,26 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { cocos2dxGLSurfaceView.queueEvent { Cocos2dxAccelerometer.onSensorChanged(x, y, z, timestamp) } + + @RequiresApi(Build.VERSION_CODES.R) + fun onKeyboardInsets(windowInsets: WindowInsets) { + val visible = windowInsets.isVisible(WindowInsets.Type.ime()) + val offsetHeight = if (visible) windowInsets.getInsets(WindowInsets.Type.ime()).bottom else 0 + + if (visible == keyboardVisible && offsetHeight == keyboardHeight) { + return + } + + keyboardVisible = visible + keyboardHeight = offsetHeight + + handler.removeCallbacks(keyboardHideCheck) + if (!visible) { + handler.postDelayed(keyboardHideCheck, 150L) + } else { + cocos2dxGLSurfaceView.cocos2dxRenderer.handleKeyboardWillShow(keyboardHeight) + } + } } private lateinit var cocos2dxRenderer: Cocos2dxRenderer @@ -65,6 +129,9 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { private set var supportsResizeEvents = false + // 2.209+ events for keyboard and such + var sendNewCocosEvents = false + var cocos2dxEditText: AppCompatEditText? = null set(value) { field = value @@ -85,9 +152,11 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { HANDLER_OPEN_IME_KEYBOARD -> { if (cocos2dxEditText?.requestFocus() == true) { cocos2dxEditText?.apply { - removeTextChangedListener( - cocos2dxTextInputWrapper - ) + removeTextChangedListener(cocos2dxTextInputWrapper) + + inputType = inputTypeForFlags(BaseRobTopActivity.curKeyboardInputFlags) + imeOptions = imeOptionsForFlags(BaseRobTopActivity.curKeyboardInputFlags) + setText("") val text = msg.obj as String append(text) @@ -100,16 +169,13 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { cocos2dxEditText, 0 ) - Log.d("GLSurfaceView", "showSoftInput") return } return } HANDLER_CLOSE_IME_KEYBOARD -> { cocos2dxEditText?.apply { - removeTextChangedListener( - cocos2dxTextInputWrapper - ) + removeTextChangedListener(cocos2dxTextInputWrapper) (context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow( windowToken, 0 @@ -117,8 +183,10 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { } requestFocus() - Log.d("GLSurfaceView", "HideSoftInput") - cocos2dxRenderer.handleTextClosed() + + queueEvent { + cocos2dxRenderer.handleTextClosed() + } return } else -> return @@ -297,6 +365,58 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { } } + private fun queueKeyboardEvent(keyCode: Int, event: KeyEvent, down: Boolean) { + sendNextEventTimestamp(event.eventTime * MS_TO_NS) + + if (useKeyboardEvents) { + if (down) { + queueEvent { + GeodeUtils.nativeKeyDown(keyCode, event.modifiers, event.repeatCount != 0) + } + } else { + queueEvent { + GeodeUtils.nativeKeyUp(keyCode, event.modifiers) + } + } + return + } + + if (controllerEventsEnabled) { + queueEvent { + GeodeUtils.internalKeyEvent( + event.eventTime * MS_TO_NS, + event.deviceId, + event.source, + event.keyCode, + event.modifiers, + down, + event.repeatCount + ) + } + return + } + + if (sendNewCocosEvents) { + if (KeyEvent.isGamepadButton(keyCode)) { + return + } + + if (keyCode == KeyEvent.KEYCODE_ESCAPE && BaseRobTopActivity.blockBackButton) { + return + } + + val isRepeat = event.repeatCount > 0 + val eventTimestamp = event.eventTime / 1000.0 + + queueEvent { + cocos2dxRenderer.handleKeyboardEvent( + keyCode, down, isRepeat, eventTimestamp, + event.isShiftPressed, event.isCtrlPressed, event.isAltPressed + ) + } + } + } + private fun legacyKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean { return when (keyCode) { KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_MENU -> { @@ -354,67 +474,51 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { } override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { - if (!useKeyboardEvents && !controllerEventsEnabled) { + val deliverGeodeEvents = useKeyboardEvents || controllerEventsEnabled + if (!deliverGeodeEvents && !sendNewCocosEvents) { return legacyKeyDown(keyCode, event) } - return when (keyCode) { - // ignore system keys - KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_MUTE -> { - super.onKeyDown(keyCode, event) + if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) { + if (deliverGeodeEvents) { + queueKeyboardEvent(keyCode, event, true) + return true } - else -> { - if (BaseRobTopActivity.blockBackButton) { - return true - } - queueEvent { - sendNextEventTimestamp(event.eventTime * MS_TO_NS) - - if (useKeyboardEvents) { - GeodeUtils.nativeKeyDown(keyCode, event.modifiers, event.repeatCount != 0) - } - - if (controllerEventsEnabled) { - GeodeUtils.internalKeyEvent(event.eventTime * MS_TO_NS, event.deviceId, event.source, event.keyCode, event.modifiers, true, event.repeatCount) - } - } - true + if (event.repeatCount != 0 || BaseRobTopActivity.blockBackButton) { + return true } + + queueEvent { + cocos2dxRenderer.handleKeyDown(keyCode) + } + return true } + + if (event.isSystem) { + return super.onKeyDown(keyCode, event) + } + + queueKeyboardEvent(keyCode, event, true) + return true } override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { - if (!useKeyboardEvents && !controllerEventsEnabled) { + val deliverGeodeEvents = useKeyboardEvents || controllerEventsEnabled + if (!deliverGeodeEvents && !sendNewCocosEvents) { return super.onKeyUp(keyCode, event) } - return when (keyCode) { - KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_MUTE -> { - super.onKeyUp(keyCode, event) - } - else -> { - if (event.repeatCount != 0 || BaseRobTopActivity.blockBackButton) { - return true - } - - queueEvent { - sendNextEventTimestamp(event.eventTime * MS_TO_NS) - - if (useKeyboardEvents) { - GeodeUtils.nativeKeyUp(keyCode, event.modifiers) - } - - if (controllerEventsEnabled) { - GeodeUtils.internalKeyEvent(event.eventTime * MS_TO_NS, event.deviceId, event.source, event.keyCode, event.modifiers, false, event.repeatCount) - } - } - - true - } + if (event.isSystem) { + return super.onKeyUp(keyCode, event) } + + queueKeyboardEvent(keyCode, event, false) + return true } + + // i'm lazy. // https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input private fun getCenteredAxis( @@ -475,11 +579,15 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { } override fun onGenericMotionEvent(event: MotionEvent?): Boolean { - if (!useKeyboardEvents && !controllerEventsEnabled) { + val hasGeodeEvents = useKeyboardEvents || controllerEventsEnabled + if (!hasGeodeEvents && !sendNewCocosEvents) { return super.onGenericMotionEvent(event) } if (event?.action == MotionEvent.ACTION_SCROLL) { + val mouseX = event.x + val mouseY = event.y + val scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL) val scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL) val timestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @@ -493,7 +601,11 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { } if (controllerEventsEnabled) { - GeodeUtils.internalScrollEvent(timestamp, event.deviceId, event.source, scrollX, scrollY) + GeodeUtils.internalScrollEvent(timestamp, event.deviceId, event.source, scrollX, scrollY, mouseX, mouseY) + } + + if (!hasGeodeEvents && sendNewCocosEvents) { + cocos2dxRenderer.handleScroll(mouseX, mouseY, scrollY, scrollX) } } diff --git a/app/src/main/java/org/cocos2dx/lib/Cocos2dxRenderer.kt b/app/src/main/java/org/cocos2dx/lib/Cocos2dxRenderer.kt index f81e5cb..f97b314 100644 --- a/app/src/main/java/org/cocos2dx/lib/Cocos2dxRenderer.kt +++ b/app/src/main/java/org/cocos2dx/lib/Cocos2dxRenderer.kt @@ -28,12 +28,29 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi @JvmStatic private external fun nativeKeyDown(keyCode: Int): Boolean + @JvmStatic + private external fun nativeKeyboardEvent( + keyCode: Int, + down: Boolean, + isRepeat: Boolean, + timestamp: Double, + shiftPressed: Boolean, + ctrlPressed: Boolean, + altPressed: Boolean + ) + + @JvmStatic + private external fun nativeKeyboardWillShow(offset: Int) + @JvmStatic private external fun nativeOnPause() @JvmStatic private external fun nativeOnResume() + @JvmStatic + private external fun nativeScroll(x: Float, y: Float, scrollY: Float, scrollX: Float) + @JvmStatic private external fun nativeRender() @@ -133,6 +150,18 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi nativeKeyDown(keyCode) } + fun handleKeyboardWillShow(offset: Int) { + nativeKeyboardWillShow(offset) + } + + fun handleKeyboardEvent(keyCode: Int, down: Boolean, isRepeat: Boolean, timestamp: Double, shiftPressed: Boolean, ctrlPressed: Boolean, altPressed: Boolean) { + nativeKeyboardEvent(keyCode, down, isRepeat, timestamp, shiftPressed, ctrlPressed, altPressed) + } + + fun handleScroll(x: Float, y: Float, scrollY: Float, scrollX: Float) { + nativeScroll(x, y, scrollY, scrollX) + } + fun handleOnPause() { nativeOnPause() } From c8994c248481f3487b039dc48d97f7b9ec7f4735 Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 16 Oct 2026 17:48:35 -0500 Subject: [PATCH 2/5] add restart with safe mode util --- app/src/main/java/com/geode/launcher/MainActivity.kt | 6 ++++-- app/src/main/java/com/geode/launcher/utils/GeodeUtils.kt | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/geode/launcher/MainActivity.kt b/app/src/main/java/com/geode/launcher/MainActivity.kt index f0dfaa3..85b1284 100644 --- a/app/src/main/java/com/geode/launcher/MainActivity.kt +++ b/app/src/main/java/com/geode/launcher/MainActivity.kt @@ -104,8 +104,10 @@ class MainActivity : ComponentActivity() { val gdInstalled = GamePackageUtils.isGameInstalled(packageManager) val geodeInstalled = LaunchUtils.isGeodeInstalled(this) - if (gdInstalled && geodeInstalled && intent.getBooleanExtra("restarted", false)) { - onLaunch(this) + val restartImmediately = intent.getBooleanExtra("restarted", false) + if (gdInstalled && geodeInstalled && restartImmediately) { + val withSafeMode = intent.getBooleanExtra("safe-mode", false) + onLaunch(this, withSafeMode) } enableEdgeToEdge() 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 44f0afa..f21c0ee 100644 --- a/app/src/main/java/com/geode/launcher/utils/GeodeUtils.kt +++ b/app/src/main/java/com/geode/launcher/utils/GeodeUtils.kt @@ -191,10 +191,16 @@ object GeodeUtils { @JvmStatic fun restartGame() { + restartGame(true) + } + + @JvmStatic + fun restartGame(safeMode: Boolean) { activity.get()?.run { packageManager.getLaunchIntentForPackage(packageName)?.also { val mainIntent = Intent.makeRestartActivityTask(it.component) mainIntent.putExtra("restarted", true) + mainIntent.putExtra("safe-mode", safeMode) startActivity(mainIntent) exitProcess(0) } From e0a31694af61348414dd0636508e8a0141eaa275 Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 16 Oct 2026 17:52:38 -0500 Subject: [PATCH 3/5] i accidentally defaulted it to true --- app/src/main/java/com/geode/launcher/utils/GeodeUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f21c0ee..7f4d44f 100644 --- a/app/src/main/java/com/geode/launcher/utils/GeodeUtils.kt +++ b/app/src/main/java/com/geode/launcher/utils/GeodeUtils.kt @@ -191,7 +191,7 @@ object GeodeUtils { @JvmStatic fun restartGame() { - restartGame(true) + restartGame(false) } @JvmStatic From e40af4c49b1f0e6e62616f642731cb20b9180723 Mon Sep 17 00:00:00 2001 From: Chloe Date: Sat, 17 Oct 2026 02:43:07 -0500 Subject: [PATCH 4/5] implement new file pickers --- .../com/customRobTop/BaseRobTopActivity.kt | 222 +++++++++++++++++- .../main/java/com/customRobTop/JniToCpp.kt | 12 + .../contracts/ImportFileActivityContract.kt | 40 ++++ .../contracts/ShareFileActivityContract.kt | 40 ++++ 4 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/customRobTop/contracts/ImportFileActivityContract.kt create mode 100644 app/src/main/java/com/customRobTop/contracts/ShareFileActivityContract.kt diff --git a/app/src/main/java/com/customRobTop/BaseRobTopActivity.kt b/app/src/main/java/com/customRobTop/BaseRobTopActivity.kt index 79a4c2f..bdf00c8 100644 --- a/app/src/main/java/com/customRobTop/BaseRobTopActivity.kt +++ b/app/src/main/java/com/customRobTop/BaseRobTopActivity.kt @@ -9,13 +9,28 @@ import android.content.Context import android.content.Intent import android.net.ConnectivityManager import android.net.NetworkCapabilities +import android.net.Uri import android.os.Build +import android.provider.DocumentsContract +import android.provider.OpenableColumns import android.provider.Settings import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity import androidx.core.net.toUri +import com.customRobTop.contracts.ImportFileActivityContract +import com.customRobTop.contracts.ShareFileActivityContract import com.geode.launcher.R +import com.geode.launcher.utils.DownloadUtils +import com.geode.launcher.utils.LaunchUtils +import org.cocos2dx.lib.Cocos2dxGLSurfaceView import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.closeIMEKeyboard import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.openIMEKeyboard +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.OutputStream import java.lang.ref.WeakReference import java.util.UUID @@ -24,6 +39,7 @@ object BaseRobTopActivity { const val KEYBOARD_INPUT_MULTILINE = 1 const val KEYBOARD_INPUT_NUMERIC = 4 const val KEYBOARD_INPUT_PASSWORD = 2 + const val LEVEL_EXTENSION: String = ".gdlvl" private var isLoaded = false var blockBackButton = false @@ -35,8 +51,57 @@ object BaseRobTopActivity { lateinit var me: WeakReference private var shouldResumeSound = true - fun setCurrentActivity(currentActivity: Activity) { + private var pendingExportPaths = emptyList() + + private lateinit var importFileActivityContract: ActivityResultLauncher + private lateinit var shareFileActivityContract: ActivityResultLauncher + private lateinit var shareFolderActivityContract: ActivityResultLauncher + + fun setCurrentActivity(currentActivity: AppCompatActivity) { me = WeakReference(currentActivity) + + importFileActivityContract = currentActivity.registerForActivityResult(ImportFileActivityContract()) { uris -> + val filePaths = copyPickedFiles(uris) + + Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent { + JniToCpp.filesPicked(filePaths.toTypedArray()) + } + } + + shareFileActivityContract = currentActivity.registerForActivityResult(ShareFileActivityContract()) { uri -> + val currentlyPending = pendingExportPaths + pendingExportPaths = emptyList() + + val exportPath = currentlyPending.firstOrNull() + if (exportPath == null || uri == null) { + Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent { + JniToCpp.fileShareFinished(false) + } + return@registerForActivityResult + } + + val result = saveToDocument(exportPath, uri) + Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent { + JniToCpp.fileShareFinished(result) + } + } + + shareFolderActivityContract = currentActivity.registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> + val currentlyPending = pendingExportPaths + pendingExportPaths = emptyList() + + if (uri == null) { + Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent { + JniToCpp.fileShareFinished(false) + } + return@registerForActivityResult + } + + val result = saveToFolder(currentlyPending, uri) > 0 + Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent { + JniToCpp.fileShareFinished(result) + } + } } @SuppressLint("HardwareIds") @@ -289,4 +354,159 @@ object BaseRobTopActivity { fun setKeyboardInputFlags(flags: Int) { curKeyboardInputFlags = flags } + + @JvmStatic + fun pickImportFiles() { + try { + importFileActivityContract.launch(Unit) + } catch (_: ActivityNotFoundException) { + me.get()?.apply { + runOnUiThread { + Toast.makeText( + this, + R.string.no_activity_found, + Toast.LENGTH_SHORT, + ).show() + } + } + } + } + + private fun copyPickedFiles(uris: List): List { + val me = me.get() ?: return emptyList() + + val importsDir = File(LaunchUtils.getSaveDirectory(me), "imports") + importsDir.mkdirs() + + return uris.mapIndexedNotNull { index, uri -> + val displayName = displayName(uri) + ?: "import_$index$LEVEL_EXTENSION" + if (!displayName.endsWith(LEVEL_EXTENSION)) { + return@mapIndexedNotNull null + } + + var outputFile = File(importsDir, displayName) + var counter = 2 + while (outputFile.exists()) { + outputFile = File(importsDir, "${counter}_$displayName") + counter++ + } + + try { + me.contentResolver.openInputStream(uri)?.use { input -> + outputFile.outputStream().use { output -> + DownloadUtils.copyFile(input, output) + } + + outputFile.absolutePath + } + } catch (_: Exception) { + outputFile.delete() + null + } + } + } + + private fun displayName(uri: Uri): String? { + me.get()?.contentResolver?.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursorQuery -> + if (cursorQuery.moveToFirst()) { + val displayName = cursorQuery.getString(0) + if (displayName.isNotEmpty()) { + return File(displayName).name + } + } + } + + return null + } + + @JvmStatic + fun shareFiles(files: Array) { + pendingExportPaths = files.toList() + + try { + if (files.size == 1) { + shareFileActivityContract.launch(files.first()) + return + } else if (files.size > 1) { + shareFolderActivityContract.launch(null) + return + } + } catch (_: ActivityNotFoundException) { + me.get()?.apply { + runOnUiThread { + Toast.makeText( + this, + R.string.no_activity_found, + Toast.LENGTH_SHORT, + ).show() + } + } + + Cocos2dxGLSurfaceView.cocos2dxGLSurfaceView.queueEvent { + JniToCpp.fileShareFinished(false) + } + } + + } + + private fun saveToFolder(exportPaths: List, treeUri: Uri): Int { + val uri = DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri)) + + val me = me.get() ?: return 0 + + return exportPaths.count { path -> + val filename = File(path).name + val documentUri = DocumentsContract.createDocument(me.contentResolver, uri, "application/octet-stream", filename) + ?: return@count false + + saveToDocument(path, documentUri) + } + } + + private fun saveToDocument(exportPath: String, uri: Uri): Boolean { + try { + FileInputStream(exportPath).use { inputStream -> + openOutput(uri).use { outputStream -> + if (outputStream != null) { + DownloadUtils.copyFile(inputStream, outputStream) + } + } + } + + ensureLevelExtension(uri) + return true + } catch (_: IOException) { + return false + } + } + + private fun openOutput(uri: Uri): OutputStream? { + val me = me.get() ?: return null + return try { + me.contentResolver.openOutputStream(uri, "wt") + } catch (_: IOException) { + me.contentResolver.openOutputStream(uri) + } + } + + private fun ensureLevelExtension(uri: Uri) { + val displayName = displayName(uri) + if (displayName == null || displayName.endsWith(LEVEL_EXTENSION)) { + return + } + val me = me.get() ?: return + + val extensionIdx = displayName.lastIndexOf(".gdlvl.") + val substring = if (extensionIdx < 0) { + displayName + LEVEL_EXTENSION + } else { + displayName.substring(0, extensionIdx + 6) + } + + try { + DocumentsContract.renameDocument(me.contentResolver, uri, substring) + } catch (_: IOException) { + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/customRobTop/JniToCpp.kt b/app/src/main/java/com/customRobTop/JniToCpp.kt index f952b1d..4f547b1 100644 --- a/app/src/main/java/com/customRobTop/JniToCpp.kt +++ b/app/src/main/java/com/customRobTop/JniToCpp.kt @@ -55,4 +55,16 @@ object JniToCpp { @JvmStatic external fun videoAdShowed() + + @JvmStatic + external fun filesPicked(files: Array) + + @JvmStatic + external fun fileShareFinished(success: Boolean) + + @JvmStatic + external fun textInputCaretToEnd() + + @JvmStatic + external fun textInputDone() } \ No newline at end of file diff --git a/app/src/main/java/com/customRobTop/contracts/ImportFileActivityContract.kt b/app/src/main/java/com/customRobTop/contracts/ImportFileActivityContract.kt new file mode 100644 index 0000000..7fed2b5 --- /dev/null +++ b/app/src/main/java/com/customRobTop/contracts/ImportFileActivityContract.kt @@ -0,0 +1,40 @@ +package com.customRobTop.contracts + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.activity.result.contract.ActivityResultContract + +class ImportFileActivityContract : ActivityResultContract>() { + override fun createIntent( + context: Context, + input: Unit + ): Intent { + return Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "*/*" + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + } + } + + override fun parseResult( + resultCode: Int, + intent: Intent? + ): List { + if (resultCode != Activity.RESULT_OK || intent == null) { + return emptyList() + } + + val clipData = intent.clipData + return if (clipData != null) { + List(clipData.itemCount) { i -> clipData.getItemAt(i).uri } + } else { + val intentData = intent.data + if (intentData != null) + listOf(intentData) + else + emptyList() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/customRobTop/contracts/ShareFileActivityContract.kt b/app/src/main/java/com/customRobTop/contracts/ShareFileActivityContract.kt new file mode 100644 index 0000000..ca69eb9 --- /dev/null +++ b/app/src/main/java/com/customRobTop/contracts/ShareFileActivityContract.kt @@ -0,0 +1,40 @@ +package com.customRobTop.contracts + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.DocumentsContract +import androidx.activity.result.contract.ActivityResultContract +import java.io.File + +class ShareFileActivityContract : ActivityResultContract() { + override fun createIntent( + context: Context, + input: String + ): Intent { + return Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/octet-stream" + putExtra(Intent.EXTRA_TITLE, File(input).name) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + putExtra(DocumentsContract.EXTRA_INITIAL_URI, + DocumentsContract.buildDocumentUri("com.android.externalstorage.documents", "primary:Download") + ) + } + } + } + + override fun parseResult( + resultCode: Int, + intent: Intent? + ): Uri? { + if (resultCode != Activity.RESULT_OK || intent == null) { + return null + } + + return intent.data + } +} \ No newline at end of file From 3d60bd3d53e815894aa1ef3b3bf499c35e7a5e6d Mon Sep 17 00:00:00 2001 From: Chloe Date: Sat, 17 Oct 2026 02:50:27 -0500 Subject: [PATCH 5/5] do textinputwrapper changes --- .../org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt | 2 +- .../cocos2dx/lib/Cocos2dxTextInputWrapper.kt | 88 +++++++++++++------ 2 files changed, 63 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt b/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt index 93bd501..5f90add 100644 --- a/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt +++ b/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt @@ -37,7 +37,7 @@ fun inputTypeForFlags(i: Int): Int { } if ((i and BaseRobTopActivity.KEYBOARD_INPUT_MULTILINE) != 0) { - InputType.TYPE_TEXT_FLAG_MULTI_LINE or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or InputType.TYPE_CLASS_TEXT + return InputType.TYPE_TEXT_FLAG_MULTI_LINE or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or InputType.TYPE_CLASS_TEXT } return InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_CLASS_TEXT diff --git a/app/src/main/java/org/cocos2dx/lib/Cocos2dxTextInputWrapper.kt b/app/src/main/java/org/cocos2dx/lib/Cocos2dxTextInputWrapper.kt index 6fab41d..b2e3b80 100644 --- a/app/src/main/java/org/cocos2dx/lib/Cocos2dxTextInputWrapper.kt +++ b/app/src/main/java/org/cocos2dx/lib/Cocos2dxTextInputWrapper.kt @@ -5,8 +5,10 @@ import android.text.TextWatcher import android.widget.TextView.OnEditorActionListener import android.text.Editable import android.view.KeyEvent +import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.TextView +import com.customRobTop.JniToCpp class Cocos2dxTextInputWrapper(private val cocos2dxGLSurfaceView: Cocos2dxGLSurfaceView) : TextWatcher, OnEditorActionListener { private var originText: String? = null @@ -19,21 +21,33 @@ class Cocos2dxTextInputWrapper(private val cocos2dxGLSurfaceView: Cocos2dxGLSurf this.originText = originText } - override fun afterTextChanged(s: Editable) { - if (!isFullScreenEdit) { - var nModified = s.length - text!!.length - if (nModified > 0) { - cocos2dxGLSurfaceView.insertText( - s.subSequence(text!!.length, s.length).toString() - ) - } else { - while (nModified < 0) { - cocos2dxGLSurfaceView.deleteBackward() - nModified++ - } - } - text = s.toString() + override fun afterTextChanged(editable: Editable) { + if (isFullScreenEdit) { + return } + + val cText = text ?: return + + val str = editable.toString() + val unchanged = str.commonPrefixWith(cText) + + var unchangedUntil = unchanged.length + if (unchangedUntil > 0 && unchangedUntil < str.length && Character.isLowSurrogate(unchanged.last())) { + unchangedUntil-- + } + + val codePointLength = cText.codePointCount(unchangedUntil, cText.length) + repeat(codePointLength) { + cocos2dxGLSurfaceView.deleteBackward() + } + + if (unchangedUntil < str.length) { + cocos2dxGLSurfaceView.insertText( + str.substring(unchangedUntil) + ) + } + + text = str } override fun beforeTextChanged( @@ -47,22 +61,44 @@ class Cocos2dxTextInputWrapper(private val cocos2dxGLSurfaceView: Cocos2dxGLSurf override fun onTextChanged(pCharSequence: CharSequence, start: Int, before: Int, count: Int) {} override fun onEditorAction(textView: TextView, actionID: Int, keyEvent: KeyEvent?): Boolean { - if (cocos2dxGLSurfaceView.cocos2dxEditText == textView && isFullScreenEdit) { - for (i in originText!!.length downTo 1) { - cocos2dxGLSurfaceView.deleteBackward() - } + val fullscreenIme = cocos2dxGLSurfaceView.cocos2dxEditText == textView && isFullScreenEdit + if (fullscreenIme) { var text = textView.text.toString() - if (!text.endsWith('\n')) { - text += '\n' - } + if (originText != text) { + if (cocos2dxGLSurfaceView.sendNewCocosEvents) { + cocos2dxGLSurfaceView.queueEvent { + JniToCpp.textInputCaretToEnd() + } + } - cocos2dxGLSurfaceView.insertText(text) + for (i in originText!!.length downTo 1) { + cocos2dxGLSurfaceView.deleteBackward() + } + + // this bit was removed in 2.209, and I don't know if it's important on previous versions + if (!cocos2dxGLSurfaceView.sendNewCocosEvents) { + if (!text.endsWith('\n')) { + text += '\n' + } + } + + cocos2dxGLSurfaceView.insertText(text) + } } - if (actionID != 6) { - return false + + if (fullscreenIme || actionID == EditorInfo.IME_ACTION_DONE) { + if (cocos2dxGLSurfaceView.sendNewCocosEvents) { + cocos2dxGLSurfaceView.queueEvent { + JniToCpp.textInputDone() + } + } } - cocos2dxGLSurfaceView.requestFocus() - Cocos2dxGLSurfaceView.closeIMEKeyboard() + + if (actionID == EditorInfo.IME_ACTION_DONE) { + cocos2dxGLSurfaceView.requestFocus() + Cocos2dxGLSurfaceView.closeIMEKeyboard() + } + return false } } \ No newline at end of file