Commit 39089099 authored by Roger Yang's avatar Roger Yang
Browse files

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

parent b1516dc7
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -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"
@@ -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>
+36 −22
Original line number Diff line number Diff line
@@ -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,
@@ -31,7 +33,17 @@ internal class CrashNotification(
                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
        }
    }
}
+91 −0
Original line number Diff line number Diff line
/* 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
        }
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -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>
+78 −0
Original line number Diff line number Diff line
/* 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())
    }
}