@@ -2,16 +2,21 @@ package org.draken.usagi.core.ui
22
33import android.animation.ObjectAnimator
44import android.animation.ValueAnimator
5+ import android.content.ContentValues
56import android.content.Intent
67import android.os.Build
78import android.os.Bundle
9+ import android.os.Environment
810import android.os.Handler
911import android.os.Looper
1012import android.os.Process.killProcess
1113import android.os.Process.myPid
14+ import android.provider.MediaStore
1215import android.util.TypedValue
1316import android.view.View
1417import android.widget.TextView
18+ import android.widget.Toast
19+ import androidx.activity.result.contract.ActivityResultContracts
1520import androidx.appcompat.app.AppCompatDelegate
1621import androidx.core.graphics.ColorUtils
1722import androidx.core.view.WindowInsetsCompat
@@ -26,21 +31,26 @@ import com.google.android.material.R as materialR
2631class AppCrashActivity : BaseActivity <ActivityCrashBinding >() {
2732 private var headingRunnable: Runnable ? = null
2833 private val handler = Handler (Looper .getMainLooper())
34+ private var crashReport = NO_TRACE
35+ private val saveCrashLogLauncher =
36+ registerForActivityResult(ActivityResultContracts .CreateDocument (" text/plain" )) { uri ->
37+ if (uri != null ) writeCrashReport(uri)
38+ }
2939
3040 override fun onCreate (savedInstanceState : Bundle ? ) {
3141 applyTheme()
3242 super .onCreate(savedInstanceState)
3343 setContentView(ActivityCrashBinding .inflate(layoutInflater))
3444 val stackTrace = intent.getStringExtra(EXTRA_STACK_TRACE ) ? : NO_TRACE
35- val report =
45+ crashReport =
3646 buildString {
3747 appendLine(" Android: ${Build .VERSION .RELEASE } (SDK ${Build .VERSION .SDK_INT } )" )
3848 appendLine(" Device: ${Build .MANUFACTURER } ${Build .MODEL } " )
3949 appendLine(" Version: ${BuildConfig .VERSION_NAME } (${BuildConfig .VERSION_CODE } )" )
4050 appendLine(" -------------------------------------" )
4151 append(stackTrace)
4252 }
43- viewBinding.crashTextView.text = report
53+ viewBinding.crashTextView.text = crashReport
4454 viewBinding.buttonReport.setOnClickListener {
4555 runCatching {
4656 val intent =
@@ -50,11 +60,12 @@ class AppCrashActivity : BaseActivity<ActivityCrashBinding>() {
5060 Intent .EXTRA_SUBJECT ,
5161 " Usagi v${BuildConfig .VERSION_NAME } (${Build .MANUFACTURER } ${Build .MODEL } ) w/ ${Build .VERSION .RELEASE } " ,
5262 )
53- putExtra(Intent .EXTRA_TEXT , report )
63+ putExtra(Intent .EXTRA_TEXT , crashReport )
5464 }
5565 startActivity(Intent .createChooser(intent, getString(R .string.report)))
5666 }
5767 }
68+ viewBinding.buttonSaveCrashLog.setOnClickListener { saveCrashReport() }
5869 viewBinding.buttonRestart.setOnClickListener {
5970 startActivity(
6071 Intent (this , MainActivity ::class .java)
@@ -81,6 +92,50 @@ class AppCrashActivity : BaseActivity<ActivityCrashBinding>() {
8192 if (isFinishing) killProcess(myPid())
8293 }
8394
95+ private fun saveCrashReport () {
96+ val fileName = " usagi-crash-${System .currentTimeMillis()} .txt"
97+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q ) {
98+ val values =
99+ ContentValues ().apply {
100+ put(MediaStore .Downloads .DISPLAY_NAME , fileName)
101+ put(MediaStore .Downloads .MIME_TYPE , " text/plain" )
102+ put(MediaStore .Downloads .RELATIVE_PATH , Environment .DIRECTORY_DOWNLOADS )
103+ put(MediaStore .Downloads .IS_PENDING , 1 )
104+ }
105+ val uri = contentResolver.insert(MediaStore .Downloads .EXTERNAL_CONTENT_URI , values)
106+ if (uri == null ) {
107+ showCrashLogSaveResult(false )
108+ return
109+ }
110+ runCatching {
111+ contentResolver.openOutputStream(uri)?.bufferedWriter(Charsets .UTF_8 )?.use { it.write(crashReport) }
112+ ? : error(" Unable to open crash log output stream" )
113+ contentResolver.update(uri, ContentValues ().apply { put(MediaStore .Downloads .IS_PENDING , 0 ) }, null , null )
114+ }.onFailure {
115+ contentResolver.delete(uri, null , null )
116+ }.onSuccess { showCrashLogSaveResult(true ) }
117+ } else {
118+ saveCrashLogLauncher.launch(fileName)
119+ }
120+ }
121+
122+ private fun writeCrashReport (uri : android.net.Uri ) {
123+ runCatching {
124+ contentResolver.openOutputStream(uri)?.bufferedWriter(Charsets .UTF_8 )?.use { it.write(crashReport) }
125+ ? : error(" Unable to open crash log output stream" )
126+ }.onSuccess { showCrashLogSaveResult(true ) }
127+ .onFailure { showCrashLogSaveResult(false ) }
128+ }
129+
130+ private fun showCrashLogSaveResult (isSuccess : Boolean ) {
131+ Toast
132+ .makeText(
133+ this ,
134+ getString(if (isSuccess) R .string.crash_log_saved else R .string.crash_log_save_failed),
135+ Toast .LENGTH_SHORT ,
136+ ).show()
137+ }
138+
84139 private fun applyTheme () {
85140 val nightMode = intent.getIntExtra(EXTRA_THEME_NIGHT_MODE , AppCompatDelegate .MODE_NIGHT_FOLLOW_SYSTEM )
86141 AppCompatDelegate .setDefaultNightMode(nightMode)
0 commit comments