add crash handler

This commit is contained in:
Josia Pietsch 2025-05-13 13:33:48 +02:00
parent 7a874ef89f
commit 04330ff407
Signed by: jrpie
GPG key ID: E70B571D66986A2D
12 changed files with 270 additions and 7 deletions

View file

@ -8,6 +8,7 @@
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.ACCESS_HIDDEN_PROFILES" /> <uses-permission android:name="android.permission.ACCESS_HIDDEN_PROFILES" />
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" /> <uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:name=".Application" android:name=".Application"
@ -19,6 +20,7 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/launcherBaseTheme" android:theme="@style/launcherBaseTheme"
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
<activity <activity
android:name=".ui.widgets.manage.ManageWidgetPanelsActivity" android:name=".ui.widgets.manage.ManageWidgetPanelsActivity"
android:exported="false" /> android:exported="false" />
@ -80,6 +82,9 @@
<activity <activity
android:name=".ui.LegalInfoActivity" android:name=".ui.LegalInfoActivity"
android:exported="false" /> android:exported="false" />
<activity
android:name=".ui.ReportCrashActivity"
android:exported="false" />
<receiver <receiver
android:name=".actions.lock.LauncherDeviceAdmin" android:name=".actions.lock.LauncherDeviceAdmin"
@ -110,5 +115,4 @@
android:resource="@xml/accessibility_service_config" /> android:resource="@xml/accessibility_service_config" />
</service> </service>
</application> </application>
</manifest> </manifest>

View file

@ -25,6 +25,7 @@ import de.jrpie.android.launcher.preferences.resetPreferences
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.system.exitProcess
const val APP_WIDGET_HOST_ID = 42; const val APP_WIDGET_HOST_ID = 42;
@ -106,6 +107,11 @@ class Application : android.app.Application() {
// TODO Error: Invalid resource ID 0x00000000. // TODO Error: Invalid resource ID 0x00000000.
// DynamicColors.applyToActivitiesIfAvailable(this) // DynamicColors.applyToActivitiesIfAvailable(this)
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
sendCrashNotification(this@Application, throwable)
exitProcess(1)
}
if (Build.VERSION.SDK_INT >= VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
torchManager = TorchManager(this) torchManager = TorchManager(this)
@ -157,6 +163,8 @@ class Application : android.app.Application() {
removeUnusedShortcuts(this) removeUnusedShortcuts(this)
} }
loadApps() loadApps()
createNotificationChannels(this)
} }
fun getCustomAppNames(): HashMap<AbstractAppInfo, String> { fun getCustomAppNames(): HashMap<AbstractAppInfo, String> {

View file

@ -6,9 +6,6 @@ import android.app.role.RoleManager
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.appwidget.AppWidgetProviderInfo
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
@ -227,3 +224,13 @@ fun copyToClipboard(context: Context, text: String) {
val clipData = ClipData.newPlainText("Debug Info", text) val clipData = ClipData.newPlainText("Debug Info", text)
clipboardManager.setPrimaryClip(clipData) clipboardManager.setPrimaryClip(clipData)
} }
fun writeEmail(context: Context, to: String, subject: String, text: String) {
val intent = Intent(Intent.ACTION_SENDTO)
intent.setData("mailto:".toUri())
intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(to))
intent.putExtra(Intent.EXTRA_SUBJECT, subject)
intent.putExtra(Intent.EXTRA_TEXT, text)
context.startActivity(Intent.createChooser(intent, context.getString(R.string.send_email)))
}

View file

