diff --git a/components/lib/crash/src/main/AndroidManifest.xml b/components/lib/crash/src/main/AndroidManifest.xml
index 251fcfa92f9..6792595fd3a 100644
--- a/components/lib/crash/src/main/AndroidManifest.xml
+++ b/components/lib/crash/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
+
+
+
diff --git a/components/lib/crash/src/main/java/mozilla/components/lib/crash/notification/CrashNotification.kt b/components/lib/crash/src/main/java/mozilla/components/lib/crash/notification/CrashNotification.kt
index 3ddfb84587a..06e86b1fe66 100644
--- a/components/lib/crash/src/main/java/mozilla/components/lib/crash/notification/CrashNotification.kt
+++ b/components/lib/crash/src/main/java/mozilla/components/lib/crash/notification/CrashNotification.kt
@@ -15,12 +15,14 @@ import mozilla.components.lib.crash.Crash
import mozilla.components.lib.crash.CrashReporter
import mozilla.components.lib.crash.R
import mozilla.components.lib.crash.prompt.CrashPrompt
+import mozilla.components.lib.crash.service.SendCrashReportService
import mozilla.components.support.base.ids.notify
-private const val NOTIFICATION_CHANNEL_ID = "Crashes"
-private const val NOTIFICATION_TAG = "mozac.lib.crash.CRASH"
private const val NOTIFICATION_SDK_LEVEL = 29 // On Android Q+ we show a notification instead of a prompt
+internal const val NOTIFICATION_CHANNEL_ID = "Crashes"
+internal const val NOTIFICATION_TAG = "mozac.lib.crash.CRASH"
+
internal class CrashNotification(
private val context: Context,
private val crash: Crash,
@@ -28,10 +30,20 @@ internal class CrashNotification(
) {
fun show() {
val pendingIntent = PendingIntent.getActivity(
- context, 0, CrashPrompt.createIntent(context, crash), 0
+ context, 0, CrashPrompt.createIntent(context, crash), 0
)
- val channel = ensureChannelExists()
+ val reportPendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ PendingIntent.getForegroundService(
+ context, 0, SendCrashReportService.createReportIntent(context, crash), 0
+ )
+ } else {
+ PendingIntent.getService(
+ context, 0, SendCrashReportService.createReportIntent(context, crash), 0
+ )
+ }
+
+ val channel = ensureChannelExists(context)
val notification = NotificationCompat.Builder(context, channel)
.setContentTitle(context.getString(R.string.mozac_lib_crash_dialog_title, configuration.appName))
@@ -39,6 +51,8 @@ internal class CrashNotification(
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(NotificationCompat.CATEGORY_ERROR)
.setContentIntent(pendingIntent)
+ .addAction(R.drawable.mozac_lib_crash_notification, context.getString(
+ R.string.mozac_lib_crash_notification_action_report), reportPendingIntent)
.setAutoCancel(true)
.build()
@@ -46,24 +60,6 @@ internal class CrashNotification(
.notify(context, NOTIFICATION_TAG, notification)
}
- private fun ensureChannelExists(): String {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val notificationManager: NotificationManager = context.getSystemService(
- Context.NOTIFICATION_SERVICE
- ) as NotificationManager
-
- val channel = NotificationChannel(
- NOTIFICATION_CHANNEL_ID,
- context.getString(R.string.mozac_lib_crash_channel),
- NotificationManager.IMPORTANCE_DEFAULT
- )
-
- notificationManager.createNotificationChannel(channel)
- }
-
- return NOTIFICATION_CHANNEL_ID
- }
-
companion object {
/**
* Whether to show a notification instead of a prompt (activity). Android introduced restrictions on background
@@ -82,5 +78,23 @@ internal class CrashNotification(
else -> crash is Crash.NativeCodeCrash && crash.isFatal
}
}
+
+ fun ensureChannelExists(context: Context): String {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val notificationManager: NotificationManager = context.getSystemService(
+ Context.NOTIFICATION_SERVICE
+ ) as NotificationManager
+
+ val channel = NotificationChannel(
+ NOTIFICATION_CHANNEL_ID,
+ context.getString(R.string.mozac_lib_crash_channel),
+ NotificationManager.IMPORTANCE_DEFAULT
+ )
+
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ return NOTIFICATION_CHANNEL_ID
+ }
}
}
diff --git a/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashReportService.kt b/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashReportService.kt
new file mode 100644
index 00000000000..3fbf5bacf14
--- /dev/null
+++ b/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashReportService.kt
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.lib.crash.service
+
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.IBinder
+import androidx.annotation.VisibleForTesting
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.lib.crash.R
+import mozilla.components.lib.crash.notification.CrashNotification
+import mozilla.components.lib.crash.notification.NOTIFICATION_TAG
+import mozilla.components.support.base.ids.NotificationIds
+import mozilla.components.support.base.ids.cancel
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+class SendCrashReportService : Service() {
+ private val crashReporter: CrashReporter by lazy { CrashReporter.requireInstance }
+ private val logger by lazy { CrashReporter
+ .requireInstance
+ .logger
+ }
+
+ private var reporterCoroutineContext: CoroutineContext = EmptyCoroutineContext
+
+ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = CrashNotification.ensureChannelExists(this)
+ val notification = NotificationCompat.Builder(this, channel)
+ .setContentTitle(getString(R.string.mozac_lib_send_crash_report_in_progress,
+ crashReporter.promptConfiguration.organizationName))
+ .setSmallIcon(R.drawable.mozac_lib_crash_notification)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setCategory(NotificationCompat.CATEGORY_ERROR)
+ .setAutoCancel(true)
+ .setProgress(0, 0, true)
+ .build()
+
+ val notificationId = NotificationIds.getIdForTag(this, NOTIFICATION_TAG)
+ startForeground(notificationId, notification)
+ }
+
+ intent.extras?.let { extras ->
+ val crash = Crash.NativeCodeCrash.fromBundle(extras)
+ NotificationManagerCompat.from(this).cancel(this, NOTIFICATION_TAG)
+
+ sendCrashReport(crash) {
+ stopSelf()
+ }
+ } ?: logger.error("Received intent with null extras")
+
+ return START_NOT_STICKY
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun sendCrashReport(crash: Crash, then: () -> Unit) {
+ GlobalScope.launch(reporterCoroutineContext) {
+ crashReporter.submitReport(crash)
+
+ withContext(Dispatchers.Main) {
+ then()
+ }
+ }
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ // We don't provide binding, so return null
+ return null
+ }
+
+ companion object {
+ fun createReportIntent(context: Context, crash: Crash): Intent {
+ val intent = Intent(context, SendCrashReportService::class.java)
+ crash.fillIn(intent)
+
+ return intent
+ }
+ }
+}
diff --git a/components/lib/crash/src/main/res/values/strings.xml b/components/lib/crash/src/main/res/values/strings.xml
index f1823ffb561..b98fa27148f 100644
--- a/components/lib/crash/src/main/res/values/strings.xml
+++ b/components/lib/crash/src/main/res/values/strings.xml
@@ -21,4 +21,6 @@
Report
+
+ Sending crash report to %1$s
diff --git a/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashReportServiceTest.kt b/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashReportServiceTest.kt
new file mode 100644
index 00000000000..55f6dac8fb0
--- /dev/null
+++ b/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashReportServiceTest.kt
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.lib.crash.service
+
+import android.content.ComponentName
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.Robolectric
+
+@RunWith(AndroidJUnit4::class)
+class SendCrashReportServiceTest {
+ private var service: SendCrashReportService? = null
+
+ @Before
+ fun setUp() {
+ service = spy(Robolectric.setupService(SendCrashReportService::class.java))
+ service?.startService(Intent())
+ }
+
+ @After
+ fun tearDown() {
+ service?.stopService(Intent())
+ CrashReporter.reset()
+ }
+
+ @Test
+ fun `CrashRHandlerService will forward same crash to crash reporter`() {
+ spy(CrashReporter(
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(object : CrashReporterService {
+ override fun report(crash: Crash.UncaughtExceptionCrash) {
+ }
+
+ override fun report(crash: Crash.NativeCodeCrash) {
+ }
+ }))
+ ).install(testContext)
+ val originalCrash = Crash.NativeCodeCrash(
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ true,
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ false
+ )
+
+ val intent = Intent("org.mozilla.gecko.ACTION_CRASHED")
+ intent.component = ComponentName(
+ "org.mozilla.samples.browser",
+ "mozilla.components.lib.crash.handler.CrashHandlerService"
+ )
+ intent.putExtra(
+ "minidumpPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp"
+ )
+ intent.putExtra("fatal", false)
+ intent.putExtra(
+ "extrasPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra"
+ )
+ intent.putExtra("minidumpSuccess", true)
+ originalCrash.fillIn(intent)
+
+ service?.onStartCommand(intent, 0, 0)
+ verify(service)?.sendCrashReport(eq(originalCrash), any())
+ }
+}