From 3e3bda42e5078d7ce92606e4c93e8cc3b96e75d0 Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Wed, 19 May 2021 22:27:23 +0200 Subject: [PATCH] Follow screen rotation even to Portrait on Android with Opt-in (#3936) --- android/AndroidManifest.xml | 1 - android/src/com/unciv/app/AndroidLauncher.kt | 14 +++- .../app/LimitOrientationsHelperAndroid.kt | 65 +++++++++++++++++++ core/src/com/unciv/MainMenuScreen.kt | 18 +++-- core/src/com/unciv/UncivGame.kt | 1 + core/src/com/unciv/UncivGameParameters.kt | 6 +- core/src/com/unciv/logic/GameSaver.kt | 2 +- .../com/unciv/models/metadata/GameSettings.kt | 2 + .../unciv/ui/utils/CameraStageBaseScreen.kt | 35 +++++++++- .../unciv/ui/utils/LimitOrientationsHelper.kt | 22 +++++++ .../com/unciv/ui/worldscreen/WorldScreen.kt | 2 +- .../ui/worldscreen/mainmenu/OptionsPopup.kt | 12 +++- .../mainmenu/WorldScreenMenuPopup.kt | 4 +- 13 files changed, 164 insertions(+), 20 deletions(-) create mode 100644 android/src/com/unciv/app/LimitOrientationsHelperAndroid.kt create mode 100644 core/src/com/unciv/ui/utils/LimitOrientationsHelper.kt diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 36f8bcd151..fd4cb61813 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -22,7 +22,6 @@ android:name="com.unciv.app.AndroidLauncher" android:launchMode="singleTask" android:label="@string/app_name" - android:screenOrientation="userLandscape" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" tools:ignore="LockedOrientationActivity"> diff --git a/android/src/com/unciv/app/AndroidLauncher.kt b/android/src/com/unciv/app/AndroidLauncher.kt index eb14d9ec50..8abbd41294 100644 --- a/android/src/com/unciv/app/AndroidLauncher.kt +++ b/android/src/com/unciv/app/AndroidLauncher.kt @@ -1,6 +1,7 @@ package com.unciv.app import android.content.Intent +import android.content.pm.ActivityInfo import android.os.Build import android.os.Bundle import androidx.core.app.NotificationManagerCompat @@ -29,12 +30,19 @@ open class AndroidLauncher : AndroidApplication() { if (externalfilesDir != null) GameSaver.externalFilesDirForAndroid = externalfilesDir.path } - val config = AndroidApplicationConfiguration().apply { useImmersiveMode = true; } + // Manage orientation lock + val limitOrientationsHelper = LimitOrientationsHelperAndroid(this) + limitOrientationsHelper.limitOrientations(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) + + val config = AndroidApplicationConfiguration().apply { + useImmersiveMode = true; + } val androidParameters = UncivGameParameters( version = BuildConfig.VERSION_NAME, crashReportSender = CrashReportSenderAndroid(this), fontImplementation = NativeFontAndroid(Fonts.ORIGINAL_FONT_SIZE.toInt()), - customSaveLocationHelper = customSaveLocationHelper + customSaveLocationHelper = customSaveLocationHelper, + limitOrientationsHelper = limitOrientationsHelper ) val game = UncivGame(androidParameters) initialize(game, config) @@ -89,4 +97,4 @@ open class AndroidLauncher : AndroidApplication() { } } -class AndroidTvLauncher:AndroidLauncher() \ No newline at end of file +class AndroidTvLauncher:AndroidLauncher() diff --git a/android/src/com/unciv/app/LimitOrientationsHelperAndroid.kt b/android/src/com/unciv/app/LimitOrientationsHelperAndroid.kt new file mode 100644 index 0000000000..918c081b51 --- /dev/null +++ b/android/src/com/unciv/app/LimitOrientationsHelperAndroid.kt @@ -0,0 +1,65 @@ +package com.unciv.app + +import android.app.Activity +import android.content.pm.ActivityInfo +import android.os.Build +import com.badlogic.gdx.files.FileHandle +import com.unciv.logic.GameSaver +import com.unciv.ui.utils.LimitOrientationsHelper +import java.io.File + +/** See also interface [LimitOrientationsHelper]. + * + * The Android implementation (currently the only one) effectively ends up doing + * [Activity.setRequestedOrientation] + */ +class LimitOrientationsHelperAndroid(private val activity: Activity) : LimitOrientationsHelper { +/* + companion object { + // from android.content.res.Configuration.java + // applicable to activity.resources.configuration + const val ORIENTATION_UNDEFINED = 0 + const val ORIENTATION_PORTRAIT = 1 + const val ORIENTATION_LANDSCAPE = 2 + } +*/ + + private class GameSettingsPreview(var allowAndroidPortrait: Boolean = false) + + override fun allowPortrait(allow: Boolean) { + val orientation = when { + allow -> ActivityInfo.SCREEN_ORIENTATION_USER + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE + else -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + } + // Comparison ensures ActivityTaskManager.getService().setRequestedOrientation isn't called unless necessary + if (activity.requestedOrientation != orientation) activity.requestedOrientation = orientation + } + + override fun limitOrientations(newOrientation: Int) { +// Sources for Info about current orientation in case need: +// val windowManager = (activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager) +// val displayRotation = windowManager.defaultDisplay.rotation +// val currentOrientation = activity.resources.configuration.orientation + if (newOrientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) { + // Currently only the AndroidLauncher onCreate calls this with 'unspecified'. + // determine whether to allow portrait from our settings file... + // Gdx.files at this point is null, UncivGame.Current worse, so we'll do it classically. + // Gdx parts used that *do* work: FileHandle (constructor, exists, reader) and Json + val settingsPath = activity.applicationContext.filesDir.absolutePath + File.separator + GameSaver.settingsFileName + val settingsFile = FileHandle(settingsPath) + val setting = + if (!settingsFile.exists()) { + GameSettingsPreview() + } else try { + GameSaver.json().fromJson(GameSettingsPreview::class.java, settingsFile.reader()) + } catch (ex: java.lang.Exception) { + GameSettingsPreview() + } + allowPortrait(setting.allowAndroidPortrait) + } else { + // Currently unused + if (activity.requestedOrientation != newOrientation) activity.requestedOrientation = newOrientation + } + } +} diff --git a/core/src/com/unciv/MainMenuScreen.kt b/core/src/com/unciv/MainMenuScreen.kt index bef3dee57b..3c0a21f070 100644 --- a/core/src/com/unciv/MainMenuScreen.kt +++ b/core/src/com/unciv/MainMenuScreen.kt @@ -27,6 +27,7 @@ import kotlin.concurrent.thread class MainMenuScreen: CameraStageBaseScreen() { private val autosave = "Autosave" private val backgroundTable = Table().apply { background=ImageGetter.getBackground(Color.WHITE) } + private val singleColumn = isCrampedPortrait() private fun getTableBlock(text: String, icon: String, function: () -> Unit): Table { val table = Table().pad(15f, 30f, 15f, 30f) @@ -70,7 +71,8 @@ class MainMenuScreen: CameraStageBaseScreen() { } val column1 = Table().apply { defaults().pad(10f) } - val column2 = Table().apply { defaults().pad(10f) } + val column2 = if(singleColumn) column1 else Table().apply { defaults().pad(10f) } + val autosaveGame = GameSaver.getSave(autosave, false) if (autosaveGame.exists()) { val resumeTable = getTableBlock("Resume","OtherIcons/Resume") { autoLoadGame() } @@ -104,19 +106,21 @@ class MainMenuScreen: CameraStageBaseScreen() { column2.add(modsTable).row() - - val optionsTable = getTableBlock("Options", "OtherIcons/Options") - { OptionsPopup(this).open() } + val optionsTable = getTableBlock("Options", "OtherIcons/Options") { + this.openOptionsPopup() + } column2.add(optionsTable).row() val table=Table().apply { defaults().pad(10f) } table.add(column1) - table.add(column2) + if (!singleColumn) table.add(column2) table.pack() - stage.addActor(table) - table.center(stage) + val scrollPane = AutoScrollPane(table) + scrollPane.setFillParent(true) + stage.addActor(scrollPane) + table.center(scrollPane) onBackButtonClicked { if(hasOpenPopups()) { diff --git a/core/src/com/unciv/UncivGame.kt b/core/src/com/unciv/UncivGame.kt index 774d6a7569..bf24edc579 100644 --- a/core/src/com/unciv/UncivGame.kt +++ b/core/src/com/unciv/UncivGame.kt @@ -34,6 +34,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { val fontImplementation = parameters.fontImplementation val consoleMode = parameters.consoleMode val customSaveLocationHelper = parameters.customSaveLocationHelper + val limitOrientationsHelper = parameters.limitOrientationsHelper lateinit var gameInfo: GameInfo fun isGameInfoInitialized() = this::gameInfo.isInitialized diff --git a/core/src/com/unciv/UncivGameParameters.kt b/core/src/com/unciv/UncivGameParameters.kt index 86357dcb50..9ee7153b36 100644 --- a/core/src/com/unciv/UncivGameParameters.kt +++ b/core/src/com/unciv/UncivGameParameters.kt @@ -2,6 +2,7 @@ package com.unciv import com.unciv.logic.CustomSaveLocationHelper import com.unciv.ui.utils.CrashReportSender +import com.unciv.ui.utils.LimitOrientationsHelper import com.unciv.ui.utils.NativeFontImplementation class UncivGameParameters(val version: String, @@ -9,5 +10,6 @@ class UncivGameParameters(val version: String, val cancelDiscordEvent: (() -> Unit)? = null, val fontImplementation: NativeFontImplementation? = null, val consoleMode: Boolean = false, - val customSaveLocationHelper: CustomSaveLocationHelper? = null) { -} \ No newline at end of file + val customSaveLocationHelper: CustomSaveLocationHelper? = null, + val limitOrientationsHelper: LimitOrientationsHelper? = null +) { } diff --git a/core/src/com/unciv/logic/GameSaver.kt b/core/src/com/unciv/logic/GameSaver.kt index d8c7cbc52e..b5c44c8677 100644 --- a/core/src/com/unciv/logic/GameSaver.kt +++ b/core/src/com/unciv/logic/GameSaver.kt @@ -11,7 +11,7 @@ import kotlin.concurrent.thread object GameSaver { private const val saveFilesFolder = "SaveFiles" private const val multiplayerFilesFolder = "MultiplayerGames" - private const val settingsFileName = "GameSettings.json" + const val settingsFileName = "GameSettings.json" @Volatile var customSaveLocationHelper: CustomSaveLocationHelper? = null diff --git a/core/src/com/unciv/models/metadata/GameSettings.kt b/core/src/com/unciv/models/metadata/GameSettings.kt index dfbc426f66..b8b058d5a4 100644 --- a/core/src/com/unciv/models/metadata/GameSettings.kt +++ b/core/src/com/unciv/models/metadata/GameSettings.kt @@ -44,6 +44,8 @@ class GameSettings { var lastOverviewPage: String = "Cities" + var allowAndroidPortrait = false // Opt-in to allow Unciv to follow a screen rotation to portrait + init { // 26 = Android Oreo. Versions below may display permanent icon in notification bar. if (Gdx.app?.type == Application.ApplicationType.Android && Gdx.app.version < 26) { diff --git a/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt b/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt index 5f5d5a5dfa..1aeed00f6b 100644 --- a/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt +++ b/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt @@ -13,9 +13,13 @@ import com.badlogic.gdx.scenes.scene2d.* import com.badlogic.gdx.scenes.scene2d.ui.* import com.badlogic.gdx.scenes.scene2d.utils.Drawable import com.badlogic.gdx.utils.viewport.ExtendViewport +import com.unciv.MainMenuScreen import com.unciv.UncivGame import com.unciv.models.Tutorial import com.unciv.ui.tutorials.TutorialController +import com.unciv.ui.worldscreen.WorldScreen +import com.unciv.ui.worldscreen.mainmenu.OptionsPopup +import kotlin.concurrent.thread open class CameraStageBaseScreen : Screen { @@ -28,10 +32,10 @@ open class CameraStageBaseScreen : Screen { init { val resolutions: List = game.settings.resolution.split("x").map { it.toInt().toFloat() } - val width = resolutions[0] val height = resolutions[1] - stage = Stage(ExtendViewport(width, height), SpriteBatch()) + /** The ExtendViewport sets the _minimum_(!) world size - the actual world size will be larger, fitted to screen/window aspect ratio. */ + stage = Stage(ExtendViewport(height, height), SpriteBatch()) stage.addListener( object : InputListener() { @@ -118,4 +122,31 @@ open class CameraStageBaseScreen : Screen { return listener } + fun isPortrait() = stage.viewport.screenHeight > stage.viewport.screenWidth + fun isCrampedPortrait() = isPortrait() && + game.settings.resolution.split("x").map { it.toInt() }.last() <= 700 + + fun openOptionsPopup() { + val limitOrientationsHelper = game.limitOrientationsHelper + if (limitOrientationsHelper == null || !game.settings.allowAndroidPortrait || !isCrampedPortrait()) { + OptionsPopup(this).open(force = true) + return + } + if (!(this is MainMenuScreen || this is WorldScreen)) { + throw IllegalArgumentException("openOptionsPopup called on wrong derivative class") + } + limitOrientationsHelper.allowPortrait(false) + thread(name="WaitForRotation") { + var waited = 0 + while (true) { + val newScreen = (UncivGame.Current.screen as? CameraStageBaseScreen) + if (waited >= 10000 || newScreen!=null && !newScreen.isPortrait() ) { + Gdx.app.postRunnable { OptionsPopup(newScreen ?: this).open(true) } + break + } + Thread.sleep(200) + waited += 200 + } + } + } } diff --git a/core/src/com/unciv/ui/utils/LimitOrientationsHelper.kt b/core/src/com/unciv/ui/utils/LimitOrientationsHelper.kt new file mode 100644 index 0000000000..cbc125a2c9 --- /dev/null +++ b/core/src/com/unciv/ui/utils/LimitOrientationsHelper.kt @@ -0,0 +1,22 @@ +package com.unciv.ui.utils + +import com.unciv.models.metadata.GameSettings + +/** Interface to support managing orientations + * + * You can turn a mobile device on its side or upside down, and a mobile OS may or may not allow the + * position changes to automatically result in App orientation changes. This is about limiting that feature. + */ +interface LimitOrientationsHelper { + /** Set a specific requested orientation or pull the setting from disk and act accordingly + * @param newOrientation A SCREEN_ORIENTATION_* value from [ActivityInfo] + * or SCREEN_ORIENTATION_UNSPECIFIED to load the setting + */ + fun limitOrientations(newOrientation: Int) + + /** Pass a Boolean setting as used in [allowAndroidPortrait][GameSettings.allowAndroidPortrait] to the OS. + * @param allow `true`: allow all orientations (follows sensor as limited by OS settings) + * `false`: allow only landscape orientations (both if supported, otherwise default landscape only) + */ + fun allowPortrait(allow: Boolean) +} diff --git a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt index 1dce065502..ae428bbbff 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt @@ -237,7 +237,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Cam keyPressDispatcher[Input.Keys.F11] = quickSave // Quick Save keyPressDispatcher[Input.Keys.F12] = quickLoad // Quick Load keyPressDispatcher[Input.Keys.HOME] = { mapHolder.setCenterPosition(gameInfo.currentPlayerCiv.getCapital().location) } // Capital City View - keyPressDispatcher['\u000F'] = { OptionsPopup(this).open() } // Ctrl-O: Game Options + keyPressDispatcher['\u000F'] = { this.openOptionsPopup() } // Ctrl-O: Game Options keyPressDispatcher['\u0013'] = { game.setScreen(SaveGameScreen(gameInfo)) } // Ctrl-S: Save keyPressDispatcher['\u000C'] = { game.setScreen(LoadGameScreen(this)) } // Ctrl-L: Load keyPressDispatcher['+'] = { this.mapHolder.zoomIn() } // '+' Zoom - Input.Keys.NUMPAD_ADD would need dispatcher patch diff --git a/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt b/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt index d61730089b..c3d531a276 100644 --- a/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt +++ b/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt @@ -45,6 +45,7 @@ class OptionsPopup(val previousScreen:CameraStageBaseScreen) : Popup(previousScr add(scrollPane).maxHeight(screen.stage.height * 0.6f).row() addCloseButton { + previousScreen.game.limitOrientationsHelper?.allowPortrait(settings.allowAndroidPortrait) if (previousScreen is WorldScreen) previousScreen.enableNextTurnButtonAfterOptions() } @@ -73,10 +74,11 @@ class OptionsPopup(val previousScreen:CameraStageBaseScreen) : Popup(previousScr if (previousScreen is WorldScreen) { previousScreen.game.worldScreen = WorldScreen(previousScreen.gameInfo, previousScreen.viewingCiv) previousScreen.game.setWorldScreen() + } else if (previousScreen is MainMenuScreen) { previousScreen.game.setScreen(MainMenuScreen()) } - OptionsPopup(previousScreen.game.screen as CameraStageBaseScreen).open() + (previousScreen.game.screen as CameraStageBaseScreen).openOptionsPopup() } private fun rebuildOptionsTable() { @@ -137,9 +139,17 @@ class OptionsPopup(val previousScreen:CameraStageBaseScreen) : Popup(previousScr settings.showExperimentalWorldWrap) { settings.showExperimentalWorldWrap = it } + if (previousScreen.game.limitOrientationsHelper != null) { + addYesNoRow("Enable portrait orientation", settings.allowAndroidPortrait) { + settings.allowAndroidPortrait = it + // Note the following might close the options screen indirectly and delayed + previousScreen.game.limitOrientationsHelper!!.allowPortrait(it) + } + } addSoundEffectsVolumeSlider() addMusicVolumeSlider() + addTranslationGeneration() addModCheckerPopup() addSetUserId() diff --git a/core/src/com/unciv/ui/worldscreen/mainmenu/WorldScreenMenuPopup.kt b/core/src/com/unciv/ui/worldscreen/mainmenu/WorldScreenMenuPopup.kt index 34fa4a9237..8cf9a4847e 100644 --- a/core/src/com/unciv/ui/worldscreen/mainmenu/WorldScreenMenuPopup.kt +++ b/core/src/com/unciv/ui/worldscreen/mainmenu/WorldScreenMenuPopup.kt @@ -35,7 +35,7 @@ class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen) { } addMenuButton("Victory status") { worldScreen.game.setScreen(VictoryScreen(worldScreen)) } - addMenuButton("Options") { OptionsPopup(worldScreen).open(force = true) } + addMenuButton("Options") { worldScreen.openOptionsPopup() } addMenuButton("Community") { WorldScreenCommunityPopup(worldScreen).open(force = true) } addSquareButton(Constants.close) { @@ -80,4 +80,4 @@ class WorldScreenCommunityPopup(val worldScreen: WorldScreen) : Popup(worldScree addCloseButton() } -} \ No newline at end of file +}