From def92d1b8e9d373e2f6f27c366d578d97d8960c6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 15 May 2024 05:34:50 +0200 Subject: Merging upstream version 126.0. Signed-off-by: Daniel Baumann --- .../components/lib/auth/build.gradle | 38 + .../components/lib/auth/proguard-rules.pro | 21 + .../lib/auth/src/main/AndroidManifest.xml | 4 + .../components/lib/auth/AuthenticationDelegate.kt | 29 + .../components/lib/auth/BiometricPromptAuth.kt | 78 ++ .../mozilla/components/lib/auth/BiometricUtils.kt | 51 ++ .../components/lib/auth/BiometricPromptAuthTest.kt | 91 ++ .../components/lib/auth/BiometricUtilsTest.kt | 50 ++ .../components/lib/crash-sentry/build.gradle | 45 + .../components/lib/crash-sentry/proguard-rules.pro | 21 + .../lib/crash-sentry/src/main/AndroidManifest.xml | 15 + .../components/lib/crash/sentry/SentryService.kt | 201 +++++ .../eventprocessors/AddMechanismEventProcessor.kt | 41 + .../eventprocessors/RustCrashEventProcessor.kt | 36 + .../lib/crash/sentry/SentryServiceTest.kt | 276 ++++++ .../AddMechanismEventProcessorTest.kt | 46 + .../eventprocessors/RustCrashEventProcessorTest.kt | 35 + .../org.mockito.plugins.MockMaker | 2 + .../src/test/resources/robolectric.properties | 1 + .../components/lib/crash/README.md | 239 ++++++ .../components/lib/crash/build.gradle | 99 +++ .../components/lib/crash/images/crash-dialog.png | Bin 0 -> 20052 bytes .../components/lib/crash/images/crash-in-app.png | Bin 0 -> 6574 bytes .../components/lib/crash/metrics.yaml | 154 ++++ .../components/lib/crash/pings.yaml | 28 + .../components/lib/crash/proguard-rules.pro | 21 + .../1.json | 84 ++ .../lib/crash/src/main/AndroidManifest.xml | 49 ++ .../java/mozilla/components/lib/crash/Crash.kt | 175 ++++ .../mozilla/components/lib/crash/CrashReporter.kt | 376 +++++++++ .../mozilla/components/lib/crash/db/CrashDao.kt | 78 ++ .../components/lib/crash/db/CrashDatabase.kt | 40 + .../mozilla/components/lib/crash/db/CrashEntity.kt | 54 ++ .../components/lib/crash/db/CrashWithReports.kt | 22 + .../components/lib/crash/db/ReportEntity.kt | 52 ++ .../lib/crash/handler/CrashHandlerService.kt | 76 ++ .../lib/crash/handler/ExceptionHandler.kt | 60 ++ .../lib/crash/notification/CrashNotification.kt | 121 +++ .../components/lib/crash/prompt/CrashPrompt.kt | 48 ++ .../lib/crash/prompt/CrashReporterActivity.kt | 158 ++++ .../lib/crash/service/CrashReporterService.kt | 54 ++ .../lib/crash/service/CrashTelemetryService.kt | 27 + .../lib/crash/service/GleanCrashReporterService.kt | 312 +++++++ .../lib/crash/service/MozillaSocorroService.kt | 566 +++++++++++++ .../lib/crash/service/SendCrashReportService.kt | 93 ++ .../lib/crash/service/SendCrashTelemetryService.kt | 66 ++ .../lib/crash/ui/AbstractCrashListActivity.kt | 36 + .../components/lib/crash/ui/CrashListAdapter.kt | 163 ++++ .../components/lib/crash/ui/CrashListFragment.kt | 65 ++ .../res/drawable/mozac_lib_crash_notification.xml | 14 + .../main/res/layout/mozac_lib_crash_crashlist.xml | 23 + .../res/layout/mozac_lib_crash_crashreporter.xml | 81 ++ .../main/res/layout/mozac_lib_crash_item_crash.xml | 40 + .../lib/crash/src/main/res/values-am/strings.xml | 41 + .../lib/crash/src/main/res/values-an/strings.xml | 32 + .../lib/crash/src/main/res/values-ar/strings.xml | 32 + .../lib/crash/src/main/res/values-ast/strings.xml | 41 + .../lib/crash/src/main/res/values-az/strings.xml | 32 + .../lib/crash/src/main/res/values-azb/strings.xml | 42 + .../lib/crash/src/main/res/values-ban/strings.xml | 11 + .../lib/crash/src/main/res/values-be/strings.xml | 42 + .../lib/crash/src/main/res/values-bg/strings.xml | 41 + .../lib/crash/src/main/res/values-bn/strings.xml | 32 + .../lib/crash/src/main/res/values-br/strings.xml | 41 + .../lib/crash/src/main/res/values-bs/strings.xml | 41 + .../lib/crash/src/main/res/values-ca/strings.xml | 41 + .../lib/crash/src/main/res/values-cak/strings.xml | 41 + .../lib/crash/src/main/res/values-ceb/strings.xml | 33 + .../lib/crash/src/main/res/values-ckb/strings.xml | 32 + .../lib/crash/src/main/res/values-co/strings.xml | 41 + .../lib/crash/src/main/res/values-cs/strings.xml | 41 + .../lib/crash/src/main/res/values-cy/strings.xml | 41 + .../lib/crash/src/main/res/values-da/strings.xml | 42 + .../lib/crash/src/main/res/values-de/strings.xml | 41 + .../lib/crash/src/main/res/values-dsb/strings.xml | 41 + .../lib/crash/src/main/res/values-el/strings.xml | 42 + .../crash/src/main/res/values-en-rCA/strings.xml | 41 + .../crash/src/main/res/values-en-rGB/strings.xml | 41 + .../lib/crash/src/main/res/values-eo/strings.xml | 42 + .../crash/src/main/res/values-es-rAR/strings.xml | 41 + .../crash/src/main/res/values-es-rCL/strings.xml | 41 + .../crash/src/main/res/values-es-rES/strings.xml | 41 + .../crash/src/main/res/values-es-rMX/strings.xml | 41 + .../lib/crash/src/main/res/values-es/strings.xml | 41 + .../lib/crash/src/main/res/values-et/strings.xml | 35 + .../lib/crash/src/main/res/values-eu/strings.xml | 41 + .../lib/crash/src/main/res/values-fa/strings.xml | 41 + .../lib/crash/src/main/res/values-ff/strings.xml | 30 + .../lib/crash/src/main/res/values-fi/strings.xml | 42 + .../lib/crash/src/main/res/values-fr/strings.xml | 42 + .../lib/crash/src/main/res/values-fur/strings.xml | 41 + .../crash/src/main/res/values-fy-rNL/strings.xml | 41 + .../crash/src/main/res/values-ga-rIE/strings.xml | 25 + .../lib/crash/src/main/res/values-gd/strings.xml | 41 + .../lib/crash/src/main/res/values-gl/strings.xml | 41 + .../lib/crash/src/main/res/values-gn/strings.xml | 41 + .../crash/src/main/res/values-gu-rIN/strings.xml | 32 + .../crash/src/main/res/values-hi-rIN/strings.xml | 33 + .../lib/crash/src/main/res/values-hil/strings.xml | 13 + .../lib/crash/src/main/res/values-hr/strings.xml | 42 + .../lib/crash/src/main/res/values-hsb/strings.xml | 41 + .../lib/crash/src/main/res/values-hu/strings.xml | 41 + .../crash/src/main/res/values-hy-rAM/strings.xml | 41 + .../lib/crash/src/main/res/values-ia/strings.xml | 41 + .../lib/crash/src/main/res/values-in/strings.xml | 42 + .../lib/crash/src/main/res/values-is/strings.xml | 41 + .../lib/crash/src/main/res/values-it/strings.xml | 41 + .../lib/crash/src/main/res/values-iw/strings.xml | 42 + .../lib/crash/src/main/res/values-ja/strings.xml | 41 + .../lib/crash/src/main/res/values-ka/strings.xml | 41 + .../lib/crash/src/main/res/values-kaa/strings.xml | 42 + .../lib/crash/src/main/res/values-kab/strings.xml | 41 + .../lib/crash/src/main/res/values-kk/strings.xml | 41 + .../lib/crash/src/main/res/values-kmr/strings.xml | 42 + .../lib/crash/src/main/res/values-kn/strings.xml | 33 + .../lib/crash/src/main/res/values-ko/strings.xml | 41 + .../lib/crash/src/main/res/values-lij/strings.xml | 33 + .../lib/crash/src/main/res/values-lo/strings.xml | 41 + .../lib/crash/src/main/res/values-lt/strings.xml | 41 + .../lib/crash/src/main/res/values-ml/strings.xml | 33 + .../lib/crash/src/main/res/values-mr/strings.xml | 32 + .../lib/crash/src/main/res/values-my/strings.xml | 33 + .../crash/src/main/res/values-nb-rNO/strings.xml | 41 + .../crash/src/main/res/values-ne-rNP/strings.xml | 36 + .../lib/crash/src/main/res/values-nl/strings.xml | 41 + .../crash/src/main/res/values-nn-rNO/strings.xml | 41 + .../lib/crash/src/main/res/values-oc/strings.xml | 42 + .../crash/src/main/res/values-pa-rIN/strings.xml | 42 + .../crash/src/main/res/values-pa-rPK/strings.xml | 12 + .../lib/crash/src/main/res/values-pl/strings.xml | 41 + .../crash/src/main/res/values-pt-rBR/strings.xml | 41 + .../crash/src/main/res/values-pt-rPT/strings.xml | 41 + .../lib/crash/src/main/res/values-rm/strings.xml | 41 + .../lib/crash/src/main/res/values-ro/strings.xml | 32 + .../lib/crash/src/main/res/values-ru/strings.xml | 41 + .../lib/crash/src/main/res/values-sat/strings.xml | 42 + .../lib/crash/src/main/res/values-sc/strings.xml | 42 + .../lib/crash/src/main/res/values-si/strings.xml | 39 + .../lib/crash/src/main/res/values-sk/strings.xml | 41 + .../lib/crash/src/main/res/values-skr/strings.xml | 42 + .../lib/crash/src/main/res/values-sl/strings.xml | 41 + .../lib/crash/src/main/res/values-sq/strings.xml | 41 + .../lib/crash/src/main/res/values-sr/strings.xml | 41 + .../lib/crash/src/main/res/values-su/strings.xml | 41 + .../crash/src/main/res/values-sv-rSE/strings.xml | 42 + .../lib/crash/src/main/res/values-ta/strings.xml | 33 + .../lib/crash/src/main/res/values-te/strings.xml | 33 + .../lib/crash/src/main/res/values-tg/strings.xml | 41 + .../lib/crash/src/main/res/values-th/strings.xml | 41 + .../lib/crash/src/main/res/values-tl/strings.xml | 39 + .../lib/crash/src/main/res/values-tr/strings.xml | 42 + .../lib/crash/src/main/res/values-trs/strings.xml | 41 + .../lib/crash/src/main/res/values-tt/strings.xml | 42 + .../lib/crash/src/main/res/values-tzm/strings.xml | 9 + .../lib/crash/src/main/res/values-ug/strings.xml | 42 + .../lib/crash/src/main/res/values-uk/strings.xml | 41 + .../lib/crash/src/main/res/values-ur/strings.xml | 33 + .../lib/crash/src/main/res/values-uz/strings.xml | 41 + .../lib/crash/src/main/res/values-vec/strings.xml | 24 + .../lib/crash/src/main/res/values-vi/strings.xml | 41 + .../lib/crash/src/main/res/values-yo/strings.xml | 41 + .../crash/src/main/res/values-zh-rCN/strings.xml | 41 + .../crash/src/main/res/values-zh-rTW/strings.xml | 41 + .../lib/crash/src/main/res/values/strings.xml | 44 + .../lib/crash/src/main/res/values/styles.xml | 13 + .../mozilla/components/lib/crash/BreadcrumbTest.kt | 192 +++++ .../components/lib/crash/CrashReporterTest.kt | 931 +++++++++++++++++++++ .../java/mozilla/components/lib/crash/CrashTest.kt | 105 +++ .../components/lib/crash/NativeCodeCrashTest.kt | 74 ++ .../lib/crash/UncaughtExceptionCrashTest.kt | 34 + .../lib/crash/handler/CrashHandlerServiceTest.kt | 122 +++ .../lib/crash/handler/ExceptionHandlerTest.kt | 98 +++ .../crash/notification/CrashNotificationTest.kt | 175 ++++ .../lib/crash/prompt/CrashReporterActivityTest.kt | 263 ++++++ .../crash/service/GleanCrashReporterServiceTest.kt | 464 ++++++++++ .../lib/crash/service/MozillaSocorroServiceTest.kt | 693 +++++++++++++++ .../crash/service/SendCrashReportServiceTest.kt | 169 ++++ .../crash/service/SendCrashTelemetryServiceTest.kt | 142 ++++ .../lib/crash/src/test/resources/BadTestExtrasFile | 1 + .../lib/crash/src/test/resources/TestExtrasFile | 1 + .../crash/src/test/resources/TestLegacyExtrasFile | 31 + .../org.mockito.plugins.MockMaker | 2 + .../src/test/resources/robolectric.properties | 1 + .../components/lib/dataprotect/README.md | 19 + .../components/lib/dataprotect/build.gradle | 39 + .../components/lib/dataprotect/proguard-rules.pro | 21 + .../lib/dataprotect/src/main/AndroidManifest.xml | 4 + .../mozilla/components/lib/dataprotect/Keystore.kt | 314 +++++++ .../lib/dataprotect/KeystoreException.kt | 17 + .../lib/dataprotect/SecureAbove22Preferences.kt | 231 +++++ .../SecurePrefsReliabilityExperiment.kt | 151 ++++ .../components/lib/dataprotect/KeystoreTest.kt | 137 +++ .../dataprotect/SecureAbove22PreferencesTest.kt | 190 +++++ .../SecurePrefsReliabilityExperimentTest.kt | 215 +++++ .../src/test/resources/robolectric.properties | 1 + .../lib/fetch-httpurlconnection/README.md | 25 + .../lib/fetch-httpurlconnection/build.gradle | 40 + .../lib/fetch-httpurlconnection/proguard-rules.pro | 21 + .../src/main/AndroidManifest.xml | 4 + .../httpurlconnection/HttpURLConnectionClient.kt | 195 +++++ .../HttpUrlConnectionFetchTestCases.kt | 40 + .../org.mockito.plugins.MockMaker | 2 + .../src/test/resources/robolectric.properties | 1 + .../components/lib/fetch-okhttp/README.md | 25 + .../components/lib/fetch-okhttp/build.gradle | 43 + .../components/lib/fetch-okhttp/proguard-rules.pro | 21 + .../lib/fetch-okhttp/src/main/AndroidManifest.xml | 4 + .../components/lib/fetch/okhttp/OkHttpClient.kt | 149 ++++ .../lib/fetch/okhttp/OkHttpFetchTestCases.kt | 27 + .../src/test/resources/robolectric.properties | 1 + .../components/lib/jexl/README.md | 236 ++++++ .../components/lib/jexl/build.gradle | 34 + .../components/lib/jexl/proguard-rules.pro | 21 + .../lib/jexl/src/main/AndroidManifest.xml | 4 + .../main/java/mozilla/components/lib/jexl/Jexl.kt | 102 +++ .../java/mozilla/components/lib/jexl/ast/nodes.kt | 230 +++++ .../components/lib/jexl/evaluator/Evaluator.kt | 86 ++ .../lib/jexl/evaluator/EvaluatorHandlers.kt | 158 ++++ .../components/lib/jexl/evaluator/JexlContext.kt | 51 ++ .../components/lib/jexl/ext/JexlExtensions.kt | 32 + .../mozilla/components/lib/jexl/grammar/Grammar.kt | 141 ++++ .../mozilla/components/lib/jexl/lexer/Lexer.kt | 223 +++++ .../components/lib/jexl/lexer/LexerInput.kt | 85 ++ .../mozilla/components/lib/jexl/lexer/Token.kt | 32 + .../mozilla/components/lib/jexl/parser/Parser.kt | 252 ++++++ .../components/lib/jexl/parser/StateMachine.kt | 230 +++++ .../mozilla/components/lib/jexl/value/JexlValue.kt | 307 +++++++ .../java/mozilla/components/lib/jexl/JexlTest.kt | 112 +++ .../mozilla/components/lib/jexl/LanguageTest.kt | 53 ++ .../components/lib/jexl/evaluator/EvaluatorTest.kt | 375 +++++++++ .../components/lib/jexl/ext/JexlExtensionsTest.kt | 65 ++ .../mozilla/components/lib/jexl/lexer/LexerTest.kt | 483 +++++++++++ .../components/lib/jexl/parser/ParserTest.kt | 506 +++++++++++ .../components/lib/jexl/value/JexlValueTest.kt | 170 ++++ .../components/lib/publicsuffixlist/README.md | 64 ++ .../components/lib/publicsuffixlist/build.gradle | 49 ++ .../lib/publicsuffixlist/proguard-rules.pro | 21 + .../publicsuffixlist/src/main/AndroidManifest.xml | 4 + .../src/main/assets/publicsuffixes | Bin 0 -> 107497 bytes .../lib/publicsuffixlist/PublicSuffixList.kt | 138 +++ .../lib/publicsuffixlist/PublicSuffixListData.kt | 158 ++++ .../lib/publicsuffixlist/PublicSuffixListLoader.kt | 50 ++ .../lib/publicsuffixlist/ext/ByteArray.kt | 122 +++ .../lib/publicsuffixlist/PublicSuffixListTest.kt | 482 +++++++++++ .../src/test/resources/robolectric.properties | 1 + .../components/lib/push-firebase/README.md | 59 ++ .../components/lib/push-firebase/build.gradle | 42 + .../lib/push-firebase/proguard-rules.pro | 21 + .../lib/push-firebase/src/main/AndroidManifest.xml | 4 + .../push/firebase/AbstractFirebasePushService.kt | 103 +++ .../firebase/AbstractFirebasePushServiceTest.kt | 115 +++ .../org.mockito.plugins.MockMaker | 2 + .../src/test/resources/robolectric.properties | 1 + .../components/lib/state/README.md | 69 ++ .../components/lib/state/build.gradle | 69 ++ .../components/lib/state/proguard-rules.pro | 21 + .../lib/state/ext/ComposeExtensionsKtTest.kt | 182 ++++ .../lib/state/src/main/AndroidManifest.xml | 8 + .../java/mozilla/components/lib/state/Action.kt | 14 + .../mozilla/components/lib/state/DelicateAction.kt | 23 + .../mozilla/components/lib/state/Middleware.kt | 53 ++ .../java/mozilla/components/lib/state/Observer.kt | 10 + .../java/mozilla/components/lib/state/Reducer.kt | 13 + .../java/mozilla/components/lib/state/State.kt | 10 + .../java/mozilla/components/lib/state/Store.kt | 187 +++++ .../components/lib/state/ext/ComposeExtensions.kt | 147 ++++ .../mozilla/components/lib/state/ext/Fragment.kt | 105 +++ .../components/lib/state/ext/StoreExtensions.kt | 265 ++++++ .../java/mozilla/components/lib/state/ext/View.kt | 39 + .../lib/state/helpers/AbstractBinding.kt | 44 + .../lib/state/internal/ReducerChainBuilder.kt | 67 ++ .../lib/state/internal/StoreThreadFactory.kt | 58 ++ .../components/lib/state/StoreExceptionTest.kt | 33 + .../java/mozilla/components/lib/state/StoreTest.kt | 311 +++++++ .../components/lib/state/ext/FragmentKtTest.kt | 301 +++++++ .../lib/state/ext/StoreExtensionsKtTest.kt | 572 +++++++++++++ .../mozilla/components/lib/state/ext/ViewKtTest.kt | 89 ++ .../lib/state/helpers/AbstractBindingTest.kt | 98 +++ .../org.mockito.plugins.MockMaker | 2 + .../src/test/resources/robolectric.properties | 1 + 280 files changed, 22471 insertions(+) create mode 100644 mobile/android/android-components/components/lib/auth/build.gradle create mode 100644 mobile/android/android-components/components/lib/auth/proguard-rules.pro create mode 100644 mobile/android/android-components/components/lib/auth/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/AuthenticationDelegate.kt create mode 100644 mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/BiometricPromptAuth.kt create mode 100644 mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/BiometricUtils.kt create mode 100644 mobile/android/android-components/components/lib/auth/src/test/java/mozilla/components/lib/auth/BiometricPromptAuthTest.kt create mode 100644 mobile/android/android-components/components/lib/auth/src/test/java/mozilla/components/lib/auth/BiometricUtilsTest.kt create mode 100644 mobile/android/android-components/components/lib/crash-sentry/build.gradle create mode 100644 mobile/android/android-components/components/lib/crash-sentry/proguard-rules.pro create mode 100644 mobile/android/android-components/components/lib/crash-sentry/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/SentryService.kt create mode 100644 mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/eventprocessors/AddMechanismEventProcessor.kt create mode 100644 mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/eventprocessors/RustCrashEventProcessor.kt create mode 100644 mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/SentryServiceTest.kt create mode 100644 mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/eventprocessors/AddMechanismEventProcessorTest.kt create mode 100644 mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/eventprocessors/RustCrashEventProcessorTest.kt create mode 100644 mobile/android/android-components/components/lib/crash-sentry/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 mobile/android/android-components/components/lib/crash-sentry/src/test/resources/robolectric.properties create mode 100644 mobile/android/android-components/components/lib/crash/README.md create mode 100644 mobile/android/android-components/components/lib/crash/build.gradle create mode 100644 mobile/android/android-components/components/lib/crash/images/crash-dialog.png create mode 100644 mobile/android/android-components/components/lib/crash/images/crash-in-app.png create mode 100644 mobile/android/android-components/components/lib/crash/metrics.yaml create mode 100644 mobile/android/android-components/components/lib/crash/pings.yaml create mode 100644 mobile/android/android-components/components/lib/crash/proguard-rules.pro create mode 100644 mobile/android/android-components/components/lib/crash/schemas/mozilla.components.lib.crash.db.CrashDatabase/1.json create mode 100644 mobile/android/android-components/components/lib/crash/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/Crash.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/CrashReporter.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashDao.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashDatabase.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashEntity.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashWithReports.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/ReportEntity.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/handler/CrashHandlerService.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/handler/ExceptionHandler.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/notification/CrashNotification.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/prompt/CrashPrompt.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/prompt/CrashReporterActivity.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashReporterService.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashTelemetryService.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/GleanCrashReporterService.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/MozillaSocorroService.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashReportService.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashTelemetryService.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/AbstractCrashListActivity.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/CrashListAdapter.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/CrashListFragment.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/drawable/mozac_lib_crash_notification.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_crashlist.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_crashreporter.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_item_crash.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-am/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-an/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ar/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ast/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-az/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-azb/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ban/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-be/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-bg/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-bn/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-br/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-bs/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ca/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-cak/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ceb/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ckb/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-co/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-cs/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-cy/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-da/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-de/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-dsb/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-el/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-en-rCA/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-en-rGB/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-eo/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-es-rAR/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-es-rCL/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-es-rES/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-es-rMX/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-es/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-et/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-eu/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-fa/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ff/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-fi/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-fr/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-fur/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-fy-rNL/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ga-rIE/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-gd/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-gl/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-gn/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-gu-rIN/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-hi-rIN/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-hil/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-hr/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-hsb/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-hu/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-hy-rAM/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ia/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-in/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-is/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-it/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-iw/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ja/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ka/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-kaa/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-kab/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-kk/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-kmr/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-kn/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ko/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-lij/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-lo/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-lt/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ml/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-mr/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-my/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-nb-rNO/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ne-rNP/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-nl/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-nn-rNO/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-oc/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-pa-rIN/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-pa-rPK/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-pl/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-pt-rBR/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-pt-rPT/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-rm/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ro/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ru/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-sat/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-sc/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-si/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-sk/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-skr/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-sl/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-sq/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-sr/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-su/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-sv-rSE/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ta/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-te/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-tg/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-th/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-tl/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-tr/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-trs/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-tt/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-tzm/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ug/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-uk/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-ur/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-uz/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-vec/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-vi/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-yo/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-zh-rCN/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values-zh-rTW/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values/strings.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/main/res/values/styles.xml create mode 100644 mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/BreadcrumbTest.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashReporterTest.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashTest.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/NativeCodeCrashTest.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/UncaughtExceptionCrashTest.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/CrashHandlerServiceTest.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/ExceptionHandlerTest.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/notification/CrashNotificationTest.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/prompt/CrashReporterActivityTest.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/GleanCrashReporterServiceTest.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/MozillaSocorroServiceTest.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashReportServiceTest.kt create mode 100644 mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashTelemetryServiceTest.kt create mode 100755 mobile/android/android-components/components/lib/crash/src/test/resources/BadTestExtrasFile create mode 100755 mobile/android/android-components/components/lib/crash/src/test/resources/TestExtrasFile create mode 100755 mobile/android/android-components/components/lib/crash/src/test/resources/TestLegacyExtrasFile create mode 100644 mobile/android/android-components/components/lib/crash/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 mobile/android/android-components/components/lib/crash/src/test/resources/robolectric.properties create mode 100644 mobile/android/android-components/components/lib/dataprotect/README.md create mode 100644 mobile/android/android-components/components/lib/dataprotect/build.gradle create mode 100644 mobile/android/android-components/components/lib/dataprotect/proguard-rules.pro create mode 100644 mobile/android/android-components/components/lib/dataprotect/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/Keystore.kt create mode 100644 mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/KeystoreException.kt create mode 100644 mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/SecureAbove22Preferences.kt create mode 100644 mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/SecurePrefsReliabilityExperiment.kt create mode 100644 mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/KeystoreTest.kt create mode 100644 mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/SecureAbove22PreferencesTest.kt create mode 100644 mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/SecurePrefsReliabilityExperimentTest.kt create mode 100644 mobile/android/android-components/components/lib/dataprotect/src/test/resources/robolectric.properties create mode 100644 mobile/android/android-components/components/lib/fetch-httpurlconnection/README.md create mode 100644 mobile/android/android-components/components/lib/fetch-httpurlconnection/build.gradle create mode 100644 mobile/android/android-components/components/lib/fetch-httpurlconnection/proguard-rules.pro create mode 100644 mobile/android/android-components/components/lib/fetch-httpurlconnection/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/lib/fetch-httpurlconnection/src/main/java/mozilla/components/lib/fetch/httpurlconnection/HttpURLConnectionClient.kt create mode 100644 mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/java/mozilla/components/lib/fetch/httpurlconnection/HttpUrlConnectionFetchTestCases.kt create mode 100644 mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/resources/robolectric.properties create mode 100644 mobile/android/android-components/components/lib/fetch-okhttp/README.md create mode 100644 mobile/android/android-components/components/lib/fetch-okhttp/build.gradle create mode 100644 mobile/android/android-components/components/lib/fetch-okhttp/proguard-rules.pro create mode 100644 mobile/android/android-components/components/lib/fetch-okhttp/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/lib/fetch-okhttp/src/main/java/mozilla/components/lib/fetch/okhttp/OkHttpClient.kt create mode 100644 mobile/android/android-components/components/lib/fetch-okhttp/src/test/java/mozilla/components/lib/fetch/okhttp/OkHttpFetchTestCases.kt create mode 100644 mobile/android/android-components/components/lib/fetch-okhttp/src/test/resources/robolectric.properties create mode 100644 mobile/android/android-components/components/lib/jexl/README.md create mode 100644 mobile/android/android-components/components/lib/jexl/build.gradle create mode 100644 mobile/android/android-components/components/lib/jexl/proguard-rules.pro create mode 100644 mobile/android/android-components/components/lib/jexl/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/Jexl.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/ast/nodes.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/Evaluator.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/EvaluatorHandlers.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/JexlContext.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/ext/JexlExtensions.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/grammar/Grammar.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/Lexer.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/LexerInput.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/Token.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/parser/Parser.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/parser/StateMachine.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/value/JexlValue.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/JexlTest.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/LanguageTest.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/evaluator/EvaluatorTest.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/ext/JexlExtensionsTest.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/lexer/LexerTest.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/parser/ParserTest.kt create mode 100644 mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/value/JexlValueTest.kt create mode 100644 mobile/android/android-components/components/lib/publicsuffixlist/README.md create mode 100644 mobile/android/android-components/components/lib/publicsuffixlist/build.gradle create mode 100644 mobile/android/android-components/components/lib/publicsuffixlist/proguard-rules.pro create mode 100644 mobile/android/android-components/components/lib/publicsuffixlist/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/lib/publicsuffixlist/src/main/assets/publicsuffixes create mode 100644 mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt create mode 100644 mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt create mode 100644 mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.kt create mode 100644 mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt create mode 100644 mobile/android/android-components/components/lib/publicsuffixlist/src/test/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListTest.kt create mode 100644 mobile/android/android-components/components/lib/publicsuffixlist/src/test/resources/robolectric.properties create mode 100644 mobile/android/android-components/components/lib/push-firebase/README.md create mode 100644 mobile/android/android-components/components/lib/push-firebase/build.gradle create mode 100644 mobile/android/android-components/components/lib/push-firebase/proguard-rules.pro create mode 100644 mobile/android/android-components/components/lib/push-firebase/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/lib/push-firebase/src/main/java/mozilla/components/lib/push/firebase/AbstractFirebasePushService.kt create mode 100644 mobile/android/android-components/components/lib/push-firebase/src/test/java/mozilla/components/lib/push/firebase/AbstractFirebasePushServiceTest.kt create mode 100644 mobile/android/android-components/components/lib/push-firebase/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 mobile/android/android-components/components/lib/push-firebase/src/test/resources/robolectric.properties create mode 100644 mobile/android/android-components/components/lib/state/README.md create mode 100644 mobile/android/android-components/components/lib/state/build.gradle create mode 100644 mobile/android/android-components/components/lib/state/proguard-rules.pro create mode 100644 mobile/android/android-components/components/lib/state/src/androidTest/java/mozilla/components/lib/state/ext/ComposeExtensionsKtTest.kt create mode 100644 mobile/android/android-components/components/lib/state/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Action.kt create mode 100644 mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/DelicateAction.kt create mode 100644 mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Middleware.kt create mode 100644 mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Observer.kt create mode 100644 mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Reducer.kt create mode 100644 mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/State.kt create mode 100644 mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Store.kt create mode 100644 mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/ComposeExtensions.kt create mode 100644 mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/Fragment.kt create mode 100644 mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/StoreExtensions.kt create mode 100644 mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/View.kt create mode 100644 mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/helpers/AbstractBinding.kt create mode 100644 mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/internal/ReducerChainBuilder.kt create mode 100644 mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/internal/StoreThreadFactory.kt create mode 100644 mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreExceptionTest.kt create mode 100644 mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreTest.kt create mode 100644 mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/FragmentKtTest.kt create mode 100644 mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/StoreExtensionsKtTest.kt create mode 100644 mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/ViewKtTest.kt create mode 100644 mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/helpers/AbstractBindingTest.kt create mode 100644 mobile/android/android-components/components/lib/state/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 mobile/android/android-components/components/lib/state/src/test/resources/robolectric.properties (limited to 'mobile/android/android-components/components/lib') diff --git a/mobile/android/android-components/components/lib/auth/build.gradle b/mobile/android/android-components/components/lib/auth/build.gradle new file mode 100644 index 0000000000..26f11505ee --- /dev/null +++ b/mobile/android/android-components/components/lib/auth/build.gradle @@ -0,0 +1,38 @@ +/* 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/. */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + + defaultConfig { + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + namespace 'mozilla.components.lib.auth' +} + +dependencies { + implementation project(':support-base') + implementation ComponentsDependencies.androidx_biometric + + testImplementation project(':support-test') + testImplementation ComponentsDependencies.androidx_test_core + testImplementation ComponentsDependencies.androidx_test_junit + testImplementation ComponentsDependencies.testing_robolectric +} + +apply from: '../../../android-lint.gradle' +apply from: '../../../publish.gradle' +ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) diff --git a/mobile/android/android-components/components/lib/auth/proguard-rules.pro b/mobile/android/android-components/components/lib/auth/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/mobile/android/android-components/components/lib/auth/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/mobile/android/android-components/components/lib/auth/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/auth/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..41078a7325 --- /dev/null +++ b/mobile/android/android-components/components/lib/auth/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + diff --git a/mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/AuthenticationDelegate.kt b/mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/AuthenticationDelegate.kt new file mode 100644 index 0000000000..c1cb5265c3 --- /dev/null +++ b/mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/AuthenticationDelegate.kt @@ -0,0 +1,29 @@ +/* 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.auth + +/** + * Callbacks for BiometricPrompt Authentication + */ +interface AuthenticationDelegate { + + /** + * Called when a biometric (e.g. fingerprint, face, etc.) + * is presented but not recognized as belonging to the user. + */ + fun onAuthFailure() + + /** + * Called when a biometric (e.g. fingerprint, face, etc.) is recognized, + * indicating that the user has successfully authenticated. + */ + fun onAuthSuccess() + + /** + * Called when an unrecoverable error has been encountered and authentication has stopped. + * @param errorText A human-readable error string that can be shown on an UI + */ + fun onAuthError(errorText: String) +} diff --git a/mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/BiometricPromptAuth.kt b/mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/BiometricPromptAuth.kt new file mode 100644 index 0000000000..a815bebe39 --- /dev/null +++ b/mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/BiometricPromptAuth.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.auth + +import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK +import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import mozilla.components.support.base.feature.LifecycleAwareFeature +import mozilla.components.support.base.log.logger.Logger + +/** + * A [LifecycleAwareFeature] for the Android Biometric API to prompt for user authentication. + * The prompt also requests support for the device PIN as a fallback authentication mechanism. + * + * @param context Android context. + * @param fragment The fragment on which this feature will live. + * @param authenticationDelegate Callbacks for BiometricPrompt. + */ +class BiometricPromptAuth( + private val context: Context, + private val fragment: Fragment, + private val authenticationDelegate: AuthenticationDelegate, +) : LifecycleAwareFeature { + private val logger = Logger(javaClass.simpleName) + + @VisibleForTesting + internal var biometricPrompt: BiometricPrompt? = null + + override fun start() { + val executor = ContextCompat.getMainExecutor(context) + biometricPrompt = BiometricPrompt(fragment, executor, PromptCallback()) + } + + override fun stop() { + biometricPrompt = null + } + + /** + * Requests the user for biometric authentication. + * + * @param title Adds a title for the authentication prompt. + * @param subtitle Adds a subtitle for the authentication prompt. + */ + fun requestAuthentication( + title: String, + subtitle: String = "", + ) { + val promptInfo: BiometricPrompt.PromptInfo = BiometricPrompt.PromptInfo.Builder() + .setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL) + .setTitle(title) + .setSubtitle(subtitle) + .build() + biometricPrompt?.authenticate(promptInfo) + } + + internal inner class PromptCallback : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + logger.error("onAuthenticationError: errorMessage $errString errorCode=$errorCode") + authenticationDelegate.onAuthError(errString.toString()) + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + logger.debug("onAuthenticationSucceeded") + authenticationDelegate.onAuthSuccess() + } + + override fun onAuthenticationFailed() { + logger.error("onAuthenticationFailed") + authenticationDelegate.onAuthFailure() + } + } +} diff --git a/mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/BiometricUtils.kt b/mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/BiometricUtils.kt new file mode 100644 index 0000000000..3f4ca88fc1 --- /dev/null +++ b/mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/BiometricUtils.kt @@ -0,0 +1,51 @@ +/* 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.auth + +import android.content.Context +import android.os.Build +import androidx.biometric.BiometricManager + +/** + * Utility class for BiometricPromptAuth + */ + +fun Context.canUseBiometricFeature(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val manager = BiometricManager.from(this) + return BiometricUtils.canUseFeature(manager) + } else { + false + } +} + +internal object BiometricUtils { + + /** + * Checks if the appropriate SDK version and hardware capabilities are met to use the feature. + */ + internal fun canUseFeature(manager: BiometricManager): Boolean { + return isHardwareAvailable(manager) && isEnrolled(manager) + } + + /** + * Checks if the hardware requirements are met for using the [BiometricManager]. + */ + internal fun isHardwareAvailable(biometricManager: BiometricManager): Boolean { + val status = + biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) + return status != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE && + status != BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE + } + + /** + * Checks if the user can use the [BiometricManager] and is therefore enrolled. + */ + internal fun isEnrolled(biometricManager: BiometricManager): Boolean { + val status = + biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) + return status == BiometricManager.BIOMETRIC_SUCCESS + } +} diff --git a/mobile/android/android-components/components/lib/auth/src/test/java/mozilla/components/lib/auth/BiometricPromptAuthTest.kt b/mobile/android/android-components/components/lib/auth/src/test/java/mozilla/components/lib/auth/BiometricPromptAuthTest.kt new file mode 100644 index 0000000000..1c74d24da9 --- /dev/null +++ b/mobile/android/android-components/components/lib/auth/src/test/java/mozilla/components/lib/auth/BiometricPromptAuthTest.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.auth + +import androidx.biometric.BiometricPrompt +import androidx.fragment.app.Fragment +import mozilla.components.support.test.any +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.createAddedTestFragment +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class BiometricPromptAuthTest { + + private lateinit var biometricPromptAuth: BiometricPromptAuth + private lateinit var fragment: Fragment + + @Before + fun setup() { + fragment = createAddedTestFragment { Fragment() } + biometricPromptAuth = BiometricPromptAuth( + testContext, + fragment, + object : AuthenticationDelegate { + override fun onAuthFailure() { + } + + override fun onAuthSuccess() { + } + + override fun onAuthError(errorText: String) { + } + }, + ) + } + + @Test + fun `prompt is created and destroyed on start and stop`() { + assertNull(biometricPromptAuth.biometricPrompt) + + biometricPromptAuth.start() + + assertNotNull(biometricPromptAuth.biometricPrompt) + + biometricPromptAuth.stop() + + assertNull(biometricPromptAuth.biometricPrompt) + } + + @Test + fun `requestAuthentication invokes biometric prompt`() { + val prompt: BiometricPrompt = mock() + + biometricPromptAuth.biometricPrompt = prompt + + biometricPromptAuth.requestAuthentication("title", "subtitle") + + verify(prompt).authenticate(any()) + } + + @Test + fun `promptCallback fires feature callbacks`() { + val authenticationDelegate: AuthenticationDelegate = mock() + val feature = BiometricPromptAuth(testContext, fragment, authenticationDelegate) + val callback = feature.PromptCallback() + val prompt = BiometricPrompt(fragment, callback) + + feature.biometricPrompt = prompt + + callback.onAuthenticationError(BiometricPrompt.ERROR_CANCELED, "") + + verify(authenticationDelegate).onAuthError("") + + callback.onAuthenticationFailed() + + verify(authenticationDelegate).onAuthFailure() + + callback.onAuthenticationSucceeded(mock()) + + verify(authenticationDelegate).onAuthSuccess() + } +} diff --git a/mobile/android/android-components/components/lib/auth/src/test/java/mozilla/components/lib/auth/BiometricUtilsTest.kt b/mobile/android/android-components/components/lib/auth/src/test/java/mozilla/components/lib/auth/BiometricUtilsTest.kt new file mode 100644 index 0000000000..c8c9d53b70 --- /dev/null +++ b/mobile/android/android-components/components/lib/auth/src/test/java/mozilla/components/lib/auth/BiometricUtilsTest.kt @@ -0,0 +1,50 @@ +/* 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.auth + +import android.os.Build +import androidx.biometric.BiometricManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import mozilla.components.support.test.whenever +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class BiometricUtilsTest { + + @Config(sdk = [Build.VERSION_CODES.LOLLIPOP]) + @Test + fun `canUseFeature checks for SDK compatible`() { + assertFalse(testContext.canUseBiometricFeature()) + } + + @Test + fun `isHardwareAvailable is true based on AuthenticationStatus`() { + val manager: BiometricManager = mock { + whenever(canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) + .thenReturn(BiometricManager.BIOMETRIC_SUCCESS) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) + } + + assertTrue(BiometricUtils.isHardwareAvailable(manager)) + assertFalse(BiometricUtils.isHardwareAvailable(manager)) + assertFalse(BiometricUtils.isHardwareAvailable(manager)) + } + + @Test + fun `isEnrolled is true based on AuthenticationStatus`() { + val manager: BiometricManager = mock { + whenever(canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) + .thenReturn(BiometricManager.BIOMETRIC_SUCCESS) + } + assertTrue(BiometricUtils.isEnrolled(manager)) + } +} diff --git a/mobile/android/android-components/components/lib/crash-sentry/build.gradle b/mobile/android/android-components/components/lib/crash-sentry/build.gradle new file mode 100644 index 0000000000..caba4d5650 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash-sentry/build.gradle @@ -0,0 +1,45 @@ +/* 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/. */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + defaultConfig { + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + namespace 'mozilla.components.lib.crash.sentry' +} + +dependencies { + implementation project(':support-base') + implementation project(':support-ktx') + implementation project(':support-utils') + implementation project(':lib-crash') + + implementation ComponentsDependencies.thirdparty_sentry + testImplementation ComponentsDependencies.thirdparty_sentry + + testImplementation project(':support-test') + testImplementation ComponentsDependencies.androidx_test_core + testImplementation ComponentsDependencies.androidx_test_junit + testImplementation ComponentsDependencies.testing_robolectric +} + +apply from: '../../../android-lint.gradle' +apply from: '../../../publish.gradle' +ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) diff --git a/mobile/android/android-components/components/lib/crash-sentry/proguard-rules.pro b/mobile/android/android-components/components/lib/crash-sentry/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/components/lib/crash-sentry/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/android-components/components/lib/crash-sentry/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/crash-sentry/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..7b04326db6 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash-sentry/src/main/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/SentryService.kt b/mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/SentryService.kt new file mode 100644 index 0000000000..fbe4c5e874 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/SentryService.kt @@ -0,0 +1,201 @@ +/* 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.sentry + +import android.content.Context +import androidx.annotation.GuardedBy +import androidx.annotation.VisibleForTesting +import io.sentry.Breadcrumb +import io.sentry.Sentry +import io.sentry.SentryLevel +import io.sentry.android.core.SentryAndroid +import io.sentry.protocol.SentryId +import mozilla.components.Build +import mozilla.components.lib.crash.Crash +import mozilla.components.lib.crash.sentry.eventprocessors.AddMechanismEventProcessor +import mozilla.components.lib.crash.sentry.eventprocessors.RustCrashEventProcessor +import mozilla.components.lib.crash.service.CrashReporterService +import java.util.Locale +import mozilla.components.concept.base.crash.Breadcrumb as MozillaBreadcrumb + +/** + * A [CrashReporterService] implementation that uploads crash reports using + * the Sentry SDK version 5.6.1 and above. + * + * This implementation will add default tags to every sent crash report + * (like which Android Components version is being used) prefixed with "ac". + * + * @param applicationContext The application [Context]. + * @param dsn Data Source Name of the Sentry server. + * @param tags A list of additional tags that will be sent together with crash reports. + * @param environment An optional, environment name string or null to set none + * @param sendEventForNativeCrashes Allows configuring if native crashes should be submitted. Disabled by default. + * @param sentryProjectUrl Base URL of the Sentry web interface pointing to the app/project. + * @param sendCaughtExceptions Allows configuring if caught exceptions should be submitted. Enabled by default. + * @param autoInitializeSentry Initializes the Sentry SDK immediately on service creation. + */ +class SentryService( + private val applicationContext: Context, + private val dsn: String, + private val tags: Map = emptyMap(), + private val environment: String? = null, + private val sendEventForNativeCrashes: Boolean = false, + private val sentryProjectUrl: String? = null, + private val sendCaughtExceptions: Boolean = true, +) : CrashReporterService { + + override val id: String = "new-sentry-instance" + override val name: String = "New Sentry Instance" + + @VisibleForTesting + @GuardedBy("this") + internal var isInitialized: Boolean = false + + override fun createCrashReportUrl(identifier: String): String? { + return sentryProjectUrl?.let { + val id = identifier.replace("-", "") + return "$it&query=$id" + } + } + + override fun report(crash: Crash.UncaughtExceptionCrash): String { + prepareReport(crash.breadcrumbs, SentryLevel.FATAL) + return reportToSentry(crash.throwable) + } + + override fun report(crash: Crash.NativeCodeCrash): String? { + return if (sendEventForNativeCrashes) { + val level = when (crash.isFatal) { + true -> SentryLevel.FATAL + else -> SentryLevel.ERROR + } + + prepareReport(crash.breadcrumbs, level) + + return reportToSentry(crash) + } else { + null + } + } + + override fun report(throwable: Throwable, breadcrumbs: ArrayList): String? { + if (!sendCaughtExceptions) { + return null + } + prepareReport(breadcrumbs, SentryLevel.INFO) + return reportToSentry(throwable) + } + + @VisibleForTesting + internal fun reportToSentry(throwable: Throwable): String { + return Sentry.captureException(throwable).alsoClearBreadcrumbs() + } + + @VisibleForTesting + internal fun reportToSentry(crash: Crash.NativeCodeCrash): String { + return Sentry.captureMessage(createMessage(crash)).alsoClearBreadcrumbs() + } + + private fun addDefaultTags() { + Sentry.setTag("ac.version", Build.version) + Sentry.setTag("ac.git", Build.gitHash) + Sentry.setTag("ac.as.build_version", Build.applicationServicesVersion) + Sentry.setTag("ac.glean.build_version", Build.gleanSdkVersion) + Sentry.setTag("user.locale", Locale.getDefault().toString()) + tags.forEach { entry -> + Sentry.setTag(entry.key, entry.value) + } + } + + /** + * Initializes Sentry if needed. + * + * N.B: We've temporarily made this public so that Fenix can initialize Sentry on startup. + * As a result of https://bugzilla.mozilla.org/show_bug.cgi?id=1853059 we will have a better way + * to control how / when Sentry gets initialized and we will make this internal again. + */ + @Synchronized + fun initIfNeeded() { + if (isInitialized) { + return + } + initSentry() + addDefaultTags() + isInitialized = true + } + + @VisibleForTesting + internal fun initSentry() { + SentryAndroid.init(applicationContext) { options -> + // Disable uncaught non-native exceptions from being reported. + // We already have our own uncaught exception handler [ExceptionHandler], + // so we don't need Sentry's default one. + options.setEnableUncaughtExceptionHandler(false) + // Disable uncaught native exceptions from being reported. + // Sentry don't have a way to disable uncaught native exceptions from being reported. + // As a fallback we had to disable all native integrations. + // More info can be found https://github.com/getsentry/sentry-java/issues/1993 + options.isEnableNdk = false + options.dsn = dsn + options.environment = environment + options.addEventProcessor(RustCrashEventProcessor()) + options.addEventProcessor(AddMechanismEventProcessor()) + } + } + + @VisibleForTesting + internal fun prepareReport( + breadcrumbs: ArrayList, + level: SentryLevel? = null, + ) { + initIfNeeded() + + breadcrumbs.forEach { + Sentry.addBreadcrumb(it.toSentryBreadcrumb()) + } + + level?.apply { + Sentry.setLevel(level) + } + } + + private fun SentryId.alsoClearBreadcrumbs(): String { + Sentry.clearBreadcrumbs() + return this.toString() + } + + @VisibleForTesting + internal fun createMessage(crash: Crash.NativeCodeCrash): String { + val fatal = crash.isFatal.toString() + val processType = crash.processType + val minidumpSuccess = crash.minidumpSuccess + + return "NativeCodeCrash(fatal=$fatal, processType=$processType, minidumpSuccess=$minidumpSuccess)" + } +} + +@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) +internal fun MozillaBreadcrumb.toSentryBreadcrumb(): Breadcrumb { + val sentryLevel = this.level.toSentryBreadcrumbLevel() + val breadcrumb = Breadcrumb(this.date).apply { + message = this@toSentryBreadcrumb.message + category = this@toSentryBreadcrumb.category + level = sentryLevel + type = this@toSentryBreadcrumb.type.value + } + this.data.forEach { + breadcrumb.setData(it.key, it.value) + } + return breadcrumb +} + +@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) +internal fun MozillaBreadcrumb.Level.toSentryBreadcrumbLevel() = when (this) { + MozillaBreadcrumb.Level.CRITICAL -> SentryLevel.FATAL + MozillaBreadcrumb.Level.ERROR -> SentryLevel.ERROR + MozillaBreadcrumb.Level.WARNING -> SentryLevel.WARNING + MozillaBreadcrumb.Level.INFO -> SentryLevel.INFO + MozillaBreadcrumb.Level.DEBUG -> SentryLevel.DEBUG +} diff --git a/mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/eventprocessors/AddMechanismEventProcessor.kt b/mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/eventprocessors/AddMechanismEventProcessor.kt new file mode 100644 index 0000000000..92adbd2906 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/eventprocessors/AddMechanismEventProcessor.kt @@ -0,0 +1,41 @@ +/* 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.sentry.eventprocessors + +import androidx.annotation.VisibleForTesting +import io.sentry.EventProcessor +import io.sentry.Hint +import io.sentry.SentryEvent +import io.sentry.SentryLevel +import io.sentry.protocol.Mechanism + +/** + * A [EventProcessor] implementation that adds a [Machanism] + * to [SentryLevel.FATAL] events. + */ +class AddMechanismEventProcessor : EventProcessor { + override fun process(event: SentryEvent, hint: Hint): SentryEvent { + if (event.level == SentryLevel.FATAL) { + // Sentry now uses the `Mechanism` to determine whether or not an exception is + // handled. Any exception sent with `Sentry.captureException` is assumed to be handled + // by Sentry. We can attach a `UncaughtExceptionHandler` mechanism to the `SentryException` + // to correctly signal to Sentry that this is an uncaught exception. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1835107 + event.exceptions?.firstOrNull()?.let { sentryException -> + sentryException.mechanism = Mechanism().apply { + type = UNCAUGHT_EXCEPTION_TYPE + isHandled = false + } + } + } + + return event + } + + companion object { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal const val UNCAUGHT_EXCEPTION_TYPE = "UncaughtExceptionHandler" + } +} diff --git a/mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/eventprocessors/RustCrashEventProcessor.kt b/mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/eventprocessors/RustCrashEventProcessor.kt new file mode 100644 index 0000000000..a6069699bc --- /dev/null +++ b/mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/eventprocessors/RustCrashEventProcessor.kt @@ -0,0 +1,36 @@ +/* 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.sentry.eventprocessors + +import io.sentry.EventProcessor +import io.sentry.Hint +import io.sentry.SentryEvent +import mozilla.components.concept.base.crash.RustCrashReport as RustCrashReport + +/** + * A [EventProcessor] implementation that cleans up exceptions for + * crashes coming from our Rust libraries. + */ +class RustCrashEventProcessor : EventProcessor { + override fun process(event: SentryEvent, hint: Hint): SentryEvent { + val throwable = event.throwable + + if (throwable is RustCrashReport) { + event.fingerprints = listOf(throwable.typeName) + // Sentry supports multiple exceptions in an event, modify + // the top-level one controls how the event is displayed + // + // It's technically possible for the event to have a null + // or empty exception list, but that shouldn't happen in + // practice. + event.exceptions?.firstOrNull()?.let { sentryException -> + sentryException.type = throwable.typeName + sentryException.value = throwable.message + } + } + + return event + } +} diff --git a/mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/SentryServiceTest.kt b/mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/SentryServiceTest.kt new file mode 100644 index 0000000000..e6a2aa25b6 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/SentryServiceTest.kt @@ -0,0 +1,276 @@ +/* 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.sentry + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.Sentry +import io.sentry.SentryLevel +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertNull +import junit.framework.TestCase.assertTrue +import mozilla.components.concept.base.crash.Breadcrumb +import mozilla.components.lib.crash.Crash +import mozilla.components.support.test.robolectric.testContext +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import java.util.Date +import mozilla.components.concept.base.crash.Breadcrumb as MozillaBreadcrumb + +@RunWith(AndroidJUnit4::class) +class SentryServiceTest { + class TestException : Exception() + + @Before + fun setup() { + Sentry.close() + } + + @Test + fun `WHEN calling initIfNeeded THEN initialize sentry once`() { + val service = spy( + SentryService( + testContext, + "https://not:real6@sentry.prod.example.net/405", + sendCaughtExceptions = false, + ), + ) + + assertFalse(service.isInitialized) + + service.initIfNeeded() + + assertTrue(service.isInitialized) + + service.initIfNeeded() + + verify(service, times(1)).initSentry() + } + + @Test + fun `WHEN report a uncaught exception THEN forward a fatal exception to the Sentry sdk`() { + val service = spy( + SentryService( + testContext, + "https://not:real6@sentry.prod.example.net/405", + ), + ) + + val exception = RuntimeException("Hello World") + val breadcrumbs = arrayListOf() + + service.report(Crash.UncaughtExceptionCrash(0, exception, breadcrumbs)) + + verify(service).prepareReport(breadcrumbs, SentryLevel.FATAL) + verify(service).reportToSentry(exception) + } + + @Test + fun `GIVEN a main process native crash WHEN reporting THEN forward to a fatal crash the Sentry sdk`() { + val service = spy( + SentryService( + applicationContext = testContext, + dsn = "https://not:real6@sentry.prod.example.net/405", + sendEventForNativeCrashes = true, + ), + ) + + val breadcrumbs = arrayListOf() + val nativeCrash = Crash.NativeCodeCrash( + timestamp = 0, + minidumpPath = "", + minidumpSuccess = true, + extrasPath = "", + processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN, + breadcrumbs = breadcrumbs, + remoteType = null, + ) + + service.report(nativeCrash) + + verify(service).prepareReport(breadcrumbs, SentryLevel.FATAL) + verify(service).reportToSentry(nativeCrash) + } + + @Test + fun `GIVEN a foreground child process native crash WHEN reporting THEN forward an error to the Sentry sdk`() { + val service = spy( + SentryService( + applicationContext = testContext, + dsn = "https://not:real6@sentry.prod.example.net/405", + sendEventForNativeCrashes = true, + ), + ) + + val breadcrumbs = arrayListOf() + val nativeCrash = Crash.NativeCodeCrash( + timestamp = 0, + minidumpPath = "", + minidumpSuccess = true, + extrasPath = "", + processType = Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD, + breadcrumbs = breadcrumbs, + remoteType = null, + ) + + service.report(nativeCrash) + + verify(service).prepareReport(breadcrumbs, SentryLevel.ERROR) + verify(service).reportToSentry(nativeCrash) + } + + @Test + fun `GIVEN a background child process native crash WHEN reporting THEN forward an error to the Sentry sdk`() { + val service = spy( + SentryService( + applicationContext = testContext, + dsn = "https://not:real6@sentry.prod.example.net/405", + sendEventForNativeCrashes = true, + ), + ) + + val breadcrumbs = arrayListOf() + val nativeCrash = Crash.NativeCodeCrash( + timestamp = 0, + minidumpPath = "", + minidumpSuccess = true, + extrasPath = "", + processType = Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD, + breadcrumbs = breadcrumbs, + remoteType = null, + ) + + service.report(nativeCrash) + + verify(service).prepareReport(breadcrumbs, SentryLevel.ERROR) + verify(service).reportToSentry(nativeCrash) + } + + @Test + fun `GIVEN sendEventForNativeCrashes is false WHEN reporting a native crash THEN DO NOT forward to the Sentry sdk`() { + val service = spy( + SentryService( + applicationContext = testContext, + dsn = "https://not:real6@sentry.prod.example.net/405", + sendEventForNativeCrashes = false, + ), + ) + + val breadcrumbs = arrayListOf() + val nativeCrash = Crash.NativeCodeCrash( + timestamp = 0, + minidumpPath = "", + minidumpSuccess = true, + extrasPath = "", + processType = Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD, + breadcrumbs = breadcrumbs, + remoteType = null, + ) + + val result = service.report(nativeCrash) + + verify(service, times(0)).prepareReport(breadcrumbs, SentryLevel.ERROR) + verify(service, times(0)).reportToSentry(nativeCrash) + assertNull(result) + } + + @Test + fun `WHEN createMessage THEN create a message version of the Native crash`() { + val service = SentryService( + applicationContext = testContext, + dsn = "https://not:real6@sentry.prod.example.net/405", + sendEventForNativeCrashes = false, + ) + + val breadcrumbs = arrayListOf() + val nativeCrash = Crash.NativeCodeCrash( + timestamp = 0, + minidumpPath = "", + minidumpSuccess = true, + extrasPath = "", + processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN, + breadcrumbs = breadcrumbs, + remoteType = null, + ) + + val result = service.createMessage(nativeCrash) + val expected = + "NativeCodeCrash(fatal=${nativeCrash.isFatal}, processType=${nativeCrash.processType}, minidumpSuccess=${nativeCrash.minidumpSuccess})" + + assertEquals(expected, result) + } + + @Test + fun `GIVEN MozillaBreadcrumb WHEN calling toSentryBreadcrumb THEN parse it to a SentryBreadcrumb`() { + val mozillaBreadcrumb = MozillaBreadcrumb( + message = "message", + data = mapOf("key" to "value"), + category = "category", + level = MozillaBreadcrumb.Level.INFO, + type = MozillaBreadcrumb.Type.DEFAULT, + date = Date(1640995200L), // 2022-01-01 + ) + val sentryBreadcrumb = mozillaBreadcrumb.toSentryBreadcrumb() + + assertEquals(mozillaBreadcrumb.message, sentryBreadcrumb.message) + assertEquals(mozillaBreadcrumb.data["key"], sentryBreadcrumb.getData("key")) + assertEquals(mozillaBreadcrumb.category, sentryBreadcrumb.category) + assertEquals(SentryLevel.INFO, sentryBreadcrumb.level) + assertEquals(MozillaBreadcrumb.Type.DEFAULT.value, sentryBreadcrumb.type) + assertEquals(mozillaBreadcrumb.date, sentryBreadcrumb.timestamp) + } + + @Test + fun `GIVEN MozillaBreadcrumb level WHEN calling toSentryBreadcrumbLevel THEN parse it to a SentryBreadcrumbLevel`() { + assertEquals(MozillaBreadcrumb.Level.CRITICAL.toSentryBreadcrumbLevel(), SentryLevel.FATAL) + assertEquals(MozillaBreadcrumb.Level.ERROR.toSentryBreadcrumbLevel(), SentryLevel.ERROR) + assertEquals(MozillaBreadcrumb.Level.WARNING.toSentryBreadcrumbLevel(), SentryLevel.WARNING) + assertEquals(MozillaBreadcrumb.Level.INFO.toSentryBreadcrumbLevel(), SentryLevel.INFO) + assertEquals(MozillaBreadcrumb.Level.DEBUG.toSentryBreadcrumbLevel(), SentryLevel.DEBUG) + } + + @Test + fun `GIVEN sending caught exceptions disabled WHEN reporting a caught exception THEN do nothing`() { + val service = spy( + SentryService( + testContext, + "https://not:real6@sentry.prod.example.net/405", + sendCaughtExceptions = false, + ), + ) + + val exception = RuntimeException("Hello World") + val breadcrumbs = arrayListOf() + + service.report(exception, breadcrumbs) + verify(service, never()).prepareReport(breadcrumbs, SentryLevel.INFO) + verify(service, never()).prepareReport(breadcrumbs, SentryLevel.FATAL) + verify(service, never()).reportToSentry(exception) + } + + @Test + fun `GIVEN sending caught exceptions enabled WHEN reporting a caught exception THEN forward it to Sentry SDK with level INFO`() { + val service = spy( + // Sending caught exceptions is enabled by default. + SentryService( + testContext, + "https://not:real6@sentry.prod.example.net/405", + ), + ) + + val exception = RuntimeException("Hello World") + val breadcrumbs = arrayListOf() + + service.report(exception, breadcrumbs) + + verify(service).prepareReport(breadcrumbs, SentryLevel.INFO) + verify(service).reportToSentry(exception) + } +} diff --git a/mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/eventprocessors/AddMechanismEventProcessorTest.kt b/mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/eventprocessors/AddMechanismEventProcessorTest.kt new file mode 100644 index 0000000000..5f14b0232a --- /dev/null +++ b/mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/eventprocessors/AddMechanismEventProcessorTest.kt @@ -0,0 +1,46 @@ +/* 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.sentry.eventprocessors + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.Hint +import io.sentry.SentryEvent +import io.sentry.SentryLevel +import io.sentry.protocol.SentryException +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNull +import junit.framework.TestCase.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class AddMechanismEventProcessorTest { + @Test + fun `GIVEN a FATAL SentryEvent WHEN process is called THEN a Mechanism is attached to the exception`() { + val processor = AddMechanismEventProcessor() + val event = SentryEvent().apply { + level = SentryLevel.FATAL + exceptions = listOf(SentryException()) + } + + assertNull(event.exceptions?.first()?.mechanism) + processor.process(event, Hint()) + assertEquals(AddMechanismEventProcessor.UNCAUGHT_EXCEPTION_TYPE, event.exceptions?.first()?.mechanism?.type) + assertTrue(event.exceptions?.first()?.mechanism?.isHandled == false) + } + + @Test + fun `GIVEN a less than FATAL SentryEvent WHEN process is called THEN no Mechanism is attached to the exception`() { + val processor = AddMechanismEventProcessor() + val event = SentryEvent().apply { + level = SentryLevel.INFO + exceptions = listOf(SentryException()) + } + + assertNull(event.exceptions?.first()?.mechanism) + processor.process(event, Hint()) + assertNull(event.exceptions?.first()?.mechanism) + } +} diff --git a/mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/eventprocessors/RustCrashEventProcessorTest.kt b/mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/eventprocessors/RustCrashEventProcessorTest.kt new file mode 100644 index 0000000000..7190f44b25 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/eventprocessors/RustCrashEventProcessorTest.kt @@ -0,0 +1,35 @@ +/* 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.sentry.eventprocessors + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.Hint +import io.sentry.SentryEvent +import io.sentry.protocol.SentryException +import junit.framework.TestCase.assertEquals +import mozilla.components.concept.base.crash.RustCrashReport +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RustCrashEventProcessorTest { + class TestRustException : Exception(), RustCrashReport { + override val typeName = "test_rust_crash" + override val message = "test_rust_message" + } + + @Test + fun `GIVEN a SentryEvent that contains a RustCrashReport WHEN process is called THEN a fingerprint is added and the exception type and value are cleaned up`() { + val processor = RustCrashEventProcessor() + val event = SentryEvent(TestRustException()).apply { + exceptions = listOf(SentryException()) + } + + processor.process(event, Hint()) + assertEquals("test_rust_crash", event.fingerprints?.first()) + assertEquals("test_rust_crash", event.exceptions?.firstOrNull()?.type) + assertEquals("test_rust_message", event.exceptions?.firstOrNull()?.value) + } +} diff --git a/mobile/android/android-components/components/lib/crash-sentry/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/lib/crash-sentry/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..cf1c399ea8 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash-sentry/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1,2 @@ +mock-maker-inline +// This allows mocking final classes (classes are final by default in Kotlin) diff --git a/mobile/android/android-components/components/lib/crash-sentry/src/test/resources/robolectric.properties b/mobile/android/android-components/components/lib/crash-sentry/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..932b01b9eb --- /dev/null +++ b/mobile/android/android-components/components/lib/crash-sentry/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +sdk=28 diff --git a/mobile/android/android-components/components/lib/crash/README.md b/mobile/android/android-components/components/lib/crash/README.md new file mode 100644 index 0000000000..43208dd3ce --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/README.md @@ -0,0 +1,239 @@ +# [Android Components](../../../README.md) > Libraries > Crash + +A generic crash reporter component that can report crashes to multiple services. + +Main features: + +* Support for multiple crash reporting services (included is support for [Sentry](https://sentry.io) and [Socorro](https://wiki.mozilla.org/Socorro)). +* Support for crashes caused by uncaught exceptions. +* Support for native code crashes (currently primarily focused on GeckoView crashes). +* Can optionally prompt the user for confirmation before sending a crash report. +* Support for showing in-app confirmation UI for non-fatal crashes. + +## Usage + +### Setting up the dependency + +Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)): + +```Groovy +implementation "org.mozilla.components:lib-crash:{latest-version}" +``` + +### Setting up crash reporting + +In the `onCreate()` method of your Application class create a `CrashReporter` instance and call `install()`: + +```Kotlin +CrashReporter( + services = listOf( + // List the crash reporting services you want to use + ) +).install(this) +``` + +With this minimal setup the crash reporting library will capture "uncaught exception" crashes and "native code" crashes and forward them to the configured crash reporting services. + +⚠️ Note: To avoid conflicting setups do not use any other crash reporting libraries/services independently from this library. + +### Recording crash breadcrumbs to supported services + +Using the `CrashReporter` instance to record crash breadcrumbs. These breadcrumbs will then be sent when a crash occurs to aid in debugging. Breadcrumbs are reported only if the underlying crash reporter service supports it. + +⚠️ Note: Directly using Sentry's breadcrumb will not work as expected on Android 10 or above. Using the `CrashReporter` breadcrumb is preferred. + +```Kotlin +crashReporter.recordCrashBreadcrumb( + CrashBreadcrumb("Settings button clicked", data, "UI", Level.INFO, Type.USER) +) +``` + +### Sending crash reports to Sentry + +⚠️ Note: The crash reporter library is compiled against the Sentry SDK but it doesn't require it as a dependency. The app using the component is responsible for adding the Sentry dependency to its build files in order to use Sentry crash reporting. + +Add a `SentryService` instance to your `CrashReporter` in order to upload crashes to Sentry: + +```Kotlin +CrashReporter( + services = listOf( + SentryService(applicationContext, "your sentry DSN") + ) +).install(applicationContext) +``` + +By default only the `DSN` is needed. But there are additional option configuration parameters: + +```Kotlin +SentryService( + applicationContext, + "your sentry DSN", + + // Optionally add tags that will be sent with every crash report + tags = mapOf( + "build_flavor" to BuildConfig.FLAVOR, + "build_type" to BuildConfig.BUILD_TYPE + ), + + // Send an event to Sentry for every native code crash. Native code crashes + // can't be uploaded to Sentry currently. But sending an event to Sentry + // gives you an idea about how often native code crashes. For sending native + // crash reports add additional services like Socorro. + sendEventForNativeCrashes = true +) +``` + +### Sending crash reports to Mozilla Socorro + +[Socorro](https://wiki.mozilla.org/Socorro) is the name for the [Mozilla Crash Stats](https://crash-stats.mozilla.org/) project. + +⚠️ Note: Socorro filters crashes by "app name". New app names need to be safelisted for the server to accept the crash. [File a bug](https://bugzilla.mozilla.org/enter_bug.cgi?product=Socorro) if you would like to get your app added to the safelist. + +Add a `MozillaSocorroService` instance to your `CrashReporter` in order to upload crashes to Socorro: + +```Kotlin +CrashReporter( + services = listOf( + MozillaSocorroService(applicationContext, "your app name") + ) +).install(applicationContext) +``` + +`MozillaSocorroService` will report version information such as App version, Android Component version, Glean version, Application Services version, GeckoView version and Build ID +⚠️ Note: Currently only native code crashes get uploaded to Socorro. Socorro has limited support for "uncaught exception" crashes too, but it is recommended to use a more elaborate solution like Sentry for that. + +### Sending crash reports to Glean + +[Glean](https://docs.telemetry.mozilla.org/concepts/glean/glean.html) is a new way to collect telemetry by Mozilla. +This will record crash counts as a labeled counter with each label corresponding to a specific type of crash (`fatal_native_code_crash`, `nonfatal_native_code_crash`, `caught_exception`, `uncaught_exception`, currently). +The list of collected metrics is available in the [metrics.yaml file](metrics.yaml), with their documentation [living here](https://dictionary.telemetry.mozilla.org/apps/fenix/pings/crash). +Due to the fact that Glean can only be recorded to in the main process and lib-crash runs in a separate process when it runs to handle the crash, +lib-crash persists the data in a file format and then reads and records the data from the main process when the application is next run since the `GleanCrashReporterService` +constructor is loaded from the main process. + +Add a `GleanCrashReporterService` instance to your `CrashReporter` in order to record crashes in Glean: + +```Kotlin +CrashReporter( + services = listOf( + GleanCrashReporterService() + ) +).install(applicationContext) +``` + +⚠️ Note: Applications using the `GleanCrashReporterService` are **required** to undergo [Data Collection Review](https://wiki.mozilla.org/Firefox/Data_Collection) for the crash counts that they will be collecting. + +### Showing a crash reporter prompt + +![](images/crash-dialog.png) + +Optionally the library can show a prompt asking the user for confirmation before sending a crash report. + +The behavior can be controlled using the `shouldPrompt` parameter: + +```Kotlin +CrashReporter( + // Always prompt + shouldPrompt = CrashReporter.Prompt.ALWAYS, + + // Or: Only prompt for native crashes + shouldPrompt = CrashReporter.Prompt.ONLY_NATIVE_CRASH, + + // Or: Never show the prompt + shouldPrompt = CrashReporter.Prompt.NEVER, + + // .. +).install(applicationContext) +``` + +#### Customizing the prompt + +The crash reporter prompt can be customized by providing a `PromptConfiguration` object: + +```Kotlin +CrashReporter( + promptConfiguration = CrashReporter.PromptConfiguration( + appName = "My App", + organizationName = "My Organization", + + // An additional message that will be shown in the prompt + message = "We are very sorry!" + + // Use a custom theme for the prompt (Extend Theme.Mozac.CrashReporter) + theme = android.R.style.Theme_Holo_Dialog + ), + + // .. +).install(applicationContext) +``` + +#### Handling non-fatal crashes + +A native code crash can be non-fatal. In this situation a child process crashed but the main process (in which the application runs) is not affected. In this situation a crash can be handled more gracefully and instead of using the crash reporter prompt of the component an app may want to show an in-app UI for asking the user for confirmation. + +![](images/crash-in-app.png) + +Provide a `PendingIntent` that will be invoked when a non-fatal crash occurs: + +```Kotlin +// Launch this activity when a crash occurs. +val pendingIntent = PendingIntent.getActivity( + context, + 0, + Intent(this, MyActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }, + PendingIntentUtils.defaultFlags +) + +CrashReporter( + shouldPrompt = CrashReporter.Prompt.ALWAYS, + services = listOf( + // ... + ), + nonFatalCrashIntent = pendingIntent +).install(this) +``` + +In your component that receives the Intent (e.g. `Activity`) you can use `Crash.fromIntent()` to receive the `Crash` object. Once the user has approved sending a report call `submitReport()` on your `CrashReporter` instance. + +```Kotlin +// In your crash handling component (e.g. Activity) +if (Crash.isCrashIntent(intent) { + val crash = Crash.fromIntent(intent) + + ... +} + +// Once the user has confirmed sending a crash report: +crashReporter.submitReport(crash) +``` + +⚠️ Note: `submitReport()` may block and perform I/O on the calling thread. + +### Sending GeckoView crash reports + +⚠️ Note: For sending GeckoView crash reports GeckoView **64.0** or higher is required! + +Register `CrashHandlerService` as crash handler for GeckoView: + +```Kotlin +val settings = GeckoRuntimeSettings.Builder() + .crashHandler(CrashHandlerService::class.java) + .build() + +// Crashes of this runtime will be forwarded to the crash reporter component +val runtime = GeckoRuntime.create(applicationContext, settings) + +// If you are using the browser-engine-gecko component then pass the runtime +// to your code initializing the engine: +val engine = GeckoEngine(applicationContext, defaultSettings, runtime) +``` + +ℹ️ You can force a child process crash (non fatal!) using a multi-process (E10S) GeckoView by loading the test URL `about:crashcontent`. Using a non-multi-process GeckoView you can use `about:crashparent` to force a fatal crash. + +## License + + 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/ diff --git a/mobile/android/android-components/components/lib/crash/build.gradle b/mobile/android/android-components/components/lib/crash/build.gradle new file mode 100644 index 0000000000..afedcf3044 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/build.gradle @@ -0,0 +1,99 @@ +/* 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/. */ + +buildscript { + repositories { + gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORIES.each { repository -> + maven { + url repository + if (gradle.mozconfig.substs.ALLOW_INSECURE_GRADLE_REPOSITORIES) { + allowInsecureProtocol = true + } + } + } + } + + dependencies { + classpath "org.mozilla.telemetry:glean-gradle-plugin:${Versions.mozilla_glean}" + classpath ComponentsDependencies.plugin_serialization + } +} + +plugins { + id "com.jetbrains.python.envs" version "$python_envs_plugin" +} + +apply plugin: 'com.android.library' +apply plugin: 'com.google.devtools.ksp' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +android { + defaultConfig { + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + javaCompileOptions { + annotationProcessorOptions { + arguments += ["room.incremental": "true"] + } + } + } + + ksp { + arg("room.schemaLocation", "$projectDir/schemas".toString()) + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + buildFeatures { + viewBinding true + buildConfig true + } + + namespace 'mozilla.components.lib.crash' +} + +dependencies { + implementation ComponentsDependencies.kotlin_coroutines + implementation ComponentsDependencies.kotlin_json + + implementation ComponentsDependencies.androidx_appcompat + implementation ComponentsDependencies.androidx_constraintlayout + implementation ComponentsDependencies.androidx_recyclerview + + implementation project(':support-base') + implementation project(':support-ktx') + implementation project(':support-utils') + + implementation ComponentsDependencies.androidx_room_runtime + ksp ComponentsDependencies.androidx_room_compiler + + // We only compile against GeckoView and Glean. It's up to the app to add those dependencies if it wants to + // send crash reports to Socorro (GV). + compileOnly project(":service-glean") + testImplementation project(":service-glean") + testImplementation ComponentsDependencies.androidx_work_testing + + testImplementation project(':support-test') + testImplementation ComponentsDependencies.androidx_test_core + testImplementation ComponentsDependencies.androidx_test_junit + testImplementation ComponentsDependencies.testing_robolectric + testImplementation ComponentsDependencies.testing_coroutines + testImplementation ComponentsDependencies.testing_mockwebserver + testImplementation ComponentsDependencies.mozilla_glean_forUnitTests +} + +apply plugin: "org.mozilla.telemetry.glean-gradle-plugin" +apply from: '../../../android-lint.gradle' +apply from: '../../../publish.gradle' +ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) diff --git a/mobile/android/android-components/components/lib/crash/images/crash-dialog.png b/mobile/android/android-components/components/lib/crash/images/crash-dialog.png new file mode 100644 index 0000000000..6fc96cc167 Binary files /dev/null and b/mobile/android/android-components/components/lib/crash/images/crash-dialog.png differ diff --git a/mobile/android/android-components/components/lib/crash/images/crash-in-app.png b/mobile/android/android-components/components/lib/crash/images/crash-in-app.png new file mode 100644 index 0000000000..25392af00c Binary files /dev/null and b/mobile/android/android-components/components/lib/crash/images/crash-in-app.png differ diff --git a/mobile/android/android-components/components/lib/crash/metrics.yaml b/mobile/android/android-components/components/lib/crash/metrics.yaml new file mode 100644 index 0000000000..bf30991944 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/metrics.yaml @@ -0,0 +1,154 @@ +# 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/. + +# This file defines the metrics that are recorded by glean telemetry. They are +# automatically converted to Kotlin code at build time using the `glean_parser` +# PyPI package. +--- + +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 + +crash_metrics: + crash_count: + type: labeled_counter + description: > + Counts the number of crashes that occur in the application. + This measures only the counts of each crash in association + with the labeled type of the crash. + The labels correspond to the types of crashes handled by lib-crash. + + Deprecated: `native_code_crash`, `fatal_native_code_crash` and + `nonfatal_native_code_crash` replaced by `main_proc_native_code_crash`, + `fg_proc_native_code_crash` and `bg_proc_native_code_crash`. + labels: + - uncaught_exception + - caught_exception + - main_proc_native_code_crash + - fg_proc_native_code_crash + - bg_proc_native_code_crash + - fatal_native_code_crash + - nonfatal_native_code_crash + bugs: + - https://bugzilla.mozilla.org/1553935 + - https://github.com/mozilla-mobile/android-components/issues/5175 + - https://github.com/mozilla-mobile/android-components/issues/11876 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1553935#c3 + - https://github.com/mozilla-mobile/android-components/pull/5700#pullrequestreview-347721248 + - https://github.com/mozilla-mobile/android-components/pull/11908#issuecomment-1075243414 + data_sensitivity: + - technical + notification_emails: + - android-probes@mozilla.com + - jnicol@mozilla.com + expires: never + +crash: + uptime: + type: timespan + description: > + The application uptime. This is equivalent to the legacy crash ping's + `UptimeTS` field. + notification_emails: + - crash-reporting-wg@mozilla.org + - stability@mozilla.org + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569#c12 + data_sensitivity: + - technical + expires: never + send_in_pings: + - crash + + process_type: + type: string + # yamllint disable + description: > + The type of process that experienced a crash. See the full list of + options + [here](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/data/crash-ping.html#process-types). + # yamllint enable + notification_emails: + - crash-reporting-wg@mozilla.org + - stability@mozilla.org + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569#c12 + data_sensitivity: + - technical + expires: never + send_in_pings: + - crash + + remote_type: + type: string + description: > + Type of the child process, can be set to "web", "file" or "extension" but could also be unavailable. + notification_emails: + - crash-reporting-wg@mozilla.org + - stability@mozilla.org + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1851518 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1851518#c6 + data_sensitivity: + - technical + expires: never + send_in_pings: + - crash + + time: + type: datetime + time_unit: minute + description: > + The time at which the crash occurred. + notification_emails: + - crash-reporting-wg@mozilla.org + - stability@mozilla.org + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569#c12 + data_sensitivity: + - technical + expires: never + send_in_pings: + - crash + + startup: + type: boolean + description: > + If true, the crash occurred during process startup. + notification_emails: + - crash-reporting-wg@mozilla.org + - stability@mozilla.org + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569#c12 + data_sensitivity: + - technical + expires: never + send_in_pings: + - crash + + cause: + type: string + description: > + The cause of the crash. May be one of `os_fault` or `java_exception`. + notification_emails: + - crash-reporting-wg@mozilla.org + - stability@mozilla.org + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1839697 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1839697#c5 + data_sensitivity: + - technical + expires: never + send_in_pings: + - crash diff --git a/mobile/android/android-components/components/lib/crash/pings.yaml b/mobile/android/android-components/components/lib/crash/pings.yaml new file mode 100644 index 0000000000..620e185872 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/pings.yaml @@ -0,0 +1,28 @@ +# 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/. + +--- +$schema: moz://mozilla.org/schemas/glean/pings/2-0-0 + +crash: + description: > + A ping to report crash information. This information is sent as soon as + possible after a crash occurs (whether the crash is a background/content + process or the main process). It is expected to be used for crash report + analysis and to reduce blind spots in crash reporting. + include_client_id: true + send_if_empty: false + notification_emails: + - crash-reporting-wg@mozilla.org + - stability@mozilla.org + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1790569#c12 + reasons: + crash: > + A process crashed and a ping was immediately sent. + event_found: > + A process crashed and produced a crash event, which was later found and + sent in a ping. diff --git a/mobile/android/android-components/components/lib/crash/proguard-rules.pro b/mobile/android/android-components/components/lib/crash/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/android-components/components/lib/crash/schemas/mozilla.components.lib.crash.db.CrashDatabase/1.json b/mobile/android/android-components/components/lib/crash/schemas/mozilla.components.lib.crash.db.CrashDatabase/1.json new file mode 100644 index 0000000000..7ecfe0bbd3 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/schemas/mozilla.components.lib.crash.db.CrashDatabase/1.json @@ -0,0 +1,84 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "212dfa0b59d6a78d81e65cead34d40e0", + "entities": [ + { + "tableName": "crashes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `stacktrace` TEXT NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`uuid`))", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stacktrace", + "columnName": "stacktrace", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "uuid" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "reports", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `crash_uuid` TEXT NOT NULL, `service_id` TEXT NOT NULL, `report_id` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "crashUuid", + "columnName": "crash_uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "reportId", + "columnName": "report_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '212dfa0b59d6a78d81e65cead34d40e0')" + ] + } +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/crash/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..3e5c8f7da1 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/Crash.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/Crash.kt new file mode 100644 index 0000000000..db064e7a6c --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/Crash.kt @@ -0,0 +1,175 @@ +/* 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 + +import android.content.Intent +import android.os.Bundle +import androidx.annotation.StringDef +import mozilla.components.concept.base.crash.Breadcrumb +import mozilla.components.support.utils.ext.getParcelableArrayListCompat +import mozilla.components.support.utils.ext.getSerializableCompat +import java.io.Serializable +import java.util.UUID + +// Intent extra used to store crash data under when passing crashes in Intent objects +private const val INTENT_CRASH = "mozilla.components.lib.crash.CRASH" + +// Uncaught exception crash intent extras +private const val INTENT_EXCEPTION = "exception" + +// Breadcrumbs intent extras +private const val INTENT_BREADCRUMBS = "breadcrumbs" + +// Crash timestamp intent extras +private const val INTENT_CRASH_TIMESTAMP = "crashTimestamp" + +// Native code crash intent extras (Mirroring GeckoView values) +private const val INTENT_UUID = "uuid" +private const val INTENT_MINIDUMP_PATH = "minidumpPath" +private const val INTENT_EXTRAS_PATH = "extrasPath" +private const val INTENT_MINIDUMP_SUCCESS = "minidumpSuccess" +private const val INTENT_PROCESS_TYPE = "processType" +private const val INTENT_REMOTE_TYPE = "remoteType" + +/** + * Crash types that are handled by this library. + */ +sealed class Crash { + /** + * Unique ID identifying this crash. + */ + abstract val uuid: String + + /** + * A crash caused by an uncaught exception. + * + * @property timestamp Time of when the crash happened. + * @property throwable The [Throwable] that caused the crash. + * @property breadcrumbs List of breadcrumbs to send with the crash report. + */ + data class UncaughtExceptionCrash( + val timestamp: Long, + val throwable: Throwable, + val breadcrumbs: ArrayList, + override val uuid: String = UUID.randomUUID().toString(), + ) : Crash() { + override fun toBundle() = Bundle().apply { + putString(INTENT_UUID, uuid) + putSerializable(INTENT_EXCEPTION, throwable as Serializable) + putLong(INTENT_CRASH_TIMESTAMP, timestamp) + putParcelableArrayList(INTENT_BREADCRUMBS, breadcrumbs) + } + + companion object { + internal fun fromBundle(bundle: Bundle) = UncaughtExceptionCrash( + uuid = bundle.getString(INTENT_UUID) as String, + throwable = bundle.getSerializableCompat(INTENT_EXCEPTION, Throwable::class.java) as Throwable, + breadcrumbs = bundle.getParcelableArrayListCompat(INTENT_BREADCRUMBS, Breadcrumb::class.java) + ?: arrayListOf(), + timestamp = bundle.getLong(INTENT_CRASH_TIMESTAMP, System.currentTimeMillis()), + ) + } + } + + /** + * A crash that happened in native code. + * + * @property timestamp Time of when the crash happened. + * @property minidumpPath Path to a Breakpad minidump file containing information about the crash. + * @property minidumpSuccess Indicating whether or not the crash dump was successfully retrieved. If this is false, + * the dump file may be corrupted or incomplete. + * @property extrasPath Path to a file containing extra metadata about the crash. The file contains key-value pairs + * in the form `Key=Value`. Be aware, it may contain sensitive data such as the URI that was + * loaded at the time of the crash. + * @property processType The type of process the crash occurred in. Affects whether or not the crash is fatal + * or whether the application can recover from it. + * @property breadcrumbs List of breadcrumbs to send with the crash report. + * @property remoteType The type of child process (when available). + */ + data class NativeCodeCrash( + val timestamp: Long, + val minidumpPath: String?, + val minidumpSuccess: Boolean, + val extrasPath: String?, + @ProcessType val processType: String?, + val breadcrumbs: ArrayList, + val remoteType: String?, + override val uuid: String = UUID.randomUUID().toString(), + ) : Crash() { + override fun toBundle() = Bundle().apply { + putString(INTENT_UUID, uuid) + putString(INTENT_MINIDUMP_PATH, minidumpPath) + putBoolean(INTENT_MINIDUMP_SUCCESS, minidumpSuccess) + putString(INTENT_EXTRAS_PATH, extrasPath) + putString(INTENT_PROCESS_TYPE, processType) + putLong(INTENT_CRASH_TIMESTAMP, timestamp) + putParcelableArrayList(INTENT_BREADCRUMBS, breadcrumbs) + putString(INTENT_REMOTE_TYPE, remoteType) + } + + /** + * Whether the crash was fatal or not: If true, the main application process was affected by + * the crash. If false, only an internal process used by Gecko has crashed and the application + * may be able to recover. + */ + val isFatal: Boolean + get() = processType == PROCESS_TYPE_MAIN + + companion object { + /** + * Indicates a crash occurred in the main process and is therefore fatal. + */ + const val PROCESS_TYPE_MAIN = "MAIN" + + /** + * Indicates a crash occurred in a foreground child process. The application may be + * able to recover from this crash, but it was likely noticeable to the user. + */ + const val PROCESS_TYPE_FOREGROUND_CHILD = "FOREGROUND_CHILD" + + /** + * Indicates a crash occurred in a background child process. This should have been + * recovered from automatically, and will have had minimal impact to the user, if any. + */ + const val PROCESS_TYPE_BACKGROUND_CHILD = "BACKGROUND_CHILD" + + @StringDef(PROCESS_TYPE_MAIN, PROCESS_TYPE_FOREGROUND_CHILD, PROCESS_TYPE_BACKGROUND_CHILD) + @Retention(AnnotationRetention.SOURCE) + annotation class ProcessType + + internal fun fromBundle(bundle: Bundle) = NativeCodeCrash( + uuid = bundle.getString(INTENT_UUID) ?: UUID.randomUUID().toString(), + minidumpPath = bundle.getString(INTENT_MINIDUMP_PATH, null), + minidumpSuccess = bundle.getBoolean(INTENT_MINIDUMP_SUCCESS, false), + extrasPath = bundle.getString(INTENT_EXTRAS_PATH, null), + processType = bundle.getString(INTENT_PROCESS_TYPE, PROCESS_TYPE_MAIN), + breadcrumbs = bundle.getParcelableArrayListCompat(INTENT_BREADCRUMBS, Breadcrumb::class.java) + ?: arrayListOf(), + remoteType = bundle.getString(INTENT_REMOTE_TYPE, null), + timestamp = bundle.getLong(INTENT_CRASH_TIMESTAMP, System.currentTimeMillis()), + ) + } + } + + internal abstract fun toBundle(): Bundle + + internal fun fillIn(intent: Intent) { + intent.putExtra(INTENT_CRASH, toBundle()) + } + + companion object { + fun fromIntent(intent: Intent): Crash { + val bundle = intent.getBundleExtra(INTENT_CRASH)!! + + return if (bundle.containsKey(INTENT_MINIDUMP_PATH)) { + NativeCodeCrash.fromBundle(bundle) + } else { + UncaughtExceptionCrash.fromBundle(bundle) + } + } + + fun isCrashIntent(intent: Intent) = intent.extras?.containsKey(INTENT_CRASH) ?: false + } +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/CrashReporter.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/CrashReporter.kt new file mode 100644 index 0000000000..74daaef197 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/CrashReporter.kt @@ -0,0 +1,376 @@ +/* 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 + +import android.app.ActivityOptions +import android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.annotation.StyleRes +import androidx.annotation.VisibleForTesting +import androidx.core.content.ContextCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import mozilla.components.concept.base.crash.Breadcrumb +import mozilla.components.concept.base.crash.CrashReporting +import mozilla.components.lib.crash.db.CrashDatabase +import mozilla.components.lib.crash.db.insertCrashSafely +import mozilla.components.lib.crash.db.insertReportSafely +import mozilla.components.lib.crash.db.toEntity +import mozilla.components.lib.crash.db.toReportEntity +import mozilla.components.lib.crash.handler.ExceptionHandler +import mozilla.components.lib.crash.notification.CrashNotification +import mozilla.components.lib.crash.prompt.CrashPrompt +import mozilla.components.lib.crash.service.CrashReporterService +import mozilla.components.lib.crash.service.CrashTelemetryService +import mozilla.components.lib.crash.service.SendCrashReportService +import mozilla.components.lib.crash.service.SendCrashTelemetryService +import mozilla.components.support.base.android.NotificationsDelegate +import mozilla.components.support.base.log.logger.Logger + +/** + * Stores a list of `Breadcrumb` objects for the crash reporter. + * + * This is shared between multiple threads and needs to be thread-safe. + */ +private class BreadcrumbList(val maxBreadCrumbs: Int) { + private val breadcrumbs = ArrayDeque() + + @Synchronized + internal fun copy(): ArrayList { + return ArrayList(breadcrumbs) + } + + @Synchronized + internal fun add(breadcrumb: Breadcrumb) { + if (breadcrumbs.size >= maxBreadCrumbs) { + breadcrumbs.removeFirst() + } + breadcrumbs.add(breadcrumb) + } +} + +/** + * + * A generic crash reporter that can report crashes to multiple services. + * + * In the `onCreate()` method of your Application class create a `CrashReporter` instance and call `install()`: + * + * ```Kotlin + * CrashReporter( + * services = listOf( + * // List the crash reporting services you want to use + * ) + * ).install(this) + * ``` + * + * With this minimal setup the crash reporting library will capture "uncaught exception" crashes and "native code" + * crashes and forward them to the configured crash reporting services. + * + * @property enabled Enable/Disable crash reporting. + * + * @param services List of crash reporting services that should receive crash reports. + * @param telemetryServices List of telemetry crash reporting services that should receive crash reports. + * @param shouldPrompt Whether or not the user should be prompted to confirm sending crash reports. + * @param enabled Enable/Disable crash reporting. + * @param promptConfiguration Configuration for customizing the crash reporter prompt. + * @param nonFatalCrashIntent A [PendingIntent] that will be launched if a non fatal crash (main process not affected) + * happened. This gives the app the opportunity to show an in-app confirmation UI before + * sending a crash report. See component README for details. + */ +class CrashReporter( + context: Context, + private val services: List = emptyList(), + private val telemetryServices: List = emptyList(), + private val shouldPrompt: Prompt = Prompt.NEVER, + var enabled: Boolean = true, + internal val promptConfiguration: PromptConfiguration = PromptConfiguration(), + private val nonFatalCrashIntent: PendingIntent? = null, + private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO), + private val maxBreadCrumbs: Int = 30, + private val notificationsDelegate: NotificationsDelegate, +) : CrashReporting { + private val database: CrashDatabase by lazy { CrashDatabase.get(context) } + + internal val logger = Logger("mozac/CrashReporter") + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + private val crashBreadcrumbs = BreadcrumbList(maxBreadCrumbs) + + init { + if (services.isEmpty() and telemetryServices.isEmpty()) { + throw IllegalArgumentException("No crash reporter services defined") + } + } + + /** + * Install this [CrashReporter] instance. At this point the component will be setup to collect crash reports. + */ + fun install(applicationContext: Context): CrashReporter { + instance = this + + val defaultHandler = Thread.getDefaultUncaughtExceptionHandler() + val handler = ExceptionHandler(applicationContext, this, defaultHandler) + Thread.setDefaultUncaughtExceptionHandler(handler) + + return this + } + + /** + * Get a copy of the crashBreadcrumbs + */ + fun crashBreadcrumbsCopy(): ArrayList { + return crashBreadcrumbs.copy() + } + + /** + * Submit a crash report to all registered services. + */ + fun submitReport(crash: Crash, then: () -> Unit = {}): Job { + return scope.launch { + services.forEach { service -> + val reportId = when (crash) { + is Crash.NativeCodeCrash -> service.report(crash) + is Crash.UncaughtExceptionCrash -> service.report(crash) + } + + if (reportId != null) { + database.crashDao().insertReportSafely(service.toReportEntity(crash, reportId)) + } + + val reportUrl = reportId?.let { service.createCrashReportUrl(it) } + + logger.info("Submitted crash to ${service.name} (id=$reportId, url=$reportUrl)") + } + + logger.info("Crash report submitted to ${services.size} services") + withContext(Dispatchers.Main) { + then() + } + } + } + + /** + * Submit a crash report to all registered telemetry services. + */ + fun submitCrashTelemetry(crash: Crash, then: () -> Unit = {}): Job { + return scope.launch { + telemetryServices.forEach { telemetryService -> + when (crash) { + is Crash.NativeCodeCrash -> telemetryService.record(crash) + is Crash.UncaughtExceptionCrash -> telemetryService.record(crash) + } + } + + logger.info("Crash report submitted to ${telemetryServices.size} telemetry services") + withContext(Dispatchers.Main) { + then() + } + } + } + + /** + * Submit a caught exception report to all registered services. + */ + override fun submitCaughtException(throwable: Throwable): Job { + /* + * if stacktrace is empty, replace throwable with UnexpectedlyMissingStacktrace exception so + * we can figure out which module is submitting caught exception reports without a stacktrace. + */ + var reportThrowable = throwable + if (throwable.stackTrace.isEmpty()) { + reportThrowable = CrashReporterException.UnexpectedlyMissingStacktrace("Missing Stacktrace", throwable) + } + + logger.info("Caught Exception report submitted to ${services.size} services") + return scope.launch { + services.forEach { + it.report(reportThrowable, crashBreadcrumbsCopy()) + } + } + } + + /** + * Add a crash breadcrumb to all registered services with breadcrumb support. + * + * ```Kotlin + * crashReporter.recordCrashBreadcrumb( + * Breadcrumb("Settings button clicked", data, "UI", Level.INFO, Type.USER) + * ) + * ``` + */ + override fun recordCrashBreadcrumb(breadcrumb: Breadcrumb) { + crashBreadcrumbs.add(breadcrumb) + } + + internal fun onCrash(context: Context, crash: Crash) { + if (!enabled) { + return + } + + logger.info("Received crash: $crash") + + database.crashDao().insertCrashSafely(crash.toEntity()) + + if (telemetryServices.isNotEmpty()) { + sendCrashTelemetry(context, crash) + } + + // If crash is native code and non fatal then the view will handle the user prompt + if (shouldSendIntent(crash)) { + // App has registered a pending intent + sendNonFatalCrashIntent(context, crash) + return + } + + if (services.isNotEmpty()) { + if (CrashPrompt.shouldPromptForCrash(shouldPrompt, crash)) { + showPromptOrNotification(context, crash) + } else { + sendCrashReport(context, crash) + } + } + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun sendNonFatalCrashIntent(context: Context, crash: Crash) { + logger.info("Invoking non-fatal PendingIntent") + + val additionalIntent = Intent() + crash.fillIn(additionalIntent) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + val onFinished = null + val handler = null + val requiredPermission = null + val activityOptions = ActivityOptions.makeBasic() + activityOptions.pendingIntentBackgroundActivityStartMode = + MODE_BACKGROUND_ACTIVITY_START_ALLOWED + + nonFatalCrashIntent?.send( + context, + 0, + additionalIntent, + onFinished, + handler, + requiredPermission, + activityOptions.toBundle(), + ) + } else { + nonFatalCrashIntent?.send(context, 0, additionalIntent) + } + } + + private fun showPromptOrNotification(context: Context, crash: Crash) { + if (services.isEmpty()) { + return + } + + if (CrashNotification.shouldShowNotificationInsteadOfPrompt(crash)) { + // If this is a fatal crash taking down the app then we may not be able to show a crash reporter + // prompt on Android Q+. Unfortunately it's not possible to easily determine if we can launch an + // activity here. So instead we fallback to just showing a notification + // https://developer.android.com/preview/privacy/background-activity-starts + logger.info("Showing notification") + val notification = CrashNotification(context, crash, promptConfiguration, notificationsDelegate) + notification.show() + } else { + logger.info("Showing prompt") + showPrompt(context, crash) + } + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun sendCrashReport(context: Context, crash: Crash) { + ContextCompat.startForegroundService(context, SendCrashReportService.createReportIntent(context, crash)) + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun sendCrashTelemetry(context: Context, crash: Crash) { + ContextCompat.startForegroundService(context, SendCrashTelemetryService.createReportIntent(context, crash)) + } + + @VisibleForTesting + internal fun showPrompt(context: Context, crash: Crash) { + val prompt = CrashPrompt(context, crash) + prompt.show() + } + + private fun shouldSendIntent(crash: Crash): Boolean { + return if (nonFatalCrashIntent == null) { + // If the app has not registered any intent then we can't send one. + false + } else { + // If this is a native code crash in a foreground child process then we can recover + // and can notify the app. Background child process crashes will be recovered from + // automatically, and main process crashes cannot be recovered from, so we do not + // send the intent for those. + crash is Crash.NativeCodeCrash && crash.processType == Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD + } + } + + internal fun getCrashReporterServiceById(id: String): CrashReporterService? { + return services.firstOrNull { it.id == id } + } + + enum class Prompt { + /** + * Never prompt the user. Always submit crash reports immediately. + */ + NEVER, + + /** + * Only prompt the user for native code crashes. + */ + ONLY_NATIVE_CRASH, + + /** + * Always prompt the user for confirmation before sending crash reports. + */ + ALWAYS, + } + + /** + * Configuration for the crash reporter prompt. + */ + data class PromptConfiguration( + internal val appName: String = "App", + internal val organizationName: String = "Mozilla", + internal val message: String? = null, + @StyleRes internal val theme: Int = R.style.Theme_Mozac_CrashReporter, + ) + + companion object { + @Volatile + private var instance: CrashReporter? = null + + @VisibleForTesting + internal fun reset() { + instance = null + } + + internal val requireInstance: CrashReporter + get() = instance ?: throw IllegalStateException( + "You need to call install() on your CrashReporter instance from Application.onCreate().", + ) + } +} + +/** + * A base class for exceptions describing crash reporter exception. + */ +internal abstract class CrashReporterException(message: String, cause: Throwable?) : Exception(message, cause) { + /** + * Stacktrace was expected to be present, but it wasn't. + */ + internal class UnexpectedlyMissingStacktrace( + message: String, + cause: Throwable?, + ) : CrashReporterException(message, cause) +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashDao.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashDao.kt new file mode 100644 index 0000000000..48dcf7aefe --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashDao.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.db + +import android.annotation.SuppressLint +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Transaction +import java.lang.Exception + +/** + * Dao for saving and accessing crash related information. + */ +@Dao +internal interface CrashDao { + /** + * Inserts a crash into the database. + */ + @Insert + fun insertCrash(crash: CrashEntity): Long + + /** + * Inserts a report to the database. + */ + @Insert + fun insertReport(report: ReportEntity): Long + + /** + * Returns saved crashes with their reports. + */ + @Transaction + @Query("SELECT * FROM crashes ORDER BY created_at DESC") + fun getCrashesWithReports(): LiveData> + + /** + * Delete table. + */ + @Transaction + @Query("DELETE FROM crashes") + fun deleteAll() +} + +/** + * Insert crash into database safely, ignoring any exceptions. + * + * When handling a crash we want to avoid causing another crash when writing to the database. In the + * case of an error we will just ignore it and continue without saving to the database. + */ +@SuppressLint("LogUsage") // We do not want to use our custom logger while handling the crash +@Suppress("TooGenericExceptionCaught") +internal fun CrashDao.insertCrashSafely(entity: CrashEntity) { + try { + insertCrash(entity) + } catch (e: Exception) { + Log.e("CrashDao", "Failed to insert crash into database", e) + } +} + +/** + * Insert report into database safely, ignoring any exceptions. + * + * When handling a crash we want to avoid causing another crash when writing to the database. In the + * case of an error we will just ignore it and continue without saving to the database. + */ +@SuppressLint("LogUsage") // We do not want to use our custom logger while handling the crash +@Suppress("TooGenericExceptionCaught") +internal fun CrashDao.insertReportSafely(entity: ReportEntity) { + try { + insertReport(entity) + } catch (e: Exception) { + Log.e("CrashDao", "Failed to insert report into database", e) + } +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashDatabase.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashDatabase.kt new file mode 100644 index 0000000000..2a7ac4e4e3 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashDatabase.kt @@ -0,0 +1,40 @@ +/* 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.db + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +/** + * Internal database for storing collections and their tabs. + */ +@Database( + entities = [CrashEntity::class, ReportEntity::class], + version = 1, +) +internal abstract class CrashDatabase : RoomDatabase() { + abstract fun crashDao(): CrashDao + + companion object { + @Volatile private var instance: CrashDatabase? = null + + @Synchronized + fun get(context: Context): CrashDatabase { + instance?.let { return it } + + return Room.databaseBuilder(context.applicationContext, CrashDatabase::class.java, "crashes") + // We are allowing main thread queries here since we need to write to disk blocking + // in a crash event before the process gets shutdown. At this point the app already + // crashed and temporarily blocking the UI thread is not that problematic anymore. + .allowMainThreadQueries() + .build() + .also { + instance = it + } + } + } +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashEntity.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashEntity.kt new file mode 100644 index 0000000000..26ba0b0991 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashEntity.kt @@ -0,0 +1,54 @@ +/* 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.db + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import mozilla.components.lib.crash.Crash +import mozilla.components.support.base.ext.getStacktraceAsString + +/** + * Database entity modeling a crash that has happened. + */ +@Entity( + tableName = "crashes", +) +internal data class CrashEntity( + /** + * Generated UUID for this crash. + */ + @PrimaryKey + @ColumnInfo(name = "uuid") + var uuid: String, + + /** + * The stacktrace of the crash (if this crash was caused by an exception/throwable): otherwise + * a string describing the type of crash. + */ + @ColumnInfo(name = "stacktrace") + var stacktrace: String, + + /** + * Timestamp (in milliseconds) of when the crash happened. + */ + @ColumnInfo(name = "created_at") + var createdAt: Long, +) + +internal fun Crash.toEntity(): CrashEntity { + return CrashEntity( + uuid = uuid, + stacktrace = getStacktrace(), + createdAt = System.currentTimeMillis(), + ) +} + +internal fun Crash.getStacktrace(): String { + return when (this) { + is Crash.NativeCodeCrash -> "" + is Crash.UncaughtExceptionCrash -> throwable.getStacktraceAsString() + } +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashWithReports.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashWithReports.kt new file mode 100644 index 0000000000..079b283168 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashWithReports.kt @@ -0,0 +1,22 @@ +/* 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.db + +import androidx.room.Embedded +import androidx.room.Relation + +/** + * Data class modelling the relationship between [CrashEntity] and [ReportEntity] objects. + */ +internal data class CrashWithReports( + @Embedded + val crash: CrashEntity, + + @Relation( + parentColumn = "uuid", + entityColumn = "crash_uuid", + ) + val reports: List, +) diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/ReportEntity.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/ReportEntity.kt new file mode 100644 index 0000000000..5136a8526e --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/ReportEntity.kt @@ -0,0 +1,52 @@ +/* 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.db + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import mozilla.components.lib.crash.Crash +import mozilla.components.lib.crash.service.CrashReporterService + +/** + * Datanase entry describing a crash report that was sent to a crash reporting service. + */ +@Entity( + tableName = "reports", +) +internal data class ReportEntity( + /** + * Database internal primary key of the entry. + */ + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "id") + var id: Long? = null, + + /** + * UUID of the crash that was reported. + */ + @ColumnInfo(name = "crash_uuid") + var crashUuid: String, + + /** + * Id of the service the crash was reported to (matching [CrashReporterService.id]. + */ + @ColumnInfo(name = "service_id") + var serviceId: String, + + /** + * The id of the crash report as returned by [CrashReporterService.report]. + */ + @ColumnInfo(name = "report_id") + var reportId: String, +) + +internal fun CrashReporterService.toReportEntity(crash: Crash, reportId: String): ReportEntity { + return ReportEntity( + crashUuid = crash.uuid, + serviceId = id, + reportId = reportId, + ) +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/handler/CrashHandlerService.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/handler/CrashHandlerService.kt new file mode 100644 index 0000000000..2c21298412 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/handler/CrashHandlerService.kt @@ -0,0 +1,76 @@ +/* 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.handler + +import android.app.Service +import android.content.Intent +import android.os.Build +import android.os.IBinder +import androidx.annotation.VisibleForTesting +import androidx.annotation.VisibleForTesting.Companion.PRIVATE +import androidx.core.app.NotificationCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +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.support.base.ids.SharedIdsHelper + +private const val NOTIFICATION_TAG = "mozac.lib.crash.handlecrash" + +/** + * Service receiving native code crashes (from GeckoView). + */ +class CrashHandlerService : Service() { + private val crashReporter: CrashReporter by lazy { CrashReporter.requireInstance } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent != null) { + crashReporter.logger.error("CrashHandlerService received native code crash") + handleCrashIntent(intent) + } else { + crashReporter.logger.error("CrashHandlerService received a null intent unable to handle") + } + return START_NOT_STICKY + } + + override fun onBind(intent: Intent): IBinder? { + // We don't provide binding, so return null + return null + } + + @VisibleForTesting(otherwise = PRIVATE) + internal fun handleCrashIntent( + intent: Intent, + scope: CoroutineScope = CoroutineScope(Dispatchers.IO), + ) { + 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_gathering_crash_data_in_progress), + ) + .setSmallIcon(R.drawable.mozac_lib_crash_notification) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setCategory(NotificationCompat.CATEGORY_ERROR) + .setAutoCancel(true) + .build() + + val notificationId = SharedIdsHelper.getIdForTag(this, NOTIFICATION_TAG) + startForeground(notificationId, notification) + } + + scope.launch { + intent.extras?.let { extras -> + val crash = Crash.NativeCodeCrash.fromBundle(extras) + CrashReporter.requireInstance.onCrash(this@CrashHandlerService, crash) + } ?: crashReporter.logger.error("Received intent with null extras") + + stopSelf() + } + } +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/handler/ExceptionHandler.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/handler/ExceptionHandler.kt new file mode 100644 index 0000000000..1c764da8fe --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/handler/ExceptionHandler.kt @@ -0,0 +1,60 @@ +/* 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.handler + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Process +import android.util.Log +import mozilla.components.lib.crash.Crash +import mozilla.components.lib.crash.CrashReporter + +private const val TAG = "ExceptionHandler" + +/** + * [Thread.UncaughtExceptionHandler] implementation that forwards crashes to the [CrashReporter] instance. + */ +class ExceptionHandler( + private val context: Context, + private val crashReporter: CrashReporter, + private val defaultExceptionHandler: Thread.UncaughtExceptionHandler? = null, +) : Thread.UncaughtExceptionHandler { + private var crashing = false + + @SuppressLint("LogUsage") // We do not want to use our custom logger while handling the crash + override fun uncaughtException(thread: Thread, throwable: Throwable) { + Log.e(TAG, "Uncaught exception handled: ", throwable) + + if (crashing) { + return + } + + // We want to catch and log all exceptions that can take down the crash reporter. + // This is the best we can do without being able to report it. + @Suppress("TooGenericExceptionCaught") + try { + crashing = true + + crashReporter.onCrash( + context, + Crash.UncaughtExceptionCrash( + timestamp = System.currentTimeMillis(), + throwable = throwable, + breadcrumbs = crashReporter.crashBreadcrumbsCopy(), + ), + ) + + defaultExceptionHandler?.uncaughtException(thread, throwable) + } catch (e: Exception) { + Log.e(TAG, "Crash reporter has crashed.", e) + } finally { + terminateProcess() + } + } + + private fun terminateProcess() { + Process.killProcess(Process.myPid()) + } +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/notification/CrashNotification.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/notification/CrashNotification.kt new file mode 100644 index 0000000000..73b4c0c789 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/notification/CrashNotification.kt @@ -0,0 +1,121 @@ +/* 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.notification + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.os.Build +import androidx.core.app.NotificationCompat +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.support.base.android.NotificationsDelegate +import mozilla.components.support.base.ids.SharedIdsHelper +import mozilla.components.support.utils.PendingIntentUtils + +private const val NOTIFICATION_SDK_LEVEL = 29 // On Android Q+ we show a notification instead of a prompt + +internal const val NOTIFICATION_TAG = "mozac.lib.crash.notification" +internal const val NOTIFICATION_ID = 1 +private const val NOTIFICATION_CHANNEL_ID = "mozac.lib.crash.channel" +private const val PENDING_INTENT_TAG = "mozac.lib.crash.pendingintent" + +internal class CrashNotification( + private val context: Context, + private val crash: Crash, + private val configuration: CrashReporter.PromptConfiguration, + private val notificationsDelegate: NotificationsDelegate, +) { + fun show() { + val pendingIntent = PendingIntent.getActivity( + context, + SharedIdsHelper.getNextIdForTag(context, PENDING_INTENT_TAG), + CrashPrompt.createIntent(context, crash), + getNotificationFlag(), + ) + + val channel = ensureChannelExists(context) + + val title = if (crash is Crash.NativeCodeCrash && + crash.processType == Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD + ) { + context.getString( + R.string.mozac_lib_crash_background_process_notification_title, + configuration.appName, + ) + } else { + context.getString(R.string.mozac_lib_crash_dialog_title, configuration.appName) + } + + val notification = NotificationCompat.Builder(context, channel) + .setContentTitle(title) + .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, + ), + pendingIntent, + ) + .setAutoCancel(true) + .build() + + notificationsDelegate.notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification) + } + + companion object { + /** + * Whether to show a notification instead of a prompt (activity). Android introduced restrictions on background + * services launching activities in Q+. On those system we may need to show a notification for the given [crash] + * and launch the reporter from the notification. + */ + fun shouldShowNotificationInsteadOfPrompt( + crash: Crash, + sdkLevel: Int = Build.VERSION.SDK_INT, + ): Boolean { + return when { + // We can always launch an activity from a background service pre Android Q. + sdkLevel < NOTIFICATION_SDK_LEVEL -> false + + // We may not be able to launch an activity if a background process crash occurs + // while the application is in the background. + crash is Crash.NativeCodeCrash && crash.processType == + Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD -> true + + // An uncaught exception is crashing the app and we may not be able to launch an activity from here. + crash is Crash.UncaughtExceptionCrash -> true + + // This is a fatal native crash. We may not be able to launch an activity from here. + 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 + } + } + + private fun getNotificationFlag() = PendingIntentUtils.defaultFlags +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/prompt/CrashPrompt.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/prompt/CrashPrompt.kt new file mode 100644 index 0000000000..dcdf2a7a2e --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/prompt/CrashPrompt.kt @@ -0,0 +1,48 @@ +/* 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.prompt + +import android.content.Context +import android.content.Intent +import mozilla.components.lib.crash.Crash +import mozilla.components.lib.crash.CrashReporter + +internal class CrashPrompt( + private val context: Context, + private val crash: Crash, +) { + fun show() { + context.startActivity(createIntent(context, crash)) + } + + companion object { + fun createIntent(context: Context, crash: Crash): Intent { + val intent = Intent(context, CrashReporterActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + // For background process native crashes we want to keep the browser visible in the + // background behind the prompt. For other types we want to clear the existing task. + if (crash is Crash.NativeCodeCrash && + crash.processType == Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD + ) { + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + } else { + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + } + + crash.fillIn(intent) + + return intent + } + + fun shouldPromptForCrash(shouldPrompt: CrashReporter.Prompt, crash: Crash): Boolean { + return when (shouldPrompt) { + CrashReporter.Prompt.ALWAYS -> true + CrashReporter.Prompt.NEVER -> false + CrashReporter.Prompt.ONLY_NATIVE_CRASH -> crash is Crash.NativeCodeCrash + } + } + } +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/prompt/CrashReporterActivity.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/prompt/CrashReporterActivity.kt new file mode 100644 index 0000000000..fad866f139 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/prompt/CrashReporterActivity.kt @@ -0,0 +1,158 @@ +/* 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.prompt + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.annotation.VisibleForTesting.Companion.PRIVATE +import androidx.appcompat.app.AppCompatActivity +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.app.NotificationManagerCompat +import mozilla.components.lib.crash.Crash +import mozilla.components.lib.crash.CrashReporter +import mozilla.components.lib.crash.R +import mozilla.components.lib.crash.databinding.MozacLibCrashCrashreporterBinding +import mozilla.components.lib.crash.notification.CrashNotification +import mozilla.components.lib.crash.notification.NOTIFICATION_ID +import mozilla.components.lib.crash.notification.NOTIFICATION_TAG +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +/** + * Activity showing the crash reporter prompt asking the user for confirmation before submitting a crash report. + */ +class CrashReporterActivity : AppCompatActivity() { + + private val crashReporter: CrashReporter by lazy { CrashReporter.requireInstance } + private val crash: Crash by lazy { Crash.fromIntent(intent) } + private val sharedPreferences by lazy { + getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) + } + + /** + * Coroutine context for crash reporter operations. Can be used to setup dispatcher for tests. + */ + @VisibleForTesting(otherwise = PRIVATE) + internal var reporterCoroutineContext: CoroutineContext = EmptyCoroutineContext + + @VisibleForTesting(otherwise = PRIVATE) + internal lateinit var binding: MozacLibCrashCrashreporterBinding + + override fun onCreate(savedInstanceState: Bundle?) { + // if the activity is started by user tapping on the crash notification's report button, + // remove the crash notification. + if (CrashNotification.shouldShowNotificationInsteadOfPrompt(crash)) { + NotificationManagerCompat.from(applicationContext).cancel(NOTIFICATION_TAG, NOTIFICATION_ID) + } + + setTheme(crashReporter.promptConfiguration.theme) + + super.onCreate(savedInstanceState) + + binding = MozacLibCrashCrashreporterBinding.inflate(layoutInflater) + setContentView(binding.root) + + setupViews() + } + + private fun setupViews() { + val appName = crashReporter.promptConfiguration.appName + val organizationName = crashReporter.promptConfiguration.organizationName + + binding.titleView.text = when (isRecoverableBackgroundCrash(crash)) { + true -> getString( + R.string.mozac_lib_crash_background_process_notification_title, + appName, + ) + false -> getString(R.string.mozac_lib_crash_dialog_title, appName) + } + + binding.sendCheckbox.text = getString(R.string.mozac_lib_crash_dialog_checkbox, organizationName) + binding.sendCheckbox.isChecked = sharedPreferences.getBoolean(PREFERENCE_KEY_SEND_REPORT, true) + + binding.restartButton.apply { + text = getString(R.string.mozac_lib_crash_dialog_button_restart, appName) + setOnClickListener { restart() } + } + binding.closeButton.setOnClickListener { close() } + + // For background crashes show just the close button. Otherwise show close and restart. + if (isRecoverableBackgroundCrash(crash)) { + binding.restartButton.visibility = View.GONE + val closeButtonParams = binding.closeButton.layoutParams as ConstraintLayout.LayoutParams + closeButtonParams.startToStart = ConstraintLayout.LayoutParams.UNSET + closeButtonParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID + } else { + binding.restartButton.visibility = View.VISIBLE + val closeButtonParams = binding.closeButton.layoutParams as ConstraintLayout.LayoutParams + closeButtonParams.startToStart = ConstraintLayout.LayoutParams.PARENT_ID + closeButtonParams.endToEnd = ConstraintLayout.LayoutParams.UNSET + } + + if (crashReporter.promptConfiguration.message == null) { + binding.messageView.visibility = View.GONE + } else { + binding.messageView.text = crashReporter.promptConfiguration.message + } + } + + private fun close() { + sendCrashReportIfNeeded { + finish() + } + } + + private fun restart() { + sendCrashReportIfNeeded { + val launchIntent = packageManager.getLaunchIntentForPackage(packageName) + if (launchIntent != null) { + launchIntent.flags = launchIntent.flags or Intent.FLAG_ACTIVITY_NEW_TASK + startActivity(launchIntent) + } + + finish() + } + } + + private fun sendCrashReportIfNeeded(then: () -> Unit) { + sharedPreferences.edit().putBoolean(PREFERENCE_KEY_SEND_REPORT, binding.sendCheckbox.isChecked).apply() + + if (!binding.sendCheckbox.isChecked) { + then() + return + } + + crashReporter.submitReport(crash) { + then() + } + } + + override fun onBackPressed() { + sendCrashReportIfNeeded { + finish() + } + } + + /* + * Return true if the crash occurred in the background and is recoverable. (ex: GPU process crash) + */ + @VisibleForTesting + internal fun isRecoverableBackgroundCrash(crash: Crash): Boolean { + return crash is Crash.NativeCodeCrash && + crash.processType == Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD + } + + companion object { + + @VisibleForTesting(otherwise = PRIVATE) + internal const val SHARED_PREFERENCES_NAME = "mozac_lib_crash_settings" + + @VisibleForTesting(otherwise = PRIVATE) + internal const val PREFERENCE_KEY_SEND_REPORT = "sendCrashReport" + } +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashReporterService.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashReporterService.kt new file mode 100644 index 0000000000..698d33128b --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashReporterService.kt @@ -0,0 +1,54 @@ +/* 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 mozilla.components.concept.base.crash.Breadcrumb +import mozilla.components.lib.crash.Crash + +const val LIB_CRASH_INFO_PREFIX = "[INFO]" + +/** + * Interface to be implemented by external services that accept crash reports. + */ +interface CrashReporterService { + /** + * A unique ID to identify this crash reporter service. + */ + val id: String + + /** + * A human-readable name for this crash reporter service (to be displayed in UI). + */ + val name: String + + /** + * Returns a URL to a website with the crash report if possible. Otherwise returns null. + */ + fun createCrashReportUrl(identifier: String): String? + + /** + * Submits a crash report for this [Crash.UncaughtExceptionCrash]. + * + * @return Unique crash report identifier that can be used by/with this crash reporter service + * to find this reported crash - or null if no identifier can be provided. + */ + fun report(crash: Crash.UncaughtExceptionCrash): String? + + /** + * Submits a crash report for this [Crash.NativeCodeCrash]. + * + * @return Unique crash report identifier that can be used by/with this crash reporter service + * to find this reported crash - or null if no identifier can be provided. + */ + fun report(crash: Crash.NativeCodeCrash): String? + + /** + * Submits a caught exception report for this [Throwable]. + * + * @return Unique crash report identifier that can be used by/with this crash reporter service + * to find this reported crash - or null if no identifier can be provided. + */ + fun report(throwable: Throwable, breadcrumbs: ArrayList): String? +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashTelemetryService.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashTelemetryService.kt new file mode 100644 index 0000000000..ce85fee75e --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashTelemetryService.kt @@ -0,0 +1,27 @@ +/* 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 mozilla.components.lib.crash.Crash + +/** + * Interface to be implemented by external services that collect telemetry about crash reports. + */ +interface CrashTelemetryService { + /** + * Records telemetry for this [Crash.UncaughtExceptionCrash]. + */ + fun record(crash: Crash.UncaughtExceptionCrash) + + /** + * Records telemetry for this [Crash.NativeCodeCrash]. + */ + fun record(crash: Crash.NativeCodeCrash) + + /** + * Records telemetry for this caught [Throwable] (non-crash). + */ + fun record(throwable: Throwable) +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/GleanCrashReporterService.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/GleanCrashReporterService.kt new file mode 100644 index 0000000000..be6b411816 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/GleanCrashReporterService.kt @@ -0,0 +1,312 @@ +/* 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.Context +import android.os.SystemClock +import androidx.annotation.VisibleForTesting +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.DecodeSequenceMode +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeToSequence +import kotlinx.serialization.json.encodeToStream +import mozilla.components.lib.crash.Crash +import mozilla.components.lib.crash.GleanMetrics.CrashMetrics +import mozilla.components.lib.crash.GleanMetrics.Pings +import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.ktx.android.content.isMainProcess +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.util.Date +import mozilla.components.lib.crash.GleanMetrics.Crash as GleanCrash + +/** + * A [CrashReporterService] implementation for recording metrics with Glean. The purpose of this + * crash reporter is to collect crash count metrics by capturing [Crash.UncaughtExceptionCrash], + * [Throwable] and [Crash.NativeCodeCrash] events and record to the respective + * [mozilla.components.service.glean.private.CounterMetricType]. + */ +class GleanCrashReporterService( + val context: Context, + @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal val file: File = File(context.applicationInfo.dataDir, CRASH_FILE_NAME), +) : CrashTelemetryService { + companion object { + // This file is stored in the application's data directory, so it should be located in the + // same location as the application. + // The format of this file is simple and uses the keys named below, one per line, to record + // crashes. That format allows for multiple crashes to be appended to the file if, for some + // reason, the application cannot run and record them. + const val CRASH_FILE_NAME = "glean_crash_counts" + + // These keys correspond to the labels found for crashCount metric in metrics.yaml as well + // as the persisted crashes in the crash count file (see above comment) + const val UNCAUGHT_EXCEPTION_KEY = "uncaught_exception" + const val CAUGHT_EXCEPTION_KEY = "caught_exception" + const val MAIN_PROCESS_NATIVE_CODE_CRASH_KEY = "main_proc_native_code_crash" + const val FOREGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY = "fg_proc_native_code_crash" + const val BACKGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY = "bg_proc_native_code_crash" + + // These keys are deprecated and should be removed after a period to allow for persisted + // crashes to be submitted. + const val FATAL_NATIVE_CODE_CRASH_KEY = "fatal_native_code_crash" + const val NONFATAL_NATIVE_CODE_CRASH_KEY = "nonfatal_native_code_crash" + } + + /** + * The subclasses of GleanCrashAction are used to persist Glean actions to handle them later + * (in the application which has Glean initialized). They are serialized to JSON objects and + * appended to a file, in case multiple crashes occur prior to being able to submit the metrics + * to Glean. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + @Serializable + internal sealed class GleanCrashAction { + /** + * Submit the glean metrics/pings. + */ + abstract fun submit() + + @Serializable + @SerialName("count") + data class Count(val label: String) : GleanCrashAction() { + override fun submit() { + CrashMetrics.crashCount[label].add() + } + } + + @Serializable + @SerialName("ping") + data class Ping( + val uptimeNanos: Long, + val processType: String, + val timeMillis: Long, + val startup: Boolean, + val reason: Pings.crashReasonCodes, + val cause: String = "os_fault", + val remoteType: String = "", + ) : GleanCrashAction() { + override fun submit() { + GleanCrash.uptime.setRawNanos(uptimeNanos) + GleanCrash.processType.set(processType) + GleanCrash.remoteType.set(remoteType) + GleanCrash.time.set(Date(timeMillis)) + GleanCrash.startup.set(startup) + GleanCrash.cause.set(cause) + Pings.crash.submit(reason) + } + } + } + + private val logger = Logger("glean/GleanCrashReporterService") + private val creationTime = SystemClock.elapsedRealtimeNanos() + + init { + run { + // We only want to record things on the main process because that is the only one in which + // Glean is properly initialized. Checking to see if we are on the main process here will + // prevent the situation that arises because the integrating app's Application will be + // re-created when prompting to report the crash, and Glean is not initialized there since + // it's not technically the main process. + if (!context.isMainProcess()) { + logger.info("GleanCrashReporterService initialized off of main process") + return@run + } + + if (!checkFileConditions()) { + // checkFileConditions() internally logs error conditions + return@run + } + + // Parse the persisted crashes + parseCrashFile() + + // Clear persisted counts by deleting the file + file.delete() + } + } + + /** + * Calculates the application uptime based on the creation time of this class (assuming it is + * created in the application's `OnCreate`). + */ + private fun uptime() = SystemClock.elapsedRealtimeNanos() - creationTime + + /** + * Checks the file conditions to ensure it can be opened and read. + * + * @return True if the file exists and is able to be read, otherwise false + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun checkFileConditions(): Boolean { + return if (!file.exists()) { + // This is just an info line, as most of the time we hope there is no file which means + // there were no crashes + logger.info("No crashes to record, or file not found.") + false + } else if (!file.canRead()) { + logger.error("Cannot read file") + false + } else if (!file.isFile) { + logger.error("Expected file, but found directory") + false + } else { + true + } + } + + /** + * Parses the crashes collected in the persisted crash file. The format of this file is simple, + * a stream of serialized JSON GleanCrashAction objects. + * + * Example: + * + * <--Beginning of file--> + * {"type":"count","label":"uncaught_exception"}\n + * {"type":"count","label":"uncaught_exception"}\n + * {"type":"count","label":"main_process_native_code_crash"}\n + * {"type":"ping","uptimeNanos":2000000,"processType":"main","timeMillis":42000000000, + * "startup":false}\n + * <--End of file--> + * + * It is unlikely that there will be more than one crash in a file, but not impossible. This + * could happen, for instance, if the application crashed again before the file could be + * processed. + */ + @Suppress("ComplexMethod") + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun parseCrashFile() { + try { + @OptIn(ExperimentalSerializationApi::class) + val actionSequence = Json.decodeToSequence( + file.inputStream(), + DecodeSequenceMode.WHITESPACE_SEPARATED, + ) + for (action in actionSequence) { + action.submit() + } + } catch (e: IOException) { + logger.error("Error reading crash file", e) + return + } catch (e: SerializationException) { + logger.error("Error deserializing crash file", e) + return + } + } + + /** + * This function handles the actual recording of the crash to the persisted crash file. We are + * only guaranteed runtime for the lifetime of the [CrashReporterService.report] function, + * anything that we do in this function **MUST** be synchronous and blocking. We cannot spawn + * work to background processes or threads here if we want to guarantee that the work is + * completed. Also, since the [CrashReporterService.report] functions are called synchronously, + * and from lib-crash's own process, it is unlikely that this would be called from more than one + * place at the same time. + * + * @param action Pass in the crash action to record. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun recordCrashAction(action: GleanCrashAction) { + // Persist the crash in a file so that it can be recorded on the next application start. We + // cannot directly record to Glean here because CrashHandler process is not the same process + // as Glean is initialized in. + // Create the file if it doesn't exist + if (!file.exists()) { + try { + file.createNewFile() + } catch (e: IOException) { + logger.error("Failed to create crash file", e) + } + } + + // Add a line representing the crash that was received + if (file.canWrite()) { + try { + @OptIn(ExperimentalSerializationApi::class) + Json.encodeToStream(action, FileOutputStream(file, true)) + file.appendText("\n") + } catch (e: IOException) { + logger.error("Failed to write to crash file", e) + } + } + } + + override fun record(crash: Crash.UncaughtExceptionCrash) { + recordCrashAction(GleanCrashAction.Count(UNCAUGHT_EXCEPTION_KEY)) + recordCrashAction( + GleanCrashAction.Ping( + uptimeNanos = uptime(), + processType = "main", + remoteType = "", + timeMillis = crash.timestamp, + startup = false, + reason = Pings.crashReasonCodes.crash, + cause = "java_exception", + ), + ) + } + + override fun record(crash: Crash.NativeCodeCrash) { + when (crash.processType) { + Crash.NativeCodeCrash.PROCESS_TYPE_MAIN -> + recordCrashAction(GleanCrashAction.Count(MAIN_PROCESS_NATIVE_CODE_CRASH_KEY)) + Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD -> + recordCrashAction( + GleanCrashAction.Count( + FOREGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY, + ), + ) + Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD -> + recordCrashAction( + GleanCrashAction.Count( + BACKGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY, + ), + ) + } + + // The `processType` property on a crash is a bit confusing because it does not map to the actual process types + // (like main, content, gpu, etc.). This property indicates what UI we should show to users given that "main" + // crashes essentially kill the app, "foreground child" crashes are likely tab crashes, and "background child" + // crashes are occurring in other processes (like GPU and extensions) for which users shouldn't notice anything + // (because there shouldn't be any noticeable impact in the app and the processes will be recreated + // automatically). + val processType = when (crash.processType) { + Crash.NativeCodeCrash.PROCESS_TYPE_MAIN -> "main" + + Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD -> { + when (crash.remoteType) { + // The extensions process is a content process as per: + // https://firefox-source-docs.mozilla.org/dom/ipc/process_model.html#webextensions + "extension" -> "content" + + else -> "utility" + } + } + + Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD -> "content" + + else -> "main" + } + recordCrashAction( + GleanCrashAction.Ping( + uptimeNanos = uptime(), + processType = processType, + remoteType = crash.remoteType ?: "", + timeMillis = crash.timestamp, + startup = false, + reason = Pings.crashReasonCodes.crash, + cause = "os_fault", + ), + ) + } + + override fun record(throwable: Throwable) { + recordCrashAction(GleanCrashAction.Count(CAUGHT_EXCEPTION_KEY)) + } +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/MozillaSocorroService.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/MozillaSocorroService.kt new file mode 100644 index 0000000000..df50580b0c --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/MozillaSocorroService.kt @@ -0,0 +1,566 @@ +/* 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.ActivityManager +import android.content.Context +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import androidx.annotation.VisibleForTesting +import androidx.core.content.pm.PackageInfoCompat +import mozilla.components.concept.base.crash.Breadcrumb +import mozilla.components.lib.crash.Crash +import mozilla.components.support.base.ext.getStacktraceAsJsonString +import mozilla.components.support.base.ext.getStacktraceAsString +import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.utils.ext.getPackageInfoCompat +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.io.BufferedReader +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileReader +import java.io.IOException +import java.io.InputStreamReader +import java.io.OutputStream +import java.net.HttpURLConnection +import java.net.URL +import java.nio.channels.Channels +import java.util.Locale +import java.util.concurrent.TimeUnit +import java.util.zip.GZIPOutputStream +import kotlin.random.Random +import mozilla.components.Build as AcBuild + +/* This ID is used for all Mozilla products. Setting as default if no ID is passed in */ +private const val MOZILLA_PRODUCT_ID = "{eeb82917-e434-4870-8148-5c03d4caa81b}" + +@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) +internal const val CAUGHT_EXCEPTION_TYPE = "caught exception" +internal const val UNCAUGHT_EXCEPTION_TYPE = "uncaught exception" +internal const val FATAL_NATIVE_CRASH_TYPE = "fatal native crash" +internal const val NON_FATAL_NATIVE_CRASH_TYPE = "non-fatal native crash" + +internal const val DEFAULT_VERSION_NAME = "N/A" +internal const val DEFAULT_VERSION_CODE = "N/A" +internal const val DEFAULT_VERSION = "N/A" +internal const val DEFAULT_BUILD_ID = "N/A" +internal const val DEFAULT_VENDOR = "N/A" +internal const val DEFAULT_RELEASE_CHANNEL = "N/A" +internal const val DEFAULT_DISTRIBUTION_ID = "N/A" + +private const val KEY_CRASH_ID = "CrashID" + +private const val MINI_DUMP_FILE_EXT = "dmp" +private const val EXTRAS_FILE_EXT = "extra" +private const val FILE_REGEX = "([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\." + +/** + * A [CrashReporterService] implementation uploading crash reports to crash-stats.mozilla.com. + * + * @param applicationContext The application [Context]. + * @param appName A human-readable app name. This name is used on crash-stats.mozilla.com to filter crashes by app. + * The name needs to be safelisted for the server to accept the crash. + * [File a bug](https://bugzilla.mozilla.org/enter_bug.cgi?product=Socorro) if you would like to get your + * app added to the safelist. + * @param appId The application ID assigned by Socorro server. + * @param version The engine version. + * @param buildId The engine build ID. + * @param vendor The application vendor name. + * @param serverUrl The URL of the server. + * @param versionName The version of the application. + * @param versionCode The version code of the application. + * @param releaseChannel The release channel of the application. + * @param distributionId The distribution id of the application. + */ +@Suppress("LargeClass") +class MozillaSocorroService( + private val applicationContext: Context, + private val appName: String, + private val appId: String = MOZILLA_PRODUCT_ID, + private val version: String = DEFAULT_VERSION, + private val buildId: String = DEFAULT_BUILD_ID, + private val vendor: String = DEFAULT_VENDOR, + @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal var serverUrl: String? = null, + private var versionName: String = DEFAULT_VERSION_NAME, + private var versionCode: String = DEFAULT_VERSION_CODE, + private val releaseChannel: String = DEFAULT_RELEASE_CHANNEL, + private val distributionId: String = DEFAULT_DISTRIBUTION_ID, +) : CrashReporterService { + private val logger = Logger("mozac/MozillaSocorroCrashHelperService") + private val startTime = System.currentTimeMillis() + private val ignoreKeys = hashSetOf("URL", "ServerURL", "StackTraces") + + override val id: String = "socorro" + + override val name: String = "Socorro" + + override fun createCrashReportUrl(identifier: String): String? { + return "https://crash-stats.mozilla.org/report/index/$identifier" + } + + init { + val packageInfo = try { + applicationContext.packageManager.getPackageInfoCompat(applicationContext.packageName, 0) + } catch (e: PackageManager.NameNotFoundException) { + logger.error("package name not found, failed to get application version") + null + } + + packageInfo?.let { + if (versionName == DEFAULT_VERSION_NAME) { + try { + versionName = packageInfo.versionName ?: DEFAULT_VERSION_NAME + } catch (e: IllegalStateException) { + logger.error("failed to get application version") + } + } + + if (versionCode == DEFAULT_VERSION_CODE) { + try { + versionCode = PackageInfoCompat.getLongVersionCode(packageInfo).toString() + } catch (e: IllegalStateException) { + logger.error("failed to get application version code") + } + } + } + + if (serverUrl == null) { + serverUrl = Uri.parse("https://crash-reports.mozilla.com/submit") + .buildUpon() + .appendQueryParameter("id", appId) + .appendQueryParameter("version", versionName) + .appendQueryParameter("android_component_version", AcBuild.version) + .build().toString() + } + } + + override fun report(crash: Crash.UncaughtExceptionCrash): String? { + return sendReport( + crash.timestamp, + crash.throwable, + miniDumpFilePath = null, + extrasFilePath = null, + isNativeCodeCrash = false, + isFatalCrash = true, + breadcrumbs = crash.breadcrumbs, + ) + } + + override fun report(crash: Crash.NativeCodeCrash): String? { + return sendReport( + crash.timestamp, + throwable = null, + miniDumpFilePath = crash.minidumpPath, + extrasFilePath = crash.extrasPath, + isNativeCodeCrash = true, + isFatalCrash = crash.isFatal, + breadcrumbs = crash.breadcrumbs, + ) + } + + override fun report(throwable: Throwable, breadcrumbs: ArrayList): String? { + /* Not sending caught exceptions to Socorro */ + return null + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun sendReport( + timestamp: Long, + throwable: Throwable?, + miniDumpFilePath: String?, + extrasFilePath: String?, + isNativeCodeCrash: Boolean, + isFatalCrash: Boolean, + breadcrumbs: ArrayList, + ): String? { + val url = URL(serverUrl) + val boundary = generateBoundary() + var conn: HttpURLConnection? = null + + val breadcrumbsJson = JSONArray() + for (breadcrumb in breadcrumbs) { + breadcrumbsJson.put(breadcrumb.toJson()) + } + + try { + conn = url.openConnection() as HttpURLConnection + conn.requestMethod = "POST" + conn.doOutput = true + conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=$boundary") + conn.setRequestProperty("Content-Encoding", "gzip") + + sendCrashData( + conn.outputStream, boundary, timestamp, throwable, miniDumpFilePath, extrasFilePath, + isNativeCodeCrash, isFatalCrash, breadcrumbsJson.toString(), + ) + + BufferedReader(InputStreamReader(conn.inputStream)).use { reader -> + val map = parseResponse(reader) + + val id = map?.get(KEY_CRASH_ID) + + if (id != null) { + logger.info("Crash reported to Socorro: $id") + } else { + logger.info("Server rejected crash report") + } + + return id + } + } catch (e: IOException) { + try { + logger.error("failed to send report to Socorro with " + conn?.responseCode, e) + } catch (e: IOException) { + logger.error("failed to send report to Socorro", e) + } + + return null + } finally { + conn?.disconnect() + } + } + + private fun parseResponse(reader: BufferedReader): Map? { + val map = mutableMapOf() + + reader.readLines().forEach { line -> + val position = line.indexOf("=") + if (position != -1) { + val key = line.substring(0, position) + val value = unescape(line.substring(position + 1)) + map[key] = value + } + } + + return map + } + + @Suppress("LongParameterList", "LongMethod", "ComplexMethod") + private fun sendCrashData( + os: OutputStream, + boundary: String, + timestamp: Long, + throwable: Throwable?, + miniDumpFilePath: String?, + extrasFilePath: String?, + isNativeCodeCrash: Boolean, + isFatalCrash: Boolean, + breadcrumbs: String, + ) { + val nameSet = mutableSetOf() + val gzipOs = GZIPOutputStream(os) + sendPart(gzipOs, boundary, "ProductName", appName, nameSet) + sendPart(gzipOs, boundary, "ProductID", appId, nameSet) + sendPart(gzipOs, boundary, "Version", versionName, nameSet) + sendPart(gzipOs, boundary, "ApplicationBuildID", versionCode, nameSet) + sendPart(gzipOs, boundary, "AndroidComponentVersion", AcBuild.version, nameSet) + sendPart(gzipOs, boundary, "GleanVersion", AcBuild.gleanSdkVersion, nameSet) + sendPart(gzipOs, boundary, "ApplicationServicesVersion", AcBuild.applicationServicesVersion, nameSet) + sendPart(gzipOs, boundary, "GeckoViewVersion", version, nameSet) + sendPart(gzipOs, boundary, "BuildID", buildId, nameSet) + sendPart(gzipOs, boundary, "Vendor", vendor, nameSet) + sendPart(gzipOs, boundary, "Breadcrumbs", breadcrumbs, nameSet) + sendPart(gzipOs, boundary, "useragent_locale", Locale.getDefault().toLanguageTag(), nameSet) + sendPart(gzipOs, boundary, "DistributionID", distributionId, nameSet) + + extrasFilePath?.let { + val regex = "$FILE_REGEX$EXTRAS_FILE_EXT".toRegex() + if (regex.matchEntire(it.substringAfterLast("/")) != null) { + val extrasFile = File(it) + val extrasMap = readExtrasFromFile(extrasFile) + for (key in extrasMap.keys) { + sendPart(gzipOs, boundary, key, extrasMap[key], nameSet) + } + extrasFile.delete() + } + } + + if (throwable?.stackTrace?.isEmpty() == false) { + sendPart( + gzipOs, + boundary, + "JavaStackTrace", + getExceptionStackTrace( + throwable, + !isNativeCodeCrash && !isFatalCrash, + ), + nameSet, + ) + + sendPart(gzipOs, boundary, "JavaException", throwable.getStacktraceAsJsonString(), nameSet) + } + + miniDumpFilePath?.let { + val regex = "$FILE_REGEX$MINI_DUMP_FILE_EXT".toRegex() + if (regex.matchEntire(it.substringAfterLast("/")) != null) { + val minidumpFile = File(it) + sendFile(gzipOs, boundary, "upload_file_minidump", minidumpFile, nameSet) + minidumpFile.delete() + } + } + + when { + isNativeCodeCrash && isFatalCrash -> + sendPart(gzipOs, boundary, "CrashType", FATAL_NATIVE_CRASH_TYPE, nameSet) + isNativeCodeCrash && !isFatalCrash -> + sendPart(gzipOs, boundary, "CrashType", NON_FATAL_NATIVE_CRASH_TYPE, nameSet) + !isNativeCodeCrash && isFatalCrash -> + sendPart(gzipOs, boundary, "CrashType", UNCAUGHT_EXCEPTION_TYPE, nameSet) + !isNativeCodeCrash && !isFatalCrash -> + sendPart(gzipOs, boundary, "CrashType", CAUGHT_EXCEPTION_TYPE, nameSet) + } + + sendPackageInstallTime(gzipOs, boundary, nameSet) + sendProcessName(gzipOs, boundary, nameSet) + sendPart(gzipOs, boundary, "ReleaseChannel", releaseChannel, nameSet) + sendPart( + gzipOs, + boundary, + "StartupTime", + TimeUnit.MILLISECONDS.toSeconds(startTime).toString(), + nameSet, + ) + sendPart( + gzipOs, + boundary, + "CrashTime", + TimeUnit.MILLISECONDS.toSeconds(timestamp).toString(), + nameSet, + ) + sendPart(gzipOs, boundary, "Android_PackageName", applicationContext.packageName, nameSet) + sendPart(gzipOs, boundary, "Android_Manufacturer", Build.MANUFACTURER, nameSet) + sendPart(gzipOs, boundary, "Android_Model", Build.MODEL, nameSet) + sendPart(gzipOs, boundary, "Android_Board", Build.BOARD, nameSet) + sendPart(gzipOs, boundary, "Android_Brand", Build.BRAND, nameSet) + sendPart(gzipOs, boundary, "Android_Device", Build.DEVICE, nameSet) + sendPart(gzipOs, boundary, "Android_Display", Build.DISPLAY, nameSet) + sendPart(gzipOs, boundary, "Android_Fingerprint", Build.FINGERPRINT, nameSet) + sendPart(gzipOs, boundary, "Android_Hardware", Build.HARDWARE, nameSet) + sendPart( + gzipOs, + boundary, + "Android_Version", + "${Build.VERSION.SDK_INT} (${Build.VERSION.CODENAME})", + nameSet, + ) + + if (Build.SUPPORTED_ABIS.isNotEmpty()) { + sendPart(gzipOs, boundary, "Android_CPU_ABI", Build.SUPPORTED_ABIS[0], nameSet) + if (Build.SUPPORTED_ABIS.size >= 2) { + sendPart(gzipOs, boundary, "Android_CPU_ABI2", Build.SUPPORTED_ABIS[1], nameSet) + } + } + + gzipOs.write(("\r\n--$boundary--\r\n").toByteArray()) + gzipOs.flush() + gzipOs.close() + } + + private fun sendProcessName(os: OutputStream, boundary: String, nameSet: MutableSet) { + val pid = android.os.Process.myPid() + val manager = applicationContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + manager.runningAppProcesses.filter { it.pid == pid }.forEach { + sendPart(os, boundary, "Android_ProcessName", it.processName, nameSet) + return + } + } + + private fun sendPackageInstallTime(os: OutputStream, boundary: String, nameSet: MutableSet) { + val packageManager = applicationContext.packageManager + try { + val packageInfo = packageManager.getPackageInfoCompat(applicationContext.packageName, 0) + sendPart( + os, + boundary, + "InstallTime", + TimeUnit.MILLISECONDS.toSeconds( + packageInfo.lastUpdateTime, + ).toString(), + nameSet, + ) + } catch (e: PackageManager.NameNotFoundException) { + logger.error("Error getting package info", e) + } + } + + private fun generateBoundary(): String { + val r0 = Random.nextInt(0, Int.MAX_VALUE) + val r1 = Random.nextInt(0, Int.MAX_VALUE) + return String.format("---------------------------%08X%08X", r0, r1) + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun sendPart( + os: OutputStream, + boundary: String, + name: String, + data: String?, + nameSet: MutableSet, + ) { + if (data == null) { + return + } + + if (nameSet.contains(name)) { + return + } else { + nameSet.add(name) + } + + try { + os.write( + ( + "--$boundary\r\nContent-Disposition: form-data; " + + "name=$name\r\n\r\n$data\r\n" + ).toByteArray(), + ) + } catch (e: IOException) { + logger.error("Exception when sending $name", e) + } + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun sendFile( + os: OutputStream, + boundary: String, + name: String, + file: File, + nameSet: MutableSet, + ) { + if (nameSet.contains(name)) { + return + } else { + nameSet.add(name) + } + + try { + os.write( + ( + "--${boundary}\r\n" + + "Content-Disposition: form-data; name=\"$name\"; " + + "filename=\"${file.getName()}\"\r\n" + + "Content-Type: application/octet-stream\r\n\r\n" + ).toByteArray(), + ) + } catch (e: IOException) { + logger.error("failed to write boundary", e) + return + } + + try { + val fileInputStream = FileInputStream(file).channel + fileInputStream.transferTo(0, fileInputStream.size(), Channels.newChannel(os)) + fileInputStream.close() + } catch (e: IOException) { + logger.error("failed to send file", e) + } + + try { + // Add EOL to separate from the next part + os.write("\r\n".toByteArray()) + } catch (e: IOException) { + logger.error("failed to write EOL", e) + } + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun unescape(string: String): String { + return string.replace("\\\\\\\\", "\\").replace("\\\\n", "\n").replace("\\\\t", "\t") + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun jsonUnescape(string: String): String { + return string.replace("""\\\\""", "\\").replace("""\n""", "\n").replace("""\t""", "\t") + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + @Suppress("NestedBlockDepth") + internal fun readExtrasFromLegacyFile(file: File): HashMap { + var fileReader: FileReader? = null + var bufReader: BufferedReader? = null + var line: String? + val map = HashMap() + + try { + fileReader = FileReader(file) + bufReader = BufferedReader(fileReader) + line = bufReader.readLine() + while (line != null) { + val equalsPos = line.indexOf('=') + if ((equalsPos) != -1) { + val key = line.substring(0, equalsPos) + val value = unescape(line.substring(equalsPos + 1)) + if (!ignoreKeys.contains(key)) { + map[key] = value + } + } + line = bufReader.readLine() + } + } catch (e: IOException) { + logger.error("failed to convert extras to map", e) + } finally { + try { + fileReader?.close() + bufReader?.close() + } catch (e: IOException) { + // do nothing + } + } + + return map + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + @Suppress("NestedBlockDepth") + internal fun readExtrasFromFile(file: File): HashMap { + var resultMap = HashMap() + var notJson = false + + try { + FileReader(file).use { fileReader -> + val input = fileReader.readLines().firstOrNull() + ?: throw JSONException("failed to read json file") + + val jsonObject = JSONObject(input) + for (key in jsonObject.keys()) { + if (!key.isNullOrEmpty() && !ignoreKeys.contains(key)) { + resultMap[key] = jsonUnescape(jsonObject.getString(key)) + } + } + } + } catch (e: FileNotFoundException) { + logger.error("failed to find extra file", e) + } catch (e: IOException) { + logger.error("failed read the extra file", e) + } catch (e: JSONException) { + logger.info("extras file JSON syntax error, trying legacy format") + notJson = true + } + + if (notJson) { + resultMap = readExtrasFromLegacyFile(file) + } + + return resultMap + } + + @Suppress("TooGenericExceptionCaught") + // printStackTrace() can throw a NullPointerException exception even if throwable is not null + private fun getExceptionStackTrace(throwable: Throwable, isCaughtException: Boolean): String? { + return try { + when (isCaughtException) { + true -> "$LIB_CRASH_INFO_PREFIX ${throwable.getStacktraceAsString()}" + false -> throwable.getStacktraceAsString() + } + } catch (e: NullPointerException) { + null + } + } +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashReportService.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashReportService.kt new file mode 100644 index 0000000000..5c1d8d18b7 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashReportService.kt @@ -0,0 +1,93 @@ +/* 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.annotation.VisibleForTesting.Companion.PRIVATE +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +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.support.base.ids.SharedIdsHelper + +private const val NOTIFICATION_TAG = "mozac.lib.crash.sendcrash" +private const val NOTIFICATION_ID = 1 + +@VisibleForTesting(otherwise = PRIVATE) +internal const val NOTIFICATION_TAG_KEY = "mozac.lib.crash.notification.tag" + +@VisibleForTesting(otherwise = PRIVATE) +internal const val NOTIFICATION_ID_KEY = "mozac.lib.crash.notification.id" + +class SendCrashReportService : Service() { + private val crashReporter: CrashReporter by lazy { CrashReporter.requireInstance } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + intent.getStringExtra(NOTIFICATION_TAG_KEY)?.apply { + NotificationManagerCompat.from(applicationContext) + .cancel(this, intent.getIntExtra(NOTIFICATION_ID_KEY, 0)) + } + + 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 = SharedIdsHelper.getIdForTag(this, NOTIFICATION_TAG) + startForeground(notificationId, notification) + } + + NotificationManagerCompat.from(this).cancel(NOTIFICATION_TAG, NOTIFICATION_ID) + val crash = Crash.fromIntent(intent) + crashReporter.submitReport(crash) { + stopSelf() + } + + return START_NOT_STICKY + } + + override fun onBind(intent: Intent): IBinder? { + // We don't provide binding, so return null + return null + } + + companion object { + fun createReportIntent( + context: Context, + crash: Crash, + notificationTag: String? = null, + notificationId: Int = 0, + ): Intent { + val intent = Intent(context, SendCrashReportService::class.java) + + notificationTag?.apply { + intent.putExtra(NOTIFICATION_TAG_KEY, notificationTag) + intent.putExtra(NOTIFICATION_ID_KEY, notificationId) + } + + crash.fillIn(intent) + + return intent + } + } +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashTelemetryService.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashTelemetryService.kt new file mode 100644 index 0000000000..1f312911a9 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashTelemetryService.kt @@ -0,0 +1,66 @@ +/* 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.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +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.support.base.ids.SharedIdsHelper + +private const val NOTIFICATION_TAG = "mozac.lib.crash.sendtelemetry" +private const val NOTIFICATION_ID = 1 + +class SendCrashTelemetryService : Service() { + private val crashReporter: CrashReporter by lazy { CrashReporter.requireInstance } + + 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_gathering_crash_telemetry_in_progress), + ) + .setSmallIcon(R.drawable.mozac_lib_crash_notification) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setCategory(NotificationCompat.CATEGORY_ERROR) + .setAutoCancel(true) + .setProgress(0, 0, true) + .build() + + val notificationId = SharedIdsHelper.getIdForTag(this, NOTIFICATION_TAG) + startForeground(notificationId, notification) + } + + NotificationManagerCompat.from(this).cancel(NOTIFICATION_TAG, NOTIFICATION_ID) + val crash = Crash.fromIntent(intent) + crashReporter.submitCrashTelemetry(crash) { + stopSelf() + } + + return START_NOT_STICKY + } + + 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, SendCrashTelemetryService::class.java) + crash.fillIn(intent) + + return intent + } + } +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/AbstractCrashListActivity.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/AbstractCrashListActivity.kt new file mode 100644 index 0000000000..eec78c334c --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/AbstractCrashListActivity.kt @@ -0,0 +1,36 @@ +/* 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.ui + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import mozilla.components.lib.crash.CrashReporter +import mozilla.components.lib.crash.R + +/** + * Activity for displaying the list of reported crashes. + */ +abstract class AbstractCrashListActivity : AppCompatActivity() { + abstract val crashReporter: CrashReporter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setTitle(R.string.mozac_lib_crash_activity_title) + + if (savedInstanceState == null) { + supportFragmentManager.beginTransaction() + .add(android.R.id.content, CrashListFragment()) + .commit() + } + } + + /** + * Gets invoked whenever the user selects a crash reporting service. + * + * @param url URL pointing to the crash report for the selected crash reporting service. + */ + abstract fun onCrashServiceSelected(url: String) +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/CrashListAdapter.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/CrashListAdapter.kt new file mode 100644 index 0000000000..e3d2b48cba --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/CrashListAdapter.kt @@ -0,0 +1,163 @@ +/* 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.ui + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.format.DateUtils +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.view.ViewCompat +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.lib.crash.CrashReporter +import mozilla.components.lib.crash.R +import mozilla.components.lib.crash.db.CrashWithReports +import mozilla.components.lib.crash.db.ReportEntity + +/** + * RecyclerView adapter for displaying the list of crashes. + */ +internal class CrashListAdapter( + private val crashReporter: CrashReporter, + private val onSelection: (String) -> Unit, +) : RecyclerView.Adapter() { + private var crashes: List = emptyList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CrashViewHolder { + val view = LayoutInflater.from( + parent.context, + ).inflate( + R.layout.mozac_lib_crash_item_crash, + parent, + false, + ) + + return CrashViewHolder(view) + } + + override fun getItemCount(): Int { + return crashes.size + } + + override fun onBindViewHolder(holder: CrashViewHolder, position: Int) { + val crashWithReports = crashes[position] + + holder.idView.text = crashWithReports.crash.uuid + + holder.titleView.text = crashWithReports.crash.stacktrace.lines().first() + + val time = DateUtils.getRelativeDateTimeString( + holder.footerView.context, + crashWithReports.crash.createdAt, + DateUtils.MINUTE_IN_MILLIS, + DateUtils.WEEK_IN_MILLIS, + 0, + ) + + holder.footerView.text = SpannableStringBuilder(time).apply { + append(" - ") + + append( + holder.itemView.context.getString(R.string.mozac_lib_crash_share), + object : ClickableSpan() { + override fun onClick(widget: View) { + shareCrash(widget.context, crashWithReports) + } + }, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ) + + if (crashWithReports.reports.isNotEmpty()) { + append(" - ") + append(crashReporter, crashWithReports.reports, onSelection) + } + } + ViewCompat.enableAccessibleClickableSpanSupport(holder.footerView) + } + + @SuppressLint("NotifyDataSetChanged") + fun updateList(list: List) { + crashes = list + notifyDataSetChanged() + } + + private fun shareCrash(context: Context, crashWithReports: CrashWithReports) { + val text = StringBuilder() + + text.append(crashWithReports.crash.uuid) + text.appendLine() + text.append(crashWithReports.crash.stacktrace.lines().first()) + text.appendLine() + + crashWithReports.reports.forEach { report -> + val service = crashReporter.getCrashReporterServiceById(report.serviceId) + text.append(" * ") + text.append(service?.name ?: report.serviceId) + text.append(": ") + text.append(service?.createCrashReportUrl(report.reportId) ?: "") + text.appendLine() + } + + text.append("----") + text.appendLine() + text.append(crashWithReports.crash.stacktrace) + text.appendLine() + + val intent = Intent(Intent.ACTION_SEND) + intent.type = "text/plain" + intent.putExtra(Intent.EXTRA_TEXT, text.toString()) + context.startActivity(Intent.createChooser(intent, "Crash")) + } +} + +internal class CrashViewHolder( + view: View, +) : RecyclerView.ViewHolder( + view, +) { + val titleView = view.findViewById(R.id.mozac_lib_crash_title) + val idView = view.findViewById(R.id.mozac_lib_crash_id) + val footerView = view.findViewById(R.id.mozac_lib_crash_footer).apply { + movementMethod = LinkMovementMethod.getInstance() + } +} + +private fun SpannableStringBuilder.append( + crashReporter: CrashReporter, + services: List, + onSelection: (String) -> Unit, +): SpannableStringBuilder { + services.forEachIndexed { index, entity -> + val service = crashReporter.getCrashReporterServiceById(entity.serviceId) + val name = service?.name ?: entity.serviceId + val url = service?.createCrashReportUrl(entity.reportId) + + if (url != null) { + append( + name, + object : ClickableSpan() { + override fun onClick(widget: View) { + onSelection(url) + } + }, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ) + } else { + append(name) + } + + if (index < services.lastIndex) { + append(" ") + } + } + return this +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/CrashListFragment.kt b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/CrashListFragment.kt new file mode 100644 index 0000000000..4305d2ac16 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/CrashListFragment.kt @@ -0,0 +1,65 @@ +/* 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.ui + +import android.database.sqlite.SQLiteBlobTooBigException +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.lib.crash.R +import mozilla.components.lib.crash.db.CrashDatabase + +/** + * Fragment displaying the list of crashes. + */ +internal class CrashListFragment : Fragment(R.layout.mozac_lib_crash_crashlist) { + private val database by lazy { CrashDatabase.get(requireContext()) } + private val reporter by lazy { (activity as AbstractCrashListActivity).crashReporter } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val listView: RecyclerView = view.findViewById(R.id.mozac_lib_crash_list) + listView.layoutManager = LinearLayoutManager( + requireContext(), + LinearLayoutManager.VERTICAL, + false, + ) + + val emptyView = view.findViewById(R.id.mozac_lib_crash_empty) + + val adapter = CrashListAdapter(reporter, ::onSelection) + listView.adapter = adapter + + val dividerItemDecoration = DividerItemDecoration( + requireContext(), + LinearLayoutManager.VERTICAL, + ) + listView.addItemDecoration(dividerItemDecoration) + + try { + database.crashDao().getCrashesWithReports().observe( + viewLifecycleOwner, + Observer { list -> + if (list.isEmpty()) { + emptyView.visibility = View.VISIBLE + } else { + adapter.updateList(list) + } + }, + ) + } catch (e: SQLiteBlobTooBigException) { + /* recover by deleting all entries */ + database.crashDao().deleteAll() + } + } + + private fun onSelection(url: String) { + (requireActivity() as AbstractCrashListActivity).onCrashServiceSelected(url) + } +} diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/drawable/mozac_lib_crash_notification.xml b/mobile/android/android-components/components/lib/crash/src/main/res/drawable/mozac_lib_crash_notification.xml new file mode 100644 index 0000000000..3eeed541e0 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/res/drawable/mozac_lib_crash_notification.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_crashlist.xml b/mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_crashlist.xml new file mode 100644 index 0000000000..8754a8ee11 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_crashlist.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_crashreporter.xml b/mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_crashreporter.xml new file mode 100644 index 0000000000..3214d191c6 --- /dev/null +++ b/mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_crashreporter.xml @@ -0,0 +1,81 @@ + + + + + + + + + + +