/ *
* Copyright ( c ) 2023 to present Androidacy and contributors . Names , logos , icons , and the Androidacy name are all trademarks of Androidacy and may not be used without license . See LICENSE for more information .
* /
package com.fox2code.mmm
import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.Toast
import com.fox2code.foxcompat.app.FoxActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textview.MaterialTextView
import io.sentry.Sentry
import io.sentry.UserFeedback
import io.sentry.protocol.SentryId
import timber.log.Timber
import java.io.PrintWriter
import java.io.StringWriter
class CrashHandler : FoxActivity ( ) {
@Suppress ( " DEPRECATION " , " KotlinConstantConditions " )
@SuppressLint ( " RestrictedApi " )
override fun onCreate ( savedInstanceState : Bundle ? ) {
Timber . i ( " CrashHandler.onCreate(%s) " , savedInstanceState )
// log intent with extras
if ( BuildConfig . DEBUG ) Timber . d ( " CrashHandler.onCreate: intent=%s " , intent )
super . onCreate ( savedInstanceState )
setContentView ( R . layout . activity _crash _handler )
// set crash_details MaterialTextView to the exception passed in the intent or unknown if null
// convert stacktrace from array to string, and pretty print it (first line is the exception, the rest is the stacktrace, with each line indented by 4 spaces)
val crashDetails = findViewById < MaterialTextView > ( R . id . crash _details )
crashDetails . text = " "
// get the exception from the intent
val exception = intent . getSerializableExtra ( " exception " ) as Throwable ?
// get the crashReportingEnabled from the intent
val crashReportingEnabled = intent . getBooleanExtra ( " crashReportingEnabled " , false )
// if the exception is null, set the crash details to "Unknown"
if ( exception == null ) {
crashDetails . setText ( R . string . crash _details )
} else {
// if the exception is not null, set the crash details to the exception and stacktrace
// stacktrace is an StacktraceElement, so convert it to a string and replace the commas with newlines
val stringWriter = StringWriter ( )
exception . printStackTrace ( PrintWriter ( stringWriter ) )
var stacktrace = stringWriter . toString ( )
stacktrace = stacktrace . replace ( " , " , " \n " )
crashDetails . text = getString ( R . string . crash _full _stacktrace , stacktrace )
}
// force sentry to send all events
Sentry . flush ( 2000 )
var lastEventId = intent . getStringExtra ( " lastEventId " )
// if event id is all zeros, set it to "". test this by matching the regex ^0+$ (all zeros)
if ( lastEventId ?. matches ( " ^0+ $ " . toRegex ( ) ) == true ) {
lastEventId = " "
}
if ( BuildConfig . DEBUG ) Timber . d (
" CrashHandler.onCreate: lastEventId=%s, crashReportingEnabled=%s " ,
lastEventId ,
crashReportingEnabled
)
// get name, email, and message fields
val name = findViewById < EditText > ( R . id . feedback _name )
val email = findViewById < EditText > ( R . id . feedback _email )
val description = findViewById < EditText > ( R . id . feedback _message )
val submit = findViewById < View > ( R . id . feedback _submit )
if ( lastEventId . isNullOrEmpty ( ) && crashReportingEnabled ) {
// if lastEventId is null, hide the feedback button
if ( BuildConfig . DEBUG ) Timber . d ( " CrashHandler.onCreate: lastEventId is null but crash reporting is enabled. This may indicate a bug in the crash reporting system. " )
submit . visibility = View . GONE
findViewById < MaterialTextView > ( R . id . feedback _text ) . setText ( R . string . no _sentry _id )
} else {
// if lastEventId is not null, enable the feedback name, email, message, and submit button
email . isEnabled = true
name . isEnabled = true
description . isEnabled = true
submit . isEnabled = true
}
// disable feedback if sentry is disabled
if ( crashReportingEnabled && lastEventId != null ) {
// get submit button
findViewById < View > ( R . id . feedback _submit ) . setOnClickListener { _ : View ? ->
// require the feedback_message, rest is optional
if ( description . text . toString ( ) == " " ) {
Toast . makeText ( this , R . string . sentry _dialogue _empty _message , Toast . LENGTH _LONG )
. show ( )
return @setOnClickListener
}
// if email or name is empty, use "Anonymous"
val nameString =
arrayOf ( if ( name . text . toString ( ) == " " ) " Anonymous " else name . text . toString ( ) )
val emailString =
arrayOf ( if ( email . text . toString ( ) == " " ) " Anonymous " else email . text . toString ( ) )
Thread {
try {
val userFeedback =
UserFeedback ( SentryId ( lastEventId ) )
// Setups the JSON body
if ( nameString [ 0 ] == " " ) nameString [ 0 ] = " Anonymous "
if ( emailString [ 0 ] == " " ) emailString [ 0 ] = " Anonymous "
userFeedback . name = nameString [ 0 ]
userFeedback . email = emailString [ 0 ]
userFeedback . comments = description . text . toString ( )
Sentry . captureUserFeedback ( userFeedback )
Timber . i (
" Submitted user feedback: name %s email %s comment %s " ,
nameString [ 0 ] ,
emailString [ 0 ] ,
description . text . toString ( )
)
runOnUiThread {
Toast . makeText (
this , R . string . sentry _dialogue _success , Toast . LENGTH _LONG
) . show ( )
}
// Close the activity
finish ( )
// start the main activity
startActivity ( packageManager . getLaunchIntentForPackage ( packageName ) )
} catch ( e : Exception ) {
Timber . e ( e , " Failed to submit user feedback " )
// Show a toast if the user feedback could not be submitted
runOnUiThread {
Toast . makeText (
this , R . string . sentry _dialogue _failed _toast , Toast . LENGTH _LONG
) . show ( )
}
}
} . start ( )
}
// get restart button
findViewById < View > ( R . id . restart ) . setOnClickListener { _ : View ? ->
// Restart the app and submit sans feedback
val sentryException = intent . getSerializableExtra ( " sentryException " ) as Throwable ?
if ( crashReportingEnabled ) Sentry . captureException ( sentryException !! )
finish ( )
startActivity ( packageManager . getLaunchIntentForPackage ( packageName ) )
}
} else if ( ! crashReportingEnabled ) {
// set feedback_text to "Crash reporting is disabled"
( findViewById < View > ( R . id . feedback _text ) as MaterialTextView ) . setText ( R . string . sentry _enable _nag )
submit . setOnClickListener { _ : View ? ->
Toast . makeText (
this , R . string . sentry _dialogue _disabled , Toast . LENGTH _LONG
) . show ( )
}
// handle restart button
// we have to explicitly enable it because it's disabled by default
findViewById < View > ( R . id . restart ) . isEnabled = true
findViewById < View > ( R . id . restart ) . setOnClickListener { _ : View ? ->
// Restart the app
finish ( )
startActivity ( packageManager . getLaunchIntentForPackage ( packageName ) )
}
}
// handle reset button
findViewById < View > ( R . id . reset ) . setOnClickListener { _ : View ? ->
// show a confirmation material dialog
val builder = MaterialAlertDialogBuilder ( this )
builder . setTitle ( R . string . reset _app )
builder . setMessage ( R . string . reset _app _confirmation )
builder . setPositiveButton ( R . string . reset ) { _ : DialogInterface ? , _ : Int ->
// reset the app
MainApplication . INSTANCE !! . resetApp ( )
}
builder . setNegativeButton ( R . string . cancel ) { _ : DialogInterface ? , _ : Int -> }
builder . show ( )
}
}
fun copyCrashDetails ( view : View ) {
// change view to a checkmark
view . setBackgroundResource ( R . drawable . baseline _check _24 )
// copy crash_details to clipboard
val clipboard = getSystemService ( CLIPBOARD _SERVICE ) as ClipboardManager
val crashDetails =
( findViewById < View > ( R . id . crash _details ) as MaterialTextView ) . text . toString ( )
clipboard . setPrimaryClip ( ClipData . newPlainText ( " crash_details " , crashDetails ) )
// show a toast
Toast . makeText ( this , R . string . crash _details _copied , Toast . LENGTH _LONG ) . show ( )
// after 1 second, change the view back to a copy button
Thread {
try {
Thread . sleep ( 1000 )
} catch ( e : InterruptedException ) {
Thread . currentThread ( ) . interrupt ( )
}
runOnUiThread { view . setBackgroundResource ( R . drawable . baseline _copy _all _24 ) }
} . start ( )
}
}