Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

For #3439 - Add "report" action to crash notification #4087

Merged
merged 1 commit into from
Aug 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions components/lib/crash/src/main/AndroidManifest.xml
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to change that in the commit. But this indentation seems to come from the default AS formatting that is a bit different than what ktlint does.

You can get Android Studio to follow ktlint (on Mac) like this:

brew install ktlint
ktlint --apply-to-idea-project --android

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After applying and restarting android studio. It still wants to do double intent for this line. I'll look into why. Thanks for the ktlint apply commands.

)

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
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: We could consider moving the creation of such a pending intent behind a helper. Maybe even in support-utils.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had some issues moving this to support-util. Couldn't find a clean way because of the Crash class dependency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opened #4113 to track this.

}
rocketsroger marked this conversation as resolved.
Show resolved Hide resolved

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))
rocketsroger marked this conversation as resolved.
Show resolved Hide resolved
.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
}
}
}
2 changes: 2 additions & 0 deletions components/lib/crash/src/main/res/values/strings.xml
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>
rocketsroger marked this conversation as resolved.
Show resolved Hide resolved
</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())
}
}