@ -0,0 +1,87 @@
package de.jrpie.android.launcher
import android.app.Activity
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import de.jrpie.android.launcher.ui.EXTRA_CRASH_LOG
import de.jrpie.android.launcher.ui.ReportCrashActivity
import java.io.PrintWriter
import java.io.StringWriter
import kotlin.random.Random
private val NOTIFICATION_CHANNEL_CRASH = "launcher:crash"
fun createNotificationChannels(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel = NotificationChannel(
NOTIFICATION_CHANNEL_CRASH,
context.getString(R.string.notification_channel_crash),
NotificationManager.IMPORTANCE_HIGH
)
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(notificationChannel)
}
}
fun requestNotificationPermission(activity: Activity) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
return
}
val permission =
(activity.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED)
if (!permission) {
ActivityCompat.requestPermissions(
activity,
arrayOf( android.Manifest.permission.POST_NOTIFICATIONS ),
1
)
}
}
fun sendCrashNotification(context: Context, throwable: Throwable) {
val stringWriter = StringWriter()
val printWriter = PrintWriter(stringWriter)
throwable.printStackTrace(printWriter)
val intent = Intent(context, ReportCrashActivity::class.java)
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.putExtra(EXTRA_CRASH_LOG, stringWriter.toString())
val pendingIntent = PendingIntent.getActivity(
context,
Random.nextInt(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_CRASH)
.setSmallIcon(R.drawable.baseline_bug_report_24)
.setContentTitle(context.getString(R.string.notification_crash_title))
.setContentText(context.getString(R.string.notification_crash_explanation))
.setContentIntent(pendingIntent)
.setAutoCancel(false)
.setPriority(NotificationCompat.PRIORITY_HIGH)
val notificationManager = NotificationManagerCompat.from(context)
try {
notificationManager.notify(
0,
builder.build()
)
} catch (e: SecurityException) {
Log.e("Crash Notification", "Could not send notification")
}
}

View file

@ -14,6 +14,7 @@ import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersio
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion3 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion3
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion4 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion4
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersionUnknown import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersionUnknown
import de.jrpie.android.launcher.sendCrashNotification
import de.jrpie.android.launcher.ui.HomeActivity import de.jrpie.android.launcher.ui.HomeActivity
import de.jrpie.android.launcher.widgets.ClockWidget import de.jrpie.android.launcher.widgets.ClockWidget
import de.jrpie.android.launcher.widgets.DebugInfoWidget import de.jrpie.android.launcher.widgets.DebugInfoWidget
@ -76,6 +77,7 @@ fun migratePreferencesToNewVersion(context: Context) {
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to restore preferences:\n${e.stackTrace}") Log.e(TAG, "Unable to restore preferences:\n${e.stackTrace}")
sendCrashNotification(context, e)
resetPreferences(context) resetPreferences(context)
} }
} }

View file

@ -0,0 +1,57 @@
package de.jrpie.android.launcher.ui
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.copyToClipboard
import de.jrpie.android.launcher.databinding.ActivityReportCrashBinding
import de.jrpie.android.launcher.getDeviceInfo
import de.jrpie.android.launcher.openInBrowser
import de.jrpie.android.launcher.writeEmail
const val EXTRA_CRASH_LOG = "crashLog"
class ReportCrashActivity : AppCompatActivity() {
// We don't know what caused the crash, so this Activity should use as little functionality as possible.
// In particular it is not a UIObject (and hence looks quite ugly)
private lateinit var binding: ActivityReportCrashBinding
private var report: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialise layout
binding = ActivityReportCrashBinding.inflate(layoutInflater)
setContentView(binding.root)
setTitle(R.string.report_crash_title)
setSupportActionBar(binding.reportCrashAppbar)
supportActionBar?.setDisplayHomeAsUpEnabled(false)
report = intent.getStringExtra(EXTRA_CRASH_LOG)
binding.reportCrashButtonCopy.setOnClickListener {
copyToClipboard(this,
"Device Info:\n${getDeviceInfo()}\n\nCrash Log:\n${report}")
}
binding.reportCrashButtonMail.setOnClickListener {
writeEmail(
this,
getString(R.string.settings_meta_report_bug_mail),
"Crash in μLauncher",
"Hi!\nUnfortunately, μLauncher crashed:\n" +
"\nDevice Info\n\n${getDeviceInfo()}\n\n" +
"\nCrash Log\n\n${report}\n" +
"\nAdditional Information\n\n" +
"[Please add additional information: What did you do when the crash happened? Do you know how to trigger it? ... ]"
)
}
binding.reportCrashButtonReport.setOnClickListener {
openInBrowser(
getString(R.string.settings_meta_report_bug_link),
this
)
}
}
}

View file

@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment
import de.jrpie.android.launcher.BuildConfig.VERSION_CODE import de.jrpie.android.launcher.BuildConfig.VERSION_CODE
import de.jrpie.android.launcher.databinding.Tutorial5FinishBinding import de.jrpie.android.launcher.databinding.Tutorial5FinishBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.requestNotificationPermission
import de.jrpie.android.launcher.setDefaultHomeScreen import de.jrpie.android.launcher.setDefaultHomeScreen
import de.jrpie.android.launcher.ui.UIObject import de.jrpie.android.launcher.ui.UIObject
@ -31,8 +32,10 @@ class TutorialFragment5Finish : Fragment(), UIObject {
override fun onStart() { override fun onStart() {
super<Fragment>.onStart() super<Fragment>.onStart()
super<UIObject>.onStart() super<UIObject>.onStart()
requestNotificationPermission(requireActivity())
} }
override fun setOnClicks() { override fun setOnClicks() {
super.setOnClicks() super.setOnClicks()
binding.tutorialFinishButtonStart.setOnClickListener { finishTutorial() } binding.tutorialFinishButtonStart.setOnClickListener { finishTutorial() }
@ -44,6 +47,7 @@ class TutorialFragment5Finish : Fragment(), UIObject {
LauncherPreferences.internal().startedTime(System.currentTimeMillis() / 1000L) LauncherPreferences.internal().startedTime(System.currentTimeMillis() / 1000L)
} }
context?.let { setDefaultHomeScreen(it, checkDefault = true) } context?.let { setDefaultHomeScreen(it, checkDefault = true) }
activity?.finish() activity?.finish()
} }
} }

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?android:textColor"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z" />
</vector>

