CrashScreen info improved, allow easier testing of CrashScreen (#10485)

* Drop AndroidTvLauncher - unused

* Additional memory info on Android CrashScreen

* Refactor getModsAndBaseRuleset - reduce clones and put base first

* Add Permanent audiovisual Mods to CrashScreen report

* Add a secret Debug option to intentionally crash Unciv

* Make "Secret" debug OptionsPopup page available on Android w/o physical keyboard

* Add AndroidTvLauncher back in

---------

Co-authored-by: Yair Morgenstern <yairm210@hotmail.com>
This commit is contained in:
SomeTroglodyte
2023-11-13 21:26:38 +01:00
committed by GitHub
parent 60e2af3bba
commit 659a01c166
14 changed files with 102 additions and 36 deletions

View File

@ -60,8 +60,8 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<receiver android:name=".CopyToClipboardReceiver" android:exported="false" />
</application>

View File

@ -20,7 +20,7 @@ open class AndroidLauncher : AndroidApplication() {
super.onCreate(savedInstanceState)
// Setup Android logging
Log.backend = AndroidLogBackend()
Log.backend = AndroidLogBackend(this)
// Setup Android display
Display.platform = AndroidDisplay(this)
@ -109,5 +109,3 @@ open class AndroidLauncher : AndroidApplication() {
super.onActivityResult(requestCode, resultCode, data)
}
}
class AndroidTvLauncher:AndroidLauncher()

View File

@ -1,5 +1,8 @@
package com.unciv.app
import android.app.Activity
import android.app.ActivityManager
import android.content.Context
import android.os.Build
import android.util.Log
import com.unciv.utils.LogBackend
@ -7,7 +10,14 @@ import com.unciv.utils.Tag
private const val TAG_MAX_LENGTH = 23
class AndroidLogBackend : LogBackend {
/**
* Unciv's logger implementation for Android
*
* * Note: Gets and keeps a reference to [AndroidLauncher] as [activity] only to get memory info for [CrashScreen][com.unciv.ui.crashhandling.CrashScreen].
*
* @see com.unciv.utils.Log
*/
class AndroidLogBackend(private val activity: Activity) : LogBackend {
override fun debug(tag: Tag, curThreadName: String, msg: String) {
Log.d(toAndroidTag(tag), "[$curThreadName] $msg")
@ -21,12 +31,29 @@ class AndroidLogBackend : LogBackend {
return !BuildConfig.DEBUG
}
/**
* @see com.unciv.app.desktop.SystemUtils.getSystemInfo
*/
override fun getSystemInfo(): String {
val memoryInfo = getMemoryInfo()
val javaRuntime = Runtime.getRuntime()
return """
Device Model: ${Build.MODEL}
API Level: ${Build.VERSION.SDK_INT}
System Memory: ${memoryInfo.totalMem.formatMB()}
Available (used by Kernel): ${memoryInfo.availMem.formatMB()}
System Low Memory state: ${memoryInfo.lowMemory}
Java heap limit: ${javaRuntime.maxMemory().formatMB()}
Java heap free: ${javaRuntime.freeMemory().formatMB()}
""".trimIndent()
}
private fun getMemoryInfo() = ActivityManager.MemoryInfo().apply {
val activityManager = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
activityManager.getMemoryInfo(this) // API writes into a structure we must supply
}
private fun Long.formatMB() = "${(this + 524288L) / 1048576L} MB"
}
private fun toAndroidTag(tag: Tag): String {

View File

@ -0,0 +1,6 @@
package com.unciv.app
/**
* Proxy without functionality, referenced in AndroidManifest.xml for intent.category.LEANBACK_LAUNCHER
*/
class AndroidTvLauncher : AndroidLauncher()

View File

@ -105,7 +105,14 @@ class GameParameters : IsPartOfGameInfoSerialization { // Default values are the
yield(if (mods.isEmpty()) "no mods" else mods.joinToString(",", "mods=(", ")", 6) )
}.joinToString(prefix = "(", postfix = ")")
fun getModsAndBaseRuleset(): HashSet<String> {
return mods.toHashSet().apply { add(baseRuleset) }
/** Get all mods including base
*
* The returned Set is ordered base first, then in the order they are stored in a save.
* This creates a fresh instance, and the caller is allowed to mutate it.
*/
fun getModsAndBaseRuleset() =
LinkedHashSet<String>(mods.size + 1).apply {
add(baseRuleset)
addAll(mods)
}
}

View File

@ -98,7 +98,6 @@ fun Actor.onRightClick(sound: UncivSound = UncivSound.Click, action: ActivationA
* A [sound] will be played (concurrently) on activation unless you specify [UncivSound.Silent].
* @return `this` to allow chaining
*/
@Suppress("unused") // Just in case - for now, the Longpress in WorldMapHolder is using onActivation directly
fun Actor.onLongPress(sound: UncivSound = UncivSound.Click, action: ActivationAction): Actor =
onActivation(ActivationTypes.Longpress, sound, noEquivalence = true, action)

View File

@ -10,11 +10,11 @@ import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.files.UncivFiles
import com.unciv.models.ruleset.RulesetCache
import com.unciv.ui.components.widgets.AutoScrollPane
import com.unciv.ui.components.extensions.addBorder
import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.extensions.setFontSize
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.widgets.AutoScrollPane
import com.unciv.ui.images.IconTextButton
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popups.ToastPopup
@ -24,6 +24,8 @@ import java.io.PrintWriter
import java.io.StringWriter
/** Screen to crash to when an otherwise unhandled exception or error is thrown. */
//todo We may be in a critical low-memory situation. Using a lot ot String concatenation and trimIndent
// could make the display fail when a more efficient StringBuilder approach might still succeed.
class CrashScreen(val exception: Throwable) : BaseScreen() {
private companion object {
@ -59,14 +61,23 @@ class CrashScreen(val exception: Throwable) : BaseScreen() {
/** @return Mods from the last active save game if any, or an informational note otherwise. */
private fun tryGetSaveMods(): String {
if (!UncivGame.isCurrentInitialized() || UncivGame.Current.gameInfo == null)
return ""
return "\n**Save Mods:**\n```\n" +
if (!UncivGame.isCurrentInitialized()) return ""
val game = UncivGame.Current.gameInfo ?: return ""
val sb = StringBuilder(160) // capacity: Just some guess
sb.append("\n**Save Mods:**\n```\n")
try { // Also from old CrashController().buildReport(), also could still error at .toString().
LinkedHashSet(UncivGame.Current.gameInfo!!.gameParameters.getModsAndBaseRuleset()).toString()
sb.append(game.gameParameters.getModsAndBaseRuleset().toString())
} catch (e: Throwable) {
"No mod data: $e"
} + "\n```\n"
sb.append("No mod data: $e")
}
sb.append("\n```\n")
val visualMods = UncivGame.Current.settings.visualMods
if (visualMods.isEmpty())
return sb.toString()
sb.append("**Permanent audiovisual Mods**:\n```\n")
sb.append(visualMods.toString())
sb.append("\n```\n")
return sb.toString()
}

View File

@ -1,18 +1,21 @@
package com.unciv.ui.popups.options
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton.TextButtonStyle
import com.unciv.GUI
import com.unciv.UncivGame
import com.unciv.logic.UncivShowableException
import com.unciv.logic.files.MapSaver
import com.unciv.logic.files.UncivFiles
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.ui.components.widgets.UncivSlider
import com.unciv.ui.components.UncivTextField
import com.unciv.ui.components.extensions.addSeparator
import com.unciv.ui.components.extensions.toCheckBox
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.widgets.UncivSlider
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.utils.DebugUtils
@ -121,4 +124,10 @@ fun debugTab(
GUI.setUpdateWorldOnNextRender()
}
add(giveResourcesButton).colspan(2).row()
addSeparator()
add("* Crash Unciv! *".toTextButton(skin.get("negative", TextButtonStyle::class.java)).onClick {
throw UncivShowableException("Intentional crash")
}).colspan(2).row()
addSeparator()
}

View File

@ -7,10 +7,10 @@ import com.unciv.GUI
import com.unciv.UncivGame
import com.unciv.models.metadata.BaseRuleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.ui.components.widgets.TabbedPager
import com.unciv.ui.components.extensions.areSecretKeysPressed
import com.unciv.ui.components.extensions.center
import com.unciv.ui.components.extensions.toCheckBox
import com.unciv.ui.components.widgets.TabbedPager
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popups.Popup
import com.unciv.ui.screens.basescreen.BaseScreen
@ -28,6 +28,7 @@ import kotlin.reflect.KMutableProperty0
class OptionsPopup(
screen: BaseScreen,
private val selectPage: Int = defaultPage,
withDebug: Boolean = false,
private val onClose: () -> Unit = {}
) : Popup(screen.stage, /** [TabbedPager] handles scrolling */ scrollable = Scrollability.None) {
@ -110,7 +111,7 @@ class OptionsPopup(
val content = ModCheckTab(screen)
tabs.addPage("Locate mod errors", content, ImageGetter.getImage("OtherIcons/Mods"), 24f)
}
if (Gdx.input.areSecretKeysPressed()) {
if (withDebug || Gdx.input.areSecretKeysPressed()) {
tabs.addPage("Debug", debugTab(this), ImageGetter.getImage("OtherIcons/SecretOptions"), 24f, secret = true)
}

View File

@ -174,8 +174,8 @@ abstract class BaseScreen : Screen {
/** @return `true` if the screen is narrower than 4:3 landscape */
fun isNarrowerThan4to3() = stage.isNarrowerThan4to3()
open fun openOptionsPopup(startingPage: Int = OptionsPopup.defaultPage, onClose: () -> Unit = {}) {
OptionsPopup(this, startingPage, onClose).open(force = true)
open fun openOptionsPopup(startingPage: Int = OptionsPopup.defaultPage, withDebug: Boolean = false, onClose: () -> Unit = {}) {
OptionsPopup(this, startingPage, withDebug, onClose).open(force = true)
}
}

View File

@ -22,7 +22,6 @@ import com.unciv.models.metadata.GameSetupInfo
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.tilesets.TileSetCache
import com.unciv.ui.components.widgets.AutoScrollPane
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
import com.unciv.ui.components.extensions.center
import com.unciv.ui.components.extensions.surroundWithCircle
@ -32,7 +31,9 @@ import com.unciv.ui.components.input.KeyShortcutDispatcherVeto
import com.unciv.ui.components.input.KeyboardBinding
import com.unciv.ui.components.input.keyShortcuts
import com.unciv.ui.components.input.onActivation
import com.unciv.ui.components.input.onLongPress
import com.unciv.ui.components.tilegroups.TileGroupMap
import com.unciv.ui.components.widgets.AutoScrollPane
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popups.Popup
import com.unciv.ui.popups.ToastPopup
@ -45,9 +46,9 @@ import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen
import com.unciv.ui.screens.mainmenuscreen.EasterEggRulesets.modifyForEasterEgg
import com.unciv.ui.screens.mapeditorscreen.EditorMapHolder
import com.unciv.ui.screens.mapeditorscreen.MapEditorScreen
import com.unciv.ui.screens.modmanager.ModManagementScreen
import com.unciv.ui.screens.multiplayerscreens.MultiplayerScreen
import com.unciv.ui.screens.newgamescreen.NewGameScreen
import com.unciv.ui.screens.modmanager.ModManagementScreen
import com.unciv.ui.screens.savescreens.LoadGameScreen
import com.unciv.ui.screens.savescreens.QuickSave
import com.unciv.ui.screens.worldscreen.BackgroundActor
@ -164,7 +165,8 @@ class MainMenuScreen: BaseScreen(), RecreateOnResize {
column2.add(modsTable).row()
val optionsTable = getMenuButton("Options", "OtherIcons/Options", KeyboardBinding.MainMenuOptions)
{ this.openOptionsPopup() }
{ openOptionsPopup() }
optionsTable.onLongPress { openOptionsPopup(withDebug = true) }
column2.add(optionsTable).row()

View File

@ -17,10 +17,6 @@ import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.tr
import com.unciv.ui.audio.MusicMood
import com.unciv.ui.audio.MusicTrackChooserFlags
import com.unciv.ui.components.widgets.AutoScrollPane
import com.unciv.ui.components.widgets.ExpanderTab
import com.unciv.ui.components.widgets.TranslatedSelectBox
import com.unciv.ui.components.widgets.UncivSlider
import com.unciv.ui.components.extensions.pad
import com.unciv.ui.components.extensions.toCheckBox
import com.unciv.ui.components.extensions.toImageButton
@ -31,6 +27,10 @@ import com.unciv.ui.components.input.keyShortcuts
import com.unciv.ui.components.input.onActivation
import com.unciv.ui.components.input.onChange
import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.widgets.AutoScrollPane
import com.unciv.ui.components.widgets.ExpanderTab
import com.unciv.ui.components.widgets.TranslatedSelectBox
import com.unciv.ui.components.widgets.UncivSlider
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popups.Popup
import com.unciv.ui.screens.basescreen.BaseScreen
@ -458,7 +458,7 @@ class GameOptionsTable(
}
private fun onChooseMod(mod: String) {
val activeMods: LinkedHashSet<String> = LinkedHashSet(gameParameters.getModsAndBaseRuleset())
val activeMods = gameParameters.getModsAndBaseRuleset()
UncivGame.Current.translations.translationActiveMods = activeMods
reloadRuleset()
update()

View File

@ -276,10 +276,10 @@ class WorldScreen(
}
// Handle disabling and re-enabling WASD listener while Options are open
override fun openOptionsPopup(startingPage: Int, onClose: () -> Unit) {
override fun openOptionsPopup(startingPage: Int, withDebug: Boolean, onClose: () -> Unit) {
val oldListener = stage.root.listeners.filterIsInstance<KeyboardPanningListener>().firstOrNull()
if (oldListener != null) stage.removeListener(oldListener)
super.openOptionsPopup(startingPage) {
super.openOptionsPopup(startingPage, withDebug) {
addKeyboardListener()
onClose()
}

View File

@ -1,6 +1,7 @@
package com.unciv.ui.screens.worldscreen.mainmenu
import com.unciv.ui.components.input.KeyboardBinding
import com.unciv.ui.components.input.onLongPress
import com.unciv.ui.popups.Popup
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen
import com.unciv.ui.screens.savescreens.LoadGameScreen
@ -35,10 +36,15 @@ class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen, sc
close()
worldScreen.game.pushScreen(VictoryScreen(worldScreen))
}.row()
addButton("Options", KeyboardBinding.Options) {
val optionsCell = addButton("Options", KeyboardBinding.Options) {
close()
worldScreen.openOptionsPopup()
}.row()
}
optionsCell.actor.onLongPress {
close()
worldScreen.openOptionsPopup(withDebug = true)
}
optionsCell.row()
addButton("Community") {
close()
WorldScreenCommunityPopup(worldScreen).open(force = true)