summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/lib
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/android-components/components/lib')
-rw-r--r--mobile/android/android-components/components/lib/auth/build.gradle38
-rw-r--r--mobile/android/android-components/components/lib/auth/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/auth/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/AuthenticationDelegate.kt29
-rw-r--r--mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/BiometricPromptAuth.kt78
-rw-r--r--mobile/android/android-components/components/lib/auth/src/main/java/mozilla/components/lib/auth/BiometricUtils.kt51
-rw-r--r--mobile/android/android-components/components/lib/auth/src/test/java/mozilla/components/lib/auth/BiometricPromptAuthTest.kt91
-rw-r--r--mobile/android/android-components/components/lib/auth/src/test/java/mozilla/components/lib/auth/BiometricUtilsTest.kt50
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/build.gradle45
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/src/main/AndroidManifest.xml15
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/SentryService.kt201
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/eventprocessors/AddMechanismEventProcessor.kt41
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/src/main/java/mozilla/components/lib/crash/sentry/eventprocessors/RustCrashEventProcessor.kt36
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/SentryServiceTest.kt276
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/eventprocessors/AddMechanismEventProcessorTest.kt46
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/src/test/java/mozilla/components/lib/crash/sentry/eventprocessors/RustCrashEventProcessorTest.kt35
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/lib/crash-sentry/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/lib/crash/README.md239
-rw-r--r--mobile/android/android-components/components/lib/crash/build.gradle99
-rw-r--r--mobile/android/android-components/components/lib/crash/images/crash-dialog.pngbin0 -> 20052 bytes
-rw-r--r--mobile/android/android-components/components/lib/crash/images/crash-in-app.pngbin0 -> 6574 bytes
-rw-r--r--mobile/android/android-components/components/lib/crash/metrics.yaml154
-rw-r--r--mobile/android/android-components/components/lib/crash/pings.yaml28
-rw-r--r--mobile/android/android-components/components/lib/crash/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/crash/schemas/mozilla.components.lib.crash.db.CrashDatabase/1.json84
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/AndroidManifest.xml49
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/Crash.kt175
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/CrashReporter.kt376
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashDao.kt78
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashDatabase.kt40
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashEntity.kt54
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/CrashWithReports.kt22
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/db/ReportEntity.kt52
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/handler/CrashHandlerService.kt76
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/handler/ExceptionHandler.kt60
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/notification/CrashNotification.kt121
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/prompt/CrashPrompt.kt48
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/prompt/CrashReporterActivity.kt158
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashReporterService.kt54
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashTelemetryService.kt27
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/GleanCrashReporterService.kt312
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/MozillaSocorroService.kt566
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashReportService.kt93
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/SendCrashTelemetryService.kt66
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/AbstractCrashListActivity.kt36
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/CrashListAdapter.kt163
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/ui/CrashListFragment.kt65
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/drawable/mozac_lib_crash_notification.xml14
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_crashlist.xml23
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_crashreporter.xml81
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_item_crash.xml40
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-am/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-an/strings.xml32
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ar/strings.xml32
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ast/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-az/strings.xml32
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-azb/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ban/strings.xml11
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-be/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-bg/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-bn/strings.xml32
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-br/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-bs/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ca/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-cak/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ceb/strings.xml33
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ckb/strings.xml32
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-co/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-cs/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-cy/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-da/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-de/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-dsb/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-el/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-en-rCA/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-en-rGB/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-eo/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-es-rAR/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-es-rCL/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-es-rES/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-es-rMX/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-es/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-et/strings.xml35
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-eu/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-fa/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ff/strings.xml30
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-fi/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-fr/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-fur/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-fy-rNL/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ga-rIE/strings.xml25
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-gd/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-gl/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-gn/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-gu-rIN/strings.xml32
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-hi-rIN/strings.xml33
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-hil/strings.xml13
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-hr/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-hsb/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-hu/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-hy-rAM/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ia/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-in/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-is/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-it/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-iw/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ja/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ka/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-kaa/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-kab/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-kk/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-kmr/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-kn/strings.xml33
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ko/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-lij/strings.xml33
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-lo/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-lt/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ml/strings.xml33
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-mr/strings.xml32
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-my/strings.xml33
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-nb-rNO/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ne-rNP/strings.xml36
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-nl/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-nn-rNO/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-oc/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-pa-rIN/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-pa-rPK/strings.xml12
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-pl/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-pt-rBR/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-pt-rPT/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-rm/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ro/strings.xml32
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ru/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-sat/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-sc/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-si/strings.xml39
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-sk/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-skr/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-sl/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-sq/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-sr/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-su/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-sv-rSE/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ta/strings.xml33
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-te/strings.xml33
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-tg/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-th/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-tl/strings.xml39
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-tr/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-trs/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-tt/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-tzm/strings.xml9
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ug/strings.xml42
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-uk/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-ur/strings.xml33
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-uz/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-vec/strings.xml24
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-vi/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-yo/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-zh-rCN/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values-zh-rTW/strings.xml41
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values/strings.xml44
-rw-r--r--mobile/android/android-components/components/lib/crash/src/main/res/values/styles.xml13
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/BreadcrumbTest.kt192
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashReporterTest.kt931
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashTest.kt105
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/NativeCodeCrashTest.kt74
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/UncaughtExceptionCrashTest.kt34
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/CrashHandlerServiceTest.kt122
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/ExceptionHandlerTest.kt98
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/notification/CrashNotificationTest.kt175
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/prompt/CrashReporterActivityTest.kt263
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/GleanCrashReporterServiceTest.kt464
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/MozillaSocorroServiceTest.kt693
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashReportServiceTest.kt169
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashTelemetryServiceTest.kt142
-rwxr-xr-xmobile/android/android-components/components/lib/crash/src/test/resources/BadTestExtrasFile1
-rwxr-xr-xmobile/android/android-components/components/lib/crash/src/test/resources/TestExtrasFile1
-rwxr-xr-xmobile/android/android-components/components/lib/crash/src/test/resources/TestLegacyExtrasFile31
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/lib/crash/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/README.md19
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/build.gradle39
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/Keystore.kt314
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/KeystoreException.kt17
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/SecureAbove22Preferences.kt231
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/SecurePrefsReliabilityExperiment.kt151
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/KeystoreTest.kt137
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/SecureAbove22PreferencesTest.kt190
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/SecurePrefsReliabilityExperimentTest.kt215
-rw-r--r--mobile/android/android-components/components/lib/dataprotect/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/lib/fetch-httpurlconnection/README.md25
-rw-r--r--mobile/android/android-components/components/lib/fetch-httpurlconnection/build.gradle40
-rw-r--r--mobile/android/android-components/components/lib/fetch-httpurlconnection/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/fetch-httpurlconnection/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/lib/fetch-httpurlconnection/src/main/java/mozilla/components/lib/fetch/httpurlconnection/HttpURLConnectionClient.kt195
-rw-r--r--mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/java/mozilla/components/lib/fetch/httpurlconnection/HttpUrlConnectionFetchTestCases.kt40
-rw-r--r--mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/lib/fetch-okhttp/README.md25
-rw-r--r--mobile/android/android-components/components/lib/fetch-okhttp/build.gradle43
-rw-r--r--mobile/android/android-components/components/lib/fetch-okhttp/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/fetch-okhttp/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/lib/fetch-okhttp/src/main/java/mozilla/components/lib/fetch/okhttp/OkHttpClient.kt149
-rw-r--r--mobile/android/android-components/components/lib/fetch-okhttp/src/test/java/mozilla/components/lib/fetch/okhttp/OkHttpFetchTestCases.kt27
-rw-r--r--mobile/android/android-components/components/lib/fetch-okhttp/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/lib/jexl/README.md236
-rw-r--r--mobile/android/android-components/components/lib/jexl/build.gradle34
-rw-r--r--mobile/android/android-components/components/lib/jexl/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/Jexl.kt102
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/ast/nodes.kt230
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/Evaluator.kt86
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/EvaluatorHandlers.kt158
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/JexlContext.kt51
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/ext/JexlExtensions.kt32
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/grammar/Grammar.kt141
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/Lexer.kt223
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/LexerInput.kt85
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/Token.kt32
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/parser/Parser.kt252
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/parser/StateMachine.kt230
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/value/JexlValue.kt307
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/JexlTest.kt112
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/LanguageTest.kt53
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/evaluator/EvaluatorTest.kt375
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/ext/JexlExtensionsTest.kt65
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/lexer/LexerTest.kt483
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/parser/ParserTest.kt506
-rw-r--r--mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/value/JexlValueTest.kt170
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/README.md64
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/build.gradle49
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/src/main/assets/publicsuffixesbin0 -> 107497 bytes
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt138
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt158
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.kt50
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt122
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/src/test/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListTest.kt482
-rw-r--r--mobile/android/android-components/components/lib/publicsuffixlist/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/lib/push-firebase/README.md59
-rw-r--r--mobile/android/android-components/components/lib/push-firebase/build.gradle42
-rw-r--r--mobile/android/android-components/components/lib/push-firebase/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/push-firebase/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/lib/push-firebase/src/main/java/mozilla/components/lib/push/firebase/AbstractFirebasePushService.kt103
-rw-r--r--mobile/android/android-components/components/lib/push-firebase/src/test/java/mozilla/components/lib/push/firebase/AbstractFirebasePushServiceTest.kt115
-rw-r--r--mobile/android/android-components/components/lib/push-firebase/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/lib/push-firebase/src/test/resources/robolectric.properties1
-rw-r--r--mobile/android/android-components/components/lib/state/README.md69
-rw-r--r--mobile/android/android-components/components/lib/state/build.gradle69
-rw-r--r--mobile/android/android-components/components/lib/state/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/lib/state/src/androidTest/java/mozilla/components/lib/state/ext/ComposeExtensionsKtTest.kt182
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/AndroidManifest.xml8
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Action.kt14
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/DelicateAction.kt23
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Middleware.kt53
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Observer.kt10
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Reducer.kt13
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/State.kt10
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Store.kt187
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/ComposeExtensions.kt147
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/Fragment.kt105
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/StoreExtensions.kt265
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/View.kt39
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/helpers/AbstractBinding.kt44
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/internal/ReducerChainBuilder.kt67
-rw-r--r--mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/internal/StoreThreadFactory.kt58
-rw-r--r--mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreExceptionTest.kt33
-rw-r--r--mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreTest.kt311
-rw-r--r--mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/FragmentKtTest.kt301
-rw-r--r--mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/StoreExtensionsKtTest.kt572
-rw-r--r--mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/ViewKtTest.kt89
-rw-r--r--mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/helpers/AbstractBindingTest.kt98
-rw-r--r--mobile/android/android-components/components/lib/state/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/lib/state/src/test/resources/robolectric.properties1
280 files changed, 22471 insertions, 0 deletions
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 @@
+<!-- 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/. -->
+<manifest />
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 @@
+<!-- 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/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+ <application>
+ <meta-data
+ android:name="io.sentry.auto-init"
+ android:value="false" />
+ <provider
+ android:name="io.sentry.android.core.SentryInitProvider"
+ android:authorities="${applicationId}.SentryInitProvider"
+ tools:node="remove" />
+ </application>
+</manifest>
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<String, String> = 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<MozillaBreadcrumb>): 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<MozillaBreadcrumb>,
+ 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<Breadcrumb>()
+
+ 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<Breadcrumb>()
+ 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<Breadcrumb>()
+ 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<Breadcrumb>()
+ 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<Breadcrumb>()
+ 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<Breadcrumb>()
+ 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<Breadcrumb>()
+
+ 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<Breadcrumb>()
+
+ 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
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/images/crash-dialog.png
Binary files 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
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/images/crash-in-app.png
Binary files 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 @@
+<!-- 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/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+
+ <application android:supportsRtl="true">
+ <activity android:name=".prompt.CrashReporterActivity"
+ android:process=":mozilla.components.lib.crash.CrashReporter"
+ android:exported="false"
+ android:excludeFromRecents="true"
+ android:theme="@style/Theme.Mozac.CrashReporter" />
+
+ <service android:name=".handler.CrashHandlerService"
+ android:process=":mozilla.components.lib.crash.CrashHandler"
+ android:exported="false"
+ android:foregroundServiceType="specialUse">
+ <property
+ android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+ android:value="This foreground service allows users to report crashes" />
+ </service>
+
+ <!-- Separate process to avoid starting the application when starting this service -->
+ <service android:name=".service.SendCrashReportService"
+ android:process=":crashReportingProcess"
+ android:exported="false"
+ android:foregroundServiceType="specialUse">
+ <property
+ android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+ android:value="This foreground service allows users to report crashes" />
+ </service>
+
+ <!-- Separate process to avoid starting the application when starting this service -->
+ <service android:name=".service.SendCrashTelemetryService"
+ android:process=":crashReportingProcess"
+ android:exported="false"
+ android:foregroundServiceType="specialUse">
+ <property
+ android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+ android:value="This foreground service allows users to report crashes" />
+ </service>
+ </application>
+
+</manifest>
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<Breadcrumb>,
+ 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<Breadcrumb>,
+ 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<Breadcrumb>()
+
+ @Synchronized
+ internal fun copy(): ArrayList<Breadcrumb> {
+ return ArrayList<Breadcrumb>(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<CrashReporterService> = emptyList(),
+ private val telemetryServices: List<CrashTelemetryService> = 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<Breadcrumb> {
+ 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<List<CrashWithReports>>
+
+ /**
+ * 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 -> "<native crash>"
+ 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<ReportEntity>,
+)
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<Breadcrumb>): 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<GleanCrashAction>(
+ 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<Breadcrumb>): 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<Breadcrumb>,
+ ): 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<String, String>? {
+ val map = mutableMapOf<String, String>()
+
+ 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<String>()
+ 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<String>) {
+ 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<String>) {
+ 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<String>,
+ ) {
+ 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<String>,
+ ) {
+ 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<String, String> {
+ var fileReader: FileReader? = null
+ var bufReader: BufferedReader? = null
+ var line: String?
+ val map = HashMap<String, String>()
+
+ 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<String, String> {
+ var resultMap = HashMap<String, String>()
+ 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<CrashViewHolder>() {
+ private var crashes: List<CrashWithReports> = 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<CrashWithReports>) {
+ 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) ?: "<No URL>")
+ 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<TextView>(R.id.mozac_lib_crash_title)
+ val idView = view.findViewById<TextView>(R.id.mozac_lib_crash_id)
+ val footerView = view.findViewById<TextView>(R.id.mozac_lib_crash_footer).apply {
+ movementMethod = LinkMovementMethod.getInstance()
+ }
+}
+
+private fun SpannableStringBuilder.append(
+ crashReporter: CrashReporter,
+ services: List<ReportEntity>,
+ 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<TextView>(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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M3.215,18.106l6.996,-14.004c0.737,-1.475 2.841,-1.475 3.578,0l6.996,14.004A2,2 0,0 1,18.995 21L5.005,21a2,2 0,0 1,-1.79 -2.894zM12,9a1,1 0,0 1,1 1v4a1,1 0,1 1,-2 0v-4a1,1 0,0 1,1 -1zM12,18a1,1 0,1 0,0 -2,1 1,0 0,0 0,2z"
+ android:fillColor="#ffffffff"
+ android:fillType="evenOdd"/>
+</vector>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/mozac_lib_crash_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <TextView
+ android:id="@+id/mozac_lib_crash_empty"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:text="@string/mozac_lib_crash_no_crashes"
+ android:textAlignment="center"
+ android:visibility="gone" />
+
+</FrameLayout> \ 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="8dp">
+
+ <TextView
+ android:id="@+id/titleView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ android:padding="10dp"
+ android:maxLines="3"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="@string/mozac_lib_crash_dialog_title"
+ style="@style/Base.DialogWindowTitle.AppCompat" />
+
+ <TextView
+ android:id="@+id/messageView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ android:padding="8dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/titleView"
+ tools:text="As a private browser, we never save and cannot restore your last browsing session." />
+
+ <CheckBox
+ android:id="@+id/sendCheckbox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ android:checked="true"
+ android:padding="10dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/messageView"
+ tools:text="@string/mozac_lib_crash_dialog_checkbox" />
+
+ <Button
+ android:id="@+id/closeButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:text="@string/mozac_lib_crash_dialog_button_close"
+ android:textAlignment="center"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/sendCheckbox"
+ style="@style/Widget.AppCompat.Button.Borderless.Colored" />
+
+ <Button
+ android:id="@+id/restartButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="8dp"
+ android:text="@string/mozac_lib_crash_dialog_button_restart"
+ android:textAlignment="center"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/closeButton"
+ app:layout_constraintTop_toBottomOf="@+id/sendCheckbox"
+ style="@style/Widget.AppCompat.Button.Borderless.Colored" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_item_crash.xml b/mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_item_crash.xml
new file mode 100644
index 0000000000..a801b4938e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/layout/mozac_lib_crash_item_crash.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="4dp">
+
+ <TextView
+ android:id="@+id/mozac_lib_crash_id"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="2dp"
+ android:textSize="10sp"
+ tools:text="15b666ae-fc9d-41d1-a5c0-8af6961a22d4"
+ tools:ignore="SmallSp" />
+
+ <TextView
+ android:id="@+id/mozac_lib_crash_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/mozac_lib_crash_id"
+ android:padding="2dp"
+ android:textSize="14sp"
+ android:textStyle="bold"
+ tools:text="java.lang.RuntimeException: Background crash" />
+
+ <TextView
+ android:id="@+id/mozac_lib_crash_footer"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/mozac_lib_crash_title"
+ android:padding="2dp"
+ android:textSize="12sp"
+ tools:text="12 minutes ago - Sentry Socorro"/>
+
+</RelativeLayout>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..9555f40a83
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-am/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">አዝናለሁ። %1$s ችግር ነበረበት እና ተሰናክሏል።</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">የብልሽት ሪፖርት ወደ %1$s ላክ</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">ዝጋ</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$sን እንደገና ያስጀምሩ</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">ብልሽቶች</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">አዝናለሁ። በ%1$s ውስጥ ችግር ተፈጥሯል።</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">ሪፖርት ያድርጉ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">የብልሽት ሪፖርት ወደ %1$s በመላክ ላይ</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">የብልሽት ውሂብ በመሰብሰብ ላይ</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">የብልሽት ቴሌሜትሪ መረጃን በመሰብሰብ ላይ</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">የብልሽት ሪፖርቶች</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">ምንም የብልሽት ሪፖርቶች አልገቡም።</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">አጋራ</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..baab129de7
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-an/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">%1$s ha teniu un problema y ha fallau.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Ninviar reporte de fallos a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Zarrar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reiniciar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Fallos</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Reportar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Ninviar reporte de fallos a %1$s</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Reportes de fallos</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No s’ha ninviau garra reporte de fallos.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartir</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..6e5064e869
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ar/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">نأسف. واجه %1$s مشكلة وانهار.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">أرسِل تقرير الانهيار إلى %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">أغلِق</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">أعِد تشغيل %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">الانهيارات</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">أبلِغ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">يُرسل بلاغ الانهيار إلى %1$s</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">بلاغات الانهيار</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">لم تُرسل أي بلاغات بانهيار.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">شارك</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..d4d75fadfa
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ast/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sentímoslo, %1$s tuvo un problema ya cascó.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Unviar l\'informe del error a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Zarrar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reaniciar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Casques</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Sentímoslo, prodúxose un problema en %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Informar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Unviando l\'informe del error a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Recoyendo los datos del casque</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Recoyendo los datos telemétricos del casque</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Informes de casques</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nun s\'unvió nengún informe de casques.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartir</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..c8dfad29d5
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-az/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Üzr istəyirik. %1$s səyyahında xəta oldu və çökdü.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Çökmə hesabatını %1$s üçün göndər</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Qapat</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s tətbiqini yenidən başlat</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Qəzalar</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Xəbər ver</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Çökmə xəbəri %1$s üçün göndərilir</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Çökmə Hesabatları</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Hələlik heç bir çökmə hesabatı göndərilməyib.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Paylaş</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..f17a92f5a0
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-azb/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">باغیشلایین. %1$s موشکولونه اوزلشدی و سیندی.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">سینماق راپورتونو %1$s -یه گؤندر</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">باغلا</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s اَپینی یئنی‌دن باشلات</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">سینماق‌لار</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">باغیشلایین، %1$s اپینده موشکول قاباغا گلدی.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">راپورت</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">سینماق راپورتو %1$s-یا گؤندریلیر</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">سینماق دیتالاری یئغیلیر</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">سینماق تله‌متری دیتاسی یئغیلیر</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">سینماق راپورت‌لاری</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">هئچ بیر سینماق راپورتو گؤندیریلمه‌دی</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">پایلاش</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..f60b516a08
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ban/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Ampura. %1$s wenten galat lan usak.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Gatra</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Bagiang</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..d342233088
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-be/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Прабачце. %1$s меў цяжкасці, і адбыўся збой.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Адправіць справаздачу аб краху ў %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Закрыць</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Перазапусціць %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Крахі</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Прабачце. Узнікла праблема ў %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Паведаміць</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Дасыланне справаздачы пра крах у %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Збор дадзеных пра збой</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Збор дадзеных тэлеметрыі аб збоях</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Cправаздачы пра крахі</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ніякага паведамлення аб збоі даслана не было.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Падзяліцца</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..97d7148bf7
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-bg/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Извинявайте. %1$s имаше проблем и се срина.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Изпращане на докладите за срив до %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Затваряне</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Рестартиране на %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Сривове</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Извинявайте. Възникна проблем с %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Докладване</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Изпращане на докладите за срив до %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Събиране на информация за срива</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Събиране на телеметрични данни за срива</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Доклади за срив</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Няма изпратени доклади за срив.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Споделяне</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..e219bb78aa
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-bn/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">দুঃখিত। %1$s এর একটি সমস্যা ছিল এবং ক্র্যাশ করেছে।</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s এ ক্র্যাশ প্রতিবেদন পাঠান</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">বন্ধ</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s পুনরায় চালু করুন</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">ক্র্যাশসমূহ</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">প্রতিবেদন</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s এ ক্র্যাশ প্রতিবেদন পাঠানো হচ্ছে</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">ক্র্যাশ রিপোর্ট</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">কোনো ক্র্যাশের রিপোর্ট জমা দেওয়া হয়নি।</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">শেয়ার করুন</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..c17ba9bdbc
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-br/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Digarezit, ur gudenn a zo bet gant %1$s ha sacʼhet eo.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Kas an danevell sacʼhadenn da: %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Serriñ</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Adlocʼhañ %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Sacʼhadennoù</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Hon digarezit, degouezhet ez eus bet ur fazi e %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Danevelliñ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">O kas an danevell sacʼhadenn da: %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">O tastum roadennoù ar sac’hadenn</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">O tastum roadennoù telemetrek ar sac’hadenn</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Danevelloù sacʼhadenn</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Danevell sacʼhadenn ebet bet treuzkaset.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Rannañ</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..acb7937611
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-bs/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Oprostite. %1$s je imao problem i srušio se.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Pošalji izvještaj o rušenju %1$s-i</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Zatvori</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Restartuj %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Rušenja</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Nažalost, došlo je do problema u pozadinskom procesu %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Prijavi</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Šaljem izvještaj o rušenju %1$s-i</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Prikupljanje podataka o padu</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Prikupljanje telemetrijskih podataka o padu</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Izvještaji o rušenju</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nema poslanih izvještaja o rušenju.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Podijeli</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..8e201ba93e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ca/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">El %1$s ha tingut un problema i ha fallat.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Envia un informe de fallada a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Tanca</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reinicia el %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Fallades</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">S\'ha produït un problema al %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Informa</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">S’està enviant l’informe de fallada a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">S’estan recollint dades de la fallada</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">S’estan recollint dades de la fallada de telemetria</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Informes de fallada</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No s’ha enviat cap informe de fallada.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Comparteix</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..df611ad6bd
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-cak/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Kojakuyu\'. %1$s xk\'oje\' jun ruk\'ayewal chuqa\' xsach.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Titaq rutzijol sachoj chi re %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Titz\'apïx</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Titikirisäx chik %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Taq sachoj</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Takuyu\'. Xk\'oje\' jun k\'ayewal pa %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Tiya\' rutzijol</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Nitaq rutzijol sachoj chi re %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Rumolik sachoj taq tzij</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Kimolik taq rutzij rusachoj telemetriya\'</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Rutzijol Taq Sachoj</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Man etaqon ta ri taq rutzijol sachoj.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Tikomonïx</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..5b6f44d93c
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sorry. Ang %1$s nagproblema ug nicrash</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">i-Padala ang crash report sa %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">i-Close</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">i-Restart %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Mga Crash</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Report</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">i-Padala ang crash report sa %1$s</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Crash Report</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Walay crash report nga ge submit.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">i-Share</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..91d84be3b9
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">ببورە. %1$s کێشەیەکی هەبوو تێکشکا.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">ڕاپۆرتی داخستنی لەناکاو بنێرە بۆ %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">داخستن</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">پێکردنەوی %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">تێکشکانەکان</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">ڕاپۆرت</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">ناردنی ڕاپۆرتی تێکشکان بۆ %1$s</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">ڕاپۆرتی داخستنی لەناکاو</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">هیچ ڕاپۆرتێکی داخستنی لەناکاو نەنێردراوە.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">بڵاوکردنەوە</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..1f2b3fa104
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-co/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Per disgrazia, %1$s hà scuntratu un prublema chì hà cagiunatu un accidente.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Mandà un raportu d’accidente à %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Chjode</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Rilancià %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Accidenti</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Per disgrazia, un prublema hè accadutu in %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Signalà</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Inviu di u raportu d’accidente à %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Culletta di i dati di l’accidente</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Culletta di i dati di telemetria di l’accidente</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Raporti d’accidente</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Alcunu raportu d’accidente ùn hè statu mandatu.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Sparte</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..95e35643ac
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-cs/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Promiňte. V aplikaci %1$s nastal problém a spadla.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Poslat hlášení o pádu společnosti %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Zavřít</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Restartovat aplikaci %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Pády</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">V aplikaci %1$s došlo k chybě.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Nahlásit</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Odesílání hlášení o pádu společnosti %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Shromažďování dat o pádu</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Shromažďování telemetrických dat o pádu</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Hlášení pádů</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Žádná hlášení nebyla odeslána.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Sdílet</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..ee8b79fc90
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-cy/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Ymddiheuriadau. Cafodd %1$s anhawster a chwalu.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Anfon adroddiad chwalu at %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Cau</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Ailgychwyn %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Chwalfeydd</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Ymddiheuriadau. Digwyddodd anhawster yn %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Adrodd</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Anfon adroddiad chwalu at %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Casglu data chwaliadau</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Casglu data telemetreg chwaliadau</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Adroddiadau Chwalu</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Does dim adroddiadau chwalu wedi eu cyflwyno.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Rhannu</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..e4d0ef279e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-da/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Beklager, %1$s fik et problem og gik ned.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Send fejlrapport til %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Luk</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Genstart %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Nedbrud</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Beklager, men der opstod et problem i %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Rapporter</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Sender fejlrapport til %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Indsamler data om nedbrud</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Indsamler telemetri-data om nedbrud</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Fejlrapporter</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Du har ikke indsendt nogen fejlrapporter.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Del</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..54e7c24653
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-de/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Es tut uns leid. %1$s hatte ein Problem und ist abgestürzt.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Absturzbericht an %1$s senden</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Schließen</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s neu starten</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Abstürze</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Entschuldigung. Bei %1$s ist ein Problem aufgetreten.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Melden</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Absturzbericht wird an %1$s gesendet</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Absturzdaten werden erfasst</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Telemetriedaten zum Absturz werden gesammelt</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Absturzberichte</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Es wurden noch keine Absturzberichte versendet.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Teilen</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..ba05b962f6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Bóžko %1$s jo měł problem a jo se wowalił.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s rozpšawu wowalenja pósłaś</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Zacyniś</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s znowego startowaś</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Wówalenja</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Wódajśo. Problem jo nastał w %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">K wěsći daś</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Wowaleńska rozpšawa se %1$s sćelo</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Daty wowalenja se gromaźe</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Telemetrijowe daty wowalenjow se gromaźe</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Rozpšawy wowalenjow</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Rozpšawy wó wowalenjach njejsu se rozpósłali.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Źěliś</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..452732b283
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-el/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Λυπούμαστε. Το %1$s αντιμετώπισε πρόβλημα και κατέρρευσε.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Αποστολή αναφοράς κατάρρευσης στη %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Κλείσιμο</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Επανεκκίνηση του %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Καταρρεύσεις</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Δυστυχώς, προέκυψε ένα πρόβλημα στο %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Αναφορά</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Αποστολή αναφοράς κατάρρευσης στη %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Συλλογή δεδομένων κατάρρευσης</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Συλλογή δεδομένων τηλεμετρίας κατάρρευσης</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Αναφορές κατάρρευσης</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Δεν έχουν υποβληθεί αναφορές κατάρρευσης.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Κοινή χρήση</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..38b0b2e647
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sorry. %1$s had a problem and crashed.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Send crash report to %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Close</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Restart %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Crashes</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Sorry. A problem occurred in %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Report</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Sending crash report to %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Gathering crash data</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Gathering crash telemetry data</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Crash Reports</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No crash reports have been submitted.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Share</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..38b0b2e647
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sorry. %1$s had a problem and crashed.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Send crash report to %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Close</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Restart %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Crashes</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Sorry. A problem occurred in %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Report</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Sending crash report to %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Gathering crash data</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Gathering crash telemetry data</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Crash Reports</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No crash reports have been submitted.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Share</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..5c7bc08637
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-eo/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Bedaŭrinde %1$s alfrontis problemon kaj paneis.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Sendi raporton pri paneo al %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Fermi</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Restartigi %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Paneoj</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Bedaŭrinde problemo okazis en %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Raporto</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Raporto pri paneo sendata al %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Kolektado de datumoj pri paneo</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Telemezuraj datumoj akirataj</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Raportoj pri paneo</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Neniu raporto pri paneo estis sendita.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Dividi</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..05bfa9734c
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Disculpá. %1$s tuvo un problema y falló.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Enviar informe del fallo a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Cerrar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reiniciar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Fallos</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Disculpá. Ocurrió un problema en %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Informar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Enviando informe de fallo a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Recopilando datos de la colgada</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Recopilación de datos de telemetría de fallos</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Informes de fallos</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No se enviaron informes de fallos.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartir</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..b4cd64fb0d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Lo sentimos. %1$s tuvo un problema y se cayó.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Enviar reporte de fallos a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Cerrar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reiniciar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Fallos</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Lo sentimos. Ocurrió un problema en %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Reportar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Enviando reporte de fallos a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Recopilando datos de fallos</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Recopilación de datos de telemetría de fallos</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Reportes de fallos</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No se ha enviado ningún reporte de fallos.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartir</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..c367b36eb6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Lo sentimos. Hubo un problema con %1$s y se ha cerrado.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Enviar informe de fallos a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Cerrar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reiniciar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Fallos</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Lo sentimos. Ha ocurrido un problema en %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Informe</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Enviando informe de fallo a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Recopilando datos de fallos</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Recopilando datos de telemetría de fallos</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Informes de fallos</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No se ha enviado ningún informe de fallos.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartir</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..f68b71a3ef
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Lo sentimos. Hubo un problema con %1$s y se cerró.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Enviar informe de fallos a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Cerrar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reiniciar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Fallos</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Lo siento. Ha ocurrido un problema en %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Informar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Enviando informe de fallo a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Recopilación de datos de errores</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Recopilación de datos de telemetría de fallos</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Reportes de fallo</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No se han enviado reportes de fallo.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartir</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..c367b36eb6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-es/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Lo sentimos. Hubo un problema con %1$s y se ha cerrado.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Enviar informe de fallos a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Cerrar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reiniciar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Fallos</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Lo sentimos. Ha ocurrido un problema en %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Informe</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Enviando informe de fallo a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Recopilando datos de fallos</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Recopilando datos de telemetría de fallos</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Informes de fallos</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No se ha enviado ningún informe de fallos.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartir</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..53a9e483d6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-et/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Vabandust. %1$sil esines probleem ja see jooksis kokku.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$sle saadetakse vearaport</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Sulge</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Taaskäivita %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Kokkujooksmised</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Vabandust. %1$s esines probleem.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Raporteeri</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Vearaporti saatmine %1$sle</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Vearaportid</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ühtegi vearaportit pole saadetud.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Jaga</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..de1d055c87
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-eu/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Barkatu. %1$s(e)k arazo bat izan du eta huts egin du.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Bidali hutsegite-txostena %1$s(e)ra</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Itxi</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Berrabiarazi %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Hutsegiteak</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Barkatu. Arazo bat gertatu da %1$s(e)n.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Jakinarazi</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Hutsegite-txostena %1$s(e)ra bidaltzen</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Hutsegitearen datuak biltzen</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Hutsegitearen datu telemetrikoak biltzen</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Hutsegite-txostenak</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ez da bidali hutsegite-txostenik.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Partekatu</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..bc98d38b0f
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-fa/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">متأسفیم؛ %1$s مشکلی داشته و فروپاشیده است.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">ارسال گزارش فروپاشی‌ها به %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">بستن</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">شروع دوبارهٔ %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">فروپاشی‌ها</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">متأسفیم؛ مشکلی در %1$s رخ داد.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">گزارش</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">در حال ارسال گزارش فروپاشی به %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">جمع‌آوری داده‌های فروپاشی</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">جمع‌آوری داده‌های دورسنجی فروپاشی</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">گزارش‌های فروپاشی</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">هیچ گزارش فروپاشی‌ای ارسال نشده است.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">هم‌رسانی</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..0b13934ffe
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ff/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Njaafoɗaa. %1$s dañiino caɗeele etee hookii.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Neldu jaŋte kooki e %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Uddu</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Fuɗɗito %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Kooki</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Jaŋtol</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Neldugol jaŋte kooki e %1$s</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Jaŋtol Kooke</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Alaa jaŋte hookre neldaa.</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..6a720554ce
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-fi/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Valitettavasti %1$s kohtasi ongelman ja kaatui.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Lähetä kaatumisraportti %1$slle</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Sulje</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Käynnistä %1$s uudelleen</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Kaatumiset</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">%1$sissa ilmeni ongelma.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Lähetä raportti</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Lähetetään kaatumisraportti %1$slle</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Kerätään kaatumistietoja</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Kerätään kaatumistelemetriatietoja</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Kaatumisraportit</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Kaatumisraportteja ei ole lähetetty.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Jaa</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..87ebf25320
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-fr/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Désolé, %1$s a rencontré un problème et a planté.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Envoyer le rapport de plantage à %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Fermer</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Redémarrer %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Plantages</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Désolé. Un problème est survenu dans %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Signaler</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Envoi du rapport de plantage à %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Collecte des données de plantage</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Collecte des données de télémétrie du plantage</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Rapports de plantage</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Aucun rapport de plantage n’a été envoyé.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Partager</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..ac731a80af
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-fur/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Nus displâs. %1$s al à vût un probleme e al è colassât.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Invie la segnalazion di colàs a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Siere</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Torne invie %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Colàs</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Nus displâs. Al è capitât un probleme in %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Segnale</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Daûr a inviâ la segnalazion di colàs a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Daûr a racuei i dâts sui colàs</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Daûr a racuei i dâts di telemetrie dai colàs</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Segnalazions di colàs</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No je stade mandade nissune segnalazion di colàs.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Condivît</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..5d6dfb1db1
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sorry, %1$s hie in probleem en is ferûngelokke.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Ungelokrapport nei %1$s ferstjoere</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Slute</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s opnij starte</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Ungelokken</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Sorry. Der is in probleem bard yn %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Melde</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Ungelokrapport nei %1$s ferstjoere</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Ungelokgegevens sammelje</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Gegevens oer ûngelok-telemetry sammelje</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Ungelokrapporten</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Der binne gjin ûngelokrapporten ynstjoerd.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Diele</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..0451788058
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Tá %1$s tar éis tuairteála.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Seol tuairisc tuairteála chuig %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Dún</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Atosaigh %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Tuairteanna</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Déan Tuairisc</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Tuairisc tuairteála á seoladh chuig %1$s</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..cb41a84614
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-gd/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Tha sinn duilich ach dh’èirich duilgheadas dha %1$s ’s thuislich e.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Cuir aithisg tuislidh gu %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Dùin</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Ath-thòisich %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Tuislidhean</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Tha sinn duilich ach dh’èirich duilgheadas ann an %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Dèan aithris</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">A’ cur aithisg an tuislidh gu %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">A’ cruinneachadh an dàta mun tuisleadh</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">A’ cruinneachadh dàta telemeatraidh mun tuisleadh</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Aithisgean tuislidh</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Cha deach aithisg air tuisleadh a chur.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Co-roinn</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..46b18fd78b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-gl/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sentímolo. %1$s tivo un problema e fallou.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Enviar informe de fallo a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Pechar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reiniciar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Quebras</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Sentímolo. Ocorreu un problema en %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Informar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Enviar informe de quebra a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Recompilando datos de quebras</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Recompilando datos de telemetría de quebras</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Informes de quebra</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Non se enviou ningún informe de quebra.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartir</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..d1e7c70dc6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-gn/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Rombyasy. %1$s iñapañuãi ha oñemboty.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Emomarandu jejavygua %1$s-pe</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Mboty</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Emoñepyrũjey %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Jejavy</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Ambyasy. Oiko apañuãi %1$s-pe.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Momarandu</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Emomarandu jejavygua %1$s-pe</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Ombyatyhína mba’ekuaarã javypyre</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Telemetría marandu ñembyaty rehegua</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Marandu Javygua</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ndojeguerahaukái jejavy momarandu.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Moherakuã</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..9bff714e6c
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">માફ કરશો. %1$s ને કોઈ સમસ્યા હતી અને ક્રેશ થયું.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$sને ક્રેશ રિપોર્ટ મોકલો</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">બંધ કરો</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s ફરીથી શરૂ કરો</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">ક્રેશ</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">અહેવાલ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s પર ક્રેશ રિપોર્ટ મોકલી રહ્યાં છીએ</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">ભંગાણ અહેવાલો</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">કોઈ ભંગાણ અહેવાલો જમા થયેલ નથી.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">શેર કરો</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..6e7bd2fbb6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">क्षमा करें, %1$s में एक त्रुटि उत्पन्न हुई और क्रैश हो गया।</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s को क्रैश की रिपोर्ट भेजें</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">बंद करें</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s को पुनः प्रारंभ करें</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">क्रैश</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">रिपोर्ट</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s को क्रैश रिपोर्ट भेजा जा रहा है</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">क्रैश रिपोर्ट</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">कोई क्रैश रिपोर्ट जमा नहीं किया गया है।</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">साझा करें</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..749f53f61e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-hil/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Isarado</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Crashes</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Ibalita</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..f82ee68fca
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-hr/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Oprosti, %1$s je imao problem i urušio se.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Pošalji izvještaj o rušenju na %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Zatvori</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Ponovo pokreni %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Rušenja</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Oprostite. Došlo je do problema u %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Prijavi</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Šalje se izvještaj o rušenju na %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Prikupljanje podataka o padu</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Prikupljanje telemetrijskih podataka o padu</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Izvještaji rušenja</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nema poslanih izvještaja rušenja.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Podijeli</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..d838bfa9e4
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Bohužel je %1$s problem měł a spadnył.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s spadowu rozprawu pósłać</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Začinić</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s znowa startować</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Spady</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Wodajće. Problem je w %1$s nastał.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Zdźělić</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Spadowa rozprawa so %1$s sćele</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Daty spada so hromadźa</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Telemetrijowe daty spadow so hromadźa</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Rozprawy wo spadach</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Rozprawy wo spadach njejsu so rozpósłali.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Dźělić</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..e5529da97b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-hu/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sajnáljuk. A %1$s problémába ütközött és összeomlott.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Összeomlás-jelentés elküldése a %1$s számára</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Bezárás</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s újraindítása</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Összeomlások</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Elnézést. Probléma történt itt: %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Jelentés</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Összeomlás-jelentés elküldése a %1$s számára</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Összeomlási adatok gyűjtése</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Összeomlási telemetriai adatok gyűjtése</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Összeomlásjelentések</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nem volt még beküldve jelentés.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Megosztás</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..b30eed3fce
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Ներողություն. %1$s-ը խնդիր ունեցավ և խափանվեց:</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Ուղարկել վթարի զեկույցը %1$sին</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Փակել</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Վերամեկնարկել %1$s-ը</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Վթարներ</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Ներողություն. %1$s-ում խնդիր է առաջացել:</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Զեկույց</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Ուղարկել վթարի զեկույցը %1$s-ին</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Վթարի տվյալների հավաքում</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Վթարի հեռաչափության տվյալների հավաքում</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Վթարի զեկույց</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Վթարային ոչ մի հաղորդագրություն չի ուղարկվել:</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Տարածել</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..841dcecd25
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ia/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Nos regretta. %1$s habeva un problema e collabeva.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Inviar reporto de crash a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Clauder</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reinitiar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Crashes</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Desolate. Un problema occurreva in %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Reportar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Invio de reporto de crash a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Collection datos de crash</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Colligente datos de telemetria de crash</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Reportos de collapso</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nulle reportos de collapso esseva submittite.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartir</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..24f7d67e80
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-in/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Maaf. %1$s mengalami masalah dan mogok.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Kirim laporan mogok ke %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Tutup</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Mulai Ulang %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Mogok</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Maaf. Terjadi masalah pada %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Laporkan</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Kirim laporan mogok ke %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Mengumpulkan data mogok</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Mengumpulkan data telemetri mogok</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Laporan Kerusakan</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Tidak ada laporan kerusakan yang pernah dikirim.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Bagikan</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..e13fe8bef7
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-is/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Því miður þá lenti %1$s í erfiðleikum og lokaðist.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Senda hrunskýrslu til %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Loka</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Endurræsa %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Hrun</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Því miður. Vandamál kom upp í %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Skýrsla</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Senda hrunaskýrslu til %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Safna hrungögnum</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Safna fjarmælingargögnum um hrun</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Hrunskýrslur</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Engar hrunaskýrslur hafa verið sendar.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Deila</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..3a1cb7deef
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-it/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Si è verificato un problema in %1$s che ha provocato un arresto anomalo.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Invia la segnalazione di arresto anomalo a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Chiudi</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Riavvia %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Arresti anomali</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Siamo spiacenti. Si è verificato un problema in %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Segnala</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Invio in corso della segnalazione di arresto anomalo a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Raccolta dei dati sugli arresti anomali</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Raccolta dei dati di telemetria relativi agli arresti anomali</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Segnalazioni di arresto anomalo</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Non è stata inviata alcuna segnalazione.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Condividi</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..36da8bccdb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-iw/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">קרתה תקלה עם %1$s שהובילה לקריסה. עמך הסליחה.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">שליחת דיווח קריסה אל %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">סגירה</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">הפעלת %1$s מחדש</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">קריסות</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">מצטערים. אירעה שגיאה ב־%1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">דיווח</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">דיווח קריסה נשלח אל %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">בתהליך איסוף נתוני קריסה</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">בתהליך איסוף נתוני קריסה</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">דיווחי קריסה</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">לא נשלחו דיווחי קריסה.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">שיתוף</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..94f58ef947
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ja/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">申し訳ありません。%1$s に問題がありクラッシュしました。</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">クラッシュレポートを %1$s へ送信する</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">閉じる</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s を再起動</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">クラッシュ</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">申し訳ありません。%1$s で問題が発生しました。</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">レポート</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">クラッシュレポートを %1$s へ送信しています</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">クラッシュデータを収集しています</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">クラッシュのテレメトリーデータを収集しています</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">クラッシュレポート</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">送信したクラッシュレポートはありません。</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">共有</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..ca895abf3d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ka/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">ვწუხვართ. %1$s გაუმართაობის გამო გაითიშა.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">გათიშვის მოხსენების გადაგზავნა %1$s-სთვის</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">დახურვა</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">ხელახლა გაეშვას %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">უეცარი გათიშვები</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">სამწუხაროდ, ხარვეზს წააწყდა %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">მოხსენება</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">გათიშვის მოხსენება ეგზავნება %1$s-ს</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">გროვდება გათიშვის მონაცემები</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">აღირიცხება უეცარი გათიშვის მონაცემები</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">გათიშვების მოხსენებები</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">გათიშვების მოხსენებები არ გაგზავნილა.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">გაზიარება</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..3a741dc889
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Keshiresiz. %1$s da mashqala sebepli nasazlıq júz berdi.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Nasazlıq haqqındaǵı maģlıwmattı %1$s ǵa jiberiw</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Jabıw</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s qayta baslaw</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Nasazlıqlar</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Keshiresiz. %1$s da nasazlıq júz berdi.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Xabar berıw</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Nasazlıq esabattı %1$s ǵa jiberilip atır</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Nasazlıq haqqındaǵı maģlıwmatlardı toplaw</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Telemetriyanıń nasazlıq haqqındaǵı maǵlıwmatların jıynaw</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Nasazlıqlar haqqında esabatlar</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Hesh qanday nasazlıq haqqında esabat jiberilmegen</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Bólisiw</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..093ac3608f
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-kab/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Nesḥasef. %1$s isεa ugur sakin yeɣli.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Azen aneqqis n uɣelluy i %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Mdel</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Ales asenker n %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Aɣelluy</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Nesḥassef. Yeḍra-d wugur deg %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Aneqqis</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Tuzna n uneqqis n uɣelluy i %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Alqaḍ n yisefka yerrẓen</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Alqaḍ n yisefka n tilisɣelt yerrẓen</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Ineqqisen n uɣelluy</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ula d yiwen n uneqqis n uɣelluy ur yettwazen.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Bḍu</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..631c4cda4e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-kk/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Кешіріңіз. %1$s мәселеге тап болды және құлады.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s адресіне құлау жөнінде хабарламаны жіберу</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Жабу</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s қайта қосу</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Құлаулар</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Кешіріңіз. %1$s ішінде қате орын алды.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Хабарлау</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s адресіне құлау жөнінде хабарламаны жіберу</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Құлау деректерін жинау</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Құлау телеметрия деректерін жинау</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Құлау туралы хабарлар</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Құлау туралы ешбір хабар жіберілмеген.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Бөлісу</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..1ac1afcd07
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Bibore. %1$s li rastî pirsgirêkekê hat û têk çû.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Rapora têkçûnê ji %1$s’ê re bişîne</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Bigire</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s’ê dîsa bide destpêkirin</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Têkçûn</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Bibore. Di %1$s`ê de problemek derket.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Rapor bike</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Rapora têkçûnê ji %1$s’ê re tê şandin</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Agahiyên têkçûnê tên berhevkirin</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Agahiyên têkçûnê tên berhevkirin</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Raporên Têkçûnê</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Heta niha ti raporên têkçûnê nehatine şandin.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Parve bike</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..ec0ced1ab7
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-kn/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">ಕ್ಷಮಿಸಿ. %1$s ಸಮಸ್ಯೆ ಮತ್ತು ಕ್ರ್ಯಾಶ್ ಆಗಿದೆ.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">ಕ್ರ್ಯಾಶ್ ವರದಿಯನ್ನು %1$s ಗೆ ಕಳುಹಿಸಿ</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">ಮುಚ್ಚು</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s ಅನ್ನು ಮರು ಆರಂಭಿಸು</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">ಕುಸಿತಗಳು</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">ವರದಿ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">ಕ್ರ್ಯಾಶ್ ವರದಿಯನ್ನು %1$s ಗೆ ಕಳುಹಿಸಿ</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">ಕ್ರಿಯಾವೈಫಲ್ಯ ವರದಿಗಳು</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">ಯಾವುದೆ ಕುಸಿತ ವರದಿಗಳನ್ನು ಸಲ್ಲಿಸಲಾಗಿಲ್ಲ.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">ಹಂಚು</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..c3b3ce333e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ko/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">죄송합니다. %1$s에 문제가 발생하여 충돌했습니다.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s에 충돌 보고서 보내기</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">닫기</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s 다시 시작</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">충돌</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">죄송합니다. %1$s에서 문제가 발생했습니다.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">보고하기</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s에 충돌 보고서 보내기</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">충돌 데이터 수집 중</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">충돌 원격 분석 데이터 수집 중</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">충돌 보고서</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">전송한 충돌 보고서가 없습니다.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">공유</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..13283a6e5d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-lij/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Ne spiaxe. %1$s o l\'à avuto \'n problema e o s\'é ciantou.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Manda a segnalaçion do cianto a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Særa</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Arvi torna %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Cianti</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Denunçia</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Mando o report do cianto a %1$s</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Segnalaçion di Cianti</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nisciun report mandou.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Condividdi</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..7be9bbe480
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-lo/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">ຂໍ​ອະໄພ. %1$s ມີປັນຫາ ແລະ ລົ້ມເຫລວ.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">ສົ່ງລາຍງານການຂັດຂ້ອງໄປຫາ %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">ປິດ</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">ລີສຕາດ %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">ການຂັດຂ້ອງ</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">ຂໍ​ອະໄພ. ໄດ້ມີບັນຫາເກີດຂື້ນໃນ %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">ລາຍງານ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">ກຳລັງສົ່ງລາຍງານການຂັດຂ້ອງໄປຫາ %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">ກຳລັງເກັບກຳຂໍ້ມູນມທີມີບັນຫາ</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">ກຳລັງເກັບກຳຂໍ້ມູນ telemetry crash</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">ລາຍງານຂໍ້ຂັດຂ້ອງ</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">ຍັງບໍ່ເຄີຍສົ່ງລາຍງານຂໍ້ຜິດພາດຈັກເທື່ອ.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">ແບ່ງປັນ</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..1915fd6bd8
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-lt/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Atsiprašome. „%1$s“ susidūrė su problema ir užstrigo.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Pranešti apie strigtį „%1$s“</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Užverti</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Paleisti „%1$s“ iš naujo</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Strigtys</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Atsiprašome. „%1$s“ susidūrė su problema.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Siųsti pranešimą</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Strigties pranešimas siunčiamas į „%1$s“</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Renkami strigčių duomenys</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Renkami strigčių telemetrijos duomenys</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Strigčių pranešimai</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Išsiųstų strigčių pranešimų nėra.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Dalintis</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..1b9f328dbc
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ml/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">ക്ഷമിക്കണം. %1$s ന് ഒരു പ്രശ്‌നമുണ്ടായി, തകർന്നു.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">തകരാർ റിപ്പോർട്ട് %1$s ലേക്ക് അയയ്ക്കുക</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">അടയ്ക്കുക</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s പുനരാരംഭിക്കുക</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">തകരാറുകള്‍</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">റിപ്പോര്‍ട്ട്</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">തകരാ‍ർ വിവരണം %1$s ലേക്ക് അയയ്ക്കുന്നു</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">തകരാർ വിവരണം</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">തകരാർ വിവരങ്ങൾ സമര്‍പ്പിച്ചിട്ടില്ല.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">പങ്കിടുക</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..33ef33ca32
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-mr/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">क्षमस्व. %1$s मध्ये समस्या आली आणि बंद पडला.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">समस्येचा अहवाल %1$s ला पाठवा</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">बंद</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s पुन्हा सुरू करा</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">क्रॅश</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">अहवाल द्या</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">समस्येचा चा अहवाल %1$s ला पाठवत आहे</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">क्रॅश अहवाल</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">कुठलेही क्रॅश अहवाल दाखल केले गेले नाहीत.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">सामायिक करा</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..d60c7f80cb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-my/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">ဝမ်းနည်းပါတယ်။ %1$s တွင် ပြဿနာတစ်ခု ပေါ်ခဲ့သဖြင့် ရပ်ဆိုင်းသွားသည်။</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s သို့ ပျက်စီးမှုအစီရင်ခံစာပို့ပါ</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">ပိတ်ပါ</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s ကိုပြန်စပါ</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">ပျက်စီးမှုများ</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">အစီရင်ခံပါ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s သို့ ပျက်စီးမှုအစီရင်ခံစာပို့ပါ</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">ပျက်ဆီးမှု အစီရင်ခံစာများ</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">မည်သည့် ပျက်စီးမှု အစီရင်ခံစာ မျှ မတင်သွင်းထားပါ။</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">မျှဝေ</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..12f4477c51
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Beklager. %1$s fikk problem og krasjet.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Send krasjrapport til %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Lukk</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Start %1$s på nytt</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Krasj</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Beklager. Det oppsto et problem i %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Rapporter</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Sender krasjrapport til %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Samler inn krasjdata</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Samler krasj-telemetridata</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Krasjrapporter</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ingen krasjrapporter er sendt inn.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Del</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..2ee6319010
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">माफ गर्नुहोस्। %1$s मा समस्या थियो र क्र्यास भएको थियो।</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s लाई क्र्यास प्रतिबेदन पठाउनुहोस्</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">बन्द गर्नुहोस्</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s पुनः सुचारु गर्नुहोस्</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">क्र्यासहरु</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">माफ गर्नुहोस्। %1$s मा एउटा समस्या आयो।</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">रिपोर्ट</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s लाई क्र्यास प्रतिबेदन पठाइँदै</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">क्र्यास प्रतिवेदनहरु</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">कुनै पनि क्रा्यास प्रतिबेदनहरु पेश गरिएको छैन।</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">सेयर</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..f0ed896cc7
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-nl/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sorry. %1$s had een probleem en is gecrasht.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Crashrapport naar %1$s verzenden</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Sluiten</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s herstarten</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Crashes</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Sorry. Er is een probleem opgetreden in %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Melden</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Crashrapport naar %1$s verzenden</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Crashgegevens verzamelen</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Gegevens over crash-telemetrie verzamelen</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Crashrapporten</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Er zijn geen crashrapporten verzonden.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Delen</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..7406570a66
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Beklagar. %1$s fekk problem og krasja</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Send krasjrapport til %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Lat att</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Start %1$s på nytt</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Krasj</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Beklagar. Det oppsto eit problem i %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Rapporter</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Sender krasjrapport til %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Samlar inn krasjdata</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Samlar krasj-telemetridata</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Krasjrapportar</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ingen krasjrapportar er sende inn.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Del</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..58f48f7a0c
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-oc/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">O planhèm. %1$s a rescontrat un problèma e a quitat de foncionar.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Enviar un senhalament de bug a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Tampar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reaviar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Plantatges</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Desolat. Un problèma s’es produch dins %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Senhalar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Mandadís del rapòrt de plantatge a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Amassada de las donadas de plantatge</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Amassada de las donadas de telemetria de plantatge</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Rapòrts de plantatge</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Cap de rapòrt de plantatge es pas estat mandat.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Partejar</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..39b371daa1
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">ਅਫ਼ਸੋਸ ਹੈ। %1$s ਨੂੰ ਸਮੱਸਿਆ ਆਈ ਤੇ ਕਰੈਸ਼ ਹੋ ਗਿਆ ਹੈ।</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">ਕਰੈਸ਼ ਰਿਪੋਰਟ %1$s ਨੂੰ ਭੇਜੋ</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">ਬੰਦ ਕਰੋ</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s ਮੁੜ-ਚਾਲੂ ਕਰੋ</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">ਕਰੈਸ਼</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">ਅਫ਼ਸੋਸ ਹੈ। %1$s ਵਿੱਚ ਸਮੱਸਿਆ ਆਈ ਹੈ।</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">ਰਿਪੋਰਟ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">ਕਰੈਸ਼ ਰਿਪੋਰਟ %1$s ਨੂੰ ਭੇਜੀ ਜਾ ਰਹੀ ਹੈ</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">ਕਰੈਸ਼ ਸੰਬੰਧੀ ਡਾਟੇ ਨੂੰ ਇਕੱਤਰ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">ਕਰੈਸ਼ ਟੈਲੀਮੈਂਟਰੀ ਡਾਟਾ ਇਕੱਤਰ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">ਕਰੈਸ਼ ਰਿਪੋਰਟਾਂ</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">ਕੋਈ ਕਰੈਸ਼ ਰਿਪੋਰਟ ਨਹੀਂ ਦਿੱਤੀ ਗਈ</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">ਸਾਂਝਾ ਕਰੋ</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..f31d7f3974
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">بند کرو</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">ریپورٹ کرو</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">سانجھا کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..bd70ea643b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-pl/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">%1$s uległ awarii.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Zgłoś awarię organizacji %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Zamknij</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Uruchom program %1$s ponownie</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Awarie</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">%1$s uległ awarii</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Zgłoś</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Zgłaszanie awarii organizacji %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Zbieranie danych o awarii</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Zbieranie danych telemetrycznych awarii</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Zgłoszenia awarii</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nie przesłano żadnych zgłoszeń awarii.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Udostępnij</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..9da369c166
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Desculpe, o %1$s teve um problema e travou.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Enviar relatório de travamento para a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Fechar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reiniciar o %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Travamentos</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Desculpe, houve um problema no %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Relatar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Enviando relatório de travamento para a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Coletando dados de falha</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Recolhendo dados de telemetria de travamentos</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Relatórios de travamento</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nenhum relatório de travamento foi enviado.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Compartilhar</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..4d52fb69b6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Desculpe. %1$s teve um problema e falhou.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Enviar relatório de falha para %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Fechar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reiniciar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Falhas</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Pedimos desculpa. Ocorreu um problema no %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Reportar</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">A enviar relatório de falha para %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">A reunir dados de falha</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Recolha de dados de telemetria de falhas</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Relatórios de falhas</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Não foram submetidos relatórios de falhas.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Partilhar</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..0787a90e99
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-rm/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Perstgisa. %1$s ha gì in problem ed è collabà.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Trametter in rapport da collaps a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Serrar</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reaviar %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Collaps</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Perstgisa. Igl ha dà in problem en %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Rapport</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Spediziun dal rapport da collaps a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Datas da collaps vegnan rimnadas</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Rimnada da datas da telemetria davart collaps</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Rapports da collaps</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Anc nagins rapports da collaps tramess.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Cundivider</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..0d67a3b1ba
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ro/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Ne pare rău. %1$s a avut o problemă și s-a închis neașteptat.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Trimite raportul de defecțiune la %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Închide</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Repornește %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Defecțiuni</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Raportează</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Se trimite raportul de defecțiune la %1$s</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Rapoarte de defecțiuni</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nu au fost trimise rapoarte de defecțiuni.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Partajează</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..c8629a9534
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ru/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Извините. В %1$s возникла проблема и произошёл сбой.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Отправлять сообщения о падениях в %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Закрыть</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Перезапустить %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Падения</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Извините. В %1$s возникла проблема.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Сообщить</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Сообщение о падении отправляется в %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Сбор данных о падении</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Сбор данных телеметрии о падениях</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Сообщения о падениях</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ни одного сообщения о падении отправлено не было.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Сообщить</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..037564fa1e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-sat/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">ᱤᱠᱟᱹ %1$s ᱥᱟᱶ ᱛᱮ ᱫᱤᱜᱫᱷᱟ ᱦᱚᱭ ᱱᱟ ᱟᱨ ᱨᱟᱹᱯᱩᱫᱮᱱᱟ ᱾</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s ᱴᱷᱮᱱ ᱨᱟᱹᱯᱩᱫ ᱨᱤᱯᱚᱨᱴ ᱠᱩᱞ ᱢᱮ</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">ᱵᱚᱸᱫᱚᱭ ᱢᱮ</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s ᱫᱩᱦᱲᱟᱹ ᱮᱦᱚᱵᱽ ᱢᱮ</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">ᱨᱟᱹᱯᱩᱫ ᱠᱚ</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">ᱤᱠᱟᱹ ᱾ %1$s ᱨᱮ ᱵᱷᱩᱞ ᱦᱩᱭᱮᱱᱟ ᱾</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">ᱨᱤᱯᱚᱴ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s ᱴᱷᱮᱱ ᱨᱟᱹᱯᱩᱫ ᱨᱤᱯᱚᱴ ᱠᱩᱞ ᱦᱩᱭᱩ ᱠᱟᱱᱟ</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">ᱠᱨᱟᱥ ᱰᱟᱴᱟ ᱡᱟᱣᱨᱟᱜ ᱠᱟᱱᱟ</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">ᱠᱨᱟᱥ ᱴᱮᱞᱤᱢᱮᱴᱨᱤ ᱰᱟᱴᱟ ᱡᱟᱣᱨᱜ ᱠᱟᱱᱟ</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">ᱨᱟᱹᱯᱩᱫ ᱨᱤᱯᱚᱨᱴ ᱠᱚ</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">ᱚᱠᱟ ᱦᱚᱸ ᱨᱟᱹᱯᱩᱫ ᱨᱤᱯᱚᱨᱴ ᱡᱚᱢᱟ ᱵᱟᱝ ᱦᱩᱭ ᱠᱟᱱᱟ ᱾</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">ᱦᱟᱹᱴᱤᱧ</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..7e95e3a65e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-sc/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">%1$s at tentu unu problema e est faddidu.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Imbia s’informe de faddina a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Serra</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Torra a aviare %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Faddinas</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Ddoe est istada una faddina in %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Sinnala</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Imbiende s’informe de faddina a %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Regollende datos de sa faddina</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Si sunt regollende is datos de telemetria de sa faddina</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Informes de faddinas</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nissunu informe de faddina imbiadu.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Cumpartzi</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..a58aed9c7c
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-si/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">කණගාටුයි. %1$s හි ගැටලුවක් මතු වී බිඳ වැටුණි.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">බිඳ වැටීමේ වාර්තාව %1$s වෙත යවන්න</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">වසන්න</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s යළි අරඹන්න</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">බිඳ වැටීම්</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">කණගාටුයි. %1$s හි ගැටලුවක් මතු විය.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">වාර්තාව</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">බිඳ වැටීමේ වාර්තාව %1$s වෙත යවමින්</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">බිඳ වැටීමේ දත්ත එකතැන් වෙමින්</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">බිඳවැටීම් වාර්තා</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">බිඳවැටීමේ වාර්තා කිසිවක් යොමු කර නැත.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">බෙදාගන්න</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..2d35f006a0
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-sk/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Ospravedlňujeme sa. Aplikácia %1$s narazila na problém a zlyhala.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Odoslať správu o zlyhaní spoločnosti %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Zavrieť</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Reštartovať %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Zlyhania</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Ospravedlňujeme sa. Vyskytol sa problém s aplikáciou %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Nahlásiť</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Odosielanie správy o zlyhaní spoločnosti %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Zhromažďujú sa údaje o zlyhaní</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Zhromažďujú sa telemetrické údaje o zlyhaní</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Správy o zlyhaní</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Neboli odoslané žiadne správy o zlyhaní.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Zdieľať</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..6cda4101b6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-skr/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">افسوس۔ %1$s وچ کوئی مسئلہ ہے تے تباہ تھی ڳئے۔</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s کوں کریش رپوٹ بھیڄو</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">بند کرو</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s ولدا شروع کرو</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">کریش</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">افسوس۔ %1$s وچ ہک مسئلہ تھی ڳیا ہے۔</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">رپورٹ کرو</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s کوں کریش رپوٹ بھیڄیندا پئے</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">کریش ڈیٹا کٹھا کریندا پئے</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">کریش ٹیلی میٹری ڈیٹا کٹھا کرݨ</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">کریش رپورٹاں</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">کوئی کریش رپوٹاں جمع کائنی کرائیاں۔</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">شیئر</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..a0a020d1c4
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-sl/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Oprostite. %1$s je naletel na težavo in se sesul.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Pošlji poročilo o sesutju organizaciji %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Zapri</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Ponovno zaženi %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Sesutja</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Oprostite. V %1$su je prišlo do težave.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Prijavi</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Pošiljanje poročila o sesutju organizaciji %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Zbiranje podatkov o sesutju</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Zbiranje telemetričnih podatkov o sesutju</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Poročila o sesutjih</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nobeno poročilo o sesutju ni bilo poslano.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Deli</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..90dadd927d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-sq/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Na ndjeni. %1$s pati një problem dhe u vithis.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Dërgoni raport vithisjeje te %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Mbylle</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Rinise %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Vithisje</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Na ndjeni. Ndodhi një problem në %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Raportoje</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Po dërgohet njoftim vithisjeje te %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Po mblidhen të dhëna vithisjeje</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Po mblidhen të dhëna telemetrike vithisjeje</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Njoftime Vithisjesh</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nuk ka të parashtruar njoftime vithisjesh.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Ndajeni me të tjerët</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..1e5cefabbd
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-sr/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Извињавам се. %1$s је имао проблем и срушио се.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Пошаљи извештај о паду на %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Затвори</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Поново покрени %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Пад</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Нажалост, догодио се проблем у позадинском процесу %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Извештај</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Слање извештаја о паду на %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Прикупљање података о паду</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Прикупљање телеметријских података о паду</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Извештаји о рушењу</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ниједан извештај о рушењу није поднесен.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Подели</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..e620497e6f
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-su/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Hampura. %1$s aya masalah tur ruksak.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Kirim laporan nu ruksak ka %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Tutup</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Mimitian deui %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Karuksakan</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Hampura. Aya masalah dina %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Laporan</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Kirim laporan nu ruksak ka %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Ngumpulkeun data ruksak</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Ngumpulkeun data telemétri ruksak</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Laporan Karuksakan</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Teu aya laporan karuksakan nu tos dipasihkeun.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Bagikeun</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..a4c7ed9511
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Förlåt. %1$s hade problem och kraschade.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Skicka kraschrapport till %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Stäng</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Starta om %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Krascher</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Förlåt. Ett problem uppstod i %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Rapportera</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Skicka kraschrapport till %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Samlar in kraschdata</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Samlar in krasch-telemetridata</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Kraschrapporter</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Inga kraschrapporter har skickats in.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Dela</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..39047ebb22
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ta/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">மன்னிக்க. %1$s சிக்கலேற்பட்டுச் செயலிழந்தது.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s க்குச் சிதைவு அறிக்கையை அனுப்பு</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">மூடுக</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s ஐ மறுதுவக்கு</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">செயலிழப்புகள்</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">அறிக்கையிடுக</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">செயலிழப்பு அறிக்கையை %1$s க்கு அனுப்புகிறது</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">சிதைவு அறிக்கைகள்</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">எந்தச் சிதைவு அறிக்கைகளும் சமர்பிக்கப்படவில்லை.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">பகிர்</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..3a7ea16d05
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-te/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">క్షమించండి. ఏదో సమస్య వల్ల %1$s క్రాష్ అయ్యింది.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">క్రాష్ నివేదికను %1$s‌కి పంపించు</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">మూసివేయి</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s‌ను పునఃప్రారంభించు</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">క్రాషులు</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">నివేదించు</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">క్రాష్ నివేదికను %1$s‌ కి పంపిస్తోంది</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">క్రాష్ నివేదికలు</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">క్రాష్ నివేదికలేమీ సమర్పించబడలేదు.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">పంచుకోండి</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..ed7ddd9b30
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-tg/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Мутаассифона, %1$s мушкилӣ дошта, бо вайронӣ дучор шуд.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Фиристодани гузориш дар бораи вайронӣ ба %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Пӯшидан</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Аз нав оғоз кардани %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Вайрониҳо</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Бубахшед. Дар %1$s мушкилӣ ба миён омад.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Гузориш додан</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Гузориш дар бораи вайронӣ ба %1$s фиристода шуда истодааст</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Ҷамъоварии маълумот дар бораи вайрониҳо</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Ҷамъоварии маълумот дар бораи вайрониҳои дурсанҷӣ (телеметрия)</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Гузоришҳо дар бораи вайронӣ</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ягон гузориш дар бораи вайронӣ пешниҳод карда нашуд.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Мубодила кардан</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..9f1fe18638
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-th/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">ขออภัย %1$s มีปัญหา และหยุดการทำงานแล้ว</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">ส่งรายงานข้อขัดข้องไปยัง %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">ปิด</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">เริ่ม %1$s ใหม่</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">ข้อขัดข้อง</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">ขออภัย เกิดข้อผิดพลาดใน %1$s</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">รายงาน</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">กำลังส่งรายงานข้อขัดข้องไปยัง %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">กำลังรวบรวมข้อมูลข้อขัดข้อง</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">กำลังรวบรวมข้อมูลการวัดและส่งข้อมูลทางไกลเกี่ยวกับข้อขัดข้อง</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">รายงานข้อขัดข้อง</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">ยังไม่เคยมีการรายงานข้อขัดข้อง</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">แบ่งปัน</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..1b7d285a21
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-tl/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Paumanhin. Nagkaproblema ang %1$s.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Ipadala ang crash report sa %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Isara</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">i-Restart ang %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Mga crash</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Iulat</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Ipinadadala ang crash report sa %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Nangongolekta ng data ng pag-crash</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Pagtitipon ng data ng telemetry ng pag-crash</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Mga Crash Report</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Walang pang mga crash report na naipadala.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Ibahagi</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..c4b923ffb7
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-tr/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">%1$s bir sorunla karşılaştı ve çöktü.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Çökme raporunu %1$s’ya gönder</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Kapat</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s uygulamasını yeniden başlat</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Çökmeler</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">%1$s uygulamasında bir sorun oluştu.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Raporla</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Çökme raporu %1$s\'ya gönderiliyor</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Çökme verileri toplanıyor</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Çökme verileri toplanıyor</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Çökme Raporları</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Henüz hiç çökme raporu gönderilmedi.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Paylaş</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..f5d345a3e7
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-trs/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sī ga\'man ruhuât. %1$s ga \'ngō sañuun riñanj.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Gā\'nïnj gan\'ānj nuguan\' rayi\'î sa \'iaj re\'ej riña %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Nārán</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Nāyi\'ì ñû %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Nej sa gahui a\'nan\'</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Sī ga’man ruhuât. Huā sa gahui a’nan’ riña %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Gānātà\'</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Hīaj a\'nïn hua\'ānj nuguan\' rayi\'î sa \'iaj re\' riña %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Sa naran’ andaj gire’ riña aga’ nan</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Sa naran’ andaj telemetría gire’ riña aga’ nan</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Nuguan\' nata\' sa gahui a\'nan\'an</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Nu gan\'ānj gà\' si \'ngō nuguan\' ganata\'a.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Dūyingô\'</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..8056936c37
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-tt/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Гафу. %1$s хатага юлыкты һәм ватылды.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s адресына ватылу турында хәбәр җибәрү</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Ябу</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s-ны яңадан ачу</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Өзеклеклер</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Гафу итегез. %1$s кушымтасында проблема килеп чыкты.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Шикаять итү</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s адресына ватылу турында хәбәр җибәрү</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Ватылу турында мәгълүмат җыю</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Ватылу турында телеметрия мәгълүматларын туплау</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Ватылу турында хәбәрләр</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Ватылу турында хәбәрләр җибәрелмәгән.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Уртаклашу</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..9005336bc5
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Mḍel</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Bḍu</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..d968d43ce4
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ug/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">كەچۈرۈڭ، %1$s مەسىلىگە يولۇقۇپ يىمىرىلدى.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">يىمىرىلىش دوكلاتىنى %1$s غا يوللايدۇ</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">تاقاش</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart"> %1$s نى قايتا قوزغات</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">يىمىرىلىش</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">كەچۈرۈڭ، %1$s دا مەسىلە كۆرۈلدى.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">دوكلات</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">يىمىرىلىش دوكلاتىنى %1$s غا يوللاۋاتىدۇ</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">يىمىرىلىش سانلىق مەلۇماتلىرىنى توپلاۋاتىدۇ</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">يىمىرىلىش تېلېگراف سانلىق مەلۇماتلىرىنى توپلاۋاتىدۇ</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">يىمىرىلىش دوكلاتى</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">يىمىرىلىش دوكلاتى تاپشۇرۇلمىدى.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">ھەمبەھىرلەش</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..26c3f264f2
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-uk/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Вибачте. Виникла проблема з %1$s і стався збій.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Надіслати звіт про збій до %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Закрити</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Перезапустити %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Збої</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Вибачте. Виникла проблема в %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Звіт</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Надсилання звіту про збій до %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Збір даних про збої</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Збір даних телеметрії про збої</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Звіти про збої</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Жодних звітів про збої не надсилалось.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Поділитися</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..dfd0f6c7d5
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-ur/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">معاف کریں، %1$s میں کوئی خرابی آئی ہے اور کریش ہو گئی ہے۔</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">%1$s کو کریش رپورٹ بھیجیں</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">بند کریں</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$s دوبارہ شروع کریں</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">کریش</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">رپورٹ</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$s کو کریش رپورٹ بھیجآ جا رہا ہے</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">کریش رپورٹیں</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">کوئی کریش رپورٹیں ارسال نہی کی گئی۔</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">شیئر کریں</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..41ca048ca5
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-uz/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Kechirasiz, %1$s ilovasida muammo yuz berdi.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Nosozlik maʼlumotini %1$sga yuborish</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Yopish</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">%1$sni qayta ishga tushirish</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Nosozliklar</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Kechirasiz. %1$s da muammo yuz berdi.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Hisobot berish</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">%1$sga nosozlik hisobotini yuborlmoqda</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Nosozlik maʼlumotlari yigʻilmoqda</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Nosozlik telemetriya maʼlumotlari yigʻilmoqda</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Nosozlik hisobotlari</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Hech qanday nosozlik hisobotlari yuborilmadi.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Ulashish</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..90f030e2f5
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-vec/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Se xe verificà on problema en %1$s che gà provocà on aresto anomaƚo.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Manda na segnaƚasion de aresto anomaƚo a %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Sara su</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Invia de novo %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Aresto anomaƚo</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Segnaƚa</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Son drio mandare ƚa segnaƚasion de aresto anomaƚo a %1$s</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..13cae1333c
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-vi/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Rất tiếc. %1$s đã gặp sự cố và buộc phải đóng.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Gửi báo cáo sự cố cho %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Đóng</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Khởi động lại %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Sự cố</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Xin lỗi. Đã xảy ra sự cố trong %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Báo cáo</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Gửi báo cáo sự cố đến %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Thu thập dữ liệu sự cố</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Thu thập dữ liệu đo từ xa của sự cố</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Trình báo cáo lỗi</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Chưa có báo cáo lỗi nào được gửi.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Chia sẻ</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..0676f335e1
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-yo/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Pẹ̀lẹ́. %1$s ní ìṣòro tó sì lulẹ̀.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Fi àwọn ìròyìn ìjákulẹ̀ ránṣẹ́ sí %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Padé</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Tún-un bẹ̀rẹ̀ %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Àwọn ìjákulẹ̀</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Pẹ̀lẹ́. Ìṣòrò kan wáyé ní %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Ìròyìn </string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Fifi àwọn ìròyìn ìjákulẹ̀ ránṣẹ́ sí %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Kíkó àwọn dátà tó ti ní ìjákulẹ̀ pọ̀</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Kíkó àwọn dátà tẹlímẹ́tírì tó ti ní ìjákulẹ̀ pọ̀</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Àwọn ìròyìn ìjákulẹ̀</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">Kò sí àwọn ìròyìn ìjákulẹ̀ tí a ti fi sílẹ̀.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Pín</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..9a0bf4aadb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">很抱歉,%1$s 遇到问题,已经崩溃。</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">向 %1$s 发送崩溃报告</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">关闭</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">重启 %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">崩溃信息</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">抱歉,%1$s 出现问题。</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">反馈</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">正在向 %1$s 发送崩溃报告</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">正在收集崩溃数据</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">收集崩溃遥测数据</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">崩溃报告</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">尚未提交任何崩溃报告。</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">共享</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..21177e0a22
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">很抱歉,%1$s 遇到問題,發生錯誤。</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">傳送錯誤報告給 %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">關閉</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">重新啟動 %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">程式錯誤</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">很抱歉,%1$s 發生問題。</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">回報</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">正在傳送錯誤報告給 %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">收集錯誤資料</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">正在取得發生錯誤的 telemetry 資料</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">錯誤資訊報表</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">未送出任何錯誤資訊報表。</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">分享</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values/strings.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..c0403c6995
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values/strings.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<resources>
+ <!-- Title of the crash reporter dialog. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_dialog_title">Sorry. %1$s had a problem and crashed.</string>
+
+ <!-- Label of the checkbox for sending crash reports in the crash reporter dialog. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_crash_dialog_checkbox">Send crash report to %1$s</string>
+
+ <!-- Label of the button closing the crash reporter dialog. -->
+ <string name="mozac_lib_crash_dialog_button_close">Close</string>
+
+ <!-- Label of the button closing the crash reporter dialog and restarting the app. -->
+ <string name="mozac_lib_crash_dialog_button_restart">Restart %1$s</string>
+
+ <!-- Name of the "notification channel" used for displaying a notification when the app crashed and we can't show a prompt. See https://developer.android.com/training/notify-user/channels -->
+ <string name="mozac_lib_crash_channel">Crashes</string>
+
+ <!-- Title of the crash reporter notification for background process crashes. %1$s will be replaced with the name of the app (e.g. Firefox Focus). -->
+ <string name="mozac_lib_crash_background_process_notification_title">Sorry. A problem occurred in %1$s.</string>
+
+ <!-- Label of a notification action/button that will send the crash report to Mozilla. -->
+ <string name="mozac_lib_crash_notification_action_report">Report</string>
+
+ <!-- Label of notification showing that the crash report service is running. %1$s will be replaced with the name of the organization (e.g. Mozilla). -->
+ <string name="mozac_lib_send_crash_report_in_progress">Sending crash report to %1$s</string>
+
+ <!-- Label of notification showing that the crash handling service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_data_in_progress">Gathering crash data</string>
+
+ <!-- Label of notification showing that the telemetry service is gathering the crash data. -->
+ <string name="mozac_lib_gathering_crash_telemetry_in_progress">Gathering crash telemetry data</string>
+
+ <!-- Title of the activity that shows the list of past crashes (similar to about:crashes)-->
+ <string name="mozac_lib_crash_activity_title">Crash Reports</string>
+
+ <!-- Text shown instead of crash list if no crashes have been submitted yet -->
+ <string name="mozac_lib_crash_no_crashes">No crash reports have been submitted.</string>
+
+ <!-- Text link that will show an app chooser to share a crash report with a third-party app (e.g. gmail) -->
+ <string name="mozac_lib_crash_share">Share</string>
+</resources>
diff --git a/mobile/android/android-components/components/lib/crash/src/main/res/values/styles.xml b/mobile/android/android-components/components/lib/crash/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..8e441529e9
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/main/res/values/styles.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<resources>
+ <style name="Theme.Mozac.CrashReporter" parent="Theme.AppCompat.Light.Dialog">
+ <item name="windowNoTitle">true</item>
+ <item name="android:windowMinWidthMajor">96%</item>
+ <item name="android:windowMinWidthMinor">96%</item>
+ <item name="android:windowAnimationStyle">@null</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/BreadcrumbTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/BreadcrumbTest.kt
new file mode 100644
index 0000000000..b54947712d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/BreadcrumbTest.kt
@@ -0,0 +1,192 @@
+/* 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 androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import java.lang.Thread.sleep
+import java.util.Date
+
+@RunWith(AndroidJUnit4::class)
+class BreadcrumbTest {
+
+ @Before
+ fun setUp() {
+ CrashReporter.reset()
+ }
+
+ @Test
+ fun `RecordBreadCrumb stores breadCrumb in reporter`() {
+ val testMessage = "test_Message"
+ val testData = hashMapOf("1" to "one", "2" to "two")
+ val testCategory = "testing_category"
+ val testLevel = Breadcrumb.Level.CRITICAL
+ val testType = Breadcrumb.Type.USER
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ reporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ testMessage,
+ testData,
+ testCategory,
+ testLevel,
+ testType,
+ ),
+ )
+
+ reporter.crashBreadcrumbsCopy().elementAt(0).let {
+ assertEquals(it.message, testMessage)
+ assertEquals(it.data, testData)
+ assertEquals(it.category, testCategory)
+ assertEquals(it.level, testLevel)
+ assertEquals(it.type, testType)
+ assertNotNull(it.date)
+ }
+ }
+
+ @Test
+ fun `Reporter stores current number of breadcrumbs`() {
+ val testMessage = "test_Message"
+ val testData = hashMapOf("1" to "one", "2" to "two")
+ val testCategory = "testing_category"
+ val testLevel = Breadcrumb.Level.CRITICAL
+ val testType = Breadcrumb.Type.USER
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ reporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ testMessage,
+ testData,
+ testCategory,
+ testLevel,
+ testType,
+ ),
+ )
+ assertEquals(reporter.crashBreadcrumbsCopy().size, 1)
+
+ reporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ testMessage,
+ testData,
+ testCategory,
+ testLevel,
+ testType,
+ ),
+ )
+ assertEquals(reporter.crashBreadcrumbsCopy().size, 2)
+
+ reporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ testMessage,
+ testData,
+ testCategory,
+ testLevel,
+ testType,
+ ),
+ )
+ assertEquals(reporter.crashBreadcrumbsCopy().size, 3)
+ }
+
+ @Test
+ fun `RecordBreadcumb stores correct date`() {
+ val testMessage = "test_Message"
+ val testData = hashMapOf("1" to "one", "2" to "two")
+ val testCategory = "testing_category"
+ val testLevel = Breadcrumb.Level.CRITICAL
+ val testType = Breadcrumb.Type.USER
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val beginDate = Date()
+ sleep(100) // make sure time elapsed
+ reporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ testMessage,
+ testData,
+ testCategory,
+ testLevel,
+ testType,
+ ),
+ )
+ sleep(100) // make sure time elapsed
+ val afterDate = Date()
+
+ reporter.crashBreadcrumbsCopy().elementAt(0).let {
+ assertTrue(it.date.after(beginDate))
+ assertTrue(it.date.before(afterDate))
+ }
+
+ val date = Date()
+ reporter.recordCrashBreadcrumb(
+ Breadcrumb(
+ testMessage,
+ testData,
+ testCategory,
+ testLevel,
+ testType,
+ date,
+ ),
+ )
+
+ reporter.crashBreadcrumbsCopy().elementAt(1).let {
+ assertEquals(it.date.compareTo(date), 0)
+ }
+ }
+
+ @Test
+ fun `Breadcrumb converts correctly to JSON`() {
+ val testMessage = "test_Message"
+ val testData = hashMapOf("1" to "one", "2" to "two")
+ val testCategory = "testing_category"
+ val testLevel = Breadcrumb.Level.CRITICAL
+ val testType = Breadcrumb.Type.USER
+ val testDate = Date(0)
+ val testString = "{\"timestamp\":\"1970-01-01T00:00:00\",\"message\":\"test_Message\"," +
+ "\"category\":\"testing_category\",\"level\":\"Critical\",\"type\":\"User\"," +
+ "\"data\":{\"1\":\"one\",\"2\":\"two\"}}"
+
+ val breadcrumb = Breadcrumb(
+ testMessage,
+ testData,
+ testCategory,
+ testLevel,
+ testType,
+ testDate,
+ )
+ assertEquals(breadcrumb.toJson().toString(), testString)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashReporterTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashReporterTest.kt
new file mode 100644
index 0000000000..1ec2333325
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashReporterTest.kt
@@ -0,0 +1,931 @@
+/* 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.Activity
+import android.app.PendingIntent
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.lib.crash.service.CrashReporterService
+import mozilla.components.lib.crash.service.CrashTelemetryService
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.expectException
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+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 org.robolectric.Robolectric
+import org.robolectric.Shadows.shadowOf
+import java.lang.Thread.sleep
+import java.lang.reflect.Modifier
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class CrashReporterTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ @Before
+ fun setUp() {
+ CrashReporter.reset()
+ }
+
+ @Test
+ fun `Calling install() will setup uncaught exception handler`() {
+ val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
+
+ CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val newHandler = Thread.getDefaultUncaughtExceptionHandler()
+ assertNotNull(newHandler)
+
+ assertNotEquals(defaultHandler, newHandler)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `CrashReporter throws if no service is defined`() {
+ CrashReporter(
+ context = testContext,
+ services = emptyList(),
+ notificationsDelegate = mock(),
+ ).install(testContext)
+ }
+
+ @Test
+ fun `CrashReporter will submit report immediately if setup with Prompt-NEVER`() {
+ val service: CrashReporterService = mock()
+ val telemetryService: CrashTelemetryService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ telemetryServices = listOf(telemetryService),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val crash: Crash.UncaughtExceptionCrash = createUncaughtExceptionCrash()
+
+ reporter.onCrash(testContext, crash)
+
+ verify(reporter).sendCrashTelemetry(testContext, crash)
+ verify(reporter).sendCrashReport(testContext, crash)
+ verify(reporter, never()).showPrompt(any(), eq(crash))
+ }
+
+ @Test
+ fun `CrashReporter will show prompt if setup with Prompt-ALWAYS`() {
+ val service: CrashReporterService = mock()
+ val telemetryService: CrashTelemetryService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ telemetryServices = listOf(telemetryService),
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val crash: Crash.UncaughtExceptionCrash = createUncaughtExceptionCrash()
+
+ reporter.onCrash(testContext, crash)
+
+ verify(reporter).sendCrashTelemetry(testContext, crash)
+ verify(reporter, never()).sendCrashReport(testContext, crash)
+ verify(reporter).showPrompt(any(), eq(crash))
+ }
+
+ @Test
+ fun `CrashReporter will submit report immediately for non native crash and with setup Prompt-ONLY_NATIVE_CRASH`() {
+ val service: CrashReporterService = mock()
+ val telemetryService: CrashTelemetryService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ telemetryServices = listOf(telemetryService),
+ shouldPrompt = CrashReporter.Prompt.ONLY_NATIVE_CRASH,
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val crash: Crash.UncaughtExceptionCrash = createUncaughtExceptionCrash()
+
+ reporter.onCrash(testContext, crash)
+
+ verify(reporter).sendCrashTelemetry(testContext, crash)
+ verify(reporter).sendCrashReport(testContext, crash)
+ verify(reporter, never()).showPrompt(any(), eq(crash))
+ }
+
+ @Test
+ fun `CrashReporter will show prompt for main process native crash and with setup Prompt-ONLY_NATIVE_CRASH`() {
+ val service: CrashReporterService = mock()
+ val telemetryService: CrashTelemetryService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ telemetryServices = listOf(telemetryService),
+ shouldPrompt = CrashReporter.Prompt.ONLY_NATIVE_CRASH,
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val crash = Crash.NativeCodeCrash(
+ 0,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ reporter.onCrash(testContext, crash)
+
+ verify(reporter).sendCrashTelemetry(testContext, crash)
+ verify(reporter).showPrompt(any(), eq(crash))
+ verify(reporter, never()).sendCrashReport(testContext, crash)
+ verify(service, never()).report(crash)
+ }
+
+ @Test
+ fun `CrashReporter will submit crash telemetry even if crash report requires prompt`() {
+ val service: CrashReporterService = mock()
+ val telemetryService: CrashTelemetryService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ telemetryServices = listOf(telemetryService),
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val crash: Crash.UncaughtExceptionCrash = createUncaughtExceptionCrash()
+
+ reporter.onCrash(testContext, crash)
+
+ verify(reporter).sendCrashTelemetry(testContext, crash)
+ verify(reporter, never()).sendCrashReport(testContext, crash)
+ verify(reporter).showPrompt(any(), eq(crash))
+ }
+
+ @Test
+ fun `CrashReporter will not prompt the user if there is no crash services`() {
+ val telemetryService: CrashTelemetryService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ telemetryServices = listOf(telemetryService),
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val crash: Crash.UncaughtExceptionCrash = createUncaughtExceptionCrash()
+
+ reporter.onCrash(testContext, crash)
+
+ verify(reporter).sendCrashTelemetry(testContext, crash)
+ verify(reporter, never()).sendCrashReport(testContext, crash)
+ verify(reporter, never()).showPrompt(any(), eq(crash))
+ }
+
+ @Test
+ fun `CrashReporter will not send crash telemetry if there is no telemetry service`() {
+ val service: CrashReporterService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val crash: Crash.UncaughtExceptionCrash = createUncaughtExceptionCrash()
+
+ reporter.onCrash(testContext, crash)
+
+ verify(reporter, never()).sendCrashTelemetry(testContext, crash)
+ verify(reporter).showPrompt(any(), eq(crash))
+ }
+
+ @Test
+ fun `Calling install() with no crash services or telemetry crash services will throw exception`() {
+ var exceptionThrown = false
+
+ try {
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+ } catch (e: IllegalArgumentException) {
+ exceptionThrown = true
+ }
+
+ assert(exceptionThrown)
+ }
+
+ @Test
+ fun `Calling install() with at least one crash service or telemetry crash service will not throw exception`() {
+ var exceptionThrown = false
+
+ try {
+ CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ notificationsDelegate = mock(),
+ ).install(testContext)
+ } catch (e: IllegalArgumentException) {
+ exceptionThrown = true
+ }
+ assert(!exceptionThrown)
+
+ try {
+ CrashReporter(
+ context = testContext,
+ telemetryServices = listOf(mock()),
+ notificationsDelegate = mock(),
+ ).install(testContext)
+ } catch (e: IllegalArgumentException) {
+ exceptionThrown = true
+ }
+ assert(!exceptionThrown)
+ }
+
+ @Test
+ fun `CrashReporter is enabled by default`() {
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ shouldPrompt = CrashReporter.Prompt.ONLY_NATIVE_CRASH,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ assertTrue(reporter.enabled)
+ }
+
+ @Test
+ fun `CrashReporter will not prompt and not submit report if not enabled`() {
+ val service: CrashReporterService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ reporter.enabled = false
+
+ val crash: Crash.UncaughtExceptionCrash = mock()
+ reporter.onCrash(testContext, crash)
+
+ verify(reporter, never()).sendCrashReport(testContext, crash)
+ verify(reporter, never()).sendCrashTelemetry(testContext, crash)
+ verify(reporter, never()).showPrompt(any(), eq(crash))
+
+ verify(service, never()).report(crash)
+ }
+
+ @Test
+ fun `CrashReporter sends telemetry`() {
+ val crash = createUncaughtExceptionCrash()
+
+ val service = mock<CrashReporterService>()
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ reporter.onCrash(testContext, crash)
+ verify(reporter, never()).sendCrashTelemetry(testContext, crash)
+ }
+
+ @Test
+ fun `CrashReporter forwards uncaught exception crashes to service`() {
+ var exceptionCrash = false
+
+ val service = object : CrashReporterService {
+ override val id: String = "test"
+
+ override val name: String = "TestReporter"
+
+ override fun createCrashReportUrl(identifier: String): String? = null
+
+ override fun report(crash: Crash.UncaughtExceptionCrash): String? {
+ exceptionCrash = true
+ return null
+ }
+
+ override fun report(crash: Crash.NativeCodeCrash): String? = null
+
+ override fun report(throwable: Throwable, breadcrumbs: ArrayList<Breadcrumb>): String? = null
+ }
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ reporter.submitReport(
+ Crash.UncaughtExceptionCrash(0, RuntimeException(), arrayListOf()),
+ ).joinBlocking()
+ assertTrue(exceptionCrash)
+ }
+
+ @Test
+ fun `CrashReporter forwards native crashes to service`() {
+ var nativeCrash = false
+
+ val service = object : CrashReporterService {
+ override val id: String = "test"
+
+ override val name: String = "TestReporter"
+
+ override fun createCrashReportUrl(identifier: String): String? = null
+
+ override fun report(crash: Crash.UncaughtExceptionCrash): String? = null
+
+ override fun report(crash: Crash.NativeCodeCrash): String? {
+ nativeCrash = true
+ return null
+ }
+
+ override fun report(throwable: Throwable, breadcrumbs: ArrayList<Breadcrumb>): String? = null
+ }
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ reporter.submitReport(
+ Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ ),
+ ).joinBlocking()
+ assertTrue(nativeCrash)
+ }
+
+ @Test
+ fun `CrashReporter forwards caught exception crashes to service`() = runTestOnMain {
+ val testMessage = "test_Message"
+ val testData = hashMapOf("1" to "one", "2" to "two")
+ val testCategory = "testing_category"
+ val testLevel = Breadcrumb.Level.CRITICAL
+ val testType = Breadcrumb.Type.USER
+ var exceptionCrash = false
+ var exceptionThrowable: Throwable? = null
+ var exceptionBreadcrumb: ArrayList<Breadcrumb>? = null
+ val service = object : CrashReporterService {
+ override val id: String = "test"
+
+ override val name: String = "TestReporter"
+
+ override fun createCrashReportUrl(identifier: String): String? = null
+
+ override fun report(crash: Crash.UncaughtExceptionCrash): String? = null
+
+ override fun report(crash: Crash.NativeCodeCrash): String? = null
+
+ override fun report(throwable: Throwable, breadcrumbs: ArrayList<Breadcrumb>): String? {
+ exceptionCrash = true
+ exceptionThrowable = throwable
+ exceptionBreadcrumb = breadcrumbs
+ return null
+ }
+ }
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val throwable = RuntimeException()
+ val breadcrumb = Breadcrumb(
+ testMessage,
+ testData,
+ testCategory,
+ testLevel,
+ testType,
+ )
+ reporter.recordCrashBreadcrumb(breadcrumb)
+ advanceUntilIdle()
+
+ reporter.submitCaughtException(throwable).joinBlocking()
+
+ assertTrue(exceptionCrash)
+ assert(exceptionThrowable == throwable)
+ assert(exceptionBreadcrumb?.get(0) == breadcrumb)
+ }
+
+ @Test
+ fun `Caught exception with no stack trace should be reported as CrashReporterException`() = runTestOnMain {
+ val testMessage = "test_Message"
+ val testData = hashMapOf("1" to "one", "2" to "two")
+ val testCategory = "testing_category"
+ val testLevel = Breadcrumb.Level.CRITICAL
+ val testType = Breadcrumb.Type.USER
+ var exceptionCrash = false
+ var exceptionThrowable: Throwable? = null
+ var exceptionBreadcrumb: ArrayList<Breadcrumb>? = null
+ val service = object : CrashReporterService {
+ override val id: String = "test"
+
+ override val name: String = "TestReporter"
+
+ override fun createCrashReportUrl(identifier: String): String? = null
+
+ override fun report(crash: Crash.UncaughtExceptionCrash): String? = null
+
+ override fun report(crash: Crash.NativeCodeCrash): String? = null
+
+ override fun report(throwable: Throwable, breadcrumbs: ArrayList<Breadcrumb>): String? {
+ exceptionCrash = true
+ exceptionThrowable = throwable
+ exceptionBreadcrumb = breadcrumbs
+ return null
+ }
+ }
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val throwable = RuntimeException()
+ throwable.stackTrace = emptyArray()
+ val breadcrumb = Breadcrumb(
+ testMessage,
+ testData,
+ testCategory,
+ testLevel,
+ testType,
+ )
+ reporter.recordCrashBreadcrumb(breadcrumb)
+ advanceUntilIdle()
+
+ reporter.submitCaughtException(throwable).joinBlocking()
+
+ assertTrue(exceptionCrash)
+ assert(exceptionThrowable is CrashReporterException.UnexpectedlyMissingStacktrace)
+ assert(exceptionThrowable?.cause is java.lang.RuntimeException)
+ assertEquals(exceptionBreadcrumb?.get(0), breadcrumb)
+ }
+
+ @Test
+ fun `CrashReporter forwards native crashes to telemetry service`() {
+ var nativeCrash = false
+
+ val telemetryService = object : CrashTelemetryService {
+ override fun record(crash: Crash.UncaughtExceptionCrash) = Unit
+
+ override fun record(crash: Crash.NativeCodeCrash) {
+ nativeCrash = true
+ }
+
+ override fun record(throwable: Throwable) = Unit
+ }
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ telemetryServices = listOf(telemetryService),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ reporter.submitCrashTelemetry(
+ Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ ),
+ ).joinBlocking()
+ assertTrue(nativeCrash)
+ }
+
+ @Test
+ fun `Internal reference is set after calling install`() {
+ expectException(IllegalStateException::class) {
+ CrashReporter.requireInstance
+ }
+
+ val reporter = CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ notificationsDelegate = mock(),
+ )
+
+ expectException(IllegalStateException::class) {
+ CrashReporter.requireInstance
+ }
+
+ reporter.install(testContext)
+
+ assertNotNull(CrashReporter.requireInstance)
+ }
+
+ @Test
+ fun `CrashReporter invokes PendingIntent if provided for foreground child process crashes`() {
+ val context = Robolectric.buildActivity(Activity::class.java).setup().get()
+
+ val intent = Intent("action")
+ val pendingIntent = spy(PendingIntent.getActivity(context, 0, intent, 0))
+
+ val reporter = CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(mock()),
+ nonFatalCrashIntent = pendingIntent,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val nativeCrash = Crash.NativeCodeCrash(
+ 0,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ reporter.onCrash(context, nativeCrash)
+
+ verify(pendingIntent).send(eq(context), eq(0), any())
+
+ val receivedIntent = shadowOf(context).nextStartedActivity
+
+ val receivedCrash = Crash.fromIntent(receivedIntent) as? Crash.NativeCodeCrash
+ ?: throw AssertionError("Expected NativeCodeCrash instance")
+
+ assertEquals(nativeCrash, receivedCrash)
+ assertEquals("dump.path", receivedCrash.minidumpPath)
+ assertEquals(true, receivedCrash.minidumpSuccess)
+ assertEquals("extras.path", receivedCrash.extrasPath)
+ assertEquals(false, receivedCrash.isFatal)
+ assertEquals(Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD, receivedCrash.processType)
+ }
+
+ @Test
+ fun `CrashReporter does not invoke PendingIntent if provided for main process crashes`() {
+ val context = Robolectric.buildActivity(Activity::class.java).setup().get()
+
+ val intent = Intent("action")
+ val pendingIntent = spy(PendingIntent.getActivity(context, 0, intent, 0))
+
+ val reporter = CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(mock()),
+ nonFatalCrashIntent = pendingIntent,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val nativeCrash = Crash.NativeCodeCrash(
+ 0,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ reporter.onCrash(context, nativeCrash)
+
+ verify(pendingIntent, never()).send(eq(context), eq(0), any())
+ }
+
+ @Test
+ fun `CrashReporter does not invoke PendingIntent if provided for background child process crashes`() {
+ val context = Robolectric.buildActivity(Activity::class.java).setup().get()
+
+ val intent = Intent("action")
+ val pendingIntent = spy(PendingIntent.getActivity(context, 0, intent, 0))
+
+ val reporter = CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(mock()),
+ nonFatalCrashIntent = pendingIntent,
+ notificationsDelegate = mock(),
+ ).install(context)
+
+ val nativeCrash = Crash.NativeCodeCrash(
+ 0,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ reporter.onCrash(context, nativeCrash)
+
+ verify(pendingIntent, never()).send(eq(context), eq(0), any())
+ }
+
+ @Test
+ fun `CrashReporter sends telemetry but don't send native crash if the crash is in foreground child process and nonFatalPendingIntent is not null`() {
+ val service: CrashReporterService = mock()
+ val telemetryService: CrashTelemetryService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ telemetryServices = listOf(telemetryService),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ nonFatalCrashIntent = mock(),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val nativeCrash = Crash.NativeCodeCrash(
+ 0,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ reporter.onCrash(testContext, nativeCrash)
+
+ verify(reporter, never()).sendCrashReport(testContext, nativeCrash)
+ verify(reporter, times(1)).sendCrashTelemetry(testContext, nativeCrash)
+ verify(reporter, never()).showPrompt(any(), eq(nativeCrash))
+ }
+
+ @Test
+ fun `CrashReporter sends telemetry and crash if the crash is in foreground child process and nonFatalPendingIntent is null`() {
+ val service: CrashReporterService = mock()
+ val telemetryService: CrashTelemetryService = mock()
+
+ val reporter = spy(
+ CrashReporter(
+ context = testContext,
+ services = listOf(service),
+ telemetryServices = listOf(telemetryService),
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext),
+ )
+
+ val nativeCrash = Crash.NativeCodeCrash(
+ 0,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ reporter.onCrash(testContext, nativeCrash)
+
+ verify(reporter, times(1)).sendCrashReport(testContext, nativeCrash)
+ verify(reporter, times(1)).sendCrashTelemetry(testContext, nativeCrash)
+ verify(reporter, never()).showPrompt(any(), eq(nativeCrash))
+ }
+
+ @Test
+ fun `CrashReporter instance writes are visible across threads`() {
+ val instanceField = CrashReporter::class.java.getDeclaredField("instance")
+ assertTrue(Modifier.isVolatile(instanceField.modifiers))
+ }
+
+ @Test
+ fun `Breadcrumbs stores only max number of breadcrumbs`() = runTestOnMain {
+ val testMessage = "test_Message"
+ val testData = hashMapOf("1" to "one", "2" to "two")
+ val testCategory = "testing_category"
+ val testLevel = Breadcrumb.Level.CRITICAL
+ val testType = Breadcrumb.Type.USER
+
+ var crashReporter = CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ maxBreadCrumbs = 5,
+ scope = scope,
+ notificationsDelegate = mock(),
+ )
+
+ repeat(10) {
+ crashReporter.recordCrashBreadcrumb(Breadcrumb(testMessage, testData, testCategory, testLevel, testType))
+ }
+ advanceUntilIdle()
+ assertEquals(crashReporter.crashBreadcrumbsCopy().size, 5)
+
+ crashReporter = CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ maxBreadCrumbs = 5,
+ scope = scope,
+ notificationsDelegate = mock(),
+ )
+ repeat(15) {
+ crashReporter.recordCrashBreadcrumb(Breadcrumb(testMessage, testData, testCategory, testLevel, testType))
+ }
+ advanceUntilIdle()
+ assertEquals(crashReporter.crashBreadcrumbsCopy().size, 5)
+ }
+
+ @Test
+ fun `Breadcrumb priority queue stores the latest breadcrumbs`() = runTestOnMain {
+ val testMessage = "test_Message"
+ val testData = hashMapOf("1" to "one", "2" to "two")
+ val testCategory = "testing_category"
+ val testType = Breadcrumb.Type.USER
+ val maxNum = 10
+
+ var crashReporter = CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ maxBreadCrumbs = maxNum,
+ scope = scope,
+ notificationsDelegate = mock(),
+ )
+
+ repeat(maxNum) {
+ crashReporter.recordCrashBreadcrumb(
+ Breadcrumb(testMessage, testData, testCategory, Breadcrumb.Level.CRITICAL, testType),
+ )
+ sleep(10) // make sure time elapsed
+ }
+ advanceUntilIdle()
+
+ crashReporter.crashBreadcrumbsCopy().let {
+ for (i in 0 until maxNum) {
+ assertEquals(it.elementAt(i).level, Breadcrumb.Level.CRITICAL)
+ }
+
+ var time = it[0].date
+ for (i in 1 until it.size) {
+ assertTrue(time.before(it[i].date))
+ time = it[i].date
+ }
+ }
+
+ repeat(maxNum) {
+ crashReporter.recordCrashBreadcrumb(
+ Breadcrumb(testMessage, testData, testCategory, Breadcrumb.Level.DEBUG, testType),
+ )
+ sleep(10) // make sure time elapsed
+ }
+ advanceUntilIdle()
+
+ crashReporter.crashBreadcrumbsCopy().let {
+ for (i in 0 until maxNum) {
+ assertEquals(it.elementAt(i).level, Breadcrumb.Level.DEBUG)
+ }
+
+ var time = it[0].date
+ for (i in 1 until it.size) {
+ assertTrue(time.before(it[i].date))
+ time = it[i].date
+ }
+ }
+ }
+
+ @Test
+ fun `Breadcrumb priority queue output list result is sorted by time`() = runTestOnMain {
+ val testMessage = "test_Message"
+ val testData = hashMapOf("1" to "one", "2" to "two")
+ val testCategory = "testing_category"
+ val testType = Breadcrumb.Type.USER
+ val maxNum = 10
+
+ var crashReporter = CrashReporter(
+ context = testContext,
+ services = listOf(mock()),
+ maxBreadCrumbs = 5,
+ scope = scope,
+ notificationsDelegate = mock(),
+ )
+
+ repeat(maxNum) {
+ crashReporter.recordCrashBreadcrumb(
+ Breadcrumb(testMessage, testData, testCategory, Breadcrumb.Level.DEBUG, testType),
+ )
+ sleep(10) // make sure time elapsed
+ }
+ advanceUntilIdle()
+
+ crashReporter.crashBreadcrumbsCopy().let {
+ var time = it[0].date
+ for (i in 1 until it.size) {
+ assertTrue(time.before(it[i].date))
+ time = it[i].date
+ }
+ }
+
+ repeat(maxNum / 2) {
+ crashReporter.recordCrashBreadcrumb(
+ Breadcrumb(testMessage, testData, testCategory, Breadcrumb.Level.INFO, testType),
+ )
+ sleep(10) // make sure time elapsed
+ }
+ advanceUntilIdle()
+
+ crashReporter.crashBreadcrumbsCopy().let {
+ var time = it[0].date
+ for (i in 1 until it.size) {
+ assertTrue(time.before(it[i].date))
+ time = it[i].date
+ }
+ }
+ }
+}
+
+private fun createUncaughtExceptionCrash(): Crash.UncaughtExceptionCrash {
+ return Crash.UncaughtExceptionCrash(
+ 0,
+ RuntimeException(),
+ ArrayList(),
+ )
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashTest.kt
new file mode 100644
index 0000000000..653655a65a
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashTest.kt
@@ -0,0 +1,105 @@
+/* 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 androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class CrashTest {
+
+ @Test
+ fun `fromIntent() can deserialize a GeckoView crash Intent`() {
+ val originalCrash = Crash.NativeCodeCrash(
+ 123,
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ true,
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = "web",
+ )
+
+ val intent = Intent()
+ originalCrash.fillIn(intent)
+
+ val recoveredCrash = Crash.fromIntent(intent) as? Crash.NativeCodeCrash
+ ?: throw AssertionError("Expected NativeCodeCrash instance")
+
+ assertEquals(recoveredCrash.timestamp, 123)
+ assertEquals(recoveredCrash.minidumpSuccess, true)
+ assertEquals(recoveredCrash.isFatal, false)
+ assertEquals(recoveredCrash.processType, Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD)
+ assertEquals(
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ recoveredCrash.minidumpPath,
+ )
+ assertEquals(
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ recoveredCrash.extrasPath,
+ )
+ assertEquals("web", recoveredCrash.remoteType)
+ }
+
+ @Test
+ fun `Serialize and deserialize UncaughtExceptionCrash`() {
+ val exception = RuntimeException("Hello World!")
+
+ val originalCrash = Crash.UncaughtExceptionCrash(0, exception, arrayListOf())
+
+ val intent = Intent()
+ originalCrash.fillIn(intent)
+
+ val recoveredCrash = Crash.fromIntent(intent) as? Crash.UncaughtExceptionCrash
+ ?: throw AssertionError("Expected UncaughtExceptionCrash instance")
+
+ assertEquals(exception, recoveredCrash.throwable)
+ assertEquals("Hello World!", recoveredCrash.throwable.message)
+ assertArrayEquals(exception.stackTrace, recoveredCrash.throwable.stackTrace)
+ }
+
+ @Test
+ fun `isCrashIntent()`() {
+ assertFalse(Crash.isCrashIntent(Intent()))
+
+ assertFalse(
+ Crash.isCrashIntent(
+ Intent()
+ .putExtra("crash", "I am a crash!"),
+ ),
+ )
+
+ assertTrue(
+ Crash.isCrashIntent(
+ Intent().apply {
+ Crash.UncaughtExceptionCrash(0, RuntimeException(), arrayListOf()).fillIn(this)
+ },
+ ),
+ )
+
+ assertTrue(
+ Crash.isCrashIntent(
+ Intent().apply {
+ val crash = Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ "",
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ crash.fillIn(this)
+ },
+ ),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/NativeCodeCrashTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/NativeCodeCrashTest.kt
new file mode 100644
index 0000000000..a8e83154b2
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/NativeCodeCrashTest.kt
@@ -0,0 +1,74 @@
+/* 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.ComponentName
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class NativeCodeCrashTest {
+
+ @Test
+ fun `Creating NativeCodeCrash object from sample GeckoView intent`() {
+ val intent = Intent("org.mozilla.gecko.ACTION_CRASHED")
+ intent.component = ComponentName(
+ "org.mozilla.samples.browser",
+ "mozilla.components.lib.crash.handler.CrashHandlerService",
+ )
+ intent.putExtra("uuid", "afc91225-93d7-4328-b3eb-d26ad5af4d86")
+ intent.putExtra(
+ "minidumpPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ )
+ intent.putExtra("processType", "FOREGROUND_CHILD")
+ intent.putExtra(
+ "extrasPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ )
+ intent.putExtra("minidumpSuccess", true)
+ intent.putExtra("remoteType", "web")
+
+ val crash = Crash.NativeCodeCrash.fromBundle(intent.extras!!)
+
+ assertEquals(
+ "afc91225-93d7-4328-b3eb-d26ad5af4d86",
+ crash.uuid,
+ )
+ assertEquals(crash.minidumpSuccess, true)
+ assertEquals(crash.isFatal, false)
+ assertEquals(crash.processType, Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD)
+ assertEquals(
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ crash.minidumpPath,
+ )
+ assertEquals(
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ crash.extrasPath,
+ )
+ assertEquals(crash.remoteType, "web")
+ }
+
+ @Test
+ fun `to and from bundle`() {
+ val crash = Crash.NativeCodeCrash(
+ 0,
+ "minidumpPath",
+ true,
+ "extrasPath",
+ Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ val bundle = crash.toBundle()
+ val otherCrash = Crash.NativeCodeCrash.fromBundle(bundle)
+
+ assertEquals(crash, otherCrash)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/UncaughtExceptionCrashTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/UncaughtExceptionCrashTest.kt
new file mode 100644
index 0000000000..f8400df289
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/UncaughtExceptionCrashTest.kt
@@ -0,0 +1,34 @@
+/* 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 androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class UncaughtExceptionCrashTest {
+
+ @Test
+ fun `UncaughtExceptionCrash wraps exception`() {
+ val exception = RuntimeException("Kaput")
+
+ val crash = Crash.UncaughtExceptionCrash(0, exception, arrayListOf())
+
+ assertEquals(exception, crash.throwable)
+ }
+
+ @Test
+ fun `to and from bundle`() {
+ val exception = RuntimeException("Kaput")
+ val crash = Crash.UncaughtExceptionCrash(0, exception, arrayListOf())
+
+ val bundle = crash.toBundle()
+ val otherCrash = Crash.UncaughtExceptionCrash.fromBundle(bundle)
+
+ assertEquals(crash, otherCrash)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/CrashHandlerServiceTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/CrashHandlerServiceTest.kt
new file mode 100644
index 0000000000..14f607b038
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/CrashHandlerServiceTest.kt
@@ -0,0 +1,122 @@
+/* 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.content.ComponentName
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.robolectric.Robolectric
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class CrashHandlerServiceTest {
+ private var service: CrashHandlerService? = null
+ private var reporter: CrashReporter? = null
+ private val intent = Intent("org.mozilla.gecko.ACTION_CRASHED")
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ @Before
+ fun setUp() {
+ service = spy(Robolectric.setupService(CrashHandlerService::class.java))
+ reporter = spy(
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ services = listOf(mock()),
+ nonFatalCrashIntent = mock(),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ),
+ ).install(testContext)
+
+ intent.component = ComponentName(
+ "org.mozilla.samples.browser",
+ "mozilla.components.lib.crash.handler.CrashHandlerService",
+ )
+ intent.putExtra(
+ "uuid",
+ "94f66ed7-50c7-41d1-96a7-299139a8c2af",
+ )
+ intent.putExtra(
+ "minidumpPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ )
+ intent.putExtra(
+ "extrasPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ )
+ intent.putExtra("minidumpSuccess", true)
+
+ service!!.startService(intent)
+ }
+
+ @After
+ fun tearDown() {
+ service!!.stopService(intent)
+ CrashReporter.reset()
+ }
+
+ @Test
+ fun `CrashHandlerService forwards main process native code crash to crash reporter`() = runTestOnMain {
+ doNothing().`when`(reporter)!!.sendCrashReport(any(), any())
+
+ intent.putExtra("processType", "MAIN")
+ service!!.handleCrashIntent(intent, coroutinesTestRule.scope)
+ verify(reporter)!!.onCrash(any(), any())
+ verify(reporter)!!.sendCrashReport(any(), any())
+ verify(reporter, never())!!.sendNonFatalCrashIntent(any(), any())
+ }
+
+ @Test
+ fun `CrashHandlerService forwards foreground child process native code crash to crash reporter`() = runTestOnMain {
+ doNothing().`when`(reporter)!!.sendCrashReport(any(), any())
+
+ intent.putExtra("processType", "FOREGROUND_CHILD")
+ service!!.handleCrashIntent(intent, coroutinesTestRule.scope)
+ verify(reporter)!!.onCrash(any(), any())
+ verify(reporter)!!.sendNonFatalCrashIntent(any(), any())
+ verify(reporter, never())!!.sendCrashReport(any(), any())
+ }
+
+ @Test
+ fun `CrashHandlerService forwards background child process native code crash to crash reporter`() = runTestOnMain {
+ doNothing().`when`(reporter)!!.sendCrashReport(any(), any())
+
+ intent.putExtra("processType", "BACKGROUND_CHILD")
+ service!!.handleCrashIntent(intent, coroutinesTestRule.scope)
+ verify(reporter)!!.onCrash(any(), any())
+ verify(reporter)!!.sendCrashReport(any(), any())
+ verify(reporter, never())!!.sendNonFatalCrashIntent(any(), any())
+ }
+
+ @Test
+ fun `CrashHandlerService null intent in onStartCommand`() = runTestOnMain {
+ doNothing().`when`(service)!!.handleCrashIntent(any(), any())
+
+ service!!.onStartCommand(null, 0, 0)
+
+ verify(service, times(0))!!.handleCrashIntent(any(), any())
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/ExceptionHandlerTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/ExceptionHandlerTest.kt
new file mode 100644
index 0000000000..348c36df10
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/ExceptionHandlerTest.kt
@@ -0,0 +1,98 @@
+/* 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 androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.lib.crash.service.CrashReporterService
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class ExceptionHandlerTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ @Test
+ fun `ExceptionHandler forwards crashes to CrashReporter`() {
+ val service: CrashReporterService = mock()
+
+ val crashReporter = spy(
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ services = listOf(service),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ),
+ )
+
+ val handler = ExceptionHandler(
+ testContext,
+ crashReporter,
+ )
+
+ val exception = RuntimeException("Hello World")
+ handler.uncaughtException(Thread.currentThread(), exception)
+
+ verify(crashReporter).onCrash(eq(testContext), any())
+ verify(crashReporter).sendCrashReport(eq(testContext), any())
+ }
+
+ @Test
+ fun `ExceptionHandler invokes default exception handler`() {
+ val defaultExceptionHandler: Thread.UncaughtExceptionHandler = mock()
+
+ val crashReporter = CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ services = listOf(
+ object : CrashReporterService {
+ override val id: String = "test"
+
+ override val name: String = "TestReporter"
+
+ override fun createCrashReportUrl(identifier: String): String? = null
+
+ override fun report(crash: Crash.UncaughtExceptionCrash): String? = null
+
+ override fun report(crash: Crash.NativeCodeCrash): String? = null
+
+ override fun report(throwable: Throwable, breadcrumbs: ArrayList<Breadcrumb>): String? = null
+ },
+ ),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val handler = ExceptionHandler(
+ testContext,
+ crashReporter,
+ defaultExceptionHandler,
+ )
+
+ verify(defaultExceptionHandler, never()).uncaughtException(any(), any())
+
+ val exception = RuntimeException()
+ handler.uncaughtException(Thread.currentThread(), exception)
+
+ verify(defaultExceptionHandler).uncaughtException(Thread.currentThread(), exception)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/notification/CrashNotificationTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/notification/CrashNotificationTest.kt
new file mode 100644
index 0000000000..ff4d8438ef
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/notification/CrashNotificationTest.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.notification
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import androidx.core.app.NotificationManagerCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.support.base.android.NotificationsDelegate
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class CrashNotificationTest {
+ @Test
+ fun shouldShowNotificationInsteadOfPrompt() {
+ val foregroundChildNativeCrash = Crash.NativeCodeCrash(
+ timestamp = 0,
+ minidumpPath = "",
+ minidumpSuccess = true,
+ extrasPath = "",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 21))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 22))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 23))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 24))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 25))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 26))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 27))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 28))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 29))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 30))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(foregroundChildNativeCrash, sdkLevel = 31))
+
+ val mainProcessNativeCrash = Crash.NativeCodeCrash(
+ timestamp = 0,
+ minidumpPath = "",
+ minidumpSuccess = true,
+ extrasPath = "",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 21))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 22))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 23))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 24))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 25))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 26))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 27))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 28))
+ assertTrue(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 29))
+ assertTrue(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 30))
+ assertTrue(CrashNotification.shouldShowNotificationInsteadOfPrompt(mainProcessNativeCrash, sdkLevel = 31))
+
+ val backgroundChildNativeCrash = Crash.NativeCodeCrash(
+ timestamp = 0,
+ minidumpPath = "",
+ minidumpSuccess = true,
+ extrasPath = "",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 21))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 22))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 23))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 24))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 25))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 26))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 27))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 28))
+ assertTrue(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 29))
+ assertTrue(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 30))
+ assertTrue(CrashNotification.shouldShowNotificationInsteadOfPrompt(backgroundChildNativeCrash, sdkLevel = 31))
+
+ val exceptionCrash = Crash.UncaughtExceptionCrash(0, RuntimeException("Boom"), arrayListOf())
+
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 21))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 22))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 23))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 24))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 25))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 26))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 27))
+ assertFalse(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 28))
+ assertTrue(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 29))
+ assertTrue(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 30))
+ assertTrue(CrashNotification.shouldShowNotificationInsteadOfPrompt(exceptionCrash, sdkLevel = 31))
+ }
+
+ @Test
+ fun `Showing notification`() {
+ val notificationManager = testContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val shadowNotificationManager = shadowOf(notificationManager)
+
+ assertEquals(0, shadowNotificationManager.notificationChannels.size)
+ assertEquals(0, shadowNotificationManager.size())
+
+ val crash = Crash.UncaughtExceptionCrash(0, RuntimeException("Boom"), arrayListOf())
+ val notificationManagerCompat = spy(NotificationManagerCompat.from(testContext))
+ val notificationsDelegate = NotificationsDelegate(notificationManagerCompat)
+
+ whenever(notificationManagerCompat.areNotificationsEnabled()).thenReturn(true)
+
+ val crashNotification = CrashNotification(
+ testContext,
+ crash,
+ CrashReporter.PromptConfiguration(
+ appName = "TestApp",
+ ),
+ notificationsDelegate = notificationsDelegate,
+ )
+ crashNotification.show()
+
+ assertEquals(1, shadowNotificationManager.notificationChannels.size)
+ assertEquals(
+ "Crashes",
+ (shadowNotificationManager.notificationChannels[0] as NotificationChannel).name,
+ )
+
+ assertEquals(1, shadowNotificationManager.size())
+ }
+
+ @Test
+ fun `not showing notification when permission is denied`() {
+ val notificationManager = testContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val shadowNotificationManager = shadowOf(notificationManager)
+
+ assertEquals(0, shadowNotificationManager.notificationChannels.size)
+ assertEquals(0, shadowNotificationManager.size())
+
+ val crash = Crash.UncaughtExceptionCrash(0, RuntimeException("Boom"), arrayListOf())
+ val notificationManagerCompat = spy(NotificationManagerCompat.from(testContext))
+ val notificationsDelegate = spy(NotificationsDelegate(notificationManagerCompat))
+
+ whenever(notificationManagerCompat.areNotificationsEnabled()).thenReturn(false)
+
+ val crashNotification = CrashNotification(
+ testContext,
+ crash,
+ CrashReporter.PromptConfiguration(
+ appName = "TestApp",
+ ),
+ notificationsDelegate = notificationsDelegate,
+ )
+ crashNotification.show()
+
+ assertEquals(1, shadowNotificationManager.notificationChannels.size)
+ assertEquals(
+ "Crashes",
+ (shadowNotificationManager.notificationChannels[0] as NotificationChannel).name,
+ )
+
+ assertEquals(0, shadowNotificationManager.size())
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/prompt/CrashReporterActivityTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/prompt/CrashReporterActivityTest.kt
new file mode 100644
index 0000000000..4459d75cae
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/prompt/CrashReporterActivityTest.kt
@@ -0,0 +1,263 @@
+/* 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.content.SharedPreferences
+import android.view.View
+import android.widget.Button
+import android.widget.CheckBox
+import android.widget.TextView
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ActivityScenario.launch
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.lib.crash.prompt.CrashReporterActivity.Companion.PREFERENCE_KEY_SEND_REPORT
+import mozilla.components.lib.crash.prompt.CrashReporterActivity.Companion.SHARED_PREFERENCES_NAME
+import mozilla.components.lib.crash.service.CrashReporterService
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.openMocks
+import kotlin.coroutines.CoroutineContext
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class CrashReporterActivityTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ @Mock
+ lateinit var service: CrashReporterService
+
+ @Before
+ fun setUp() {
+ openMocks(this)
+ }
+
+ @Test
+ fun `Pressing close button sends report`() = runTestOnMain {
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(service),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val crash = Crash.UncaughtExceptionCrash(0, RuntimeException("Hello World"), arrayListOf())
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
+
+ scenario.onActivity { activity ->
+ // When
+ activity.closeButton.performClick()
+ }
+
+ // Await for all coroutines to be finished
+ advanceUntilIdle()
+
+ // Then
+ verify(service).report(crash)
+ }
+
+ @Test
+ fun `Pressing restart button sends report`() = runTestOnMain {
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(service),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val crash = Crash.UncaughtExceptionCrash(0, RuntimeException("Hello World"), arrayListOf())
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
+
+ scenario.onActivity { activity ->
+ // When
+ activity.restartButton.performClick()
+ }
+
+ // Await for all coroutines to be finished
+ advanceUntilIdle()
+
+ // Then
+ verify(service).report(crash)
+ }
+
+ @Test
+ fun `Custom message is set on CrashReporterActivity`() = runTestOnMain {
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ promptConfiguration = CrashReporter.PromptConfiguration(
+ message = "Hello World!",
+ theme = android.R.style.Theme_DeviceDefault, // Yolo!
+ ),
+ services = listOf(mock()),
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val crash = Crash.UncaughtExceptionCrash(0, RuntimeException("Hello World"), arrayListOf())
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
+
+ scenario.onActivity { activity ->
+ // Then
+ assertEquals("Hello World!", activity.messageView.text)
+ }
+ }
+
+ @Test
+ fun `Sending crash report saves checkbox state`() = runTestOnMain {
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(service),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val crash = Crash.UncaughtExceptionCrash(0, RuntimeException("Hello World"), arrayListOf())
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
+
+ scenario.onActivity { activity ->
+ // When
+ activity.sendCheckbox.isChecked = true
+
+ // Then
+ assertFalse(activity.isSendReportPreferenceEnabled)
+
+ // When
+ activity.restartButton.performClick()
+
+ // Then
+ assertTrue(activity.isSendReportPreferenceEnabled)
+ }
+ }
+
+ @Test
+ fun `Restart button visible for main process crash`() = runTestOnMain {
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(service),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val crash = Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
+
+ scenario.onActivity { activity ->
+ assertEquals(activity.restartButton.visibility, View.VISIBLE)
+ }
+ }
+
+ @Test
+ fun `Restart button hidden for background child process crash`() = runTestOnMain {
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(service),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val crash = Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
+
+ scenario.onActivity { activity ->
+ assertEquals(activity.restartButton.visibility, View.GONE)
+ }
+ }
+
+ @Test
+ fun `WHEN crash is native AND background child THEN is background returns true`() = runTestOnMain {
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.ALWAYS,
+ services = listOf(service),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ).install(testContext)
+
+ val crash = Crash.NativeCodeCrash(
+ 123,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
+
+ scenario.onActivity { activity ->
+ assert(activity.isRecoverableBackgroundCrash(crash))
+ }
+ }
+}
+
+/**
+ * Launch activity scenario for certain [crash].
+ */
+@ExperimentalCoroutinesApi
+private fun CoroutineContext.launchActivityWithCrash(
+ crash: Crash,
+): ActivityScenario<CrashReporterActivity> = run {
+ val intent = Intent(testContext, CrashReporterActivity::class.java)
+ .also { crash.fillIn(it) }
+
+ launch<CrashReporterActivity>(intent).apply {
+ onActivity { activity ->
+ activity.reporterCoroutineContext = this@run
+ }
+ }
+}
+
+// Views
+private val CrashReporterActivity.closeButton: Button get() = binding.closeButton
+private val CrashReporterActivity.restartButton: Button get() = binding.restartButton
+private val CrashReporterActivity.messageView: TextView get() = binding.messageView
+private val CrashReporterActivity.sendCheckbox: CheckBox get() = binding.sendCheckbox
+
+// Preferences
+private val CrashReporterActivity.preferences: SharedPreferences
+ get() = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
+private val CrashReporterActivity.isSendReportPreferenceEnabled: Boolean
+ get() = preferences.getBoolean(PREFERENCE_KEY_SEND_REPORT, false)
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/GleanCrashReporterServiceTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/GleanCrashReporterServiceTest.kt
new file mode 100644
index 0000000000..1097d3521f
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/GleanCrashReporterServiceTest.kt
@@ -0,0 +1,464 @@
+/* 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 androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.GleanMetrics.CrashMetrics
+import mozilla.components.service.glean.testing.GleanTestRule
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import java.io.File
+import java.util.Calendar
+import java.util.Date
+import java.util.GregorianCalendar
+import mozilla.components.lib.crash.GleanMetrics.Crash as GleanCrash
+import mozilla.components.lib.crash.GleanMetrics.Pings as GleanPings
+
+@RunWith(AndroidJUnit4::class)
+class GleanCrashReporterServiceTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ @get:Rule
+ val gleanRule = GleanTestRule(context)
+
+ private fun crashCountJson(key: String): String = "{\"type\":\"count\",\"label\":\"$key\"}"
+
+ private fun crashPingJson(uptime: Long, type: String, time: Long, startup: Boolean): String =
+ "{\"type\":\"ping\",\"uptimeNanos\":$uptime,\"processType\":\"$type\"," +
+ "\"timeMillis\":$time,\"startup\":$startup,\"reason\":\"crash\"}"
+
+ private fun crashPingJsonWithRemoteType(
+ uptime: Long,
+ type: String,
+ time: Long,
+ startup: Boolean,
+ remoteType: String,
+ ): String =
+ "{\"type\":\"ping\",\"uptimeNanos\":$uptime,\"processType\":\"$type\"," +
+ "\"timeMillis\":$time,\"startup\":$startup,\"reason\":\"crash\",\"remoteType\":\"$remoteType\"}"
+
+ private fun exceptionPingJson(uptime: Long, time: Long, startup: Boolean): String =
+ "{\"type\":\"ping\",\"uptimeNanos\":$uptime,\"processType\":\"main\"," +
+ "\"timeMillis\":$time,\"startup\":$startup,\"reason\":\"crash\",\"cause\":\"java_exception\"}"
+
+ @Test
+ fun `GleanCrashReporterService records all crash types`() {
+ val crashTypes = hashMapOf(
+ GleanCrashReporterService.MAIN_PROCESS_NATIVE_CODE_CRASH_KEY to Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ ),
+ GleanCrashReporterService.FOREGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY to Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = "web",
+ ),
+ GleanCrashReporterService.BACKGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY to Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ ),
+ GleanCrashReporterService.UNCAUGHT_EXCEPTION_KEY to Crash.UncaughtExceptionCrash(
+ 0,
+ RuntimeException("Test"),
+ arrayListOf(),
+ ),
+ GleanCrashReporterService.CAUGHT_EXCEPTION_KEY to RuntimeException("Test"),
+ )
+
+ for ((type, crash) in crashTypes) {
+ // Because of how Glean is implemented, it can potentially persist information between
+ // tests or even between test classes, so we compensate by capturing the initial value
+ // to compare to.
+ val initialValue = try {
+ CrashMetrics.crashCount[type].testGetValue()!!
+ } catch (e: NullPointerException) {
+ 0
+ }
+
+ run {
+ val service = spy(GleanCrashReporterService(context))
+
+ assertFalse("No previous persisted crashes must exist", service.file.exists())
+
+ when (crash) {
+ is Crash.NativeCodeCrash -> service.record(crash)
+ is Crash.UncaughtExceptionCrash -> service.record(crash)
+ is Throwable -> service.record(crash)
+ }
+
+ assertTrue("Persistence file must exist", service.file.exists())
+ val lines = service.file.readLines()
+ assertEquals(
+ "Must be $type",
+ crashCountJson(type),
+ lines.first(),
+ )
+ }
+
+ // Initialize a fresh GleanCrashReporterService and ensure metrics are recorded in Glean
+ run {
+ GleanCrashReporterService(context)
+
+ assertEquals(
+ "Glean must record correct value",
+ 1,
+ CrashMetrics.crashCount[type].testGetValue()!! - initialValue,
+ )
+ }
+ }
+ }
+
+ @Test
+ fun `GleanCrashReporterService correctly handles multiple crashes in a single file`() {
+ val initialExceptionValue = try {
+ CrashMetrics.crashCount[GleanCrashReporterService.UNCAUGHT_EXCEPTION_KEY].testGetValue()!!
+ } catch (e: NullPointerException) {
+ 0
+ }
+ val initialMainProcessNativeCrashValue = try {
+ CrashMetrics.crashCount[GleanCrashReporterService.MAIN_PROCESS_NATIVE_CODE_CRASH_KEY].testGetValue()!!
+ } catch (e: NullPointerException) {
+ 0
+ }
+
+ val initialForegroundChildProcessNativeCrashValue = try {
+ CrashMetrics.crashCount[GleanCrashReporterService.FOREGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY].testGetValue()!!
+ } catch (e: NullPointerException) {
+ 0
+ }
+
+ val initialBackgroundChildProcessNativeCrashValue = try {
+ CrashMetrics.crashCount[GleanCrashReporterService.BACKGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY].testGetValue()!!
+ } catch (e: NullPointerException) {
+ 0
+ }
+
+ run {
+ val service = spy(GleanCrashReporterService(context))
+
+ assertFalse("No previous persisted crashes must exist", service.file.exists())
+
+ val uncaughtExceptionCrash =
+ Crash.UncaughtExceptionCrash(0, RuntimeException("Test"), arrayListOf())
+ val mainProcessNativeCodeCrash = Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ val foregroundChildProcessNativeCodeCrash = Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = "web",
+ )
+ val backgroundChildProcessNativeCodeCrash = Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ val extensionProcessNativeCodeCrash = Crash.NativeCodeCrash(
+ 0,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = "extension",
+ )
+
+ // Record some crashes
+ service.record(uncaughtExceptionCrash)
+ service.record(mainProcessNativeCodeCrash)
+ service.record(uncaughtExceptionCrash)
+ service.record(foregroundChildProcessNativeCodeCrash)
+ service.record(backgroundChildProcessNativeCodeCrash)
+ service.record(extensionProcessNativeCodeCrash)
+
+ // Make sure the file exists
+ assertTrue("Persistence file must exist", service.file.exists())
+
+ // Get the file lines
+ val lines = service.file.readLines().iterator()
+ assertEquals(
+ "element must be uncaught exception",
+ crashCountJson(GleanCrashReporterService.UNCAUGHT_EXCEPTION_KEY),
+ lines.next(),
+ )
+ assertEquals(
+ "element must be uncaught exception ping",
+ exceptionPingJson(0, 0, false),
+ lines.next(),
+ )
+ assertEquals(
+ "element must be main process native code crash",
+ crashCountJson(GleanCrashReporterService.MAIN_PROCESS_NATIVE_CODE_CRASH_KEY),
+ lines.next(),
+ )
+ assertEquals(
+ "element must be main process crash ping",
+ crashPingJson(0, "main", 0, false),
+ lines.next(),
+ )
+ assertEquals(
+ "element must be uncaught exception",
+ crashCountJson(GleanCrashReporterService.UNCAUGHT_EXCEPTION_KEY),
+ lines.next(), // skip crash ping line in this test
+ )
+ assertEquals(
+ "element must be uncaught exception ping",
+ exceptionPingJson(0, 0, false),
+ lines.next(),
+ )
+ assertEquals(
+ "element must be foreground child process native code crash",
+ crashCountJson(GleanCrashReporterService.FOREGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY),
+ lines.next(),
+ )
+ assertEquals(
+ "element must be foreground process crash ping",
+ crashPingJsonWithRemoteType(0, "content", 0, false, "web"),
+ lines.next(),
+ )
+ assertEquals(
+ "element must be background child process native code crash",
+ crashCountJson(GleanCrashReporterService.BACKGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY),
+ lines.next(), // skip crash ping line
+ )
+ assertEquals(
+ "element must be background process crash ping",
+ crashPingJson(0, "utility", 0, false),
+ lines.next(),
+ )
+ assertEquals(
+ "element must be background child process native code crash",
+ crashCountJson(GleanCrashReporterService.BACKGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY),
+ lines.next(),
+ )
+ assertEquals(
+ "element must be extensions process crash ping",
+ crashPingJsonWithRemoteType(0, "content", 0, false, "extension"),
+ lines.next(),
+ )
+ assertFalse(lines.hasNext())
+ }
+
+ // Initialize a fresh GleanCrashReporterService and ensure metrics are recorded in Glean
+ run {
+ GleanCrashReporterService(context)
+
+ assertEquals(
+ "Glean must record correct value",
+ 2,
+ CrashMetrics.crashCount[GleanCrashReporterService.UNCAUGHT_EXCEPTION_KEY].testGetValue()!! - initialExceptionValue,
+ )
+ assertEquals(
+ "Glean must record correct value",
+ 1,
+ CrashMetrics.crashCount[GleanCrashReporterService.MAIN_PROCESS_NATIVE_CODE_CRASH_KEY].testGetValue()!! - initialMainProcessNativeCrashValue,
+ )
+ assertEquals(
+ "Glean must record correct value",
+ 1,
+ CrashMetrics.crashCount[GleanCrashReporterService.FOREGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY].testGetValue()!! - initialForegroundChildProcessNativeCrashValue,
+ )
+ assertEquals(
+ "Glean must record correct value",
+ 2,
+ CrashMetrics.crashCount[GleanCrashReporterService.BACKGROUND_CHILD_PROCESS_NATIVE_CODE_CRASH_KEY].testGetValue()!! - initialBackgroundChildProcessNativeCrashValue,
+ )
+ }
+ }
+
+ @Test
+ fun `GleanCrashReporterService does not crash if it can't write to it's file`() {
+ val file =
+ spy(File(context.applicationInfo.dataDir, GleanCrashReporterService.CRASH_FILE_NAME))
+ whenever(file.canWrite()).thenReturn(false)
+ val service = spy(GleanCrashReporterService(context, file))
+
+ assertFalse("No previous persisted crashes must exist", service.file.exists())
+
+ val crash = Crash.UncaughtExceptionCrash(0, RuntimeException("Test"), arrayListOf())
+ service.record(crash)
+
+ assertTrue("Persistence file must exist", service.file.exists())
+ val lines = service.file.readLines()
+ assertEquals("Must be empty due to mocked write error", 0, lines.count())
+ }
+
+ @Test
+ fun `GleanCrashReporterService does not crash if the persistent file is corrupted`() {
+ // Because of how Glean is implemented, it can potentially persist information between
+ // tests or even between test classes, so we compensate by capturing the initial value
+ // to compare to.
+ val initialValue = try {
+ CrashMetrics.crashCount[GleanCrashReporterService.UNCAUGHT_EXCEPTION_KEY].testGetValue()!!
+ } catch (e: NullPointerException) {
+ 0
+ }
+
+ run {
+ val service = spy(GleanCrashReporterService(context))
+
+ assertFalse("No previous persisted crashes must exist", service.file.exists())
+
+ val crash = Crash.UncaughtExceptionCrash(
+ 0,
+ RuntimeException("Test"),
+ arrayListOf(),
+ )
+ service.record(crash)
+
+ assertTrue("Persistence file must exist", service.file.exists())
+
+ // Add bad data
+ service.file.appendText("bad data in here\n")
+
+ val lines = service.file.readLines()
+ assertEquals(
+ "must be native code crash",
+ "{\"type\":\"count\",\"label\":\"${GleanCrashReporterService.UNCAUGHT_EXCEPTION_KEY}\"}",
+ lines.first(),
+ )
+ assertEquals(
+ "must be uncaught exception ping",
+ exceptionPingJson(0, 0, false),
+ lines[1],
+ )
+ assertEquals("bad data in here", lines[2])
+ }
+
+ run {
+ GleanCrashReporterService(context)
+
+ assertEquals(
+ "Glean must record correct value",
+ 1,
+ CrashMetrics.crashCount[GleanCrashReporterService.UNCAUGHT_EXCEPTION_KEY].testGetValue()!! - initialValue,
+ )
+ }
+ }
+
+ @Test
+ fun `GleanCrashReporterService sends crash pings`() {
+ val service = spy(GleanCrashReporterService(context))
+
+ val crash = Crash.NativeCodeCrash(
+ 12340000,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ service.record(crash)
+
+ assertTrue("Persistence file must exist", service.file.exists())
+
+ val lines = service.file.readLines()
+ assertEquals(
+ "First element must be main process native code crash",
+ crashCountJson(GleanCrashReporterService.MAIN_PROCESS_NATIVE_CODE_CRASH_KEY),
+ lines[0],
+ )
+ assertEquals(
+ "Second element must be main process crash ping",
+ crashPingJson(0, "main", 12340000, false),
+ lines[1],
+ )
+
+ run {
+ var pingReceived = false
+ GleanPings.crash.testBeforeNextSubmit { _ ->
+ val date = GregorianCalendar().apply {
+ time = Date(12340000)
+ }
+ date.set(Calendar.SECOND, 0)
+ date.set(Calendar.MILLISECOND, 0)
+ assertEquals(date.time, GleanCrash.time.testGetValue())
+ assertEquals(0L, GleanCrash.uptime.testGetValue())
+ assertEquals("main", GleanCrash.processType.testGetValue())
+ assertEquals(false, GleanCrash.startup.testGetValue())
+ assertEquals("os_fault", GleanCrash.cause.testGetValue())
+ assertEquals("", GleanCrash.remoteType.testGetValue())
+ pingReceived = true
+ }
+
+ GleanCrashReporterService(context)
+ assertTrue("Expected ping to be sent", pingReceived)
+ }
+ }
+
+ @Test
+ fun `GleanCrashReporterService serialized pings are forward compatible`() {
+ val service = spy(GleanCrashReporterService(context))
+
+ // Original ping fields (no e.g. `cause` field)
+ service.file.appendText(
+ "{\"type\":\"ping\",\"uptimeNanos\":0,\"processType\":\"main\"," +
+ "\"timeMillis\":0,\"startup\":false,\"reason\":\"crash\"}\n",
+ )
+
+ assertTrue("Persistence file must exist", service.file.exists())
+
+ run {
+ var pingReceived = false
+ GleanPings.crash.testBeforeNextSubmit { _ ->
+ val date = GregorianCalendar().apply {
+ time = Date(0)
+ }
+ date.set(Calendar.SECOND, 0)
+ date.set(Calendar.MILLISECOND, 0)
+ assertEquals(date.time, GleanCrash.time.testGetValue())
+ assertEquals(0L, GleanCrash.uptime.testGetValue())
+ assertEquals("main", GleanCrash.processType.testGetValue())
+ assertEquals(false, GleanCrash.startup.testGetValue())
+ assertEquals("os_fault", GleanCrash.cause.testGetValue())
+ assertEquals("", GleanCrash.remoteType.testGetValue())
+ pingReceived = true
+ }
+
+ GleanCrashReporterService(context)
+ assertTrue("Expected ping to be sent", pingReceived)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/MozillaSocorroServiceTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/MozillaSocorroServiceTest.kt
new file mode 100644
index 0000000000..0b2ace44fd
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/MozillaSocorroServiceTest.kt
@@ -0,0 +1,693 @@
+/* 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 androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.io.Resources.getResource
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.lib.crash.Crash
+import mozilla.components.support.test.any
+import mozilla.components.support.test.robolectric.testContext
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.io.BufferedReader
+import java.io.ByteArrayInputStream
+import java.io.File
+import java.io.InputStreamReader
+import java.util.zip.GZIPInputStream
+
+@RunWith(AndroidJUnit4::class)
+class MozillaSocorroServiceTest {
+
+ @Test
+ fun `MozillaSocorroService sends native code crashes to GeckoView crash reporter`() {
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ ),
+ )
+ doReturn("").`when`(service).sendReport(anyLong(), any(), any(), any(), anyBoolean(), anyBoolean(), any())
+
+ val crash = Crash.NativeCodeCrash(
+ 123,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ service.report(crash)
+
+ verify(service).report(crash)
+ verify(service).sendReport(123, null, crash.minidumpPath, crash.extrasPath, true, false, crash.breadcrumbs)
+ }
+
+ @Test
+ fun `MozillaSocorroService generated server URL have no spaces`() {
+ val service = MozillaSocorroService(
+ testContext,
+ "Test App",
+ versionName = "test version name",
+ )
+
+ assertFalse(service.serverUrl!!.contains(" "))
+ assertFalse(service.serverUrl!!.contains("}"))
+ assertFalse(service.serverUrl!!.contains("{"))
+ }
+
+ @Test
+ fun `MozillaSocorroService send uncaught exception crashes`() {
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ ),
+ )
+ doReturn("").`when`(service).sendReport(anyLong(), any(), any(), any(), anyBoolean(), anyBoolean(), any())
+
+ val crash = Crash.UncaughtExceptionCrash(123, RuntimeException("Test"), arrayListOf())
+ service.report(crash)
+
+ verify(service).report(crash)
+ verify(service).sendReport(123, crash.throwable, null, null, false, true, crash.breadcrumbs)
+ }
+
+ @Test
+ fun `MozillaSocorroService do not send caught exception`() {
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ ),
+ )
+ doReturn("").`when`(service).sendReport(anyLong(), any(), any(), any(), anyBoolean(), anyBoolean(), any())
+ val throwable = RuntimeException("Test")
+ val breadcrumbs: ArrayList<Breadcrumb> = arrayListOf()
+ val id = service.report(throwable, breadcrumbs)
+
+ verify(service).report(throwable, breadcrumbs)
+ verify(service, never()).sendReport(anyLong(), any(), any(), any(), anyBoolean(), anyBoolean(), any())
+ assertNull(id)
+ }
+
+ @Test
+ fun `MozillaSocorroService native fatal crash request is correct`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(
+ MockResponse().setResponseCode(200)
+ .setBody("CrashID=bp-924121d3-4de3-4b32-ab12-026fc0190928"),
+ )
+ mockWebServer.start()
+ val serverUrl = mockWebServer.url("/")
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ appId = "{aa3c5121-dab2-40e2-81ca-7ea25febc110}",
+ serverUrl = serverUrl.toString(),
+ ),
+ )
+
+ val crash = Crash.NativeCodeCrash(
+ 123456,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ service.report(crash)
+
+ val fileInputStream =
+ ByteArrayInputStream(mockWebServer.takeRequest().body.inputStream().readBytes())
+ val inputStream = GZIPInputStream(fileInputStream)
+ val reader = InputStreamReader(inputStream)
+ val bufferedReader = BufferedReader(reader)
+ val request = bufferedReader.readText()
+
+ assert(request.contains("name=Android_ProcessName\r\n\r\nmozilla.components.lib.crash.test"))
+ assert(request.contains("name=ProductID\r\n\r\n{aa3c5121-dab2-40e2-81ca-7ea25febc110}"))
+ assert(request.contains("name=Vendor\r\n\r\nN/A"))
+ assert(request.contains("name=ReleaseChannel\r\n\r\nN/A"))
+ assert(request.contains("name=Android_PackageName\r\n\r\nmozilla.components.lib.crash.test"))
+ assert(request.contains("name=Android_Device\r\n\r\nrobolectric"))
+ assert(request.contains("name=CrashType\r\n\r\n$FATAL_NATIVE_CRASH_TYPE"))
+ assert(request.contains("name=CrashTime\r\n\r\n123"))
+ assert(request.contains("name=useragent_locale\r\n\r\nen-US"))
+
+ verify(service).report(crash)
+ verify(service).sendReport(123456, null, "dump.path", "extras.path", true, true, crash.breadcrumbs)
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+
+ @Test
+ fun `incorrect file extension is ignored in native fatal crash requests`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(
+ MockResponse().setResponseCode(200)
+ .setBody("CrashID=bp-924121d3-4de3-4b32-ab12-026fc0190928"),
+ )
+ mockWebServer.start()
+ val serverUrl = mockWebServer.url("/")
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ appId = "{aa3c5121-dab2-40e2-81ca-7ea25febc110}",
+ serverUrl = serverUrl.toString(),
+ ),
+ )
+
+ val crash = Crash.NativeCodeCrash(
+ 123456,
+ "test/minidumps/3fa772dc-dc89-c08d-c03e-7f441c50821e.ini",
+ true,
+ "test/file/66dd8af2-643c-ca11-5178-e61c6819f827",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ doReturn(HashMap<String, String>()).`when`(service).readExtrasFromFile(any())
+ doNothing().`when`(service).sendFile(any(), any(), any(), any(), any())
+ service.report(crash)
+
+ verify(service).report(crash)
+ verify(service, times(0)).readExtrasFromFile(any())
+ verify(service, times(0)).sendFile(any(), any(), any(), any(), any())
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+
+ @Test
+ fun `incorrect file format is ignored in native fatal crash requests`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(
+ MockResponse().setResponseCode(200)
+ .setBody("CrashID=bp-924121d3-4de3-4b32-ab12-026fc0190928"),
+ )
+ mockWebServer.start()
+ val serverUrl = mockWebServer.url("/")
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ appId = "{aa3c5121-dab2-40e2-81ca-7ea25febc110}",
+ serverUrl = serverUrl.toString(),
+ ),
+ )
+
+ val crash = Crash.NativeCodeCrash(
+ 123456,
+ "test/minidumps/test.dmp",
+ true,
+ "test/file/test.extra",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ doReturn(HashMap<String, String>()).`when`(service).readExtrasFromFile(any())
+ doNothing().`when`(service).sendFile(any(), any(), any(), any(), any())
+ service.report(crash)
+
+ verify(service).report(crash)
+ verify(service, times(0)).readExtrasFromFile(any())
+ verify(service, times(0)).sendFile(any(), any(), any(), any(), any())
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+
+ @Test
+ fun `correct file format is used in native fatal crash requests`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(
+ MockResponse().setResponseCode(200)
+ .setBody("CrashID=bp-924121d3-4de3-4b32-ab12-026fc0190928"),
+ )
+ mockWebServer.start()
+ val serverUrl = mockWebServer.url("/")
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ appId = "{aa3c5121-dab2-40e2-81ca-7ea25febc110}",
+ serverUrl = serverUrl.toString(),
+ ),
+ )
+
+ val crash = Crash.NativeCodeCrash(
+ 123456,
+ "test/minidumps/3fa772dc-dc89-c08d-c03e-7f441c50821e.dmp",
+ true,
+ "test/file/66dd8af2-643c-ca11-5178-e61c6819f827.extra",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ doReturn(HashMap<String, String>()).`when`(service).readExtrasFromFile(any())
+ doNothing().`when`(service).sendFile(any(), any(), any(), any(), any())
+ service.report(crash)
+
+ verify(service).report(crash)
+ verify(service).readExtrasFromFile(any())
+ verify(service).sendFile(any(), any(), any(), any(), any())
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+
+ @Test
+ fun `MozillaSocorroService parameters is reported correctly`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(
+ MockResponse().setResponseCode(200)
+ .setBody("CrashID=bp-924121d3-4de3-4b32-ab12-026fc0190928"),
+ )
+ mockWebServer.start()
+ val serverUrl = mockWebServer.url("/")
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ appId = "{aa3c5121-dab2-40e2-81ca-7ea25febc110}",
+ version = "test version",
+ buildId = "test build id",
+ vendor = "test vendor",
+ serverUrl = serverUrl.toString(),
+ versionName = "1.0.1",
+ versionCode = "1000",
+ releaseChannel = "test channel",
+ distributionId = "test distribution id",
+ ),
+ )
+
+ val crash = Crash.NativeCodeCrash(
+ 123456,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ service.report(crash)
+
+ val fileInputStream =
+ ByteArrayInputStream(mockWebServer.takeRequest().body.inputStream().readBytes())
+ val inputStream = GZIPInputStream(fileInputStream)
+ val reader = InputStreamReader(inputStream)
+ val bufferedReader = BufferedReader(reader)
+ val request = bufferedReader.readText()
+
+ assert(request.contains("name=Android_ProcessName\r\n\r\nmozilla.components.lib.crash.test"))
+ assert(request.contains("name=ProductID\r\n\r\n{aa3c5121-dab2-40e2-81ca-7ea25febc110}"))
+ assert(request.contains("name=Vendor\r\n\r\ntest vendor"))
+ assert(request.contains("name=ReleaseChannel\r\n\r\ntest channel"))
+ assert(request.contains("name=Android_PackageName\r\n\r\nmozilla.components.lib.crash.test"))
+ assert(request.contains("name=Android_Device\r\n\r\nrobolectric"))
+ assert(request.contains("name=CrashType\r\n\r\n$FATAL_NATIVE_CRASH_TYPE"))
+ assert(request.contains("name=CrashTime\r\n\r\n123"))
+ assert(request.contains("name=GeckoViewVersion\r\n\r\ntest version"))
+ assert(request.contains("name=BuildID\r\n\r\ntest build id"))
+ assert(request.contains("name=Version\r\n\r\n1.0.1"))
+ assert(request.contains("name=ApplicationBuildID\r\n\r\n1000"))
+ assert(request.contains("name=useragent_locale\r\n\r\nen-US"))
+ assert(request.contains("name=DistributionID\r\n\r\ntest distribution id"))
+
+ verify(service).report(crash)
+ verify(service).sendReport(123456, null, "dump.path", "extras.path", true, true, crash.breadcrumbs)
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+
+ @Test
+ fun `MozillaSocorroService native non-fatal crash request is correct`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(
+ MockResponse().setResponseCode(200)
+ .setBody("CrashID=bp-924121d3-4de3-4b32-ab12-026fc0190928"),
+ )
+ mockWebServer.start()
+ val serverUrl = mockWebServer.url("/")
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ appId = "{aa3c5121-dab2-40e2-81ca-7ea25febc110}",
+ vendor = "Mozilla",
+ releaseChannel = "nightly",
+ serverUrl = serverUrl.toString(),
+ ),
+ )
+
+ val crash = Crash.NativeCodeCrash(
+ 123456,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ service.report(crash)
+
+ val fileInputStream =
+ ByteArrayInputStream(mockWebServer.takeRequest().body.inputStream().readBytes())
+ val inputStream = GZIPInputStream(fileInputStream)
+ val reader = InputStreamReader(inputStream)
+ val bufferedReader = BufferedReader(reader)
+ val request = bufferedReader.readText()
+
+ assert(request.contains("name=Android_ProcessName\r\n\r\nmozilla.components.lib.crash.test"))
+ assert(request.contains("name=ProductID\r\n\r\n{aa3c5121-dab2-40e2-81ca-7ea25febc110}"))
+ assert(request.contains("name=Vendor\r\n\r\nMozilla"))
+ assert(request.contains("name=ReleaseChannel\r\n\r\nnightly"))
+ assert(request.contains("name=Android_PackageName\r\n\r\nmozilla.components.lib.crash.test"))
+ assert(request.contains("name=Android_Device\r\n\r\nrobolectric"))
+ assert(request.contains("name=CrashType\r\n\r\n$NON_FATAL_NATIVE_CRASH_TYPE"))
+ assert(request.contains("name=CrashTime\r\n\r\n123"))
+ assert(request.contains("name=useragent_locale\r\n\r\nen-US"))
+
+ verify(service).report(crash)
+ verify(service).sendReport(123456, null, "dump.path", "extras.path", true, false, crash.breadcrumbs)
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+
+ @Test
+ fun `MozillaSocorroService uncaught exception request is correct`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(
+ MockResponse().setResponseCode(200)
+ .setBody("CrashID=bp-924121d3-4de3-4b32-ab12-026fc0190928"),
+ )
+ mockWebServer.start()
+ val serverUrl = mockWebServer.url("/")
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ appId = "{aa3c5121-dab2-40e2-81ca-7ea25febc110}",
+ vendor = "Mozilla",
+ releaseChannel = "nightly",
+ serverUrl = serverUrl.toString(),
+ ),
+ )
+
+ val crash = Crash.UncaughtExceptionCrash(123456, RuntimeException("Test"), arrayListOf())
+ service.report(crash)
+
+ val fileInputStream =
+ ByteArrayInputStream(mockWebServer.takeRequest().body.inputStream().readBytes())
+ val inputStream = GZIPInputStream(fileInputStream)
+ val reader = InputStreamReader(inputStream)
+ val bufferedReader = BufferedReader(reader)
+ val request = bufferedReader.readText()
+
+ assert(request.contains("name=JavaStackTrace\r\n\r\njava.lang.RuntimeException: Test"))
+ assert(request.contains("name=JavaException\r\n\r\n{\"exception\":{\"values\":[{\"stacktrace\":{\"frames\":[{\"module\":\"mozilla.components.lib.crash.service.MozillaSocorroServiceTest\",\"function\":\"MozillaSocorroService uncaught exception request is correct\",\"in_app\":true"))
+ assert(request.contains("name=Android_ProcessName\r\n\r\nmozilla.components.lib.crash.test"))
+ assert(request.contains("name=ProductID\r\n\r\n{aa3c5121-dab2-40e2-81ca-7ea25febc110}"))
+ assert(request.contains("name=Vendor\r\n\r\nMozilla"))
+ assert(request.contains("name=ReleaseChannel\r\n\r\nnightly"))
+ assert(request.contains("name=Android_PackageName\r\n\r\nmozilla.components.lib.crash.test"))
+ assert(request.contains("name=Android_Device\r\n\r\nrobolectric"))
+ assert(request.contains("name=CrashType\r\n\r\n$UNCAUGHT_EXCEPTION_TYPE"))
+ assert(request.contains("name=CrashTime\r\n\r\n123"))
+ assert(request.contains("name=useragent_locale\r\n\r\nen-US"))
+
+ verify(service).report(crash)
+ verify(service).sendReport(123456, crash.throwable, null, null, false, true, crash.breadcrumbs)
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+
+ @Test
+ fun `MozillaSocorroService handles 200 response correctly`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(
+ MockResponse().setResponseCode(200)
+ .setBody("CrashID=bp-924121d3-4de3-4b32-ab12-026fc0190928"),
+ )
+ mockWebServer.start()
+ val serverUrl = mockWebServer.url("/")
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ serverUrl = serverUrl.toString(),
+ ),
+ )
+
+ val crash = Crash.UncaughtExceptionCrash(123, RuntimeException("Test"), arrayListOf())
+ service.report(crash)
+
+ mockWebServer.shutdown()
+ verify(service).report(crash)
+ verify(service).sendReport(123, crash.throwable, null, null, false, true, crash.breadcrumbs)
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+
+ @Test
+ fun `MozillaSocorroService handles 404 response correctly`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(MockResponse().setResponseCode(404).setBody("error"))
+ mockWebServer.start()
+ val serverUrl = mockWebServer.url("/")
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ serverUrl = serverUrl.toString(),
+ ),
+ )
+
+ val crash = Crash.NativeCodeCrash(
+ 123,
+ null,
+ true,
+ null,
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ service.report(crash)
+ mockWebServer.shutdown()
+
+ verify(service).report(crash)
+ verify(service).sendReport(123, null, crash.minidumpPath, crash.extrasPath, true, false, crash.breadcrumbs)
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+
+ @Test
+ fun `MozillaSocorroService parses extrasFile correctly`() {
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ ),
+ )
+ val file = File(getResource("TestExtrasFile").file)
+ val extrasMap = service.readExtrasFromFile(file)
+
+ assertEquals(extrasMap.size, 25)
+ assertEquals(extrasMap["ContentSandboxLevel"], "2")
+ assertEquals(extrasMap["TelemetryEnvironment"], "{\"EscapedField\":\"EscapedData\n\nfoo\"}")
+ assertEquals(extrasMap["EMCheckCompatibility"], "true")
+ assertEquals(extrasMap["ProductName"], "Firefox")
+ assertEquals(extrasMap["ContentSandboxCapabilities"], "119")
+ assertEquals(extrasMap["TelemetryClientId"], "")
+ assertEquals(extrasMap["Vendor"], "Mozilla")
+ assertEquals(extrasMap["InstallTime"], "1000000000")
+ assertEquals(extrasMap["Theme"], "classic/1.0")
+ assertEquals(extrasMap["ReleaseChannel"], "default")
+ assertEquals(extrasMap["SafeMode"], "0")
+ assertEquals(extrasMap["ContentSandboxCapable"], "1")
+ assertEquals(extrasMap["useragent_locale"], "en-US")
+ assertEquals(extrasMap["Version"], "55.0a1")
+ assertEquals(extrasMap["BuildID"], "20170512114708")
+ assertEquals(extrasMap["ProductID"], "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}")
+ assertEquals(extrasMap["TelemetryServerURL"], "")
+ assertEquals(extrasMap["DOMIPCEnabled"], "1")
+ assertEquals(extrasMap["Add-ons"], "")
+ assertEquals(extrasMap["CrashTime"], "1494582646")
+ assertEquals(extrasMap["UptimeTS"], "14.9179586")
+ assertEquals(extrasMap["ThreadIdNameMapping"], "")
+ assertEquals(extrasMap["ContentSandboxEnabled"], "1")
+ assertEquals(extrasMap["StartupTime"], "1000000000")
+ assertFalse(extrasMap.contains("URL"))
+ assertFalse(extrasMap.contains("ServerURL"))
+ assertFalse(extrasMap.contains("StackTraces"))
+ }
+
+ @Test
+ fun `MozillaSocorroService parses legacyExtraFile correctly`() {
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ ),
+ )
+ val file = File(getResource("TestLegacyExtrasFile").file)
+ val extrasMap = service.readExtrasFromFile(file)
+
+ assertEquals(extrasMap.size, 25)
+ assertEquals(extrasMap["ContentSandboxLevel"], "2")
+ assertEquals(extrasMap["TelemetryEnvironment"], "{\"EscapedField\":\"EscapedData\n\nfoo\"}")
+ assertEquals(extrasMap["EMCheckCompatibility"], "true")
+ assertEquals(extrasMap["ProductName"], "Firefox")
+ assertEquals(extrasMap["ContentSandboxCapabilities"], "119")
+ assertEquals(extrasMap["TelemetryClientId"], "")
+ assertEquals(extrasMap["Vendor"], "Mozilla")
+ assertEquals(extrasMap["InstallTime"], "1000000000")
+ assertEquals(extrasMap["Theme"], "classic/1.0")
+ assertEquals(extrasMap["ReleaseChannel"], "default")
+ assertEquals(extrasMap["SafeMode"], "0")
+ assertEquals(extrasMap["ContentSandboxCapable"], "1")
+ assertEquals(extrasMap["useragent_locale"], "en-US")
+ assertEquals(extrasMap["Version"], "55.0a1")
+ assertEquals(extrasMap["BuildID"], "20170512114708")
+ assertEquals(extrasMap["ProductID"], "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}")
+ assertEquals(extrasMap["TelemetryServerURL"], "")
+ assertEquals(extrasMap["DOMIPCEnabled"], "1")
+ assertEquals(extrasMap["Add-ons"], "")
+ assertEquals(extrasMap["CrashTime"], "1494582646")
+ assertEquals(extrasMap["UptimeTS"], "14.9179586")
+ assertEquals(extrasMap["ThreadIdNameMapping"], "")
+ assertEquals(extrasMap["ContentSandboxEnabled"], "1")
+ assertEquals(extrasMap["StartupTime"], "1000000000")
+ assertFalse(extrasMap.contains("URL"))
+ assertFalse(extrasMap.contains("ServerURL"))
+ assertFalse(extrasMap.contains("StackTraces"))
+ }
+
+ @Test
+ fun `MozillaSocorroService handles bad extrasFile correctly`() {
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ ),
+ )
+ val file = File(getResource("BadTestExtrasFile").file)
+ val extrasMap = service.readExtrasFromFile(file)
+
+ assertEquals(extrasMap.size, 0)
+ }
+
+ @Test
+ fun `MozillaSocorroService unescape strings correctly`() {
+ val service = spy(
+ MozillaSocorroService(
+ testContext,
+ "Test App",
+ ),
+ )
+ val test1 = "\\\\\\\\"
+ val expected1 = "\\"
+ assert(service.unescape(test1) == expected1)
+
+ val test2 = "\\\\n"
+ val expected2 = "\n"
+ assert(service.unescape(test2) == expected2)
+
+ val test3 = "\\\\t"
+ val expected3 = "\t"
+ assert(service.unescape(test3) == expected3)
+
+ val test4 = "\\\\\\\\\\\\t\\\\t\\\\n\\\\\\\\"
+ val expected4 = "\\\t\t\n\\"
+ assert(service.unescape(test4) == expected4)
+ }
+
+ @Test
+ fun `MozillaSocorroService returns crash id from Socorro`() {
+ val mockWebServer = MockWebServer()
+
+ try {
+ mockWebServer.enqueue(
+ MockResponse().setResponseCode(200)
+ .setBody("CrashID=bp-924121d3-4de3-4b32-ab12-026fc0190928"),
+ )
+ mockWebServer.start()
+
+ val service = MozillaSocorroService(
+ testContext,
+ "Test App",
+ "{1234-1234-1234}",
+ "0.1",
+ "1.0",
+ "Mozilla Test",
+ mockWebServer.url("/").toString(),
+ "0.0.1",
+ "123",
+ "test channel",
+ "test distribution id",
+ )
+
+ val crash = Crash.NativeCodeCrash(
+ 0,
+ "dump.path",
+ true,
+ "extras.path",
+ processType = Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+ val id = service.report(crash)
+
+ assertEquals("bp-924121d3-4de3-4b32-ab12-026fc0190928", id)
+ } finally {
+ mockWebServer.shutdown()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashReportServiceTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashReportServiceTest.kt
new file mode 100644
index 0000000000..e44dc64c9a
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashReportServiceTest.kt
@@ -0,0 +1,169 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.lib.crash.service
+
+import android.content.ComponentName
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.concept.base.crash.Breadcrumb
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.Robolectric
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class SendCrashReportServiceTest {
+ private var service: SendCrashReportService? = null
+ private val intent = Intent("org.mozilla.gecko.ACTION_CRASHED")
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ @Before
+ fun setUp() {
+ intent.component = ComponentName(
+ "org.mozilla.samples.browser",
+ "mozilla.components.lib.crash.handler.CrashHandlerService",
+ )
+ intent.putExtra(
+ "minidumpPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ )
+ intent.putExtra("fatal", false)
+ intent.putExtra(
+ "extrasPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ )
+ intent.putExtra("minidumpSuccess", true)
+ intent.putParcelableArrayListExtra("breadcrumbs", null)
+ service = spy(Robolectric.setupService(SendCrashReportService::class.java))
+ service?.startService(intent)
+ }
+
+ @After
+ fun tearDown() {
+ service?.stopService(intent)
+ CrashReporter.reset()
+ }
+
+ @Test
+ fun `Send crash report will forward same crash to crash service`() {
+ var caughtCrash: Crash.NativeCodeCrash? = null
+ val crashReporter = spy(
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ services = listOf(
+ object : CrashReporterService {
+ override val id: String = "test"
+
+ override val name: String = "TestReporter"
+
+ override fun createCrashReportUrl(identifier: String): String? = null
+
+ override fun report(crash: Crash.UncaughtExceptionCrash): String? {
+ fail("Didn't expect uncaught exception crash")
+ return null
+ }
+
+ override fun report(crash: Crash.NativeCodeCrash): String? {
+ caughtCrash = crash
+ return null
+ }
+
+ override fun report(throwable: Throwable, breadcrumbs: ArrayList<Breadcrumb>): String? {
+ fail("Didn't expect caught exception")
+ return null
+ }
+ },
+ ),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ),
+ ).install(testContext)
+ val originalCrash = Crash.NativeCodeCrash(
+ 123,
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ true,
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ val intent = Intent("org.mozilla.gecko.ACTION_CRASHED")
+ intent.component = ComponentName(
+ "org.mozilla.samples.browser",
+ "mozilla.components.lib.crash.handler.CrashHandlerService",
+ )
+ intent.putExtra(
+ "minidumpPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ )
+ intent.putExtra("processType", "FOREGROUND_CHILD")
+ intent.putExtra(
+ "extrasPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ )
+ intent.putExtra("minidumpSuccess", true)
+ intent.putParcelableArrayListExtra("breadcrumbs", null)
+ originalCrash.fillIn(intent)
+
+ service?.onStartCommand(intent, 0, 0)
+ verify(crashReporter).submitReport(eq(originalCrash), any())
+ assertNotNull(caughtCrash)
+
+ val nativeCrash = caughtCrash
+ ?: throw AssertionError("Expected NativeCodeCrash instance")
+
+ assertEquals(123, nativeCrash.timestamp)
+ assertEquals(true, nativeCrash.minidumpSuccess)
+ assertEquals(false, nativeCrash.isFatal)
+ assertEquals(Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD, nativeCrash.processType)
+ assertEquals(
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ nativeCrash.minidumpPath,
+ )
+ assertEquals(
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ nativeCrash.extrasPath,
+ )
+ }
+
+ @Test
+ fun `notification tag and id is added to the report intent`() {
+ val crash: Crash = Crash.NativeCodeCrash(
+ 123,
+ "",
+ true,
+ "",
+ Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
+ breadcrumbs = arrayListOf(),
+ remoteType = null,
+ )
+
+ val intent = SendCrashReportService.createReportIntent(testContext, crash, "test_tag", 123)
+
+ assertEquals(intent.getStringExtra(NOTIFICATION_TAG_KEY), "test_tag")
+ assertEquals(intent.getIntExtra(NOTIFICATION_ID_KEY, 0), 123)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashTelemetryServiceTest.kt b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashTelemetryServiceTest.kt
new file mode 100644
index 0000000000..7aa7dcbe55
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashTelemetryServiceTest.kt
@@ -0,0 +1,142 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.lib.crash.service
+
+import android.content.ComponentName
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.lib.crash.Crash
+import mozilla.components.lib.crash.CrashReporter
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.Robolectric
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class SendCrashTelemetryServiceTest {
+ private var service: SendCrashTelemetryService? = null
+ private val intent = Intent("org.mozilla.gecko.ACTION_CRASHED")
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
+
+ @Before
+ fun setUp() {
+ intent.component = ComponentName(
+ "org.mozilla.samples.browser",
+ "mozilla.components.lib.crash.handler.CrashHandlerService",
+ )
+ intent.putExtra(
+ "minidumpPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ )
+ intent.putExtra("fatal", false)
+ intent.putExtra(
+ "extrasPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ )
+ intent.putExtra("minidumpSuccess", true)
+ intent.putParcelableArrayListExtra("breadcrumbs", null)
+ service = spy(Robolectric.setupService(SendCrashTelemetryService::class.java))
+ service?.startService(intent)
+ }
+
+ @After
+ fun tearDown() {
+ service?.stopService(intent)
+ CrashReporter.reset()
+ }
+
+ @Test
+ fun `Send crash telemetry will forward same crash to crash telemetry service`() {
+ var caughtCrash: Crash.NativeCodeCrash? = null
+ val crashReporter = spy(
+ CrashReporter(
+ context = testContext,
+ shouldPrompt = CrashReporter.Prompt.NEVER,
+ telemetryServices = listOf(
+ object : CrashTelemetryService {
+ override fun record(crash: Crash.UncaughtExceptionCrash) {
+ fail("Didn't expect uncaught exception crash")
+ }
+
+ override fun record(crash: Crash.NativeCodeCrash) {
+ caughtCrash = crash
+ }
+
+ override fun record(throwable: Throwable) {
+ fail("Didn't expect caught exception")
+ }
+ },
+ ),
+ scope = scope,
+ notificationsDelegate = mock(),
+ ),
+ ).install(testContext)
+ val originalCrash = Crash.NativeCodeCrash(
+ 123,
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ true,
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD,
+ breadcrumbs = arrayListOf(),
+ remoteType = "null",
+ )
+
+ val intent = Intent("org.mozilla.gecko.ACTION_CRASHED")
+ intent.component = ComponentName(
+ "org.mozilla.samples.browser",
+ "mozilla.components.lib.crash.handler.CrashHandlerService",
+ )
+ intent.putExtra(
+ "minidumpPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ )
+ intent.putExtra("processType", "FOREGROUND_CHILD")
+ intent.putExtra(
+ "extrasPath",
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ )
+ intent.putExtra("minidumpSuccess", true)
+ intent.putParcelableArrayListExtra("breadcrumbs", null)
+ originalCrash.fillIn(intent)
+
+ service?.onStartCommand(intent, 0, 0)
+
+ verify(crashReporter).submitCrashTelemetry(eq(originalCrash), any())
+ assertNotNull(caughtCrash)
+
+ val nativeCrash = caughtCrash
+ ?: throw AssertionError("Expected NativeCodeCrash instance")
+
+ assertEquals(123, nativeCrash.timestamp)
+ assertEquals(true, nativeCrash.minidumpSuccess)
+ assertEquals(false, nativeCrash.isFatal)
+ assertEquals(Crash.NativeCodeCrash.PROCESS_TYPE_FOREGROUND_CHILD, nativeCrash.processType)
+ assertEquals(
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.dmp",
+ nativeCrash.minidumpPath,
+ )
+ assertEquals(
+ "/data/data/org.mozilla.samples.browser/files/mozilla/Crash Reports/pending/3ba5f665-8422-dc8e-a88e-fc65c081d304.extra",
+ nativeCrash.extrasPath,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/resources/BadTestExtrasFile b/mobile/android/android-components/components/lib/crash/src/test/resources/BadTestExtrasFile
new file mode 100755
index 0000000000..20098e30d8
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/resources/BadTestExtrasFile
@@ -0,0 +1 @@
+{"ContentSandboxLevel":"2","TelemetryEnvironment":"{"EscapedField":"EscapedData\\n\\nfoo"}","EMCheckCompatibility":"true","ProductName":"Firefox","ContentSandboxCapabilities":"119","TelemetryClientId":"","Vendor":"Mozilla","InstallTime":"1000000000","Theme":"classic/1.0","ReleaseChannel":"default","ServerURL":"https://crash-reports.mozilla.com","SafeMode":"0","ContentSandboxCapable":"1","useragent_locale":"en-US","Version":"55.0a1","BuildID":"20170512114708","ProductID":"{ec8030f7-c20a-464f-9b0e-13a3a9e97384}","TelemetryServerURL":"","DOMIPCEnabled":"1","Add-ons":"","CrashTime":"1494582646","UptimeTS":"14.9179586","ThreadIdNameMapping":"","ContentSandboxEnabled":"1","ProcessType":"content","StartupTime":"1000000000","URL":"about:home"}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/resources/TestExtrasFile b/mobile/android/android-components/components/lib/crash/src/test/resources/TestExtrasFile
new file mode 100755
index 0000000000..a95eb68ac3
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/resources/TestExtrasFile
@@ -0,0 +1 @@
+{"ContentSandboxLevel":"2","TelemetryEnvironment":"{\"EscapedField\":\"EscapedData\\n\\nfoo\"}","EMCheckCompatibility":"true","ProductName":"Firefox","ContentSandboxCapabilities":"119","TelemetryClientId":"","Vendor":"Mozilla","InstallTime":"1000000000","Theme":"classic/1.0","ReleaseChannel":"default","ServerURL":"https://crash-reports.mozilla.com","SafeMode":"0","ContentSandboxCapable":"1","useragent_locale":"en-US","Version":"55.0a1","BuildID":"20170512114708","ProductID":"{ec8030f7-c20a-464f-9b0e-13a3a9e97384}","TelemetryServerURL":"","DOMIPCEnabled":"1","Add-ons":"","CrashTime":"1494582646","UptimeTS":"14.9179586","ThreadIdNameMapping":"","ContentSandboxEnabled":"1","ProcessType":"content","StartupTime":"1000000000","URL":"about:home","StackTraces":"test"}
diff --git a/mobile/android/android-components/components/lib/crash/src/test/resources/TestLegacyExtrasFile b/mobile/android/android-components/components/lib/crash/src/test/resources/TestLegacyExtrasFile
new file mode 100755
index 0000000000..7260d4d951
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/resources/TestLegacyExtrasFile
@@ -0,0 +1,31 @@
+ContentSandboxLevel=2
+TelemetryEnvironment={"EscapedField":"EscapedData\\n\\nfoo"}
+EMCheckCompatibility=true
+ProductName=Firefox
+ContentSandboxCapabilities=119
+TelemetryClientId=
+Vendor=Mozilla
+InstallTime=1000000000
+Theme=classic/1.0
+ReleaseChannel=default
+ServerURL=https://crash-reports.mozilla.com
+SafeMode=0
+ContentSandboxCapable=1
+useragent_locale=en-US
+Version=55.0a1
+BuildID=20170512114708
+ProductID={ec8030f7-c20a-464f-9b0e-13a3a9e97384}
+TelemetryServerURL=
+DOMIPCEnabled=1
+Add-ons=
+CrashTime=1494582646
+UptimeTS=14.9179586
+ThreadIdNameMapping=
+ContentSandboxLevel=2
+ContentSandboxEnabled=1
+ProcessType=content
+DOMIPCEnabled=1
+StartupTime=1000000000
+URL=about:home
+ContentSandboxCapabilities=119
+StackTraces=test \ No newline at end of file
diff --git a/mobile/android/android-components/components/lib/crash/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/lib/crash/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/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/src/test/resources/robolectric.properties b/mobile/android/android-components/components/lib/crash/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/crash/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/lib/dataprotect/README.md b/mobile/android/android-components/components/lib/dataprotect/README.md
new file mode 100644
index 0000000000..b9ca9068a4
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Libraries > Dataprotect
+
+A component using AndroidKeyStore to protect user data.
+
+## 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-dataprotect:{latest-version}"
+```
+
+## 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/dataprotect/build.gradle b/mobile/android/android-components/components/lib/dataprotect/build.gradle
new file mode 100644
index 0000000000..200ee8b2e0
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/build.gradle
@@ -0,0 +1,39 @@
+/* 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.dataprotect'
+}
+
+dependencies {
+ implementation project(':support-base')
+
+ implementation ComponentsDependencies.androidx_annotation
+
+ testImplementation project(':support-test')
+ 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/dataprotect/proguard-rules.pro b/mobile/android/android-components/components/lib/dataprotect/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/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/dataprotect/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/dataprotect/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<manifest />
diff --git a/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/Keystore.kt b/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/Keystore.kt
new file mode 100644
index 0000000000..8b0b09f2c2
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/Keystore.kt
@@ -0,0 +1,314 @@
+/* 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.dataprotect
+
+import android.annotation.TargetApi
+import android.os.Build.VERSION_CODES.M
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyProperties
+import mozilla.components.support.base.log.logger.Logger
+import java.security.GeneralSecurityException
+import java.security.InvalidKeyException
+import java.security.Key
+import java.security.KeyStore
+import java.security.UnrecoverableKeyException
+import javax.crypto.Cipher
+import javax.crypto.KeyGenerator
+import javax.crypto.SecretKey
+import javax.crypto.spec.GCMParameterSpec
+
+private const val KEYSTORE_TYPE = "AndroidKeyStore"
+private const val ENCRYPTED_VERSION = 0x02
+
+@TargetApi(M)
+internal const val CIPHER_ALG = KeyProperties.KEY_ALGORITHM_AES
+
+@TargetApi(M)
+internal const val CIPHER_MOD = KeyProperties.BLOCK_MODE_GCM
+
+@TargetApi(M)
+internal const val CIPHER_PAD = KeyProperties.ENCRYPTION_PADDING_NONE
+internal const val CIPHER_KEY_LEN = 256
+internal const val CIPHER_TAG_LEN = 128
+internal const val CIPHER_SPEC = "$CIPHER_ALG/$CIPHER_MOD/$CIPHER_PAD"
+
+internal const val CIPHER_NONCE_LEN = 12
+
+/**
+ * Wraps the critical functions around a Java KeyStore to better facilitate testing
+ * and instrumenting.
+ *
+ */
+@TargetApi(M)
+open class KeyStoreWrapper {
+ private var keystore: KeyStore? = null
+ private val logger = Logger("KeyStoreWrapper")
+
+ /**
+ * Retrieves the underlying KeyStore, loading it if necessary.
+ */
+ fun getKeyStore(): KeyStore {
+ var ks = keystore
+ if (ks == null) {
+ ks = loadKeyStore()
+ keystore = ks
+ }
+
+ return ks
+ }
+
+ /**
+ * Retrieves the SecretKey for the given label.
+ *
+ * This method queries for a SecretKey with the given label and no passphrase.
+ *
+ * Subclasses override this method if additional properties are needed
+ * to retrieve the key.
+ *
+ * @param label The label to query
+ * @return The key for the given label, or `null` if not present
+ * @throws InvalidKeyException If there is a Key but it is not a SecretKey
+ * @throws NoSuchAlgorithmException If the recovery algorithm is not supported
+ */
+ open fun getKeyFor(label: String): Key? = try {
+ loadKeyStore().getKey(label, null)
+ } catch (e: UnrecoverableKeyException) {
+ logger.error("Failed to get key", e)
+ null
+ }
+
+ /**
+ * Creates a SecretKey for the given label.
+ *
+ * This method generates a SecretKey pre-bound to the `AndroidKeyStore` and configured
+ * with the strongest "algorithm/blockmode/padding" (and key size) available.
+ *
+ * Subclasses override this method to properly associate the generated key with
+ * the given label in the underlying KeyStore.
+ *
+ * @param label The label to associate with the created key
+ * @return The newly-generated key for `label`
+ * @throws NoSuchAlgorithmException If the cipher algorithm is not supported
+ */
+ open fun makeKeyFor(label: String): SecretKey {
+ val spec = KeyGenParameterSpec.Builder(
+ label,
+ KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
+ )
+ .setKeySize(CIPHER_KEY_LEN)
+ .setBlockModes(CIPHER_MOD)
+ .setEncryptionPaddings(CIPHER_PAD)
+ .build()
+ val gen = KeyGenerator.getInstance(CIPHER_ALG, KEYSTORE_TYPE)
+ gen.init(spec)
+ return gen.generateKey()
+ }
+
+ /**
+ * Deletes a key with the given label.
+ *
+ * @param label The label of the associated key to delete
+ * @throws KeyStoreException If there is no key for `label`
+ */
+ fun removeKeyFor(label: String) {
+ getKeyStore().deleteEntry(label)
+ }
+
+ /**
+ * Creates and initializes the KeyStore in use.
+ *
+ * This method loads a`"AndroidKeyStore"` type KeyStore.
+ *
+ * Subclasses override this to load a KeyStore appropriate to the testing environment.
+ *
+ * @return The KeyStore, already initialized
+ * @throws KeyStoreException if the type of store is not supported
+ */
+ open fun loadKeyStore(): KeyStore {
+ val ks = KeyStore.getInstance(KEYSTORE_TYPE)
+ ks.load(null)
+ return ks
+ }
+}
+
+/**
+ * Manages data protection using a system-isolated cryptographic key.
+ *
+ * This class provides for both:
+ * * management for a specific crypto graphic key (identified by a string label)
+ * * protection (encryption/decryption) of data using the managed key
+ *
+ * The specific cryptographic properties are pre-chosen to be the following:
+ * * Algorithm is "AES/GCM/NoPadding"
+ * * Key size is 256 bits
+ * * Tag size is 128 bits
+ *
+ * @property label The label the cryptographic key is identified as
+ * @constructor Creates a new instance around a key identified by the given label
+ *
+ * Unless `manual` is `true`, the key is created if not already present in the
+ * platform's key storage.
+ */
+@TargetApi(M)
+open class Keystore(
+ val label: String,
+ manual: Boolean = false,
+ internal val wrapper: KeyStoreWrapper = KeyStoreWrapper(),
+) {
+ init {
+ if (!manual and !available()) {
+ generateKey()
+ }
+ }
+
+ private fun getKey(): SecretKey? =
+ wrapper.getKeyFor(label) as? SecretKey?
+
+ /**
+ * Determines if the managed key is available for use. Consumers can use this to
+ * determine if the key was somehow lost and should treat any previously-protected
+ * data as invalid.
+ *
+ * @return `true` if the managed key exists and ready for use.
+ */
+ fun available(): Boolean = (getKey() != null)
+
+ /**
+ * Generates the managed key if it does not already exist.
+ *
+ * @return `true` if a new key was generated; `false` if the key already exists and can
+ * be used.
+ * @throws GeneralSecurityException If the key could not be created
+ */
+ @Throws(GeneralSecurityException::class)
+ fun generateKey(): Boolean {
+ val key = wrapper.getKeyFor(label)
+ if (key != null) {
+ when (key) {
+ is SecretKey -> return false
+ else -> throw InvalidKeyException("unsupported key type")
+ }
+ }
+
+ wrapper.makeKeyFor(label)
+
+ return true
+ }
+
+ /**
+ * Deletes the managed key.
+ *
+ * **NOTE:** Once this method returns, any data protected with the (formerly) managed
+ * key cannot be decrypted and therefore is inaccessble.
+ */
+ fun deleteKey() {
+ val key = wrapper.getKeyFor(label)
+ if (key != null) {
+ wrapper.removeKeyFor(label)
+ }
+ }
+
+ /**
+ * Encrypts data using the managed key.
+ *
+ * The output of this method includes the input factors (i.e., initialization vector),
+ * ciphertext, and authentication tag as a single byte string; this output can be passed
+ * directly to [decryptBytes].
+ *
+ * @param plain The "plaintext" data to encrypt
+ * @return The encrypted data to be stored
+ * @throws GeneralSecurityException If the data could not be encrypted
+ */
+ @Throws(GeneralSecurityException::class)
+ open fun encryptBytes(plain: ByteArray): ByteArray {
+ // 5116-style interface = [ inputs || ciphertext || atag ]
+ // - inputs = [ version = 0x02 || cipher.iv (always 12 bytes) ]
+ // - cipher.doFinal() provides [ ciphertext || atag ]
+ // Cipher operations are not thread-safe so we synchronize over them through doFinal to
+ // prevent crashes with quickly repeated encrypt/decrypt operations
+ // https://github.com/mozilla-mobile/android-components/issues/5342
+ synchronized(this) {
+ val cipher = createEncryptCipher()
+ val cdata = cipher.doFinal(plain)
+ val nonce = cipher.iv
+
+ return byteArrayOf(ENCRYPTED_VERSION.toByte()) + nonce + cdata
+ }
+ }
+
+ /**
+ * Decrypts data using the managed key.
+ *
+ * The input of this method is expected to include input factors (i.e., initialization
+ * vector), ciphertext, and authentication tag as a single byte string; it is the direct
+ * output from [encryptBytes].
+ *
+ * @param encrypted The encrypted data to decrypt
+ * @return The decrypted "plaintext" data
+ * @throws KeystoreException If the data could not be decrypted
+ */
+ @Throws(KeystoreException::class)
+ open fun decryptBytes(encrypted: ByteArray): ByteArray {
+ val version = encrypted[0].toInt()
+ if (version != ENCRYPTED_VERSION) {
+ throw KeystoreException("unsupported encrypted version: $version")
+ }
+
+ // Cipher operations are not thread-safe so we synchronize over them through doFinal to
+ // prevent crashes with quickly repeated encrypt/decrypt operations
+ // https://github.com/mozilla-mobile/android-components/issues/5342
+ synchronized(this) {
+ val iv = encrypted.sliceArray(1..CIPHER_NONCE_LEN)
+ val cdata = encrypted.sliceArray((CIPHER_NONCE_LEN + 1)..encrypted.size - 1)
+ val cipher = createDecryptCipher(iv)
+ return cipher.doFinal(cdata)
+ }
+ }
+
+ /**
+ * Create a cipher initialized for encrypting data with the managed key.
+ *
+ * This "low-level" method is useful when a cryptographic context is needed to integrate with
+ * other APIs, such as the `FingerprintManager`.
+ *
+ * **NOTE:** The caller is responsible for associating certain encryption factors, such as
+ * the initialization vector and/or additional authentication data (AAD), with the resulting
+ * ciphertext or decryption will fail.
+ *
+ * @return The [Cipher], initialized and ready to encrypt data with.
+ * @throws GeneralSecurityException If the Cipher could not be created and initialized
+ */
+ @Throws(GeneralSecurityException::class)
+ open fun createEncryptCipher(): Cipher {
+ val key = getKey() ?: throw InvalidKeyException("unknown label: $label")
+ val cipher = Cipher.getInstance(CIPHER_SPEC)
+ cipher.init(Cipher.ENCRYPT_MODE, key)
+
+ return cipher
+ }
+
+ /**
+ * Create a cipher initialized for decrypting data with the managed key.
+ *
+ * This "low-level" method is useful when a cryptographic context is needed to integrate with
+ * other APIs, such as the `FingerprintManager`.
+ *
+ * **NOTE:** The caller is responsible for associating certain encryption factors, such as
+ * the initialization vector and/or additional authentication data (AAD), with the stored
+ * ciphertext or decryption will fail.
+ *
+ * @param iv The initialization vector/nonce to decrypt with
+ * @return The [Cipher], initialized and ready to decrypt data with.
+ * @throws GeneralSecurityException If the cipher could not be created and initialized
+ */
+ @Throws(GeneralSecurityException::class)
+ open fun createDecryptCipher(iv: ByteArray): Cipher {
+ val key = getKey() ?: throw InvalidKeyException("unknown label: $label")
+ val cipher = Cipher.getInstance(CIPHER_SPEC)
+ cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(CIPHER_TAG_LEN, iv))
+
+ return cipher
+ }
+}
diff --git a/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/KeystoreException.kt b/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/KeystoreException.kt
new file mode 100644
index 0000000000..3b95d4bc32
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/KeystoreException.kt
@@ -0,0 +1,17 @@
+/* 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.dataprotect
+
+import java.security.GeneralSecurityException
+
+/**
+ * Exception type thrown by {@link Keystore} when an error is encountered that
+ * is not otherwise covered by an existing sub-class to `GeneralSecurityException`.
+ *
+ */
+class KeystoreException(
+ message: String? = null,
+ cause: Throwable? = null,
+) : GeneralSecurityException(message, cause)
diff --git a/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/SecureAbove22Preferences.kt b/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/SecureAbove22Preferences.kt
new file mode 100644
index 0000000000..28ae337df9
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/SecureAbove22Preferences.kt
@@ -0,0 +1,231 @@
+/* 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.dataprotect
+
+import android.annotation.TargetApi
+import android.content.Context
+import android.content.Context.MODE_PRIVATE
+import android.content.SharedPreferences
+import android.os.Build
+import android.os.Build.VERSION_CODES.M
+import android.util.Base64
+import mozilla.components.support.base.log.logger.Logger
+import java.nio.charset.StandardCharsets
+import java.security.GeneralSecurityException
+
+private interface KeyValuePreferences {
+ /**
+ * Retrieves all key/value pairs present in the store.
+ *
+ * @return A [Map] containing all key/value pairs present in the store.
+ */
+ fun all(): Map<String, String>
+
+ /**
+ * Retrieves a stored [key]. See [putString] for storing a [key].
+ *
+ * @param key A key name.
+ * @return An optional [String] if [key] is present in the store.
+ */
+ fun getString(key: String): String?
+
+ /**
+ * Stores [value] under [key]. Retrieve it using [getString].
+ *
+ * @param key A key name.
+ * @param value A value for [key].
+ */
+ fun putString(key: String, value: String)
+
+ /**
+ * Removes key/value pair from storage for the provided [key].
+ */
+ fun remove(key: String)
+
+ /**
+ * Clears all key/value pairs from the storage.
+ */
+ fun clear()
+}
+
+/**
+ * A wrapper around [SharedPreferences] which encrypts contents on supported API versions (23+).
+ * Otherwise, this simply delegates to [SharedPreferences].
+ *
+ * In rare circumstances (such as APK signing key rotation) a master key which protects this storage may be lost,
+ * in which case previously stored values will be lost as well. Applications are encouraged to instrument such events.
+ *
+ * @param context A [Context], used for accessing [SharedPreferences].
+ * @param name A name for this storage, used for isolating different instances of [SecureAbove22Preferences].
+ * @param forceInsecure A flag indicating whether to force plaintext storage. If set to `true`,
+ * [InsecurePreferencesImpl21] will be used as a storage layer, otherwise a storage implementation
+ * will be decided based on Android API version, with a preference given to secure storage
+ */
+class SecureAbove22Preferences(context: Context, name: String, forceInsecure: Boolean = false) :
+ KeyValuePreferences {
+ private val impl = if (Build.VERSION.SDK_INT >= M && !forceInsecure) {
+ SecurePreferencesImpl23(context, name)
+ } else {
+ InsecurePreferencesImpl21(context, name)
+ }
+
+ override fun all(): Map<String, String> = impl.all()
+
+ override fun getString(key: String) = impl.getString(key)
+
+ override fun putString(key: String, value: String) = impl.putString(key, value)
+
+ override fun remove(key: String) = impl.remove(key)
+
+ override fun clear() = impl.clear()
+}
+
+/**
+ * A simple [KeyValuePreferences] implementation which entirely delegates to [SharedPreferences] and doesn't perform any
+ * encryption/decryption.
+ */
+@SuppressWarnings("TooGenericExceptionCaught")
+private class InsecurePreferencesImpl21(
+ context: Context,
+ name: String,
+ migrateFromSecureStorage: Boolean = true,
+) : KeyValuePreferences {
+ companion object {
+ private const val SUFFIX = "_kp_pre_m"
+ }
+
+ internal val logger = Logger("mozac/InsecurePreferencesImpl21")
+
+ private val prefs = context.getSharedPreferences("$name$SUFFIX", MODE_PRIVATE)
+
+ init {
+ // Check if we have any encrypted values stored on disk.
+ if (migrateFromSecureStorage && Build.VERSION.SDK_INT >= M && prefs.all.isEmpty()) {
+ val secureStorage = SecurePreferencesImpl23(context, name, false)
+ // Copy over any old values.
+ try {
+ secureStorage.all().forEach {
+ putString(it.key, it.value)
+ }
+ } catch (e: Exception) {
+ // Certain devices crash on various Keystore exceptions. While trying to migrate
+ // to use the plaintext storage we don't want to crash if we can't access secure
+ // storage, and just catch the errors.
+ logger.error("Migrating from secure storage failed", e)
+ }
+ // Erase old storage.
+ secureStorage.clear()
+ }
+ }
+
+ override fun all(): Map<String, String> {
+ return prefs.all.mapNotNull {
+ if (it.value is String) {
+ it.key to it.value as String
+ } else {
+ null
+ }
+ }.toMap()
+ }
+
+ override fun getString(key: String) = prefs.getString(key, null)
+
+ override fun putString(key: String, value: String) = prefs.edit().putString(key, value).apply()
+
+ override fun remove(key: String) = prefs.edit().remove(key).apply()
+
+ override fun clear() = prefs.edit().clear().apply()
+}
+
+/**
+ * A [KeyValuePreferences] which is backed by [SharedPreferences] and performs encryption/decryption of values.
+ */
+@TargetApi(M)
+private class SecurePreferencesImpl23(
+ context: Context,
+ name: String,
+ migrateFromPlaintextStorage: Boolean = true,
+) : KeyValuePreferences {
+ companion object {
+ private const val SUFFIX = "_kp_post_m"
+ private const val BASE_64_FLAGS = Base64.URL_SAFE or Base64.NO_PADDING
+ }
+
+ private val logger = Logger("SecurePreferencesImpl23")
+ private val prefs = context.getSharedPreferences("$name$SUFFIX", MODE_PRIVATE)
+ private val keystore by lazy { Keystore(context.packageName) }
+
+ init {
+ if (migrateFromPlaintextStorage && prefs.all.isEmpty()) {
+ // Check if we have any plaintext values stored on disk. That indicates that we've hit
+ // an API upgrade situation. We just went from pre-M to post-M. Since we already have
+ // the plaintext keys, we can transparently migrate them to use the encrypted storage layer.
+ val insecureStorage = InsecurePreferencesImpl21(context, name, false)
+ // Copy over any old values.
+ insecureStorage.all().forEach {
+ putString(it.key, it.value)
+ }
+ // Erase old storage.
+ insecureStorage.clear()
+ }
+ }
+
+ override fun all(): Map<String, String> {
+ return prefs.all.keys.mapNotNull { key ->
+ getString(key)?.let { value ->
+ key to value
+ }
+ }.toMap()
+ }
+
+ override fun getString(key: String): String? {
+ // The fact that we're possibly generating a managed key here implies that this key could be lost after being
+ // for some reason. One possible reason for a key to be lost is rotating signing keys for the APK.
+ // Applications are encouraged to instrument such events.
+ generateManagedKeyIfNecessary()
+
+ if (!prefs.contains(key)) {
+ return null
+ }
+
+ val value = prefs.getString(key, "")
+ val encrypted = Base64.decode(value, BASE_64_FLAGS)
+
+ return try {
+ String(keystore.decryptBytes(encrypted), StandardCharsets.UTF_8)
+ } catch (error: IllegalArgumentException) {
+ logger.error("IllegalArgumentException exception: ", error)
+ null
+ } catch (error: GeneralSecurityException) {
+ logger.error("Decrypt exception: ", error)
+ null
+ }
+ }
+
+ override fun putString(key: String, value: String) {
+ generateManagedKeyIfNecessary()
+ val editor = prefs.edit()
+
+ val encrypted = keystore.encryptBytes(value.toByteArray(StandardCharsets.UTF_8))
+ val data = Base64.encodeToString(encrypted, BASE_64_FLAGS)
+
+ editor.putString(key, data).apply()
+ }
+
+ override fun remove(key: String) = prefs.edit().remove(key).apply()
+
+ override fun clear() = prefs.edit().clear().apply()
+
+ /**
+ * Generates a "managed key" - a key used to encrypt data stored by this class. This key is "managed" by [Keystore],
+ * which stores it in system's secure storage layer exposed via [AndroidKeyStore].
+ */
+ private fun generateManagedKeyIfNecessary() {
+ // Do we need to check this on every access, or just during instantiation? Is the overhead here worth it?
+ if (!keystore.available()) {
+ keystore.generateKey()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/SecurePrefsReliabilityExperiment.kt b/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/SecurePrefsReliabilityExperiment.kt
new file mode 100644
index 0000000000..8ca545eb3b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/src/main/java/mozilla/components/lib/dataprotect/SecurePrefsReliabilityExperiment.kt
@@ -0,0 +1,151 @@
+/* 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.dataprotect
+
+import android.content.Context
+import android.content.SharedPreferences
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.collect
+import java.lang.Exception
+
+/**
+ * This class exists so that we can measure how reliable our usage of AndroidKeyStore is.
+ *
+ * All of the actions here are executed against SecureAbove22Preferences, which encrypts/decrypts prefs
+ * using a key managed by AndroidKeyStore.
+ * If device is running on API<23, encryption/decryption won't be used.
+ *
+ * Experiment actions are:
+ * - on every invocation, read a persisted value and verify it's correct; if it's missing write it.
+ * - if an error is encountered (e.g. corrupt/missing value), experiment state is reset and the
+ * experiment starts from scratch.
+ *
+ * For each step (get, write, reset), a Fact is emitted describing what happened (success, type of failure).
+ * A special "experiment" Fact will be emitted in case of an unexpected failure.
+ *
+ * Consumers of this experiment are expected to inspect emitted Facts (e.g. record them into telemetry).
+ */
+class SecurePrefsReliabilityExperiment(private val context: Context) {
+ companion object {
+ const val PREFS_NAME = "KsReliabilityExp"
+ const val PREF_DID_STORE_VALUE = "valueStored"
+ const val SECURE_PREFS_NAME = "KsReliabilityExpSecure"
+ const val PREF_KEY = "expKey"
+ const val PREF_VALUE = "some long, mildly interesting string we'd like to store"
+
+ object Actions {
+ const val EXPERIMENT = "experiment"
+ const val GET = "get"
+ const val WRITE = "write"
+ const val RESET = "reset"
+ }
+
+ @Suppress("MagicNumber")
+ enum class Values(val v: Int) {
+ SUCCESS_MISSING(1),
+ SUCCESS_PRESENT(2),
+ FAIL(3),
+ LOST(4),
+ CORRUPTED(5),
+ PRESENT_UNEXPECTED(6),
+ SUCCESS_WRITE(7),
+ SUCCESS_RESET(8),
+ }
+ }
+
+ private val securePrefs by lazy { SecureAbove22Preferences(context, SECURE_PREFS_NAME) }
+
+ private fun prefs(): SharedPreferences {
+ return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+ }
+
+ /**
+ * Runs an experiment. This will emit one or more [Fact]s describing results.
+ */
+ @Suppress("TooGenericExceptionCaught", "ComplexMethod")
+ operator fun invoke() {
+ try {
+ val storedVal = try {
+ securePrefs.getString(PREF_KEY)
+ } catch (e: Exception) {
+ emitFact(Actions.GET, Values.FAIL, mapOf("javaClass" to e.nameForTelemetry()))
+
+ // should this return? or proceed to the write part..?
+ return
+ }
+
+ val valueAlreadyPersisted = prefs().getBoolean(PREF_DID_STORE_VALUE, false)
+
+ val getResult = when {
+ // we didn't store the value yet, and didn't get anything back either
+ (!valueAlreadyPersisted && storedVal == null) -> {
+ Values.SUCCESS_MISSING
+ }
+ // we got back the value we stored
+ (valueAlreadyPersisted && storedVal == PREF_VALUE) -> {
+ Values.SUCCESS_PRESENT
+ }
+ // value was lost
+ (valueAlreadyPersisted && storedVal == null) -> {
+ Values.LOST
+ }
+ // we got some value back, but not what we stored
+ (valueAlreadyPersisted && storedVal != PREF_VALUE) -> {
+ Values.CORRUPTED
+ }
+ // we didn't store the value yet, but got something back either way
+ else -> {
+ Values.PRESENT_UNEXPECTED
+ }
+ }
+
+ emitFact(Actions.GET, getResult)
+
+ when (getResult) {
+ // perform a write of the missing value
+ Values.SUCCESS_MISSING -> {
+ try {
+ securePrefs.putString(PREF_KEY, PREF_VALUE)
+ emitFact(Actions.WRITE, Values.SUCCESS_WRITE)
+ } catch (e: Exception) {
+ emitFact(Actions.WRITE, Values.FAIL, mapOf("javaClass" to e.nameForTelemetry()))
+ }
+ prefs().edit().putBoolean(PREF_DID_STORE_VALUE, true).apply()
+ }
+ // reset our experiment in case of detected failures. this lets us measure the failure rate
+ Values.LOST, Values.CORRUPTED, Values.PRESENT_UNEXPECTED -> {
+ securePrefs.clear()
+ prefs().edit().clear().apply()
+ emitFact(Actions.RESET, Values.SUCCESS_RESET)
+ }
+ else -> {
+ // no-op
+ }
+ }
+ } catch (e: Exception) {
+ emitFact(Actions.EXPERIMENT, Values.FAIL, mapOf("javaClass" to e.nameForTelemetry()))
+ }
+ }
+}
+
+private fun emitFact(
+ item: String,
+ value: SecurePrefsReliabilityExperiment.Companion.Values,
+ metadata: Map<String, Any>? = null,
+) {
+ Fact(
+ Component.LIB_DATAPROTECT,
+ Action.IMPLEMENTATION_DETAIL,
+ item,
+ "${value.v}",
+ metadata,
+ ).collect()
+}
+
+private fun Exception.nameForTelemetry(): String {
+ return this.javaClass.canonicalName ?: "anonymous"
+}
diff --git a/mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/KeystoreTest.kt b/mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/KeystoreTest.kt
new file mode 100644
index 0000000000..ce11b43ad3
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/KeystoreTest.kt
@@ -0,0 +1,137 @@
+/* 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.dataprotect
+
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+import java.nio.charset.StandardCharsets
+import java.security.GeneralSecurityException
+import java.security.Key
+import java.security.KeyStore
+import java.security.SecureRandom
+import java.security.Security
+import javax.crypto.Cipher
+import javax.crypto.KeyGenerator
+import javax.crypto.SecretKey
+
+private val DEFAULTPASS = "testit!".toCharArray()
+
+/* mock keystore wrapper to deal with intricacies of how Java/Anroid key management work */
+internal class MockStoreWrapper : KeyStoreWrapper() {
+ override fun loadKeyStore(): KeyStore {
+ val ks = KeyStore.getInstance("JCEKS")
+ ks.load(null)
+ return ks
+ }
+
+ override fun getKeyFor(label: String): Key? =
+ getKeyStore().getKey(label, DEFAULTPASS)
+ override fun makeKeyFor(label: String): SecretKey {
+ val gen = KeyGenerator.getInstance("AES")
+ gen.init(256)
+ val key = gen.generateKey()
+ getKeyStore().setKeyEntry(label, key, DEFAULTPASS, null)
+
+ return key
+ }
+}
+
+class KeystoreTest {
+
+ private var wrapper = MockStoreWrapper()
+ private var rng = SecureRandom()
+
+ @Before
+ fun setUp() {
+ Security.setProperty("crypto.policy", "unlimited")
+ }
+
+ @Test
+ fun workingWithLabel() {
+ val keystore = Keystore("test-labels", true, wrapper)
+
+ Assert.assertFalse(keystore.available())
+ keystore.generateKey()
+ Assert.assertTrue(keystore.available())
+ keystore.deleteKey()
+ Assert.assertFalse(keystore.available())
+ }
+
+ @Test
+ fun createEncryptCipher() {
+ val keystore = Keystore("test-encrypt-ciphers", true, wrapper)
+
+ Assert.assertFalse(keystore.available())
+ var caught = false
+ var cipher: Cipher? = null
+ try {
+ cipher = keystore.createEncryptCipher()
+ } catch (ex: GeneralSecurityException) {
+ caught = true
+ } finally {
+ Assert.assertTrue("unexpected success", caught)
+ Assert.assertNull(cipher)
+ }
+
+ keystore.generateKey()
+ Assert.assertTrue(keystore.available())
+ cipher = keystore.createEncryptCipher()
+ Assert.assertEquals(CIPHER_SPEC, cipher.algorithm)
+ Assert.assertNotNull(cipher.iv)
+ }
+
+ @Test
+ fun createDecryptCipher() {
+ val keystore = Keystore("test-decrypt-ciphers", true, wrapper)
+ val iv = ByteArray(12)
+ rng.nextBytes(iv)
+
+ Assert.assertFalse(keystore.available())
+ var caught = false
+ var cipher: Cipher? = null
+ try {
+ cipher = keystore.createDecryptCipher(iv)
+ } catch (ex: GeneralSecurityException) {
+ caught = true
+ } finally {
+ Assert.assertTrue("unexpected success", caught)
+ Assert.assertNull(cipher)
+ }
+
+ keystore.generateKey()
+ Assert.assertTrue(keystore.available())
+ cipher = keystore.createDecryptCipher(iv)
+ Assert.assertEquals(CIPHER_SPEC, cipher.algorithm)
+ Assert.assertArrayEquals(iv, cipher.iv)
+ }
+
+ @Test
+ fun testAutoInit() {
+ val keystore = Keystore("test-auto-init", false, wrapper)
+
+ Assert.assertTrue(keystore.available())
+ Assert.assertFalse(keystore.generateKey())
+
+ var cipher: Cipher?
+ cipher = keystore.createEncryptCipher()
+ Assert.assertNotNull(cipher)
+ cipher = keystore.createDecryptCipher(ByteArray(12))
+ Assert.assertNotNull(cipher)
+ }
+
+ @Ignore("https://github.com/mozilla-mobile/android-components/issues/4956")
+ @Test
+ fun cryptoRoundTrip() {
+ val keystore = Keystore("test-roundtrip", wrapper = wrapper)
+
+ var input = "classic plaintext 'hello, world'".toByteArray(StandardCharsets.UTF_8)
+ var encrypted = keystore.encryptBytes(input)
+ Assert.assertNotNull(encrypted)
+ var output = keystore.decryptBytes(encrypted)
+ Assert.assertArrayEquals(input, output)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/SecureAbove22PreferencesTest.kt b/mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/SecureAbove22PreferencesTest.kt
new file mode 100644
index 0000000000..2a8a87308d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/SecureAbove22PreferencesTest.kt
@@ -0,0 +1,190 @@
+/* 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.dataprotect
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+import java.security.Security
+
+@RunWith(AndroidJUnit4::class)
+class SecureAbove22PreferencesTest {
+ @Config(sdk = [21])
+ @Test
+ fun `CRUD tests API level 21 unencrypted`() {
+ val storage = SecureAbove22Preferences(testContext, "hello")
+ val storage2 = SecureAbove22Preferences(testContext, "world")
+
+ // no keys
+ assertNull(storage.getString("hello"))
+ assertTrue(storage2.all().isEmpty())
+
+ // single key
+ storage.putString("hello", "world")
+ assertEquals("world", storage.getString("hello"))
+ assertTrue(storage2.all().isEmpty())
+
+ // single key, updated
+ storage.putString("hello", "you")
+ assertEquals("you", storage.getString("hello"))
+ assertTrue(storage2.all().isEmpty())
+
+ // multiple keys
+ storage.putString("test", "string")
+ assertEquals("string", storage.getString("test"))
+ assertEquals("you", storage.getString("hello"))
+ val all = storage.all()
+ assertEquals(2, all.size)
+ assertEquals("string", all["test"])
+ assertEquals("you", all["hello"])
+ assertTrue(storage2.all().isEmpty())
+
+ // clearing one storage doesn't affect another with a different name
+ storage2.putString("another", "test")
+ assertEquals(1, storage2.all().size)
+ storage2.clear()
+ assertEquals(2, storage.all().size)
+
+ // key removal
+ storage.remove("hello")
+ assertNull(storage.getString("hello"))
+ storage.remove("test")
+ assertNull(storage.getString("test"))
+ assertTrue(storage2.all().isEmpty())
+
+ // clearing
+ storage.putString("one", "two")
+ assertEquals("two", storage.getString("one"))
+ storage.putString("three", "four")
+ assertEquals("four", storage.getString("three"))
+ storage.putString("five", "six")
+ assertEquals("six", storage.getString("five"))
+ assertTrue(storage2.all().isEmpty())
+
+ storage.clear()
+ assertNull(storage.getString("one"))
+ assertNull(storage.getString("three"))
+ assertNull(storage.getString("five"))
+ assertTrue(storage.all().isEmpty())
+ assertTrue(storage2.all().isEmpty())
+ }
+
+ @Config(sdk = [22])
+ @Test
+ fun `CRUD tests API level 22 unencrypted`() {
+ val storage = SecureAbove22Preferences(testContext, "hello")
+ val storage2 = SecureAbove22Preferences(testContext, "world")
+
+ // no keys
+ assertNull(storage.getString("hello"))
+ assertTrue(storage2.all().isEmpty())
+
+ // single key
+ storage.putString("hello", "world")
+ assertEquals("world", storage.getString("hello"))
+ assertTrue(storage2.all().isEmpty())
+
+ // single key, updated
+ storage.putString("hello", "you")
+ assertEquals("you", storage.getString("hello"))
+ assertTrue(storage2.all().isEmpty())
+
+ // multiple keys
+ storage.putString("test", "string")
+ assertEquals("string", storage.getString("test"))
+ assertEquals("you", storage.getString("hello"))
+ val all = storage.all()
+ assertEquals(2, all.size)
+ assertEquals("string", all["test"])
+ assertEquals("you", all["hello"])
+ assertTrue(storage2.all().isEmpty())
+
+ // clearing one storage doesn't affect another with a different name
+ storage2.putString("another", "test")
+ assertEquals(1, storage2.all().size)
+ storage2.clear()
+ assertEquals(2, storage.all().size)
+
+ // key removal
+ storage.remove("hello")
+ assertNull(storage.getString("hello"))
+ storage.remove("test")
+ assertNull(storage.getString("test"))
+ assertTrue(storage2.all().isEmpty())
+
+ // clearing
+ storage.putString("one", "two")
+ assertEquals("two", storage.getString("one"))
+ storage.putString("three", "four")
+ assertEquals("four", storage.getString("three"))
+ storage.putString("five", "six")
+ assertEquals("six", storage.getString("five"))
+ assertTrue(storage2.all().isEmpty())
+
+ storage.clear()
+ assertNull(storage.getString("one"))
+ assertNull(storage.getString("three"))
+ assertNull(storage.getString("five"))
+ assertTrue(storage.all().isEmpty())
+ assertTrue(storage2.all().isEmpty())
+ }
+
+ @Config(sdk = [21])
+ @Test
+ fun `storage instances of the same name are interchangeable`() {
+ val storage = SecureAbove22Preferences(testContext, "hello")
+ val storage2 = SecureAbove22Preferences(testContext, "hello")
+
+ storage.putString("key1", "value1")
+ assertEquals("value1", storage2.getString("key1"))
+
+ storage2.putString("something", "other")
+ assertEquals("other", storage.getString("something"))
+
+ assertEquals(storage.all().size, storage2.all().size)
+ assertEquals(storage.all(), storage2.all())
+
+ storage.clear()
+ assertTrue(storage2.all().isEmpty())
+ }
+
+ @Ignore("https://github.com/mozilla-mobile/android-components/issues/4956")
+ @Config(sdk = [23])
+ @Test
+ fun `CRUD tests API level 23+ encrypted`() {
+ // TODO find out what this is; lockwise tests set it.
+ Security.setProperty("crypto.policy", "unlimited")
+
+ val storage = SecureAbove22Preferences(testContext, "test")
+
+ // no keys
+ assertNull(storage.getString("hello"))
+
+ // single key
+ storage.putString("hello", "world")
+ assertEquals("world", storage.getString("hello"))
+
+ // single key, updated
+ storage.putString("hello", "you")
+ assertEquals("you", storage.getString("hello"))
+
+ // multiple keys
+ storage.putString("test", "string")
+ assertEquals("string", storage.getString("test"))
+ assertEquals("you", storage.getString("hello"))
+
+ // key removal
+ storage.remove("hello")
+ assertNull(storage.getString("hello"))
+ storage.remove("test")
+ assertNull(storage.getString("test"))
+ }
+}
diff --git a/mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/SecurePrefsReliabilityExperimentTest.kt b/mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/SecurePrefsReliabilityExperimentTest.kt
new file mode 100644
index 0000000000..3cd0497dd3
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/src/test/java/mozilla/components/lib/dataprotect/SecurePrefsReliabilityExperimentTest.kt
@@ -0,0 +1,215 @@
+/* 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.dataprotect
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.lib.dataprotect.SecurePrefsReliabilityExperiment.Companion.Actions
+import mozilla.components.lib.dataprotect.SecurePrefsReliabilityExperiment.Companion.Values
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.base.facts.FactProcessor
+import mozilla.components.support.base.facts.Facts
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.times
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class SecurePrefsReliabilityExperimentTest {
+ @Config(sdk = [21])
+ @Test
+ fun `working first run and rerurns emit correct facts`() {
+ val processor: FactProcessor = mock()
+
+ Facts.registerProcessor(processor)
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_MISSING,
+ Actions.WRITE to Values.SUCCESS_WRITE,
+ )
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_PRESENT,
+ )
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_PRESENT,
+ )
+ }
+
+ @Config(sdk = [21])
+ @Test
+ fun `corrupt value returned`() {
+ val processor: FactProcessor = mock()
+
+ Facts.registerProcessor(processor)
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_MISSING,
+ Actions.WRITE to Values.SUCCESS_WRITE,
+ )
+
+ // Now, let's corrupt the value manually
+ val securePrefs = SecureAbove22Preferences(
+ testContext,
+ SecurePrefsReliabilityExperiment.SECURE_PREFS_NAME,
+ )
+ securePrefs.putString(SecurePrefsReliabilityExperiment.PREF_KEY, "wrong test string")
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.CORRUPTED,
+ Actions.RESET to Values.SUCCESS_RESET,
+ )
+
+ // ... and we should be reset now:
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_MISSING,
+ Actions.WRITE to Values.SUCCESS_WRITE,
+ )
+ }
+
+ @Config(sdk = [21])
+ @Test
+ fun `lost value`() {
+ val processor: FactProcessor = mock()
+
+ Facts.registerProcessor(processor)
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_MISSING,
+ Actions.WRITE to Values.SUCCESS_WRITE,
+ )
+
+ // Now, let's corrupt the store manually
+ val securePrefs = SecureAbove22Preferences(
+ testContext,
+ SecurePrefsReliabilityExperiment.SECURE_PREFS_NAME,
+ )
+ securePrefs.clear()
+
+ // loss is detected:
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.LOST,
+ Actions.RESET to Values.SUCCESS_RESET,
+ )
+
+ // we should be reset now:
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_MISSING,
+ Actions.WRITE to Values.SUCCESS_WRITE,
+ )
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_PRESENT,
+ )
+ }
+
+ @Config(sdk = [21])
+ @Test
+ fun `value present unexpectedly`() {
+ val processor: FactProcessor = mock()
+
+ Facts.registerProcessor(processor)
+
+ // First, let's add the correct value manually:
+ val securePrefs = SecureAbove22Preferences(
+ testContext,
+ SecurePrefsReliabilityExperiment.SECURE_PREFS_NAME,
+ )
+ securePrefs.putString(SecurePrefsReliabilityExperiment.PREF_KEY, SecurePrefsReliabilityExperiment.PREF_VALUE)
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.PRESENT_UNEXPECTED,
+ Actions.RESET to Values.SUCCESS_RESET,
+ )
+
+ // Let's try an incorrect value, as well:
+ securePrefs.putString(SecurePrefsReliabilityExperiment.PREF_KEY, "bad string")
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.PRESENT_UNEXPECTED,
+ Actions.RESET to Values.SUCCESS_RESET,
+ )
+
+ // subsequently, it's all good:
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_MISSING,
+ Actions.WRITE to Values.SUCCESS_WRITE,
+ )
+
+ triggerAndAssertFacts(
+ processor,
+ Actions.GET to Values.SUCCESS_PRESENT,
+ )
+ }
+
+ @Test
+ fun `initialization failure`() {
+ // AndroidKeyStore isn't available in the test environment.
+ // This test runs against our target sdk version, so the experiment code will attempt to init
+ // the AndroidKeyStore, that won't be available.
+ val processor: FactProcessor = mock()
+
+ Facts.registerProcessor(processor)
+
+ SecurePrefsReliabilityExperiment(testContext)()
+
+ val captor = argumentCaptor<Fact>()
+ Mockito.verify(processor).process(captor.capture())
+
+ assertEquals(1, captor.allValues.size)
+ assertExperimentFact(
+ captor.allValues[0],
+ Actions.GET,
+ Values.FAIL,
+ mapOf("javaClass" to "java.security.KeyStoreException"),
+ )
+ }
+
+ private fun triggerAndAssertFacts(processor: FactProcessor, vararg factPairs: Pair<String, Values>) {
+ with(argumentCaptor<Fact>()) {
+ SecurePrefsReliabilityExperiment(testContext)()
+ Mockito.verify(processor, times(factPairs.size)).process(this.capture())
+ assertEquals(factPairs.size, this.allValues.size)
+ factPairs.forEachIndexed { index, pair ->
+ assertExperimentFact(this.allValues[index], pair.first, pair.second)
+ }
+ }
+ reset(processor)
+ }
+
+ private fun assertExperimentFact(
+ fact: Fact,
+ item: String,
+ value: Values,
+ metadata: Map<String, Any>? = null,
+ ) {
+ assertEquals(Component.LIB_DATAPROTECT, fact.component)
+ assertEquals(Action.IMPLEMENTATION_DETAIL, fact.action)
+ assertEquals(item, fact.item)
+ assertEquals("${value.v}", fact.value)
+ assertEquals(metadata, fact.metadata)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/dataprotect/src/test/resources/robolectric.properties b/mobile/android/android-components/components/lib/dataprotect/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/dataprotect/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/lib/fetch-httpurlconnection/README.md b/mobile/android/android-components/components/lib/fetch-httpurlconnection/README.md
new file mode 100644
index 0000000000..21ed90d434
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-httpurlconnection/README.md
@@ -0,0 +1,25 @@
+# [Android Components](../../../README.md) > Libraries > Fetch-HttpURLConnection
+
+A [concept-fetch](../../concept/fetch/README.md) implementation using [HttpURLConnection](https://developer.android.com/reference/java/net/HttpURLConnection.html).
+
+This implementation of `concept-fetch` uses [HttpURLConnection](https://developer.android.com/reference/java/net/HttpURLConnection.html) from the standard library of the Android System. Therefore this component has no third-party dependencies and is smaller than other implementations. It's intended use is for apps that have strict APK size constraints.
+
+## 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-fetch-httpurlconnection:{latest-version}"
+```
+
+### Performing requests
+
+See the [concept-fetch documentation](../../concept/fetch/README.md) for generic examples of using the API of components implementing `concept-fetch`.
+
+## 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/fetch-httpurlconnection/build.gradle b/mobile/android/android-components/components/lib/fetch-httpurlconnection/build.gradle
new file mode 100644
index 0000000000..7065a851cb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-httpurlconnection/build.gradle
@@ -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/. */
+
+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.fetch.httpurlconnection'
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation project(':concept-fetch')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_mockito
+
+ testImplementation project(':tooling-fetch-tests')
+}
+
+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/fetch-httpurlconnection/proguard-rules.pro b/mobile/android/android-components/components/lib/fetch-httpurlconnection/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-httpurlconnection/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/fetch-httpurlconnection/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<manifest />
diff --git a/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/main/java/mozilla/components/lib/fetch/httpurlconnection/HttpURLConnectionClient.kt b/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/main/java/mozilla/components/lib/fetch/httpurlconnection/HttpURLConnectionClient.kt
new file mode 100644
index 0000000000..7ded64ea15
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/main/java/mozilla/components/lib/fetch/httpurlconnection/HttpURLConnectionClient.kt
@@ -0,0 +1,195 @@
+/* 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.fetch.httpurlconnection
+
+import mozilla.components.concept.fetch.BuildConfig
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Headers
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.concept.fetch.isDataUri
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient.Companion.getOrCreateCookieManager
+import java.io.FileNotFoundException
+import java.io.IOException
+import java.io.InputStream
+import java.net.CookieHandler
+import java.net.CookieManager
+import java.net.HttpURLConnection
+import java.net.URL
+import java.util.zip.GZIPInputStream
+
+/**
+ * [HttpURLConnection] implementation of [Client].
+ */
+class HttpURLConnectionClient : Client() {
+ private val defaultHeaders: Headers = MutableHeaders(
+ "User-Agent" to "MozacFetch/${BuildConfig.LIBRARY_VERSION}",
+ "Accept-Encoding" to "gzip",
+ )
+
+ @Throws(IOException::class)
+ override fun fetch(request: Request): Response {
+ if (request.private) {
+ throw IllegalArgumentException("Client doesn't support private request")
+ }
+ if (request.isDataUri()) {
+ return fetchDataUri(request)
+ }
+
+ val connection = (URL(request.url).openConnection() as HttpURLConnection)
+
+ connection.setupWith(request)
+ connection.addHeadersFrom(request, defaultHeaders)
+ connection.addBodyFrom(request)
+
+ return connection.toResponse()
+ }
+
+ companion object {
+ fun getOrCreateCookieManager(): CookieManager {
+ if (CookieHandler.getDefault() == null) {
+ CookieHandler.setDefault(CookieManager())
+ }
+ return CookieHandler.getDefault() as CookieManager
+ }
+ }
+}
+
+private fun HttpURLConnection.addBodyFrom(request: Request) {
+ if (request.body == null) {
+ return
+ }
+
+ request.body?.let { body ->
+ doOutput = true
+
+ body.useStream { inStream ->
+ outputStream.use { outStream ->
+ inStream
+ .buffered()
+ .copyTo(outStream)
+ outStream.flush()
+ }
+ }
+ }
+}
+
+internal fun HttpURLConnection.setupWith(request: Request) {
+ requestMethod = request.method.name
+ instanceFollowRedirects = request.redirect == Request.Redirect.FOLLOW
+
+ request.connectTimeout?.let { (timeout, unit) ->
+ connectTimeout = unit.toMillis(timeout).toInt()
+ }
+
+ request.readTimeout?.let { (timeout, unit) ->
+ readTimeout = unit.toMillis(timeout).toInt()
+ }
+
+ useCaches = request.useCaches
+
+ // HttpURLConnection can't be configured to omit cookies. As
+ // a workaround, we delete all cookies we have stored for
+ // the request URI.
+ val cookieManager = getOrCreateCookieManager()
+ if (request.cookiePolicy == Request.CookiePolicy.OMIT) {
+ val uri = URL(request.url).toURI()
+ for (cookie in cookieManager.cookieStore.get(uri)) {
+ cookieManager.cookieStore.remove(uri, cookie)
+ }
+ }
+}
+
+private fun HttpURLConnection.addHeadersFrom(request: Request, defaultHeaders: Headers) {
+ defaultHeaders.filter { header ->
+ request.headers?.contains(header.name) != true
+ }.forEach { header ->
+ setRequestProperty(header.name, header.value)
+ }
+
+ request.headers?.forEach { header ->
+ addRequestProperty(header.name, header.value)
+ }
+}
+
+private fun HttpURLConnection.toResponse(): Response {
+ val headers = translateHeaders(this)
+ return Response(
+ url.toString(),
+ responseCode,
+ headers,
+ createBody(this, headers["Content-Type"]),
+ )
+}
+
+private fun translateHeaders(connection: HttpURLConnection): Headers {
+ val headers = MutableHeaders()
+
+ var index = 0
+
+ while (connection.getHeaderField(index) != null) {
+ val name = connection.getHeaderFieldKey(index)
+ if (name == null) {
+ index++
+ continue
+ }
+
+ val value = connection.getHeaderField(index)
+
+ headers.append(name, value)
+
+ index++
+ }
+
+ return headers
+}
+
+private fun createBody(connection: HttpURLConnection, contentType: String?): Response.Body {
+ val gzipped = connection.contentEncoding == "gzip"
+
+ withFileNotFoundExceptionIgnored {
+ return HttpUrlConnectionBody(
+ connection,
+ connection.inputStream,
+ gzipped,
+ contentType,
+ )
+ }
+
+ withFileNotFoundExceptionIgnored {
+ return HttpUrlConnectionBody(
+ connection,
+ connection.errorStream,
+ gzipped,
+ contentType,
+ )
+ }
+
+ return EmptyBody()
+}
+
+private class EmptyBody : Response.Body("".byteInputStream())
+
+private class HttpUrlConnectionBody(
+ private val connection: HttpURLConnection,
+ stream: InputStream,
+ gzipped: Boolean,
+ contentType: String?,
+) : Response.Body(if (gzipped) GZIPInputStream(stream) else stream, contentType) {
+ override fun close() {
+ super.close()
+
+ connection.disconnect()
+ }
+}
+
+private inline fun withFileNotFoundExceptionIgnored(block: () -> Unit) {
+ try {
+ block()
+ } catch (e: FileNotFoundException) {
+ // Ignore
+ }
+}
diff --git a/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/java/mozilla/components/lib/fetch/httpurlconnection/HttpUrlConnectionFetchTestCases.kt b/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/java/mozilla/components/lib/fetch/httpurlconnection/HttpUrlConnectionFetchTestCases.kt
new file mode 100644
index 0000000000..77b086c8d3
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/java/mozilla/components/lib/fetch/httpurlconnection/HttpUrlConnectionFetchTestCases.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.fetch.httpurlconnection
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.net.HttpURLConnection
+import java.net.URL
+
+@RunWith(AndroidJUnit4::class)
+class HttpUrlConnectionFetchTestCases : mozilla.components.tooling.fetch.tests.FetchTestCases() {
+ override fun createNewClient(): Client = HttpURLConnectionClient()
+
+ // Inherits test methods from generic test suite base class
+
+ @Test
+ fun `Client instance`() {
+ // We need at least one test case defined here so that this is recognized as test class.
+ assertTrue(createNewClient() is HttpURLConnectionClient)
+ }
+
+ @Test
+ override fun get200WithCacheControl() {
+ // We can't run the base fetch test case because HttpResponseCache
+ // doesn't work in a unit test. So we test that we set the
+ // flag correctly instead.
+ val connection = (URL("https://mozilla.org").openConnection() as HttpURLConnection)
+ assertTrue(connection.useCaches)
+
+ connection.setupWith((Request("https://mozilla.org", useCaches = false)))
+ assertFalse(connection.useCaches)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/lib/fetch-httpurlconnection/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/fetch-httpurlconnection/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/fetch-httpurlconnection/src/test/resources/robolectric.properties b/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-httpurlconnection/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/lib/fetch-okhttp/README.md b/mobile/android/android-components/components/lib/fetch-okhttp/README.md
new file mode 100644
index 0000000000..d1cd0a3d2f
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-okhttp/README.md
@@ -0,0 +1,25 @@
+# [Android Components](../../../README.md) > Libraries > Fetch-OkHttp
+
+A [concept-fetch](../../concept/fetch/README.md) implementation using [OkHttp](https://github.com/square/okhttp).
+
+This implementation of `concept-fetch` uses [OkHttp](https://github.com/square/okhttp) - a third-party library from Square. It is intended for apps that already use OkHttp and want components to use the same client.
+
+## 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-fetch-okhttp:{latest-version}"
+```
+
+### Performing requests
+
+See the [concept-fetch documentation](../../concept/fetch/README.md) for generic examples of using the API of components implementing `concept-fetch`.
+
+## 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/fetch-okhttp/build.gradle b/mobile/android/android-components/components/lib/fetch-okhttp/build.gradle
new file mode 100644
index 0000000000..c7f1dd9495
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-okhttp/build.gradle
@@ -0,0 +1,43 @@
+/* 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.fetch.okhttp'
+
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.thirdparty_okhttp
+ implementation ComponentsDependencies.thirdparty_okhttp_urlconnection
+
+ implementation project(':concept-fetch')
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation project(':tooling-fetch-tests')
+}
+
+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/fetch-okhttp/proguard-rules.pro b/mobile/android/android-components/components/lib/fetch-okhttp/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-okhttp/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/fetch-okhttp/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/fetch-okhttp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-okhttp/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<manifest />
diff --git a/mobile/android/android-components/components/lib/fetch-okhttp/src/main/java/mozilla/components/lib/fetch/okhttp/OkHttpClient.kt b/mobile/android/android-components/components/lib/fetch-okhttp/src/main/java/mozilla/components/lib/fetch/okhttp/OkHttpClient.kt
new file mode 100644
index 0000000000..0b885eee44
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-okhttp/src/main/java/mozilla/components/lib/fetch/okhttp/OkHttpClient.kt
@@ -0,0 +1,149 @@
+/* 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.fetch.okhttp
+
+import android.content.Context
+import mozilla.components.concept.fetch.BuildConfig
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Headers
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.concept.fetch.isDataUri
+import mozilla.components.lib.fetch.okhttp.OkHttpClient.Companion.CACHE_MAX_SIZE
+import mozilla.components.lib.fetch.okhttp.OkHttpClient.Companion.getOrCreateCookieManager
+import okhttp3.Cache
+import okhttp3.CacheControl
+import okhttp3.JavaNetCookieJar
+import okhttp3.OkHttpClient
+import okhttp3.RequestBody.Companion.toRequestBody
+import java.net.CookieHandler
+import java.net.CookieManager
+
+typealias RequestBuilder = okhttp3.Request.Builder
+
+/**
+ * [Client] implementation using OkHttp.
+ */
+class OkHttpClient(
+ private val client: OkHttpClient = OkHttpClient(),
+ private val context: Context? = null,
+) : Client() {
+ private val defaultHeaders: Headers = MutableHeaders(
+ "User-Agent" to "MozacFetch/${BuildConfig.LIBRARY_VERSION}",
+ "Accept-Encoding" to "gzip",
+ )
+
+ override fun fetch(request: Request): Response {
+ if (request.private) {
+ throw IllegalArgumentException("Client doesn't support private request")
+ }
+
+ if (request.isDataUri()) {
+ return fetchDataUri(request)
+ }
+
+ val requestClient = client.rebuildFor(request, context)
+
+ val requestBuilder = createRequestBuilderWithBody(request)
+ requestBuilder.addHeadersFrom(request, defaultHeaders = defaultHeaders)
+
+ if (!request.useCaches) {
+ requestBuilder.cacheControl(CacheControl.FORCE_NETWORK)
+ }
+
+ val actualResponse = requestClient.newCall(
+ requestBuilder.build(),
+ ).execute()
+
+ return actualResponse.toResponse()
+ }
+
+ companion object {
+ internal const val CACHE_MAX_SIZE: Long = 10L * 1024L * 1024L
+
+ fun getOrCreateCookieManager(): CookieManager {
+ if (CookieHandler.getDefault() == null) {
+ CookieHandler.setDefault(CookieManager())
+ }
+ return CookieHandler.getDefault() as CookieManager
+ }
+ }
+}
+
+private fun OkHttpClient.rebuildFor(request: Request, context: Context?): OkHttpClient {
+ @Suppress("ComplexCondition")
+ if (request.connectTimeout != null ||
+ request.readTimeout != null ||
+ request.redirect != Request.Redirect.FOLLOW ||
+ request.cookiePolicy != Request.CookiePolicy.OMIT
+ ) {
+ val clientBuilder = newBuilder()
+
+ request.connectTimeout?.let { (timeout, unit) -> clientBuilder.connectTimeout(timeout, unit) }
+ request.readTimeout?.let { (timeout, unit) -> clientBuilder.readTimeout(timeout, unit) }
+
+ if (request.redirect == Request.Redirect.MANUAL) {
+ clientBuilder.followRedirects(false)
+ }
+
+ if (request.cookiePolicy == Request.CookiePolicy.INCLUDE) {
+ clientBuilder.cookieJar(JavaNetCookieJar(getOrCreateCookieManager()))
+ }
+
+ context?.let {
+ clientBuilder.cache(Cache(context.cacheDir, CACHE_MAX_SIZE))
+ }
+
+ return clientBuilder.build()
+ }
+
+ return this
+}
+
+private fun okhttp3.Response.toResponse(): Response {
+ val body = body
+ val headers = translateHeaders(headers)
+
+ return Response(
+ url = request.url.toString(),
+ headers = headers,
+ status = code,
+ body = if (body != null) Response.Body(body.byteStream(), headers["Content-Type"]) else Response.Body.empty(),
+ )
+}
+
+private fun createRequestBuilderWithBody(request: Request): RequestBuilder {
+ val requestBody = request.body?.useStream { it.readBytes() }?.let {
+ it.toRequestBody(null, 0, it.size)
+ }
+
+ return RequestBuilder()
+ .url(request.url)
+ .method(request.method.name, requestBody)
+}
+
+private fun RequestBuilder.addHeadersFrom(request: Request, defaultHeaders: Headers) {
+ defaultHeaders
+ .filter { header ->
+ request.headers?.contains(header.name) != true
+ }.filter { header ->
+ header.name != "Accept-Encoding" && header.value != "gzip"
+ }.forEach { header ->
+ addHeader(header.name, header.value)
+ }
+
+ request.headers?.forEach { header -> addHeader(header.name, header.value) }
+}
+
+private fun translateHeaders(actualHeaders: okhttp3.Headers): Headers {
+ val headers = MutableHeaders()
+
+ for (i in 0 until actualHeaders.size) {
+ headers.append(actualHeaders.name(i), actualHeaders.value(i))
+ }
+
+ return headers
+}
diff --git a/mobile/android/android-components/components/lib/fetch-okhttp/src/test/java/mozilla/components/lib/fetch/okhttp/OkHttpFetchTestCases.kt b/mobile/android/android-components/components/lib/fetch-okhttp/src/test/java/mozilla/components/lib/fetch/okhttp/OkHttpFetchTestCases.kt
new file mode 100644
index 0000000000..50610409c3
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-okhttp/src/test/java/mozilla/components/lib/fetch/okhttp/OkHttpFetchTestCases.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.fetch.okhttp
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.fetch.Client
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.tooling.fetch.tests.FetchTestCases
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class OkHttpFetchTestCases : FetchTestCases() {
+
+ override fun createNewClient(): Client = OkHttpClient(okhttp3.OkHttpClient(), testContext)
+
+ // Inherits test methods from generic test suite base class
+
+ @Test
+ fun `Client instance`() {
+ // We need at least one test case defined here so that this is recognized as test class.
+ assertTrue(createNewClient() is OkHttpClient)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/fetch-okhttp/src/test/resources/robolectric.properties b/mobile/android/android-components/components/lib/fetch-okhttp/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/fetch-okhttp/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/lib/jexl/README.md b/mobile/android/android-components/components/lib/jexl/README.md
new file mode 100644
index 0000000000..646034363a
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/README.md
@@ -0,0 +1,236 @@
+# [Android Components](../../../README.md) > Libraries > JEXL
+
+Javascript Expression Language: Powerful context-based expression parser and evaluator.
+
+This implementation is based on [Mozjexl](https://github.com/mozilla/mozjexl), a fork of Jexl (designed and created at TechnologyAdvice) for use at Mozilla, specifically as a part of SHIELD and Normandy.
+
+Features not supported yet:
+
+* JavaScript object properties (e.g. [String.length](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length))
+* Adding custom operators (binary/unary)
+
+Other implementations:
+
+* [JavaScript](https://github.com/mozilla/mozjexl)
+* [Python](https://github.com/mozilla/pyjexl)
+
+## 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:jexl:{latest-version}
+```
+
+### Evaluating expressions
+
+```Kotlin
+val jexl = Jexl()
+
+val result = jexl.evaluate("75 > 42")
+
+// evaluate() returns an object of type JexlValue. Calling toKotlin() converts this
+// into a matching Kotlin type (in this case a Boolean).
+println(result.value) // Prints "true"
+```
+
+Often expressions should return a `Boolean`value. In this case `evaluateBooleanExpression` is a helper that always returns a Kotlin `Boolean` and never throws an exception (Returns false).
+
+```Kotlin
+val jexl = Jexl()
+
+// "result" has type Boolean and value "true"
+val result = jexl.evaluateBooleanExpression("42 + 23 > 50", defaultValue = false)
+```
+
+
+### Unary Operators
+
+| Operation | Symbol |
+|-----------|:------:|
+| Negate | ! |
+
+### Binary Operators
+
+| Operation | Symbol |
+|------------------|:----------------:|
+| Add, Concat | + |
+| Subtract | - |
+| Multiply | * |
+| Divide | / |
+| Divide and floor | // |
+| Modulus | % |
+| Power of | ^ |
+| Logical AND | && |
+| Logical OR | &#124;&#124; |
+
+### Comparison
+
+| Comparison | Symbol |
+|----------------------------|:------:|
+| Equal | == |
+| Not equal | != |
+| Greater than | > |
+| Greater than or equal | >= |
+| Less than | < |
+| Less than or equal | <= |
+| Element in array or string | in |
+
+### Ternary operator
+
+Conditional expressions check to see if the first segment evaluates to a truthy
+value. If so, the consequent segment is evaluated. Otherwise, the alternate
+is. If the consequent section is missing, the test result itself will be used
+instead.
+
+| Expression | Result |
+|-------------------------------------|--------|
+| `"" ? "Full" : "Empty"` | Empty |
+| `"foo" in "foobar" ? "Yes" : "No"` | Yes |
+| `{agent: "Archer"}.agent ?: "Kane"` | Archer |
+
+### Native Types
+
+| Type | Examples |
+|-----------|:------------------------------:|
+| Booleans | `true`, `false` |
+| Strings | "Hello \"user\"", 'Hey there!' |
+| Integers | 6, -7, 5, -3 |
+| Doubles | -7.2, -3.14159 |
+| Objects | {hello: "world!"} |
+| Arrays | ['hello', 'world!'] |
+| Undefined | `undefined` |
+
+The JavaScript implementation of Jexl uses a `Numeric` type. This implementation dynamically casts between `Integer` and `Double` as needed.
+
+### Groups
+
+Parentheses work just how you'd expect them to:
+
+| Expression | Result |
+|---------------------------------------|:-------|
+| `(83 + 1) / 2` | 42 |
+| `1 < 3 && (4 > 2 &#124;&#124; 2 > 4)` | true |
+
+### Identifiers
+
+Access variables in the context object by just typing their name. Objects can
+be traversed with dot notation, or by using brackets to traverse to a dynamic
+property name.
+
+Example context:
+
+```javascript
+{
+ name: {
+ first: "Malory",
+ last: "Archer"
+ },
+ exes: [
+ "Nikolai Jakov",
+ "Len Trexler",
+ "Burt Reynolds"
+ ],
+ lastEx: 2
+}
+```
+
+| Expression | Result |
+|---------------------|---------------|
+| `name.first` | Malory |
+| `name['la' + 'st']` | Archer |
+| `exes[2]` | Burt Reynolds |
+| `exes[lastEx - 1]` | Len Trexler |
+
+### Collections
+
+Collections, or arrays of objects, can be filtered by including a filter
+expression in brackets. Properties of each collection can be referenced by
+prefixing them with a leading dot. The result will be an array of the objects
+for which the filter expression resulted in a truthy value.
+
+Example context:
+
+```javascript
+{
+ employees: [
+ {first: 'Sterling', last: 'Archer', age: 36},
+ {first: 'Malory', last: 'Archer', age: 75},
+ {first: 'Lana', last: 'Kane', age: 33},
+ {first: 'Cyril', last: 'Figgis', age: 45},
+ {first: 'Cheryl', last: 'Tunt', age: 28}
+ ],
+ retireAge: 62
+}
+```
+
+| Expression | Result |
+|-------------------------------------------------|---------------------------------------------------------------------------------------|
+| `employees[.first == 'Sterling']` | [{first: 'Sterling', last: 'Archer', age: 36}] |
+| `employees[.last == 'Tu' + 'nt'].first` | Cheryl |
+| `employees[.age >= 30 && .age < 40]` | [{first: 'Sterling', last: 'Archer', age: 36},{first: 'Lana', last: 'Kane', age: 33}] |
+| `employees[.age >= 30 && .age < 40][.age < 35]` | [{first: 'Lana', last: 'Kane', age: 33}] |
+| `employees[.age >= retireAge].first` | Malory |
+
+### Transforms
+
+The power of Jexl is in transforming data. Transform functions take one or more arguments: The value to be transformed, followed by anything else passed to it in the expression.
+
+```Kotlin
+val jexl = Jexl()
+
+jexl.addTransform("split") { value, arguments ->
+ value.toString().split(arguments.first().toString()).toJexlArray()
+}
+
+jexl.addTransform("lower") { value, _ ->
+ value.toString().toLowerCase().toJexl()
+}
+
+jexl.addTransform("last") { value, _ ->
+ (value as JexlArray).values.last()
+}
+```
+
+| Expression | Result |
+|-------------------------------------------------|-----------------------|
+| `"Pam Poovey"&#124;lower&#124;split(' ')|first` | poovey |
+| `"password==guest"&#124;split('=' + '=')` | ['password', 'guest'] |
+
+### Context
+
+Variable contexts are straightforward Objects that can be accessed
+in the expression.
+
+```Kotlin
+val context = Context(
+ "employees" to JexlArray(
+ JexlObject(
+ "first" to "Sterling".toJexl(),
+ "last" to "Archer".toJexl(),
+ "age" to 36.toJexl()),
+ JexlObject(
+ "first" to "Malory".toJexl(),
+ "last" to "Archer".toJexl(),
+ "age" to 75.toJexl()),
+ JexlObject(
+ "first" to "Malory".toJexl(),
+ "last" to "Archer".toJexl(),
+ "age" to 33.toJexl())
+ )
+)
+```
+
+| Expression | Result |
+|-------------------------------------------------|------------------------------------------------------------------------------|
+| `employees[.age >= 30 && .age < 40]` | [{first=Sterling, last=Archer, age=36}, {first=Malory, last=Archer, age=33}] |
+| `employees[.age >= 30 && .age < 90][.age < 37]` | [{first=Malory, last=Archer, age=33}] |
+
+
+## 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/jexl/build.gradle b/mobile/android/android-components/components/lib/jexl/build.gradle
new file mode 100644
index 0000000000..0c4e21a5af
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/build.gradle
@@ -0,0 +1,34 @@
+/* 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.jexl'
+}
+
+dependencies {
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.testing_mockito
+}
+
+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/jexl/proguard-rules.pro b/mobile/android/android-components/components/lib/jexl/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/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/jexl/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/jexl/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<manifest />
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/Jexl.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/Jexl.kt
new file mode 100644
index 0000000000..f76544fb23
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/Jexl.kt
@@ -0,0 +1,102 @@
+/* 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.jexl
+
+import mozilla.components.lib.jexl.evaluator.Evaluator
+import mozilla.components.lib.jexl.evaluator.EvaluatorException
+import mozilla.components.lib.jexl.evaluator.JexlContext
+import mozilla.components.lib.jexl.evaluator.Transform
+import mozilla.components.lib.jexl.grammar.Grammar
+import mozilla.components.lib.jexl.lexer.Lexer
+import mozilla.components.lib.jexl.lexer.LexerException
+import mozilla.components.lib.jexl.parser.Parser
+import mozilla.components.lib.jexl.parser.ParserException
+import mozilla.components.lib.jexl.value.JexlUndefined
+import mozilla.components.lib.jexl.value.JexlValue
+
+class Jexl(
+ private val grammar: Grammar = Grammar(),
+) {
+ private val lexer: Lexer = Lexer(grammar)
+ private val transforms: MutableMap<String, Transform> = mutableMapOf()
+
+ /**
+ * Adds or replaces a transform function in this Jexl instance.
+ *
+ * @param name The name of the transform function, as it will be used within Jexl expressions.
+ * @param transform The function to be executed when this transform is invoked. It will be
+ * provided with two arguments:
+ * - value: The value to be transformed
+ * - arguments: The list of arguments for this transform.
+ */
+ fun addTransform(name: String, transform: Transform) {
+ transforms[name] = transform
+ }
+
+ /**
+ * Evaluates a Jexl string within an optional context.
+ *
+ * @param expression The Jexl expression to be evaluated.
+ * @param context A mapping of variables to values, which will be made accessible to the Jexl
+ * expression when evaluating it.
+ * @return The result of the evaluation.
+ * @throws JexlException if lexing, parsing or evaluating the expression failed.
+ */
+ @Throws(JexlException::class)
+ @Suppress("ThrowsCount")
+ fun evaluate(expression: String, context: JexlContext = JexlContext()): JexlValue {
+ val parser = Parser(grammar)
+ val evaluator = Evaluator(context, grammar, transforms)
+
+ try {
+ val tokens = lexer.tokenize(expression)
+ val astTree = parser.parse(tokens)
+ ?: return JexlUndefined()
+
+ return evaluator.evaluate(astTree)
+ } catch (e: LexerException) {
+ throw JexlException(e)
+ } catch (e: ParserException) {
+ throw JexlException(e)
+ } catch (e: EvaluatorException) {
+ throw JexlException(e)
+ }
+ }
+
+ /**
+ * Evaluates a Jexl string with an optional context to a Boolean result. Optionally a default
+ * value can be provided that will be returned in the expression does not return a boolean
+ * result.
+ */
+ fun evaluateBooleanExpression(
+ expression: String,
+ context: JexlContext = JexlContext(),
+ defaultValue: Boolean? = null,
+ ): Boolean {
+ val result = try {
+ evaluate(expression, context)
+ } catch (e: EvaluatorException) {
+ throw JexlException(e)
+ }
+
+ return try {
+ result.toBoolean()
+ } catch (e: EvaluatorException) {
+ if (defaultValue != null) {
+ return defaultValue
+ } else {
+ throw JexlException(e)
+ }
+ }
+ }
+}
+
+/**
+ * Generic exception thrown when evaluating an expression failed.
+ */
+class JexlException(
+ cause: Exception? = null,
+ message: String? = null,
+) : Exception(message, cause)
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/ast/nodes.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/ast/nodes.kt
new file mode 100644
index 0000000000..5a7858db1b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/ast/nodes.kt
@@ -0,0 +1,230 @@
+/* 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.jexl.ast
+
+/**
+ * A node of the abstract syntax tree.
+ */
+sealed class AstNode {
+
+ var parent: AstNode? = null
+
+ open fun toString(level: Int, isTopLevel: Boolean = true): String = toString()
+}
+
+internal interface OperatorNode {
+ val operator: String?
+}
+
+internal interface BranchNode {
+ var right: AstNode?
+}
+
+// node types
+
+internal data class Literal(
+ val value: Any?,
+) : AstNode() {
+
+ override fun toString(level: Int, isTopLevel: Boolean) =
+ buildNodeDescription("< $value >", "LITERAL", level, isTopLevel)
+
+ override fun toString() = toString(level = 0)
+}
+
+internal data class BinaryExpression(
+ override val operator: String?,
+ var left: AstNode?,
+ override var right: AstNode? = null,
+) : AstNode(), OperatorNode, BranchNode {
+
+ override fun toString(level: Int, isTopLevel: Boolean) =
+ buildNodeDescription("[ $operator ]", "BINARY_EXPRESSION", level, isTopLevel) {
+ appendChildNode(left, "left", level + 1)
+ appendChildNode(right, "right", level + 1)
+ }
+
+ override fun toString() = toString(level = 0)
+}
+
+internal data class UnaryExpression(
+ override val operator: String?,
+ override var right: AstNode? = null,
+) : AstNode(), OperatorNode, BranchNode {
+
+ override fun toString(level: Int, isTopLevel: Boolean) =
+ buildNodeDescription("[ $operator ]", "UNARY_EXPRESSION", level, isTopLevel) {
+ appendChildNode(right, "right", level + 1)
+ }
+
+ override fun toString() = toString(level = 0)
+}
+
+internal data class Identifier(
+ var value: Any?,
+ var from: AstNode? = null,
+ var relative: Boolean = false,
+) : AstNode() {
+
+ override fun toString(level: Int, isTopLevel: Boolean) =
+ buildNodeDescription(
+ "< $value >",
+ "IDENTIFIER",
+ level,
+ isTopLevel,
+ withinHeader = { append(" [ relative = $relative ]") },
+ ) {
+ appendChildNode(from, "from", level + 1)
+ }
+
+ override fun toString() = toString(level = 0)
+}
+
+internal data class ObjectLiteral(
+ val properties: Map<String, AstNode>,
+) : AstNode() {
+
+ constructor(vararg properties: Pair<String, AstNode>) : this(properties.toMap())
+
+ override fun toString(level: Int, isTopLevel: Boolean) =
+ buildNodeDescription("<Object>", "OBJECT_LITERAL", level, isTopLevel) {
+ appendNodeMapValues(this@ObjectLiteral, level + 1)
+ }
+
+ override fun toString() = toString(level = 0)
+}
+
+internal data class ConditionalExpression(
+ var test: AstNode?,
+ var consequent: AstNode? = null,
+ var alternate: AstNode? = null,
+) : AstNode() {
+
+ override fun toString(level: Int, isTopLevel: Boolean) =
+ buildNodeDescription("< ? >", "CONDITIONAL_EXPRESSION", level, isTopLevel) {
+ appendChildNode(test, "test", level + 1)
+ appendChildNode(consequent, "consequent", level + 1)
+ appendChildNode(alternate, "alternate", level + 1)
+ }
+
+ override fun toString() = toString(level = 0)
+}
+
+internal data class ArrayLiteral(
+ val values: MutableList<Any?>,
+) : AstNode() {
+
+ constructor(vararg values: Any?) : this(values.toMutableList())
+
+ override fun toString(level: Int, isTopLevel: Boolean) =
+ buildNodeDescription(
+ "[Array]",
+ "ARRAY_LITERAL",
+ level,
+ isTopLevel,
+ withinHeader = { append(" [ size = ${values.size} ]") },
+ ) {
+ appendNodeListValues(this@ArrayLiteral, level + 1)
+ }
+
+ override fun toString() = toString(level = 0)
+}
+
+internal data class Transformation(
+ var name: String?,
+ val arguments: MutableList<AstNode> = mutableListOf(),
+ var subject: AstNode? = null,
+) : AstNode() {
+
+ override fun toString(level: Int, isTopLevel: Boolean) =
+ buildNodeDescription("( $name )", "TRANSFORMATION", level, isTopLevel) {
+ appendChildNode(subject, "subject", level + 1)
+
+ for (argument in arguments) {
+ appendChildNode(argument, "arg", level + 1)
+ }
+ }
+
+ override fun toString() = toString(level = 0)
+}
+
+internal data class FilterExpression(
+ var expression: AstNode?,
+ var subject: AstNode?,
+ var relative: Boolean,
+) : AstNode() {
+
+ override fun toString(level: Int, isTopLevel: Boolean) =
+ buildNodeDescription(
+ "[ . ]",
+ "FILTER_EXPRESSION",
+ level,
+ isTopLevel,
+ withinHeader = { append(" [ relative = $relative ]") },
+ ) {
+ appendChildNode(expression, "expression", level + 1)
+ appendChildNode(subject, "subject", level + 1)
+ }
+
+ override fun toString() = toString(level = 0)
+}
+
+// string representation helpers
+
+private fun buildNodeDescription(
+ value: String,
+ name: String,
+ level: Int,
+ isTopLevel: Boolean = true,
+ withinHeader: StringBuilder.() -> Unit = {},
+ block: StringBuilder.() -> Unit = {},
+) = buildString {
+ if (isTopLevel) {
+ appendLevelPad(level)
+ }
+
+ append(value)
+ append(" ( $name )")
+
+ withinHeader()
+ appendLine()
+
+ block()
+}
+
+private fun StringBuilder.appendNodeMapValues(node: ObjectLiteral, level: Int) {
+ node.properties.forEach { (key, value) ->
+ val objectValue = value.toString(level, isTopLevel = false)
+
+ appendLevelPad(level)
+ append("$key : $objectValue")
+ }
+}
+
+private fun StringBuilder.appendNodeListValues(node: ArrayLiteral, level: Int) {
+ val array = node.values
+
+ array.withIndex().forEach { (i, child) ->
+ appendLevelPad(level)
+
+ val value = if (child is AstNode) {
+ child.toString(level, isTopLevel = false)
+ } else {
+ child.toString()
+ }
+
+ append("$i : $value")
+ }
+}
+
+private fun StringBuilder.appendChildNode(node: AstNode?, name: String, level: Int) {
+ node ?: return
+
+ appendLevelPad(level)
+ append("$name = ")
+ append(node.toString(level, isTopLevel = false))
+}
+
+private fun StringBuilder.appendLevelPad(level: Int) = append("".padStart(level * 2, ' '))
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/Evaluator.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/Evaluator.kt
new file mode 100644
index 0000000000..654abf465f
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/Evaluator.kt
@@ -0,0 +1,86 @@
+/* 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.jexl.evaluator
+
+import mozilla.components.lib.jexl.ast.AstNode
+import mozilla.components.lib.jexl.grammar.Grammar
+import mozilla.components.lib.jexl.value.JexlArray
+import mozilla.components.lib.jexl.value.JexlBoolean
+import mozilla.components.lib.jexl.value.JexlDouble
+import mozilla.components.lib.jexl.value.JexlInteger
+import mozilla.components.lib.jexl.value.JexlObject
+import mozilla.components.lib.jexl.value.JexlString
+import mozilla.components.lib.jexl.value.JexlUndefined
+import mozilla.components.lib.jexl.value.JexlValue
+
+typealias Transform = (JexlValue, List<JexlValue>) -> JexlValue
+
+internal class EvaluatorException(message: String) : Exception(message)
+
+/**
+ * The evaluator takes a JEXL abstract syntax tree as generated by the <code>Parser</code> and calculates its value
+ * within a given context.
+ */
+internal class Evaluator(
+ internal val context: JexlContext = JexlContext(),
+ internal val grammar: Grammar = Grammar(),
+ internal val transforms: Map<String, Transform> = emptyMap(),
+ internal val relativeContext: JexlObject = JexlObject(),
+) {
+
+ @Throws(EvaluatorException::class)
+ fun evaluate(node: AstNode): JexlValue =
+ EvaluatorHandlers.evaluateWith(this, node)
+
+ internal fun evaluateArray(nodes: List<AstNode>): List<JexlValue> {
+ return nodes.map { evaluate(it) }
+ }
+
+ internal fun evaluateObject(properties: Map<String, AstNode>): Map<String, JexlValue> {
+ return properties.mapValues { entry ->
+ evaluate(entry.value)
+ }
+ }
+
+ fun filterRelative(subject: JexlValue, expression: AstNode): JexlValue {
+ val filterSubject = subject as? JexlArray ?: JexlArray(
+ subject,
+ )
+
+ val values = filterSubject.value.filter { element ->
+ val subContext = element as? JexlObject ?: JexlObject()
+ val evaluator = Evaluator(context, grammar, transforms, subContext)
+ val value = evaluator.evaluate(expression)
+
+ value.value as Boolean
+ }
+
+ return JexlArray(values)
+ }
+
+ fun filterStatic(subject: JexlValue, expression: AstNode): JexlValue {
+ val result = evaluate(expression)
+
+ return when {
+ result is JexlBoolean -> if (result.value) { subject } else {
+ JexlUndefined()
+ }
+
+ subject is JexlUndefined -> subject
+
+ subject is JexlObject && result is JexlString -> subject.value[result.value]
+ ?: JexlUndefined()
+
+ subject is JexlArray && result is JexlInteger -> subject.value.getOrNull(result.value)
+ ?: JexlUndefined()
+
+ // We just convert a double to int here .. hoping for the best!
+ subject is JexlArray && result is JexlDouble -> subject.value.getOrNull(result.value.toInt())
+ ?: JexlUndefined()
+
+ else -> throw EvaluatorException("Cannot filter $subject by $result")
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/EvaluatorHandlers.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/EvaluatorHandlers.kt
new file mode 100644
index 0000000000..a53e2a7aa5
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/EvaluatorHandlers.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.jexl.evaluator
+
+import mozilla.components.lib.jexl.JexlException
+import mozilla.components.lib.jexl.ast.ArrayLiteral
+import mozilla.components.lib.jexl.ast.AstNode
+import mozilla.components.lib.jexl.ast.BinaryExpression
+import mozilla.components.lib.jexl.ast.ConditionalExpression
+import mozilla.components.lib.jexl.ast.FilterExpression
+import mozilla.components.lib.jexl.ast.Identifier
+import mozilla.components.lib.jexl.ast.Literal
+import mozilla.components.lib.jexl.ast.ObjectLiteral
+import mozilla.components.lib.jexl.ast.Transformation
+import mozilla.components.lib.jexl.ast.UnaryExpression
+import mozilla.components.lib.jexl.value.JexlArray
+import mozilla.components.lib.jexl.value.JexlBoolean
+import mozilla.components.lib.jexl.value.JexlDouble
+import mozilla.components.lib.jexl.value.JexlInteger
+import mozilla.components.lib.jexl.value.JexlObject
+import mozilla.components.lib.jexl.value.JexlString
+import mozilla.components.lib.jexl.value.JexlUndefined
+import mozilla.components.lib.jexl.value.JexlValue
+
+/**
+ * A mapping of AST node type to a function that can evaluate this type of node.
+ *
+ * This mapping could be moved inside [Evaluator].
+ */
+internal object EvaluatorHandlers {
+
+ internal fun evaluateWith(evaluator: Evaluator, node: AstNode): JexlValue = when (node) {
+ is Literal -> evaluateLiteral(node)
+ is BinaryExpression -> evaluateBinaryExpression(evaluator, node)
+ is Identifier -> evaluateIdentifier(evaluator, node)
+ is ObjectLiteral -> evaluateObjectLiteral(evaluator, node)
+ is ArrayLiteral -> evaluateArrayLiteral(evaluator, node)
+ is ConditionalExpression -> evaluateConditionalExpression(evaluator, node)
+ is Transformation -> evaluateTransformation(evaluator, node)
+ is FilterExpression -> evaluateFilterExpression(evaluator, node)
+ is UnaryExpression -> throw JexlException(
+ message = "Unary expression evaluation can't be validated",
+ )
+ }
+
+ private fun evaluateLiteral(node: Literal): JexlValue = when (val value = node.value) {
+ is String -> JexlString(value)
+ is Double -> JexlDouble(value)
+ is Int -> JexlInteger(value)
+ is Boolean -> JexlBoolean(value)
+ else -> throw EvaluatorException("Unknown value type: ${value!!::class}")
+ }
+
+ private fun evaluateBinaryExpression(evaluator: Evaluator, node: BinaryExpression): JexlValue {
+ val left = evaluator.evaluate(node.left!!)
+ val right = evaluator.evaluate(node.right!!)
+ val operator = evaluator.grammar.elements[node.operator!!]
+
+ return operator!!.evaluate?.invoke(left, right)
+ ?: throw EvaluatorException("Can't evaluate _operator: ${node.operator}")
+ }
+
+ private fun evaluateIdentifier(evaluator: Evaluator, node: Identifier): JexlValue =
+ if (node.from != null) {
+ evaluateIdentifierWithScope(evaluator, node)
+ } else {
+ evaluateIdentifierWithoutScope(evaluator, node)
+ }
+
+ private fun evaluateIdentifierWithScope(evaluator: Evaluator, node: Identifier): JexlValue =
+ when (val subContext = evaluator.evaluate(node.from!!)) {
+ is JexlArray -> {
+ val obj = subContext.value[0]
+
+ when (obj) {
+ is JexlUndefined -> obj
+
+ is JexlObject -> obj.value[node.value.toString()]
+ ?: throw EvaluatorException("${node.value} is undefined")
+
+ else -> throw EvaluatorException("$obj is not an object")
+ }
+ }
+
+ is JexlObject -> subContext.value[node.value.toString()]
+ ?: JexlUndefined()
+
+ else -> JexlUndefined()
+ }
+
+ private fun evaluateIdentifierWithoutScope(evaluator: Evaluator, node: Identifier): JexlValue =
+ if (node.relative) {
+ evaluator.relativeContext.value[(node.value.toString())]
+ ?: JexlUndefined()
+ } else {
+ evaluator.context.get(node.value.toString())
+ }
+
+ private fun evaluateObjectLiteral(evaluator: Evaluator, node: ObjectLiteral): JexlValue {
+ val properties = evaluator.evaluateObject(node.properties)
+ return JexlObject(properties)
+ }
+
+ private fun evaluateArrayLiteral(evaluator: Evaluator, node: ArrayLiteral): JexlValue {
+ @Suppress("UNCHECKED_CAST")
+ val values = evaluator.evaluateArray(node.values as List<AstNode>)
+ return JexlArray(values)
+ }
+
+ private fun evaluateConditionalExpression(evaluator: Evaluator, node: ConditionalExpression): JexlValue {
+ val result = evaluator.evaluate(node.test!!)
+
+ return if (result.toBoolean()) {
+ if (node.consequent != null) {
+ evaluator.evaluate(node.consequent!!)
+ } else {
+ result
+ }
+ } else {
+ evaluator.evaluate(node.alternate!!)
+ }
+ }
+
+ private fun evaluateTransformation(evaluator: Evaluator, node: Transformation): JexlValue {
+ val transform = evaluator.transforms[node.name]
+ ?: throw EvaluatorException("Unknown transform ${node.name}")
+
+ if (node.subject == null) {
+ throw EvaluatorException("Missing subject for transform")
+ }
+
+ val subject = evaluator.evaluate(node.subject!!)
+ val arguments = evaluator.evaluateArray(node.arguments)
+
+ return transform.invoke(subject, arguments)
+ }
+
+ private fun evaluateFilterExpression(evaluator: Evaluator, node: FilterExpression): JexlValue {
+ if (node.subject == null) {
+ throw EvaluatorException("Missing subject for filter expression")
+ }
+
+ val subject = evaluator.evaluate(node.subject!!)
+
+ if (node.expression == null) {
+ throw EvaluatorException("Missing expression for filter expression")
+ }
+
+ return if (node.relative) {
+ val result = evaluator.filterRelative(subject, node.expression!!)
+ result
+ } else {
+ evaluator.filterStatic(subject, node.expression!!)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/JexlContext.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/JexlContext.kt
new file mode 100644
index 0000000000..98397aafe9
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/evaluator/JexlContext.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.jexl.evaluator
+
+import mozilla.components.lib.jexl.value.JexlValue
+
+/**
+ * Variables defined in the [JexlContext] are available to expressions.
+ *
+ * Example context:
+ * <code>
+ * val context = JexlContext(
+ * "employees" to JexlArray(
+ * JexlObject(
+ * "first" to "Sterling".toJexl(),
+ * "last" to "Archer".toJexl(),
+ * "age" to 36.toJexl()),
+ * JexlObject(
+ * "first" to "Malory".toJexl(),
+ * "last" to "Archer".toJexl(),
+ * "age" to 75.toJexl()),
+ * JexlObject(
+ * "first" to "Malory".toJexl(),
+ * "last" to "Archer".toJexl(),
+ * "age" to 33.toJexl())
+ * )
+ * )
+ * </code>
+ *
+ * This context can be accessed in an JEXL expression like this:
+ *
+ * <code>
+ * employees[.age >= 30 && .age < 90][.age < 35]
+ * </code>
+ */
+class JexlContext(
+ vararg pairs: Pair<String, JexlValue>,
+) {
+ private val properties: MutableMap<String, JexlValue> = pairs.toMap().toMutableMap()
+
+ fun set(key: String, value: JexlValue) {
+ properties[key] = value
+ }
+
+ fun get(key: String): JexlValue {
+ return properties[key]
+ ?: throw EvaluatorException("$key is undefined")
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/ext/JexlExtensions.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/ext/JexlExtensions.kt
new file mode 100644
index 0000000000..b2faedff49
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/ext/JexlExtensions.kt
@@ -0,0 +1,32 @@
+/* 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.jexl.ext
+
+import mozilla.components.lib.jexl.value.JexlArray
+import mozilla.components.lib.jexl.value.JexlBoolean
+import mozilla.components.lib.jexl.value.JexlDouble
+import mozilla.components.lib.jexl.value.JexlInteger
+import mozilla.components.lib.jexl.value.JexlString
+
+// Kotlin extensions that make it easier to work with Jexl types and values
+
+inline fun <reified T> List<T>.toJexlArray(): JexlArray {
+ val values = when (T::class) {
+ String::class -> map { JexlString(it as String) }
+ Int::class -> map { JexlInteger(it as Int) }
+ Double::class -> map { JexlDouble(it as Double) }
+ Float::class -> map { JexlDouble((it as Float).toDouble()) }
+ Boolean::class -> map { JexlBoolean(it as Boolean) }
+ else -> throw UnsupportedOperationException("Can't convert type " + T::class + " to Jexl")
+ }
+
+ return JexlArray(values)
+}
+
+fun String.toJexl(): JexlString = JexlString(this)
+fun Int.toJexl(): JexlInteger = JexlInteger(this)
+fun Double.toJexl(): JexlDouble = JexlDouble(this)
+fun Float.toJexl(): JexlDouble = JexlDouble(this.toDouble())
+fun Boolean.toJexl(): JexlBoolean = JexlBoolean(this)
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/grammar/Grammar.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/grammar/Grammar.kt
new file mode 100644
index 0000000000..eb61970c96
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/grammar/Grammar.kt
@@ -0,0 +1,141 @@
+/* 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.jexl.grammar
+
+import mozilla.components.lib.jexl.evaluator.EvaluatorException
+import mozilla.components.lib.jexl.lexer.Token
+import mozilla.components.lib.jexl.value.JexlArray
+import mozilla.components.lib.jexl.value.JexlBoolean
+import mozilla.components.lib.jexl.value.JexlDouble
+import mozilla.components.lib.jexl.value.JexlInteger
+import mozilla.components.lib.jexl.value.JexlString
+import mozilla.components.lib.jexl.value.JexlValue
+import kotlin.math.floor
+
+/**
+ * Grammar of the JEXL language.
+ *
+ * Note that changes here may require a change in the Lexer or Parser.
+ */
+@Suppress("MagicNumber") // Operator precedence uses numbers and I do not see the need for constants..
+class Grammar {
+ val elements: Map<String, GrammarElement> = mapOf(
+ "." to GrammarElement(Token.Type.DOT),
+ "[" to GrammarElement(Token.Type.OPEN_BRACKET),
+ "]" to GrammarElement(Token.Type.CLOSE_BRACKET),
+ "|" to GrammarElement(Token.Type.PIPE),
+ "{" to GrammarElement(Token.Type.OPEN_CURL),
+ "}" to GrammarElement(Token.Type.CLOSE_CURL),
+ ":" to GrammarElement(Token.Type.COLON),
+ "," to GrammarElement(Token.Type.COMMA),
+ "(" to GrammarElement(Token.Type.OPEN_PAREN),
+ ")" to GrammarElement(Token.Type.CLOSE_PAREN),
+ "?" to GrammarElement(Token.Type.QUESTION),
+ "+" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 30,
+ ) { left, right -> left + right },
+ "-" to GrammarElement(Token.Type.BINARY_OP, 30),
+ "*" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 40,
+ ) { left, right -> left * right },
+ "/" to GrammarElement(Token.Type.BINARY_OP, 40) { left, right ->
+ left / right
+ },
+ "//" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 40,
+ ) { left, right ->
+ when (val result = left.div(right)) {
+ is JexlInteger -> result
+ is JexlDouble -> JexlInteger(
+ floor(
+ result.value,
+ ).toInt(),
+ )
+ else -> throw EvaluatorException("Cannot floor type: " + result::class)
+ }
+ },
+ "%" to GrammarElement(Token.Type.BINARY_OP, 50),
+ "^" to GrammarElement(Token.Type.BINARY_OP, 50),
+ "==" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 20,
+ ) { left, right ->
+ JexlBoolean(left == right)
+ },
+ "!=" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 20,
+ ) { left, right ->
+ JexlBoolean(left != right)
+ },
+ ">" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 20,
+ ) { left, right ->
+ JexlBoolean(left > right)
+ },
+ ">=" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 20,
+ ) { left, right ->
+ JexlBoolean(left >= right)
+ },
+ "<" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 20,
+ ) { left, right ->
+ JexlBoolean(left < right)
+ },
+ "<=" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 20,
+ ) { left, right ->
+ JexlBoolean(left <= right)
+ },
+ "&&" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 10,
+ ) { left, right ->
+ JexlBoolean(left.toBoolean() && right.toBoolean())
+ },
+ "||" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 10,
+ ) { left, right ->
+ JexlBoolean(left.toBoolean() || right.toBoolean())
+ },
+ "in" to GrammarElement(
+ Token.Type.BINARY_OP,
+ 20,
+ ) { left, right ->
+ when {
+ left is JexlString -> JexlBoolean(
+ right.toString().contains(left.value),
+ )
+ right is JexlArray -> JexlBoolean(
+ right.value.contains(left),
+ )
+ else -> throw EvaluatorException(
+ "Operator 'in' not applicable to " + left::class + " and " + right::class,
+ )
+ }
+ },
+ "!" to GrammarElement(
+ Token.Type.UNARY_OP,
+ Int.MAX_VALUE,
+ ) { _, right ->
+ JexlBoolean(!right.toBoolean())
+ },
+ )
+}
+
+data class GrammarElement(
+ val type: Token.Type,
+ val precedence: Int = 0,
+ val evaluate: ((JexlValue, JexlValue) -> JexlValue)? = null,
+)
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/Lexer.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/Lexer.kt
new file mode 100644
index 0000000000..f2386c4255
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/Lexer.kt
@@ -0,0 +1,223 @@
+/* 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.jexl.lexer
+
+import mozilla.components.lib.jexl.grammar.Grammar
+import mozilla.components.lib.jexl.grammar.GrammarElement
+
+internal class LexerException(message: String) : Exception(message)
+
+/**
+ * JEXL lexer for the lexical parsing of a JEXL string.
+ *
+ * Its responsibility is to identify the "parts of speech" of a Jexl expression, and tokenize and label each, but
+ * to do only the most minimal syntax checking; the only errors the Lexer should be concerned with are if it's unable
+ * to identify the utility of any of its tokens. Errors stemming from these tokens not being in a sensible
+ * configuration should be left for the Parser to handle.
+ */
+@Suppress("LargeClass")
+internal class Lexer(private val grammar: Grammar) {
+ private val negateAfter = listOf(
+ Token.Type.BINARY_OP,
+ Token.Type.UNARY_OP,
+ Token.Type.OPEN_PAREN,
+ Token.Type.OPEN_BRACKET,
+ Token.Type.QUESTION,
+ Token.Type.COLON,
+ )
+
+ /**
+ * Splits the JEXL expression string into a list of tokens.
+ */
+ @Suppress("ComplexMethod", "LongMethod")
+ @Throws(LexerException::class)
+ fun tokenize(raw: String): List<Token> {
+ val input = LexerInput(raw)
+ val tokens = mutableListOf<Token>()
+
+ var negate = false
+
+ while (!input.end()) {
+ if (negate && !input.character().isDigit() && !input.character().isWhitespace()) {
+ throw LexerException("Negating non digit: " + input.character())
+ }
+
+ when {
+ input.character() == '\'' -> tokens.add(readString(input, input.character()))
+
+ input.character() == '"' -> tokens.add(readString(input, input.character()))
+
+ input.character().isWhitespace() -> consumeWhiteSpaces(input)
+
+ input.peekEquals("true") -> tokens.add(
+ Token(
+ Token.Type.LITERAL,
+ "true",
+ true,
+ ),
+ )
+
+ input.peekEquals("false") -> tokens.add(
+ Token(
+ Token.Type.LITERAL,
+ "false",
+ false,
+ ),
+ )
+
+ input.character() == '#' -> discardComment(input)
+
+ input.character() == '-' -> {
+ val token = minusOrNegate(tokens)
+ if (token != null) {
+ tokens.add(token)
+ } else {
+ negate = true
+ }
+ input.proceed()
+ }
+
+ isElement(input, grammar.elements) -> tokens.add(lastFoundElementToken!!)
+
+ input.character().isLetter() || input.character() == '_' || input.character() == '$' ->
+ tokens.add(readIdentifier(input))
+
+ input.character().isDigit() -> {
+ tokens.add(readDigit(input, negate))
+ negate = false
+ }
+
+ else -> throw LexerException("Do not know how to proceed: " + input.character())
+ }
+ }
+
+ return tokens
+ }
+
+ private var lastFoundElementToken: Token? = null
+
+ @Suppress("ReturnCount")
+ private fun isElement(input: LexerInput, elements: Map<String, GrammarElement>): Boolean {
+ val max = elements.keys.map { it.length }.maxOrNull() ?: return false
+
+ for (steps in max downTo 1) {
+ val candidate = input.peekRange(steps)
+ if (elements.containsKey(candidate)) {
+ if (candidate.last().isLetter() && input.peek(steps).isLetter()) {
+ return false
+ }
+
+ val element = elements[candidate]!!
+ lastFoundElementToken = Token(element.type, candidate, candidate)
+ input.proceed(candidate.length)
+
+ return true
+ }
+ }
+
+ return false
+ }
+
+ private fun minusOrNegate(tokens: List<Token>): Token? {
+ if (tokens.isEmpty()) {
+ return null
+ }
+
+ if (tokens.last().type in negateAfter) {
+ return null
+ }
+
+ return Token(Token.Type.BINARY_OP, "-", "-")
+ }
+
+ private fun discardComment(input: LexerInput) {
+ while (!input.end() && input.character() != '\n') {
+ input.proceed()
+ }
+ }
+
+ private fun consumeWhiteSpaces(input: LexerInput) {
+ while (!input.end() && input.character().isWhitespace()) {
+ input.proceed()
+ }
+ }
+
+ private fun readString(input: LexerInput, quote: Char): Token {
+ input.mark()
+ input.proceed()
+
+ while (!input.end()) {
+ // Very simple escaping implementation.
+ if (input.character() == quote && input.previous() != '\\') {
+ break
+ }
+
+ input.proceed()
+ }
+
+ input.proceed()
+
+ val raw = input.emit()
+
+ if (raw.last() != quote) {
+ throw LexerException("String literal not closed")
+ }
+
+ val value = raw.substring(1, raw.length - 1)
+ .replace("\\" + quote, quote.toString())
+ .replace("\\\\", "\\")
+
+ return Token(Token.Type.LITERAL, raw, value)
+ }
+
+ private fun readIdentifier(input: LexerInput): Token {
+ input.mark()
+
+ while (!input.end()) {
+ if (!input.character().isLetterOrDigit() && input.character() != '_' && input.character() != '$') {
+ break
+ }
+
+ input.proceed()
+ }
+
+ val raw = input.emit()
+
+ return Token(Token.Type.IDENTIFIER, raw, raw)
+ }
+
+ @Suppress("ComplexMethod")
+ private fun readDigit(input: LexerInput, negate: Boolean): Token {
+ input.mark()
+
+ var readDot = false
+
+ while (!input.end()) {
+ if (!input.character().isDigit() && input.character() != '.') {
+ break
+ } else if (input.character() == '.' && readDot) {
+ break
+ } else if (input.character() == '.') {
+ readDot = true
+ }
+
+ input.proceed()
+ }
+
+ val raw = if (negate) {
+ "-${input.emit()}"
+ } else {
+ input.emit()
+ }
+
+ val value: Any = if (raw.contains(".")) {
+ raw.toDouble()
+ } else {
+ raw.toInt()
+ }
+
+ return Token(Token.Type.LITERAL, raw, value)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/LexerInput.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/LexerInput.kt
new file mode 100644
index 0000000000..f7d946e81a
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/LexerInput.kt
@@ -0,0 +1,85 @@
+/* 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.jexl.lexer
+
+/**
+ * Helper class for reading a string character by character with the ability to "peek" at upcoming characters.
+ */
+internal class LexerInput(private val value: String) {
+ private var position: Int = 0
+ private var mark: Int = 0
+
+ /**
+ * Marks the current position in the input.
+ */
+ fun mark() {
+ mark = position
+ }
+
+ /**
+ * Emits the string from the marked position to the current position.
+ */
+ fun emit(): String {
+ return value.substring(mark, position)
+ }
+
+ /**
+ * Move the current position [steps] steps ahread.
+ */
+ fun proceed(steps: Int = 1) {
+ position += steps
+ }
+
+ /**
+ * Returns true if the string starting as the current position equals [candidate].
+ */
+ fun peekEquals(candidate: String): Boolean {
+ if (position + candidate.length > value.length) {
+ return false
+ }
+
+ for (i in 0 until candidate.length) {
+ if (candidate[i] != value[position + i]) {
+ return false
+ }
+ }
+
+ position += candidate.length
+
+ return true
+ }
+
+ /**
+ * Returns the string from the current position to [steps] ahead without moving the current position.
+ */
+ fun peekRange(steps: Int): String {
+ if (position + steps > value.length) {
+ return ""
+ }
+
+ return value.substring(position, position + steps)
+ }
+
+ /**
+ * Returns the character at the current position
+ */
+ fun character(): Char = value[position]
+
+ /**
+ * Returns true if every character from the input has been read.
+ */
+ fun end() = position == value.length
+
+ /**
+ * Returns the character [steps] steps ahead.
+ */
+ fun peek(steps: Int): Char =
+ if (position + steps == value.length) ' ' else value[position + steps]
+
+ /**
+ * Returns the previous character.
+ */
+ fun previous(): Char = if (position == 0) ' ' else value[position - 1]
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/Token.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/Token.kt
new file mode 100644
index 0000000000..2c250a1ef3
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/lexer/Token.kt
@@ -0,0 +1,32 @@
+/* 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.jexl.lexer
+
+/**
+ * A token emitted by the [Lexer].
+ */
+data class Token(
+ val type: Type,
+ val raw: String,
+ val value: Any,
+) {
+ enum class Type {
+ LITERAL,
+ IDENTIFIER,
+ DOT,
+ OPEN_BRACKET,
+ CLOSE_BRACKET,
+ PIPE,
+ OPEN_CURL,
+ CLOSE_CURL,
+ COLON,
+ COMMA,
+ OPEN_PAREN,
+ CLOSE_PAREN,
+ QUESTION,
+ BINARY_OP,
+ UNARY_OP,
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/parser/Parser.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/parser/Parser.kt
new file mode 100644
index 0000000000..0dbc47d002
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/parser/Parser.kt
@@ -0,0 +1,252 @@
+/* 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.jexl.parser
+
+import mozilla.components.lib.jexl.ast.AstNode
+import mozilla.components.lib.jexl.ast.BinaryExpression
+import mozilla.components.lib.jexl.ast.BranchNode
+import mozilla.components.lib.jexl.ast.Identifier
+import mozilla.components.lib.jexl.ast.Literal
+import mozilla.components.lib.jexl.ast.OperatorNode
+import mozilla.components.lib.jexl.ast.UnaryExpression
+import mozilla.components.lib.jexl.grammar.Grammar
+import mozilla.components.lib.jexl.lexer.Token
+
+/**
+ * JEXL parser.
+ *
+ * Takes a list of tokens from the lexer and transforms them into an abstract syntax tree (AST).
+ */
+internal class Parser(
+ internal val grammar: Grammar,
+ private val stopMap: Map<Token.Type, State> = mapOf(),
+) {
+ private var state: State = State.EXPECT_OPERAND
+ internal var tree: AstNode? = null
+ internal var cursor: AstNode? = null
+
+ internal var subParser: Parser? = null
+ private var parentStop: Boolean = false
+ internal var currentObjectKey: String? = null
+
+ internal var nextIdentEncapsulate: Boolean = false
+ internal var nextIdentRelative: Boolean = false
+ internal var relative: Boolean = false
+
+ @Synchronized
+ @Throws(ParserException::class)
+ fun parse(tokens: List<Token>): AstNode? {
+ parseTokens(tokens)
+
+ return complete()
+ }
+
+ private fun complete(): AstNode? {
+ if (cursor != null && !stateMachine[state]!!.completable) {
+ throw ParserException("Unexpected end of expression")
+ }
+
+ if (subParser != null) {
+ endSubExpression()
+ }
+
+ state = State.COMPLETE
+
+ return if (cursor != null) {
+ tree
+ } else {
+ null
+ }
+ }
+
+ private fun parseTokens(tokens: List<Token>) {
+ tokens.forEach { parseToken(it) }
+ }
+
+ @Suppress("ComplexMethod", "LongMethod", "ThrowsCount")
+ private fun parseToken(token: Token): State? {
+ if (state == State.COMPLETE) {
+ throw ParserException("Token after parsing completed")
+ }
+
+ val stateMap = stateMachine[state]
+ ?: throw ParserException("Can't continue from state: $state")
+
+ if (stateMap.subHandler != null) {
+ // Use a sub handler for this state
+ if (subParser == null) {
+ startSubExpression()
+ }
+ val stopState = subParser!!.parseToken(token)
+ if (stopState != null) {
+ endSubExpression()
+ if (parentStop) {
+ return stopState
+ }
+ state = stopState
+ }
+ } else if (stateMap.map.containsKey(token.type)) {
+ val nextState = stateMap.map.getValue(token.type)
+
+ if (nextState.handler != null) {
+ // Use handler for this transition
+ nextState.handler.invoke(this, token)
+ } else {
+ // Use generic handler for this token type (if it exists)
+ val handler = handlers[token.type]
+ handler?.invoke(this, token)
+ }
+
+ nextState.state?.let { state = it }
+ } else if (stopMap.containsKey(token.type)) {
+ return stopMap.getValue(token.type)
+ } else {
+ throw ParserException("Token ${token.raw} (${token.type}) unexpected in state $state")
+ }
+
+ return null
+ }
+
+ internal fun placeAtCursor(node: AstNode) {
+ if (cursor == null) {
+ tree = node
+ } else {
+ cursor?.let { cursor ->
+ if (cursor is BranchNode) {
+ cursor.right = node
+ }
+ }
+ node.parent = cursor
+ }
+
+ cursor = node
+ }
+
+ internal fun placeBeforeCursor(node: AstNode) {
+ cursor = cursor!!.parent
+ placeAtCursor(node)
+ }
+
+ private fun startSubExpression() {
+ var endStates = stateMachine[state]!!.endStates
+ if (endStates.isEmpty()) {
+ parentStop = true
+ endStates = stopMap
+ }
+ this.subParser = Parser(grammar, endStates)
+ }
+
+ private fun endSubExpression() {
+ val stateMap = stateMachine[state]!!
+ val subHandler = stateMap.subHandler!!
+ val subParser = this.subParser!!
+ val node = subParser.complete()
+
+ subHandler.invoke(this, node)
+ this.subParser = null
+ }
+}
+
+class ParserException(message: String) : Exception(message)
+
+internal class StateMap(
+ val map: Map<Token.Type, NextState> = mapOf(),
+ val completable: Boolean = false,
+ val subHandler: ((Parser, AstNode?) -> Unit)? = null,
+ val endStates: Map<Token.Type, State> = mapOf(),
+)
+
+internal enum class State {
+ EXPECT_OPERAND,
+ EXPECT_BIN_OP,
+ IDENTIFIER,
+ SUB_EXPRESSION,
+ EXPECT_OBJECT_KEY,
+ TRAVERSE,
+ ARRAY_VALUE,
+ EXPECT_TRANSFORM,
+ TERNARY_MID,
+ TERNARY_END,
+ COMPLETE,
+ POST_TRANSFORM,
+ EXPECT_KEY_VALUE_SEPARATOR,
+ OBJECT_VALUE,
+ ARGUMENT_VALUE,
+ FILTER,
+ POST_TRANSFORM_ARGUMENTS,
+}
+
+internal class NextState(
+ val state: State? = null,
+ val handler: ((Parser, Token) -> Unit)? = null,
+)
+
+internal val handlers: Map<Token.Type, (Parser, Token) -> Unit> = mapOf(
+ Token.Type.LITERAL to { parser, token ->
+ parser.placeAtCursor(
+ Literal(token.value),
+ )
+ },
+
+ Token.Type.BINARY_OP to { parser, token ->
+ val precedence = parser.grammar.elements[token.value]?.precedence ?: 0
+ var parent = parser.cursor!!.parent
+
+ var operator = (parent as? OperatorNode)?.operator
+
+ while (operator != null &&
+ parser.grammar.elements[operator]!!.precedence > precedence
+ ) {
+ parser.cursor = parent
+ parent = parent?.parent
+ operator = (parent as? OperatorNode)?.operator
+ }
+
+ val node = BinaryExpression(
+ left = parser.cursor,
+ operator = token.value.toString(),
+ )
+
+ parser.cursor!!.parent = node
+ parser.cursor = parent
+ parser.placeAtCursor(node)
+ },
+
+ Token.Type.IDENTIFIER to { parser, token ->
+ val node = Identifier(token.value)
+
+ if (parser.nextIdentEncapsulate) {
+ node.from = parser.cursor
+ parser.placeBeforeCursor(node)
+ parser.nextIdentEncapsulate = false
+ } else {
+ if (parser.nextIdentRelative) {
+ node.relative = true
+ }
+ parser.placeAtCursor(node)
+ }
+ },
+
+ Token.Type.UNARY_OP to { parser, token ->
+ val node = UnaryExpression(
+ operator = token.value.toString(),
+ )
+ parser.placeAtCursor(node)
+ },
+
+ Token.Type.DOT to { parser, _ ->
+ val cursor = parser.cursor
+
+ parser.nextIdentEncapsulate = cursor != null &&
+ (cursor !is BinaryExpression || cursor.right != null) &&
+ cursor !is UnaryExpression
+
+ parser.nextIdentRelative = cursor == null || !parser.nextIdentEncapsulate
+
+ if (parser.nextIdentRelative) {
+ parser.relative = true
+ }
+ },
+)
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/parser/StateMachine.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/parser/StateMachine.kt
new file mode 100644
index 0000000000..348fb537ca
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/parser/StateMachine.kt
@@ -0,0 +1,230 @@
+/* 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.jexl.parser
+
+import mozilla.components.lib.jexl.ast.ArrayLiteral
+import mozilla.components.lib.jexl.ast.AstNode
+import mozilla.components.lib.jexl.ast.ConditionalExpression
+import mozilla.components.lib.jexl.ast.FilterExpression
+import mozilla.components.lib.jexl.ast.ObjectLiteral
+import mozilla.components.lib.jexl.ast.Transformation
+import mozilla.components.lib.jexl.lexer.Token
+
+internal val stateMachine: Map<State, StateMap> = mapOf(
+ State.EXPECT_OPERAND to StateMap(
+ mapOf(
+ Token.Type.LITERAL to NextState(State.EXPECT_BIN_OP),
+ Token.Type.IDENTIFIER to NextState(
+ State.IDENTIFIER,
+ ),
+ Token.Type.UNARY_OP to NextState(),
+ Token.Type.OPEN_PAREN to NextState(
+ State.SUB_EXPRESSION,
+ ),
+ Token.Type.OPEN_CURL to NextState(
+ State.EXPECT_OBJECT_KEY,
+ ::objectStart,
+ ),
+ Token.Type.DOT to NextState(State.TRAVERSE),
+ Token.Type.OPEN_BRACKET to NextState(
+ State.ARRAY_VALUE,
+ ::arrayStart,
+ ),
+ ),
+ ),
+ State.EXPECT_BIN_OP to StateMap(
+ mapOf(
+ Token.Type.BINARY_OP to NextState(
+ State.EXPECT_OPERAND,
+ ),
+ Token.Type.PIPE to NextState(State.EXPECT_TRANSFORM),
+ Token.Type.DOT to NextState(State.TRAVERSE),
+ Token.Type.QUESTION to NextState(
+ State.TERNARY_MID,
+ ::ternaryStart,
+ ),
+ ),
+ completable = true,
+ ),
+ State.EXPECT_TRANSFORM to StateMap(
+ mapOf(
+ Token.Type.IDENTIFIER to NextState(
+ State.POST_TRANSFORM,
+ ::transform,
+ ),
+ ),
+ ),
+ State.EXPECT_OBJECT_KEY to StateMap(
+ mapOf(
+ Token.Type.IDENTIFIER to NextState(
+ State.EXPECT_KEY_VALUE_SEPARATOR,
+ ::objectKey,
+ ),
+ Token.Type.CLOSE_CURL to NextState(
+ State.EXPECT_BIN_OP,
+ ),
+ ),
+ ),
+ State.EXPECT_KEY_VALUE_SEPARATOR to StateMap(
+ mapOf(
+ Token.Type.COLON to NextState(State.OBJECT_VALUE),
+ ),
+ ),
+ State.POST_TRANSFORM to StateMap(
+ mapOf(
+ Token.Type.OPEN_PAREN to NextState(
+ State.ARGUMENT_VALUE,
+ ),
+ Token.Type.BINARY_OP to NextState(
+ State.EXPECT_OPERAND,
+ ),
+ Token.Type.DOT to NextState(State.TRAVERSE),
+ Token.Type.OPEN_BRACKET to NextState(
+ State.FILTER,
+ ),
+ Token.Type.PIPE to NextState(State.EXPECT_TRANSFORM),
+ ),
+ completable = true,
+ ),
+ State.POST_TRANSFORM_ARGUMENTS to StateMap(
+ mapOf(
+ Token.Type.BINARY_OP to NextState(
+ State.EXPECT_OPERAND,
+ ),
+ Token.Type.DOT to NextState(State.TRAVERSE),
+ Token.Type.OPEN_BRACKET to NextState(
+ State.FILTER,
+ ),
+ Token.Type.PIPE to NextState(State.EXPECT_TRANSFORM),
+ ),
+ completable = true,
+ ),
+ State.IDENTIFIER to StateMap(
+ mapOf(
+ Token.Type.BINARY_OP to NextState(
+ State.EXPECT_OPERAND,
+ ),
+ Token.Type.DOT to NextState(State.TRAVERSE),
+ Token.Type.OPEN_BRACKET to NextState(
+ State.FILTER,
+ ),
+ Token.Type.PIPE to NextState(State.EXPECT_TRANSFORM),
+ Token.Type.QUESTION to NextState(
+ State.TERNARY_MID,
+ ::ternaryStart,
+ ),
+ ),
+ completable = true,
+ ),
+ State.TRAVERSE to StateMap(
+ mapOf(
+ Token.Type.IDENTIFIER to NextState(
+ State.IDENTIFIER,
+ ),
+ ),
+ ),
+ State.FILTER to StateMap(
+ subHandler = { parser, node ->
+ val expressionNode = FilterExpression(
+ expression = node,
+ relative = parser.subParser!!.relative,
+ subject = parser.cursor,
+ )
+ parser.placeBeforeCursor(expressionNode)
+ },
+ endStates = mapOf(
+ Token.Type.CLOSE_BRACKET to State.IDENTIFIER,
+ ),
+ ),
+ State.SUB_EXPRESSION to StateMap(
+ subHandler = { parser, node ->
+ parser.placeAtCursor(node!!)
+ },
+ endStates = mapOf(
+ Token.Type.CLOSE_PAREN to State.EXPECT_BIN_OP,
+ ),
+ ),
+ State.ARGUMENT_VALUE to StateMap(
+ subHandler = { parser, node ->
+ val cursor = parser.cursor!! as Transformation
+ cursor.arguments.add(node!!)
+ },
+ endStates = mapOf(
+ Token.Type.COMMA to State.ARGUMENT_VALUE,
+ Token.Type.CLOSE_PAREN to State.EXPECT_BIN_OP,
+ ),
+ ),
+ State.OBJECT_VALUE to StateMap(
+ subHandler = { parser, node ->
+ val cursor = parser.cursor as ObjectLiteral
+ val properties = cursor.properties as MutableMap<String, AstNode>
+
+ properties[parser.currentObjectKey!!] = node!!
+ },
+ endStates = mapOf(
+ Token.Type.COMMA to State.EXPECT_OBJECT_KEY,
+ Token.Type.CLOSE_CURL to State.EXPECT_BIN_OP,
+ ),
+ ),
+ State.ARRAY_VALUE to StateMap(
+ subHandler = { parser, node ->
+ if (node != null) {
+ (parser.cursor!! as ArrayLiteral).values.add(node)
+ }
+ },
+ endStates = mapOf(
+ Token.Type.COMMA to State.ARRAY_VALUE,
+ Token.Type.CLOSE_BRACKET to State.EXPECT_BIN_OP,
+ ),
+ ),
+ State.TERNARY_MID to StateMap(
+ subHandler = { parser, node ->
+ val cursor = parser.cursor!! as ConditionalExpression
+ cursor.consequent = node
+ },
+ endStates = mapOf(
+ Token.Type.COLON to State.TERNARY_END,
+ ),
+ ),
+ State.TERNARY_END to StateMap(
+ subHandler = { parser, node ->
+ val cursor = parser.cursor!! as ConditionalExpression
+ cursor.alternate = node
+ },
+ completable = true,
+ ),
+)
+
+private fun objectStart(parser: Parser, @Suppress("UNUSED_PARAMETER") token: Token) {
+ val node = ObjectLiteral(
+ properties = mutableMapOf(),
+ )
+ parser.placeAtCursor(node)
+}
+
+private fun objectKey(parser: Parser, token: Token) {
+ parser.currentObjectKey = token.value.toString()
+}
+
+private fun arrayStart(parser: Parser, @Suppress("UNUSED_PARAMETER") token: Token) {
+ val node = ArrayLiteral()
+ parser.placeAtCursor(node)
+}
+
+private fun transform(parser: Parser, token: Token) {
+ val node = Transformation(
+ name = token.value.toString(),
+ subject = parser.cursor,
+ )
+ parser.placeBeforeCursor(node)
+}
+
+private fun ternaryStart(parser: Parser, @Suppress("UNUSED_PARAMETER") token: Token) {
+ val node = ConditionalExpression(
+ test = parser.tree,
+ )
+ parser.tree = node
+ parser.cursor = node
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/value/JexlValue.kt b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/value/JexlValue.kt
new file mode 100644
index 0000000000..0ea67a46c8
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/main/java/mozilla/components/lib/jexl/value/JexlValue.kt
@@ -0,0 +1,307 @@
+/* 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.jexl.value
+
+import mozilla.components.lib.jexl.evaluator.EvaluatorException
+
+/**
+ * A JEXL value type.
+ */
+sealed class JexlValue {
+ abstract val value: Any
+
+ abstract operator fun plus(other: JexlValue): JexlValue
+ abstract operator fun times(other: JexlValue): JexlValue
+ abstract operator fun div(other: JexlValue): JexlValue
+ abstract operator fun compareTo(other: JexlValue): Int
+
+ abstract fun toBoolean(): Boolean
+}
+
+/**
+ * JEXL Integer type.
+ */
+class JexlInteger(override val value: Int) : JexlValue() {
+ override fun div(other: JexlValue): JexlValue {
+ return when (other) {
+ is JexlInteger -> JexlInteger(value / other.value)
+ is JexlDouble -> JexlDouble(value / other.value)
+ else -> throw EvaluatorException("Can't divide by ${other::class}")
+ }
+ }
+
+ override fun times(other: JexlValue): JexlValue {
+ return when (other) {
+ is JexlInteger -> JexlInteger(value * other.value)
+ is JexlDouble -> JexlDouble(value * other.value)
+ is JexlBoolean -> JexlInteger(value * other.toInt())
+ else -> throw EvaluatorException("Can't multiply with ${other::class}")
+ }
+ }
+
+ override fun plus(other: JexlValue): JexlValue {
+ return when (other) {
+ is JexlInteger -> JexlInteger(value + other.value)
+ is JexlDouble -> JexlDouble(value + other.value)
+ is JexlString -> JexlString(value.toString() + other.value)
+ is JexlBoolean -> JexlInteger(value + (other.toInt()))
+ else -> throw EvaluatorException("Can't add ${other::class}")
+ }
+ }
+
+ override fun compareTo(other: JexlValue): Int {
+ return when (other) {
+ is JexlInteger -> value.compareTo(other.value)
+ is JexlDouble -> value.compareTo(other.value)
+ else -> throw EvaluatorException("Can't compare ${other::class}")
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return when (other) {
+ is JexlInteger -> value == other.value
+ is JexlDouble -> value.toDouble() == other.value
+ else -> false
+ }
+ }
+
+ override fun toBoolean(): Boolean = value != 0
+
+ override fun toString() = value.toString()
+
+ override fun hashCode() = value.hashCode()
+}
+
+/**
+ * JEXL Double type.
+ */
+class JexlDouble(override val value: Double) : JexlValue() {
+ override fun div(other: JexlValue): JexlValue {
+ return when (other) {
+ is JexlInteger -> JexlDouble(value / other.value)
+ is JexlDouble -> JexlDouble(value / other.value)
+ else -> throw EvaluatorException("Can't divide by ${other::class}")
+ }
+ }
+
+ override fun times(other: JexlValue): JexlValue {
+ return when (other) {
+ is JexlInteger -> JexlDouble(value * other.value)
+ is JexlDouble -> JexlDouble(value * other.value)
+ is JexlBoolean -> JexlDouble(value * other.toInt())
+ else -> throw EvaluatorException("Can't multiply with ${other::class}")
+ }
+ }
+
+ override fun plus(other: JexlValue): JexlValue {
+ return when (other) {
+ is JexlInteger -> JexlDouble(value + other.value)
+ is JexlDouble -> JexlDouble(value + other.value)
+ is JexlString -> JexlString(value.toString() + other.value)
+ is JexlBoolean -> JexlDouble(value + (other.toInt()))
+ else -> throw EvaluatorException("Can't add ${other::class}")
+ }
+ }
+
+ override fun compareTo(other: JexlValue): Int {
+ return when (other) {
+ is JexlInteger -> value.compareTo(other.value)
+ is JexlDouble -> value.compareTo(other.value)
+ else -> throw EvaluatorException("Can't compare ${other::class}")
+ }
+ }
+
+ override fun toBoolean(): Boolean = value != 0.0
+
+ override fun equals(other: Any?): Boolean {
+ return when (other) {
+ is JexlDouble -> value == other.value
+ is JexlInteger -> {
+ value == other.value.toDouble()
+ }
+ else -> false
+ }
+ }
+
+ override fun toString() = value.toString()
+
+ override fun hashCode() = value.hashCode()
+}
+
+/**
+ * JEXL Boolean type.
+ */
+class JexlBoolean(override val value: Boolean) : JexlValue() {
+ override fun div(other: JexlValue): JexlValue {
+ throw EvaluatorException("Can't divide boolean")
+ }
+
+ override fun times(other: JexlValue): JexlValue {
+ return when (other) {
+ is JexlInteger -> JexlInteger(toInt() * other.value)
+ is JexlDouble -> JexlDouble(toInt() * other.value)
+ is JexlBoolean -> JexlInteger(toInt() * other.toInt())
+ else -> throw EvaluatorException("Can't multiply with ${other::class}")
+ }
+ }
+
+ override fun plus(other: JexlValue): JexlValue {
+ return when (other) {
+ is JexlInteger -> JexlInteger((toInt()) + other.value)
+ is JexlDouble -> JexlDouble((toInt()) + other.value)
+ is JexlString -> JexlString(value.toString() + other.value)
+ is JexlBoolean -> JexlInteger((toInt()) + (other.toInt()))
+ else -> throw EvaluatorException("Can't add ${other::class}")
+ }
+ }
+
+ override fun compareTo(other: JexlValue): Int {
+ throw EvaluatorException("Can't compare ${other::class}")
+ }
+
+ fun toInt(): Int = if (value) 1 else 0
+
+ override fun equals(other: Any?) = other is JexlBoolean && value == other.value
+
+ override fun toBoolean() = value
+
+ override fun toString() = value.toString()
+
+ override fun hashCode() = value.hashCode()
+}
+
+/**
+ * JEXL String type.
+ */
+class JexlString(override val value: String) : JexlValue() {
+ override fun div(other: JexlValue): JexlValue {
+ throw EvaluatorException("Can't divide string")
+ }
+
+ override fun times(other: JexlValue): JexlValue {
+ throw EvaluatorException("Can't multiply strings")
+ }
+
+ override fun plus(other: JexlValue): JexlValue {
+ return when (other) {
+ is JexlInteger -> JexlString(value + other.value)
+ is JexlDouble -> JexlString(value + other.value)
+ is JexlString -> JexlString(value + other.value)
+ is JexlBoolean -> JexlString(value + (if (other.value) 1 else 0))
+ else -> throw EvaluatorException("Can't add ${other::class}")
+ }
+ }
+
+ override fun compareTo(other: JexlValue): Int {
+ throw EvaluatorException("Can't compare ${other::class}")
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is JexlString && value == other.value
+ }
+
+ override fun toBoolean(): Boolean {
+ return value.isNotEmpty()
+ }
+
+ override fun toString(): String {
+ return value
+ }
+
+ override fun hashCode(): Int {
+ return value.hashCode()
+ }
+}
+
+/**
+ * JEXL Array type.
+ */
+class JexlArray(
+ override val value: List<JexlValue>,
+) : JexlValue() {
+ constructor(vararg elements: JexlValue) : this(elements.toList())
+
+ override fun div(other: JexlValue): JexlValue = throw EvaluatorException("Can't divide array")
+
+ override fun times(other: JexlValue): JexlValue = throw EvaluatorException("Can't multiply arrays")
+
+ override fun plus(other: JexlValue): JexlValue = throw EvaluatorException("Can't add arrays")
+
+ override fun compareTo(other: JexlValue): Int = throw EvaluatorException("Can't compare ${other::class}")
+
+ override fun equals(other: Any?) = other is JexlArray && value == other.value
+
+ override fun toBoolean(): Boolean = throw EvaluatorException("Can't convert array to boolean")
+
+ override fun toString(): String = value.toString()
+
+ override fun hashCode(): Int = value.hashCode()
+}
+
+/**
+ * JEXL Object type.
+ */
+class JexlObject(
+ override val value: Map<String, JexlValue>,
+) : JexlValue() {
+ constructor(vararg pairs: Pair<String, JexlValue>) : this(pairs.toMap())
+
+ override fun div(other: JexlValue): JexlValue {
+ throw EvaluatorException("Can't divide object")
+ }
+
+ override fun times(other: JexlValue): JexlValue {
+ throw EvaluatorException("Can't multiply objects")
+ }
+
+ override fun plus(other: JexlValue): JexlValue {
+ throw EvaluatorException("Can't add objects")
+ }
+
+ override fun compareTo(other: JexlValue): Int {
+ throw EvaluatorException("Can't compare ${other::class}")
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is JexlObject && value == other.value
+ }
+
+ override fun toBoolean(): Boolean {
+ throw EvaluatorException("Can't convert object to boolean")
+ }
+
+ override fun toString(): String {
+ return value.toString()
+ }
+
+ override fun hashCode(): Int {
+ return value.hashCode()
+ }
+}
+
+/**
+ * JEXL undefined type.
+ */
+class JexlUndefined : JexlValue() {
+ override val value = Any()
+
+ override fun plus(other: JexlValue): JexlValue {
+ return this
+ }
+
+ override fun times(other: JexlValue): JexlValue = throw EvaluatorException("Can't multiply undefined values")
+
+ override fun div(other: JexlValue): JexlValue = throw EvaluatorException("Can't divide undefined values")
+
+ override fun compareTo(other: JexlValue) = if (other is JexlUndefined) 0 else 1
+
+ override fun toBoolean(): Boolean = throw EvaluatorException("Can't convert undefined to boolean")
+
+ override fun toString() = "<undefined>"
+
+ override fun equals(other: Any?) = other is JexlUndefined
+
+ override fun hashCode(): Int = 7
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/JexlTest.kt b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/JexlTest.kt
new file mode 100644
index 0000000000..b9f04a04a9
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/JexlTest.kt
@@ -0,0 +1,112 @@
+/* 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.jexl
+
+import mozilla.components.lib.jexl.evaluator.JexlContext
+import mozilla.components.lib.jexl.ext.toJexl
+import mozilla.components.lib.jexl.ext.toJexlArray
+import mozilla.components.lib.jexl.value.JexlArray
+import mozilla.components.lib.jexl.value.JexlObject
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class JexlTest {
+
+ @Test
+ fun `Should evaluate expressions`() {
+ val jexl = Jexl()
+
+ val result = jexl.evaluate("75 > 42")
+ assertEquals(true, result.value)
+ }
+
+ @Test
+ fun `Should evaluate boolean expressions`() {
+ val jexl = Jexl()
+
+ val result = jexl.evaluateBooleanExpression("42 + 23 > 50", defaultValue = false)
+
+ assertEquals(true, result)
+ }
+
+ @Test
+ fun `Should apply transform`() {
+ val jexl = Jexl()
+
+ jexl.addTransform("split") { value, arguments ->
+ value.toString().split(arguments.first().toString()).toJexlArray()
+ }
+
+ jexl.addTransform("lower") { value, _ ->
+ value.toString().lowercase().toJexl()
+ }
+
+ jexl.addTransform("last") { value, _ ->
+ (value as JexlArray).value.last()
+ }
+
+ assertEquals(
+ "poovey".toJexl(),
+ jexl.evaluate(""""Pam Poovey"|lower|split(' ')|last"""),
+ )
+
+ assertEquals(
+ JexlArray("password".toJexl(), "guest".toJexl()),
+ jexl.evaluate(""""password==guest"|split('=' + '=')"""),
+ )
+ }
+
+ @Test
+ fun `Should use context`() {
+ val jexl = Jexl()
+
+ val context = JexlContext(
+ "employees" to JexlArray(
+ JexlObject(
+ "first" to "Sterling".toJexl(),
+ "last" to "Archer".toJexl(),
+ "age" to 36.toJexl(),
+ ),
+ JexlObject(
+ "first" to "Malory".toJexl(),
+ "last" to "Archer".toJexl(),
+ "age" to 75.toJexl(),
+ ),
+ JexlObject(
+ "first" to "Malory".toJexl(),
+ "last" to "Archer".toJexl(),
+ "age" to 33.toJexl(),
+ ),
+ ),
+ )
+
+ assertEquals(
+ JexlArray(
+ JexlObject(
+ "first" to "Sterling".toJexl(),
+ "last" to "Archer".toJexl(),
+ "age" to 36.toJexl(),
+ ),
+ JexlObject(
+ "first" to "Malory".toJexl(),
+ "last" to "Archer".toJexl(),
+ "age" to 33.toJexl(),
+ ),
+ ),
+ jexl.evaluate("employees[.age >= 30 && .age < 40]", context),
+ )
+
+ assertEquals(
+ JexlArray(
+ JexlObject(
+ "first" to "Malory".toJexl(),
+ "last" to "Archer".toJexl(),
+ "age" to 33.toJexl(),
+ ),
+ ),
+ jexl.evaluate("employees[.age >= 30 && .age < 90][.age < 35]", context),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/LanguageTest.kt b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/LanguageTest.kt
new file mode 100644
index 0000000000..9fd693f816
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/LanguageTest.kt
@@ -0,0 +1,53 @@
+/* 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.jexl
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.fail
+import org.junit.Test
+import kotlin.reflect.KClass
+
+/**
+ * Additional test cases that test various JEXL expressions to get a high test coverage for the lexer, parser and
+ * evaluator.
+ */
+class LanguageTest {
+ @Test
+ fun `Multiple dots in numeric expression should throw exception`() {
+ "27.42.21".evaluationThrows()
+ }
+
+ @Test
+ fun `Negating a literal should throw exception`() {
+ "-employees".evaluationThrows()
+ }
+
+ @Test
+ fun `Using non grammar character should throw exception`() {
+ "§".evaluationThrows()
+ }
+
+ private fun String.evaluatesTo(expectedResult: Any) {
+ val jexl = Jexl()
+ val actualResult = jexl.evaluate(this)
+
+ assertEquals(expectedResult, actualResult)
+ }
+
+ private fun String.evaluationThrows() {
+ evaluationThrows(JexlException::class)
+ }
+
+ private inline fun <reified T : Throwable> String.evaluationThrows(clazz: KClass<T>?) {
+ try {
+ evaluatesTo(Any())
+ fail("Expected exception to be thrown: $clazz")
+ } catch (e: Throwable) {
+ if (e !is T) {
+ throw e
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/evaluator/EvaluatorTest.kt b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/evaluator/EvaluatorTest.kt
new file mode 100644
index 0000000000..490140bc2b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/evaluator/EvaluatorTest.kt
@@ -0,0 +1,375 @@
+/* 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.jexl.evaluator
+
+import mozilla.components.lib.jexl.ast.AstNode
+import mozilla.components.lib.jexl.grammar.Grammar
+import mozilla.components.lib.jexl.lexer.Lexer
+import mozilla.components.lib.jexl.parser.Parser
+import mozilla.components.lib.jexl.value.JexlArray
+import mozilla.components.lib.jexl.value.JexlInteger
+import mozilla.components.lib.jexl.value.JexlObject
+import mozilla.components.lib.jexl.value.JexlString
+import mozilla.components.lib.jexl.value.JexlUndefined
+import mozilla.components.lib.jexl.value.JexlValue
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+
+class EvaluatorTest {
+ private lateinit var grammar: Grammar
+
+ @Before
+ fun setUp() {
+ grammar = Grammar()
+ }
+
+ @Test
+ fun `Should evaluate a literal`() {
+ assertExpressionYieldsResult("42", 42)
+ assertExpressionYieldsResult("2.0", 2.0)
+
+ assertExpressionYieldsResult("true", true)
+ assertExpressionYieldsResult("false", false)
+
+ assertExpressionYieldsResult("\"hello world\"", "hello world")
+ assertExpressionYieldsResult("'hello world'", "hello world")
+ }
+
+ @Test
+ fun `Should evaluate an arithmetic expression`() {
+ assertExpressionYieldsResult(
+ "(2 + 3) * 4",
+ 20,
+ )
+ }
+
+ @Test
+ fun `Should evaluate a string concat`() {
+ assertExpressionYieldsResult(
+ """
+ "Hello" + (4+4) + "Wo\"rld"
+ """.trimIndent(),
+ "Hello8Wo\"rld",
+ )
+ }
+
+ @Test
+ fun `Should evaluate a true comparison expression`() {
+ assertExpressionYieldsResult(
+ "2 > 1",
+ true,
+ )
+ }
+
+ @Test
+ fun `Should evaluate a false comparison expression`() {
+ assertExpressionYieldsResult(
+ "2 <= 1",
+ false,
+ )
+ }
+
+ @Test
+ fun `Should evaluate a complex expression`() {
+ assertExpressionYieldsResult(
+ "\"foo\" && 6 >= 6 && 0 + 1 && true",
+ true,
+ )
+ }
+
+ @Test
+ fun `Should evaluate an identifier chain`() {
+ val context = JexlContext(
+ "foo" to JexlObject(
+ "baz" to JexlObject(
+ "bar" to JexlString("tek"),
+ ),
+ ),
+ )
+
+ assertExpressionYieldsResult(
+ "foo.baz.bar",
+ "tek",
+ context = context,
+ )
+ }
+
+ @Test
+ fun `Should apply transforms`() {
+ val context = JexlContext(
+ "foo" to JexlInteger(10),
+ )
+
+ assertExpressionYieldsResult(
+ "foo|half + 3",
+ 8,
+ context = context,
+ transforms = mapOf(
+ "half" to { value, _ ->
+ value.div(JexlInteger(2))
+ },
+ ),
+ )
+ }
+
+ @Test
+ fun `Should filter arrays`() {
+ val context = JexlContext(
+ "foo" to JexlObject(
+ "bar" to JexlArray(
+ JexlObject("tek" to JexlString("hello")),
+ JexlObject("tek" to JexlString("baz")),
+ JexlObject("tok" to JexlString("baz")),
+ ),
+ ),
+ )
+
+ assertExpressionYieldsResult(
+ "foo.bar[.tek == \"baz\"]",
+ listOf(JexlObject("tek" to JexlString("baz"))),
+ context = context,
+ )
+ }
+
+ @Test
+ fun `Should assume array index 0 when traversing`() {
+ val context = JexlContext(
+ "foo" to JexlObject(
+ "bar" to JexlArray(
+ JexlObject(
+ "tek" to JexlObject(
+ "hello" to JexlString(
+ "world",
+ ),
+ ),
+ ),
+ JexlObject(
+ "tek" to JexlObject(
+ "hello" to JexlString(
+ "universe",
+ ),
+ ),
+ ),
+ ),
+ ),
+ )
+
+ assertExpressionYieldsResult(
+ "foo.bar.tek.hello",
+ "world",
+ context = context,
+ )
+ }
+
+ @Test
+ fun `Should make array elements addressable by index`() {
+ val context = JexlContext(
+ "foo" to JexlObject(
+ "bar" to JexlArray(
+ JexlObject("tek" to JexlString("tok")),
+ JexlObject("tek" to JexlString("baz")),
+ JexlObject("tek" to JexlString("foz")),
+ ),
+ ),
+ )
+
+ assertExpressionYieldsResult(
+ "foo.bar[1].tek",
+ "baz",
+ context = context,
+ )
+ }
+
+ @Test
+ fun `Should allow filters to select object properties`() {
+ val context = JexlContext(
+ "foo" to JexlObject(
+ "baz" to JexlObject(
+ "bar" to JexlString("tek"),
+ ),
+ ),
+ )
+
+ assertExpressionYieldsResult(
+ "foo[\"ba\" + \"z\"].bar",
+ "tek",
+ context = context,
+ )
+ }
+
+ @Test
+ fun `Should allow simple filters on undefined objects`() {
+ val context = JexlContext(
+ "foo" to JexlObject(),
+ )
+
+ assertExpressionYieldsResult(
+ "foo.bar[\"baz\"].tok",
+ JexlUndefined(),
+ context = context,
+ unpack = false,
+ )
+ }
+
+ @Test
+ fun `Should allow complex filters on undefined objects`() {
+ val context = JexlContext(
+ "foo" to JexlObject(),
+ )
+
+ assertExpressionYieldsResult(
+ "foo.bar[.size > 1].baz",
+ JexlUndefined(),
+ context = context,
+ unpack = false,
+ )
+ }
+
+ @Test(expected = EvaluatorException::class)
+ fun `Should throw when transform does not exist`() {
+ assertExpressionYieldsResult(
+ "\"hello\"|world",
+ "-- should throw",
+ )
+ }
+
+ @Test
+ fun `Should apply the DivFloor operator`() {
+ assertExpressionYieldsResult(
+ "7 // 2",
+ 3,
+ )
+ }
+
+ @Test
+ fun `Should evaluate an object literal`() {
+ assertExpressionYieldsResult(
+ "{foo: {bar: \"tek\"}}",
+ JexlObject(
+ "foo" to JexlObject(
+ "bar" to JexlString(
+ "tek",
+ ),
+ ),
+ ),
+ unpack = false,
+ )
+ }
+
+ @Test
+ fun `Should evaluate an empty object literal`() {
+ assertExpressionYieldsResult(
+ "{}",
+ emptyMap<String, JexlValue>(),
+ )
+ }
+
+ @Test
+ fun `Should evaluate a transform with multiple args`() {
+ assertExpressionYieldsResult(
+ """"foo"|concat("baz", "bar", "tek")""",
+ "foo: bazbartek",
+ transforms = mapOf(
+ "concat" to { value, arguments ->
+ value + JexlString(": ") + JexlString(
+ arguments.joinToString(""),
+ )
+ },
+ ),
+ )
+ }
+
+ @Test
+ fun `Should evaluate dot notation for object literals`() {
+ assertExpressionYieldsResult(
+ "{foo: \"bar\"}.foo",
+ "bar",
+ )
+ }
+
+ @Test
+ @Ignore("JavaScript properties are not implemented yet")
+ fun `Should allow access to literal properties`() {
+ assertExpressionYieldsResult(
+ "\"foo\".length",
+ 3,
+ )
+ }
+
+ @Test
+ fun `Should evaluate array literals`() {
+ assertExpressionYieldsResult(
+ "[\"foo\", 1+2]",
+ listOf(
+ JexlString("foo"),
+ JexlInteger(3),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should apply the 'in' operator to strings`() {
+ assertExpressionYieldsResult(""""bar" in "foobartek"""", true)
+ assertExpressionYieldsResult(""""baz" in "foobartek"""", false)
+ }
+
+ @Test
+ fun `Should apply the 'in' operator to arrays`() {
+ assertExpressionYieldsResult(""""bar" in ["foo","bar","tek"]""", true)
+ assertExpressionYieldsResult(""""baz" in ["foo","bar","tek"]""", false)
+ }
+
+ @Test
+ fun `Should evaluate a conditional expression`() {
+ assertExpressionYieldsResult("\"foo\" ? 1 : 2", 1)
+ assertExpressionYieldsResult("\"\" ? 1 : 2", 2)
+ }
+
+ @Test
+ fun `Should allow missing consequent in ternary`() {
+ assertExpressionYieldsResult(""""foo" ?: "bar"""", "foo")
+ }
+
+ @Test
+ @Ignore("JavaScript properties are not implemented yet")
+ fun `Does not treat falsey properties as undefined`() {
+ assertExpressionYieldsResult("\"\".length", 0)
+ }
+
+ @Test
+ fun `Should handle an expression with arbitrary whitespace`() {
+ assertExpressionYieldsResult("(\t2\n+\n3) *\n4\n\r\n", 20)
+ }
+
+ private fun assertExpressionYieldsResult(
+ expression: String,
+ result: Any,
+ context: JexlContext = JexlContext(),
+ transforms: Map<String, Transform> = emptyMap(),
+ unpack: Boolean = true,
+ ) {
+ val tree = toTree(expression)
+
+ println(tree)
+
+ val evaluator = Evaluator(context, grammar, transforms)
+ val actual = evaluator.evaluate(tree)
+
+ assertEquals(result, if (unpack) actual.value else actual)
+ }
+
+ private fun toTree(
+ expression: String,
+ grammar: Grammar = Grammar(),
+ ): AstNode {
+ val lexer = Lexer(grammar)
+ val parser = Parser(grammar)
+
+ return parser.parse(lexer.tokenize(expression))
+ ?: throw AssertionError("Expression yielded null AST tree")
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/ext/JexlExtensionsTest.kt b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/ext/JexlExtensionsTest.kt
new file mode 100644
index 0000000000..0787d17ea1
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/ext/JexlExtensionsTest.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.jexl.ext
+
+import mozilla.components.lib.jexl.value.JexlArray
+import mozilla.components.lib.jexl.value.JexlBoolean
+import mozilla.components.lib.jexl.value.JexlDouble
+import mozilla.components.lib.jexl.value.JexlInteger
+import mozilla.components.lib.jexl.value.JexlString
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.lang.UnsupportedOperationException
+
+class JexlExtensionsTest {
+ @Test
+ fun `Simple types`() {
+ assertEquals("Hello", "Hello".toJexl().value)
+ assertEquals(23, 23.toJexl().value)
+ assertEquals(23.0, 23.0.toJexl().value, 0.00001)
+ assertEquals(1.0, 1.0f.toJexl().value, 0.00001)
+ assertEquals(0, 0.toJexl().value)
+ assertEquals(true, true.toJexl().value)
+ assertEquals(false, false.toJexl().value)
+ }
+
+ @Test
+ fun `Arrays`() {
+ assertEquals(
+ JexlArray(JexlInteger(1), JexlInteger(2), JexlInteger(3)),
+ listOf(1, 2, 3).toJexlArray(),
+ )
+
+ assertEquals(
+ JexlArray(),
+ emptyList<String>().toJexlArray(),
+ )
+
+ assertEquals(
+ JexlArray(JexlString("Hello"), JexlString("World")),
+ listOf("Hello", "World").toJexlArray(),
+ )
+
+ assertEquals(
+ JexlArray(JexlDouble(1.0), JexlDouble(23.0)),
+ listOf(1.0, 23.0).toJexlArray(),
+ )
+
+ assertEquals(
+ JexlArray(JexlDouble(52.0), JexlDouble(-2.0)),
+ listOf(52.0f, -2f).toJexlArray(),
+ )
+
+ assertEquals(
+ JexlArray(JexlBoolean(false), JexlBoolean(true)),
+ listOf(false, true).toJexlArray(),
+ )
+ }
+
+ @Test(expected = UnsupportedOperationException::class)
+ fun `Unsupported array type`() {
+ listOf(Pair(1, 2), Pair(3, 4)).toJexlArray()
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/lexer/LexerTest.kt b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/lexer/LexerTest.kt
new file mode 100644
index 0000000000..fc8b518f0b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/lexer/LexerTest.kt
@@ -0,0 +1,483 @@
+/* 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.jexl.lexer
+
+import mozilla.components.lib.jexl.grammar.Grammar
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+class LexerTest {
+ private lateinit var lexer: Lexer
+
+ @Before
+ fun setUp() {
+ lexer = Lexer(Grammar())
+ }
+
+ @Test
+ fun `should count a string as one element`() {
+ val expression = "\"foo\""
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(
+ Token.Type.LITERAL,
+ "\"foo\"",
+ "foo",
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support single-quote strings`() {
+ val expression = "'foo'"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "'foo'", "foo"),
+ ),
+ )
+ }
+
+ @Test
+ fun `should find multiple strings`() {
+ val expression = "\"foo\" 'bar' \"baz\""
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(
+ Token.Type.LITERAL,
+ "\"foo\"",
+ "foo",
+ ),
+ Token(
+ Token.Type.LITERAL,
+ "'bar'",
+ "bar",
+ ),
+ Token(
+ Token.Type.LITERAL,
+ "\"baz\"",
+ "baz",
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support escaping double-quotes`() {
+ val expression = "\"f\\\"oo\""
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(
+ Token.Type.LITERAL,
+ "\"f\\\"oo\"",
+ "f\"oo",
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support escaping single-quotes`() {
+ val expression = "'f\\'oo'"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(
+ Token.Type.LITERAL,
+ "'f\\'oo'",
+ "f'oo",
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `should count an identifier as one element`() {
+ val expression = "alpha12345"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(
+ Token.Type.IDENTIFIER,
+ "alpha12345",
+ "alpha12345",
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support boolean true`() {
+ val expression = "true"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "true", true),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support boolean false`() {
+ val expression = "false"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "false", false),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support comments`() {
+ val expression = "# This is a comment"
+
+ assertExpressionYieldsTokens(expression, emptyList())
+ }
+
+ @Test
+ fun `should support comments after expressions`() {
+ val expression = "true false #This is a comment"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "true", true),
+ Token(Token.Type.LITERAL, "false", false),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support operators`() {
+ val expression = "true + false - true + false++"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "true", true),
+ Token(Token.Type.BINARY_OP, "+", "+"),
+ Token(
+ Token.Type.LITERAL,
+ "false",
+ false,
+ ),
+ Token(Token.Type.BINARY_OP, "-", "-"),
+ Token(Token.Type.LITERAL, "true", true),
+ Token(Token.Type.BINARY_OP, "+", "+"),
+ Token(
+ Token.Type.LITERAL,
+ "false",
+ false,
+ ),
+ Token(Token.Type.BINARY_OP, "+", "+"),
+ Token(Token.Type.BINARY_OP, "+", "+"),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support operators with two characters`() {
+ val expression = "true == false + true != false >= true > in false"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "true", true),
+ Token(Token.Type.BINARY_OP, "==", "=="),
+ Token(
+ Token.Type.LITERAL,
+ "false",
+ false,
+ ),
+ Token(Token.Type.BINARY_OP, "+", "+"),
+ Token(Token.Type.LITERAL, "true", true),
+ Token(Token.Type.BINARY_OP, "!=", "!="),
+ Token(
+ Token.Type.LITERAL,
+ "false",
+ false,
+ ),
+ Token(Token.Type.BINARY_OP, ">=", ">="),
+ Token(Token.Type.LITERAL, "true", true),
+ Token(Token.Type.BINARY_OP, ">", ">"),
+ Token(Token.Type.BINARY_OP, "in", "in"),
+ Token(Token.Type.LITERAL, "false", false),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support numerics`() {
+ val expression = "1234 == 782"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "1234", 1234),
+ Token(Token.Type.BINARY_OP, "==", "=="),
+ Token(Token.Type.LITERAL, "782", 782),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support negative numerics`() {
+ val expression = "-7.6 + (-20 * -1)"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "-7.6", -7.6),
+ Token(Token.Type.BINARY_OP, "+", "+"),
+ Token(Token.Type.OPEN_PAREN, "(", "("),
+ Token(Token.Type.LITERAL, "-20", -20),
+ Token(Token.Type.BINARY_OP, "*", "*"),
+ Token(Token.Type.LITERAL, "-1", -1),
+ Token(Token.Type.CLOSE_PAREN, ")", ")"),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support floating point numerics`() {
+ val expression = "1.337 != 2.42"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(
+ Token.Type.LITERAL,
+ "1.337",
+ 1.337,
+ ),
+ Token(Token.Type.BINARY_OP, "!=", "!="),
+ Token(Token.Type.LITERAL, "2.42", 2.42),
+ ),
+ )
+ }
+
+ @Test
+ fun `should support identifiers, numerics and operators`() {
+ val expression = "person.age == 12 && (person.hasJob == true || person.onVacation != false)"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(
+ Token.Type.IDENTIFIER,
+ "person",
+ "person",
+ ),
+ Token(Token.Type.DOT, ".", "."),
+ Token(
+ Token.Type.IDENTIFIER,
+ "age",
+ "age",
+ ),
+ Token(Token.Type.BINARY_OP, "==", "=="),
+ Token(Token.Type.LITERAL, "12", 12),
+ Token(Token.Type.BINARY_OP, "&&", "&&"),
+ Token(Token.Type.OPEN_PAREN, "(", "("),
+ Token(
+ Token.Type.IDENTIFIER,
+ "person",
+ "person",
+ ),
+ Token(Token.Type.DOT, ".", "."),
+ Token(
+ Token.Type.IDENTIFIER,
+ "hasJob",
+ "hasJob",
+ ),
+ Token(Token.Type.BINARY_OP, "==", "=="),
+ Token(Token.Type.LITERAL, "true", true),
+ Token(Token.Type.BINARY_OP, "||", "||"),
+ Token(
+ Token.Type.IDENTIFIER,
+ "person",
+ "person",
+ ),
+ Token(Token.Type.DOT, ".", "."),
+ Token(
+ Token.Type.IDENTIFIER,
+ "onVacation",
+ "onVacation",
+ ),
+ Token(Token.Type.BINARY_OP, "!=", "!="),
+ Token(
+ Token.Type.LITERAL,
+ "false",
+ false,
+ ),
+ Token(Token.Type.CLOSE_PAREN, ")", ")"),
+ ),
+ )
+ }
+
+ @Test
+ fun `tokenize math expression`() {
+ val expression = "age * (3 - 1)"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(
+ Token.Type.IDENTIFIER,
+ "age",
+ "age",
+ ),
+ Token(Token.Type.BINARY_OP, "*", "*"),
+ Token(Token.Type.OPEN_PAREN, "(", "("),
+ Token(Token.Type.LITERAL, "3", 3),
+ Token(Token.Type.BINARY_OP, "-", "-"),
+ Token(Token.Type.LITERAL, "1", 1),
+ Token(Token.Type.CLOSE_PAREN, ")", ")"),
+ ),
+ )
+ }
+
+ @Test
+ fun `should not split grammar elements out of transforms`() {
+ val expression = "inString"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(
+ Token.Type.IDENTIFIER,
+ "inString",
+ "inString",
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `should handle a complex mix of comments in single, multiline and value contexts`() {
+ val expression = """
+ 6+x - -17.55*y #end comment
+ <= !foo.bar["baz\"foz"] # with space
+ && b=="not a #comment" # is a comment
+ # comment # 2nd comment
+ """.trimIndent()
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "6", 6),
+ Token(Token.Type.BINARY_OP, "+", "+"),
+ Token(Token.Type.IDENTIFIER, "x", "x"),
+ Token(Token.Type.BINARY_OP, "-", "-"),
+ Token(
+ Token.Type.LITERAL,
+ "-17.55",
+ -17.55,
+ ),
+ Token(Token.Type.BINARY_OP, "*", "*"),
+ Token(Token.Type.IDENTIFIER, "y", "y"),
+ Token(Token.Type.BINARY_OP, "<=", "<="),
+ Token(Token.Type.UNARY_OP, "!", "!"),
+ Token(
+ Token.Type.IDENTIFIER,
+ "foo",
+ "foo",
+ ),
+ Token(Token.Type.DOT, ".", "."),
+ Token(
+ Token.Type.IDENTIFIER,
+ "bar",
+ "bar",
+ ),
+ Token(Token.Type.OPEN_BRACKET, "[", "["),
+ Token(
+ Token.Type.LITERAL,
+ "\"baz\\\"foz\"",
+ "baz\"foz",
+ ),
+ Token(
+ Token.Type.CLOSE_BRACKET,
+ "]",
+ "]",
+ ),
+ Token(Token.Type.BINARY_OP, "&&", "&&"),
+ Token(Token.Type.IDENTIFIER, "b", "b"),
+ Token(Token.Type.BINARY_OP, "==", "=="),
+ Token(
+ Token.Type.LITERAL,
+ "\"not a #comment\"",
+ "not a #comment",
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `should tokenize a full expression`() {
+ val expression = """6+x - -17.55*y<= !foo.bar["baz\\"foz"]"""
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "6", 6),
+ Token(Token.Type.BINARY_OP, "+", "+"),
+ Token(Token.Type.IDENTIFIER, "x", "x"),
+ Token(Token.Type.BINARY_OP, "-", "-"),
+ Token(Token.Type.LITERAL, "-17.55", -17.55),
+ Token(Token.Type.BINARY_OP, "*", "*"),
+ Token(Token.Type.IDENTIFIER, "y", "y"),
+ Token(Token.Type.BINARY_OP, "<=", "<="),
+ Token(Token.Type.UNARY_OP, "!", "!"),
+ Token(Token.Type.IDENTIFIER, "foo", "foo"),
+ Token(Token.Type.DOT, ".", "."),
+ Token(Token.Type.IDENTIFIER, "bar", "bar"),
+ Token(Token.Type.OPEN_BRACKET, "[", "["),
+ Token(Token.Type.LITERAL, """"baz\\"foz"""", """baz\"foz"""),
+ Token(Token.Type.CLOSE_BRACKET, "]", "]"),
+ ),
+ )
+ }
+
+ @Test
+ fun `should consider minus to be negative appropriately`() {
+ val expression = "-1?-2:-3"
+
+ assertExpressionYieldsTokens(
+ expression,
+ listOf(
+ Token(Token.Type.LITERAL, "-1", -1),
+ Token(Token.Type.QUESTION, "?", "?"),
+ Token(Token.Type.LITERAL, "-2", -2),
+ Token(Token.Type.COLON, ":", ":"),
+ Token(Token.Type.LITERAL, "-3", -3),
+ ),
+ )
+ }
+
+ private fun assertExpressionYieldsTokens(expression: String, tokens: List<Token>) {
+ val actual = lexer.tokenize(expression)
+
+ println(actual)
+
+ assertEquals(tokens.size, actual.size)
+
+ for (i in 0 until tokens.size) {
+ assertEquals(tokens[i], actual[i])
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/parser/ParserTest.kt b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/parser/ParserTest.kt
new file mode 100644
index 0000000000..0dbb07bc27
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/parser/ParserTest.kt
@@ -0,0 +1,506 @@
+/* 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.jexl.parser
+
+import mozilla.components.lib.jexl.ast.ArrayLiteral
+import mozilla.components.lib.jexl.ast.AstNode
+import mozilla.components.lib.jexl.ast.BinaryExpression
+import mozilla.components.lib.jexl.ast.ConditionalExpression
+import mozilla.components.lib.jexl.ast.FilterExpression
+import mozilla.components.lib.jexl.ast.Identifier
+import mozilla.components.lib.jexl.ast.Literal
+import mozilla.components.lib.jexl.ast.ObjectLiteral
+import mozilla.components.lib.jexl.ast.Transformation
+import mozilla.components.lib.jexl.ast.UnaryExpression
+import mozilla.components.lib.jexl.grammar.Grammar
+import mozilla.components.lib.jexl.lexer.Lexer
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+
+class ParserTest {
+
+ @Test
+ fun `Should parse literal`() {
+ val expression = "42"
+
+ assertExpressionYieldsTree(
+ expression,
+ Literal(42),
+ )
+ }
+
+ @Test
+ fun `Should parse math expression`() {
+ val expression = "42 + 23"
+
+ assertExpressionYieldsTree(
+ expression,
+ BinaryExpression(
+ left = Literal(42),
+ right = Literal(23),
+ operator = "+",
+ ),
+ )
+ }
+
+ @Test(expected = ParserException::class)
+ fun `Should throw on incomplete expression`() {
+ val expression = "42 +"
+ parse(expression)
+ }
+
+ @Test
+ fun `Should parse expression with identifier`() {
+ val expression = "age > 21"
+
+ assertExpressionYieldsTree(
+ expression,
+ BinaryExpression(
+ operator = ">",
+ left = Identifier("age"),
+ right = Literal(21),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should parse expression with sub expression`() {
+ val expression = "(age + 5) > 42"
+
+ assertExpressionYieldsTree(
+ expression,
+ BinaryExpression(
+ operator = ">",
+ left = BinaryExpression(
+ operator = "+",
+ left = Identifier("age"),
+ right = Literal(5),
+ ),
+ right = Literal(42),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should parse expression following operator precedence`() {
+ assertExpressionYieldsTree(
+ "5 + 7 * 2",
+ BinaryExpression(
+ operator = "+",
+ left = Literal(5),
+ right = BinaryExpression(
+ operator = "*",
+ left = Literal(7),
+ right = Literal(2),
+ ),
+ ),
+ )
+
+ assertExpressionYieldsTree(
+ "5 * 7 + 2",
+ BinaryExpression(
+ operator = "+",
+ left = BinaryExpression(
+ operator = "*",
+ left = Literal(5),
+ right = Literal(7),
+ ),
+ right = Literal(2),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle encapsulation of subtree`() {
+ assertExpressionYieldsTree(
+ "2+3*4==5/6-7",
+ BinaryExpression(
+ operator = "==",
+ left = BinaryExpression(
+ operator = "+",
+ left = Literal(2),
+ right = BinaryExpression(
+ operator = "*",
+ left = Literal(3),
+ right = Literal(4),
+ ),
+ ),
+ right = BinaryExpression(
+ operator = "-",
+ left = BinaryExpression(
+ operator = "/",
+ left = Literal(5),
+ right = Literal(6),
+ ),
+ right = Literal(7),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle a unary operator`() {
+ assertExpressionYieldsTree(
+ "1*!!true-2",
+ BinaryExpression(
+ operator = "-",
+ left = BinaryExpression(
+ operator = "*",
+ left = Literal(1),
+ right = UnaryExpression(
+ operator = "!",
+ right = UnaryExpression(
+ operator = "!",
+ right = Literal(true),
+ ),
+ ),
+ ),
+ right = Literal(2),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle nested subexpressions`() {
+ assertExpressionYieldsTree(
+ "(4*(2+3))/5",
+ BinaryExpression(
+ operator = "/",
+ left = BinaryExpression(
+ operator = "*",
+ left = Literal(4),
+ right = BinaryExpression(
+ operator = "+",
+ left = Literal(2),
+ right = Literal(3),
+ ),
+ ),
+ right = Literal(5),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle whitespace in an expression`() {
+ assertExpressionYieldsTree(
+ "\t2\r\n+\n\r3\n\n",
+ BinaryExpression(
+ operator = "+",
+ left = Literal(2),
+ right = Literal(3),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle object literals`() {
+ assertExpressionYieldsTree(
+ "{foo: \"bar\", tek: 1+2}",
+ ObjectLiteral(
+ "foo" to Literal("bar"),
+ "tek" to BinaryExpression(
+ operator = "+",
+ left = Literal(1),
+ right = Literal(2),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle nested object literals`() {
+ assertExpressionYieldsTree(
+ """{
+ foo: {
+ bar: "tek",
+ baz: 42
+ }
+ }""",
+ ObjectLiteral(
+ "foo" to ObjectLiteral(
+ "bar" to Literal("tek"),
+ "baz" to Literal(42),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle empty object literals`() {
+ assertExpressionYieldsTree(
+ "{}",
+ ObjectLiteral(),
+ )
+ }
+
+ @Test
+ fun `Should handle array literals`() {
+ assertExpressionYieldsTree(
+ "[\"foo\", 1+2]",
+ ArrayLiteral(
+ Literal("foo"),
+ BinaryExpression(
+ operator = "+",
+ left = Literal(1),
+ right = Literal(2),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle nested array literals`() {
+ assertExpressionYieldsTree(
+ "[\"foo\", [\"bar\", \"tek\"]]",
+ ArrayLiteral(
+ Literal("foo"),
+ ArrayLiteral(
+ Literal("bar"),
+ Literal("tek"),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle empty array literals`() {
+ assertExpressionYieldsTree(
+ "[]",
+ ArrayLiteral(),
+ )
+ }
+
+ @Test
+ fun `Should chain traversed identifiers`() {
+ assertExpressionYieldsTree(
+ "foo.bar.baz + 1",
+ BinaryExpression(
+ operator = "+",
+ left = Identifier(
+ "baz",
+ from = Identifier(
+ "bar",
+ from = Identifier("foo"),
+ ),
+ ),
+ right = Literal(1),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should apply transforms and arguments`() {
+ assertExpressionYieldsTree(
+ "foo|tr1|tr2.baz|tr3({bar:\"tek\"})",
+ Transformation(
+ name = "tr3",
+ arguments = mutableListOf(
+ ObjectLiteral(
+ "bar" to Literal("tek"),
+ ),
+ ),
+ subject = Identifier(
+ value = "baz",
+ from = Transformation(
+ name = "tr2",
+ subject = Transformation(
+ name = "tr1",
+ subject = Identifier("foo"),
+ ),
+ ),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle multiple arguments in transforms`() {
+ assertExpressionYieldsTree(
+ "foo|bar(\"tek\", 5, true)",
+ Transformation(
+ name = "bar",
+ subject = Identifier("foo"),
+ arguments = mutableListOf(
+ Literal("tek"),
+ Literal(5),
+ Literal(true),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should apply filters to identifiers`() {
+ assertExpressionYieldsTree(
+ """foo[1][.bar[0] == "tek"].baz""",
+ Identifier(
+ "baz",
+ from = FilterExpression(
+ relative = true,
+ expression = BinaryExpression(
+ operator = "==",
+ left = FilterExpression(
+ relative = false,
+ expression = Literal(0),
+ subject = Identifier(
+ value = "bar",
+ relative = true,
+ ),
+ ),
+ right = Literal("tek"),
+ ),
+ subject = FilterExpression(
+ relative = false,
+ expression = Literal(1),
+ subject = Identifier("foo"),
+ ),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should allow dot notation for all operands`() {
+ assertExpressionYieldsTree(
+ "\"foo\".length + {foo: \"bar\"}.foo",
+ BinaryExpression(
+ operator = "+",
+ left = Identifier("length", from = Literal("foo")),
+ right = Identifier(
+ "foo",
+ from = ObjectLiteral(
+ "foo" to Literal("bar"),
+ ),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should allow dot notation on subexpressions`() {
+ assertExpressionYieldsTree(
+ "(\"foo\" + \"bar\").length",
+ Identifier(
+ "length",
+ from = BinaryExpression(
+ operator = "+",
+ left = Literal("foo"),
+ right = Literal("bar"),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should allow dot notation on arrays`() {
+ assertExpressionYieldsTree(
+ "[\"foo\", \"bar\"].length",
+ Identifier(
+ "length",
+ from = ArrayLiteral(
+ Literal("foo"),
+ Literal("bar"),
+ ),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle a ternary expression`() {
+ assertExpressionYieldsTree(
+ "foo ? 1 : 0",
+ ConditionalExpression(
+ test = Identifier("foo"),
+ consequent = Literal(1),
+ alternate = Literal(0),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle nested and grouped ternary expressions`() {
+ assertExpressionYieldsTree(
+ "foo ? (bar ? 1 : 2) : 3",
+ ConditionalExpression(
+ test = Identifier("foo"),
+ consequent = ConditionalExpression(
+ test = Identifier("bar"),
+ consequent = Literal(1),
+ alternate = Literal(2),
+ ),
+ alternate = Literal(3),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle nested, non-grouped ternary expressions`() {
+ assertExpressionYieldsTree(
+ "foo ? bar ? 1 : 2 : 3",
+ ConditionalExpression(
+ test = Identifier("foo"),
+ consequent = ConditionalExpression(
+ test = Identifier("bar"),
+ consequent = Literal(1),
+ alternate = Literal(2),
+ ),
+ alternate = Literal(3),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should handle ternary expression with objects`() {
+ assertExpressionYieldsTree(
+ "foo ? {bar: \"tek\"} : \"baz\"",
+ ConditionalExpression(
+ test = Identifier("foo"),
+ consequent = ObjectLiteral(
+ "bar" to Literal("tek"),
+ ),
+ alternate = Literal("baz"),
+ ),
+ )
+ }
+
+ @Test
+ fun `Should correctly balance a binary op between complex identifiers`() {
+ assertExpressionYieldsTree(
+ "a.b == c.d",
+ BinaryExpression(
+ operator = "==",
+ left = Identifier(
+ value = "b",
+ from = Identifier("a"),
+ ),
+ right = Identifier(
+ value = "d",
+ from = Identifier("c"),
+ ),
+ ),
+ )
+ }
+
+ private fun assertExpressionYieldsTree(expression: String, tree: AstNode?) {
+ val actual = parse(expression)
+
+ if (tree != null) {
+ assertNotNull(actual)
+ }
+
+ println(actual)
+
+ assertEquals(tree, actual)
+ }
+
+ private fun parse(expression: String): AstNode? {
+ val grammar = Grammar()
+ val lexer = Lexer(grammar)
+ val parser = Parser(grammar)
+
+ return parser.parse(lexer.tokenize(expression))
+ }
+}
diff --git a/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/value/JexlValueTest.kt b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/value/JexlValueTest.kt
new file mode 100644
index 0000000000..8cef9cb0da
--- /dev/null
+++ b/mobile/android/android-components/components/lib/jexl/src/test/java/mozilla/components/lib/jexl/value/JexlValueTest.kt
@@ -0,0 +1,170 @@
+/* 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.jexl.value
+
+import mozilla.components.lib.jexl.Jexl
+import mozilla.components.lib.jexl.JexlException
+import org.junit.Assert.assertEquals
+import org.junit.Assert.fail
+import org.junit.Test
+import kotlin.reflect.KClass
+
+class JexlValueTest {
+ @Test
+ fun `double arithmetic`() {
+ "2.0 + 1".evaluatesTo(3.0)
+ "2.0 + 4.0".evaluatesTo(6.0)
+ "2.0 + 'a'".evaluatesTo("2.0a")
+ "2.0 + true".evaluatesTo(3.0)
+ "2.0 + false".evaluatesTo(2.0)
+ "2.0 + {}".evaluationThrows()
+
+ "2.0 * 2".evaluatesTo(4.0)
+ "3.0 * 3.0".evaluatesTo(9.0)
+ "2.0 * true".evaluatesTo(2.0)
+ "2.0 * false".evaluatesTo(0.0)
+ "2.0 * a".evaluationThrows()
+
+ "4.0 / 2".evaluatesTo(2.0)
+ "6.0 / 3.0".evaluatesTo(2.0)
+ "2.0 / 'a'".evaluationThrows()
+
+ "2.0 == 2.0".evaluatesTo(true)
+ "2.0 == 2".evaluatesTo(true)
+ "2.1 == 2.0".evaluatesTo(false)
+
+ "2.0 > 1.0".evaluatesTo(true)
+ "2.0 < 4.0".evaluatesTo(true)
+ "1.0 > 2.0".evaluatesTo(false)
+ "5.0 < 2.0".evaluatesTo(false)
+ "1.0 < 1.0".evaluatesTo(false)
+ "1.0 > 1.0".evaluatesTo(false)
+ }
+
+ @Test
+ fun `integer arithmetic`() {
+ "2 + 1".evaluatesTo(3)
+ "2 + 4.0".evaluatesTo(6.0)
+ "2 + 'a'".evaluatesTo("2a")
+ "2 + true".evaluatesTo(3)
+ "2 + false".evaluatesTo(2)
+ "2 + {}".evaluationThrows()
+
+ "2 * 2".evaluatesTo(4)
+ "3 * 3.0".evaluatesTo(9.0)
+ "2 * true".evaluatesTo(2)
+ "2 * false".evaluatesTo(0)
+ "2 * a".evaluationThrows()
+
+ "4 / 2".evaluatesTo(2)
+ "6 / 3.0".evaluatesTo(2.0)
+ "2 / 'a'".evaluationThrows()
+
+ "2 == 2.0".evaluatesTo(true)
+ "2 == 2".evaluatesTo(true)
+
+ "2 > 1".evaluatesTo(true)
+ "2 < 4".evaluatesTo(true)
+ "1 > 2".evaluatesTo(false)
+ "5 < 2".evaluatesTo(false)
+ "1 < 1".evaluatesTo(false)
+ "1 > 1".evaluatesTo(false)
+ }
+
+ @Test
+ fun `boolean arithmetic`() {
+ "true / false".evaluationThrows()
+
+ "true * 2".evaluatesTo(2)
+ "false * 5".evaluatesTo(0)
+ "true * 2.0".evaluatesTo(2.0)
+ "false * 5.0".evaluatesTo(0.0)
+ "true * {}".evaluationThrows()
+ "true * []".evaluationThrows()
+ "true * true".evaluatesTo(1)
+ "false * false".evaluatesTo(0)
+ "true * false".evaluatesTo(0)
+
+ "true + 1".evaluatesTo(2)
+ "false + 1".evaluatesTo(1)
+ "true + 1.0".evaluatesTo(2.0)
+ "false + 1.0".evaluatesTo(1.0)
+ "true + 'hello'".evaluatesTo("truehello")
+ "true + true".evaluatesTo(2)
+ "true + false".evaluatesTo(1)
+ "false + true".evaluatesTo(1)
+ "false + false".evaluatesTo(0)
+ "false + {}".evaluationThrows()
+ "true + []".evaluationThrows()
+
+ "true > false".evaluationThrows()
+ "false < false".evaluationThrows()
+
+ "true == true".evaluatesTo(true)
+ "false == false".evaluatesTo(true)
+ "true == false".evaluatesTo(false)
+ "false == true".evaluatesTo(false)
+ "true == 'hello'".evaluatesTo(false)
+ }
+
+ @Test
+ fun `string arithmetic`() {
+ "'a' / 2".evaluationThrows()
+ "'a' * 2".evaluationThrows()
+
+ "'hello' + 1".evaluatesTo("hello1")
+ "'hello' + 2.0".evaluatesTo("hello2.0")
+ "'hello' + ' ' + 'world'".evaluatesTo("hello world")
+
+ "'hello' > 'world'".evaluationThrows()
+ "'world' < 'hello'".evaluationThrows()
+
+ "'hello' == true".evaluatesTo(false)
+ "'' == true".evaluatesTo(false)
+ "'hello' == false".evaluatesTo(false)
+ "'' == false".evaluatesTo(false)
+ }
+
+ @Test
+ fun `array arithmetic`() {
+ "[1,2,3] == [1,2,3]".evaluatesTo(true)
+ "[2,3,4] == [2,3,5]".evaluatesTo(false)
+ }
+
+ @Test
+ fun `object arithmetic`() {
+ "{} / 2".evaluationThrows()
+ "{} * 5".evaluationThrows()
+ "{} + 'hello'".evaluationThrows()
+ "{} > 9".evaluationThrows()
+
+ "{} == {}".evaluatesTo(true)
+ "{agent:'Archer'} == { agent: 'Archer' }".evaluatesTo(true)
+ "{a: 1, b: 2} == {b: 2, a: 1}".evaluatesTo(true)
+ "{} == 2".evaluatesTo(false)
+ }
+}
+
+private fun String.evaluatesTo(expectedResult: Any, unpacked: Boolean = true) {
+ val jexl = Jexl()
+ val actualResult = jexl.evaluate(this)
+
+ assertEquals(expectedResult, if (unpacked) actualResult.value else actualResult)
+}
+
+private fun String.evaluationThrows() {
+ evaluationThrows(JexlException::class)
+}
+
+private inline fun <reified T : Throwable> String.evaluationThrows(clazz: KClass<T>?) {
+ try {
+ evaluatesTo(Any())
+ fail("Expected exception to be thrown: $clazz")
+ } catch (e: Throwable) {
+ if (e !is T) {
+ throw e
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/publicsuffixlist/README.md b/mobile/android/android-components/components/lib/publicsuffixlist/README.md
new file mode 100644
index 0000000000..1ac28bcb60
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/README.md
@@ -0,0 +1,64 @@
+# [Android Components](../../../README.md) > Libraries > Public Suffix List
+
+A library for reading and using the Public Suffix List.
+
+> A "public suffix" is one under which Internet users can (or historically could) directly register names. Some examples of public suffixes are .com, .co.uk and pvt.k12.ma.us. The Public Suffix List is a list of all known public suffixes.
+> [https://publicsuffix.org/](https://publicsuffix.org/)
+
+## 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-publicsuffixlist:{latest-version}"
+```
+
+### Using the public suffix list
+
+The `PublicSuffixList` class offers multiple methods for using the public suffix list data. For every instance the list needs to be read from disk into memory once. Therefore all methods return [Deferred](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/) types. The list data is cached in the `PublicSuffixList` and therefore it is recommended to keep a single instance in memory when frequently accessing the list. The list data can be prefetched to guarantee fast access for subsequent access.
+
+```Kotlin
+val publicSuffixList = PublicSuffixList(context)
+
+// Not needed, but allows a consumer to decide when the read is happening:
+publicSuffixList.prefetch()
+ // Optionally you can wait for the read to complete:
+publicSuffixList.prefetch().await()
+```
+
+```Kotlin
+// Extracting the effective top-level domain (eTLD)
+publicSuffixList.getPublicSuffixPlusOne("www.mozilla.org") // -> mozilla.org
+publicSuffixList.getPublicSuffixPlusOne("www.bbc.co.uk") // -> bbc.co.uk
+publicSuffixList.getPublicSuffixPlusOne("a.b.ide.kyoto.jp") // -> b.ide.kyoto.jp
+```
+
+```Kotlin
+// Checking whether a value is a public suffix:
+publicSuffixList.isPublicSuffix("org") // -> true
+publicSuffixList.isPublicSuffix("co.uk") // -> true
+publicSuffixList.isPublicSuffix("org") // -> true
+publicSuffixList.isPublicSuffix("ide.kyoto.jp") --> true
+```
+
+```Kotlin
+// Extracting the public suffix from a domain
+publicSuffixList.getPublicSuffix("www.mozilla.org") // -> org
+publicSuffixList.getPublicSuffix("www.bbc.co.uk") // -> co.uk
+publicSuffixList.getPublicSuffix("a.b.ide.kyoto.jp") // -> ide.kyoto.jp
+```
+
+```Kotlin
+// Removing the public suffix from a domain
+publicSuffixList.stripPublicSuffix("www.mozilla.org") // -> www.mozilla
+publicSuffixList.stripPublicSuffix("foobar.blogspot.com") // -> foobar
+publicSuffixList.stripPublicSuffix("www.example.pvt.k12.ma.us") // -> www.example
+```
+
+## 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/publicsuffixlist/build.gradle b/mobile/android/android-components/components/lib/publicsuffixlist/build.gradle
new file mode 100644
index 0000000000..6728b4cc46
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/build.gradle
@@ -0,0 +1,49 @@
+/* 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/. */
+
+plugins {
+ id 'mozac.PublicSuffixListPlugin'
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'mozac.PublicSuffixListPlugin'
+
+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'
+ }
+ }
+
+ buildFeatures {
+ viewBinding true
+ }
+
+ namespace 'mozilla.components.lib.publicsuffixlist'
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+ implementation ComponentsDependencies.androidx_annotation
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+}
+
+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/publicsuffixlist/proguard-rules.pro b/mobile/android/android-components/components/lib/publicsuffixlist/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/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/publicsuffixlist/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<manifest />
diff --git a/mobile/android/android-components/components/lib/publicsuffixlist/src/main/assets/publicsuffixes b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/assets/publicsuffixes
new file mode 100644
index 0000000000..6fbd7cfa64
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/assets/publicsuffixes
Binary files differ
diff --git a/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt
new file mode 100644
index 0000000000..dbaab5530d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt
@@ -0,0 +1,138 @@
+/* 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.publicsuffixlist
+
+import android.content.Context
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+
+/**
+ * API for reading and accessing the public suffix list.
+ *
+ * > A "public suffix" is one under which Internet users can (or historically could) directly register names. Some
+ * > examples of public suffixes are .com, .co.uk and pvt.k12.ma.us. The Public Suffix List is a list of all known
+ * > public suffixes.
+ *
+ * Note that this implementation applies the rules of the public suffix list only and does not validate domains.
+ *
+ * https://publicsuffix.org/
+ * https://github.com/publicsuffix/list
+ */
+class PublicSuffixList(
+ context: Context,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ private val scope: CoroutineScope = CoroutineScope(dispatcher),
+) {
+ private val data: PublicSuffixListData by lazy { PublicSuffixListLoader.load(context) }
+
+ /**
+ * Prefetch the public suffix list from disk so that it is available in memory.
+ */
+ fun prefetch(): Deferred<Unit> = scope.async {
+ data.run { Unit }
+ }
+
+ /**
+ * Returns true if the given [domain] is a public suffix; false otherwise.
+ *
+ * E.g.:
+ * ```
+ * co.uk -> true
+ * com -> true
+ * mozilla.org -> false
+ * org -> true
+ * ```
+ *
+ * Note that this method ignores the default "prevailing rule" described in the formal public suffix list algorithm:
+ * If no rule matches then the passed [domain] is assumed to *not* be a public suffix.
+ *
+ * @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values
+ * are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result.
+ */
+ fun isPublicSuffix(domain: String): Deferred<Boolean> = scope.async {
+ when (data.getPublicSuffixOffset(domain)) {
+ is PublicSuffixOffset.PublicSuffix -> true
+ else -> false
+ }
+ }
+
+ /**
+ * Returns the public suffix and one more level; known as the registrable domain. Returns `null` if
+ * [domain] is a public suffix itself.
+ *
+ * E.g.:
+ * ```
+ * wwww.mozilla.org -> mozilla.org
+ * www.bcc.co.uk -> bbc.co.uk
+ * a.b.ide.kyoto.jp -> b.ide.kyoto.jp
+ * ```
+ *
+ * @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values
+ * are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result.
+ */
+ fun getPublicSuffixPlusOne(domain: String): Deferred<String?> = scope.async {
+ when (val offset = data.getPublicSuffixOffset(domain)) {
+ is PublicSuffixOffset.Offset ->
+ domain
+ .split('.')
+ .drop(offset.value)
+ .joinToString(separator = ".")
+ else -> null
+ }
+ }
+
+ /**
+ * Returns the public suffix of the given [domain]; known as the effective top-level domain (eTLD). Returns `null`
+ * if the [domain] is a public suffix itself.
+ *
+ * E.g.:
+ * ```
+ * wwww.mozilla.org -> org
+ * www.bcc.co.uk -> co.uk
+ * a.b.ide.kyoto.jp -> ide.kyoto.jp
+ * ```
+ *
+ * @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values
+ * are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result.
+ */
+ fun getPublicSuffix(domain: String) = scope.async {
+ when (val offset = data.getPublicSuffixOffset(domain)) {
+ is PublicSuffixOffset.Offset ->
+ domain
+ .split('.')
+ .drop(offset.value + 1)
+ .joinToString(separator = ".")
+ else -> null
+ }
+ }
+
+ /**
+ * Strips the public suffix from the given [domain]. Returns the original domain if no public suffix could be
+ * stripped.
+ *
+ * E.g.:
+ * ```
+ * wwww.mozilla.org -> www.mozilla
+ * www.bcc.co.uk -> www.bbc
+ * a.b.ide.kyoto.jp -> a.b
+ * ```
+ *
+ * @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values
+ * are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result.
+ */
+ fun stripPublicSuffix(domain: String) = scope.async {
+ when (val offset = data.getPublicSuffixOffset(domain)) {
+ is PublicSuffixOffset.Offset ->
+ domain
+ .split('.')
+ .joinToString(separator = ".", limit = offset.value + 1, truncated = "")
+ .dropLast(1)
+ else -> domain
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt
new file mode 100644
index 0000000000..85986e5a4f
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.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.publicsuffixlist
+
+import mozilla.components.lib.publicsuffixlist.ext.binarySearch
+import java.net.IDN
+
+/**
+ * Class wrapping the public suffix list data and offering methods for accessing rules in it.
+ */
+internal class PublicSuffixListData(
+ private val rules: ByteArray,
+ private val exceptions: ByteArray,
+) {
+ private fun binarySearchRules(labels: List<ByteArray>, labelIndex: Int): String? {
+ return rules.binarySearch(labels, labelIndex)
+ }
+
+ private fun binarySearchExceptions(labels: List<ByteArray>, labelIndex: Int): String? {
+ return exceptions.binarySearch(labels, labelIndex)
+ }
+
+ @Suppress("ReturnCount")
+ fun getPublicSuffixOffset(domain: String): PublicSuffixOffset? {
+ if (domain.isEmpty()) {
+ return null
+ }
+
+ val domainLabels = IDN.toUnicode(domain).split('.')
+ if (domainLabels.find { it.isEmpty() } != null) {
+ // At least one of the labels is empty: Bail out.
+ return null
+ }
+
+ val rule = findMatchingRule(domainLabels)
+
+ if (domainLabels.size == rule.size && rule[0][0] != PublicSuffixListData.EXCEPTION_MARKER) {
+ // The domain is a public suffix.
+ return if (rule == PublicSuffixListData.PREVAILING_RULE) {
+ PublicSuffixOffset.PrevailingRule
+ } else {
+ PublicSuffixOffset.PublicSuffix
+ }
+ }
+
+ return if (rule[0][0] == PublicSuffixListData.EXCEPTION_MARKER) {
+ // Exception rules hold the effective TLD plus one.
+ PublicSuffixOffset.Offset(domainLabels.size - rule.size)
+ } else {
+ // Otherwise the rule is for a public suffix, so we must take one more label.
+ PublicSuffixOffset.Offset(domainLabels.size - (rule.size + 1))
+ }
+ }
+
+ /**
+ * Find a matching rule for the given domain labels.
+ *
+ * This algorithm is based on OkHttp's PublicSuffixDatabase class:
+ * https://github.com/square/okhttp/blob/master/okhttp/src/main/java/okhttp3/internal/publicsuffix/PublicSuffixDatabase.java
+ */
+ private fun findMatchingRule(domainLabels: List<String>): List<String> {
+ // Break apart the domain into UTF-8 labels, i.e. foo.bar.com turns into [foo, bar, com].
+ val domainLabelsBytes = domainLabels.map { it.toByteArray(Charsets.UTF_8) }
+
+ val exactMatch = findExactMatch(domainLabelsBytes)
+ val wildcardMatch = findWildcardMatch(domainLabelsBytes)
+ val exceptionMatch = findExceptionMatch(domainLabelsBytes, wildcardMatch)
+
+ if (exceptionMatch != null) {
+ return ("${PublicSuffixListData.EXCEPTION_MARKER}$exceptionMatch").split('.')
+ }
+
+ if (exactMatch == null && wildcardMatch == null) {
+ return PublicSuffixListData.PREVAILING_RULE
+ }
+
+ val exactRuleLabels = exactMatch?.split('.') ?: PublicSuffixListData.EMPTY_RULE
+ val wildcardRuleLabels = wildcardMatch?.split('.') ?: PublicSuffixListData.EMPTY_RULE
+
+ return if (exactRuleLabels.size > wildcardRuleLabels.size) {
+ exactRuleLabels
+ } else {
+ wildcardRuleLabels
+ }
+ }
+
+ /**
+ * Returns an exact match or null.
+ */
+ private fun findExactMatch(labels: List<ByteArray>): String? {
+ // Start by looking for exact matches. We start at the leftmost label. For example, foo.bar.com
+ // will look like: [foo, bar, com], [bar, com], [com]. The longest matching rule wins.
+
+ for (i in 0 until labels.size) {
+ val rule = binarySearchRules(labels, i)
+
+ if (rule != null) {
+ return rule
+ }
+ }
+
+ return null
+ }
+
+ /**
+ * Returns a wildcard match or null.
+ */
+ private fun findWildcardMatch(labels: List<ByteArray>): String? {
+ // In theory, wildcard rules are not restricted to having the wildcard in the leftmost position.
+ // In practice, wildcards are always in the leftmost position. For now, this implementation
+ // cheats and does not attempt every possible permutation. Instead, it only considers wildcards
+ // in the leftmost position. We assert this fact when we generate the public suffix file. If
+ // this assertion ever fails we'll need to refactor this implementation.
+ if (labels.size > 1) {
+ val labelsWithWildcard = labels.toMutableList()
+ for (labelIndex in 0 until labelsWithWildcard.size) {
+ labelsWithWildcard[labelIndex] = PublicSuffixListData.WILDCARD_LABEL
+ val rule = binarySearchRules(labelsWithWildcard, labelIndex)
+ if (rule != null) {
+ return rule
+ }
+ }
+ }
+
+ return null
+ }
+
+ private fun findExceptionMatch(labels: List<ByteArray>, wildcardMatch: String?): String? {
+ // Exception rules only apply to wildcard rules, so only try it if we matched a wildcard.
+ if (wildcardMatch == null) {
+ return null
+ }
+
+ for (labelIndex in 0 until labels.size) {
+ val rule = binarySearchExceptions(labels, labelIndex)
+ if (rule != null) {
+ return rule
+ }
+ }
+
+ return null
+ }
+
+ companion object {
+ val WILDCARD_LABEL = byteArrayOf('*'.code.toByte())
+ val PREVAILING_RULE = listOf("*")
+ val EMPTY_RULE = listOf<String>()
+ const val EXCEPTION_MARKER = '!'
+ }
+}
+
+internal sealed class PublicSuffixOffset {
+ data class Offset(val value: Int) : PublicSuffixOffset()
+ object PublicSuffix : PublicSuffixOffset()
+ object PrevailingRule : PublicSuffixOffset()
+}
diff --git a/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.kt b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.kt
new file mode 100644
index 0000000000..d9afcbe7b0
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.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.publicsuffixlist
+
+import android.content.Context
+import java.io.BufferedInputStream
+import java.io.IOException
+
+private const val PUBLIC_SUFFIX_LIST_FILE = "publicsuffixes"
+
+internal object PublicSuffixListLoader {
+ fun load(context: Context): PublicSuffixListData = context.assets.open(
+ PUBLIC_SUFFIX_LIST_FILE,
+ ).buffered().use { stream ->
+ val publicSuffixSize = stream.readInt()
+ val publicSuffixBytes = stream.readFully(publicSuffixSize)
+
+ val exceptionSize = stream.readInt()
+ val exceptionBytes = stream.readFully(exceptionSize)
+
+ PublicSuffixListData(publicSuffixBytes, exceptionBytes)
+ }
+}
+
+@Suppress("MagicNumber")
+private fun BufferedInputStream.readInt(): Int {
+ return (
+ read() and 0xff shl 24
+ or (read() and 0xff shl 16)
+ or (read() and 0xff shl 8)
+ or (read() and 0xff)
+ )
+}
+
+private fun BufferedInputStream.readFully(size: Int): ByteArray {
+ val bytes = ByteArray(size)
+
+ var offset = 0
+ while (offset < size) {
+ val read = read(bytes, offset, size - offset)
+ if (read == -1) {
+ throw IOException("Unexpected end of stream")
+ }
+ offset += read
+ }
+
+ return bytes
+}
diff --git a/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt
new file mode 100644
index 0000000000..c0a215ebe0
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt
@@ -0,0 +1,122 @@
+/* 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.publicsuffixlist.ext
+
+import kotlin.experimental.and
+
+private const val BITMASK = 0xff.toByte()
+
+/**
+ * Performs a binary search for the provided [labels] on the [ByteArray]'s data.
+ *
+ * This algorithm is based on OkHttp's PublicSuffixDatabase class:
+ * https://github.com/square/okhttp/blob/master/okhttp/src/main/java/okhttp3/internal/publicsuffix/PublicSuffixDatabase.java
+ */
+@Suppress("ComplexMethod", "NestedBlockDepth")
+internal fun ByteArray.binarySearch(labels: List<ByteArray>, labelIndex: Int): String? {
+ var low = 0
+ var high = size
+ var match: String? = null
+
+ while (low < high) {
+ val mid = (low + high) / 2
+ val start = findStartOfLineFromIndex(mid)
+ val end = findEndOfLineFromIndex(start)
+
+ val publicSuffixLength = start + end - start
+
+ var compareResult: Int
+ var currentLabelIndex = labelIndex
+ var currentLabelByteIndex = 0
+ var publicSuffixByteIndex = 0
+
+ var expectDot = false
+ while (true) {
+ val byte0 = if (expectDot) {
+ expectDot = false
+ '.'.code.toByte()
+ } else {
+ labels[currentLabelIndex][currentLabelByteIndex] and BITMASK
+ }
+
+ val byte1 = this[start + publicSuffixByteIndex] and BITMASK
+
+ // Compare the bytes. Note that the file stores UTF-8 encoded bytes, so we must compare the
+ // unsigned bytes.
+ @Suppress("EXPERIMENTAL_API_USAGE")
+ compareResult = (byte0.toUByte() - byte1.toUByte()).toInt()
+ if (compareResult != 0) {
+ break
+ }
+
+ publicSuffixByteIndex++
+ currentLabelByteIndex++
+
+ if (publicSuffixByteIndex == publicSuffixLength) {
+ break
+ }
+
+ if (labels[currentLabelIndex].size == currentLabelByteIndex) {
+ // We've exhausted our current label. Either there are more labels to compare, in which
+ // case we expect a dot as the next character. Otherwise, we've checked all our labels.
+ if (currentLabelIndex == labels.size - 1) {
+ break
+ } else {
+ currentLabelIndex++
+ currentLabelByteIndex = -1
+ expectDot = true
+ }
+ }
+ }
+
+ if (compareResult < 0) {
+ high = start - 1
+ } else if (compareResult > 0) {
+ low = start + end + 1
+ } else {
+ // We found a match, but are the lengths equal?
+ val publicSuffixBytesLeft = publicSuffixLength - publicSuffixByteIndex
+ var labelBytesLeft = labels[currentLabelIndex].size - currentLabelByteIndex
+ for (i in currentLabelIndex + 1 until labels.size) {
+ labelBytesLeft += labels[i].size
+ }
+
+ if (labelBytesLeft < publicSuffixBytesLeft) {
+ high = start - 1
+ } else if (labelBytesLeft > publicSuffixBytesLeft) {
+ low = start + end + 1
+ } else {
+ // Found a match.
+ match = String(this, start, publicSuffixLength, Charsets.UTF_8)
+ break
+ }
+ }
+ }
+
+ return match
+}
+
+/**
+ * Search for a '\n' that marks the start of a value. Don't go back past the start of the array.
+ */
+private fun ByteArray.findStartOfLineFromIndex(start: Int): Int {
+ var index = start
+ while (index > -1 && this[index] != '\n'.code.toByte()) {
+ index--
+ }
+ index++
+ return index
+}
+
+/**
+ * Search for a '\n' that marks the end of a value.
+ */
+private fun ByteArray.findEndOfLineFromIndex(start: Int): Int {
+ var end = 1
+ while (this[start + end] != '\n'.code.toByte()) {
+ end++
+ }
+ return end
+}
diff --git a/mobile/android/android-components/components/lib/publicsuffixlist/src/test/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListTest.kt b/mobile/android/android-components/components/lib/publicsuffixlist/src/test/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListTest.kt
new file mode 100644
index 0000000000..86ffd43ac9
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/src/test/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListTest.kt
@@ -0,0 +1,482 @@
+/* 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.publicsuffixlist
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class PublicSuffixListTest {
+
+ private val publicSuffixList
+ get() = PublicSuffixList(testContext)
+
+ @Test
+ fun `Verify getPublicSuffixPlusOne for known domains`() = runTest {
+ assertEquals(
+ "mozilla.org",
+ publicSuffixList.getPublicSuffixPlusOne("www.mozilla.org").await(),
+ )
+
+ assertEquals(
+ "google.com",
+ publicSuffixList.getPublicSuffixPlusOne("google.com").await(),
+ )
+
+ assertEquals(
+ "foobar.blogspot.com",
+ publicSuffixList.getPublicSuffixPlusOne("foobar.blogspot.com").await(),
+ )
+
+ assertEquals(
+ "independent.co.uk",
+ publicSuffixList.getPublicSuffixPlusOne("independent.co.uk").await(),
+ )
+
+ assertEquals(
+ "independent.co.uk",
+ publicSuffixList.getPublicSuffixPlusOne("www.independent.co.uk").await(),
+ )
+
+ assertEquals(
+ "biz.com.ua",
+ publicSuffixList.getPublicSuffixPlusOne("www.biz.com.ua").await(),
+ )
+
+ assertEquals(
+ "example.org",
+ publicSuffixList.getPublicSuffixPlusOne("example.org").await(),
+ )
+
+ assertEquals(
+ "example.pvt.k12.ma.us",
+ publicSuffixList.getPublicSuffixPlusOne("www.example.pvt.k12.ma.us").await(),
+ )
+
+ assertEquals(
+ "δπθ.gr",
+ publicSuffixList.getPublicSuffixPlusOne("www.ουτοπία.δπθ.gr").await(),
+ )
+ }
+
+ @Test
+ fun `Verify getPublicSuffix for known domains`() = runTest {
+ assertEquals(
+ "org",
+ publicSuffixList.getPublicSuffix("www.mozilla.org").await(),
+ )
+
+ assertEquals(
+ "com",
+ publicSuffixList.getPublicSuffix("google.com").await(),
+ )
+
+ assertEquals(
+ "blogspot.com",
+ publicSuffixList.getPublicSuffix("foobar.blogspot.com").await(),
+ )
+
+ assertEquals(
+ "co.uk",
+ publicSuffixList.getPublicSuffix("independent.co.uk").await(),
+ )
+
+ assertEquals(
+ "co.uk",
+ publicSuffixList.getPublicSuffix("www.independent.co.uk").await(),
+ )
+
+ assertEquals(
+ "com.ua",
+ publicSuffixList.getPublicSuffix("www.biz.com.ua").await(),
+ )
+
+ assertEquals(
+ "org",
+ publicSuffixList.getPublicSuffix("example.org").await(),
+ )
+
+ assertEquals(
+ "pvt.k12.ma.us",
+ publicSuffixList.getPublicSuffix("www.example.pvt.k12.ma.us").await(),
+ )
+
+ assertEquals(
+ "gr",
+ publicSuffixList.getPublicSuffix("www.ουτοπία.δπθ.gr").await(),
+ )
+ }
+
+ @Test
+ fun `Verify stripPublicSuffix for known domains`() = runTest {
+ assertEquals(
+ "www.mozilla",
+ publicSuffixList.stripPublicSuffix("www.mozilla.org").await(),
+ )
+
+ assertEquals(
+ "google",
+ publicSuffixList.stripPublicSuffix("google.com").await(),
+ )
+
+ assertEquals(
+ "foobar",
+ publicSuffixList.stripPublicSuffix("foobar.blogspot.com").await(),
+ )
+
+ assertEquals(
+ "independent",
+ publicSuffixList.stripPublicSuffix("independent.co.uk").await(),
+ )
+
+ assertEquals(
+ "www.independent",
+ publicSuffixList.stripPublicSuffix("www.independent.co.uk").await(),
+ )
+
+ assertEquals(
+ "www.biz",
+ publicSuffixList.stripPublicSuffix("www.biz.com.ua").await(),
+ )
+
+ assertEquals(
+ "example",
+ publicSuffixList.stripPublicSuffix("example.org").await(),
+ )
+
+ assertEquals(
+ "www.example",
+ publicSuffixList.stripPublicSuffix("www.example.pvt.k12.ma.us").await(),
+ )
+
+ assertEquals(
+ "www.ουτοπία.δπθ",
+ publicSuffixList.stripPublicSuffix("www.ουτοπία.δπθ.gr").await(),
+ )
+ }
+
+ /**
+ * Short set of test data from:
+ * https://raw.githubusercontent.com/publicsuffix/list/master/tests/test_psl.txt
+ */
+ @Test
+ fun `Verify getPublicSuffixPlusOne against official test data`() = runTest {
+ // empty input
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("").await())
+
+ // Mixed case.
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("COM").await())
+ assertEquals(
+ "example.COM",
+ publicSuffixList.getPublicSuffixPlusOne("example.COM").await(),
+ )
+ assertEquals(
+ "eXample.COM",
+ publicSuffixList.getPublicSuffixPlusOne("WwW.eXample.COM").await(),
+ )
+
+ // Leading dot.
+ // ArrayIndexOutOfBoundsException: assertEquals("", publicSuffixList.getPublicSuffixPlusOne(".example.com").await())
+
+ // TLD with only 1 rule.
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("biz").await())
+ assertEquals(
+ "domain.biz",
+ publicSuffixList.getPublicSuffixPlusOne("domain.biz").await(),
+ )
+ assertEquals(
+ "domain.biz",
+ publicSuffixList.getPublicSuffixPlusOne("b.domain.biz").await(),
+ )
+ assertEquals(
+ "domain.biz",
+ publicSuffixList.getPublicSuffixPlusOne("a.b.domain.biz").await(),
+ )
+
+ // TLD with some 2-level rules.
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("com").await())
+ assertEquals(
+ "example.com",
+ publicSuffixList.getPublicSuffixPlusOne("example.com").await(),
+ )
+ assertEquals(
+ "example.com",
+ publicSuffixList.getPublicSuffixPlusOne("b.example.com").await(),
+ )
+ assertEquals(
+ "example.com",
+ publicSuffixList.getPublicSuffixPlusOne("a.b.example.com").await(),
+ )
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("uk.com").await())
+ assertEquals(
+ "example.uk.com",
+ publicSuffixList.getPublicSuffixPlusOne("example.uk.com").await(),
+ )
+ assertEquals(
+ "example.uk.com",
+ publicSuffixList.getPublicSuffixPlusOne("b.example.uk.com").await(),
+ )
+ assertEquals(
+ "example.uk.com",
+ publicSuffixList.getPublicSuffixPlusOne("a.b.example.uk.com").await(),
+ )
+ assertEquals(
+ "test.ac",
+ publicSuffixList.getPublicSuffixPlusOne("test.ac").await(),
+ )
+
+ // TLD with only 1 (wildcard) rule.
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("mm").await())
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("c.mm").await())
+ assertEquals(
+ "b.c.mm",
+ publicSuffixList.getPublicSuffixPlusOne("b.c.mm").await(),
+ )
+ assertEquals(
+ "b.c.mm",
+ publicSuffixList.getPublicSuffixPlusOne("a.b.c.mm").await(),
+ )
+
+ // More complex TLD.
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("jp").await())
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("ac.jp").await())
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("kyoto.jp").await())
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("ide.kyoto.jp").await())
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("c.kobe.jp").await())
+ assertEquals(
+ "test.jp",
+ publicSuffixList.getPublicSuffixPlusOne("test.jp").await(),
+ )
+ assertEquals(
+ "test.jp",
+ publicSuffixList.getPublicSuffixPlusOne("www.test.jp").await(),
+ )
+ assertEquals(
+ "test.ac.jp",
+ publicSuffixList.getPublicSuffixPlusOne("test.ac.jp").await(),
+ )
+ assertEquals(
+ "test.ac.jp",
+ publicSuffixList.getPublicSuffixPlusOne("www.test.ac.jp").await(),
+ )
+ assertEquals(
+ "test.kyoto.jp",
+ publicSuffixList.getPublicSuffixPlusOne("test.kyoto.jp").await(),
+ )
+ assertEquals(
+ "b.ide.kyoto.jp",
+ publicSuffixList.getPublicSuffixPlusOne("b.ide.kyoto.jp").await(),
+ )
+ assertEquals(
+ "b.ide.kyoto.jp",
+ publicSuffixList.getPublicSuffixPlusOne("a.b.ide.kyoto.jp").await(),
+ )
+ assertEquals(
+ "b.c.kobe.jp",
+ publicSuffixList.getPublicSuffixPlusOne("b.c.kobe.jp").await(),
+ )
+ assertEquals(
+ "b.c.kobe.jp",
+ publicSuffixList.getPublicSuffixPlusOne("a.b.c.kobe.jp").await(),
+ )
+ assertEquals(
+ "city.kobe.jp",
+ publicSuffixList.getPublicSuffixPlusOne("city.kobe.jp").await(),
+ )
+ assertEquals(
+ "city.kobe.jp",
+ publicSuffixList.getPublicSuffixPlusOne("www.city.kobe.jp").await(),
+ )
+
+ // TLD with a wildcard rule and exceptions.
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("ck").await())
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("test.ck").await())
+ assertEquals(
+ "b.test.ck",
+ publicSuffixList.getPublicSuffixPlusOne("b.test.ck").await(),
+ )
+ assertEquals(
+ "b.test.ck",
+ publicSuffixList.getPublicSuffixPlusOne("a.b.test.ck").await(),
+ )
+ assertEquals(
+ "www.ck",
+ publicSuffixList.getPublicSuffixPlusOne("www.ck").await(),
+ )
+ assertEquals(
+ "www.ck",
+ publicSuffixList.getPublicSuffixPlusOne("www.www.ck").await(),
+ )
+
+ // US K12.
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("us").await())
+ assertEquals(
+ "test.us",
+ publicSuffixList.getPublicSuffixPlusOne("test.us").await(),
+ )
+ assertEquals(
+ "test.us",
+ publicSuffixList.getPublicSuffixPlusOne("www.test.us").await(),
+ )
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("ak.us").await())
+ assertEquals(
+ "test.ak.us",
+ publicSuffixList.getPublicSuffixPlusOne("www.test.ak.us").await(),
+ )
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("k12.ak.us").await())
+ assertEquals(
+ "test.k12.ak.us",
+ publicSuffixList.getPublicSuffixPlusOne("test.k12.ak.us").await(),
+ )
+ assertEquals(
+ "test.k12.ak.us",
+ publicSuffixList.getPublicSuffixPlusOne("www.test.k12.ak.us").await(),
+ )
+
+ // IDN labels.
+ assertEquals(
+ "食狮.com.cn",
+ publicSuffixList.getPublicSuffixPlusOne("食狮.com.cn").await(),
+ )
+ // https://github.com/mozilla-mobile/android-components/issues/1777
+ assertEquals(
+ "食狮.公司.cn",
+ publicSuffixList.getPublicSuffixPlusOne("食狮.公司.cn").await(),
+ )
+ assertEquals(
+ "食狮.公司.cn",
+ publicSuffixList.getPublicSuffixPlusOne("www.食狮.公司.cn").await(),
+ )
+ assertEquals(
+ "shishi.公司.cn",
+ publicSuffixList.getPublicSuffixPlusOne("shishi.公司.cn").await(),
+ )
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("公司.cn").await())
+ assertEquals(
+ "食狮.中国",
+ publicSuffixList.getPublicSuffixPlusOne("食狮.中国").await(),
+ )
+ assertEquals(
+ "食狮.中国",
+ publicSuffixList.getPublicSuffixPlusOne("www.食狮.中国").await(),
+ )
+ assertEquals(
+ "shishi.中国",
+ publicSuffixList.getPublicSuffixPlusOne("shishi.中国").await(),
+ )
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("中国").await())
+
+ // Same as above, but punycoded.
+ assertEquals(
+ "xn--85x722f.com.cn",
+ publicSuffixList.getPublicSuffixPlusOne("xn--85x722f.com.cn").await(),
+ )
+ // https://github.com/mozilla-mobile/android-components/issues/1777
+ assertEquals(
+ "xn--85x722f.xn--55qx5d.cn",
+ publicSuffixList.getPublicSuffixPlusOne("xn--85x722f.xn--55qx5d.cn").await(),
+ )
+ assertEquals(
+ "xn--85x722f.xn--55qx5d.cn",
+ publicSuffixList.getPublicSuffixPlusOne("www.xn--85x722f.xn--55qx5d.cn").await(),
+ )
+ assertEquals(
+ "shishi.xn--55qx5d.cn",
+ publicSuffixList.getPublicSuffixPlusOne("shishi.xn--55qx5d.cn").await(),
+ )
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("xn--55qx5d.cn").await())
+ assertEquals(
+ "xn--85x722f.xn--fiqs8s",
+ publicSuffixList.getPublicSuffixPlusOne("xn--85x722f.xn--fiqs8s").await(),
+ )
+ assertEquals(
+ "xn--85x722f.xn--fiqs8s",
+ publicSuffixList.getPublicSuffixPlusOne("www.xn--85x722f.xn--fiqs8s").await(),
+ )
+ assertEquals(
+ "shishi.xn--fiqs8s",
+ publicSuffixList.getPublicSuffixPlusOne("shishi.xn--fiqs8s").await(),
+ )
+ assertNull(publicSuffixList.getPublicSuffixPlusOne("xn--fiqs8s").await())
+ }
+
+ @Test
+ fun `Accessing with and without prefetch`() = runTest {
+ run {
+ val publicSuffixList = PublicSuffixList(testContext)
+ assertEquals("org", publicSuffixList.getPublicSuffix("mozilla.org").await())
+ }
+
+ run {
+ val publicSuffixList = PublicSuffixList(testContext).apply {
+ prefetch().await()
+ }
+ assertEquals("org", publicSuffixList.getPublicSuffix("mozilla.org").await())
+ }
+ }
+
+ @Test
+ fun `Verify isPublicSuffix with known and unknown suffixes`() = runTest {
+ assertTrue(publicSuffixList.isPublicSuffix("org").await())
+ assertTrue(publicSuffixList.isPublicSuffix("com").await())
+ assertTrue(publicSuffixList.isPublicSuffix("us").await())
+ assertTrue(publicSuffixList.isPublicSuffix("de").await())
+ assertTrue(publicSuffixList.isPublicSuffix("de.com").await())
+ assertTrue(publicSuffixList.isPublicSuffix("co.uk").await())
+ assertTrue(publicSuffixList.isPublicSuffix("taxi.br").await())
+ assertTrue(publicSuffixList.isPublicSuffix("edu.cw").await())
+ assertTrue(publicSuffixList.isPublicSuffix("chirurgiens-dentistes.fr").await())
+ assertTrue(publicSuffixList.isPublicSuffix("trani-andria-barletta.it").await())
+ assertTrue(publicSuffixList.isPublicSuffix("yabuki.fukushima.jp").await())
+ assertTrue(publicSuffixList.isPublicSuffix("research.museum").await())
+ assertTrue(publicSuffixList.isPublicSuffix("lamborghini").await())
+ assertTrue(publicSuffixList.isPublicSuffix("reisen").await())
+ assertTrue(publicSuffixList.isPublicSuffix("github.io").await())
+
+ assertFalse(publicSuffixList.isPublicSuffix("").await())
+ assertFalse(publicSuffixList.isPublicSuffix("mozilla").await())
+ assertFalse(publicSuffixList.isPublicSuffix("mozilla.org").await())
+ assertFalse(publicSuffixList.isPublicSuffix("ork").await())
+ assertFalse(publicSuffixList.isPublicSuffix("us.com.uk").await())
+ }
+
+ /**
+ * Test cases inspired by Guava tests:
+ * https://github.com/google/guava/blob/master/guava-tests/test/com/google/common/net/InternetDomainNameTest.java
+ */
+ @Test
+ fun `Verify getPublicSuffix can handle obscure and invalid input`() = runTest {
+ assertEquals("cOM", publicSuffixList.getPublicSuffix("f-_-o.cOM").await())
+ assertEquals("com", publicSuffixList.getPublicSuffix("f11-1.com").await())
+ assertNull(publicSuffixList.getPublicSuffix("www").await())
+ assertEquals("a23", publicSuffixList.getPublicSuffix("abc.a23").await())
+ assertEquals("com", publicSuffixList.getPublicSuffix("a\u0394b.com").await())
+ assertNull(publicSuffixList.getPublicSuffix("").await())
+ assertNull(publicSuffixList.getPublicSuffix(" ").await())
+ assertNull(publicSuffixList.getPublicSuffix(".").await())
+ assertNull(publicSuffixList.getPublicSuffix("..").await())
+ assertNull(publicSuffixList.getPublicSuffix("...").await())
+ assertNull(publicSuffixList.getPublicSuffix("woo.com.").await())
+ assertNull(publicSuffixList.getPublicSuffix("::1").await())
+ assertNull(publicSuffixList.getPublicSuffix("13").await())
+
+ // The following input returns an empty string which does not seem correct:
+ // https://github.com/mozilla-mobile/android-components/issues/3541
+ assertEquals("", publicSuffixList.getPublicSuffix("foo.net.us\uFF61ocm").await())
+
+ // Technically that may be correct; but it doesn't make sense to return part of an IP as public suffix:
+ // https://github.com/mozilla-mobile/android-components/issues/3540
+ assertEquals("1", publicSuffixList.getPublicSuffix("127.0.0.1").await())
+ }
+}
diff --git a/mobile/android/android-components/components/lib/publicsuffixlist/src/test/resources/robolectric.properties b/mobile/android/android-components/components/lib/publicsuffixlist/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/publicsuffixlist/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/lib/push-firebase/README.md b/mobile/android/android-components/components/lib/push-firebase/README.md
new file mode 100644
index 0000000000..f4e00ce8cc
--- /dev/null
+++ b/mobile/android/android-components/components/lib/push-firebase/README.md
@@ -0,0 +1,59 @@
+# [Android Components](../../../README.md) > Libraries > Push-Firebase
+
+A [concept-push](../../concept/push/README.md) implementation using [Firebase Cloud Messaging](https://firebase.google.com/products/cloud-messaging/) (FCM).
+
+This implementation of `concept-push` uses [Firebase Cloud Messaging](https://firebase.google.com/products/cloud-messaging/). It can be used by Android devices that are supposed by Google Play Services.
+
+## Usage
+
+Add the push service for providing the encrypted messages:
+
+```kotlin
+class FirebasePush : AbstractFirebasePushService()
+```
+
+Expose the service in the `AndroidManifest.xml`:
+```xml
+<service android:name=".push.FirebasePush">
+ <intent-filter>
+ <action android:name="com.google.firebase.MESSAGING_EVENT" />
+ </intent-filter>
+</service>
+```
+
+The service can be started/stopped directly if required:
+```kotlin
+val service = FirebasePush()
+
+serivce.start()
+serivce.stop()
+```
+
+See `feature-push` for more details on how to use the service with Autopush.
+
+### 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-push-firebase:{latest-version}"
+```
+
+### Adding Firebase Support
+
+Extend `AbstractFirebasePushService` with your own class:
+```kotlin
+class FirebasePush : AbstractFirebasePushService()
+```
+
+Place your keys file (`google-services.json`) for FCM in the app module of the project.
+
+Optionally, add meta tags to your `AndroidManifest.xml` to disable the push service from automatically starting.
+
+See the [concept-push documentation](../../concept/push/README.md) for generic examples of using the API of components implementing `concept-push`.
+
+## 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/push-firebase/build.gradle b/mobile/android/android-components/components/lib/push-firebase/build.gradle
new file mode 100644
index 0000000000..c8bc41debe
--- /dev/null
+++ b/mobile/android/android-components/components/lib/push-firebase/build.gradle
@@ -0,0 +1,42 @@
+/* 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.push.firebase'
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation project(':concept-push')
+ implementation project(':support-base')
+
+ api ComponentsDependencies.firebase_messaging
+
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.testing_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/push-firebase/proguard-rules.pro b/mobile/android/android-components/components/lib/push-firebase/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/push-firebase/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/push-firebase/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/push-firebase/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/lib/push-firebase/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<manifest />
diff --git a/mobile/android/android-components/components/lib/push-firebase/src/main/java/mozilla/components/lib/push/firebase/AbstractFirebasePushService.kt b/mobile/android/android-components/components/lib/push-firebase/src/main/java/mozilla/components/lib/push/firebase/AbstractFirebasePushService.kt
new file mode 100644
index 0000000000..e378f199a3
--- /dev/null
+++ b/mobile/android/android-components/components/lib/push-firebase/src/main/java/mozilla/components/lib/push/firebase/AbstractFirebasePushService.kt
@@ -0,0 +1,103 @@
+/* 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.push.firebase
+
+import android.content.Context
+import com.google.android.gms.common.ConnectionResult
+import com.google.android.gms.common.GoogleApiAvailability
+import com.google.android.gms.common.util.VisibleForTesting
+import com.google.firebase.FirebaseApp
+import com.google.firebase.messaging.FirebaseMessaging
+import com.google.firebase.messaging.FirebaseMessagingService
+import com.google.firebase.messaging.RemoteMessage
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.concept.push.PushError
+import mozilla.components.concept.push.PushProcessor
+import mozilla.components.concept.push.PushService
+import mozilla.components.concept.push.PushService.Companion.MESSAGE_KEY_CHANNEL_ID
+import mozilla.components.support.base.log.logger.Logger
+import java.io.IOException
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * A Firebase Cloud Messaging implementation of the [PushService] for Android devices that support Google Play Services.
+ */
+abstract class AbstractFirebasePushService(
+ internal val coroutineContext: CoroutineContext = Dispatchers.IO,
+) : FirebaseMessagingService(), PushService {
+
+ private val logger = Logger("AbstractFirebasePushService")
+
+ @VisibleForTesting
+ internal val googleApiAvailability: GoogleApiAvailability
+ get() = GoogleApiAvailability.getInstance()
+
+ /**
+ * Initializes Firebase and starts the messaging service if not already started and enables auto-start as well.
+ */
+ override fun start(context: Context) {
+ logger.info("start")
+ FirebaseApp.initializeApp(context)
+ }
+
+ override fun onNewToken(newToken: String) {
+ logger.info("Got new Firebase token: $newToken")
+ PushProcessor.requireInstance.onNewToken(newToken)
+ }
+
+ @SuppressWarnings("TooGenericExceptionCaught")
+ override fun onMessageReceived(message: RemoteMessage) {
+ logger.info("onMessageReceived")
+ // This is not an AutoPush message we can handle.
+ val chId = message.data.getOrElse(MESSAGE_KEY_CHANNEL_ID) { null }
+
+ if (chId == null) {
+ logger.info("Missing $MESSAGE_KEY_CHANNEL_ID key, skipping this message")
+ return
+ } else {
+ logger.info("Processing message with chId: $chId")
+ }
+
+ // In case of any errors, let the PushProcessor handle this exception. Instead of crashing
+ // here, just drop the message on the floor. This is fine, since we don't really need to
+ // "recover" from a bad incoming message.
+ // PushProcessor will submit relevant issues via a CrashReporter as appropriate.
+ try {
+ PushProcessor.requireInstance.onMessageReceived(message.data)
+ } catch (e: IllegalStateException) {
+ // Re-throw 'requireInstance' exceptions.
+ throw (e)
+ } catch (e: Exception) {
+ PushProcessor.requireInstance.onError(PushError.Rust(e))
+ }
+ }
+
+ /**
+ * Stops the Firebase messaging service and disables auto-start.
+ */
+ final override fun stop() {
+ stopSelf()
+ }
+
+ /**
+ * Removes the Firebase instance ID. This would lead a new token being generated when the
+ * service hits the Firebase servers.
+ */
+ override fun deleteToken() {
+ CoroutineScope(coroutineContext).launch {
+ try {
+ FirebaseMessaging.getInstance().deleteToken()
+ } catch (e: IOException) {
+ logger.error("Force registration renewable failed.", e)
+ }
+ }
+ }
+
+ override fun isServiceAvailable(context: Context): Boolean {
+ return googleApiAvailability.isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS
+ }
+}
diff --git a/mobile/android/android-components/components/lib/push-firebase/src/test/java/mozilla/components/lib/push/firebase/AbstractFirebasePushServiceTest.kt b/mobile/android/android-components/components/lib/push-firebase/src/test/java/mozilla/components/lib/push/firebase/AbstractFirebasePushServiceTest.kt
new file mode 100644
index 0000000000..b63cbbd6c4
--- /dev/null
+++ b/mobile/android/android-components/components/lib/push-firebase/src/test/java/mozilla/components/lib/push/firebase/AbstractFirebasePushServiceTest.kt
@@ -0,0 +1,115 @@
+/* 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.push.firebase
+
+import com.google.android.gms.common.ConnectionResult
+import com.google.android.gms.common.GoogleApiAvailability
+import com.google.firebase.messaging.RemoteMessage
+import kotlinx.coroutines.Dispatchers
+import mozilla.components.concept.push.PushProcessor
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+import org.mockito.Mockito.`when`
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class AbstractFirebasePushServiceTest {
+
+ private val processor: PushProcessor = mock()
+ private val service = TestService()
+
+ @Before
+ fun setup() {
+ reset(processor)
+ PushProcessor.install(processor)
+ }
+
+ @Test
+ fun `onNewToken passes token to processor`() {
+ service.onNewToken("token")
+
+ verify(processor).onNewToken("token")
+ }
+
+ @Test
+ fun `new encrypted messages are passed to the processor`() {
+ val remoteMessage: RemoteMessage = mock()
+ val data = mapOf(
+ "chid" to "1234",
+ "body" to "contents",
+ "con" to "encoding",
+ "enc" to "salt",
+ "cryptokey" to "dh256",
+ )
+ `when`(remoteMessage.data).thenReturn(data)
+ service.onMessageReceived(remoteMessage)
+
+ verify(processor).onMessageReceived(data)
+ }
+
+ @Test
+ fun `malformed message exception should not be thrown`() {
+ val remoteMessage: RemoteMessage = mock()
+ val data = mapOf(
+ "chid" to "1234",
+ )
+ `when`(remoteMessage.data).thenReturn(data)
+ service.onMessageReceived(remoteMessage)
+
+ verify(processor, never()).onError(any())
+ verify(processor).onMessageReceived(data)
+ }
+
+ @Test
+ fun `do nothing if the message is not for us`() {
+ val remoteMessage: RemoteMessage = mock()
+ val data = mapOf(
+ "con" to "encoding",
+ "enc" to "salt",
+ "cryptokey" to "dh256",
+ )
+ `when`(remoteMessage.data).thenReturn(data)
+
+ service.onMessageReceived(remoteMessage)
+
+ verifyNoInteractions(processor)
+ }
+
+ @Test
+ fun `force registration should never be on Main`() {
+ // Default dispatcher isn't main
+ assertTrue(service.coroutineContext != Dispatchers.Main)
+
+ val service = object : AbstractFirebasePushService(Dispatchers.Default) {}
+ service.deleteToken()
+ }
+
+ @Test
+ fun `service available reflects Google Play Services' availability`() {
+ val service = spy(TestService())
+
+ // By default, service is unavailable.
+ assertFalse(service.isServiceAvailable(testContext))
+
+ val googleApiAvailability = mock<GoogleApiAvailability>()
+ `when`(service.googleApiAvailability).thenReturn(googleApiAvailability)
+ `when`(googleApiAvailability.isGooglePlayServicesAvailable(testContext)).thenReturn(ConnectionResult.SUCCESS)
+
+ assertTrue(service.isServiceAvailable(testContext))
+ }
+
+ class TestService : AbstractFirebasePushService()
+}
diff --git a/mobile/android/android-components/components/lib/push-firebase/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/lib/push-firebase/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/push-firebase/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/push-firebase/src/test/resources/robolectric.properties b/mobile/android/android-components/components/lib/push-firebase/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/push-firebase/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28
diff --git a/mobile/android/android-components/components/lib/state/README.md b/mobile/android/android-components/components/lib/state/README.md
new file mode 100644
index 0000000000..eb8f54712a
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/README.md
@@ -0,0 +1,69 @@
+# [Android Components](../../../README.md) > Libraries > State
+
+A generic library for maintaining the state of a component, screen or application.
+
+The state library is inspired by existing libraries like [Redux](https://redux.js.org/) and provides a `Store` class to hold application state.
+
+## 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-state:{latest-version}"
+```
+
+### Action
+
+`Action`s represent payloads of information that send data from your application to the `Store`. You can send actions using `store.dispatch()`. An `Action` will usually be a small data class or object describing a change.
+
+```Kotlin
+data class SetVisibility(val visible: Boolean) : Action
+
+store.dispatch(SetVisibility(true))
+```
+
+### Reducer
+
+`Reducer`s are functions describing how the state should change in response to actions sent to the store.
+
+They take the previous state and an action as parameters, and return the new state as a result of that action.
+
+```Kotlin
+fun reduce(previousState: State, action: Action) = when (action) {
+ is SetVisibility -> previousState.copy(toolbarVisible = action.visible)
+ else -> previousState
+}
+```
+
+### Store
+
+The `Store` brings together actions and reducers. It holds the application state and allows access to it via the `store.state` getter. It allows state to be updated via `store.dispatch()`, and can have listeners registered through `store.observe()`.
+
+Stores can easily be created if you have a reducer.
+
+```Kotlin
+val store = Store<State, Action>(
+ initialState = State(),
+ reducer = ::reduce
+)
+```
+
+Once the store is created, you can react to changes in the state by registering an observer.
+
+```Kotlin
+store.observe(lifecycleOwner) { state ->
+ toolbarView.visibility = if (state.toolbarVisible) View.VISIBLE else View.GONE
+}
+```
+
+`store.observe` is lifecycle aware and will automatically unregister when the lifecycle owner (such as an `Activity` or `Fragment`) is destroyed. Instead of a `LifecycleOwner`, a `View` can be supplied instead.
+
+If you wish to manually control the observer subscription, you can use the `store.observeManually` function. `observeManually` returns a `Subscription` class which has an `unsubscribe` method. Calling `unsubscribe` removes the observer.
+
+## 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/state/build.gradle b/mobile/android/android-components/components/lib/state/build.gradle
new file mode 100644
index 0000000000..4424fce465
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/build.gradle
@@ -0,0 +1,69 @@
+/* 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/. */
+
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+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'
+ }
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.compose_compiler
+ }
+
+ namespace 'mozilla.components.lib.state'
+}
+
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions.freeCompilerArgs += [
+ "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
+ ]
+}
+
+dependencies {
+ implementation platform(ComponentsDependencies.androidx_compose_bom)
+ implementation ComponentsDependencies.kotlin_coroutines
+ implementation ComponentsDependencies.androidx_fragment
+ implementation ComponentsDependencies.androidx_compose_ui
+ implementation ComponentsDependencies.androidx_lifecycle_process
+
+ implementation project(':support-base')
+ implementation project(':support-ktx')
+
+ testImplementation platform(ComponentsDependencies.androidx_compose_bom)
+ testImplementation project(':support-test')
+
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.androidx_compose_ui_test
+ testImplementation ComponentsDependencies.testing_robolectric
+ testImplementation ComponentsDependencies.testing_coroutines
+
+ androidTestImplementation ComponentsDependencies.androidx_test_junit
+ androidTestImplementation ComponentsDependencies.androidx_compose_ui_test_manifest
+ androidTestImplementation ComponentsDependencies.androidx_compose_ui_test
+}
+
+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/state/proguard-rules.pro b/mobile/android/android-components/components/lib/state/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/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/state/src/androidTest/java/mozilla/components/lib/state/ext/ComposeExtensionsKtTest.kt b/mobile/android/android-components/components/lib/state/src/androidTest/java/mozilla/components/lib/state/ext/ComposeExtensionsKtTest.kt
new file mode 100644
index 0000000000..a97ceebe0d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/androidTest/java/mozilla/components/lib/state/ext/ComposeExtensionsKtTest.kt
@@ -0,0 +1,182 @@
+/* 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.state.ext
+
+import androidx.compose.ui.test.junit4.createComposeRule
+import kotlinx.coroutines.runBlocking
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.State
+import mozilla.components.lib.state.Store
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+
+class ComposeExtensionsKtTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun usingInitialValue() {
+ val store = Store(
+ initialState = TestState(counter = 42),
+ reducer = ::reducer,
+ )
+
+ var value: Int? = null
+
+ rule.setContent {
+ val composeState = store.observeAsComposableState { state -> state.counter * 2 }
+ value = composeState.value
+ }
+
+ assertEquals(84, value)
+ }
+
+ @Test
+ fun receivingUpdates() {
+ val store = Store(
+ initialState = TestState(counter = 42),
+ reducer = ::reducer,
+ )
+
+ var value: Int? = null
+
+ rule.setContent {
+ val composeState = store.observeAsComposableState { state -> state.counter * 2 }
+ value = composeState.value
+ }
+
+ store.dispatchBlockingOnIdle(TestAction.IncrementAction)
+
+ rule.runOnIdle {
+ assertEquals(86, value)
+ }
+ }
+
+ @Test
+ fun usingInitialValueWithUpdates() {
+ val loading = "Loading"
+ val content = "Content"
+ val store = Store(
+ initialState = TestState(counter = 0),
+ reducer = ::reducer,
+ )
+
+ val value = mutableListOf<String>()
+
+ rule.setContent {
+ val composeState = store.observeAsState(
+ initialValue = loading,
+ map = { if (it.counter < 5) loading else content },
+ )
+ value.add(composeState.value)
+ }
+
+ rule.runOnIdle {
+ // Initial value when counter is 0.
+ assertEquals(listOf("Loading"), value)
+ }
+
+ store.dispatchBlockingOnIdle(TestAction.IncrementAction)
+ store.dispatchBlockingOnIdle(TestAction.IncrementAction)
+ store.dispatchBlockingOnIdle(TestAction.IncrementAction)
+ store.dispatchBlockingOnIdle(TestAction.IncrementAction)
+
+ rule.runOnIdle {
+ // Value after 4 increments, aka counter is 4. Note that it doesn't recompose here
+ // as the mapped value has stayed the same. We have 1 item in the list and not 5.
+ assertEquals(listOf(loading), value)
+ }
+
+ // 5th increment
+ store.dispatchBlockingOnIdle(TestAction.IncrementAction)
+
+ rule.runOnIdle {
+ assertEquals(listOf(loading, content), value)
+ assertEquals(content, value.last())
+ }
+ }
+
+ @Test
+ fun receivingUpdatesForPartialStateUpdateOnly() {
+ val store = Store(
+ initialState = TestState(counter = 42),
+ reducer = ::reducer,
+ )
+
+ var value: Int? = null
+
+ rule.setContent {
+ val composeState = store.observeAsComposableState(
+ map = { state -> state.counter * 2 },
+ observe = { state -> state.text },
+ )
+ value = composeState.value
+ }
+
+ assertEquals(84, value)
+
+ store.dispatchBlockingOnIdle(TestAction.IncrementAction)
+
+ rule.runOnIdle {
+ // State value didn't change because value returned by `observer` function did not change
+ assertEquals(84, value)
+ }
+
+ store.dispatchBlockingOnIdle(TestAction.SetTextAction("Hello World"))
+
+ rule.runOnIdle {
+ // Now, after the value from the observer function changed, we are seeing the new value
+ assertEquals(86, value)
+ }
+
+ store.dispatchBlockingOnIdle(TestAction.SetValueAction(23))
+
+ rule.runOnIdle {
+ // Observer function result is the same, no state update
+ assertEquals(86, value)
+ }
+
+ store.dispatchBlockingOnIdle(TestAction.SetTextAction("Hello World"))
+
+ rule.runOnIdle {
+ // Text was updated to the same value, observer function result is the same, no state update
+ assertEquals(86, value)
+ }
+
+ store.dispatchBlockingOnIdle(TestAction.SetTextAction("Hello World Again"))
+
+ rule.runOnIdle {
+ // Now, after the value from the observer function changed, we are seeing the new value
+ assertEquals(46, value)
+ }
+ }
+
+ private fun Store<TestState, TestAction>.dispatchBlockingOnIdle(action: TestAction) {
+ rule.runOnIdle {
+ val job = dispatch(action)
+ runBlocking { job.join() }
+ }
+ }
+}
+
+fun reducer(state: TestState, action: TestAction): TestState = when (action) {
+ is TestAction.IncrementAction -> state.copy(counter = state.counter + 1)
+ is TestAction.DecrementAction -> state.copy(counter = state.counter - 1)
+ is TestAction.SetValueAction -> state.copy(counter = action.value)
+ is TestAction.SetTextAction -> state.copy(text = action.text)
+}
+
+data class TestState(
+ val counter: Int,
+ val text: String = "",
+) : State
+
+sealed class TestAction : Action {
+ object IncrementAction : TestAction()
+ object DecrementAction : TestAction()
+ data class SetValueAction(val value: Int) : TestAction()
+ data class SetTextAction(val text: String) : TestAction()
+}
diff --git a/mobile/android/android-components/components/lib/state/src/main/AndroidManifest.xml b/mobile/android/android-components/components/lib/state/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..aa9d1077cc
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/AndroidManifest.xml
@@ -0,0 +1,8 @@
+<!-- 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/. -->
+<manifest>
+
+ <application />
+
+</manifest>
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Action.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Action.kt
new file mode 100644
index 0000000000..e371ac8929
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Action.kt
@@ -0,0 +1,14 @@
+/* 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.state
+
+/**
+ * Generic interface for actions to be dispatched on a [Store].
+ *
+ * Actions are used to send data from the application to a [Store]. The [Store] will use the [Action] to
+ * derive a new [State]. Actions should describe what happened, while [Reducer]s will describe how the
+ * state changes.
+ */
+interface Action
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/DelicateAction.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/DelicateAction.kt
new file mode 100644
index 0000000000..18f48b8483
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/DelicateAction.kt
@@ -0,0 +1,23 @@
+/* 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.state
+
+/**
+ *
+ * Marks an [Action] in the [Store] that are **delicate** &mdash;
+ * they have limited use-case and shall ve used with care in general code.
+ * Any use of a delicate declaration has to be carefully reviewed to make sure it is
+ * properly used and is not used for non-debugging or testing purposes.
+ * Carefully read documentation of any declaration marked as `DelicateAction`.
+ */
+@MustBeDocumented
+@Retention(value = AnnotationRetention.BINARY)
+@RequiresOptIn(
+ level = RequiresOptIn.Level.WARNING,
+ message = "This is a delicate Action and should only be used for situations that require debugging or testing." +
+ " Make sure you fully read and understand documentation of the action that is marked as a delicate Action.",
+)
+@Target(AnnotationTarget.CLASS)
+public annotation class DelicateAction
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Middleware.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Middleware.kt
new file mode 100644
index 0000000000..777b8cb77b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Middleware.kt
@@ -0,0 +1,53 @@
+/* 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.state
+
+/**
+ * A [Middleware] sits between the store and the reducer. It provides an extension point between
+ * dispatching an action, and the moment it reaches the reducer.
+ *
+ * A [Middleware] can rewrite an [Action], it can intercept an [Action], dispatch additional
+ * [Action]s or perform side-effects when an [Action] gets dispatched.
+ *
+ * The [Store] will create a chain of [Middleware] instances and invoke them in order. Every
+ * [Middleware] can decide to continue the chain (by calling `next`), intercept the chain (by not
+ * invoking `next`). A [Middleware] has no knowledge of what comes before or after it in the chain.
+ */
+typealias Middleware<S, A> = (context: MiddlewareContext<S, A>, next: (A) -> Unit, action: A) -> Unit
+
+/**
+ * The context a Middleware is running in. Allows access to privileged [Store] functionality. It is
+ * passed to a [Middleware] with every [Action].
+ *
+ * Note that the [MiddlewareContext] should not be passed to other components and calling methods
+ * on non-[Store] threads may throw an exception. Instead the value of the [store] property, granting
+ * access to the underlying store, can safely be used outside of the middleware.
+ */
+interface MiddlewareContext<S : State, A : Action> {
+ /**
+ * Returns the current state of the [Store].
+ */
+ val state: S
+
+ /**
+ * Dispatches an [Action] synchronously on the [Store]. Other than calling [Store.dispatch], this
+ * will block and return after all [Store] observers have been notified about the state change.
+ * The dispatched [Action] will go through the whole chain of middleware again.
+ *
+ * This method is particular useful if a middleware wants to dispatch an additional [Action] and
+ * wait until the [state] has been updated to further process it.
+ *
+ * Note that this method should only ever be called from a [Middleware] and the calling thread.
+ * Calling it from another thread may throw an exception. For dispatching an [Action] from
+ * asynchronous code in the [Middleware] or another component use [store] which returns a
+ * reference to the underlying [Store] that offers methods for asynchronous dispatching.
+ */
+ fun dispatch(action: A)
+
+ /**
+ * Returns a reference to the [Store] the [Middleware] is running in.
+ */
+ val store: Store<S, A>
+}
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Observer.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Observer.kt
new file mode 100644
index 0000000000..c5a68d8c17
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Observer.kt
@@ -0,0 +1,10 @@
+/* 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.state
+
+/**
+ * Listener called when the state changes in the [Store].
+ */
+typealias Observer<S> = (S) -> Unit
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Reducer.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Reducer.kt
new file mode 100644
index 0000000000..0cfcf76bb6
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Reducer.kt
@@ -0,0 +1,13 @@
+/* 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.state
+
+/**
+ * Reducers specify how the application's [State] changes in response to [Action]s sent to the [Store].
+ *
+ * Remember that actions only describe what happened, but don't describe how the application's state changes.
+ * Reducers will commonly consist of a `when` statement returning different copies of the [State].
+ */
+typealias Reducer<S, A> = (S, A) -> S
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/State.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/State.kt
new file mode 100644
index 0000000000..3318ddffe8
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/State.kt
@@ -0,0 +1,10 @@
+/* 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.state
+
+/**
+ * Generic interface for a [State] maintained by a [Store].
+ */
+interface State
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Store.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Store.kt
new file mode 100644
index 0000000000..025880d780
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/Store.kt
@@ -0,0 +1,187 @@
+/* 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.state
+
+import android.os.Handler
+import android.os.Looper
+import androidx.annotation.CheckResult
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import mozilla.components.lib.state.internal.ReducerChainBuilder
+import mozilla.components.lib.state.internal.StoreThreadFactory
+import java.lang.ref.WeakReference
+import java.util.Collections
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.Executors
+
+/**
+ * A generic store holding an immutable [State].
+ *
+ * The [State] can only be modified by dispatching [Action]s which will create a new state and notify all registered
+ * [Observer]s.
+ *
+ * @param initialState The initial state until a dispatched [Action] creates a new state.
+ * @param reducer A function that gets the current [State] and [Action] passed in and will return a new [State].
+ * @param middleware Optional list of [Middleware] sitting between the [Store] and the [Reducer].
+ * @param threadNamePrefix Optional prefix with which to name threads for the [Store]. If not provided,
+ * the naming scheme will be deferred to [Executors.defaultThreadFactory]
+ */
+open class Store<S : State, A : Action>(
+ initialState: S,
+ reducer: Reducer<S, A>,
+ middleware: List<Middleware<S, A>> = emptyList(),
+ threadNamePrefix: String? = null,
+) {
+ private val threadFactory = StoreThreadFactory(threadNamePrefix)
+ private val dispatcher = Executors.newSingleThreadExecutor(threadFactory).asCoroutineDispatcher()
+ private val reducerChainBuilder = ReducerChainBuilder(threadFactory, reducer, middleware)
+ private val scope = CoroutineScope(dispatcher)
+
+ @VisibleForTesting
+ internal val subscriptions = Collections.newSetFromMap(ConcurrentHashMap<Subscription<S, A>, Boolean>())
+ private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
+ // We want exceptions in the reducer to crash the app and not get silently ignored. Therefore we rethrow the
+ // exception on the main thread.
+ Handler(Looper.getMainLooper()).postAtFrontOfQueue {
+ throw StoreException("Exception while reducing state", throwable)
+ }
+
+ // Once an exception happened we do not want to accept any further actions. So let's cancel the scope which
+ // will cancel all jobs and not accept any new ones.
+ scope.cancel()
+ }
+ private val dispatcherWithExceptionHandler = dispatcher + exceptionHandler
+
+ @Volatile private var currentState = initialState
+
+ /**
+ * The current [State].
+ */
+ val state: S
+ get() = currentState
+
+ /**
+ * Registers an [Observer] function that will be invoked whenever the [State] changes.
+ *
+ * It's the responsibility of the caller to keep track of the returned [Subscription] and call
+ * [Subscription.unsubscribe] to stop observing and avoid potentially leaking memory by keeping an unused [Observer]
+ * registered. It's is recommend to use one of the `observe` extension methods that unsubscribe automatically.
+ *
+ * The created [Subscription] is in paused state until explicitly resumed by calling [Subscription.resume].
+ * While paused the [Subscription] will not receive any state updates. Once resumed the [observer]
+ * will get invoked immediately with the latest state.
+ *
+ * @return A [Subscription] object that can be used to unsubscribe from further state changes.
+ */
+ @CheckResult(suggest = "observe")
+ @Synchronized
+ fun observeManually(observer: Observer<S>): Subscription<S, A> {
+ val subscription = Subscription(observer, store = this)
+ subscriptions.add(subscription)
+
+ return subscription
+ }
+
+ /**
+ * Dispatch an [Action] to the store in order to trigger a [State] change.
+ */
+ fun dispatch(action: A) = scope.launch(dispatcherWithExceptionHandler) {
+ synchronized(this@Store) {
+ reducerChainBuilder.get(this@Store).invoke(action)
+ }
+ }
+
+ /**
+ * Transitions from the current [State] to the passed in [state] and notifies all observers.
+ */
+ internal fun transitionTo(state: S) {
+ if (state == currentState) {
+ // Nothing has changed.
+ return
+ }
+
+ currentState = state
+ subscriptions.forEach { subscription -> subscription.dispatch(state) }
+ }
+
+ private fun removeSubscription(subscription: Subscription<S, A>) {
+ subscriptions.remove(subscription)
+ }
+
+ /**
+ * A [Subscription] is returned whenever an observer is registered via the [observeManually] method. Calling
+ * [unsubscribe] on the [Subscription] will unregister the observer.
+ */
+ class Subscription<S : State, A : Action> internal constructor(
+ internal val observer: Observer<S>,
+ store: Store<S, A>,
+ ) {
+ private val storeReference = WeakReference(store)
+ internal var binding: Binding? = null
+ private var active = false
+
+ /**
+ * Resumes the [Subscription]. The [Observer] will get notified for every state change.
+ * Additionally it will get invoked immediately with the latest state.
+ */
+ @Synchronized
+ fun resume() {
+ active = true
+
+ storeReference.get()?.state?.let(observer)
+ }
+
+ /**
+ * Pauses the [Subscription]. The [Observer] will not get notified when the state changes
+ * until [resume] is called.
+ */
+ @Synchronized
+ fun pause() {
+ active = false
+ }
+
+ /**
+ * Notifies this subscription's observer of a state change.
+ *
+ * @param state the updated state.
+ */
+ @Synchronized
+ internal fun dispatch(state: S) {
+ if (active) {
+ observer.invoke(state)
+ }
+ }
+
+ /**
+ * Unsubscribe from the [Store].
+ *
+ * Calling this method will clear all references and the subscription will not longer be
+ * active.
+ */
+ @Synchronized
+ fun unsubscribe() {
+ active = false
+
+ storeReference.get()?.removeSubscription(this)
+ storeReference.clear()
+
+ binding?.unbind()
+ }
+
+ interface Binding {
+ fun unbind()
+ }
+ }
+}
+
+/**
+ * Exception for otherwise unhandled errors caught while reducing state or
+ * while managing/notifying observers.
+ */
+class StoreException(msg: String, val e: Throwable? = null) : Exception(msg, e)
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/ComposeExtensions.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/ComposeExtensions.kt
new file mode 100644
index 0000000000..e4633b6239
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/ComposeExtensions.kt
@@ -0,0 +1,147 @@
+/* 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.state.ext
+
+import android.os.Parcelable
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.State
+import mozilla.components.lib.state.Store
+import androidx.compose.runtime.State as ComposeState
+
+/**
+ * Starts observing this [Store] and represents the mapped state (using [map]) via [ComposeState].
+ *
+ * Every time the mapped [Store] state changes, the returned [ComposeState] will be updated causing
+ * recomposition of every [ComposeState.value] usage.
+ *
+ * The [Store] observer will automatically be removed when this composable disposes or the current
+ * [LifecycleOwner] moves to the [Lifecycle.State.DESTROYED] state.
+ */
+@Composable
+fun <S : State, A : Action, R> Store<S, A>.observeAsComposableState(map: (S) -> R): ComposeState<R?> {
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val state = remember { mutableStateOf<R?>(map(state)) }
+
+ DisposableEffect(this, lifecycleOwner) {
+ val subscription = observe(lifecycleOwner) { browserState ->
+ state.value = map(browserState)
+ }
+ onDispose { subscription?.unsubscribe() }
+ }
+
+ return state
+}
+
+/**
+ * Starts observing this [Store] and represents the mapped state (using [map]) via [ComposeState].
+ *
+ * Every time the mapped [Store] state changes, the returned [ComposeState] will be updated causing
+ * recomposition of every [ComposeState.value] usage.
+ *
+ * The [Store] observer will automatically be removed when this composable disposes or the current
+ * [LifecycleOwner] moves to the [Lifecycle.State.DESTROYED] state.
+ *
+ * @param initialValue Initial value emitted.
+ * @param map The applied function to produced the mapped value [R] from [S].
+ * @return A non nullable [ComposeState], making the api more reasonable for callers where the
+ * state is non null.
+ */
+@Composable
+fun <S : State, A : Action, R> Store<S, A>.observeAsState(
+ initialValue: R,
+ map: (S) -> R,
+): ComposeState<R> {
+ val lifecycleOwner = LocalLifecycleOwner.current
+
+ return produceState(initialValue = initialValue) {
+ val subscription = observe(lifecycleOwner) { browserState ->
+ value = map(browserState)
+ }
+ awaitDispose { subscription?.unsubscribe() }
+ }
+}
+
+/**
+ * Starts observing this [Store] and represents the mapped state (using [map]) via [ComposeState].
+ *
+ * Everytime the [Store] state changes and the result of the [observe] function changes for this
+ * state, the returned [ComposeState] will be updated causing recomposition of every
+ * [ComposeState.value] usage.
+ *
+ * The [Store] observer will automatically be removed when this composable disposes or the current
+ * [LifecycleOwner] moves to the [Lifecycle.State.DESTROYED] state.
+ */
+@Composable
+fun <S : State, A : Action, O, R> Store<S, A>.observeAsComposableState(
+ observe: (S) -> O,
+ map: (S) -> R,
+): ComposeState<R?> {
+ val lifecycleOwner = LocalLifecycleOwner.current
+ var lastValue = observe(state)
+ val state = remember { mutableStateOf<R?>(map(state)) }
+
+ DisposableEffect(this, lifecycleOwner) {
+ val subscription = observe(lifecycleOwner) { browserState ->
+ val newValue = observe(browserState)
+ if (newValue != lastValue) {
+ state.value = map(browserState)
+ lastValue = newValue
+ }
+ }
+ onDispose { subscription?.unsubscribe() }
+ }
+
+ return state
+}
+
+/**
+ * Helper for creating a [Store] scoped to a `@Composable` and whose [State] gets saved and restored
+ * on process recreation.
+ */
+@Composable
+inline fun <reified S : State, A : Action> composableStore(
+ crossinline save: (S) -> Parcelable = { state ->
+ if (state is Parcelable) {
+ state
+ } else {
+ throw NotImplementedError(
+ "State of store does not implement Parcelable. Either implement Parcelable or pass " +
+ "custom save function to composableStore()",
+ )
+ }
+ },
+ crossinline restore: (Parcelable) -> S = { parcelable ->
+ if (parcelable is S) {
+ parcelable
+ } else {
+ throw NotImplementedError(
+ "Restored parcelable is not of same class as state. Either the state needs to " +
+ "implement Parcelable or you need to provide a custom restore function to composableStore()",
+ )
+ }
+ },
+ crossinline init: (S?) -> Store<S, A>,
+): Store<S, A> {
+ return rememberSaveable(
+ saver = Saver(
+ save = { store -> save(store.state) },
+ restore = { parcelable ->
+ val state = restore(parcelable)
+ init(state)
+ },
+ ),
+ init = { init(null) },
+ )
+}
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/Fragment.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/Fragment.kt
new file mode 100644
index 0000000000..0eacb1de04
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/Fragment.kt
@@ -0,0 +1,105 @@
+/* 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.state.ext
+
+import android.view.View
+import androidx.annotation.MainThread
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import kotlinx.coroutines.channels.consumeEach
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.launch
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.State
+import mozilla.components.lib.state.Store
+import mozilla.components.support.ktx.android.view.toScope
+
+/**
+ * Helper extension method for consuming [State] from a [Store] sequentially in order inside a
+ * [Fragment]. The [block] function will get invoked for every [State] update.
+ *
+ * This helper will automatically stop observing the [Store] once the [View] of the [Fragment] gets
+ * detached. The fragment's lifecycle will be used to determine when to resume/pause observing the
+ * [Store].
+ */
+@MainThread
+fun <S : State, A : Action> Fragment.consumeFrom(store: Store<S, A>, block: (S) -> Unit) {
+ val fragment = this
+ val view = checkNotNull(view) { "Fragment has no view yet. Call from onViewCreated()." }
+
+ val scope = view.toScope()
+ val channel = store.channel(owner = this)
+
+ scope.launch {
+ channel.consumeEach { state ->
+ // We are using a scope that is bound to the view being attached here. It can happen
+ // that the "view detached" callback gets executed *after* the fragment was detached. If
+ // a `consumeFrom` runs in exactly this moment then we run inside a detached fragment
+ // without a `Context` and this can cause a variety of issues/crashes.
+ // See: https://github.com/mozilla-mobile/android-components/issues/4125
+ //
+ // To avoid this, we check whether the fragment still has an activity and a view
+ // attached. If not then we run in exactly that moment between fragment detach and view
+ // detach. It would be better if we could use `viewLifecycleOwner` which is bound to
+ // onCreateView() and onDestroyView() of the fragment. But:
+ // - `viewLifecycleOwner` is only available in alpha versions of AndroidX currently.
+ // - We found a bug where `viewLifecycleOwner.lifecycleScope` is not getting cancelled
+ // causing this coroutine to run forever.
+ // See: https://github.com/mozilla-mobile/android-components/issues/3828
+ // Once those two issues get resolved we can remove the `isAdded` check and use
+ // `viewLifecycleOwner.lifecycleScope` instead of the view scope.
+ //
+ // In a previous version we tried using `isAdded` and `isDetached` here. But in certain
+ // situations they reported true/false in situations where no activity was attached to
+ // the fragment. Therefore we switched to explicitly check for the activity and view here.
+ if (fragment.activity != null && fragment.view != null) {
+ block(state)
+ }
+ }
+ }
+}
+
+/**
+ * Helper extension method for consuming [State] from a [Store] as a [Flow].
+ *
+ * The lifetime of the coroutine scope the [Flow] is launched in, and [block] is executed in, is
+ * bound to the [View] of the [Fragment]. Once the [View] gets detached, the coroutine scope will
+ * automatically be cancelled and no longer observe the [Store].
+ *
+ * An optional [LifecycleOwner] can be passed to this method. It will be used to automatically pause
+ * and resume the [Store] subscription. With that an application can, for example, automatically
+ * stop updating the UI if the application is in the background. Once the [Lifecycle] switches back
+ * to at least STARTED state then the latest [State] and further will be passed to the [Flow] again.
+ * By default, the fragment itself is used as a [LifecycleOwner].
+ */
+@MainThread
+fun <S : State, A : Action> Fragment.consumeFlow(
+ from: Store<S, A>,
+ owner: LifecycleOwner? = this,
+ block: suspend (Flow<S>) -> Unit,
+) {
+ val fragment = this
+ val view = checkNotNull(view) { "Fragment has no view yet. Call from onViewCreated()." }
+
+ // It's important to create the flow here directly instead of in the coroutine below,
+ // as otherwise the fragment could be removed before the subscription is created.
+ // This would cause us to create an unnecessary subscription leaking the fragment,
+ // as we only unsubscribe on destroy which already happened.
+ val flow = from.flow(owner)
+
+ val scope = view.toScope()
+ scope.launch {
+ val filtered = flow.filter {
+ // We ignore state updates if the fragment does not have an activity or view
+ // attached anymore.
+ // See comment in [consumeFrom] above.
+ fragment.activity != null && fragment.view != null
+ }
+
+ block(filtered)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/StoreExtensions.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/StoreExtensions.kt
new file mode 100644
index 0000000000..8250bf1376
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/StoreExtensions.kt
@@ -0,0 +1,265 @@
+/* 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.state.ext
+
+import android.view.View
+import androidx.annotation.MainThread
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.ReceiveChannel
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.buffer
+import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.Observer
+import mozilla.components.lib.state.State
+import mozilla.components.lib.state.Store
+
+/**
+ * Registers an [Observer] function that will be invoked whenever the state changes. The [Store.Subscription]
+ * will be bound to the passed in [LifecycleOwner]. Once the [Lifecycle] state changes to DESTROYED the [Observer] will
+ * be unregistered automatically.
+ *
+ * The [Observer] will get invoked with the current [State] as soon as the [Lifecycle] is in STARTED
+ * state.
+ */
+@MainThread
+fun <S : State, A : Action> Store<S, A>.observe(
+ owner: LifecycleOwner,
+ observer: Observer<S>,
+): Store.Subscription<S, A>? {
+ if (owner.lifecycle.currentState == Lifecycle.State.DESTROYED) {
+ // This owner is already destroyed. No need to register.
+ return null
+ }
+
+ val subscription = observeManually(observer)
+
+ subscription.binding = SubscriptionLifecycleBinding(owner, subscription).apply {
+ owner.lifecycle.addObserver(this)
+ }
+
+ return subscription
+}
+
+/**
+ * Registers an [Observer] function that will be invoked whenever the state changes. The [Store.Subscription]
+ * will be bound to the passed in [View]. Once the [View] gets detached the [Observer] will be unregistered
+ * automatically.
+ *
+ * Note that inside a `Fragment` using [observe] with a `viewLifecycleOwner` may be a better option.
+ * Only use this implementation if you have only access to a [View] - especially if it can exist
+ * outside of a `Fragment`.
+ *
+ * The [Observer] will get invoked with the current [State] as soon as [View] is attached.
+ *
+ * Once the [View] gets detached the [Observer] will get unregistered. It will NOT get automatically
+ * registered again if the same [View] gets attached again.
+ */
+@MainThread
+fun <S : State, A : Action> Store<S, A>.observe(
+ view: View,
+ observer: Observer<S>,
+) {
+ val subscription = observeManually(observer)
+
+ subscription.binding = SubscriptionViewBinding(view, subscription).apply {
+ view.addOnAttachStateChangeListener(this)
+ }
+
+ if (view.isAttachedToWindow) {
+ // This View is already attached. We can resume immediately and do not need to wait for
+ // onViewAttachedToWindow() getting called.
+ subscription.resume()
+ }
+}
+
+/**
+ * Registers an [Observer] function that will observe the store indefinitely.
+ *
+ * Right after registering the [Observer] will be invoked with the current [State].
+ */
+fun <S : State, A : Action> Store<S, A>.observeForever(
+ observer: Observer<S>,
+) {
+ observeManually(observer).resume()
+}
+
+/**
+ * Creates a conflated [Channel] for observing [State] changes in the [Store].
+ *
+ * The advantage of a [Channel] is that [State] changes can be processed sequentially in order from
+ * a single coroutine (e.g. on the main thread).
+ *
+ * @param owner A [LifecycleOwner] that will be used to determine when to pause and resume the store
+ * subscription. When the [Lifecycle] is in STOPPED state then no [State] will be received. Once the
+ * [Lifecycle] switches back to at least STARTED state then the latest [State] and further updates
+ * will be received.
+ */
+@ExperimentalCoroutinesApi
+@MainThread
+fun <S : State, A : Action> Store<S, A>.channel(
+ owner: LifecycleOwner = ProcessLifecycleOwner.get(),
+): ReceiveChannel<S> {
+ if (owner.lifecycle.currentState == Lifecycle.State.DESTROYED) {
+ // This owner is already destroyed. No need to register.
+ throw IllegalArgumentException("Lifecycle is already DESTROYED")
+ }
+
+ val channel = Channel<S>(Channel.CONFLATED)
+
+ val subscription = observeManually { state ->
+ runBlocking {
+ try {
+ channel.send(state)
+ } catch (e: CancellationException) {
+ // It's possible for this channel to have been closed concurrently before
+ // we had a chance to unsubscribe. In this case we can just ignore this
+ // one subscription and keep going.
+ }
+ }
+ }
+
+ subscription.binding = SubscriptionLifecycleBinding(owner, subscription).apply {
+ owner.lifecycle.addObserver(this)
+ }
+
+ channel.invokeOnClose { subscription.unsubscribe() }
+
+ return channel
+}
+
+/**
+ * Creates a [Flow] for observing [State] changes in the [Store].
+ *
+ * @param owner An optional [LifecycleOwner] that will be used to determine when to pause and resume
+ * the store subscription. When the [Lifecycle] is in STOPPED state then no [State] will be received.
+ * Once the [Lifecycle] switches back to at least STARTED state then the latest [State] and further
+ * updates will be emitted.
+ */
+@MainThread
+fun <S : State, A : Action> Store<S, A>.flow(
+ owner: LifecycleOwner? = null,
+): Flow<S> {
+ var destroyed = owner?.lifecycle?.currentState == Lifecycle.State.DESTROYED
+ val ownerDestroyedObserver = object : DefaultLifecycleObserver {
+ override fun onDestroy(owner: LifecycleOwner) {
+ destroyed = true
+ }
+ }
+ owner?.lifecycle?.addObserver(ownerDestroyedObserver)
+
+ return channelFlow {
+ // By the time this block executes the fragment or view could already be destroyed
+ // so we exit early to avoid creating an unnecessary subscription. This is important
+ // as otherwise we'd be leaking the owner via the subscription because we only
+ // unsubscribe on destroy which already happened.
+ if (destroyed) {
+ return@channelFlow
+ }
+
+ owner?.lifecycle?.removeObserver(ownerDestroyedObserver)
+
+ val subscription = observeManually { state ->
+ runBlocking {
+ try {
+ send(state)
+ } catch (e: CancellationException) {
+ // It's possible for this channel to have been closed concurrently before
+ // we had a chance to unsubscribe. In this case we can just ignore this
+ // one subscription and keep going.
+ }
+ }
+ }
+
+ if (owner == null) {
+ subscription.resume()
+ } else {
+ subscription.binding = SubscriptionLifecycleBinding(owner, subscription).apply {
+ owner.lifecycle.addObserver(this)
+ }
+ }
+
+ awaitClose {
+ subscription.unsubscribe()
+ }
+ }.buffer(Channel.CONFLATED)
+}
+
+/**
+ * Launches a coroutine in a new [MainScope] and creates a [Flow] for observing [State] changes in
+ * the [Store] in that scope. Invokes [block] inside that scope and passes the [Flow] to it.
+ *
+ * @param owner An optional [LifecycleOwner] that will be used to determine when to pause and resume
+ * the store subscription. When the [Lifecycle] is in STOPPED state then no [State] will be received.
+ * Once the [Lifecycle] switches back to at least STARTED state then the latest [State] and further
+ * updates will be emitted.
+ * @return The [CoroutineScope] [block] is getting executed in.
+ */
+@MainThread
+fun <S : State, A : Action> Store<S, A>.flowScoped(
+ owner: LifecycleOwner? = null,
+ block: suspend (Flow<S>) -> Unit,
+): CoroutineScope {
+ return MainScope().apply {
+ launch {
+ block(flow(owner))
+ }
+ }
+}
+
+/**
+ * GenericLifecycleObserver implementation to bind an observer to a Lifecycle.
+ */
+private class SubscriptionLifecycleBinding<S : State, A : Action>(
+ private val owner: LifecycleOwner,
+ private val subscription: Store.Subscription<S, A>,
+) : DefaultLifecycleObserver, Store.Subscription.Binding {
+ override fun onStart(owner: LifecycleOwner) {
+ subscription.resume()
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ subscription.pause()
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ subscription.unsubscribe()
+ }
+
+ override fun unbind() {
+ owner.lifecycle.removeObserver(this)
+ }
+}
+
+/**
+ * View.OnAttachStateChangeListener implementation to bind an observer to a View.
+ */
+private class SubscriptionViewBinding<S : State, A : Action>(
+ private val view: View,
+ private val subscription: Store.Subscription<S, A>,
+) : View.OnAttachStateChangeListener, Store.Subscription.Binding {
+ override fun onViewAttachedToWindow(v: View) {
+ subscription.resume()
+ }
+
+ override fun onViewDetachedFromWindow(view: View) {
+ subscription.unsubscribe()
+ }
+
+ override fun unbind() {
+ view.removeOnAttachStateChangeListener(this)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/View.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/View.kt
new file mode 100644
index 0000000000..aafe7d1ed9
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/View.kt
@@ -0,0 +1,39 @@
+/* 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.state.ext
+
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.LifecycleOwner
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.consumeEach
+import kotlinx.coroutines.launch
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.State
+import mozilla.components.lib.state.Store
+import mozilla.components.support.ktx.android.view.toScope
+
+/**
+ * Helper extension method for consuming [State] from a [Store] sequentially in order scoped to the
+ * lifetime of the [View]. The [block] function will get invoked for every [State] update.
+ *
+ * This helper will automatically stop observing the [Store] once the [View] gets detached. The
+ * provided [LifecycleOwner] is used to determine when observing should be stopped or resumed.
+ *
+ * Inside a [Fragment] prefer to use [Fragment.consumeFrom].
+ */
+@ExperimentalCoroutinesApi // Channel
+fun <S : State, A : Action> View.consumeFrom(
+ store: Store<S, A>,
+ owner: LifecycleOwner,
+ block: (S) -> Unit,
+) {
+ val scope = toScope()
+ val channel = store.channel(owner)
+
+ scope.launch {
+ channel.consumeEach { state -> block(state) }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/helpers/AbstractBinding.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/helpers/AbstractBinding.kt
new file mode 100644
index 0000000000..10a0859192
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/helpers/AbstractBinding.kt
@@ -0,0 +1,44 @@
+/* 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.state.helpers
+
+import androidx.annotation.CallSuper
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.Flow
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.State
+import mozilla.components.lib.state.Store
+import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+
+/**
+ * Helper class for creating small binding classes that are responsible for reacting to state
+ * changes.
+ */
+@ExperimentalCoroutinesApi // Flow
+abstract class AbstractBinding<in S : State>(
+ private val store: Store<S, out Action>,
+) : LifecycleAwareFeature {
+ private var scope: CoroutineScope? = null
+
+ @CallSuper
+ override fun start() {
+ scope = store.flowScoped { flow ->
+ onState(flow)
+ }
+ }
+
+ @CallSuper
+ override fun stop() {
+ scope?.cancel()
+ }
+
+ /**
+ * A callback that is invoked when a [Flow] on the [store] is available to use.
+ */
+ abstract suspend fun onState(flow: Flow<S>)
+}
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/internal/ReducerChainBuilder.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/internal/ReducerChainBuilder.kt
new file mode 100644
index 0000000000..69ea7dd52b
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/internal/ReducerChainBuilder.kt
@@ -0,0 +1,67 @@
+/* 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.state.internal
+
+import mozilla.components.lib.state.Action
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.lib.state.Reducer
+import mozilla.components.lib.state.State
+import mozilla.components.lib.state.Store
+
+/**
+ * Builder to lazily create a function that will invoke the chain of [middleware] and finally the
+ * [reducer].
+ */
+internal class ReducerChainBuilder<S : State, A : Action>(
+ private val storeThreadFactory: StoreThreadFactory,
+ private val reducer: Reducer<S, A>,
+ private val middleware: List<Middleware<S, A>>,
+) {
+ private var chain: ((A) -> Unit)? = null
+
+ /**
+ * Returns a function that will invoke the chain of [middleware] and the [reducer] for the given
+ * [Store].
+ */
+ fun get(store: Store<S, A>): (A) -> Unit {
+ chain?.let { return it }
+
+ return build(store).also {
+ chain = it
+ }
+ }
+
+ private fun build(store: Store<S, A>): (A) -> Unit {
+ val context = object : MiddlewareContext<S, A> {
+ override val state: S
+ get() = store.state
+
+ override fun dispatch(action: A) {
+ get(store).invoke(action)
+ }
+
+ override val store: Store<S, A>
+ get() = store
+ }
+
+ var chain: (A) -> Unit = { action ->
+ val state = reducer(store.state, action)
+ store.transitionTo(state)
+ }
+
+ val threadCheck: Middleware<S, A> = { _, next, action ->
+ storeThreadFactory.assertOnThread()
+ next(action)
+ }
+
+ (middleware.reversed() + threadCheck).forEach { middleware ->
+ val next = chain
+ chain = { action -> middleware(context, next, action) }
+ }
+
+ return chain
+ }
+}
diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/internal/StoreThreadFactory.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/internal/StoreThreadFactory.kt
new file mode 100644
index 0000000000..fb9e53d7da
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/internal/StoreThreadFactory.kt
@@ -0,0 +1,58 @@
+/* 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.state.internal
+
+import mozilla.components.lib.state.Store
+import mozilla.components.support.base.utils.NamedThreadFactory
+import java.util.concurrent.Executors
+import java.util.concurrent.ThreadFactory
+
+/**
+ * Custom [ThreadFactory] implementation wrapping [Executors.defaultThreadFactory]/[NamedThreadFactory]
+ * that allows asserting whether a caller is on the created thread.
+ *
+ * For usage with [Executors.newSingleThreadExecutor]: Only the last created thread is kept and
+ * compared when [assertOnThread] is called.
+ *
+ * @param threadNamePrefix Optional prefix with which to name threads for the [Store]. If not provided,
+ * the naming scheme will be deferred to [Executors.defaultThreadFactory]
+ */
+internal class StoreThreadFactory(
+ threadNamePrefix: String?,
+) : ThreadFactory {
+ @Volatile
+ private var thread: Thread? = null
+
+ private val actualFactory = if (threadNamePrefix != null) {
+ NamedThreadFactory(threadNamePrefix)
+ } else {
+ Executors.defaultThreadFactory()
+ }
+
+ override fun newThread(r: Runnable): Thread {
+ return actualFactory.newThread(r).also {
+ thread = it
+ }
+ }
+
+ /**
+ * Asserts that the calling thread is the thread of this [StoreDispatcher]. Otherwise throws an
+ * [IllegalThreadStateException].
+ */
+ fun assertOnThread() {
+ val currentThread = Thread.currentThread()
+ val currentThreadId = currentThread.id
+ val expectedThreadId = thread?.id
+
+ if (currentThreadId == expectedThreadId) {
+ return
+ }
+
+ throw IllegalThreadStateException(
+ "Expected `store` thread, but running on thread `${currentThread.name}`. " +
+ "Leaked MiddlewareContext or did you mean to use `MiddlewareContext.store.dispatch`?",
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreExceptionTest.kt b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreExceptionTest.kt
new file mode 100644
index 0000000000..34adcf511d
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreExceptionTest.kt
@@ -0,0 +1,33 @@
+/* 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.state
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.ext.joinBlocking
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.shadows.ShadowLooper
+
+@RunWith(AndroidJUnit4::class)
+class StoreExceptionTest {
+ // This test is in a separate class because it needs to run with Robolectric (different runner, slower) while all
+ // other tests only need a Java VM (fast).
+ @Test(expected = StoreException::class)
+ fun `Exception in reducer will be rethrown on main thread`() {
+ val throwingReducer: (TestState, TestAction) -> TestState = { _, _ ->
+ throw IllegalStateException("Not reducing today")
+ }
+
+ val store = Store(TestState(counter = 23), throwingReducer)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+
+ // Wait for the main looper to process the re-thrown exception.
+ ShadowLooper.idleMainLooper()
+
+ Assert.fail()
+ }
+}
diff --git a/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreTest.kt b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreTest.kt
new file mode 100644
index 0000000000..715e3e55ba
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/StoreTest.kt
@@ -0,0 +1,311 @@
+/* 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.state
+
+import mozilla.components.support.test.ext.joinBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import java.io.IOException
+
+class StoreTest {
+ @Test
+ fun `Dispatching Action executes reducers and creates new State`() {
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+
+ assertEquals(24, store.state.counter)
+
+ store.dispatch(TestAction.DecrementAction).joinBlocking()
+ store.dispatch(TestAction.DecrementAction).joinBlocking()
+
+ assertEquals(22, store.state.counter)
+ }
+
+ @Test
+ fun `Observer gets notified about state changes`() {
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var observedValue = 0
+
+ store.observeManually { state -> observedValue = state.counter }.also {
+ it.resume()
+ }
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+
+ assertEquals(24, observedValue)
+ }
+
+ @Test
+ fun `Observer gets initial value before state changes`() {
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var observedValue = 0
+
+ store.observeManually { state -> observedValue = state.counter }.also {
+ it.resume()
+ }
+
+ assertEquals(23, observedValue)
+ }
+
+ @Test
+ fun `Observer does not get notified if state does not change`() {
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var stateChangeObserved = false
+
+ store.observeManually { stateChangeObserved = true }.also {
+ it.resume()
+ }
+
+ // Initial state observed
+ assertTrue(stateChangeObserved)
+ stateChangeObserved = false
+
+ store.dispatch(TestAction.DoNothingAction).joinBlocking()
+
+ assertFalse(stateChangeObserved)
+ }
+
+ @Test
+ fun `Observer does not get notified after unsubscribe`() {
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var observedValue = 0
+
+ val subscription = store.observeManually { state ->
+ observedValue = state.counter
+ }.also {
+ it.resume()
+ }
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+
+ assertEquals(24, observedValue)
+
+ store.dispatch(TestAction.DecrementAction).joinBlocking()
+
+ assertEquals(23, observedValue)
+
+ subscription.unsubscribe()
+
+ store.dispatch(TestAction.DecrementAction).joinBlocking()
+
+ assertEquals(23, observedValue)
+ assertEquals(22, store.state.counter)
+ }
+
+ @Test
+ fun `Middleware chain gets executed in order`() {
+ val incrementMiddleware: Middleware<TestState, TestAction> = { store, next, action ->
+ if (action == TestAction.DoNothingAction) {
+ store.dispatch(TestAction.IncrementAction)
+ }
+
+ next(action)
+ }
+
+ val doubleMiddleware: Middleware<TestState, TestAction> = { store, next, action ->
+ if (action == TestAction.DoNothingAction) {
+ store.dispatch(TestAction.DoubleAction)
+ }
+
+ next(action)
+ }
+
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ listOf(
+ incrementMiddleware,
+ doubleMiddleware,
+ ),
+ )
+
+ store.dispatch(TestAction.DoNothingAction).joinBlocking()
+
+ assertEquals(2, store.state.counter)
+
+ store.dispatch(TestAction.DoNothingAction).joinBlocking()
+
+ assertEquals(6, store.state.counter)
+
+ store.dispatch(TestAction.DoNothingAction).joinBlocking()
+
+ assertEquals(14, store.state.counter)
+
+ store.dispatch(TestAction.DecrementAction).joinBlocking()
+
+ assertEquals(13, store.state.counter)
+ }
+
+ @Test
+ fun `Middleware can intercept actions`() {
+ val interceptingMiddleware: Middleware<TestState, TestAction> = { _, _, _ ->
+ // Do nothing!
+ }
+
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ listOf(interceptingMiddleware),
+ )
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(0, store.state.counter)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(0, store.state.counter)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(0, store.state.counter)
+ }
+
+ @Test
+ fun `Middleware can rewrite actions`() {
+ val rewritingMiddleware: Middleware<TestState, TestAction> = { _, next, _ ->
+ next(TestAction.DecrementAction)
+ }
+
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ listOf(rewritingMiddleware),
+ )
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(-1, store.state.counter)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(-2, store.state.counter)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(-3, store.state.counter)
+ }
+
+ @Test
+ fun `Middleware can intercept and dispatch other action instead`() {
+ val rewritingMiddleware: Middleware<TestState, TestAction> = { store, next, action ->
+ if (action == TestAction.IncrementAction) {
+ store.dispatch(TestAction.DecrementAction)
+ } else {
+ next(action)
+ }
+ }
+
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ listOf(rewritingMiddleware),
+ )
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(-1, store.state.counter)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(-2, store.state.counter)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(-3, store.state.counter)
+ }
+
+ @Test
+ fun `Middleware sees state before and after reducing`() {
+ var countBefore = -1
+ var countAfter = -1
+
+ val observingMiddleware: Middleware<TestState, TestAction> = { store, next, action ->
+ countBefore = store.state.counter
+ next(action)
+ countAfter = store.state.counter
+ }
+
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ listOf(observingMiddleware),
+ )
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(0, countBefore)
+ assertEquals(1, countAfter)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(1, countBefore)
+ assertEquals(2, countAfter)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(2, countBefore)
+ assertEquals(3, countAfter)
+
+ store.dispatch(TestAction.DecrementAction).joinBlocking()
+ assertEquals(3, countBefore)
+ assertEquals(2, countAfter)
+ }
+
+ @Test
+ fun `Middleware can catch exceptions in reducer`() {
+ var caughtException: Exception? = null
+
+ val catchingMiddleware: Middleware<TestState, TestAction> = { _, next, action ->
+ try {
+ next(action)
+ } catch (e: Exception) {
+ caughtException = e
+ }
+ }
+
+ val store = Store(
+ TestState(counter = 0),
+ { _: State, _: Action -> throw IOException() },
+ listOf(catchingMiddleware),
+ )
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+
+ assertNotNull(caughtException)
+ assertTrue(caughtException is IOException)
+ }
+}
+
+fun reducer(state: TestState, action: TestAction): TestState = when (action) {
+ is TestAction.IncrementAction -> state.copy(counter = state.counter + 1)
+ is TestAction.DecrementAction -> state.copy(counter = state.counter - 1)
+ is TestAction.SetValueAction -> state.copy(counter = action.value)
+ is TestAction.DoubleAction -> state.copy(counter = state.counter * 2)
+ is TestAction.DoNothingAction -> state
+}
+
+data class TestState(
+ val counter: Int,
+) : State
+
+sealed class TestAction : Action {
+ object IncrementAction : TestAction()
+ object DecrementAction : TestAction()
+ object DoNothingAction : TestAction()
+ object DoubleAction : TestAction()
+ data class SetValueAction(val value: Int) : TestAction()
+}
diff --git a/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/FragmentKtTest.kt b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/FragmentKtTest.kt
new file mode 100644
index 0000000000..b86e4485f4
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/FragmentKtTest.kt
@@ -0,0 +1,301 @@
+/* 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.state.ext
+
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.setMain
+import mozilla.components.lib.state.Store
+import mozilla.components.lib.state.TestAction
+import mozilla.components.lib.state.TestState
+import mozilla.components.lib.state.reducer
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlin.coroutines.CoroutineContext
+
+@RunWith(AndroidJUnit4::class)
+@ExperimentalCoroutinesApi
+class FragmentKtTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ @Synchronized
+ fun `consumeFrom reads states from store`() {
+ val fragment = mock<Fragment>()
+ val view = mock<View>()
+ val owner = MockedLifecycleOwner(Lifecycle.State.INITIALIZED)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ val onAttachListener = argumentCaptor<View.OnAttachStateChangeListener>()
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+
+ doNothing().`when`(view).addOnAttachStateChangeListener(onAttachListener.capture())
+ doReturn(mock<FragmentActivity>()).`when`(fragment).activity
+ doReturn(view).`when`(fragment).view
+ doReturn(owner.lifecycle).`when`(fragment).lifecycle
+
+ fragment.consumeFrom(store) { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+
+ // Nothing received yet.
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Updating state: Nothing received yet.
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Switching to STARTED state: Receiving initial state
+ owner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ latch = CountDownLatch(1)
+
+ // View gets detached
+ onAttachListener.value.onViewDetachedFromWindow(view)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ }
+
+ @Test
+ @Synchronized
+ fun `consumeFrom does not run when fragment is detached`() {
+ val fragment = mock<Fragment>()
+ val view = mock<View>()
+ val owner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+
+ doReturn(mock<FragmentActivity>()).`when`(fragment).activity
+ doReturn(view).`when`(fragment).view
+ doReturn(owner.lifecycle).`when`(fragment).lifecycle
+
+ fragment.consumeFrom(store) { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(23, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+
+ doReturn(null).`when`(fragment).activity
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+
+ doReturn(mock<FragmentActivity>()).`when`(fragment).activity
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(28, receivedValue)
+ }
+
+ @Test
+ fun `consumeFlow - reads states from store`() {
+ val fragment = mock<Fragment>()
+ val view = mock<View>()
+ val owner = MockedLifecycleOwner(Lifecycle.State.INITIALIZED)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ val onAttachListener = argumentCaptor<View.OnAttachStateChangeListener>()
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+
+ doNothing().`when`(view).addOnAttachStateChangeListener(onAttachListener.capture())
+ doReturn(mock<FragmentActivity>()).`when`(fragment).activity
+ doReturn(view).`when`(fragment).view
+ doReturn(owner.lifecycle).`when`(fragment).lifecycle
+
+ fragment.consumeFlow(
+ from = store,
+ owner = owner,
+ ) { flow ->
+ flow.collect { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+ }
+
+ // Nothing received yet.
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Updating state: Nothing received yet.
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Switching to STARTED state: Receiving initial state
+ owner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ latch = CountDownLatch(1)
+
+ // View gets detached
+ onAttachListener.value.onViewDetachedFromWindow(view)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ }
+
+ @Test
+ fun `consumeFlow - uses fragment as lifecycle owner by default`() {
+ val fragment = mock<Fragment>()
+ val fragmentLifecycleOwner = MockedLifecycleOwner(Lifecycle.State.INITIALIZED)
+ val view = mock<View>()
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ val onAttachListener = argumentCaptor<View.OnAttachStateChangeListener>()
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+
+ doNothing().`when`(view).addOnAttachStateChangeListener(onAttachListener.capture())
+ doReturn(mock<FragmentActivity>()).`when`(fragment).activity
+ doReturn(view).`when`(fragment).view
+ doReturn(fragmentLifecycleOwner.lifecycle).`when`(fragment).lifecycle
+
+ fragment.consumeFlow(
+ from = store,
+ ) { flow ->
+ flow.collect { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+ }
+
+ // Nothing received yet.
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Updating state: Nothing received yet.
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Switching to STARTED state: Receiving initial state
+ fragmentLifecycleOwner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+ latch = CountDownLatch(1)
+ }
+
+ @Test
+ fun `consumeFlow - creates flow synchronously`() {
+ val fragment = mock<Fragment>()
+ val fragmentLifecycle = mock<LifecycleRegistry>()
+ val view = mock<View>()
+ val store = Store(TestState(counter = 23), ::reducer)
+
+ doReturn(mock<FragmentActivity>()).`when`(fragment).activity
+ doReturn(fragmentLifecycle).`when`(fragment).lifecycle
+ doReturn(view).`when`(fragment).view
+
+ // Verify that we create the flow even if no other coroutine runs past this point
+ val noopDispatcher = object : CoroutineDispatcher() {
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ // NOOP
+ }
+ }
+ Dispatchers.setMain(noopDispatcher)
+ fragment.consumeFlow(store) { flow ->
+ flow.collect { }
+ }
+
+ // Only way to verify that store.flow was called without triggering the channelFlow
+ // producer and in this test we want to make sure we call store.flow before the flow
+ // is "produced."
+ verify(fragmentLifecycle).addObserver(any())
+ }
+}
diff --git a/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/StoreExtensionsKtTest.kt b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/StoreExtensionsKtTest.kt
new file mode 100644
index 0000000000..c52bdb032e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/StoreExtensionsKtTest.kt
@@ -0,0 +1,572 @@
+/* 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.state.ext
+
+import android.app.Activity
+import android.os.Looper
+import android.os.Looper.getMainLooper
+import android.view.View
+import android.view.WindowManager
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.channels.consumeEach
+import kotlinx.coroutines.launch
+import mozilla.components.lib.state.Store
+import mozilla.components.lib.state.TestAction
+import mozilla.components.lib.state.TestState
+import mozilla.components.lib.state.reducer
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.Robolectric
+import org.robolectric.Shadows.shadowOf
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+@RunWith(AndroidJUnit4::class)
+@ExperimentalCoroutinesApi
+@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage.
+class StoreExtensionsKtTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `Observer will not get registered if lifecycle is already destroyed`() = runTestOnMain {
+ val owner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+
+ // We cannot set initial DESTROYED state for LifecycleRegistry
+ // so we simulate lifecycle getting destroyed.
+ owner.lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var stateObserved = false
+
+ store.observe(owner) { stateObserved = true }
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+
+ assertFalse(stateObserved)
+ }
+
+ @Test
+ fun `Observer will get unregistered if lifecycle gets destroyed`() {
+ val owner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var stateObserved = false
+ store.observe(owner) { stateObserved = true }
+ assertTrue(stateObserved)
+
+ stateObserved = false
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(stateObserved)
+
+ stateObserved = false
+ owner.lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(stateObserved)
+ }
+
+ @Test
+ fun `non-destroy lifecycle changes do not affect observer registration`() {
+ val owner = MockedLifecycleOwner(Lifecycle.State.INITIALIZED)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ // Observer does not get invoked since lifecycle is not started
+ var stateObserved = false
+ store.observe(owner) { stateObserved = true }
+ assertFalse(stateObserved)
+
+ // CREATED: Observer does still not get invoked
+ stateObserved = false
+ owner.lifecycleRegistry.currentState = Lifecycle.State.CREATED
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(stateObserved)
+
+ // STARTED: Observer gets initial state and observers updates
+ stateObserved = false
+ owner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ assertTrue(stateObserved)
+
+ stateObserved = false
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(stateObserved)
+
+ // RESUMED: Observer continues to get updates
+ stateObserved = false
+ owner.lifecycleRegistry.currentState = Lifecycle.State.RESUMED
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(stateObserved)
+
+ // CREATED: Not observing anymore
+ stateObserved = false
+ owner.lifecycleRegistry.currentState = Lifecycle.State.CREATED
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(stateObserved)
+
+ // DESTROYED: Not observing
+ stateObserved = false
+ owner.lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(stateObserved)
+ }
+
+ @Test
+ @Synchronized
+ @ExperimentalCoroutinesApi // Channel
+ fun `Reading state updates from channel`() = runTestOnMain {
+ val owner = MockedLifecycleOwner(Lifecycle.State.INITIALIZED)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+
+ val channel = store.channel(owner)
+
+ val job = launch {
+ channel.consumeEach { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+ }
+
+ // Nothing received yet.
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Updating state: Nothing received yet.
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Switching to STARTED state: Receiving initial state
+ owner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ latch = CountDownLatch(1)
+
+ job.cancelAndJoin()
+ assertTrue(channel.isClosedForReceive)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ @ExperimentalCoroutinesApi // Channel
+ fun `Creating channel throws if lifecycle is already DESTROYED`() {
+ val owner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+
+ // We cannot set initial DESTROYED state for LifecycleRegistry
+ // so we simulate lifecycle getting destroyed.
+ owner.lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ store.channel(owner)
+ }
+
+ @Test
+ @Synchronized
+ @ExperimentalCoroutinesApi
+ fun `Reading state updates from Flow with lifecycle owner`() = runTestOnMain {
+ val owner = MockedLifecycleOwner(Lifecycle.State.INITIALIZED)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+
+ val flow = store.flow(owner)
+
+ val job = coroutinesTestRule.scope.launch {
+ flow.collect { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+ }
+
+ // Nothing received yet.
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Updating state: Nothing received yet.
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Switching to STARTED state: Receiving initial state
+ owner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ latch = CountDownLatch(1)
+
+ job.cancelAndJoin()
+
+ // Receiving nothing anymore since coroutine is cancelled
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ }
+
+ @Test
+ @ExperimentalCoroutinesApi
+ fun `Subscription is not added if owner destroyed before flow created`() {
+ val owner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+ val latch = CountDownLatch(1)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ owner.lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+ val flow = store.flow(owner)
+ GlobalScope.launch {
+ flow.collect {
+ latch.countDown()
+ }
+ }
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertTrue(store.subscriptions.isEmpty())
+ }
+
+ @Test
+ @ExperimentalCoroutinesApi
+ fun `Subscription is not added if owner destroyed before flow produced`() {
+ val owner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+ val latch = CountDownLatch(1)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ val flow = store.flow(owner)
+ owner.lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+ GlobalScope.launch {
+ flow.collect {
+ latch.countDown()
+ }
+ }
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertTrue(store.subscriptions.isEmpty())
+ }
+
+ @Test
+ @Synchronized
+ @ExperimentalCoroutinesApi
+ fun `Reading state updates from Flow without lifecycle owner`() = runTestOnMain {
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+
+ val flow = store.flow()
+
+ val job = GlobalScope.launch {
+ flow.collect { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+ }
+
+ // Receiving immediately
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(23, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+
+ latch = CountDownLatch(1)
+
+ job.cancelAndJoin()
+
+ // Receiving nothing anymore since coroutine is cancelled
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ }
+
+ @Test
+ @Synchronized
+ @ExperimentalCoroutinesApi
+ fun `Reading state from scoped flow without lifecycle owner`() {
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+
+ val scope = store.flowScoped() { flow ->
+ flow.collect { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+ }
+
+ // Receiving immediately
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(23, receivedValue)
+
+ // Updating state: Nothing received yet.
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+
+ scope.cancel()
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ }
+
+ @Test
+ @Synchronized
+ @ExperimentalCoroutinesApi
+ fun `Reading state from scoped flow with lifecycle owner`() {
+ val owner = MockedLifecycleOwner(Lifecycle.State.INITIALIZED)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+
+ val scope = store.flowScoped(owner) { flow ->
+ flow.collect { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+ }
+
+ // Nothing received yet.
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Updating state: Nothing received yet.
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Switching to STARTED state: Receiving initial state
+ latch = CountDownLatch(1)
+ owner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+
+ scope.cancel()
+
+ latch = CountDownLatch(1)
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ }
+
+ @Test
+ fun `Observer registered with observeForever will get notified about state changes`() {
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var observedValue = 0
+
+ store.observeForever { state -> observedValue = state.counter }
+ assertEquals(23, observedValue)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertEquals(24, observedValue)
+
+ store.dispatch(TestAction.DecrementAction).joinBlocking()
+ assertEquals(23, observedValue)
+ }
+
+ @Test
+ fun `Observer bound to view will get unsubscribed if view gets detached`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).create().get()
+ val view = View(testContext)
+ activity.windowManager.addView(view, WindowManager.LayoutParams(100, 100))
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(view.isAttachedToWindow)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var stateObserved = false
+ store.observe(view) { stateObserved = true }
+ assertTrue(stateObserved)
+
+ stateObserved = false
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(stateObserved)
+
+ activity.windowManager.removeView(view)
+ shadowOf(getMainLooper()).idle()
+ assertFalse(view.isAttachedToWindow)
+
+ stateObserved = false
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(stateObserved)
+ }
+
+ @Test
+ fun `Observer bound to view will not get notified about state changes until the view is attached`() = runTestOnMain {
+ val activity = Robolectric.buildActivity(Activity::class.java).create().get()
+ val view = View(testContext)
+
+ assertFalse(view.isAttachedToWindow)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ var stateObserved = false
+ store.observe(view) { stateObserved = true }
+ assertFalse(stateObserved)
+
+ stateObserved = false
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(stateObserved)
+
+ activity.windowManager.addView(view, WindowManager.LayoutParams(100, 100))
+ shadowOf(Looper.getMainLooper()).idle()
+ assertTrue(view.isAttachedToWindow)
+ assertTrue(stateObserved)
+
+ stateObserved = false
+ store.observe(view) { stateObserved = true }
+ assertTrue(stateObserved)
+
+ stateObserved = false
+ store.observe(view) { stateObserved = true }
+ assertTrue(stateObserved)
+
+ activity.windowManager.removeView(view)
+ shadowOf(Looper.getMainLooper()).idle()
+
+ assertFalse(view.isAttachedToWindow)
+
+ stateObserved = false
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(stateObserved)
+ }
+}
+
+internal class MockedLifecycleOwner(initialState: Lifecycle.State) : LifecycleOwner {
+ val lifecycleRegistry = LifecycleRegistry(this).apply {
+ currentState = initialState
+ }
+
+ override val lifecycle: Lifecycle = lifecycleRegistry
+}
diff --git a/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/ViewKtTest.kt b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/ViewKtTest.kt
new file mode 100644
index 0000000000..6dfde6f9fa
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/ext/ViewKtTest.kt
@@ -0,0 +1,89 @@
+/* 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.state.ext
+
+import android.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.lib.state.Store
+import mozilla.components.lib.state.TestAction
+import mozilla.components.lib.state.TestState
+import mozilla.components.lib.state.reducer
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doNothing
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+@RunWith(AndroidJUnit4::class)
+@ExperimentalCoroutinesApi
+class ViewKtTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ @Synchronized
+ fun `consumeFrom reads states from store`() {
+ val view = mock<View>()
+ val owner = MockedLifecycleOwner(Lifecycle.State.INITIALIZED)
+
+ val store = Store(
+ TestState(counter = 23),
+ ::reducer,
+ )
+
+ val onAttachListener = argumentCaptor<View.OnAttachStateChangeListener>()
+ var receivedValue = 0
+ var latch = CountDownLatch(1)
+ doNothing().`when`(view).addOnAttachStateChangeListener(onAttachListener.capture())
+
+ view.consumeFrom(store, owner) { state ->
+ receivedValue = state.counter
+ latch.countDown()
+ }
+
+ // Nothing received yet.
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Updating state: Nothing received yet.
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(0, receivedValue)
+
+ // Switching to STARTED state: Receiving initial state
+ owner.lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(24, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(25, receivedValue)
+ latch = CountDownLatch(1)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ latch = CountDownLatch(1)
+
+ // View gets detached
+ onAttachListener.value.onViewDetachedFromWindow(view)
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ assertFalse(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(26, receivedValue)
+ }
+}
diff --git a/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/helpers/AbstractBindingTest.kt b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/helpers/AbstractBindingTest.kt
new file mode 100644
index 0000000000..5173ddc39e
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/helpers/AbstractBindingTest.kt
@@ -0,0 +1,98 @@
+/* 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.state.helpers
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import mozilla.components.lib.state.Store
+import mozilla.components.lib.state.TestAction
+import mozilla.components.lib.state.TestState
+import mozilla.components.lib.state.reducer
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+
+@ExperimentalCoroutinesApi
+class AbstractBindingTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `binding onState is invoked when a flow is created`() {
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ )
+
+ val binding = TestBinding(store)
+
+ assertFalse(binding.invoked)
+
+ binding.start()
+
+ assertTrue(binding.invoked)
+ }
+
+ @Test
+ fun `binding has no state changes when only stop is invoked`() {
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ )
+
+ val binding = TestBinding(store)
+
+ assertFalse(binding.invoked)
+
+ binding.stop()
+
+ assertFalse(binding.invoked)
+ }
+
+ @Test
+ fun `binding does not get state updates after stopped`() {
+ val store = Store(
+ TestState(counter = 0),
+ ::reducer,
+ )
+
+ var counter = 0
+
+ val binding = TestBinding(store) {
+ counter++
+ // After we stop, we shouldn't get updates for the third action dispatched.
+ if (counter >= 3) {
+ fail()
+ }
+ }
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+
+ binding.start()
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+
+ binding.stop()
+
+ store.dispatch(TestAction.IncrementAction).joinBlocking()
+ }
+}
+
+@ExperimentalCoroutinesApi
+class TestBinding(
+ store: Store<TestState, TestAction>,
+ private val onStateUpdated: (TestState) -> Unit = {},
+) : AbstractBinding<TestState>(store) {
+ var invoked = false
+ override suspend fun onState(flow: Flow<TestState>) {
+ invoked = true
+ flow.collect { onStateUpdated(it) }
+ }
+}
diff --git a/mobile/android/android-components/components/lib/state/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/lib/state/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/state/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/state/src/test/resources/robolectric.properties b/mobile/android/android-components/components/lib/state/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/lib/state/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28