Skip to content
This repository has been archived by the owner on Jun 17, 2024. It is now read-only.

Commit

Permalink
[components] For mozilla-mobile/android-components#3439 - Add "report…
Browse files Browse the repository at this point in the history
…" action to crash notification
  • Loading branch information
rocketsroger committed Aug 14, 2019
1 parent 08af642 commit 58310fd
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

<application>
<activity android:name=".prompt.CrashReporterActivity"
Expand All @@ -17,6 +18,9 @@
<service android:name=".handler.CrashHandlerService"
android:process=":mozilla.components.lib.crash.CrashHandler"
android:exported="false" />

<service android:name=".service.SendCrashReportService"
android:exported="false" />
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -15,55 +15,51 @@ 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,
private val configuration: CrashReporter.PromptConfiguration
) {
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))
.setSmallIcon(R.drawable.mozac_lib_crash_notification)
.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()

NotificationManagerCompat.from(context)
.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
Expand All @@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@
<!-- Label of a notification action/button that will send the crash report to Mozilla. -->
<string name="mozac_lib_crash_notification_action_report">Report</string>

<!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
<string name="mozac_lib_send_crash_report_in_progress">Sending crash report to %1$s</string>
</resources>
Original file line number Diff line number Diff line change
@@ -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())
}
}

0 comments on commit 58310fd

Please sign in to comment.