diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1b10784..087ec28 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,6 +8,7 @@
tools:ignore="QueryAllPackagesPermission" />
+
+
@@ -80,6 +82,9 @@
+
-
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/Application.kt b/app/src/main/java/de/jrpie/android/launcher/Application.kt
index 3c2e3bc..cf9e697 100644
--- a/app/src/main/java/de/jrpie/android/launcher/Application.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/Application.kt
@@ -25,6 +25,7 @@ import de.jrpie.android.launcher.preferences.resetPreferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlin.system.exitProcess
const val APP_WIDGET_HOST_ID = 42;
@@ -106,6 +107,11 @@ class Application : android.app.Application() {
// TODO Error: Invalid resource ID 0x00000000.
// DynamicColors.applyToActivitiesIfAvailable(this)
+ Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
+ sendCrashNotification(this@Application, throwable)
+ exitProcess(1)
+ }
+
if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
torchManager = TorchManager(this)
@@ -157,6 +163,8 @@ class Application : android.app.Application() {
removeUnusedShortcuts(this)
}
loadApps()
+
+ createNotificationChannels(this)
}
fun getCustomAppNames(): HashMap {
diff --git a/app/src/main/java/de/jrpie/android/launcher/Functions.kt b/app/src/main/java/de/jrpie/android/launcher/Functions.kt
index 9679ae5..b6df30b 100644
--- a/app/src/main/java/de/jrpie/android/launcher/Functions.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/Functions.kt
@@ -6,9 +6,6 @@ import android.app.role.RoleManager
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
-import android.appwidget.AppWidgetManager
-import android.appwidget.AppWidgetProvider
-import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
@@ -227,3 +224,13 @@ fun copyToClipboard(context: Context, text: String) {
val clipData = ClipData.newPlainText("Debug Info", text)
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)))
+}
+
diff --git a/app/src/main/java/de/jrpie/android/launcher/Notifications.kt b/app/src/main/java/de/jrpie/android/launcher/Notifications.kt
new file mode 100644
index 0000000..0cf0efb
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/Notifications.kt
@@ -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")
+ }
+}
diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt
index fd86d79..8936675 100644
--- a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt
@@ -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.migratePreferencesFromVersion4
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.widgets.ClockWidget
import de.jrpie.android.launcher.widgets.DebugInfoWidget
@@ -76,6 +77,7 @@ fun migratePreferencesToNewVersion(context: Context) {
}
} catch (e: Exception) {
Log.e(TAG, "Unable to restore preferences:\n${e.stackTrace}")
+ sendCrashNotification(context, e)
resetPreferences(context)
}
}
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/ReportCrashActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/ReportCrashActivity.kt
new file mode 100644
index 0000000..5d95769
--- /dev/null
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/ReportCrashActivity.kt
@@ -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
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/tutorial/tabs/TutorialFragment5Finish.kt b/app/src/main/java/de/jrpie/android/launcher/ui/tutorial/tabs/TutorialFragment5Finish.kt
index 8feaa07..e15cef1 100644
--- a/app/src/main/java/de/jrpie/android/launcher/ui/tutorial/tabs/TutorialFragment5Finish.kt
+++ b/app/src/main/java/de/jrpie/android/launcher/ui/tutorial/tabs/TutorialFragment5Finish.kt
@@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment
import de.jrpie.android.launcher.BuildConfig.VERSION_CODE
import de.jrpie.android.launcher.databinding.Tutorial5FinishBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences
+import de.jrpie.android.launcher.requestNotificationPermission
import de.jrpie.android.launcher.setDefaultHomeScreen
import de.jrpie.android.launcher.ui.UIObject
@@ -31,8 +32,10 @@ class TutorialFragment5Finish : Fragment(), UIObject {
override fun onStart() {
super.onStart()
super.onStart()
+ requestNotificationPermission(requireActivity())
}
+
override fun setOnClicks() {
super.setOnClicks()
binding.tutorialFinishButtonStart.setOnClickListener { finishTutorial() }
@@ -44,6 +47,7 @@ class TutorialFragment5Finish : Fragment(), UIObject {
LauncherPreferences.internal().startedTime(System.currentTimeMillis() / 1000L)
}
context?.let { setDefaultHomeScreen(it, checkDefault = true) }
+
activity?.finish()
}
}
diff --git a/app/src/main/res/drawable/baseline_bug_report_24.xml b/app/src/main/res/drawable/baseline_bug_report_24.xml
new file mode 100644
index 0000000..ef399ba
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_bug_report_24.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_report_crash.xml b/app/src/main/res/layout/activity_report_crash.xml
new file mode 100644
index 0000000..c7f2110
--- /dev/null
+++ b/app/src/main/res/layout/activity_report_crash.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml
index f783d2a..cc38062 100644
--- a/app/src/main/res/values/donottranslate.xml
+++ b/app/src/main/res/values/donottranslate.xml
@@ -162,6 +162,7 @@
-->
https://github.com/jrpie/Launcherhttps://github.com/jrpie/Launcher/issues/new?template=bug_report.yaml
+ android-launcher-crash@jrpie.dehttps://github.com/jrpie/Launcher/security/policyhttps://s.jrpie.de/contacthttps://s.jrpie.de/android-legal
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d399d12..998fced 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -419,5 +419,28 @@
Open Widget PanelThis widget panel no longer exists.Widgets
-
+ μLauncher crashed
+ Sorry! Click for more information.
+
+ For privacy reasons, crash logs are not collected automatically.
+ 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.
+ Note that crash logs might contain sensitive information, e.g. the name of an app you tried to launch.
+ Please redact such information before sending the report.
+
What can I do now?
+ If this bug appears again and again, you can try several things:
+
+
Force stop μLauncher
+
Clear μLauncher\'s storage (Your settings will be lost!)