diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a2f7bf12..1d512d36 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -263,6 +263,11 @@ android:name=".ActMediaViewer" android:theme="@style/AppTheme.Dark.NoActionBar" /> + + diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActExitReasons.kt b/app/src/main/java/jp/juggler/subwaytooter/ActExitReasons.kt new file mode 100644 index 00000000..78be117d --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/ActExitReasons.kt @@ -0,0 +1,156 @@ +package jp.juggler.subwaytooter + +import android.app.ActivityManager +import android.app.ApplicationExitInfo +import android.content.Context +import android.os.Build +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.ListView +import android.widget.TextView +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatActivity +import jp.juggler.subwaytooter.api.entity.TootStatus +import jp.juggler.util.LogCategory +import jp.juggler.util.decodeUTF8 +import jp.juggler.util.withCaption +import org.apache.commons.io.IOUtils +import java.io.ByteArrayOutputStream +import java.io.InputStream + +@RequiresApi(Build.VERSION_CODES.R) +class ActExitReasons : AppCompatActivity() { + + companion object { + + val log = LogCategory("ActExitReasons") + + fun reasonString(v : Int) = when(v) { + ApplicationExitInfo.REASON_ANR -> + "REASON_ANR Application process was killed due to being unresponsive (ANR)." + + ApplicationExitInfo.REASON_CRASH -> + "REASON_CRASH Application process died because of an unhandled exception in Java code." + + ApplicationExitInfo.REASON_CRASH_NATIVE -> + "REASON_CRASH_NATIVE Application process died because of a native code crash." + + ApplicationExitInfo.REASON_DEPENDENCY_DIED -> + "REASON_DEPENDENCY_DIED Application process was killed because its dependency was going away, for example, a stable content provider connection's client will be killed if the provider is killed." + + ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE -> + "REASON_EXCESSIVE_RESOURCE_USAGE Application process was killed by the system due to excessive resource usage." + + ApplicationExitInfo.REASON_EXIT_SELF -> + "REASON_EXIT_SELF Application process exit normally by itself, for example, via java.lang.System#exit; getStatus will specify the exit code." + + ApplicationExitInfo.REASON_INITIALIZATION_FAILURE -> + "REASON_INITIALIZATION_FAILURE Application process was killed because of initialization failure, for example, it took too long to attach to the system during the start, or there was an error during initialization." + + ApplicationExitInfo.REASON_LOW_MEMORY -> + "REASON_LOW_MEMORY Application process was killed by the system low memory killer, meaning the system was under memory pressure at the time of kill." + + ApplicationExitInfo.REASON_OTHER -> + "REASON_OTHER Application process was killed by the system for various other reasons which are not by problems in apps and not actionable by apps, for example, the system just finished updates; getDescription will specify the cause given by the system." + + ApplicationExitInfo.REASON_PERMISSION_CHANGE -> + "REASON_PERMISSION_CHANGE Application process was killed due to a runtime permission change." + + ApplicationExitInfo.REASON_SIGNALED -> + "REASON_SIGNALED Application process died due to the result of an OS signal; for example, android.system.OsConstants#SIGKILL; getStatus will specify the signal number." + + ApplicationExitInfo.REASON_UNKNOWN -> + "REASON_UNKNOWN Application process died due to unknown reason." + + ApplicationExitInfo.REASON_USER_REQUESTED -> + "REASON_USER_REQUESTED Application process was killed because of the user request, for example, user clicked the \"Force stop\" button of the application in the Settings, or removed the application away from Recents." + + ApplicationExitInfo.REASON_USER_STOPPED -> + "REASON_USER_STOPPED Application process was killed, because the user it is running as on devices with mutlple users, was stopped." + + else -> "?($v)" + } + + fun readStream(inStream : InputStream?) = + if(inStream == null) + "(null)" + else try { + val bao = ByteArrayOutputStream() + IOUtils.copy(inStream, bao) + bao.toByteArray().decodeUTF8() + } catch(ex : Throwable) { + ex.withCaption("readStream failed.") + } finally { + inStream.close() + } + } + + private lateinit var listView : ListView + private lateinit var adapter : MyAdapter + + override fun onCreate(savedInstanceState : Bundle?) { + super.onCreate(savedInstanceState) + App1.setActivityTheme(this) + setContentView(R.layout.act_exit_reasons) + App1.initEdgeToEdge(this) + + val am = getSystemService(ActivityManager::class.java) + if(am == null) { + log.e("can't find ActivityManager") + finish() + return + } + + this.listView = findViewById(R.id.listView) + this.adapter = MyAdapter() + adapter.list = am.getHistoricalProcessExitReasons(null, 0, 200) + .filterNotNull() + .toList() + + listView.adapter = adapter + + } + + class MyViewHolder(val viewRoot : View) { + + private val textView : TextView = viewRoot.findViewById(R.id.textView) + + fun bind(context : Context, info : ApplicationExitInfo) { + + textView.text = """ + timestamp=${TootStatus.formatTime(context, info.timestamp, bAllowRelative = false)} + importance=${info.importance} + pss=${info.pss} + rss=${info.rss} + reason=${reasonString(info.reason)} + status=${info.status} + description=${info.description} + trace=${readStream(info.traceInputStream)} + """.trimIndent() + } + } + + fun genView(parent : ViewGroup?) : View = + layoutInflater.inflate(R.layout.lv_exit_reason, parent, false) + .also { it.tag = MyViewHolder(it) } + + inner class MyAdapter : BaseAdapter() { + + var list : List = emptyList() + + override fun getCount() : Int = list.size + override fun getItem(position : Int) : Any? = list[position] + override fun getItemId(position : Int) : Long = 0 + override fun getView( + position : Int, + convertView : View?, + parent : ViewGroup? + ) : View = + (convertView ?: genView(parent)).also { view -> + (view.tag as? MyViewHolder)?.bind(this@ActExitReasons, list[position]) + } + } + +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/AppSettingItem.kt b/app/src/main/java/jp/juggler/subwaytooter/AppSettingItem.kt index aae21b8d..ce02e963 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/AppSettingItem.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/AppSettingItem.kt @@ -939,10 +939,20 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett } section(R.string.developer_options) { + sw(Pref.bpCheckBetaVersion, R.string.check_beta_release) + action(R.string.drawable_list) { action = { startActivity(Intent(this, ActDrawableList::class.java)) } } - sw(Pref.bpCheckBetaVersion, R.string.check_beta_release) + action(R.string.exit_reasons) { + action = { + if(Build.VERSION.SDK_INT >= 30 ){ + startActivity(Intent(this, ActExitReasons::class.java)) + }else{ + showToast(false, "this feature requires Android 11") + } + } + } } diff --git a/app/src/main/res/layout/act_exit_reasons.xml b/app/src/main/res/layout/act_exit_reasons.xml new file mode 100644 index 00000000..792d357a --- /dev/null +++ b/app/src/main/res/layout/act_exit_reasons.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/lv_exit_reason.xml b/app/src/main/res/layout/lv_exit_reason.xml new file mode 100644 index 00000000..5b07a6d2 --- /dev/null +++ b/app/src/main/res/layout/lv_exit_reason.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 245b5dc6..e8e43c57 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1044,4 +1044,5 @@ Release note (Please find the APK file in assets section.) Release note Check beta release + Exit reasons