orientation preferences and settings UI for app and in-game orientations

This commit is contained in:
izzy2lost
2026-03-23 21:41:29 -04:00
parent a5183c376c
commit 15f102d257
10 changed files with 240 additions and 69 deletions
+5 -3
View File
@@ -11,7 +11,7 @@
<activity
android:name=".LauncherActivity"
android:screenOrientation="portrait"
android:screenOrientation="fullSensor"
android:exported="true">
<intent-filter>
@@ -35,7 +35,7 @@
<activity
android:name=".MainActivity"
android:screenOrientation="landscape"
android:screenOrientation="sensorLandscape"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:hardwareAccelerated="true"
android:process=":xemu"
@@ -48,15 +48,17 @@
<activity
android:name=".SetupWizardActivity"
android:screenOrientation="portrait"
android:screenOrientation="fullSensor"
android:exported="false" />
<activity
android:name=".GameLibraryActivity"
android:screenOrientation="fullSensor"
android:exported="false" />
<activity
android:name=".SettingsActivity"
android:screenOrientation="fullSensor"
android:exported="false" />
</application>
+22 -1
View File
@@ -55,6 +55,7 @@ static JNIEnv* GetEnv();
static jobject GetActivity(JNIEnv* env);
static bool HasException(JNIEnv* env, const char* context);
static std::string GetFilesDirPath(JNIEnv* env, jobject activity);
static std::string GetPrefString(JNIEnv* env, jobject activity, const char* key);
static void ConfigureNativeDebugLogging(JNIEnv* env, jobject activity);
static void ApplyHrtfDefaultOffMigration(JNIEnv* env, jobject activity);
static bool NativeDebugLoggingEnabled();
@@ -341,6 +342,22 @@ static std::string ResolveAndroidAudioDriverHint() {
return raw;
}
static std::string ResolveAndroidOrientationHint(JNIEnv* env, jobject activity) {
constexpr const char* kDefaultOrientationHint = "LandscapeLeft LandscapeRight";
if (!env || !activity) {
return kDefaultOrientationHint;
}
std::string value = ToLowerAscii(GetPrefString(env, activity, "setting_game_orientation"));
if (value == "landscape") {
return "LandscapeLeft";
}
if (value == "reverse_landscape") {
return "LandscapeRight";
}
return kDefaultOrientationHint;
}
static JNIEnv* GetEnv() {
return static_cast<JNIEnv*>(SDL_AndroidGetJNIEnv());
}
@@ -1390,11 +1407,15 @@ extern "C" int SDL_main(int argc, char* argv[]) {
ConfigureNativeDebugLogging(GetEnv(), GetActivity(GetEnv()));
LogInfo("SDL_main: start");
JNIEnv* env = GetEnv();
jobject activity = GetActivity(env);
std::string audio_driver_hint = ResolveAndroidAudioDriverHint();
SDL_SetHintWithPriority(SDL_HINT_AUDIODRIVER, audio_driver_hint.c_str(),
SDL_HINT_OVERRIDE);
LogInfoFmt("SDL_HINT_AUDIODRIVER=%s", audio_driver_hint.c_str());
SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight");
std::string orientation_hint = ResolveAndroidOrientationHint(env, activity);
SDL_SetHint(SDL_HINT_ORIENTATIONS, orientation_hint.c_str());
LogInfoFmt("SDL_HINT_ORIENTATIONS=%s", orientation_hint.c_str());
SDL_DisableScreenSaver();
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) != 0) {
@@ -43,8 +43,6 @@ import java.util.concurrent.ConcurrentHashMap
class GameLibraryActivity : AppCompatActivity() {
companion object {
const val EXTRA_RESTART_LAST_GAME = "com.izzy2lost.x1box.extra.RESTART_LAST_GAME"
const val EXTRA_INITIAL_ORIENTATION =
"com.izzy2lost.x1box.extra.INITIAL_ORIENTATION"
private const val SNAPSHOT_PREVIEW_HEADER_SIZE = 12
private const val TOTAL_SNAPSHOT_SLOTS = 10
private const val XDVDFS_SECTOR_SIZE = 2048L
@@ -155,7 +153,6 @@ class GameLibraryActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
applyInitialOrientationFromIntent()
OrientationLocker(this).enable()
setContentView(R.layout.activity_game_library)
EdgeToEdgeHelper.enable(this)
@@ -196,11 +193,7 @@ class GameLibraryActivity : AppCompatActivity() {
launchDashboard()
}
btnSettings.setOnClickListener {
startActivity(
Intent(this, SettingsActivity::class.java).apply {
putExtra(SettingsActivity.EXTRA_INITIAL_ORIENTATION, requestedOrientation)
}
)
startActivity(Intent(this, SettingsActivity::class.java))
}
btnSnapshots.setOnClickListener {
showSnapshotStartupPicker()
@@ -231,13 +224,9 @@ class GameLibraryActivity : AppCompatActivity() {
loadGames()
}
private fun applyInitialOrientationFromIntent() {
val initialOrientation = intent.getIntExtra(EXTRA_INITIAL_ORIENTATION, Int.MIN_VALUE)
if (initialOrientation == android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT ||
initialOrientation == android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
) {
requestedOrientation = initialOrientation
}
override fun onResume() {
super.onResume()
OrientationLocker(this).enable()
}
private fun tryRestartLastGameFromIntent(): Boolean {
@@ -204,6 +204,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
override fun onResume() {
super.onResume()
OrientationLocker(this, landscapeOnly = true).enable()
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
mLayout?.keepScreenOn = true
@@ -805,7 +806,6 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
private fun exitToGameLibrary() {
val intent = Intent(this, GameLibraryActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
putExtra(GameLibraryActivity.EXTRA_INITIAL_ORIENTATION, requestedOrientation)
}
startActivity(intent)
terminateXemuProcessSoon("exit to library")
@@ -1,45 +1,20 @@
package com.izzy2lost.x1box
import android.app.Activity
import android.content.pm.ActivityInfo
import android.view.OrientationEventListener
/**
* Prevents reversed (upside-down) orientations.
*
* UI mode: portrait when upright/right-rotated, left landscape otherwise.
* Game mode: always left landscape, no exceptions.
*/
class OrientationLocker(private val activity: Activity, private val landscapeOnly: Boolean = false) {
private val listener = object : OrientationEventListener(activity) {
override fun onOrientationChanged(orientation: Int) {
if (orientation == ORIENTATION_UNKNOWN) return
val target = if (landscapeOnly) {
// In-game: always left landscape regardless of how the device is held.
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} else {
// UI: portrait when upright or rotating right, landscape otherwise.
// 315-360 / 0-134 → portrait (upright + right rotation)
// 135-314 → landscape (upside-down + left rotation)
if (orientation < 135 || orientation >= 315)
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
else
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
if (activity.requestedOrientation != target) {
activity.requestedOrientation = target
}
}
fun enable() {
val target = if (landscapeOnly) {
OrientationPreferences.getGameRequestedOrientation(activity)
} else {
OrientationPreferences.getUiRequestedOrientation(activity)
}
fun enable() {
if (listener.canDetectOrientation()) listener.enable()
if (activity.requestedOrientation != target) {
activity.requestedOrientation = target
}
}
fun disable() {
listener.disable()
}
fun disable() {
}
}
@@ -0,0 +1,75 @@
package com.izzy2lost.x1box
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.ActivityInfo
object OrientationPreferences {
private const val PREFS_NAME = "x1box_prefs"
const val PREF_UI_ORIENTATION = "setting_ui_orientation"
const val PREF_GAME_ORIENTATION = "setting_game_orientation"
enum class UiOrientation(
val prefValue: String,
val requestedOrientation: Int,
) {
FOLLOW_DEVICE("follow_device", ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR),
PORTRAIT("portrait", ActivityInfo.SCREEN_ORIENTATION_PORTRAIT),
REVERSE_PORTRAIT("reverse_portrait", ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT),
LANDSCAPE("landscape", ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE),
REVERSE_LANDSCAPE("reverse_landscape", ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE),
;
companion object {
fun fromPrefValue(value: String?): UiOrientation {
return values().firstOrNull { it.prefValue == value } ?: FOLLOW_DEVICE
}
}
}
enum class GameOrientation(
val prefValue: String,
val requestedOrientation: Int,
) {
FOLLOW_DEVICE(
"follow_device",
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE,
),
LANDSCAPE(
"landscape",
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE,
),
REVERSE_LANDSCAPE(
"reverse_landscape",
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE,
),
;
companion object {
fun fromPrefValue(value: String?): GameOrientation {
return values().firstOrNull { it.prefValue == value } ?: FOLLOW_DEVICE
}
}
}
fun getUiOrientation(context: Context): UiOrientation {
return UiOrientation.fromPrefValue(sharedPreferences(context).getString(PREF_UI_ORIENTATION, null))
}
fun getGameOrientation(context: Context): GameOrientation {
return GameOrientation.fromPrefValue(sharedPreferences(context).getString(PREF_GAME_ORIENTATION, null))
}
fun getUiRequestedOrientation(context: Context): Int {
return getUiOrientation(context).requestedOrientation
}
fun getGameRequestedOrientation(context: Context): Int {
return getGameOrientation(context).requestedOrientation
}
private fun sharedPreferences(context: Context): SharedPreferences {
return context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
}
}
@@ -31,8 +31,6 @@ import java.util.zip.ZipInputStream
class SettingsActivity : AppCompatActivity() {
companion object {
const val EXTRA_INITIAL_ORIENTATION =
"com.izzy2lost.x1box.extra.INITIAL_ORIENTATION"
private const val PREFS_NAME = "x1box_prefs"
private const val PREF_ADVANCED_EXPERIMENTAL_EXPANDED = "settings_advanced_experimental_expanded"
private const val PREF_HRTF = "setting_hrtf"
@@ -69,6 +67,16 @@ class SettingsActivity : AppCompatActivity() {
val labelRes: Int,
)
private data class UiOrientationOption(
val value: OrientationPreferences.UiOrientation,
val labelRes: Int,
)
private data class GameOrientationOption(
val value: OrientationPreferences.GameOrientation,
val labelRes: Int,
)
private data class CacheClearResult(
val deletedEntries: Int,
val hadFailures: Boolean,
@@ -136,6 +144,20 @@ class SettingsActivity : AppCompatActivity() {
EepromRefreshRateOption(XboxEepromEditor.RefreshRate.HZ_50, R.string.settings_eeprom_refresh_rate_50),
)
private val uiOrientationOptions = listOf(
UiOrientationOption(OrientationPreferences.UiOrientation.FOLLOW_DEVICE, R.string.settings_orientation_follow_device),
UiOrientationOption(OrientationPreferences.UiOrientation.PORTRAIT, R.string.settings_orientation_portrait),
UiOrientationOption(OrientationPreferences.UiOrientation.REVERSE_PORTRAIT, R.string.settings_orientation_reverse_portrait),
UiOrientationOption(OrientationPreferences.UiOrientation.LANDSCAPE, R.string.settings_orientation_landscape),
UiOrientationOption(OrientationPreferences.UiOrientation.REVERSE_LANDSCAPE, R.string.settings_orientation_reverse_landscape),
)
private val gameOrientationOptions = listOf(
GameOrientationOption(OrientationPreferences.GameOrientation.FOLLOW_DEVICE, R.string.settings_orientation_follow_device),
GameOrientationOption(OrientationPreferences.GameOrientation.LANDSCAPE, R.string.settings_orientation_landscape),
GameOrientationOption(OrientationPreferences.GameOrientation.REVERSE_LANDSCAPE, R.string.settings_orientation_reverse_landscape),
)
private var pendingVulkanUri: String? = null
private var pendingVulkanName: String? = null
private var clearVulkan = false
@@ -156,6 +178,8 @@ class SettingsActivity : AppCompatActivity() {
private lateinit var btnRegisterInsignia: MaterialButton
private lateinit var btnImportDashboard: MaterialButton
private lateinit var layoutAdvancedExperimentalContent: LinearLayout
private lateinit var dropdownUiOrientation: AutoCompleteTextView
private lateinit var dropdownGameOrientation: AutoCompleteTextView
private lateinit var inputEepromLanguage: TextInputLayout
private lateinit var inputEepromVideoStandard: TextInputLayout
private lateinit var inputEepromAspectRatio: TextInputLayout
@@ -172,6 +196,8 @@ class SettingsActivity : AppCompatActivity() {
private var selectedEepromVideoStandard = XboxEepromEditor.VideoStandard.NTSC_M
private var selectedEepromAspectRatio = XboxEepromEditor.AspectRatio.NORMAL
private var selectedEepromRefreshRate = XboxEepromEditor.RefreshRate.DEFAULT
private var selectedUiOrientation = OrientationPreferences.UiOrientation.FOLLOW_DEVICE
private var selectedGameOrientation = OrientationPreferences.GameOrientation.FOLLOW_DEVICE
private var eepromEditable = false
private var eepromMissing = false
private var eepromError = false
@@ -234,7 +260,6 @@ class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
applyInitialOrientationFromIntent()
OrientationLocker(this).enable()
DebugLog.initialize(this)
applyHrtfDefaultOffMigration()
@@ -276,6 +301,8 @@ class SettingsActivity : AppCompatActivity() {
btnRegisterInsignia = findViewById(R.id.btn_register_insignia)
btnImportDashboard = findViewById(R.id.btn_import_dashboard)
layoutAdvancedExperimentalContent = findViewById(R.id.layout_advanced_experimental_content)
dropdownUiOrientation = findViewById(R.id.dropdown_app_orientation)
dropdownGameOrientation = findViewById(R.id.dropdown_game_orientation)
tvVulkanDriverName = findViewById(R.id.tv_vulkan_driver_name)
val btnVulkanBrowse = findViewById<MaterialButton>(R.id.btn_vulkan_browse)
val btnVulkanClear = findViewById<MaterialButton>(R.id.btn_vulkan_clear)
@@ -322,6 +349,10 @@ class SettingsActivity : AppCompatActivity() {
else -> toggleDisplayMode.check(R.id.btn_display_stretch)
}
setupOrientationControls()
setUiOrientationSelection(OrientationPreferences.getUiOrientation(this))
setGameOrientationSelection(OrientationPreferences.getGameOrientation(this))
if (prefs.getInt("setting_frame_rate_limit", 60) != 60) {
prefs.edit().putInt("setting_frame_rate_limit", 60).apply()
}
@@ -458,6 +489,8 @@ class SettingsActivity : AppCompatActivity() {
.putInt("setting_surface_scale", selectedScale)
.putInt("setting_frame_rate_limit", 60)
.putInt("setting_system_memory_mib", selectedSystemMemoryMiB)
.putString(OrientationPreferences.PREF_UI_ORIENTATION, selectedUiOrientation.prefValue)
.putString(OrientationPreferences.PREF_GAME_ORIENTATION, selectedGameOrientation.prefValue)
.putString("setting_tcg_thread", selectedThread)
.putBoolean("setting_use_dsp", switchDsp.isChecked)
.putBoolean(PREF_HRTF, switchHrtf.isChecked)
@@ -544,15 +577,6 @@ class SettingsActivity : AppCompatActivity() {
.apply()
}
private fun applyInitialOrientationFromIntent() {
val initialOrientation = intent.getIntExtra(EXTRA_INITIAL_ORIENTATION, Int.MIN_VALUE)
if (initialOrientation == android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT ||
initialOrientation == android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
) {
requestedOrientation = initialOrientation
}
}
private fun importCustomVulkanDriver(
uri: Uri,
selectedName: String?,
@@ -1066,6 +1090,39 @@ class SettingsActivity : AppCompatActivity() {
}
}
private fun setupOrientationControls() {
val uiOrientationLabels = uiOrientationOptions.map { getString(it.labelRes) }
val gameOrientationLabels = gameOrientationOptions.map { getString(it.labelRes) }
dropdownUiOrientation.setAdapter(
ArrayAdapter(this, android.R.layout.simple_list_item_1, uiOrientationLabels)
)
dropdownGameOrientation.setAdapter(
ArrayAdapter(this, android.R.layout.simple_list_item_1, gameOrientationLabels)
)
dropdownUiOrientation.setOnItemClickListener { _, _, position, _ ->
selectedUiOrientation = uiOrientationOptions[position].value
}
dropdownGameOrientation.setOnItemClickListener { _, _, position, _ ->
selectedGameOrientation = gameOrientationOptions[position].value
}
}
private fun setUiOrientationSelection(orientation: OrientationPreferences.UiOrientation) {
selectedUiOrientation = orientation
val option = uiOrientationOptions.firstOrNull { it.value == orientation }
?: uiOrientationOptions.first()
dropdownUiOrientation.setText(getString(option.labelRes), false)
}
private fun setGameOrientationSelection(orientation: OrientationPreferences.GameOrientation) {
selectedGameOrientation = orientation
val option = gameOrientationOptions.firstOrNull { it.value == orientation }
?: gameOrientationOptions.first()
dropdownGameOrientation.setText(getString(option.labelRes), false)
}
private fun applyEepromEdits(): Pair<Int, Int> {
if (eepromMissing) {
return Pair(R.string.settings_saved_eeprom_missing, Toast.LENGTH_LONG)
@@ -140,11 +140,15 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
boolean skip = false;
int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|| requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|| requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT) {
if (mWidth > mHeight) {
skip = true;
}
} else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
} else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|| requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|| requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
if (mWidth < mHeight) {
skip = true;
}
@@ -218,6 +218,46 @@
</com.google.android.material.button.MaterialButtonToggleGroup>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_app_orientation"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:hint="@string/settings_app_orientation">
<AutoCompleteTextView
android:id="@+id/dropdown_app_orientation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_game_orientation"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="@string/settings_in_game_orientation">
<AutoCompleteTextView
android:id="@+id/dropdown_game_orientation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/settings_in_game_orientation_hint"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@color/xemu_text_muted" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -122,6 +122,14 @@
<string name="settings_display_mode_stretch">Stretch</string>
<string name="settings_display_mode_4_3">4:3</string>
<string name="settings_display_mode_16_9">16:9</string>
<string name="settings_app_orientation">App Orientation</string>
<string name="settings_in_game_orientation">In-Game Orientation</string>
<string name="settings_in_game_orientation_hint">In-game rotation only uses the two landscape directions, so you can keep the charging port on the side that works best for you.</string>
<string name="settings_orientation_follow_device">Follow Device</string>
<string name="settings_orientation_portrait">Portrait</string>
<string name="settings_orientation_reverse_portrait">Portrait (Upside Down)</string>
<string name="settings_orientation_landscape">Landscape</string>
<string name="settings_orientation_reverse_landscape">Landscape (Flipped)</string>
<string name="settings_system_memory">System Memory</string>
<string name="settings_system_memory_64">64 MiB (Default)</string>
<string name="settings_system_memory_128">128 MiB (Debug/Homebrew)</string>