Commit 9d8c2ca1 authored by MickeyMoz's avatar MickeyMoz
Browse files

Merge #4087

4087: For #3439 - Add "report" action to crash notification r=pocmo a=rocketsroger


### Pull Request checklist
<!-- Before submitting the PR, please address each item -->
- [x] **Quality**: This PR builds and passes detekt/ktlint checks (A pre-push hook is recommended)
- [x] **Tests**: This PR includes thorough tests or an explanation of why it does not
- [x] **Changelog**: This PR includes [a changelog entry](https://github.com/mozilla-mobile/android-components/blob/master/docs/changelog.md) or does not need one
- [x] **Accessibility**: The code in this PR follows [accessibility best practices](https://github.com/mozilla-mobile/shared-docs/blob/master/android/accessibility_guide.md) or does not include any user facing features

### After merge
- [ ] **Milestone**: Make sure issues closed by this pull request are added to the [milestone](https://github.com/mozilla-mobile/android-components/milestones) of the version currently in development.
- [ ] **Breaking Changes**: If this is a breaking change, please push a draft PR on [Reference Browser](https://github.com/mozilla-mobile/reference-browser

) to address the breaking issues.

Co-authored-by: default avatarRoger Yang <royang@mozilla.com>
parents db2c9cf5 39089099
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())
    }
}