View file

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".ui.ReportCrashActivity">
<com.google.android.material.appbar.AppBarLayout
android:background="@null"
app:elevation="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/report_crash_appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<de.jrpie.android.launcher.ui.util.HtmlTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/crash_info" />
<Button
android:id="@+id/report_crash_button_copy"
android:text="@string/report_crash_button_copy"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Space
android:layout_width="match_parent"
android:layout_height="20dp" />
<Button
android:id="@+id/report_crash_button_mail"
android:text="@string/report_crash_button_mail"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/report_crash_button_report"
android:text="@string/report_crash_button_report"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -162,6 +162,7 @@
--> -->
<string name="settings_meta_link_github" translatable="false">https://github.com/jrpie/Launcher</string> <string name="settings_meta_link_github" translatable="false">https://github.com/jrpie/Launcher</string>
<string name="settings_meta_report_bug_link" translatable="false">https://github.com/jrpie/Launcher/issues/new?template=bug_report.yaml</string> <string name="settings_meta_report_bug_link" translatable="false">https://github.com/jrpie/Launcher/issues/new?template=bug_report.yaml</string>
<string name="settings_meta_report_bug_mail" translatable="false">android-launcher-crash@jrpie.de</string>
<string name="settings_meta_report_vulnerability_link" translatable="false">https://github.com/jrpie/Launcher/security/policy</string> <string name="settings_meta_report_vulnerability_link" translatable="false">https://github.com/jrpie/Launcher/security/policy</string>
<string name="settings_meta_fork_contact_url" translatable="false">https://s.jrpie.de/contact</string> <string name="settings_meta_fork_contact_url" translatable="false">https://s.jrpie.de/contact</string>
<string name="settings_meta_privacy_url" translatable="false">https://s.jrpie.de/android-legal</string> <string name="settings_meta_privacy_url" translatable="false">https://s.jrpie.de/android-legal</string>

View file

@ -419,5 +419,28 @@
<string name="list_other_open_widget_panel">Open Widget Panel</string> <string name="list_other_open_widget_panel">Open Widget Panel</string>
<string name="alert_widget_panel_not_found">This widget panel no longer exists.</string> <string name="alert_widget_panel_not_found">This widget panel no longer exists.</string>
<string name="settings_launcher_section_widgets">Widgets</string> <string name="settings_launcher_section_widgets">Widgets</string>
<string name="notification_crash_title">μLauncher crashed</string>
<string name="notification_crash_explanation">Sorry! Click for more information.</string>
<string name="crash_info"><![CDATA[
Looks like something went wrong, sorry about that!<br><br>
For privacy reasons, crash logs are not collected automatically.<br>
However logs are very useful for debugging, so I would be very grateful if you could send me the attached log by mail
or create a bug report on github.<br><br>
Note that crash logs might contain <strong>sensitive information</strong>, e.g. the name of an app you tried to launch.
Please <strong>redact</strong> such information before sending the report.
<h2>What can I do now?</h2>
If this bug appears again and again, you can try several things:
<ul>
<li>Force stop μLauncher</li>
<li>Clear μLauncher\'s storage (<strong>Your settings will be lost!</strong>)</li>
<li>Install an older version (<a href=\"https://github.com/jrpie/Launcher/releases\">GitHub</a>, <a href=\"https://f-droid.org/en/packages/de.jrpie.android.launcher\">F-Droid</a>)</li>
</ul>
]]>
</string>
<string name="report_crash_button_copy">Copy crash report to clipboard</string>
<string name="report_crash_button_mail">Send report by mail</string>
<string name="report_crash_button_report">Create bug report on GitHub</string>
<string name="report_crash_title">μLauncher crashed</string>
<string name="send_email">Send Email</string>
<string name="notification_channel_crash">Crashes and Debug Information</string>
</resources> </resources>

View file

@ -2,7 +2,7 @@
buildscript { buildscript {
ext.kotlin_version = '2.0.0' ext.kotlin_version = '2.0.0'
ext.android_plugin_version = '8.9.2' ext.android_plugin_version = '8.10.0'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
@ -10,7 +10,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.9.2' classpath 'com.android.tools.build:gradle:8.10.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.android.tools.build:gradle:$android_plugin_version" classpath "com.android.tools.build:gradle:$android_plugin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"