mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 12:15:31 +02:00
Merge pull request #1458 from AverageConsumer/codex/android-asymmetric-display-routing
This commit is contained in:
@@ -740,6 +740,9 @@ palette-correct blit of either canvas into an arbitrary screen rect, and the
|
|||||||
the original contract. Its optional `background` (`0xRRGGBB`) and `preference`
|
the original contract. Its optional `background` (`0xRRGGBB`) and `preference`
|
||||||
arguments request an extended presentation; a preference ending in `:cover`
|
arguments request an extended presentation; a preference ending in `:cover`
|
||||||
fills and crops the target, while other values preserve the whole frame.
|
fills and crops the target, while other values preserve the whole frame.
|
||||||
|
Android also accepts `handheld` or `secondary` (with an optional `:cover`
|
||||||
|
suffix) as routing hints; unsupported or unavailable targets fall back to the
|
||||||
|
other connected display.
|
||||||
`pollTouch()` returns the oldest queued event as `"action,x,y"` in submitted-frame
|
`pollTouch()` returns the oldest queued event as `"action,x,y"` in submitted-frame
|
||||||
coordinates, or `nil`.
|
coordinates, or `nil`.
|
||||||
This is what lets a mod lay the two passes out as two stacked Game Boy screens,
|
This is what lets a mod lay the two passes out as two stacked Game Boy screens,
|
||||||
|
|||||||
@@ -31,6 +31,9 @@
|
|||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@drawable/love"
|
android:icon="@drawable/love"
|
||||||
android:label="${NAME}" >
|
android:label="${NAME}" >
|
||||||
|
<meta-data
|
||||||
|
android:name="android.allow_multiple_resumed_activities"
|
||||||
|
android:value="true" />
|
||||||
<activity
|
<activity
|
||||||
android:name="org.love2d.android.GameActivity"
|
android:name="org.love2d.android.GameActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -49,5 +52,15 @@
|
|||||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="org.love2d.android.GameActivity$SecondaryActivity"
|
||||||
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:resizeableActivity="false"
|
||||||
|
android:screenOrientation="${ORIENTATION}"
|
||||||
|
android:taskAffinity="${applicationId}.secondary"
|
||||||
|
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1229,6 +1229,20 @@ void love_android_secondary_enable(int on)
|
|||||||
env->DeleteLocalRef(activity);
|
env->DeleteLocalRef(activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C" __attribute__((visibility("default")))
|
||||||
|
void love_android_secondary_target(int target)
|
||||||
|
{
|
||||||
|
JNIEnv *env = (JNIEnv*) SDL_AndroidGetJNIEnv();
|
||||||
|
jclass activity = env->FindClass("org/love2d/android/GameActivity");
|
||||||
|
jmethodID method = env->GetStaticMethodID(activity,
|
||||||
|
"setSecondaryDisplayTarget", "(I)V");
|
||||||
|
if (method)
|
||||||
|
env->CallStaticVoidMethod(activity, method, target);
|
||||||
|
else
|
||||||
|
env->ExceptionClear();
|
||||||
|
env->DeleteLocalRef(activity);
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" __attribute__((visibility("default")))
|
extern "C" __attribute__((visibility("default")))
|
||||||
int love_android_secondary_detected()
|
int love_android_secondary_detected()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import android.os.Environment;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.Vibrator;
|
import android.os.Vibrator;
|
||||||
|
import android.provider.Settings;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
import android.view.*;
|
import android.view.*;
|
||||||
@@ -387,10 +388,23 @@ public class GameActivity extends SDLActivity {
|
|||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
onHostResume();
|
onHostResume();
|
||||||
|
refreshDualScreenDisplayMode();
|
||||||
if (secondaryEnabled) registerSecondaryDisplayListener();
|
if (secondaryEnabled) registerSecondaryDisplayListener();
|
||||||
setupSecondaryDisplay();
|
setupSecondaryDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||||
|
// AYN's panel toggle emits virtual Right Shift, which SDL maps to a
|
||||||
|
// gameplay button. The setting is absent on other Android devices.
|
||||||
|
if (secondaryEnabled && dualScreenDisplayMode != -1
|
||||||
|
&& event.getKeyCode() == KeyEvent.KEYCODE_SHIFT_RIGHT
|
||||||
|
&& event.getDeviceId() == KeyCharacterMap.VIRTUAL_KEYBOARD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.dispatchKeyEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SDL decides the activity's requested orientation at window creation
|
* SDL decides the activity's requested orientation at window creation
|
||||||
* (SDLActivity.setOrientationBis). With a resizable window and no
|
* (SDLActivity.setOrientationBis). With a resizable window and no
|
||||||
@@ -1476,8 +1490,21 @@ public class GameActivity extends SDLActivity {
|
|||||||
// Dual-screen: mirror the engine's bottom-screen canvas onto a secondary
|
// Dual-screen: mirror the engine's bottom-screen canvas onto a secondary
|
||||||
// physical display. Driven from the engine through love_android_secondary_*
|
// physical display. Driven from the engine through love_android_secondary_*
|
||||||
// in src/jni/love/src/common/android.cpp.
|
// in src/jni/love/src/common/android.cpp.
|
||||||
|
private static final int SECONDARY_TARGET_AUTO = 0;
|
||||||
|
private static final int SECONDARY_TARGET_HANDHELD = 1;
|
||||||
|
private static final int SECONDARY_TARGET_EXTERNAL = 2;
|
||||||
|
// AYN keeps disabled panels registered as ON. This optional setting is the
|
||||||
|
// usable-state signal: 0 = both, 1 = main only, 2 = second only.
|
||||||
|
private static final String DUAL_SCREEN_DISPLAY_MODE = "dual_screen_display_mode";
|
||||||
|
private static final String AYN_SECOND_SCREEN = "Screen-2";
|
||||||
private static volatile SecondaryPresentation secondaryPresentation;
|
private static volatile SecondaryPresentation secondaryPresentation;
|
||||||
|
private static volatile SecondaryActivity secondaryActivity;
|
||||||
|
private static volatile boolean secondaryActivityPending;
|
||||||
|
private static volatile int secondaryActivityTarget = Display.INVALID_DISPLAY;
|
||||||
|
private static volatile long secondaryRetryAfter;
|
||||||
private static volatile boolean secondaryEnabled = false;
|
private static volatile boolean secondaryEnabled = false;
|
||||||
|
private static volatile int secondaryTarget = SECONDARY_TARGET_AUTO;
|
||||||
|
private static volatile int dualScreenDisplayMode = -1;
|
||||||
private static volatile byte[] secondaryFrame;
|
private static volatile byte[] secondaryFrame;
|
||||||
private static volatile int secondaryFrameWidth;
|
private static volatile int secondaryFrameWidth;
|
||||||
private static volatile int secondaryFrameHeight;
|
private static volatile int secondaryFrameHeight;
|
||||||
@@ -1487,6 +1514,14 @@ public class GameActivity extends SDLActivity {
|
|||||||
private static volatile long secondaryDetectionAt;
|
private static volatile long secondaryDetectionAt;
|
||||||
private static volatile boolean secondaryDetected;
|
private static volatile boolean secondaryDetected;
|
||||||
private SecondaryDisplayMonitor secondaryDisplayMonitor;
|
private SecondaryDisplayMonitor secondaryDisplayMonitor;
|
||||||
|
private boolean dualScreenModeObserverRegistered;
|
||||||
|
private final android.database.ContentObserver dualScreenModeObserver =
|
||||||
|
new android.database.ContentObserver(new Handler(Looper.getMainLooper())) {
|
||||||
|
@Override public void onChange(boolean selfChange, Uri uri) {
|
||||||
|
refreshDualScreenDisplayMode();
|
||||||
|
rebindSecondaryDisplay();
|
||||||
|
}
|
||||||
|
};
|
||||||
private static final int MAX_SECONDARY_TOUCHES = 32;
|
private static final int MAX_SECONDARY_TOUCHES = 32;
|
||||||
private static final java.util.ArrayDeque<String> secondaryTouches =
|
private static final java.util.ArrayDeque<String> secondaryTouches =
|
||||||
new java.util.ArrayDeque<>();
|
new java.util.ArrayDeque<>();
|
||||||
@@ -1499,56 +1534,125 @@ public class GameActivity extends SDLActivity {
|
|||||||
self.runOnUiThread(new Runnable() {
|
self.runOnUiThread(new Runnable() {
|
||||||
@Override public void run() {
|
@Override public void run() {
|
||||||
if (on) {
|
if (on) {
|
||||||
|
self.refreshDualScreenDisplayMode();
|
||||||
self.registerSecondaryDisplayListener();
|
self.registerSecondaryDisplayListener();
|
||||||
setupSecondaryDisplay();
|
rebindSecondaryDisplay();
|
||||||
} else {
|
} else {
|
||||||
self.unregisterSecondaryDisplayListener();
|
self.unregisterSecondaryDisplayListener();
|
||||||
teardownSecondaryDisplay();
|
teardownSecondaryDisplay();
|
||||||
|
secondaryRetryAfter = 0;
|
||||||
synchronized (secondaryFrameLock) { secondaryFrame = null; }
|
synchronized (secondaryFrameLock) { secondaryFrame = null; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public static void setSecondaryDisplayTarget(int target) {
|
||||||
|
int normalized = target == SECONDARY_TARGET_HANDHELD
|
||||||
|
|| target == SECONDARY_TARGET_EXTERNAL ? target : SECONDARY_TARGET_AUTO;
|
||||||
|
if (secondaryTarget == normalized) return;
|
||||||
|
secondaryTarget = normalized;
|
||||||
|
secondaryDetectionAt = 0;
|
||||||
|
rebindSecondaryDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshDualScreenDisplayMode() {
|
||||||
|
int mode = Settings.System.getInt(
|
||||||
|
getContentResolver(), DUAL_SCREEN_DISPLAY_MODE, -1);
|
||||||
|
if (dualScreenDisplayMode != mode) secondaryDetectionAt = 0;
|
||||||
|
dualScreenDisplayMode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
private void registerSecondaryDisplayListener() {
|
private void registerSecondaryDisplayListener() {
|
||||||
if (secondaryDisplayMonitor != null || android.os.Build.VERSION.SDK_INT < 17) return;
|
if (secondaryDisplayMonitor == null && android.os.Build.VERSION.SDK_INT >= 17) {
|
||||||
SecondaryDisplayMonitor monitor = new SecondaryDisplayMonitor(this);
|
SecondaryDisplayMonitor monitor = new SecondaryDisplayMonitor(this);
|
||||||
if (monitor.register()) secondaryDisplayMonitor = monitor;
|
if (monitor.register()) secondaryDisplayMonitor = monitor;
|
||||||
|
}
|
||||||
|
if (dualScreenDisplayMode != -1 && !dualScreenModeObserverRegistered) {
|
||||||
|
getContentResolver().registerContentObserver(
|
||||||
|
Settings.System.getUriFor(DUAL_SCREEN_DISPLAY_MODE), false,
|
||||||
|
dualScreenModeObserver);
|
||||||
|
dualScreenModeObserverRegistered = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unregisterSecondaryDisplayListener() {
|
private void unregisterSecondaryDisplayListener() {
|
||||||
SecondaryDisplayMonitor monitor = secondaryDisplayMonitor;
|
SecondaryDisplayMonitor monitor = secondaryDisplayMonitor;
|
||||||
secondaryDisplayMonitor = null;
|
secondaryDisplayMonitor = null;
|
||||||
if (monitor != null) monitor.unregister();
|
if (monitor != null) monitor.unregister();
|
||||||
|
if (dualScreenModeObserverRegistered) {
|
||||||
|
getContentResolver().unregisterContentObserver(dualScreenModeObserver);
|
||||||
|
dualScreenModeObserverRegistered = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void refreshSecondaryDisplay() {
|
private static boolean secondaryOutputIsPreferred(GameActivity self) {
|
||||||
GameActivity self = (GameActivity) mSingleton;
|
Display preferred = findSecondaryDisplay(self, false);
|
||||||
if (self == null || !secondaryEnabled) return;
|
if (preferred == null) return false;
|
||||||
SecondaryPresentation current = secondaryPresentation;
|
SecondaryPresentation presentation = secondaryPresentation;
|
||||||
Display display = current == null ? null : current.getDisplay();
|
Display display = presentation == null
|
||||||
|
? null : presentation.getDisplay();
|
||||||
|
if (display == null) {
|
||||||
|
SecondaryActivity activity = secondaryActivity;
|
||||||
|
display = activity == null ? null : getActivityDisplay(activity);
|
||||||
|
}
|
||||||
SecondaryDisplayMonitor monitor = self.secondaryDisplayMonitor;
|
SecondaryDisplayMonitor monitor = self.secondaryDisplayMonitor;
|
||||||
if (current == null) {
|
if (display == null || (monitor != null
|
||||||
setupSecondaryDisplay();
|
&& !monitor.hasDisplay(display.getDisplayId()))) return false;
|
||||||
} else if (display == null || monitor == null
|
return display.getDisplayId() == preferred.getDisplayId();
|
||||||
|| !monitor.hasDisplay(display.getDisplayId())) {
|
}
|
||||||
|
|
||||||
|
private static void rebindSecondaryDisplay() {
|
||||||
|
GameActivity self = (GameActivity) mSingleton;
|
||||||
|
if (self == null || !secondaryEnabled || secondaryOutputIsPreferred(self)) return;
|
||||||
|
self.runOnUiThread(() -> {
|
||||||
|
if (!secondaryEnabled || secondaryOutputIsPreferred(self)) return;
|
||||||
teardownSecondaryDisplay();
|
teardownSecondaryDisplay();
|
||||||
setupSecondaryDisplay();
|
setupSecondaryDisplay();
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setupSecondaryDisplay() {
|
private static void setupSecondaryDisplay() {
|
||||||
GameActivity self = (GameActivity) mSingleton;
|
GameActivity self = (GameActivity) mSingleton;
|
||||||
if (self == null || !secondaryEnabled || secondaryPresentation != null) return;
|
if (self == null || !secondaryEnabled || secondaryPresentation != null
|
||||||
|
|| secondaryActivity != null || secondaryActivityPending
|
||||||
|
|| android.os.SystemClock.elapsedRealtime() < secondaryRetryAfter) return;
|
||||||
try {
|
try {
|
||||||
Display chosen = findSecondaryDisplay(self, true);
|
Display chosen = findSecondaryDisplay(self, true);
|
||||||
if (chosen == null) {
|
if (chosen == null) {
|
||||||
Log.d("GameActivity", "no secondary display found");
|
Log.d("GameActivity", "no secondary display found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!isPresentationDisplay(chosen)) {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT < 29) return;
|
||||||
|
secondaryActivityPending = true;
|
||||||
|
secondaryActivityTarget = chosen.getDisplayId();
|
||||||
|
Intent intent = new Intent(self, SecondaryActivity.class)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
||||||
|
android.app.ActivityOptions options = android.app.ActivityOptions.makeBasic();
|
||||||
|
options.setLaunchDisplayId(secondaryActivityTarget);
|
||||||
|
self.startActivity(intent, options.toBundle());
|
||||||
|
final int requestedDisplay = secondaryActivityTarget;
|
||||||
|
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||||
|
if (secondaryActivityPending
|
||||||
|
&& secondaryActivityTarget == requestedDisplay) {
|
||||||
|
secondaryActivityPending = false;
|
||||||
|
secondaryRetryAfter = android.os.SystemClock.elapsedRealtime() + 1000;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
SecondaryPresentation p = new SecondaryPresentation(self, chosen);
|
SecondaryPresentation p = new SecondaryPresentation(self, chosen);
|
||||||
|
p.setOnDismissListener(dialog -> {
|
||||||
|
if (secondaryPresentation == p) {
|
||||||
|
secondaryPresentation = null;
|
||||||
|
rebindSecondaryDisplay();
|
||||||
|
}
|
||||||
|
});
|
||||||
p.show();
|
p.show();
|
||||||
secondaryPresentation = p;
|
secondaryPresentation = p;
|
||||||
|
secondaryRetryAfter = 0;
|
||||||
synchronized (secondaryFrameLock) {
|
synchronized (secondaryFrameLock) {
|
||||||
if (secondaryFrame != null) {
|
if (secondaryFrame != null) {
|
||||||
p.setBackground(secondaryBackground);
|
p.setBackground(secondaryBackground);
|
||||||
@@ -1559,39 +1663,77 @@ public class GameActivity extends SDLActivity {
|
|||||||
Log.d("GameActivity", "secondary display presentation started on id=" + chosen.getDisplayId());
|
Log.d("GameActivity", "secondary display presentation started on id=" + chosen.getDisplayId());
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
Log.d("GameActivity", "secondary display setup failed: " + t);
|
Log.d("GameActivity", "secondary display setup failed: " + t);
|
||||||
secondaryPresentation = null;
|
secondaryActivityPending = false;
|
||||||
|
secondaryActivityTarget = Display.INVALID_DISPLAY;
|
||||||
|
secondaryRetryAfter = android.os.SystemClock.elapsedRealtime() + 1000;
|
||||||
|
teardownSecondaryDisplay();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Display findSecondaryDisplay(GameActivity self, boolean logDisplays) {
|
private static Display findSecondaryDisplay(GameActivity self, boolean logDisplays) {
|
||||||
android.hardware.display.DisplayManager dm =
|
android.hardware.display.DisplayManager dm =
|
||||||
(android.hardware.display.DisplayManager) self.getSystemService(Context.DISPLAY_SERVICE);
|
(android.hardware.display.DisplayManager) self.getSystemService(Context.DISPLAY_SERVICE);
|
||||||
if (dm == null) return null;
|
if (dm == null || android.os.Build.VERSION.SDK_INT < 17) return null;
|
||||||
Display chosen = null;
|
Display gameDisplay = getActivityDisplay(self);
|
||||||
for (Display d : dm.getDisplays()) {
|
int gameDisplayId = gameDisplay == null
|
||||||
|
? Display.DEFAULT_DISPLAY : gameDisplay.getDisplayId();
|
||||||
|
Display handheld = dm.getDisplay(Display.DEFAULT_DISPLAY);
|
||||||
|
boolean handheldAvailable = android.os.Build.VERSION.SDK_INT >= 29
|
||||||
|
&& gameDisplayId != Display.DEFAULT_DISPLAY && isDisplayUsable(handheld);
|
||||||
|
Display external = null;
|
||||||
|
Display[] presentations = dm.getDisplays(
|
||||||
|
android.hardware.display.DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
|
||||||
|
for (Display d : presentations) {
|
||||||
if (logDisplays) {
|
if (logDisplays) {
|
||||||
android.graphics.Point size = new android.graphics.Point();
|
android.graphics.Point size = new android.graphics.Point();
|
||||||
d.getRealSize(size);
|
d.getRealSize(size);
|
||||||
Log.d("GameActivity", "display id=" + d.getDisplayId()
|
Log.d("GameActivity", "display id=" + d.getDisplayId()
|
||||||
+ " name=" + d.getName() + " size=" + size.x + "x" + size.y);
|
+ " name=" + d.getName() + " size=" + size.x + "x" + size.y);
|
||||||
}
|
}
|
||||||
if (chosen == null && d.getDisplayId() != Display.DEFAULT_DISPLAY) chosen = d;
|
if (external == null && d.getDisplayId() != gameDisplayId
|
||||||
|
&& isDisplayUsable(d)) external = d;
|
||||||
}
|
}
|
||||||
if (chosen == null) {
|
if (secondaryTarget == SECONDARY_TARGET_HANDHELD && handheldAvailable) return handheld;
|
||||||
Display[] presentations =
|
if (secondaryTarget == SECONDARY_TARGET_EXTERNAL && external != null) return external;
|
||||||
dm.getDisplays(android.hardware.display.DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
|
return handheldAvailable ? handheld : external;
|
||||||
if (presentations != null && presentations.length > 0) chosen = presentations[0];
|
}
|
||||||
|
|
||||||
|
private static Display getActivityDisplay(android.app.Activity activity) {
|
||||||
|
return android.os.Build.VERSION.SDK_INT >= 30
|
||||||
|
? activity.getDisplay() : activity.getWindowManager().getDefaultDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isPresentationDisplay(Display display) {
|
||||||
|
if (display == null || display.getDisplayId() == Display.DEFAULT_DISPLAY) return false;
|
||||||
|
return android.os.Build.VERSION.SDK_INT < 20
|
||||||
|
|| (display.getFlags() & Display.FLAG_PRESENTATION) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isDisplayUsable(Display display) {
|
||||||
|
if (display == null) return false;
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= 20
|
||||||
|
&& display.getState() == Display.STATE_OFF) return false;
|
||||||
|
if (dualScreenDisplayMode == 1 && AYN_SECOND_SCREEN.equals(display.getName())) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return chosen;
|
return dualScreenDisplayMode != 2
|
||||||
|
|| display.getDisplayId() != Display.DEFAULT_DISPLAY;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void teardownSecondaryDisplay() {
|
private static void teardownSecondaryDisplay() {
|
||||||
SecondaryPresentation p = secondaryPresentation;
|
SecondaryPresentation p = secondaryPresentation;
|
||||||
secondaryPresentation = null;
|
secondaryPresentation = null;
|
||||||
|
SecondaryActivity a = secondaryActivity;
|
||||||
|
secondaryActivity = null;
|
||||||
|
secondaryActivityPending = false;
|
||||||
|
secondaryActivityTarget = Display.INVALID_DISPLAY;
|
||||||
synchronized (secondaryTouches) { secondaryTouches.clear(); }
|
synchronized (secondaryTouches) { secondaryTouches.clear(); }
|
||||||
if (p != null) {
|
if (p != null) {
|
||||||
try { p.dismiss(); } catch (Throwable t) {}
|
try { p.dismiss(); } catch (Throwable t) {}
|
||||||
}
|
}
|
||||||
|
if (a != null) {
|
||||||
|
try { a.finish(); } catch (Throwable t) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@android.annotation.TargetApi(17)
|
@android.annotation.TargetApi(17)
|
||||||
@@ -1620,7 +1762,7 @@ public class GameActivity extends SDLActivity {
|
|||||||
|
|
||||||
private void changed() {
|
private void changed() {
|
||||||
secondaryDetectionAt = 0;
|
secondaryDetectionAt = 0;
|
||||||
refreshSecondaryDisplay();
|
rebindSecondaryDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void onDisplayAdded(int displayId) { changed(); }
|
@Override public void onDisplayAdded(int displayId) { changed(); }
|
||||||
@@ -1630,14 +1772,15 @@ public class GameActivity extends SDLActivity {
|
|||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
public static boolean hasSecondaryDisplay() {
|
public static boolean hasSecondaryDisplay() {
|
||||||
return secondaryPresentation != null;
|
return secondaryPresentation != null || secondaryActivity != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
public static boolean hasSecondaryDisplayCandidate() {
|
public static boolean hasSecondaryDisplayCandidate() {
|
||||||
GameActivity self = (GameActivity) mSingleton;
|
GameActivity self = (GameActivity) mSingleton;
|
||||||
if (self == null) return false;
|
if (self == null) return false;
|
||||||
if (secondaryPresentation != null) return true;
|
if (secondaryPresentation != null || secondaryActivity != null) return true;
|
||||||
|
self.refreshDualScreenDisplayMode();
|
||||||
long now = android.os.SystemClock.uptimeMillis();
|
long now = android.os.SystemClock.uptimeMillis();
|
||||||
if (secondaryDetectionAt != 0 && now - secondaryDetectionAt < 500) {
|
if (secondaryDetectionAt != 0 && now - secondaryDetectionAt < 500) {
|
||||||
return secondaryDetected;
|
return secondaryDetected;
|
||||||
@@ -1667,10 +1810,16 @@ public class GameActivity extends SDLActivity {
|
|||||||
secondaryBackground = backgroundColor;
|
secondaryBackground = backgroundColor;
|
||||||
secondaryFrameCover = cover;
|
secondaryFrameCover = cover;
|
||||||
SecondaryPresentation p = secondaryPresentation;
|
SecondaryPresentation p = secondaryPresentation;
|
||||||
if (p == null) return false;
|
SecondaryActivity a = secondaryActivity;
|
||||||
|
if (p == null && a == null) return false;
|
||||||
try {
|
try {
|
||||||
p.setBackground(backgroundColor);
|
if (p != null) {
|
||||||
p.updateFrame(rgba, width, height, cover);
|
p.setBackground(backgroundColor);
|
||||||
|
p.updateFrame(rgba, width, height, cover);
|
||||||
|
} else {
|
||||||
|
a.setBackground(backgroundColor);
|
||||||
|
a.updateFrame(rgba, width, height, cover);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
GameActivity self = (GameActivity) mSingleton;
|
GameActivity self = (GameActivity) mSingleton;
|
||||||
@@ -1686,8 +1835,10 @@ public class GameActivity extends SDLActivity {
|
|||||||
@Keep
|
@Keep
|
||||||
public static void updateSecondaryFrame(java.nio.ByteBuffer buf, int w, int h) {
|
public static void updateSecondaryFrame(java.nio.ByteBuffer buf, int w, int h) {
|
||||||
SecondaryPresentation p = secondaryPresentation;
|
SecondaryPresentation p = secondaryPresentation;
|
||||||
if (p != null && buf != null && w > 0 && h > 0) {
|
SecondaryActivity a = secondaryActivity;
|
||||||
p.updateFrame(buf, w, h);
|
if ((p != null || a != null) && buf != null && w > 0 && h > 0) {
|
||||||
|
if (p != null) p.updateFrame(buf, w, h);
|
||||||
|
else a.updateFrame(buf, w, h);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1698,6 +1849,102 @@ public class GameActivity extends SDLActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void applySecondaryImmersive(android.view.Window w) {
|
||||||
|
if (w == null) return;
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= 30) {
|
||||||
|
w.setDecorFitsSystemWindows(false);
|
||||||
|
android.view.WindowInsetsController c = w.getInsetsController();
|
||||||
|
if (c != null) {
|
||||||
|
c.hide(android.view.WindowInsets.Type.systemBars());
|
||||||
|
c.setSystemBarsBehavior(
|
||||||
|
android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w.getDecorView().setSystemUiVisibility(
|
||||||
|
android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| android.view.View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||||
|
| android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SecondaryActivity extends android.app.Activity {
|
||||||
|
private FrameView frameView;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
Display display = getActivityDisplay(this);
|
||||||
|
if (!secondaryEnabled || display == null
|
||||||
|
|| display.getDisplayId() != secondaryActivityTarget) {
|
||||||
|
secondaryActivityPending = false;
|
||||||
|
secondaryActivityTarget = Display.INVALID_DISPLAY;
|
||||||
|
secondaryRetryAfter = android.os.SystemClock.elapsedRealtime() + 1000;
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
frameView = new FrameView(this);
|
||||||
|
android.view.Window w = getWindow();
|
||||||
|
w.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||||
|
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
|
||||||
|
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||||
|
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
|
||||||
|
setContentView(frameView);
|
||||||
|
applySecondaryImmersive(w);
|
||||||
|
secondaryActivity = this;
|
||||||
|
secondaryActivityPending = false;
|
||||||
|
secondaryRetryAfter = 0;
|
||||||
|
synchronized (secondaryFrameLock) {
|
||||||
|
if (secondaryFrame != null) {
|
||||||
|
setBackground(secondaryBackground);
|
||||||
|
updateFrame(java.nio.ByteBuffer.wrap(secondaryFrame),
|
||||||
|
secondaryFrameWidth, secondaryFrameHeight, secondaryFrameCover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
if (secondaryActivity == this) secondaryActivity = null;
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) applySecondaryImmersive(getWindow());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean dispatchKeyEvent(android.view.KeyEvent event) {
|
||||||
|
GameActivity activity = (GameActivity) mSingleton;
|
||||||
|
return activity != null
|
||||||
|
? activity.dispatchKeyEvent(event) : super.dispatchKeyEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean dispatchGenericMotionEvent(android.view.MotionEvent event) {
|
||||||
|
GameActivity activity = (GameActivity) mSingleton;
|
||||||
|
return activity != null
|
||||||
|
? activity.dispatchGenericMotionEvent(event)
|
||||||
|
: super.dispatchGenericMotionEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateFrame(java.nio.ByteBuffer buf, int w, int h) {
|
||||||
|
frameView.updateFrame(buf, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateFrame(java.nio.ByteBuffer buf, int w, int h, boolean cover) {
|
||||||
|
frameView.updateFrame(buf, w, h, cover);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setBackground(int color) {
|
||||||
|
frameView.setFrameBackground(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class SecondaryPresentation extends android.app.Presentation {
|
private static class SecondaryPresentation extends android.app.Presentation {
|
||||||
private final FrameView frameView;
|
private final FrameView frameView;
|
||||||
|
|
||||||
@@ -1731,26 +1978,23 @@ public class GameActivity extends SDLActivity {
|
|||||||
if (hasFocus) applyImmersive();
|
if (hasFocus) applyImmersive();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean dispatchKeyEvent(android.view.KeyEvent event) {
|
||||||
|
GameActivity activity = (GameActivity) mSingleton;
|
||||||
|
return activity != null
|
||||||
|
? activity.dispatchKeyEvent(event) : super.dispatchKeyEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean dispatchGenericMotionEvent(android.view.MotionEvent event) {
|
||||||
|
GameActivity activity = (GameActivity) mSingleton;
|
||||||
|
return activity != null
|
||||||
|
? activity.dispatchGenericMotionEvent(event)
|
||||||
|
: super.dispatchGenericMotionEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
private void applyImmersive() {
|
private void applyImmersive() {
|
||||||
android.view.Window w = getWindow();
|
applySecondaryImmersive(getWindow());
|
||||||
if (w == null) return;
|
|
||||||
if (android.os.Build.VERSION.SDK_INT >= 30) {
|
|
||||||
w.setDecorFitsSystemWindows(false);
|
|
||||||
android.view.WindowInsetsController c = w.getInsetsController();
|
|
||||||
if (c != null) {
|
|
||||||
c.hide(android.view.WindowInsets.Type.systemBars());
|
|
||||||
c.setSystemBarsBehavior(
|
|
||||||
android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
w.getDecorView().setSystemUiVisibility(
|
|
||||||
android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
||||||
| android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
||||||
| android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
||||||
| android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
||||||
| android.view.View.SYSTEM_UI_FLAG_FULLSCREEN
|
|
||||||
| android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateFrame(java.nio.ByteBuffer buf, int w, int h) {
|
void updateFrame(java.nio.ByteBuffer buf, int w, int h) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ local C = nil
|
|||||||
local ffi = nil
|
local ffi = nil
|
||||||
local desktop = nil
|
local desktop = nil
|
||||||
local nativePresent = false
|
local nativePresent = false
|
||||||
|
local nativeTarget = false
|
||||||
|
|
||||||
local function log(msg)
|
local function log(msg)
|
||||||
pcall(function() require("src.core.Logger").info("SecondScreen: %s", msg) end)
|
pcall(function() require("src.core.Logger").info("SecondScreen: %s", msg) end)
|
||||||
@@ -25,6 +26,7 @@ do
|
|||||||
int love_android_secondary_detected();
|
int love_android_secondary_detected();
|
||||||
int love_android_present_secondary(const void *rgba, int w, int h,
|
int love_android_present_secondary(const void *rgba, int w, int h,
|
||||||
unsigned int background, int cover);
|
unsigned int background, int cover);
|
||||||
|
void love_android_secondary_target(int target);
|
||||||
const char *love_android_poll_secondary_touch();
|
const char *love_android_poll_secondary_touch();
|
||||||
]])
|
]])
|
||||||
local okLib, lib = pcall(ffi.load, "love")
|
local okLib, lib = pcall(ffi.load, "love")
|
||||||
@@ -45,8 +47,12 @@ do
|
|||||||
local okPresent, present = pcall(function()
|
local okPresent, present = pcall(function()
|
||||||
return C.love_android_present_secondary
|
return C.love_android_present_secondary
|
||||||
end)
|
end)
|
||||||
|
local okTarget, target = pcall(function()
|
||||||
|
return C.love_android_secondary_target
|
||||||
|
end)
|
||||||
nativePresent = okDetected and detected ~= nil
|
nativePresent = okDetected and detected ~= nil
|
||||||
and okPresent and present ~= nil
|
and okPresent and present ~= nil
|
||||||
|
nativeTarget = okTarget and target ~= nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -87,6 +93,15 @@ function SecondScreen.push(imageData, w, h, background, preference)
|
|||||||
end
|
end
|
||||||
if not C or not imageData then return false end
|
if not C or not imageData then return false end
|
||||||
if nativePresent and (background ~= nil or preference ~= nil) then
|
if nativePresent and (background ~= nil or preference ~= nil) then
|
||||||
|
if nativeTarget then
|
||||||
|
local target = 0
|
||||||
|
if preference == "handheld" or preference == "handheld:cover" then
|
||||||
|
target = 1
|
||||||
|
elseif preference == "secondary" or preference == "secondary:cover" then
|
||||||
|
target = 2
|
||||||
|
end
|
||||||
|
pcall(C.love_android_secondary_target, target)
|
||||||
|
end
|
||||||
local cover = type(preference) == "string"
|
local cover = type(preference) == "string"
|
||||||
and preference:sub(-6) == ":cover"
|
and preference:sub(-6) == ":cover"
|
||||||
local ok, shown = pcall(C.love_android_present_secondary,
|
local ok, shown = pcall(C.love_android_present_secondary,
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
local function read(path)
|
||||||
|
local file = assert(io.open(path, "rb"))
|
||||||
|
local source = file:read("*a")
|
||||||
|
file:close()
|
||||||
|
return source
|
||||||
|
end
|
||||||
|
|
||||||
|
local function check(value, message)
|
||||||
|
if not value then error(message, 2) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local java = read(
|
||||||
|
"mobile/android/love/src/main/java/org/love2d/android/GameActivity.java")
|
||||||
|
local manifest = read("mobile/android/app/src/main/AndroidManifest.xml")
|
||||||
|
|
||||||
|
check(manifest:find("android.allow_multiple_resumed_activities", 1, true)
|
||||||
|
and manifest:find("GameActivity$SecondaryActivity", 1, true)
|
||||||
|
and manifest:find('android:exported="false"', 1, true),
|
||||||
|
"the private companion Activity opts into Android multi-display resume")
|
||||||
|
check(java:find("android.os.Build.VERSION.SDK_INT < 29", 1, true)
|
||||||
|
and java:find("options.setLaunchDisplayId", 1, true),
|
||||||
|
"the primary-display fallback is restricted to Android 10+")
|
||||||
|
check(java:find("SECONDARY_TARGET_HANDHELD", 1, true)
|
||||||
|
and java:find("SECONDARY_TARGET_EXTERNAL", 1, true)
|
||||||
|
and java:find("handheldAvailable ? handheld : external", 1, true),
|
||||||
|
"routing hints retain a safe available-display fallback")
|
||||||
|
check(java:find("dualScreenDisplayMode != %-1")
|
||||||
|
and java:find("AYN_SECOND_SCREEN", 1, true)
|
||||||
|
and java:find("dualScreenModeObserverRegistered", 1, true),
|
||||||
|
"the optional AYN state is guarded and lifecycle-bound")
|
||||||
|
check(java:find("activity.dispatchKeyEvent", 1, true)
|
||||||
|
and java:find("activity.dispatchGenericMotionEvent", 1, true),
|
||||||
|
"companion windows forward controller input to the game Activity")
|
||||||
|
|
||||||
|
print("android asymmetric display routing: ok")
|
||||||
@@ -30,7 +30,8 @@ check(java:find("Math.max((float) vw / fw, (float) vh / fh)", 1, true)
|
|||||||
"FrameView supports cover and pixel-friendly contain fits")
|
"FrameView supports cover and pixel-friendly contain fits")
|
||||||
check(cpp:find("love_android_secondary_detected", 1, true)
|
check(cpp:find("love_android_secondary_detected", 1, true)
|
||||||
and cpp:find("love_android_present_secondary", 1, true)
|
and cpp:find("love_android_present_secondary", 1, true)
|
||||||
|
and cpp:find("love_android_secondary_target", 1, true)
|
||||||
and cpp:find('"(Ljava/nio/ByteBuffer;IIIZ)Z"', 1, true),
|
and cpp:find('"(Ljava/nio/ByteBuffer;IIIZ)Z"', 1, true),
|
||||||
"JNI exports the optional detected and presentation calls")
|
"JNI exports the optional detected, routing, and presentation calls")
|
||||||
|
|
||||||
print("android secondary presentation: ok")
|
print("android secondary presentation: ok")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ local C = {
|
|||||||
calls.push = { ptr, w, h }
|
calls.push = { ptr, w, h }
|
||||||
end,
|
end,
|
||||||
love_android_secondary_enable = function(on) calls.enabled = on end,
|
love_android_secondary_enable = function(on) calls.enabled = on end,
|
||||||
|
love_android_secondary_target = function(target) calls.target = target end,
|
||||||
love_android_secondary_detected = function() return 1 end,
|
love_android_secondary_detected = function() return 1 end,
|
||||||
love_android_present_secondary = function(ptr, w, h, background, cover)
|
love_android_present_secondary = function(ptr, w, h, background, cover)
|
||||||
calls.present = { ptr, w, h, background, cover }
|
calls.present = { ptr, w, h, background, cover }
|
||||||
@@ -44,6 +45,10 @@ T.eq(SecondScreen.push(image, 160, 144, 0x112233, "secondary:cover"), true,
|
|||||||
"extended Android presentation accepts frame metadata")
|
"extended Android presentation accepts frame metadata")
|
||||||
T.same(calls.present, { "pixels", 160, 144, 0x112233, 1 },
|
T.same(calls.present, { "pixels", 160, 144, 0x112233, 1 },
|
||||||
"cover and RGB background reach the native bridge")
|
"cover and RGB background reach the native bridge")
|
||||||
|
T.eq(calls.target, 2, "secondary routing reaches the optional native bridge")
|
||||||
|
T.eq(SecondScreen.push(image, 160, 144, 0x112233, "handheld"), true,
|
||||||
|
"handheld routing remains a contain presentation")
|
||||||
|
T.eq(calls.target, 1, "handheld routing reaches the optional native bridge")
|
||||||
T.eq(SecondScreen.push(image, 160, 144, 0x112233, "secondary"), true,
|
T.eq(SecondScreen.push(image, 160, 144, 0x112233, "secondary"), true,
|
||||||
"contain presentation remains available")
|
"contain presentation remains available")
|
||||||
T.same(calls.present, { "pixels", 160, 144, 0x112233, 0 },
|
T.same(calls.present, { "pixels", 160, 144, 0x112233, 0 },
|
||||||
@@ -52,6 +57,7 @@ T.eq(SecondScreen.push(image, 160, 144, nil, "secondary:cover"), true,
|
|||||||
"a fit preference can request extended presentation by itself")
|
"a fit preference can request extended presentation by itself")
|
||||||
T.same(calls.present, { "pixels", 160, 144, 0, 1 },
|
T.same(calls.present, { "pixels", 160, 144, 0, 1 },
|
||||||
"preference-only presentation defaults to a black background")
|
"preference-only presentation defaults to a black background")
|
||||||
|
T.eq(calls.target, 2, "a suffixed route keeps its target")
|
||||||
T.eq(SecondScreen.push(image, 160, 144), true,
|
T.eq(SecondScreen.push(image, 160, 144), true,
|
||||||
"the original push ABI remains available")
|
"the original push ABI remains available")
|
||||||
T.same(calls.push, { "pixels", 160, 144 },
|
T.same(calls.push, { "pixels", 160, 144 },
|
||||||
|
|||||||
Reference in New Issue
Block a user