From 3716f6b9b63ee5271fe5a1c4327972e02158834d Mon Sep 17 00:00:00 2001 From: huesos_96 Date: Fri, 3 Oct 2025 07:45:49 -0600 Subject: [PATCH] Android: Dual screen fixes for Handhelds that have 2 screens like Ayaneo Pocket DS (#1341) * Prevent SecondaryDisplay from stealing focus The SecondaryDisplay Activity was stealing focus from the main Activity when it was launched. Set the `FLAG_NOT_FOCUSABLE` and `FLAG_NOT_TOUCH_MODAL` window flags to prevent the SecondaryDisplay from gaining focus. * Implement touch controls for secondary display This commit introduces touch input handling for the secondary display. The following changes were made: - Added `onSecondaryTouchEvent` and `onSecondaryTouchMoved` to `NativeLibrary.kt` and `native.cpp` to process touch events on the secondary display. - Implemented `onTouchListener` in `SecondaryDisplay.kt` to capture touch events and forward them to the native layer. - Handles `ACTION_DOWN`, `ACTION_POINTER_DOWN`, `ACTION_MOVE`, `ACTION_UP`, `ACTION_POINTER_UP`, and `ACTION_CANCEL` motion events. - Tracks the active pointer to ensure correct touch event handling. * Refactor display logic for multi-display support This commit introduces a `DisplayHelper` class to centralize display-related logic, particularly for handling scenarios where the application might be launched on an external display. Key changes: - Added `DisplayHelper.kt` to manage internal and external display identification based on launch conditions. - `MainActivity` and `EmulationActivity` now use `DisplayHelper.checkLaunchDisplay()` to determine the initial display. - `SecondaryDisplay` now uses `DisplayHelper.getExternalDisplay()` to correctly identify the target display for the secondary presentation. - `InputOverlay` now queries `DisplayHelper.isBottomOnPrimary()` to determine if touch input should be processed for the primary display based on the current screen layout. - `SecondaryDisplay` now queries `DisplayHelper.isBottomOnSecondary()` to conditionally pass touch events to the native layer based on which screen (primary or secondary) is currently displaying the 3DS bottom screen. These changes ensure that the application behaves correctly when launched on either the internal or an external display, and that touch input is routed appropriately based on the user's chosen screen layout for the dual screens. * Removed primary-screen checks so the input overlay always forwards touch events, ensuring all touches reach the native handler even when multiple displays are active * Remove DisplayHelper class and adjust external display logic * Formatting adjustments --------- Co-authored-by: DavidRGriswold Co-authored-by: OpenSauce04 --- .../java/org/citra/citra_emu/NativeLibrary.kt | 18 +++++ .../citra_emu/display/SecondaryDisplay.kt | 66 ++++++++++++++++--- .../citra/citra_emu/overlay/InputOverlay.kt | 3 +- src/android/app/src/main/jni/native.cpp | 19 ++++++ src/core/frontend/emu_window.cpp | 4 ++ 5 files changed, 98 insertions(+), 12 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt index eec388463..e53354dc9 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt @@ -96,6 +96,24 @@ object NativeLibrary { */ external fun onTouchMoved(xAxis: Float, yAxis: Float) + /** + * Handles touch events on the secondary display. + * + * @param xAxis The value of the x-axis. + * @param yAxis The value of the y-axis. + * @param pressed To identify if the touch held down or released. + * @return true if the pointer is within the touchscreen + */ + external fun onSecondaryTouchEvent(xAxis: Float, yAxis: Float, pressed: Boolean): Boolean + + /** + * Handles touch movement on the secondary display. + * + * @param xAxis The value of the instantaneous x-axis. + * @param yAxis The value of the instantaneous y-axis. + */ + external fun onSecondaryTouchMoved(xAxis: Float, yAxis: Float) + external fun reloadSettings() external fun getTitleId(filename: String): Long diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt index 709bb222e..e0594a7e5 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt @@ -11,11 +11,14 @@ import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.os.Bundle import android.view.Display +import android.view.MotionEvent import android.view.Surface import android.view.SurfaceHolder import android.view.SurfaceView -import org.citra.citra_emu.NativeLibrary +import android.view.WindowManager import org.citra.citra_emu.features.settings.model.IntSetting +import org.citra.citra_emu.display.SecondaryDisplayLayout +import org.citra.citra_emu.NativeLibrary class SecondaryDisplay(val context: Context) { private var pres: SecondaryDisplayPresentation? = null @@ -41,16 +44,23 @@ class SecondaryDisplay(val context: Context) { NativeLibrary.secondarySurfaceDestroyed() } + private fun getExternalDisplay(context: Context): Display? { + val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + val internalId = context.display.displayId ?: Display.DEFAULT_DISPLAY + val displays = dm.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION) + return displays.firstOrNull { it.displayId != internalId && it.name != "HiddenDisplay" } + } + fun updateDisplay() { // decide if we are going to the external display or the internal one - var display = getCustomerDisplay() + var display = getExternalDisplay(context) if (display == null || IntSetting.SECONDARY_DISPLAY_LAYOUT.int == SecondaryDisplayLayout.NONE.int) { display = vd.display } // if our presentation is already on the right display, ignore - if (pres?.display == display) return; + if (pres?.display == display) return // otherwise, make a new presentation releasePresentation() @@ -58,13 +68,6 @@ class SecondaryDisplay(val context: Context) { pres?.show() } - private fun getCustomerDisplay(): Display? { - val displays = displayManager.displays - // code taken from MelonDS dual screen - should fix odin 2 detection bug - return displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION) - .firstOrNull { it.displayId != Display.DEFAULT_DISPLAY && it.name != "Built-in Screen" && it.name != "HiddenDisplay"} - } - fun releasePresentation() { pres?.dismiss() pres = null @@ -78,9 +81,16 @@ class SecondaryDisplayPresentation( context: Context, display: Display, val parent: SecondaryDisplay ) : Presentation(context, display) { private lateinit var surfaceView: SurfaceView + private var touchscreenPointerId = -1 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + window?.setFlags( + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + ) // Initialize SurfaceView surfaceView = SurfaceView(context) @@ -100,6 +110,42 @@ class SecondaryDisplayPresentation( } }) + this.surfaceView.setOnTouchListener { _, event -> + + val pointerIndex = event.actionIndex + val pointerId = event.getPointerId(pointerIndex) + when (event.actionMasked) { + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { + if (touchscreenPointerId == -1) { + touchscreenPointerId = pointerId + NativeLibrary.onSecondaryTouchEvent( + event.getX(pointerIndex), + event.getY(pointerIndex), + true + ) + } + } + + MotionEvent.ACTION_MOVE -> { + val index = event.findPointerIndex(touchscreenPointerId) + if (index != -1) { + NativeLibrary.onSecondaryTouchMoved( + event.getX(index), + event.getY(index) + ) + } + } + + MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_CANCEL -> { + if (pointerId == touchscreenPointerId) { + NativeLibrary.onSecondaryTouchEvent(0f, 0f, false) + touchscreenPointerId = -1 + } + } + } + true + } + setContentView(surfaceView) // Set SurfaceView as content } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt index 9828dd046..f7519bb81 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt @@ -153,8 +153,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex if (isActionMove) { NativeLibrary.onTouchMoved(xPosition.toFloat(), yPosition.toFloat()) continue - } - else if (isActionUp) { + } else if (isActionUp) { NativeLibrary.onTouchEvent(0f, 0f, false) break // Up and down actions shouldn't loop } diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 2cb34cbd0..a2880d6dc 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -657,6 +657,25 @@ void Java_org_citra_citra_1emu_NativeLibrary_onTouchMoved([[maybe_unused]] JNIEn window->OnTouchMoved((int)x, (int)y); } +jboolean Java_org_citra_citra_1emu_NativeLibrary_onSecondaryTouchEvent([[maybe_unused]] JNIEnv* env, + [[maybe_unused]] jobject obj, + jfloat x, jfloat y, + jboolean pressed) { + if (!secondary_window) { + return JNI_FALSE; + } + return static_cast(secondary_window->OnTouchEvent( + static_cast(x + 0.5), static_cast(y + 0.5), pressed)); +} + +void Java_org_citra_citra_1emu_NativeLibrary_onSecondaryTouchMoved([[maybe_unused]] JNIEnv* env, + [[maybe_unused]] jobject obj, + jfloat x, jfloat y) { + if (secondary_window) { + secondary_window->OnTouchMoved((int)x, (int)y); + } +} + jlong Java_org_citra_citra_1emu_NativeLibrary_getTitleId(JNIEnv* env, [[maybe_unused]] jobject obj, jstring j_filename) { std::string filepath = GetJString(env, j_filename); diff --git a/src/core/frontend/emu_window.cpp b/src/core/frontend/emu_window.cpp index c352dbdf0..ee81b177e 100644 --- a/src/core/frontend/emu_window.cpp +++ b/src/core/frontend/emu_window.cpp @@ -66,6 +66,10 @@ bool EmuWindow::IsWithinTouchscreen(const Layout::FramebufferLayout& layout, uns } #endif + if (!layout.bottom_screen_enabled) { + return false; + } + Settings::StereoRenderOption render_3d_mode = Settings::values.render_3d.GetValue(); if (render_3d_mode == Settings::StereoRenderOption::SideBySide ||