From 1ef2daa5b54bf84fa268fd46b50da03a86feb281 Mon Sep 17 00:00:00 2001 From: Titouan Thibaud Date: Fri, 3 Mar 2023 18:35:17 +0100 Subject: [PATCH] Bug 1805683 - upstream Nimbus Messaging from Fenix to Android Components --- mobile/android/android-components/.buildconfig.yml | 1 + .../components/service/nimbus/build.gradle | 46 +++- .../components/service/nimbus/docs/metrics.md | 9 +- .../components/service/nimbus/messaging.fml.yaml | 154 ++++++++++++ .../components/service/nimbus/metrics.yaml | 99 ++++++++ .../nimbus/messaging/JexlAttributeProvider.kt | 21 ++ .../service/nimbus/messaging}/Message.kt | 6 +- .../nimbus/messaging}/MessageMetadataStorage.kt | 5 +- .../service/nimbus/messaging/MessageSurfaceId.kt | 10 + .../nimbus/messaging}/NimbusMessagingController.kt | 50 ++-- .../nimbus/messaging}/NimbusMessagingStorage.kt | 48 ++-- .../messaging}/OnDiskMessageMetadataStorage.kt | 2 +- .../messaging}/NimbusMessagingControllerTest.kt | 127 +++++----- .../messaging}/NimbusMessagingStorageTest.kt | 262 +++++++++++--------- .../messaging}/OnDiskMessageMetadataStorageTest.kt | 41 ++-- .../mozilla/components/support}/utils/BootUtils.kt | 2 +- .../components/support}/utils/BootUtilsTest.kt | 36 +-- mobile/android/fenix/app/.experimenter.yaml | 2 +- mobile/android/fenix/app/messaging.fml.yaml | 269 --------------------- mobile/android/fenix/app/metrics.yaml | 100 -------- mobile/android/fenix/app/nimbus.fml.yaml | 129 +++++++++- .../mozilla/fenix/ui/NimbusMessagingMessageTest.kt | 8 +- .../fenix/ui/NimbusMessagingNotificationTest.kt | 3 +- .../mozilla/fenix/ui/NimbusMessagingTriggerTest.kt | 8 +- .../android/fenix/app/src/main/AndroidManifest.xml | 4 +- .../main/java/org/mozilla/fenix/HomeActivity.kt | 2 +- .../java/org/mozilla/fenix/components/Analytics.kt | 14 +- .../org/mozilla/fenix/components/Components.kt | 2 +- .../mozilla/fenix/components/appstate/AppAction.kt | 6 +- .../mozilla/fenix/components/appstate/AppState.kt | 2 +- .../fenix/components/appstate/AppStoreReducer.kt | 2 +- .../org/mozilla/fenix/experiments/NimbusSetup.kt | 7 +- .../java/org/mozilla/fenix/home/HomeFragment.kt | 8 +- .../home/sessioncontrol/SessionControlAdapter.kt | 2 +- .../sessioncontrol/SessionControlController.kt | 4 +- .../sessioncontrol/SessionControlInteractor.kt | 2 +- .../home/sessioncontrol/SessionControlView.kt | 6 +- .../onboarding/MessageCardViewHolder.kt | 2 +- .../CustomAttributeProvider.kt | 7 +- .../DefaultMessageController.kt | 4 +- .../fenix/messaging/FenixMessageSurfaceId.kt | 25 ++ .../messaging/FenixNimbusMessagingController.kt | 19 ++ .../{gleanplumb => messaging}/MessageController.kt | 4 +- .../MessageNotificationWorker.kt | 22 +- .../{gleanplumb => messaging}/MessagingFeature.kt | 5 +- .../{gleanplumb => messaging}/MessagingState.kt | 5 +- .../state/MessagingMiddleware.kt | 11 +- .../state/MessagingReducer.kt | 4 +- .../fenix/settings/studies/StudiesAdapter.kt | 2 +- .../org/mozilla/fenix/components/AppStoreTest.kt | 8 +- .../home/DefaultSessionControlControllerTest.kt | 4 +- .../home/sessioncontrol/SessionControlViewTest.kt | 2 +- .../DefaultMessageControllerTest.kt | 6 +- .../MessagingFeatureTest.kt | 5 +- .../state/MessagingMiddlewareTest.kt | 39 +-- .../state/MessagingReducerTest.kt | 15 +- .../org/mozilla/fenix/nimbus/NimbusSystemTest.kt | 1 + .../fenix/settings/studies/StudiesAdapterTest.kt | 2 +- mobile/android/fenix/build.gradle | 2 + 59 files changed, 935 insertions(+), 758 deletions(-) create mode 100644 mobile/android/android-components/components/service/nimbus/messaging.fml.yaml create mode 100644 mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/JexlAttributeProvider.kt rename mobile/android/{fenix/app/src/main/java/org/mozilla/fenix/gleanplumb => android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging}/Message.kt (92%) rename mobile/android/{fenix/app/src/main/java/org/mozilla/fenix/gleanplumb => android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging}/MessageMetadataStorage.kt (86%) create mode 100644 mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/MessageSurfaceId.kt rename mobile/android/{fenix/app/src/main/java/org/mozilla/fenix/gleanplumb => android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging}/NimbusMessagingController.kt (69%) rename mobile/android/{fenix/app/src/main/java/org/mozilla/fenix/gleanplumb => android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging}/NimbusMessagingStorage.kt (87%) rename mobile/android/{fenix/app/src/main/java/org/mozilla/fenix/gleanplumb => android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging}/OnDiskMessageMetadataStorage.kt (98%) rename mobile/android/{fenix/app/src/test/java/org/mozilla/fenix/gleanplumb => android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging}/NimbusMessagingControllerTest.kt (70%) rename mobile/android/{fenix/app/src/test/java/org/mozilla/fenix/gleanplumb => android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging}/NimbusMessagingStorageTest.kt (73%) rename mobile/android/{fenix/app/src/test/java/org/mozilla/fenix/gleanplumb => android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging}/OnDiskMessageMetadataStorageTest.kt (79%) rename mobile/android/{fenix/app/src/main/java/org/mozilla/fenix => android-components/components/support/utils/src/main/java/mozilla/components/support}/utils/BootUtils.kt (97%) rename mobile/android/{fenix/app/src/test/java/org/mozilla/fenix => android-components/components/support/utils/src/test/java/mozilla/components/support}/utils/BootUtilsTest.kt (67%) delete mode 100644 mobile/android/fenix/app/messaging.fml.yaml rename mobile/android/fenix/app/src/main/java/org/mozilla/fenix/{gleanplumb => messaging}/CustomAttributeProvider.kt (91%) rename mobile/android/fenix/app/src/main/java/org/mozilla/fenix/{gleanplumb => messaging}/DefaultMessageController.kt (87%) create mode 100644 mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/FenixMessageSurfaceId.kt create mode 100644 mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/FenixNimbusMessagingController.kt rename mobile/android/fenix/app/src/main/java/org/mozilla/fenix/{gleanplumb => messaging}/MessageController.kt (85%) rename mobile/android/fenix/app/src/main/java/org/mozilla/fenix/{gleanplumb => messaging}/MessageNotificationWorker.kt (91%) rename mobile/android/fenix/app/src/main/java/org/mozilla/fenix/{gleanplumb => messaging}/MessagingFeature.kt (77%) rename mobile/android/fenix/app/src/main/java/org/mozilla/fenix/{gleanplumb => messaging}/MessagingState.kt (79%) rename mobile/android/fenix/app/src/main/java/org/mozilla/fenix/{gleanplumb => messaging}/state/MessagingMiddleware.kt (92%) rename mobile/android/fenix/app/src/main/java/org/mozilla/fenix/{gleanplumb => messaging}/state/MessagingReducer.kt (94%) rename mobile/android/fenix/app/src/test/java/org/mozilla/fenix/{gleanplumb => messaging}/DefaultMessageControllerTest.kt (91%) rename mobile/android/fenix/app/src/test/java/org/mozilla/fenix/{gleanplumb => messaging}/MessagingFeatureTest.kt (82%) rename mobile/android/fenix/app/src/test/java/org/mozilla/fenix/{gleanplumb => messaging}/state/MessagingMiddlewareTest.kt (89%) rename mobile/android/fenix/app/src/test/java/org/mozilla/fenix/{gleanplumb => messaging}/state/MessagingReducerTest.kt (83%) diff --git a/mobile/android/android-components/.buildconfig.yml b/mobile/android/android-components/.buildconfig.yml index c03848e7b32d..5a4b6153a9f5 100644 --- a/mobile/android/android-components/.buildconfig.yml +++ b/mobile/android/android-components/.buildconfig.yml @@ -1866,6 +1866,7 @@ projects: - concept-storage - lib-publicsuffixlist - lib-state + - service-glean - support-base - support-ktx - support-locale diff --git a/mobile/android/android-components/components/service/nimbus/build.gradle b/mobile/android/android-components/components/service/nimbus/build.gradle index 3317cc5e02a6..324d880e9acb 100644 --- a/mobile/android/android-components/components/service/nimbus/build.gradle +++ b/mobile/android/android-components/components/service/nimbus/build.gradle @@ -7,9 +7,17 @@ buildscript { maven { url "https://maven.mozilla.org/maven2" } + dependencies { + classpath "org.mozilla.appservices:tooling-nimbus-gradle:${Versions.mozilla_appservices}" + classpath "org.mozilla.telemetry:glean-gradle-plugin:${Versions.mozilla_glean}" + } } } +plugins { + id "com.jetbrains.python.envs" version "0.0.26" +} + apply plugin: 'com.android.library' apply plugin: 'kotlin-android' @@ -49,13 +57,20 @@ dependencies { implementation ComponentsDependencies.androidx_constraintlayout implementation ComponentsDependencies.androidx_coordinatorlayout implementation ComponentsDependencies.androidx_recyclerview + implementation ComponentsDependencies.androidx_work_runtime implementation ComponentsDependencies.kotlin_coroutines + implementation ComponentsDependencies.mozilla_nimbus + implementation project(':support-base') implementation project(':support-locale') + implementation project(':support-ktx') + implementation project(':support-utils') - testImplementation ComponentsDependencies.androidx_work_testing + // 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 ComponentsDependencies.mozilla_full_megazord_forUnitTests testImplementation ComponentsDependencies.androidx_test_core @@ -63,9 +78,38 @@ dependencies { testImplementation ComponentsDependencies.testing_mockito testImplementation ComponentsDependencies.testing_mockwebserver testImplementation ComponentsDependencies.testing_robolectric + testImplementation ComponentsDependencies.testing_coroutines + testImplementation ComponentsDependencies.mozilla_glean_forUnitTests + testImplementation ComponentsDependencies.androidx_work_testing testImplementation project(':support-test') + testImplementation project(":service-glean") } apply from: '../../../android-lint.gradle' apply from: '../../../publish.gradle' ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) + + +apply plugin: "org.mozilla.appservices.nimbus-gradle-plugin" + +nimbus { + // The path to the Nimbus feature manifest file + manifestFile = "messaging.fml.yaml" + + // Map from the variant name to the channel as experimenter and nimbus understand it. + // If nimbus's channels were accurately set up well for this project, then this + // shouldn't be needed. + channels = [ + debug: "debug", + release: "release", + ] + + // This is an optional value, and updates the plugin to use a copy of application + // services. The path should be relative to the root project directory. + // *NOTE*: This example will not work for all projects, but should work for Fenix, Focus, and Android Components + applicationServicesDir = gradle.hasProperty('localProperties.autoPublish.application-services.dir') + ? gradle.getProperty('localProperties.autoPublish.application-services.dir') : null +} + +ext.gleanGenerateMarkdownDocs = true +apply plugin: "org.mozilla.telemetry.glean-gradle-plugin" diff --git a/mobile/android/android-components/components/service/nimbus/docs/metrics.md b/mobile/android/android-components/components/service/nimbus/docs/metrics.md index 3d3b4f5aaa49..1b8eb1479735 100644 --- a/mobile/android/android-components/components/service/nimbus/docs/metrics.md +++ b/mobile/android/android-components/components/service/nimbus/docs/metrics.md @@ -1,4 +1,4 @@ - + # Metrics @@ -22,6 +22,11 @@ In addition to those built-in metrics, the following metrics are added to the pi | Name | Type | Description | Data reviews | Extras | Expiration | [Data Sensitivity](https://wiki.mozilla.org/Firefox/Data_Collection) | | --- | --- | --- | --- | --- | --- | --- | +| messaging.malformed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A message was malformed. |[mozilla-mobile/fenix/issues/24224](https://github.com/mozilla-mobile/fenix/issues/24224), [mozilla-mobile/firefox-android#1101](https://github.com/mozilla-mobile/firefox-android/pull/1101)||never |2 | +| messaging.message_clicked |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A message was clicked by the user. |[mozilla-mobile/fenix/issues/24224](https://github.com/mozilla-mobile/fenix/issues/24224), [mozilla-mobile/firefox-android#1101](https://github.com/mozilla-mobile/firefox-android/pull/1101)||never |2 | +| messaging.message_dismissed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A message was dismissed by the user. |[mozilla-mobile/fenix/issues/24224](https://github.com/mozilla-mobile/fenix/issues/24224), [mozilla-mobile/firefox-android#1101](https://github.com/mozilla-mobile/firefox-android/pull/1101)||never |2 | +| messaging.message_expired |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A message maxDisplayCount has been surpassed. |[mozilla-mobile/fenix/issues/24224](https://github.com/mozilla-mobile/fenix/issues/24224), [mozilla-mobile/firefox-android#1101](https://github.com/mozilla-mobile/firefox-android/pull/1101)||never |2 | +| messaging.message_shown |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A message was shown to the user. |[mozilla-mobile/fenix#24426](https://github.com/mozilla-mobile/fenix/pull/24426), [mozilla-mobile/firefox-android#1101](https://github.com/mozilla-mobile/firefox-android/pull/1101)||never |2 | | nimbus_events.disqualification |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |Recorded when a user becomes ineligible to continue receiving the treatment for an enrolled experiment, for reasons such as the user opting out of the experiment or no longer matching targeting for the experiment. |[mozilla-mobile/android-components#9168](https://github.com/mozilla-mobile/android-components/pull/9168#issuecomment-743461975)||never |1 | | nimbus_events.enrollment |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |Recorded when a user has met the conditions and is first bucketed into an experiment (i.e. targeting matched and they were randomized into a bucket and branch of the experiment). Expected a maximum of once per experiment per user. |[mozilla-mobile/android-components#9168](https://github.com/mozilla-mobile/android-components/pull/9168#issuecomment-743461975)||never |1 | | nimbus_events.exposure |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |Recorded when a user actually observes an experimental treatment, or would have observed an experimental treatment if they had been in a branch that would have shown one. |[mozilla-mobile/android-components#9168](https://github.com/mozilla-mobile/android-components/pull/9168#issuecomment-743461975)||never |1 | @@ -29,5 +34,5 @@ In addition to those built-in metrics, the following metrics are added to the pi Data categories are [defined here](https://wiki.mozilla.org/Firefox/Data_Collection). - + diff --git a/mobile/android/android-components/components/service/nimbus/messaging.fml.yaml b/mobile/android/android-components/components/service/nimbus/messaging.fml.yaml new file mode 100644 index 000000000000..bf2a86a59482 --- /dev/null +++ b/mobile/android/android-components/components/service/nimbus/messaging.fml.yaml @@ -0,0 +1,154 @@ +--- +about: + description: Nimbus Feature Manifest for Android + kotlin: + package: mozilla.components.service.nimbus + class: .messaging.FxNimbusMessaging +channels: + - release + - debug +features: + nimbus-system: + description: | + Configuration of the Nimbus System in Android. + variables: + refresh-interval-foreground: + description: | + The minimum interval in minutes between fetching experiment + recipes in the foreground. + type: Int + default: 60 # 1 hour + + messaging: + description: | + Configuration for the messaging system. + + In practice this is a set of growable lookup tables for the + message controller to piece together. + + variables: + message-under-experiment: + description: Id or prefix of the message under experiment. + type: Option + default: null + + messages: + description: A growable collection of messages + type: Map + default: {} + + triggers: + description: > + A collection of out the box trigger + expressions. Each entry maps to a + valid JEXL expression. + type: Map + default: {} + styles: + description: > + A map of styles to configure message + appearance. + type: Map + default: {} + + actions: + type: Map + description: A growable map of action URLs. + default: {} + on-control: + type: ControlMessageBehavior + description: What should be displayed when a control message is selected. + default: show-next-message + notification-config: + description: Configuration of the notification worker for all notification messages. + type: NotificationConfig + default: {} + defaults: + +objects: + MessageData: + description: > + An object to describe a message. It uses human + readable strings to describe the triggers, action and + style of the message as well as the text of the message + and call to action. + fields: + action: + type: Text + description: > + A URL of a page or a deeplink. + This may have substitution variables in. + # This should never be defaulted. + default: "" + title: + type: Option + description: "The title text displayed to the user" + default: null + text: + type: Text + description: "The message text displayed to the user" + # This should never be defaulted. + default: "" + is-control: + type: Boolean + description: "Indicates if this message is the control message, if true shouldn't be displayed" + default: false + button-label: + type: Option + description: > + The text on the button. If no text + is present, the whole message is clickable. + default: null + style: + type: String + description: > + The style as described in a + `StyleData` from the styles table. + default: DEFAULT + surface: + description: + The surface identifier for this message. + type: String + default: homescreen + trigger: + type: List + description: > + A list of strings corresponding to + targeting expressions. The message will be + shown if all expressions `true`. + default: [] + StyleData: + description: > + A group of properties (predominantly visual) to + describe the style of the message. + fields: + priority: + type: Int + description: > + The importance of this message. + 0 is not very important, 100 is very important. + default: 50 + max-display-count: + type: Int + description: > + How many sessions will this message be shown to the user + before it is expired. + default: 5 + NotificationConfig: + description: Attributes controlling the global configuration of notification messages. + fields: + refresh-interval: + type: Int + description: > + How often, in minutes, the notification message worker will wake up and check for new + messages. + default: 240 # 4 hours + +enums: + ControlMessageBehavior: + description: An enum to influence what should be displayed when a control message is selected. + variants: + show-next-message: + description: The next eligible message should be shown. + show-none: + description: The surface should show no message. diff --git a/mobile/android/android-components/components/service/nimbus/metrics.yaml b/mobile/android/android-components/components/service/nimbus/metrics.yaml index 96b6dc63a1a5..6c46369493cb 100644 --- a/mobile/android/android-components/components/service/nimbus/metrics.yaml +++ b/mobile/android/android-components/components/service/nimbus/metrics.yaml @@ -103,3 +103,102 @@ nimbus_events: notification_emails: - tlong@mozilla.com, telemetry-team@mozilla.com expires: never +messaging: + message_shown: + type: event + description: | + A message was shown to the user. + extra_keys: + message_key: + description: The id of the message + type: string + bugs: + - https://github.com/mozilla-mobile/fenix/issues/24224 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/24426 + - https://github.com/mozilla-mobile/firefox-android/pull/1101 + notification_emails: + - android-probes@mozilla.com + - kbrosnan@mozilla.com + data_sensitivity: + - interaction + expires: never + message_dismissed: + type: event + description: | + A message was dismissed by the user. + extra_keys: + message_key: + description: The id of the message + type: string + bugs: + - https://github.com/mozilla-mobile/fenix/issues/24224 + data_reviews: + - https://github.com/mozilla-mobile/fenix/issues/24224 + - https://github.com/mozilla-mobile/firefox-android/pull/1101 + notification_emails: + - android-probes@mozilla.com + - kbrosnan@mozilla.com + data_sensitivity: + - interaction + expires: never + message_clicked: + type: event + description: | + A message was clicked by the user. + extra_keys: + message_key: + description: The id of the message + type: string + action_uuid: + description: The uuid of the action + type: string + bugs: + - https://github.com/mozilla-mobile/fenix/issues/24224 + data_reviews: + - https://github.com/mozilla-mobile/fenix/issues/24224 + - https://github.com/mozilla-mobile/firefox-android/pull/1101 + notification_emails: + - android-probes@mozilla.com + - kbrosnan@mozilla.com + data_sensitivity: + - interaction + expires: never + message_expired: + type: event + description: | + A message maxDisplayCount has been surpassed. + extra_keys: + message_key: + description: The id of the message + type: string + bugs: + - https://github.com/mozilla-mobile/fenix/issues/24224 + data_reviews: + - https://github.com/mozilla-mobile/fenix/issues/24224 + - https://github.com/mozilla-mobile/firefox-android/pull/1101 + notification_emails: + - android-probes@mozilla.com + - kbrosnan@mozilla.com + data_sensitivity: + - interaction + expires: never + malformed: + type: event + description: | + A message was malformed. + extra_keys: + message_key: + description: The id of the message + type: string + bugs: + - https://github.com/mozilla-mobile/fenix/issues/24224 + data_reviews: + - https://github.com/mozilla-mobile/fenix/issues/24224 + - https://github.com/mozilla-mobile/firefox-android/pull/1101 + notification_emails: + - android-probes@mozilla.com + - kbrosnan@mozilla.com + data_sensitivity: + - interaction + expires: never diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/JexlAttributeProvider.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/JexlAttributeProvider.kt new file mode 100644 index 000000000000..9213e8568cec --- /dev/null +++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/JexlAttributeProvider.kt @@ -0,0 +1,21 @@ +/* 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.service.nimbus.messaging + +import android.content.Context +import org.json.JSONObject + +/** + * A provider that will be used to evaluate if message is eligible to be shown. + */ +interface JexlAttributeProvider { + /** + * Returns a [JSONObject] that contains all the custom attributes, evaluated when the function + * was called. + * + * This is used to drive display triggers of messages. + */ + fun getCustomAttributes(context: Context): JSONObject +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/Message.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/Message.kt similarity index 92% rename from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/Message.kt rename to mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/Message.kt index 061cdbeef847..a3bf8118b19d 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/Message.kt +++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/Message.kt @@ -2,11 +2,7 @@ * 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 org.mozilla.fenix.gleanplumb - -import org.mozilla.fenix.nimbus.MessageData -import org.mozilla.fenix.nimbus.MessageSurfaceId -import org.mozilla.fenix.nimbus.StyleData +package mozilla.components.service.nimbus.messaging /** * A data class that holds a representation of GleanPlum message from Nimbus. diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/MessageMetadataStorage.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/MessageMetadataStorage.kt similarity index 86% rename from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/MessageMetadataStorage.kt rename to mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/MessageMetadataStorage.kt index c30d8535dbbd..619c5607b194 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/MessageMetadataStorage.kt +++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/MessageMetadataStorage.kt @@ -2,8 +2,11 @@ * 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 org.mozilla.fenix.gleanplumb +package mozilla.components.service.nimbus.messaging +/** + * A storage that persists [Message.Metadata] into disk. + */ interface MessageMetadataStorage { /** * Provide all the message metadata saved in the storage. diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/MessageSurfaceId.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/MessageSurfaceId.kt new file mode 100644 index 000000000000..e7f419c45598 --- /dev/null +++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/MessageSurfaceId.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.service.nimbus.messaging + +/** + * The identity of a message surface + */ +typealias MessageSurfaceId = String diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/NimbusMessagingController.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingController.kt similarity index 69% rename from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/NimbusMessagingController.kt rename to mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingController.kt index 2ab9e623cc1b..3238a102fd14 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/NimbusMessagingController.kt +++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingController.kt @@ -2,19 +2,28 @@ * 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 org.mozilla.fenix.gleanplumb +package mozilla.components.service.nimbus.messaging import android.content.Intent import android.net.Uri import androidx.core.net.toUri -import org.mozilla.fenix.BuildConfig -import org.mozilla.fenix.GleanMetrics.Messaging +import mozilla.components.service.nimbus.GleanMetrics.Messaging as GleanMessaging /** * Bookkeeping for message actions in terms of Glean messages and the messaging store. + * + * @param messagingStorage a NimbusMessagingStorage instance + * @param deepLinkScheme the deepLinkScheme for the app + * @param httpActionToDeepLinkUriConverter will be used to create a deepLinkUri from the action associated to a message. + * It can be customized to fit the needs of any app. A default implementation is provided. + * @param now will be used to get the current time */ -class NimbusMessagingController( +open class NimbusMessagingController( private val messagingStorage: NimbusMessagingStorage, + private val deepLinkScheme: String, + private val httpActionToDeepLinkUriConverter: (String) -> Uri = { action -> + "$deepLinkScheme://open?url=${Uri.encode(action)}".toUri() + }, private val now: () -> Long = { System.currentTimeMillis() }, ) { /** @@ -74,7 +83,7 @@ class NimbusMessagingController( * @return an [Intent] using the processed [Message.action]. */ fun getIntentForMessageAction(action: String): Intent { - return Intent(Intent.ACTION_VIEW, action.toDeepLinkSchemeUri()) + return Intent(Intent.ACTION_VIEW, convertActionIntoDeepLinkSchemeUri(action)) } /** @@ -98,36 +107,33 @@ class NimbusMessagingController( val (uuid, action) = messagingStorage.generateUuidAndFormatAction(message.action) sendClickedMessageTelemetry(message.id, uuid) - return action.toDeepLinkSchemeUri() + return convertActionIntoDeepLinkSchemeUri(action) } private fun sendDismissedMessageTelemetry(messageId: String) { - Messaging.messageDismissed.record(Messaging.MessageDismissedExtra(messageId)) + GleanMessaging.messageDismissed.record(GleanMessaging.MessageDismissedExtra(messageId)) } private fun sendShownMessageTelemetry(messageId: String) { - Messaging.messageShown.record(Messaging.MessageShownExtra(messageId)) + GleanMessaging.messageShown.record(GleanMessaging.MessageShownExtra(messageId)) } private fun sendExpiredMessageTelemetry(messageId: String) { - Messaging.messageExpired.record(Messaging.MessageExpiredExtra(messageId)) + GleanMessaging.messageExpired.record(GleanMessaging.MessageExpiredExtra(messageId)) } private fun sendClickedMessageTelemetry(messageId: String, uuid: String?) { - Messaging.messageClicked.record( - Messaging.MessageClickedExtra(messageKey = messageId, actionUuid = uuid), + GleanMessaging.messageClicked.record( + GleanMessaging.MessageClickedExtra(messageKey = messageId, actionUuid = uuid), ) } -} - -private fun String.toDeepLinkSchemeUri(): Uri { - val actionWithDeepLinkScheme = if (startsWith("http", ignoreCase = true)) { - "${BuildConfig.DEEP_LINK_SCHEME}://open?url=${Uri.encode(this)}" - } else if (startsWith("://")) { - "${BuildConfig.DEEP_LINK_SCHEME}$this" - } else { - this - } - return actionWithDeepLinkScheme.toUri() + private fun convertActionIntoDeepLinkSchemeUri(action: String): Uri = + if (action.startsWith("http", ignoreCase = true)) { + httpActionToDeepLinkUriConverter(action) + } else if (action.startsWith("://")) { + "$deepLinkScheme$action".toUri() + } else { + action.toUri() + } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/NimbusMessagingStorage.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingStorage.kt similarity index 87% rename from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/NimbusMessagingStorage.kt rename to mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingStorage.kt index 6fe8f37a561e..5839112d3b66 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/NimbusMessagingStorage.kt +++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingStorage.kt @@ -2,7 +2,7 @@ * 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 org.mozilla.fenix.gleanplumb +package mozilla.components.service.nimbus.messaging import android.content.Context import androidx.annotation.VisibleForTesting @@ -12,10 +12,7 @@ import org.mozilla.experiments.nimbus.GleanPlumbInterface import org.mozilla.experiments.nimbus.GleanPlumbMessageHelper import org.mozilla.experiments.nimbus.internal.FeatureHolder import org.mozilla.experiments.nimbus.internal.NimbusException -import org.mozilla.fenix.nimbus.ControlMessageBehavior -import org.mozilla.fenix.nimbus.MessageSurfaceId -import org.mozilla.fenix.nimbus.Messaging -import org.mozilla.fenix.nimbus.StyleData +import mozilla.components.service.nimbus.GleanMetrics.Messaging as GleanMessaging /** * This ID must match the name given in the `nimbus.fml.yaml` file, which @@ -36,17 +33,19 @@ const val MESSAGING_FEATURE_ID = "messaging" class NimbusMessagingStorage( private val context: Context, private val metadataStorage: MessageMetadataStorage, - private val reportMalformedMessage: (String) -> Unit, + private val reportMalformedMessage: (String) -> Unit = { + GleanMessaging.malformed.record(GleanMessaging.MalformedExtra(it)) + }, private val gleanPlumb: GleanPlumbInterface, private val messagingFeature: FeatureHolder, - private val attributeProvider: CustomAttributeProvider? = null, + private val attributeProvider: JexlAttributeProvider? = null, ) { /** * Contains all malformed messages where they key can be the value or a trigger of the message * and the value is the message id. */ @VisibleForTesting - internal val malFormedMap = mutableMapOf() + val malFormedMap = mutableMapOf() private val logger = Logger("MessagingStorage") private val nimbusFeature = messagingFeature private val customAttributes: JSONObject @@ -90,7 +89,8 @@ class NimbusMessagingStorage( return nimbusMessages .mapNotNull { (key, value) -> - val action = sanitizeAction(key, value.action, nimbusActions, value.isControl) ?: return@mapNotNull null + val action = sanitizeAction(key, value.action, nimbusActions, value.isControl) + ?: return@mapNotNull null Message( id = key, data = value, @@ -216,23 +216,31 @@ class NimbusMessagingStorage( } } + /** + * Return true if the message passed as a parameter is under experiment + * + * Aimed to be used from tests only, but currently public because some tests inside Fenix need + * it. This should be set as internal when this bug is fixed: + * https://bugzilla.mozilla.org/show_bug.cgi?id=1823472 + */ @VisibleForTesting - internal fun isMessageUnderExperiment(message: Message, expression: String?): Boolean { + fun isMessageUnderExperiment(message: Message, expression: String?): Boolean { return message.data.isControl || when { - expression.isNullOrBlank() -> { - false - } - expression.endsWith("-") -> { - message.id.startsWith(expression) - } - else -> { - message.id == expression - } + expression.isNullOrBlank() -> false + expression.endsWith("-") -> message.id.startsWith(expression) + else -> message.id == expression } } + /** + * Return true if the message passed as a parameter is eligible + * + * Aimed to be used from tests only, but currently public because some tests inside Fenix need + * it. This should be set as internal when this bug is fixed: + * https://bugzilla.mozilla.org/show_bug.cgi?id=1823472 + */ @VisibleForTesting - internal fun isMessageEligible( + fun isMessageEligible( message: Message, helper: GleanPlumbMessageHelper, jexlCache: MutableMap = mutableMapOf(), diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/OnDiskMessageMetadataStorage.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/OnDiskMessageMetadataStorage.kt similarity index 98% rename from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/OnDiskMessageMetadataStorage.kt rename to mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/OnDiskMessageMetadataStorage.kt index ecf991485fbb..c31e7e968ec2 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/OnDiskMessageMetadataStorage.kt +++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/OnDiskMessageMetadataStorage.kt @@ -2,7 +2,7 @@ * 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 org.mozilla.fenix.gleanplumb +package mozilla.components.service.nimbus.messaging import android.content.Context import android.util.AtomicFile diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/NimbusMessagingControllerTest.kt b/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/NimbusMessagingControllerTest.kt similarity index 70% rename from mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/NimbusMessagingControllerTest.kt rename to mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/NimbusMessagingControllerTest.kt index b3273751b6eb..f21eeabf07a4 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/NimbusMessagingControllerTest.kt +++ b/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/NimbusMessagingControllerTest.kt @@ -2,19 +2,17 @@ * 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 org.mozilla.fenix.gleanplumb +package mozilla.components.service.nimbus.messaging import android.content.Intent import android.net.Uri import androidx.core.net.toUri -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import mozilla.components.service.glean.testing.GleanTestRule +import mozilla.components.support.test.any import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.rule.MainCoroutineRule -import mozilla.telemetry.glean.testing.GleanTestRule import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull @@ -23,19 +21,21 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mozilla.experiments.nimbus.NullVariables -import org.mozilla.fenix.BuildConfig -import org.mozilla.fenix.GleanMetrics.Messaging -import org.mozilla.fenix.helpers.FenixRobolectricTestRunner -import org.mozilla.fenix.nimbus.MessageData -import org.mozilla.fenix.nimbus.StyleData +import org.robolectric.RobolectricTestRunner import java.util.UUID +import mozilla.components.service.nimbus.GleanMetrics.Messaging as GleanMessaging private const val MOCK_TIME_MILLIS = 1000L -@RunWith(FenixRobolectricTestRunner::class) +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) class NimbusMessagingControllerTest { - private val storage: NimbusMessagingStorage = mockk(relaxed = true) + + private val storage: NimbusMessagingStorage = mock(NimbusMessagingStorage::class.java) @get:Rule val gleanTestRule = GleanTestRule(testContext) @@ -43,7 +43,8 @@ class NimbusMessagingControllerTest { private val coroutinesTestRule = MainCoroutineRule() private val coroutineScope = coroutinesTestRule.scope - private val controller = NimbusMessagingController(storage) { MOCK_TIME_MILLIS } + private val deepLinkScheme = "deepLinkScheme" + private val controller = NimbusMessagingController(storage, deepLinkScheme) { MOCK_TIME_MILLIS } @Before fun setup() { @@ -98,21 +99,21 @@ class NimbusMessagingControllerTest { coroutineScope.runTest { val message = createMessage("id-1", style = StyleData(maxDisplayCount = 1)) // Assert telemetry is initially null - assertNull(Messaging.messageShown.testGetValue()) - assertNull(Messaging.messageExpired.testGetValue()) + assertNull(GleanMessaging.messageShown.testGetValue()) + assertNull(GleanMessaging.messageExpired.testGetValue()) controller.onMessageDisplayed(message) // Shown telemetry - assertNotNull(Messaging.messageShown.testGetValue()) - val shownEvent = Messaging.messageShown.testGetValue()!! + assertNotNull(GleanMessaging.messageShown.testGetValue()) + val shownEvent = GleanMessaging.messageShown.testGetValue()!! assertEquals(1, shownEvent.size) assertEquals(message.id, shownEvent.single().extra!!["message_key"]) // Expired telemetry - assertNull(Messaging.messageExpired.testGetValue()) + assertNull(GleanMessaging.messageExpired.testGetValue()) - coVerify { storage.updateMetadata(message.metadata) } + verify(storage).updateMetadata(message.metadata) } @Test @@ -121,61 +122,61 @@ class NimbusMessagingControllerTest { val message = createMessage("id-1", style = StyleData(maxDisplayCount = 1), displayCount = 1) // Assert telemetry is initially null - assertNull(Messaging.messageShown.testGetValue()) - assertNull(Messaging.messageExpired.testGetValue()) + assertNull(GleanMessaging.messageShown.testGetValue()) + assertNull(GleanMessaging.messageExpired.testGetValue()) controller.onMessageDisplayed(message) // Shown telemetry - assertNotNull(Messaging.messageShown.testGetValue()) - val shownEvent = Messaging.messageShown.testGetValue()!! + assertNotNull(GleanMessaging.messageShown.testGetValue()) + val shownEvent = GleanMessaging.messageShown.testGetValue()!! assertEquals(1, shownEvent.size) assertEquals(message.id, shownEvent.single().extra!!["message_key"]) // Expired telemetry - assertNotNull(Messaging.messageExpired.testGetValue()) - val expiredEvent = Messaging.messageExpired.testGetValue()!! + assertNotNull(GleanMessaging.messageExpired.testGetValue()) + val expiredEvent = GleanMessaging.messageExpired.testGetValue()!! assertEquals(1, expiredEvent.size) assertEquals(message.id, expiredEvent.single().extra!!["message_key"]) - coVerify { storage.updateMetadata(message.metadata) } + verify(storage).updateMetadata(message.metadata) } @Test fun `WHEN calling onMessageDismissed THEN record a messageDismissed event and update metadata`() = coroutineScope.runTest { val message = createMessage("id-1") - assertNull(Messaging.messageDismissed.testGetValue()) + assertNull(GleanMessaging.messageDismissed.testGetValue()) controller.onMessageDismissed(message.metadata) - assertNotNull(Messaging.messageDismissed.testGetValue()) - val event = Messaging.messageDismissed.testGetValue()!! + assertNotNull(GleanMessaging.messageDismissed.testGetValue()) + val event = GleanMessaging.messageDismissed.testGetValue()!! assertEquals(1, event.size) assertEquals(message.id, event.single().extra!!["message_key"]) - coVerify { storage.updateMetadata(message.metadata.copy(dismissed = true)) } + verify(storage).updateMetadata(message.metadata.copy(dismissed = true)) } @Test fun `GIVEN action is URL WHEN calling processMessageActionToUri THEN record a clicked telemetry event and return an open URI`() { val url = "http://mozilla.org" val message = createMessage("id-1", action = url) - every { storage.generateUuidAndFormatAction(message.action) } returns Pair( - null, - message.action, - ) + + `when`(storage.generateUuidAndFormatAction(message.action)) + .thenReturn(Pair(null, message.action)) + // Assert telemetry is initially null - assertNull(Messaging.messageClicked.testGetValue()) + assertNull(GleanMessaging.messageClicked.testGetValue()) val encodedUrl = Uri.encode(url) - val expectedUri = "${BuildConfig.DEEP_LINK_SCHEME}://open?url=$encodedUrl".toUri() + val expectedUri = "$deepLinkScheme://open?url=$encodedUrl".toUri() val actualUri = controller.processMessageActionToUri(message) // Updated telemetry - assertNotNull(Messaging.messageClicked.testGetValue()) - val clickedEvent = Messaging.messageClicked.testGetValue()!! + assertNotNull(GleanMessaging.messageClicked.testGetValue()) + val clickedEvent = GleanMessaging.messageClicked.testGetValue()!! assertEquals(1, clickedEvent.size) assertEquals(message.id, clickedEvent.single().extra!!["message_key"]) @@ -187,18 +188,18 @@ class NimbusMessagingControllerTest { val url = "http://mozilla.org?uuid={uuid}" val message = createMessage("id-1", action = url) val uuid = UUID.randomUUID().toString() - every { storage.generateUuidAndFormatAction(any()) } returns Pair(uuid, message.action) + `when`(storage.generateUuidAndFormatAction(any())).thenReturn(Pair(uuid, message.action)) // Assert telemetry is initially null - assertNull(Messaging.messageClicked.testGetValue()) + assertNull(GleanMessaging.messageClicked.testGetValue()) val encodedUrl = Uri.encode(url) - val expectedUri = "${BuildConfig.DEEP_LINK_SCHEME}://open?url=$encodedUrl".toUri() + val expectedUri = "$deepLinkScheme://open?url=$encodedUrl".toUri() val actualUri = controller.processMessageActionToUri(message) // Updated telemetry - val clickedEvents = Messaging.messageClicked.testGetValue() + val clickedEvents = GleanMessaging.messageClicked.testGetValue() assertNotNull(clickedEvents) val clickedEvent = clickedEvents!!.single() assertEquals(message.id, clickedEvent.extra!!["message_key"]) @@ -210,19 +211,18 @@ class NimbusMessagingControllerTest { @Test fun `GIVEN action is deeplink WHEN calling processMessageActionToUri THEN return a deeplink URI`() { val message = createMessage("id-1", action = "://a-deep-link") - every { storage.generateUuidAndFormatAction(message.action) } returns Pair( - null, - message.action, - ) + `when`(storage.generateUuidAndFormatAction(message.action)) + .thenReturn(Pair(null, message.action)) + // Assert telemetry is initially null - assertNull(Messaging.messageClicked.testGetValue()) + assertNull(GleanMessaging.messageClicked.testGetValue()) - val expectedUri = "${BuildConfig.DEEP_LINK_SCHEME}${message.action}".toUri() + val expectedUri = "$deepLinkScheme${message.action}".toUri() val actualUri = controller.processMessageActionToUri(message) // Updated telemetry - assertNotNull(Messaging.messageClicked.testGetValue()) - val clickedEvent = Messaging.messageClicked.testGetValue()!! + assertNotNull(GleanMessaging.messageClicked.testGetValue()) + val clickedEvent = GleanMessaging.messageClicked.testGetValue()!! assertEquals(1, clickedEvent.size) assertEquals(message.id, clickedEvent.single().extra!!["message_key"]) @@ -232,19 +232,18 @@ class NimbusMessagingControllerTest { @Test fun `GIVEN action unknown format WHEN calling processMessageActionToUri THEN return the action URI`() { val message = createMessage("id-1", action = "unknown") - every { storage.generateUuidAndFormatAction(message.action) } returns Pair( - null, - message.action, - ) + `when`(storage.generateUuidAndFormatAction(message.action)) + .thenReturn(Pair(null, message.action)) + // Assert telemetry is initially null - assertNull(Messaging.messageClicked.testGetValue()) + assertNull(GleanMessaging.messageClicked.testGetValue()) val expectedUri = message.action.toUri() val actualUri = controller.processMessageActionToUri(message) // Updated telemetry - assertNotNull(Messaging.messageClicked.testGetValue()) - val clickedEvent = Messaging.messageClicked.testGetValue()!! + assertNotNull(GleanMessaging.messageClicked.testGetValue()) + val clickedEvent = GleanMessaging.messageClicked.testGetValue()!! assertEquals(1, clickedEvent.size) assertEquals(message.id, clickedEvent.single().extra!!["message_key"]) @@ -260,16 +259,14 @@ class NimbusMessagingControllerTest { controller.onMessageClicked(message.metadata) val updatedMetadata = message.metadata.copy(pressed = true) - coVerify { storage.updateMetadata(updatedMetadata) } + verify(storage).updateMetadata(updatedMetadata) } @Test fun `WHEN getIntentForMessageAction is called THEN return a generated Intent with the processed Message action`() { val message = createMessage("id-1", action = "unknown") - every { storage.generateUuidAndFormatAction(message.action) } returns Pair( - null, - message.action, - ) + `when`(storage.generateUuidAndFormatAction(message.action)) + .thenReturn(Pair(null, message.action)) val actualIntent = controller.getIntentForMessageAction(message.action) @@ -283,7 +280,7 @@ class NimbusMessagingControllerTest { fun `GIVEN stored messages contains a matching message WHEN calling getMessage THEN return the matching message`() = coroutineScope.runTest { val message1 = createMessage("1") - coEvery { storage.getMessage(message1.id) }.returns(message1) + `when`(storage.getMessage(message1.id)).thenReturn(message1) val actualMessage = controller.getMessage(message1.id) assertEquals(message1, actualMessage) @@ -292,7 +289,7 @@ class NimbusMessagingControllerTest { @Test fun `GIVEN stored messages doesn't contain a matching message WHEN calling getMessage THEN return null`() = coroutineScope.runTest { - coEvery { storage.getMessage("unknown id") }.returns(null) + `when`(storage.getMessage("unknown id")).thenReturn(null) val actualMessage = controller.getMessage("unknown id") assertNull(actualMessage) diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/NimbusMessagingStorageTest.kt b/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/NimbusMessagingStorageTest.kt similarity index 73% rename from mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/NimbusMessagingStorageTest.kt rename to mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/NimbusMessagingStorageTest.kt index 983c8a7d2aec..0e0dfe4a842c 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/NimbusMessagingStorageTest.kt +++ b/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/NimbusMessagingStorageTest.kt @@ -2,56 +2,75 @@ * 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 org.mozilla.fenix.gleanplumb - -import io.mockk.Called -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import io.mockk.spyk -import io.mockk.verify +package mozilla.components.service.nimbus.messaging + +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest +import mozilla.components.service.nimbus.messaging.ControlMessageBehavior.SHOW_NEXT_MESSAGE +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 org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull 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.doReturn +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import org.mozilla.experiments.nimbus.FeaturesInterface import org.mozilla.experiments.nimbus.GleanPlumbInterface import org.mozilla.experiments.nimbus.GleanPlumbMessageHelper import org.mozilla.experiments.nimbus.NullVariables import org.mozilla.experiments.nimbus.Res import org.mozilla.experiments.nimbus.internal.FeatureHolder import org.mozilla.experiments.nimbus.internal.NimbusException -import org.mozilla.fenix.helpers.FenixRobolectricTestRunner -import org.mozilla.fenix.nimbus.ControlMessageBehavior.SHOW_NEXT_MESSAGE -import org.mozilla.fenix.nimbus.MessageData -import org.mozilla.fenix.nimbus.MessageSurfaceId -import org.mozilla.fenix.nimbus.Messaging -import org.mozilla.fenix.nimbus.StyleData - -@RunWith(FenixRobolectricTestRunner::class) +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +@kotlinx.coroutines.ExperimentalCoroutinesApi class NimbusMessagingStorageTest { + @Mock private lateinit var metadataStorage: MessageMetadataStorage + + @Mock private lateinit var gleanPlumb: GleanPlumbInterface + private lateinit var storage: NimbusMessagingStorage - private lateinit var metadataStorage: MessageMetadataStorage - private lateinit var gleanPlumb: GleanPlumbInterface private lateinit var messagingFeature: FeatureHolder private var malformedWasReported = false private val reportMalformedMessage: (String) -> Unit = { malformedWasReported = true } + private lateinit var featuresInterface: FeaturesInterface + + @get:Rule + val coroutinesTestRule = MainCoroutineRule() @Before fun setup() { - gleanPlumb = mockk(relaxed = true) - metadataStorage = mockk(relaxed = true) + MockitoAnnotations.openMocks(this) malformedWasReported = false NullVariables.instance.setContext(testContext) - messagingFeature = createMessagingFeature() - coEvery { metadataStorage.getMetadata() } returns mapOf("message-1" to Message.Metadata(id = "message-1")) + val (messagingFeatureTmp, featuresInterfaceTmp) = createMessagingFeature() + + messagingFeature = spy(messagingFeatureTmp) + featuresInterface = featuresInterfaceTmp + + runBlocking { + `when`(metadataStorage.getMetadata()) + .thenReturn(mapOf("message-1" to Message.Metadata(id = "message-1"))) + + `when`(metadataStorage.addMetadata(any())) + .thenReturn(mock()) + } storage = NimbusMessagingStorage( testContext, @@ -63,15 +82,16 @@ class NimbusMessagingStorageTest { } @Test - fun `WHEN calling getMessages THEN provide a list of available messages for a given surface`() = runTest { - val homescreenMessage = storage.getMessages().first() + fun `WHEN calling getMessages THEN provide a list of available messages for a given surface`() = + runTest { + val homescreenMessage = storage.getMessages().first() - assertEquals("message-1", homescreenMessage.id) - assertEquals("message-1", homescreenMessage.metadata.id) + assertEquals("message-1", homescreenMessage.id) + assertEquals("message-1", homescreenMessage.metadata.id) - val notificationMessage = storage.getMessages().last() - assertEquals("message-2", notificationMessage.id) - } + val notificationMessage = storage.getMessages().last() + assertEquals("message-2", notificationMessage.id) + } @Test fun `WHEN calling getMessages THEN provide a list of sorted messages by priority`() = @@ -86,18 +106,11 @@ class NimbusMessagingStorageTest { "medium-priority" to createStyle(priority = 50), "low-priority" to createStyle(priority = 1), ) - val metadataStorage: MessageMetadataStorage = mockk(relaxed = true) - val messagingFeature = createMessagingFeature( + val (messagingFeature, _) = createMessagingFeature( styles = styles, messages = messages, ) - coEvery { metadataStorage.getMetadata() } returns mapOf( - "message-1" to Message.Metadata( - id = "message-1", - ), - ) - val storage = NimbusMessagingStorage( testContext, metadataStorage, @@ -127,13 +140,13 @@ class NimbusMessagingStorageTest { val styles = mapOf( "high-priority" to createStyle(priority = 100), ) - val metadataStorage: MessageMetadataStorage = mockk(relaxed = true) - val messagingFeature = createMessagingFeature( + val metadataStorage: MessageMetadataStorage = mock() + val (messagingFeature, _) = createMessagingFeature( styles = styles, messages = messages, ) - coEvery { metadataStorage.getMetadata() } returns metadataList + `when`(metadataStorage.getMetadata()).thenReturn(metadataList) val storage = NimbusMessagingStorage( testContext, @@ -163,13 +176,13 @@ class NimbusMessagingStorageTest { val styles = mapOf( "high-priority" to createStyle(priority = 100), ) - val metadataStorage: MessageMetadataStorage = mockk(relaxed = true) - val messagingFeature = createMessagingFeature( + val metadataStorage: MessageMetadataStorage = mock() + val (messagingFeature, _) = createMessagingFeature( styles = styles, messages = messages, ) - coEvery { metadataStorage.getMetadata() } returns metadataList + `when`(metadataStorage.getMetadata()).thenReturn(metadataList) val storage = NimbusMessagingStorage( testContext, @@ -211,13 +224,13 @@ class NimbusMessagingStorageTest { val styles = mapOf( "high-priority" to createStyle(priority = 100, maxDisplayCount = 2), ) - val metadataStorage: MessageMetadataStorage = mockk(relaxed = true) - val messagingFeature = createMessagingFeature( + val metadataStorage: MessageMetadataStorage = mock() + val (messagingFeature, _) = createMessagingFeature( styles = styles, messages = messages, ) - coEvery { metadataStorage.getMetadata() } returns metadataList + `when`(metadataStorage.getMetadata()).thenReturn(metadataList) val storage = NimbusMessagingStorage( testContext, @@ -234,21 +247,23 @@ class NimbusMessagingStorageTest { } @Test - fun `GIVEN a malformed message WHEN calling getMessages THEN provide a list of messages ignoring the malformed one`() = runTest { - val messages = storage.getMessages() - val firstMessage = messages.first() + fun `GIVEN a malformed message WHEN calling getMessages THEN provide a list of messages ignoring the malformed one`() = + runTest { + val messages = storage.getMessages() + val firstMessage = messages.first() - assertEquals("message-1", firstMessage.id) - assertEquals("message-1", firstMessage.metadata.id) - assertTrue(messages.size == 2) - assertTrue(malformedWasReported) - } + assertEquals("message-1", firstMessage.id) + assertEquals("message-1", firstMessage.metadata.id) + assertTrue(messages.size == 2) + assertTrue(malformedWasReported) + } @Test fun `GIVEN a malformed action WHEN calling sanitizeAction THEN return null`() { val actionsMap = mapOf("action-1" to "action-1-url") - val notFoundAction = storage.sanitizeAction("messageId", "no-found-action", actionsMap, false) + val notFoundAction = + storage.sanitizeAction("messageId", "no-found-action", actionsMap, false) val emptyAction = storage.sanitizeAction("messageId", "", actionsMap, false) val blankAction = storage.sanitizeAction("messageId", " ", actionsMap, false) @@ -283,9 +298,9 @@ class NimbusMessagingStorageTest { @Test fun `WHEN calling updateMetadata THEN delegate to metadataStorage`() = runTest { - storage.updateMetadata(mockk(relaxed = true)) + storage.updateMetadata(mock()) - coEvery { metadataStorage.updateMetadata(any()) } + verify(metadataStorage).updateMetadata(any()) } @Test @@ -358,9 +373,9 @@ class NimbusMessagingStorageTest { fun `GIVEN a null or black expression WHEN calling isMessageUnderExperiment THEN return false`() { val message = Message( "id", - mockk(relaxed = true), + mock(), action = "action", - mockk(relaxed = true), + mock(), emptyList(), Message.Metadata("id"), ) @@ -374,9 +389,9 @@ class NimbusMessagingStorageTest { fun `GIVEN messages id that ends with - WHEN calling isMessageUnderExperiment THEN return true`() { val message = Message( "end-", - mockk(relaxed = true), + mock(), action = "action", - mockk(relaxed = true), + mock(), emptyList(), Message.Metadata("end-"), ) @@ -390,9 +405,9 @@ class NimbusMessagingStorageTest { fun `GIVEN message under experiment WHEN calling isMessageUnderExperiment THEN return true`() { val message = Message( "same-id", - mockk(relaxed = true), + mock(), action = "action", - mockk(relaxed = true), + mock(), emptyList(), Message.Metadata("same-id"), ) @@ -404,17 +419,17 @@ class NimbusMessagingStorageTest { @Test fun `GIVEN an eligible message WHEN calling isMessageEligible THEN return true`() { - val helper: GleanPlumbMessageHelper = mockk(relaxed = true) + val helper: GleanPlumbMessageHelper = mock() val message = Message( "same-id", - mockk(relaxed = true), + mock(), action = "action", - mockk(relaxed = true), + mock(), listOf("trigger"), Message.Metadata("same-id"), ) - every { helper.evalJexl(any()) } returns true + `when`(helper.evalJexl(any())).thenReturn(true) val result = storage.isMessageEligible(message, helper) @@ -423,17 +438,17 @@ class NimbusMessagingStorageTest { @Test fun `GIVEN a malformed trigger WHEN calling isMessageEligible THEN return false`() { - val helper: GleanPlumbMessageHelper = mockk(relaxed = true) + val helper: GleanPlumbMessageHelper = mock() val message = Message( "same-id", - mockk(relaxed = true), + mock(), action = "action", - mockk(relaxed = true), + mock(), listOf("trigger"), Message.Metadata("same-id"), ) - every { helper.evalJexl(any()) } throws NimbusException.EvaluationException("") + `when`(helper.evalJexl(any())).then { throw NimbusException.EvaluationException("") } val result = storage.isMessageEligible(message, helper) @@ -442,40 +457,40 @@ class NimbusMessagingStorageTest { @Test fun `GIVEN a previously malformed trigger WHEN calling isMessageEligible THEN return false and not evaluate`() { - val helper: GleanPlumbMessageHelper = mockk(relaxed = true) + val helper: GleanPlumbMessageHelper = mock() val message = Message( "same-id", - mockk(relaxed = true), + mock(), action = "action", - mockk(relaxed = true), + mock(), listOf("trigger"), Message.Metadata("same-id"), ) storage.malFormedMap["trigger"] = "same-id" - every { helper.evalJexl(any()) } throws NimbusException.EvaluationException("") + `when`(helper.evalJexl(any())).then { throw NimbusException.EvaluationException("") } val result = storage.isMessageEligible(message, helper) assertFalse(result) - verify(exactly = 0) { helper.evalJexl("trigger") } + verify(helper, never()).evalJexl("trigger") assertFalse(malformedWasReported) } @Test fun `GIVEN a non previously malformed trigger WHEN calling isMessageEligible THEN return false and not evaluate`() { - val helper: GleanPlumbMessageHelper = mockk(relaxed = true) + val helper: GleanPlumbMessageHelper = mock() val message = Message( "same-id", - mockk(relaxed = true), + mock(), action = "action", - mockk(relaxed = true), + mock(), listOf("trigger"), Message.Metadata("same-id"), ) - every { helper.evalJexl(any()) } throws NimbusException.EvaluationException("") + `when`(helper.evalJexl(any())).then { throw NimbusException.EvaluationException("") } assertFalse(storage.malFormedMap.containsKey("trigger")) @@ -488,79 +503,79 @@ class NimbusMessagingStorageTest { @Test fun `GIVEN none available messages are eligible WHEN calling getNextMessage THEN return null`() { - val spiedStorage = spyk(storage) + val spiedStorage = spy(storage) val message = Message( "same-id", - mockk(relaxed = true), + mock(), action = "action", - mockk(relaxed = true), + mock(), listOf("trigger"), Message.Metadata("same-id"), ) - every { spiedStorage.isMessageEligible(any(), any()) } returns false + doReturn(false).`when`(spiedStorage).isMessageEligible(any(), any(), any()) - val result = spiedStorage.getNextMessage(MessageSurfaceId.HOMESCREEN, listOf(message)) + val result = spiedStorage.getNextMessage(HOMESCREEN, listOf(message)) assertNull(result) } @Test fun `GIVEN an eligible message WHEN calling getNextMessage THEN return the message`() { - val spiedStorage = spyk(storage) + val spiedStorage = spy(storage) val message = Message( "same-id", - createMessageData(surface = MessageSurfaceId.HOMESCREEN), + createMessageData(surface = HOMESCREEN), action = "action", - mockk(relaxed = true), + mock(), listOf("trigger"), Message.Metadata("same-id"), ) - every { spiedStorage.isMessageEligible(any(), any()) } returns true - every { spiedStorage.isMessageUnderExperiment(any(), any()) } returns false + doReturn(true).`when`(spiedStorage).isMessageEligible(any(), any(), any()) + doReturn(false).`when`(spiedStorage).isMessageUnderExperiment(any(), any()) - val result = spiedStorage.getNextMessage(MessageSurfaceId.HOMESCREEN, listOf(message)) + val result = spiedStorage.getNextMessage(HOMESCREEN, listOf(message)) assertEquals(message.id, result!!.id) } @Test fun `GIVEN a message under experiment WHEN calling getNextMessage THEN call recordExposure`() { - val spiedStorage = spyk(storage) + val spiedStorage = spy(storage) val messageData: MessageData = createMessageData(isControl = false) val message = Message( "same-id", messageData, action = "action", - mockk(relaxed = true), + mock(), listOf("trigger"), Message.Metadata("same-id"), ) - every { spiedStorage.isMessageEligible(any(), any()) } returns true - every { spiedStorage.isMessageUnderExperiment(any(), any()) } returns true + doReturn(true).`when`(spiedStorage).isMessageEligible(any(), any(), any()) + doReturn(true).`when`(spiedStorage).isMessageUnderExperiment(any(), any()) - val result = spiedStorage.getNextMessage(MessageSurfaceId.HOMESCREEN, listOf(message)) + val result = spiedStorage.getNextMessage(HOMESCREEN, listOf(message)) - verify { messagingFeature.recordExposure() } + verify(featuresInterface).recordExposureEvent("messaging") assertEquals(message.id, result!!.id) } @Test fun `GIVEN a control message WHEN calling getNextMessage THEN return the next eligible message`() { - val spiedStorage = spyk(storage) + val spiedStorage = spy(storage) val messageData: MessageData = createMessageData() val controlMessageData: MessageData = createMessageData(isControl = true) - every { spiedStorage.getOnControlBehavior() } returns SHOW_NEXT_MESSAGE + doReturn(SHOW_NEXT_MESSAGE).`when`(spiedStorage).getOnControlBehavior() val message = Message( "id", messageData, action = "action", - mockk(relaxed = true), + mock(), listOf("trigger"), Message.Metadata("same-id"), ) @@ -569,20 +584,20 @@ class NimbusMessagingStorageTest { "control-id", controlMessageData, action = "action", - mockk(relaxed = true), + mock(), listOf("trigger"), Message.Metadata("same-id"), ) - every { spiedStorage.isMessageEligible(any(), any()) } returns true - every { spiedStorage.isMessageUnderExperiment(any(), any()) } returns true + doReturn(true).`when`(spiedStorage).isMessageEligible(any(), any(), any()) + doReturn(true).`when`(spiedStorage).isMessageUnderExperiment(any(), any()) val result = spiedStorage.getNextMessage( - MessageSurfaceId.HOMESCREEN, + HOMESCREEN, listOf(controlMessage, message), ) - verify { messagingFeature.recordExposure() } + verify(messagingFeature).recordExposure() assertEquals(message.id, result!!.id) } @@ -597,7 +612,7 @@ class NimbusMessagingStorageTest { ) // We should not be using the feature holder until getMessages is called. - verify { messagingFeature wasNot Called } + verify(messagingFeature, never()).value(any()) } @Test @@ -613,15 +628,15 @@ class NimbusMessagingStorageTest { "medium-priority" to createStyle(priority = 50), "low-priority" to createStyle(priority = 1), ) - val metadataStorage: MessageMetadataStorage = mockk(relaxed = true) - val messagingFeature = createMessagingFeature( + + val (messagingFeature, _) = createMessagingFeature( styles = styles, messages = messages, ) - coEvery { metadataStorage.getMetadata() } returns mapOf( - "message-1" to Message.Metadata( - id = "message-1", + `when`(metadataStorage.getMetadata()).thenReturn( + mapOf( + "message-1" to Message.Metadata(id = "message-1"), ), ) @@ -643,7 +658,7 @@ class NimbusMessagingStorageTest { action: String = "action-1", style: String = "style-1", triggers: List = listOf("trigger-1"), - surface: MessageSurfaceId = MessageSurfaceId.HOMESCREEN, + surface: MessageSurfaceId = HOMESCREEN, isControl: Boolean = false, ) = MessageData( action = Res.string(action), @@ -658,29 +673,36 @@ class NimbusMessagingStorageTest { styles: Map = mapOf("style-1" to createStyle()), actions: Map = mapOf("action-1" to "action-1-url"), messages: Map = mapOf( - "message-1" to createMessageData(surface = MessageSurfaceId.HOMESCREEN), - "message-2" to createMessageData(surface = MessageSurfaceId.NOTIFICATION), + "message-1" to createMessageData(surface = HOMESCREEN), + "message-2" to createMessageData(surface = NOTIFICATION), "malformed" to createMessageData(action = "malformed-action"), ), - ): FeatureHolder { + ): Pair, FeaturesInterface> { val messaging = Messaging( actions = actions, triggers = triggers, messages = messages, styles = styles, ) - val messagingFeature = FeatureHolder({ mockk(relaxed = true) }, "messaging") { + val featureInterface: FeaturesInterface = mock() + // "messaging" is a hard coded value generated from Nimbus. + val messagingFeature = FeatureHolder({ featureInterface }, "messaging") { messaging } messagingFeature.withCachedValue(messaging) - return spyk(messagingFeature) + return messagingFeature to featureInterface } private fun createStyle(priority: Int = 1, maxDisplayCount: Int = 5): StyleData { - val style1: StyleData = mockk(relaxed = true) - every { style1.priority } returns priority - every { style1.maxDisplayCount } returns maxDisplayCount + val style1: StyleData = mock() + `when`(style1.priority).thenReturn(priority) + `when`(style1.maxDisplayCount).thenReturn(maxDisplayCount) return style1 } + + companion object { + private const val HOMESCREEN = "homescreen" + private const val NOTIFICATION = "notification" + } } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/OnDiskMessageMetadataStorageTest.kt b/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/OnDiskMessageMetadataStorageTest.kt similarity index 79% rename from mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/OnDiskMessageMetadataStorageTest.kt rename to mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/OnDiskMessageMetadataStorageTest.kt index 579a09dcebd5..e40ee1dffca7 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/OnDiskMessageMetadataStorageTest.kt +++ b/mobile/android/android-components/components/service/nimbus/src/test/java/mozilla/components/service/nimbus/messaging/OnDiskMessageMetadataStorageTest.kt @@ -2,14 +2,9 @@ * 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 org.mozilla.fenix.gleanplumb - -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.just -import io.mockk.spyk -import io.mockk.verify +package mozilla.components.service.nimbus.messaging + +import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.test.runTest import mozilla.components.support.test.robolectric.testContext import org.json.JSONArray @@ -20,9 +15,13 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` -@RunWith(FenixRobolectricTestRunner::class) +@RunWith(AndroidJUnit4::class) +@kotlinx.coroutines.ExperimentalCoroutinesApi class OnDiskMessageMetadataStorageTest { private lateinit var storage: OnDiskMessageMetadataStorage @@ -37,50 +36,50 @@ class OnDiskMessageMetadataStorageTest { @Test fun `GIVEN metadata is not loaded from disk WHEN calling getMetadata THEN load it`() = runTest { - val spiedStorage = spyk(storage) + val spiedStorage = spy(storage) - coEvery { spiedStorage.readFromDisk() } returns emptyMap() + `when`(spiedStorage.readFromDisk()).thenReturn(emptyMap()) spiedStorage.getMetadata() - verify { spiedStorage.readFromDisk() } + verify(spiedStorage).readFromDisk() } @Test fun `GIVEN metadata is loaded from disk WHEN calling getMetadata THEN do not load it from disk`() = runTest { - val spiedStorage = spyk(storage) + val spiedStorage = spy(storage) spiedStorage.metadataMap = hashMapOf("" to Message.Metadata("id")) spiedStorage.getMetadata() - verify(exactly = 0) { spiedStorage.readFromDisk() } + verify(spiedStorage, never()).readFromDisk() } @Test fun `WHEN calling addMetadata THEN add in memory and disk`() = runTest { - val spiedStorage = spyk(storage) + val spiedStorage = spy(storage) assertTrue(spiedStorage.metadataMap.isEmpty()) - coEvery { spiedStorage.writeToDisk() } just Runs + `when`(spiedStorage.writeToDisk()).then { } spiedStorage.addMetadata(Message.Metadata("id")) assertFalse(spiedStorage.metadataMap.isEmpty()) - coVerify { spiedStorage.writeToDisk() } + verify(spiedStorage).writeToDisk() } @Test fun `WHEN calling updateMetadata THEN delegate to addMetadata`() = runTest { - val spiedStorage = spyk(storage) + val spiedStorage = spy(storage) val metadata = Message.Metadata("id") - coEvery { spiedStorage.writeToDisk() } just Runs + `when`(spiedStorage.writeToDisk()).then { } spiedStorage.updateMetadata(metadata) - coVerify { spiedStorage.addMetadata(metadata) } + verify(spiedStorage).addMetadata(metadata) } @Test diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/BootUtils.kt b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/BootUtils.kt similarity index 97% rename from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/BootUtils.kt rename to mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/BootUtils.kt index 1952fdbac107..7c6af36e271d 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/BootUtils.kt +++ b/mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/utils/BootUtils.kt @@ -2,7 +2,7 @@ * 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 org.mozilla.fenix.utils +package mozilla.components.support.utils import android.content.Context import android.os.Build diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/utils/BootUtilsTest.kt b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BootUtilsTest.kt similarity index 67% rename from mobile/android/fenix/app/src/test/java/org/mozilla/fenix/utils/BootUtilsTest.kt rename to mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BootUtilsTest.kt index 4395484c2948..cec7766059bf 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/utils/BootUtilsTest.kt +++ b/mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/BootUtilsTest.kt @@ -1,33 +1,35 @@ -package org.mozilla.fenix.utils +package mozilla.components.support.utils import android.os.Build -import io.mockk.every -import io.mockk.mockk +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.any import mozilla.components.support.test.robolectric.testContext +import mozilla.components.support.utils.BootUtils.Companion.getBootIdentifier import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mozilla.fenix.helpers.FenixRobolectricTestRunner -import org.mozilla.fenix.utils.BootUtils.Companion.getBootIdentifier +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations import org.robolectric.annotation.Config private const val NO_BOOT_IDENTIFIER = "no boot identifier available" -@RunWith(FenixRobolectricTestRunner::class) +@RunWith(AndroidJUnit4::class) class BootUtilsTest { - private lateinit var bootUtils: BootUtils + @Mock private lateinit var bootUtils: BootUtils @Before fun setUp() { - bootUtils = mockk(relaxed = true) + MockitoAnnotations.openMocks(this) } @Test @Config(sdk = [Build.VERSION_CODES.M]) fun `WHEN no boot id file & Android version is less than N(24) THEN getBootIdentifier returns NO_BOOT_IDENTIFIER`() { - every { bootUtils.bootIdFileExists }.returns(false) + `when`(bootUtils.bootIdFileExists).thenReturn(false) assertEquals(NO_BOOT_IDENTIFIER, getBootIdentifier(testContext, bootUtils)) } @@ -35,8 +37,8 @@ class BootUtilsTest { @Test @Config(sdk = [Build.VERSION_CODES.M]) fun `WHEN boot id file returns null & Android version is less than N(24) THEN getBootIdentifier returns NO_BOOT_IDENTIFIER`() { - every { bootUtils.bootIdFileExists }.returns(true) - every { bootUtils.deviceBootId }.returns(null) + `when`(bootUtils.bootIdFileExists).thenReturn(true) + `when`(bootUtils.deviceBootId).thenReturn(null) assertEquals(NO_BOOT_IDENTIFIER, getBootIdentifier(testContext, bootUtils)) } @@ -44,9 +46,9 @@ class BootUtilsTest { @Test @Config(sdk = [Build.VERSION_CODES.M]) fun `WHEN boot id file has text & Android version is less than N(24) THEN getBootIdentifier returns the boot id`() { - every { bootUtils.bootIdFileExists }.returns(true) + `when`(bootUtils.bootIdFileExists).thenReturn(true) val bootId = "test" - every { bootUtils.deviceBootId }.returns(bootId) + `when`(bootUtils.deviceBootId).thenReturn(bootId) assertEquals(bootId, getBootIdentifier(testContext, bootUtils)) } @@ -54,9 +56,9 @@ class BootUtilsTest { @Test @Config(sdk = [Build.VERSION_CODES.M]) fun `WHEN boot id file has text with whitespace & Android version is less than N(24) THEN getBootIdentifier returns the trimmed boot id`() { - every { bootUtils.bootIdFileExists }.returns(true) + `when`(bootUtils.bootIdFileExists).thenReturn(true) val bootId = " test " - every { bootUtils.deviceBootId }.returns(bootId) + `when`(bootUtils.deviceBootId).thenReturn(bootId) assertEquals(bootId, getBootIdentifier(testContext, bootUtils)) } @@ -65,7 +67,7 @@ class BootUtilsTest { @Config(sdk = [Build.VERSION_CODES.N]) fun `WHEN Android version is N(24) THEN getBootIdentifier returns the boot count`() { val bootCount = "9" - every { bootUtils.getDeviceBootCount(any()) }.returns(bootCount) + `when`(bootUtils.getDeviceBootCount(any())).thenReturn(bootCount) assertEquals(bootCount, getBootIdentifier(testContext, bootUtils)) } @@ -73,7 +75,7 @@ class BootUtilsTest { @Config(sdk = [Build.VERSION_CODES.O]) fun `WHEN Android version is more than N(24) THEN getBootIdentifier returns the boot count`() { val bootCount = "9" - every { bootUtils.getDeviceBootCount(any()) }.returns(bootCount) + `when`(bootUtils.getDeviceBootCount(any())).thenReturn(bootCount) assertEquals(bootCount, getBootIdentifier(testContext, bootUtils)) } } diff --git a/mobile/android/fenix/app/.experimenter.yaml b/mobile/android/fenix/app/.experimenter.yaml index 02ee1c061e87..51cca553c886 100644 --- a/mobile/android/fenix/app/.experimenter.yaml +++ b/mobile/android/fenix/app/.experimenter.yaml @@ -77,7 +77,7 @@ mr2022: type: json description: This property provides a lookup table of whether or not the given section should be enabled. nimbus-system: - description: "Configuration of the Nimbus System in Fenix.\n" + description: "Configuration of the Nimbus System in Android.\n" hasExposure: true exposureDescription: "" variables: diff --git a/mobile/android/fenix/app/messaging.fml.yaml b/mobile/android/fenix/app/messaging.fml.yaml deleted file mode 100644 index bd692675522d..000000000000 --- a/mobile/android/fenix/app/messaging.fml.yaml +++ /dev/null @@ -1,269 +0,0 @@ ---- -features: - nimbus-system: - description: | - Configuration of the Nimbus System in Fenix. - variables: - refresh-interval-foreground: - description: | - The minimum interval in minutes between fetching experiment - recipes in the foreground. - type: Int - default: 60 # 1 hour - - messaging: - description: | - Configuration for the messaging system. - - In practice this is a set of growable lookup tables for the - message controller to piece together. - - variables: - message-under-experiment: - description: Id or prefix of the message under experiment. - type: Option - default: null - - messages: - description: A growable collection of messages - type: Map - default: {} - - triggers: - description: > - A collection of out the box trigger - expressions. Each entry maps to a - valid JEXL expression. - type: Map - default: {} - styles: - description: > - A map of styles to configure message - appearance. - type: Map - default: {} - - actions: - type: Map - description: A growable map of action URLs. - default: {} - on-control: - type: ControlMessageBehavior - description: What should be displayed when a control message is selected. - default: show-next-message - notification-config: - description: Configuration of the notification worker for all notification messages. - type: NotificationConfig - default: {} - defaults: - - value: - triggers: - # Using attributes built into the Nimbus SDK - USER_RECENTLY_INSTALLED: days_since_install < 7 - USER_RECENTLY_UPDATED: days_since_update < 7 && days_since_install != days_since_update - USER_TIER_ONE_COUNTRY: ('US' in locale || 'GB' in locale || 'CA' in locale || 'DE' in locale || 'FR' in locale) - USER_EN_SPEAKER: "'en' in locale" - USER_ES_SPEAKER: "'es' in locale" - USER_DE_SPEAKER: "'de' in locale" - USER_FR_SPEAKER: "'fr' in locale" - DEVICE_ANDROID: os == 'Android' - DEVICE_IOS: os == 'iOS' - ALWAYS: "true" - NEVER: "false" - DAY_1_AFTER_INSTALL: days_since_install == 1 - DAY_2_AFTER_INSTALL: days_since_install == 2 - DAY_3_AFTER_INSTALL: days_since_install == 3 - DAY_4_AFTER_INSTALL: days_since_install == 4 - DAY_5_AFTER_INSTALL: days_since_install == 5 - - # Using custom attributes for the browser - I_AM_DEFAULT_BROWSER: "is_default_browser" - I_AM_NOT_DEFAULT_BROWSER: "is_default_browser == false" - USER_ESTABLISHED_INSTALL: "number_of_app_launches >=4" - - FUNNEL_PAID: "adjust_campaign != ''" - FUNNEL_ORGANIC: "adjust_campaign == ''" - - # Using Glean events, specific to the browser - INACTIVE_1_DAY: "'app_launched'|eventLastSeen('Hours') >= 24" - INACTIVE_2_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 2" - INACTIVE_3_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 3" - INACTIVE_4_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 4" - INACTIVE_5_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 5" - - # Has the user signed in the last 4 years - FXA_SIGNED_IN: "'sync_auth.sign_in'|eventLastSeen('Years', 0) <= 4" - FXA_NOT_SIGNED_IN: "'sync_auth.sign_in'|eventLastSeen('Years', 0) > 4" - - # https://mozilla-hub.atlassian.net/wiki/spaces/FJT/pages/11469471/Core+Active - USER_INFREQUENT: "'app_launched'|eventCountNonZero('Days', 28) >= 1 && 'app_launched'|eventCountNonZero('Days', 28) < 7" - USER_CASUAL: "'app_launched'|eventCountNonZero('Days', 28) >= 7 && 'app_launched'|eventCountNonZero('Days', 28) < 14" - USER_REGULAR: "'app_launched'|eventCountNonZero('Days', 28) >= 14 && 'app_launched'|eventCountNonZero('Days', 28) < 21" - USER_CORE_ACTIVE: "'app_launched'|eventCountNonZero('Days', 28) >= 21" - - LAUNCHED_ONCE_THIS_WEEK: "'app_launched'|eventSum('Days', 7) == 1" - - actions: - ENABLE_PRIVATE_BROWSING: ://enable_private_browsing - INSTALL_SEARCH_WIDGET: ://install_search_widget - MAKE_DEFAULT_BROWSER: ://make_default_browser - VIEW_BOOKMARKS: ://urls_bookmarks - VIEW_COLLECTIONS: ://home_collections - VIEW_HISTORY: ://urls_history - VIEW_HOMESCREEN: ://home - OPEN_SETTINGS_ACCESSIBILITY: ://settings_accessibility - OPEN_SETTINGS_ADDON_MANAGER: ://settings_addon_manager - OPEN_SETTINGS_DELETE_BROWSING_DATA: ://settings_delete_browsing_data - OPEN_SETTINGS_LOGINS: ://settings_logins - OPEN_SETTINGS_NOTIFICATIONS: ://settings_notifications - OPEN_SETTINGS_PRIVACY: ://settings_privacy - OPEN_SETTINGS_SEARCH_ENGINE: ://settings_search_engine - OPEN_SETTINGS_TRACKING_PROTECTION: ://settings_tracking_protection - OPEN_SETTINGS_WALLPAPERS: ://settings_wallpapers - OPEN_SETTINGS: ://settings - TURN_ON_SYNC: ://turn_on_sync - styles: - DEFAULT: - priority: 50 - max-display-count: 5 - SURVEY: - priority: 55 - max-display-count: 10 - PERSISTENT: - priority: 50 - max-display-count: 20 - WARNING: - priority: 60 - max-display-count: 10 - URGENT: - priority: 100 - max-display-count: 10 - NOTIFICATION: - priority: 50 - max-display-count: 1 - messages: - default-browser: - text: default_browser_experiment_card_text - surface: homescreen - action: "MAKE_DEFAULT_BROWSER" - trigger: [ "I_AM_NOT_DEFAULT_BROWSER","USER_ESTABLISHED_INSTALL" ] - style: "PERSISTENT" - button-label: preferences_set_as_default_browser - default-browser-notification: - title: nimbus_notification_default_browser_title - text: nimbus_notification_default_browser_text - surface: notification - style: NOTIFICATION - trigger: - - I_AM_NOT_DEFAULT_BROWSER - - DAY_3_AFTER_INSTALL - action: MAKE_DEFAULT_BROWSER - - - channel: developer - value: - styles: - DEFAULT: - priority: 50 - max-display-count: 100 - EXPIRES_QUICKLY: - priority: 100 - max-display-count: 1 - notification-config: - refresh-interval: 120 # minutes (2 hours) - -objects: - MessageData: - description: > - An object to describe a message. It uses human - readable strings to describe the triggers, action and - style of the message as well as the text of the message - and call to action. - fields: - action: - type: Text - description: > - A URL of a page or a deeplink. - This may have substitution variables in. - # This should never be defaulted. - default: empty_string - title: - type: Option - description: "The title text displayed to the user" - default: null - text: - type: Text - description: "The message text displayed to the user" - # This should never be defaulted. - default: empty_string - is-control: - type: Boolean - description: "Indicates if this message is the control message, if true shouldn't be displayed" - default: false - button-label: - type: Option - description: > - The text on the button. If no text - is present, the whole message is clickable. - default: null - style: - type: String - description: > - The style as described in a - `StyleData` from the styles table. - default: DEFAULT - surface: - description: - The surface identifier for this message. - type: MessageSurfaceId - default: homescreen - trigger: - type: List - description: > - A list of strings corresponding to - targeting expressions. The message will be - shown if all expressions `true`. - default: [] - StyleData: - description: > - A group of properties (predominantly visual) to - describe the style of the message. - fields: - priority: - type: Int - description: > - The importance of this message. - 0 is not very important, 100 is very important. - default: 50 - max-display-count: - type: Int - description: > - How many sessions will this message be shown to the user - before it is expired. - default: 5 - NotificationConfig: - description: Attributes controlling the global configuration of notification messages. - fields: - refresh-interval: - type: Int - description: > - How often, in minutes, the notification message worker will wake up and check for new - messages. - default: 240 # 4 hours - -enums: - ControlMessageBehavior: - description: An enum to influence what should be displayed when a control message is selected. - variants: - show-next-message: - description: The next eligible message should be shown. - show-none: - description: The surface should show no message. - MessageSurfaceId: - description: The identity of a message surface - variants: - homescreen: - description: A banner in the homescreen. - notification: - description: A local notification in the background, like a push notification. - survey: - description: A survey dialog that is intended to be disruptive. diff --git a/mobile/android/fenix/app/metrics.yaml b/mobile/android/fenix/app/metrics.yaml index d1c4b9f7ef59..914032f701c9 100644 --- a/mobile/android/fenix/app/metrics.yaml +++ b/mobile/android/fenix/app/metrics.yaml @@ -8712,106 +8712,6 @@ addresses: tags: - Autofill -messaging: - message_shown: - type: event - description: | - A message was shown to the user. - extra_keys: - message_key: - description: The id of the message - type: string - bugs: - - https://github.com/mozilla-mobile/fenix/issues/24224 - data_reviews: - - https://github.com/mozilla-mobile/fenix/pull/24426 - - https://github.com/mozilla-mobile/firefox-android/pull/1101 - notification_emails: - - android-probes@mozilla.com - - kbrosnan@mozilla.com - data_sensitivity: - - interaction - expires: never - message_dismissed: - type: event - description: | - A message was dismissed by the user. - extra_keys: - message_key: - description: The id of the message - type: string - bugs: - - https://github.com/mozilla-mobile/fenix/issues/24224 - data_reviews: - - https://github.com/mozilla-mobile/fenix/issues/24224 - - https://github.com/mozilla-mobile/firefox-android/pull/1101 - notification_emails: - - android-probes@mozilla.com - - kbrosnan@mozilla.com - data_sensitivity: - - interaction - expires: never - message_clicked: - type: event - description: | - A message was clicked by the user. - extra_keys: - message_key: - description: The id of the message - type: string - action_uuid: - description: The uuid of the action - type: string - bugs: - - https://github.com/mozilla-mobile/fenix/issues/24224 - data_reviews: - - https://github.com/mozilla-mobile/fenix/issues/24224 - - https://github.com/mozilla-mobile/firefox-android/pull/1101 - notification_emails: - - android-probes@mozilla.com - - kbrosnan@mozilla.com - data_sensitivity: - - interaction - expires: never - message_expired: - type: event - description: | - A message maxDisplayCount has been surpassed. - extra_keys: - message_key: - description: The id of the message - type: string - bugs: - - https://github.com/mozilla-mobile/fenix/issues/24224 - data_reviews: - - https://github.com/mozilla-mobile/fenix/issues/24224 - - https://github.com/mozilla-mobile/firefox-android/pull/1101 - notification_emails: - - android-probes@mozilla.com - - kbrosnan@mozilla.com - data_sensitivity: - - interaction - expires: never - malformed: - type: event - description: | - A message was malformed. - extra_keys: - message_key: - description: The id of the message - type: string - bugs: - - https://github.com/mozilla-mobile/fenix/issues/24224 - data_reviews: - - https://github.com/mozilla-mobile/fenix/issues/24224 - - https://github.com/mozilla-mobile/firefox-android/pull/1101 - notification_emails: - - android-probes@mozilla.com - - kbrosnan@mozilla.com - data_sensitivity: - - interaction - expires: never - wallpapers: wallpaper_settings_opened: type: event diff --git a/mobile/android/fenix/app/nimbus.fml.yaml b/mobile/android/fenix/app/nimbus.fml.yaml index 873350c203f8..6619041b39b9 100644 --- a/mobile/android/fenix/app/nimbus.fml.yaml +++ b/mobile/android/fenix/app/nimbus.fml.yaml @@ -9,8 +9,133 @@ channels: - beta - nightly - developer -includes: - - messaging.fml.yaml +import: + - path: ../../android-components/components/service/nimbus/messaging.fml.yaml + channel: release + features: + messaging: + - value: + triggers: + # Using attributes built into the Nimbus SDK + USER_RECENTLY_INSTALLED: days_since_install < 7 + USER_RECENTLY_UPDATED: days_since_update < 7 && days_since_install != days_since_update + USER_TIER_ONE_COUNTRY: ('US' in locale || 'GB' in locale || 'CA' in locale || 'DE' in locale || 'FR' in locale) + USER_EN_SPEAKER: "'en' in locale" + USER_ES_SPEAKER: "'es' in locale" + USER_DE_SPEAKER: "'de' in locale" + USER_FR_SPEAKER: "'fr' in locale" + DEVICE_ANDROID: os == 'Android' + DEVICE_IOS: os == 'iOS' + ALWAYS: "true" + NEVER: "false" + DAY_1_AFTER_INSTALL: days_since_install == 1 + DAY_2_AFTER_INSTALL: days_since_install == 2 + DAY_3_AFTER_INSTALL: days_since_install == 3 + DAY_4_AFTER_INSTALL: days_since_install == 4 + DAY_5_AFTER_INSTALL: days_since_install == 5 + + # Using custom attributes for the browser + I_AM_DEFAULT_BROWSER: "is_default_browser" + I_AM_NOT_DEFAULT_BROWSER: "is_default_browser == false" + USER_ESTABLISHED_INSTALL: "number_of_app_launches >=4" + + FUNNEL_PAID: "adjust_campaign != ''" + FUNNEL_ORGANIC: "adjust_campaign == ''" + + # Using Glean events, specific to the browser + INACTIVE_1_DAY: "'app_launched'|eventLastSeen('Hours') >= 24" + INACTIVE_2_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 2" + INACTIVE_3_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 3" + INACTIVE_4_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 4" + INACTIVE_5_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 5" + + # Has the user signed in the last 4 years + FXA_SIGNED_IN: "'sync_auth.sign_in'|eventLastSeen('Years', 0) <= 4" + FXA_NOT_SIGNED_IN: "'sync_auth.sign_in'|eventLastSeen('Years', 0) > 4" + + # https://mozilla-hub.atlassian.net/wiki/spaces/FJT/pages/11469471/Core+Active + USER_INFREQUENT: "'app_launched'|eventCountNonZero('Days', 28) >= 1 && 'app_launched'|eventCountNonZero('Days', 28) < 7" + USER_CASUAL: "'app_launched'|eventCountNonZero('Days', 28) >= 7 && 'app_launched'|eventCountNonZero('Days', 28) < 14" + USER_REGULAR: "'app_launched'|eventCountNonZero('Days', 28) >= 14 && 'app_launched'|eventCountNonZero('Days', 28) < 21" + USER_CORE_ACTIVE: "'app_launched'|eventCountNonZero('Days', 28) >= 21" + + LAUNCHED_ONCE_THIS_WEEK: "'app_launched'|eventSum('Days', 7) == 1" + + actions: + ENABLE_PRIVATE_BROWSING: ://enable_private_browsing + INSTALL_SEARCH_WIDGET: ://install_search_widget + MAKE_DEFAULT_BROWSER: ://make_default_browser + VIEW_BOOKMARKS: ://urls_bookmarks + VIEW_COLLECTIONS: ://home_collections + VIEW_HISTORY: ://urls_history + VIEW_HOMESCREEN: ://home + OPEN_SETTINGS_ACCESSIBILITY: ://settings_accessibility + OPEN_SETTINGS_ADDON_MANAGER: ://settings_addon_manager + OPEN_SETTINGS_DELETE_BROWSING_DATA: ://settings_delete_browsing_data + OPEN_SETTINGS_LOGINS: ://settings_logins + OPEN_SETTINGS_NOTIFICATIONS: ://settings_notifications + OPEN_SETTINGS_PRIVACY: ://settings_privacy + OPEN_SETTINGS_SEARCH_ENGINE: ://settings_search_engine + OPEN_SETTINGS_TRACKING_PROTECTION: ://settings_tracking_protection + OPEN_SETTINGS_WALLPAPERS: ://settings_wallpapers + OPEN_SETTINGS: ://settings + TURN_ON_SYNC: ://turn_on_sync + styles: + DEFAULT: + priority: 50 + max-display-count: 5 + SURVEY: + priority: 55 + max-display-count: 10 + PERSISTENT: + priority: 50 + max-display-count: 20 + WARNING: + priority: 60 + max-display-count: 10 + URGENT: + priority: 100 + max-display-count: 10 + NOTIFICATION: + priority: 50 + max-display-count: 1 + messages: + arturo-notification: + text: "This is our notification test" + surface: notification + action: "https://developer.android.com/guide/background/persistent/getting-started/define-work" + trigger: [ "USER_FR_SPEAKER" ] + style: "PERSISTENT" + button-label: "My new button" + default-browser: + text: default_browser_experiment_card_text + surface: homescreen + action: "MAKE_DEFAULT_BROWSER" + trigger: [ "I_AM_NOT_DEFAULT_BROWSER","USER_ESTABLISHED_INSTALL" ] + style: "PERSISTENT" + button-label: preferences_set_as_default_browser + default-browser-notification: + title: "nimbus_notification_default_browser_title" + text: nimbus_notification_default_browser_text + surface: notification + style: NOTIFICATION + trigger: + - I_AM_NOT_DEFAULT_BROWSER + - DAY_3_AFTER_INSTALL + action: MAKE_DEFAULT_BROWSER + + - channel: developer + value: + styles: + DEFAULT: + priority: 50 + max-display-count: 100 + EXPIRES_QUICKLY: + priority: 100 + max-display-count: 1 + notification-config: + refresh-interval: 120 # minutes (2 hours) + features: homescreen: description: The homescreen that the user goes to when they press home or new tab. diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingMessageTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingMessageTest.kt index 023055749f37..87b1079574fa 100644 --- a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingMessageTest.kt +++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingMessageTest.kt @@ -8,6 +8,8 @@ import android.content.Context import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import kotlinx.coroutines.test.runTest +import mozilla.components.service.nimbus.messaging.FxNimbusMessaging +import mozilla.components.service.nimbus.messaging.Messaging import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -16,11 +18,9 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.mozilla.fenix.ext.components -import org.mozilla.fenix.gleanplumb.CustomAttributeProvider import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.TestHelper -import org.mozilla.fenix.nimbus.FxNimbus -import org.mozilla.fenix.nimbus.Messaging +import org.mozilla.fenix.messaging.CustomAttributeProvider /** * This test is to test the integrity of messages hardcoded in the FML. @@ -45,7 +45,7 @@ class NimbusMessagingMessageTest { fun setUp() { context = TestHelper.appContext mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - feature = FxNimbus.features.messaging.value() + feature = FxNimbusMessaging.features.messaging.value() } /** diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingNotificationTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingNotificationTest.kt index 8549bf30085a..9f3762a21a0b 100644 --- a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingNotificationTest.kt +++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingNotificationTest.kt @@ -10,6 +10,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import androidx.test.rule.GrantPermissionRule.grant import androidx.test.uiautomator.UiDevice +import mozilla.components.service.nimbus.messaging.FxNimbusMessaging import org.json.JSONObject import org.junit.Before import org.junit.Rule @@ -85,7 +86,7 @@ class NimbusMessagingNotificationTest { mDevice.openNotification() notificationShade { val data = - FxNimbus.features.messaging.value().messages["test-default-browser-notification"] + FxNimbusMessaging.features.messaging.value().messages["test-default-browser-notification"] verifySystemNotificationExists(data!!.title!!) verifySystemNotificationExists(data.text) } diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingTriggerTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingTriggerTest.kt index eb3b8ab98205..6ba04c9e8810 100644 --- a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingTriggerTest.kt +++ b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusMessagingTriggerTest.kt @@ -6,6 +6,8 @@ package org.mozilla.fenix.ui import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice +import mozilla.components.service.nimbus.messaging.FxNimbusMessaging +import mozilla.components.service.nimbus.messaging.Messaging import org.junit.Assert import org.junit.Before import org.junit.Rule @@ -13,11 +15,9 @@ import org.junit.Test import org.mozilla.experiments.nimbus.NimbusInterface import org.mozilla.experiments.nimbus.internal.NimbusException import org.mozilla.fenix.ext.components -import org.mozilla.fenix.gleanplumb.CustomAttributeProvider import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.TestHelper -import org.mozilla.fenix.nimbus.FxNimbus -import org.mozilla.fenix.nimbus.Messaging +import org.mozilla.fenix.messaging.CustomAttributeProvider /** * Test to instantiate Nimbus and automatically test all trigger expressions shipping with the app. @@ -39,7 +39,7 @@ class NimbusMessagingTriggerTest { fun setUp() { mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) nimbus = TestHelper.appContext.components.analytics.experiments - feature = FxNimbus.features.messaging.value() + feature = FxNimbusMessaging.features.messaging.value() } @Test diff --git a/mobile/android/fenix/app/src/main/AndroidManifest.xml b/mobile/android/fenix/app/src/main/AndroidManifest.xml index 7508fd593d79..cde0ab457290 100644 --- a/mobile/android/fenix/app/src/main/AndroidManifest.xml +++ b/mobile/android/fenix/app/src/main/AndroidManifest.xml @@ -287,7 +287,7 @@ android:theme="@style/DialogActivityTheme" /> = when expandedCollections, recentBookmarks, showCollectionPlaceholder, - messaging.messageToShow[MessageSurfaceId.HOMESCREEN], + messaging.messageToShow[FenixMessageSurfaceId.HOMESCREEN], shouldShowRecentTabs(settings), shouldShowRecentSyncedTabs(settings), recentHistory, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/MessageCardViewHolder.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/MessageCardViewHolder.kt index 835ca123293c..b39a642dccd0 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/MessageCardViewHolder.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/MessageCardViewHolder.kt @@ -13,12 +13,12 @@ import androidx.compose.runtime.remember import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.LifecycleOwner import mozilla.components.lib.state.ext.observeAsComposableState +import mozilla.components.service.nimbus.messaging.Message import org.mozilla.fenix.R import org.mozilla.fenix.components.components import org.mozilla.fenix.compose.ComposeViewHolder import org.mozilla.fenix.compose.MessageCard import org.mozilla.fenix.compose.MessageCardColors -import org.mozilla.fenix.gleanplumb.Message import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.wallpapers.Wallpaper diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/CustomAttributeProvider.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/CustomAttributeProvider.kt similarity index 91% rename from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/CustomAttributeProvider.kt rename to mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/CustomAttributeProvider.kt index 433e106b7755..620ef9ce1e76 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/CustomAttributeProvider.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/CustomAttributeProvider.kt @@ -2,10 +2,11 @@ * 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 org.mozilla.fenix.gleanplumb +package org.mozilla.fenix.messaging import android.content.Context import androidx.core.app.NotificationManagerCompat +import mozilla.components.service.nimbus.messaging.JexlAttributeProvider import org.json.JSONObject import org.mozilla.fenix.ext.areNotificationsEnabledSafe import org.mozilla.fenix.ext.settings @@ -18,7 +19,7 @@ import java.util.Locale * Custom attributes that the messaging framework will use to evaluate if message is eligible * to be shown. */ -object CustomAttributeProvider { +object CustomAttributeProvider : JexlAttributeProvider { private val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US) /** @@ -48,7 +49,7 @@ object CustomAttributeProvider { * * This is used to drive display triggers of messages. */ - fun getCustomAttributes(context: Context): JSONObject { + override fun getCustomAttributes(context: Context): JSONObject { val now = Calendar.getInstance() val settings = context.settings() return JSONObject( diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/DefaultMessageController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/DefaultMessageController.kt similarity index 87% rename from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/DefaultMessageController.kt rename to mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/DefaultMessageController.kt index ebdef0aad530..21f47f76a3d6 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/DefaultMessageController.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/DefaultMessageController.kt @@ -2,9 +2,11 @@ * 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 org.mozilla.fenix.gleanplumb +package org.mozilla.fenix.messaging import android.content.Intent +import mozilla.components.service.nimbus.messaging.Message +import mozilla.components.service.nimbus.messaging.NimbusMessagingController import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/FenixMessageSurfaceId.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/FenixMessageSurfaceId.kt new file mode 100644 index 000000000000..f7092f6b01d7 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/FenixMessageSurfaceId.kt @@ -0,0 +1,25 @@ +/* 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 org.mozilla.fenix.messaging + +/** + * The identity of a message surface for Fenix + */ +object FenixMessageSurfaceId { + /** + * A local notification in the background, like a push notification. + */ + const val NOTIFICATION = "notification" + + /** + * A banner in the homescreen. + */ + const val HOMESCREEN = "homescreen" + + /** + * A survey dialog that is intended to be disruptive. + */ + const val SURVEY = "survey" +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/FenixNimbusMessagingController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/FenixNimbusMessagingController.kt new file mode 100644 index 000000000000..39b81fcfc38a --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/FenixNimbusMessagingController.kt @@ -0,0 +1,19 @@ +package org.mozilla.fenix.messaging + +import mozilla.components.service.nimbus.messaging.NimbusMessagingController +import mozilla.components.service.nimbus.messaging.NimbusMessagingStorage +import org.mozilla.fenix.BuildConfig + +/** + * Bookkeeping for message actions in terms of Glean messages and the messaging store. + * Specialized implementation of NimbusMessagingController defining the deepLinkScheme + * used by Fenix + */ +class FenixNimbusMessagingController( + messagingStorage: NimbusMessagingStorage, + now: () -> Long = { System.currentTimeMillis() }, +) : NimbusMessagingController( + messagingStorage = messagingStorage, + deepLinkScheme = BuildConfig.DEEP_LINK_SCHEME, + now = now, +) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/MessageController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MessageController.kt similarity index 85% rename from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/MessageController.kt rename to mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MessageController.kt index 5eb03b746360..79e6bd211ac9 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/MessageController.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MessageController.kt @@ -2,7 +2,9 @@ * 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 org.mozilla.fenix.gleanplumb +package org.mozilla.fenix.messaging + +import mozilla.components.service.nimbus.messaging.Message /** * Controls all the interactions with a [Message]. diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/MessageNotificationWorker.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MessageNotificationWorker.kt similarity index 91% rename from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/MessageNotificationWorker.kt rename to mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MessageNotificationWorker.kt index 9f6d2b68559a..9ca4b6c72026 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/MessageNotificationWorker.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MessageNotificationWorker.kt @@ -2,7 +2,7 @@ * 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 org.mozilla.fenix.gleanplumb +package org.mozilla.fenix.messaging import android.app.Activity import android.app.Notification @@ -18,14 +18,14 @@ import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import androidx.work.Worker import androidx.work.WorkerParameters +import mozilla.components.service.nimbus.messaging.FxNimbusMessaging +import mozilla.components.service.nimbus.messaging.Message import mozilla.components.support.base.ids.SharedIdsHelper +import mozilla.components.support.utils.BootUtils import org.mozilla.fenix.ext.areNotificationsEnabledSafe import org.mozilla.fenix.ext.components -import org.mozilla.fenix.nimbus.FxNimbus -import org.mozilla.fenix.nimbus.MessageSurfaceId import org.mozilla.fenix.onboarding.ensureMarketingChannelExists import org.mozilla.fenix.perf.runBlockingIncrement -import org.mozilla.fenix.utils.BootUtils import org.mozilla.fenix.utils.IntentUtils import org.mozilla.fenix.utils.createBaseNotification import java.util.concurrent.TimeUnit @@ -55,7 +55,7 @@ class MessageNotificationWorker( val messagingStorage = context.components.analytics.messagingStorage val messages = runBlockingIncrement { messagingStorage.getMessages() } val nextMessage = - messagingStorage.getNextMessage(MessageSurfaceId.NOTIFICATION, messages) + messagingStorage.getNextMessage(FenixMessageSurfaceId.NOTIFICATION, messages) ?: return Result.success() val currentBootUniqueIdentifier = BootUtils.getBootIdentifier(context) @@ -65,7 +65,7 @@ class MessageNotificationWorker( return Result.success() } - val nimbusMessagingController = NimbusMessagingController(messagingStorage) + val nimbusMessagingController = FenixNimbusMessagingController(messagingStorage) // Update message as displayed. val updatedMessage = @@ -147,7 +147,7 @@ class MessageNotificationWorker( * Initialize the [Worker] to begin polling Nimbus. */ fun setMessageNotificationWorker(context: Context) { - val messaging = FxNimbus.features.messaging + val messaging = FxNimbusMessaging.features.messaging val featureConfig = messaging.value() val notificationConfig = featureConfig.notificationConfig val pollingInterval = notificationConfig.refreshInterval.toLong() @@ -189,10 +189,10 @@ class NotificationDismissedService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent != null) { val nimbusMessagingController = - NimbusMessagingController(applicationContext.components.analytics.messagingStorage) + FenixNimbusMessagingController(applicationContext.components.analytics.messagingStorage) // Get the relevant message. - val message = intent.getStringExtra(DISMISSED_MESSAGE_ID) ?.let { messageId -> + val message = intent.getStringExtra(DISMISSED_MESSAGE_ID)?.let { messageId -> runBlockingIncrement { nimbusMessagingController.getMessage(messageId) } } @@ -218,10 +218,10 @@ class NotificationClickedReceiverActivity : Activity() { super.onCreate(savedInstanceState) val nimbusMessagingController = - NimbusMessagingController(components.analytics.messagingStorage) + FenixNimbusMessagingController(components.analytics.messagingStorage) // Get the relevant message. - val message = intent.getStringExtra(CLICKED_MESSAGE_ID) ?.let { messageId -> + val message = intent.getStringExtra(CLICKED_MESSAGE_ID)?.let { messageId -> runBlockingIncrement { nimbusMessagingController.getMessage(messageId) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/MessagingFeature.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MessagingFeature.kt similarity index 77% rename from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/MessagingFeature.kt rename to mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MessagingFeature.kt index 997843b38520..9e9a5ef812cd 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/MessagingFeature.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MessagingFeature.kt @@ -2,12 +2,11 @@ * 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 org.mozilla.fenix.gleanplumb +package org.mozilla.fenix.messaging import mozilla.components.support.base.feature.LifecycleAwareFeature import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction.MessagingAction -import org.mozilla.fenix.nimbus.MessageSurfaceId /** * A message observer that updates the provided. @@ -15,7 +14,7 @@ import org.mozilla.fenix.nimbus.MessageSurfaceId class MessagingFeature(val appStore: AppStore) : LifecycleAwareFeature { override fun start() { - appStore.dispatch(MessagingAction.Evaluate(MessageSurfaceId.HOMESCREEN)) + appStore.dispatch(MessagingAction.Evaluate(FenixMessageSurfaceId.HOMESCREEN)) } override fun stop() = Unit diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/MessagingState.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MessagingState.kt similarity index 79% rename from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/MessagingState.kt rename to mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MessagingState.kt index 1fa7ad3cf6d0..126f46e39aa7 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/MessagingState.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MessagingState.kt @@ -2,9 +2,10 @@ * 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 org.mozilla.fenix.gleanplumb +package org.mozilla.fenix.messaging -import org.mozilla.fenix.nimbus.MessageSurfaceId +import mozilla.components.service.nimbus.messaging.Message +import mozilla.components.service.nimbus.messaging.MessageSurfaceId /** * Represent all the state related to the Messaging framework. diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/state/MessagingMiddleware.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/state/MessagingMiddleware.kt similarity index 92% rename from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/state/MessagingMiddleware.kt rename to mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/state/MessagingMiddleware.kt index 1fa5f2999f6b..9388f3414822 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/state/MessagingMiddleware.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/state/MessagingMiddleware.kt @@ -2,13 +2,16 @@ * 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 org.mozilla.fenix.gleanplumb.state +package org.mozilla.fenix.messaging.state import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import mozilla.components.lib.state.Middleware import mozilla.components.lib.state.MiddlewareContext +import mozilla.components.service.nimbus.messaging.Message +import mozilla.components.service.nimbus.messaging.NimbusMessagingController +import mozilla.components.service.nimbus.messaging.NimbusMessagingStorage import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.ConsumeMessageToShow import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.Evaluate @@ -18,15 +21,13 @@ import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.Restore import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessages import org.mozilla.fenix.components.appstate.AppState -import org.mozilla.fenix.gleanplumb.Message -import org.mozilla.fenix.gleanplumb.NimbusMessagingController -import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage +import org.mozilla.fenix.messaging.FenixNimbusMessagingController typealias AppStoreMiddlewareContext = MiddlewareContext class MessagingMiddleware( private val messagingStorage: NimbusMessagingStorage, - private val controller: NimbusMessagingController = NimbusMessagingController(messagingStorage), + private val controller: NimbusMessagingController = FenixNimbusMessagingController(messagingStorage), private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO), ) : Middleware { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/state/MessagingReducer.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/state/MessagingReducer.kt similarity index 94% rename from mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/state/MessagingReducer.kt rename to mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/state/MessagingReducer.kt index f844a525db11..19c47cbc94d5 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/gleanplumb/state/MessagingReducer.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/state/MessagingReducer.kt @@ -2,14 +2,14 @@ * 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 org.mozilla.fenix.gleanplumb.state +package org.mozilla.fenix.messaging.state import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.ConsumeMessageToShow import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessages import org.mozilla.fenix.components.appstate.AppState -import org.mozilla.fenix.gleanplumb.MessagingState +import org.mozilla.fenix.messaging.MessagingState /** * Reducer for [MessagingState]. diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/studies/StudiesAdapter.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/studies/StudiesAdapter.kt index f2e10fbeda20..39b87b0fd16c 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/studies/StudiesAdapter.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/studies/StudiesAdapter.kt @@ -18,9 +18,9 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import com.google.android.material.button.MaterialButton +import mozilla.components.service.nimbus.messaging.MESSAGING_FEATURE_ID import org.mozilla.experiments.nimbus.internal.EnrolledExperiment import org.mozilla.fenix.R -import org.mozilla.fenix.gleanplumb.MESSAGING_FEATURE_ID import org.mozilla.fenix.settings.studies.CustomViewHolder.SectionViewHolder import org.mozilla.fenix.settings.studies.CustomViewHolder.StudyViewHolder diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/AppStoreTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/AppStoreTest.kt index 8bcf8a5fa50c..4af64d522bd3 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/AppStoreTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/AppStoreTest.kt @@ -14,6 +14,8 @@ import mozilla.components.concept.sync.DeviceType import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.top.sites.TopSite import mozilla.components.service.fxa.manager.FxaAccountManager +import mozilla.components.service.nimbus.messaging.Message +import mozilla.components.service.nimbus.messaging.MessageData import mozilla.components.service.pocket.PocketStory import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory @@ -32,7 +34,6 @@ import org.mozilla.fenix.components.appstate.AppState import org.mozilla.fenix.components.appstate.filterOut import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getFilteredStories -import org.mozilla.fenix.gleanplumb.Message import org.mozilla.fenix.home.CurrentMode import org.mozilla.fenix.home.Mode import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory @@ -44,8 +45,7 @@ import org.mozilla.fenix.home.recenttabs.RecentTab import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight -import org.mozilla.fenix.nimbus.MessageData -import org.mozilla.fenix.nimbus.MessageSurfaceId +import org.mozilla.fenix.messaging.FenixMessageSurfaceId import org.mozilla.fenix.onboarding.FenixOnboarding class AppStoreTest { @@ -118,7 +118,7 @@ class AppStoreTest { val message = Message( "message", - MessageData(surface = MessageSurfaceId.HOMESCREEN), + MessageData(surface = FenixMessageSurfaceId.HOMESCREEN), "action", mockk(), emptyList(), diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt index af988dd0585a..4b9754749f19 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt @@ -32,6 +32,7 @@ import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.top.sites.TopSite import mozilla.components.service.glean.testing.GleanTestRule +import mozilla.components.service.nimbus.messaging.Message import mozilla.components.support.test.ext.joinBlocking import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.rule.MainCoroutineRule @@ -61,12 +62,11 @@ import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.appstate.AppState import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings -import org.mozilla.fenix.gleanplumb.Message -import org.mozilla.fenix.gleanplumb.MessageController import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.home.recentbookmarks.RecentBookmark import org.mozilla.fenix.home.recenttabs.RecentTab import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController +import org.mozilla.fenix.messaging.MessageController import org.mozilla.fenix.onboarding.WallpaperOnboardingDialogFragment.Companion.THUMBNAILS_SELECTION_COUNT import org.mozilla.fenix.search.toolbar.SearchSelectorMenu import org.mozilla.fenix.settings.SupportUtils diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/SessionControlViewTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/SessionControlViewTest.kt index e070de1fbd57..4b1c0eadc06c 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/SessionControlViewTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/SessionControlViewTest.kt @@ -8,13 +8,13 @@ import io.mockk.every import io.mockk.mockk import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.top.sites.TopSite +import mozilla.components.service.nimbus.messaging.Message import mozilla.components.service.pocket.PocketStory import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith -import org.mozilla.fenix.gleanplumb.Message import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.home.recentbookmarks.RecentBookmark import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/DefaultMessageControllerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/messaging/DefaultMessageControllerTest.kt similarity index 91% rename from mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/DefaultMessageControllerTest.kt rename to mobile/android/fenix/app/src/test/java/org/mozilla/fenix/messaging/DefaultMessageControllerTest.kt index ff44605fee44..733d86de52be 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/DefaultMessageControllerTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/messaging/DefaultMessageControllerTest.kt @@ -2,12 +2,15 @@ * 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 org.mozilla.fenix.gleanplumb +package org.mozilla.fenix.messaging import androidx.core.net.toUri import io.mockk.every import io.mockk.mockk import io.mockk.verify +import mozilla.components.service.nimbus.messaging.Message +import mozilla.components.service.nimbus.messaging.MessageData +import mozilla.components.service.nimbus.messaging.NimbusMessagingController import mozilla.components.support.test.robolectric.testContext import mozilla.telemetry.glean.testing.GleanTestRule import org.junit.Before @@ -19,7 +22,6 @@ import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked import org.mozilla.fenix.helpers.FenixRobolectricTestRunner -import org.mozilla.fenix.nimbus.MessageData @RunWith(FenixRobolectricTestRunner::class) class DefaultMessageControllerTest { diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/MessagingFeatureTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/messaging/MessagingFeatureTest.kt similarity index 82% rename from mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/MessagingFeatureTest.kt rename to mobile/android/fenix/app/src/test/java/org/mozilla/fenix/messaging/MessagingFeatureTest.kt index 31538504eb98..838e852f4249 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/MessagingFeatureTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/messaging/MessagingFeatureTest.kt @@ -2,7 +2,7 @@ * 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 org.mozilla.fenix.gleanplumb +package org.mozilla.fenix.messaging import io.mockk.spyk import io.mockk.verify @@ -12,7 +12,6 @@ import org.junit.Rule import org.junit.Test import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction.MessagingAction -import org.mozilla.fenix.nimbus.MessageSurfaceId class MessagingFeatureTest { @OptIn(ExperimentalCoroutinesApi::class) @@ -26,6 +25,6 @@ class MessagingFeatureTest { binding.start() - verify { appStore.dispatch(MessagingAction.Evaluate(MessageSurfaceId.HOMESCREEN)) } + verify { appStore.dispatch(MessagingAction.Evaluate(FenixMessageSurfaceId.HOMESCREEN)) } } } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/state/MessagingMiddlewareTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/messaging/state/MessagingMiddlewareTest.kt similarity index 89% rename from mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/state/MessagingMiddlewareTest.kt rename to mobile/android/fenix/app/src/test/java/org/mozilla/fenix/messaging/state/MessagingMiddlewareTest.kt index 6ecea7db1692..d55001914802 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/state/MessagingMiddlewareTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/messaging/state/MessagingMiddlewareTest.kt @@ -2,13 +2,18 @@ * 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 org.mozilla.fenix.gleanplumb.state +package org.mozilla.fenix.messaging.state import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.test.advanceUntilIdle +import mozilla.components.service.nimbus.messaging.Message +import mozilla.components.service.nimbus.messaging.MessageData +import mozilla.components.service.nimbus.messaging.NimbusMessagingController +import mozilla.components.service.nimbus.messaging.NimbusMessagingStorage +import mozilla.components.service.nimbus.messaging.StyleData import mozilla.components.support.test.ext.joinBlocking import mozilla.components.support.test.libstate.ext.waitUntilIdle import mozilla.components.support.test.rule.MainCoroutineRule @@ -24,13 +29,9 @@ import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageCl import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDismissed import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.Restore import org.mozilla.fenix.components.appstate.AppState -import org.mozilla.fenix.gleanplumb.Message -import org.mozilla.fenix.gleanplumb.MessagingState -import org.mozilla.fenix.gleanplumb.NimbusMessagingController -import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage -import org.mozilla.fenix.nimbus.MessageData -import org.mozilla.fenix.nimbus.MessageSurfaceId -import org.mozilla.fenix.nimbus.StyleData +import org.mozilla.fenix.messaging.FenixMessageSurfaceId +import org.mozilla.fenix.messaging.FenixNimbusMessagingController +import org.mozilla.fenix.messaging.MessagingState class MessagingMiddlewareTest { @get:Rule @@ -42,7 +43,7 @@ class MessagingMiddlewareTest { @Before fun setUp() { messagingStorage = mockk(relaxed = true) - controller = NimbusMessagingController(messagingStorage) { 0 } + controller = FenixNimbusMessagingController(messagingStorage) { 0 } } @Test @@ -86,14 +87,14 @@ class MessagingMiddlewareTest { every { messagingStorage.getNextMessage( - MessageSurfaceId.HOMESCREEN, + FenixMessageSurfaceId.HOMESCREEN, any(), ) } returns message assertEquals(0, store.state.messaging.messageToShow.size) - store.dispatch(Evaluate(MessageSurfaceId.HOMESCREEN)).joinBlocking() + store.dispatch(Evaluate(FenixMessageSurfaceId.HOMESCREEN)).joinBlocking() store.waitUntilIdle() // UpdateMessageToShow to causes messageToShow to append @@ -212,12 +213,12 @@ class MessagingMiddlewareTest { every { messagingStorage.getNextMessage( - MessageSurfaceId.HOMESCREEN, + FenixMessageSurfaceId.HOMESCREEN, any(), ) } returns message - store.dispatch(Evaluate(MessageSurfaceId.HOMESCREEN)) + store.dispatch(Evaluate(FenixMessageSurfaceId.HOMESCREEN)) store.waitUntilIdle() assertEquals(1, store.state.messaging.messages.first().metadata.displayCount) @@ -245,12 +246,12 @@ class MessagingMiddlewareTest { every { messagingStorage.getNextMessage( - MessageSurfaceId.HOMESCREEN, + FenixMessageSurfaceId.HOMESCREEN, any(), ) } returns message1 - store.dispatch(Evaluate(MessageSurfaceId.HOMESCREEN)).joinBlocking() + store.dispatch(Evaluate(FenixMessageSurfaceId.HOMESCREEN)).joinBlocking() store.waitUntilIdle() assertEquals(messageDisplayed1, store.state.messaging.messages[0]) @@ -277,12 +278,12 @@ class MessagingMiddlewareTest { every { messagingStorage.getNextMessage( - MessageSurfaceId.HOMESCREEN, + FenixMessageSurfaceId.HOMESCREEN, any(), ) } returns message - store.dispatch(Evaluate(MessageSurfaceId.HOMESCREEN)).joinBlocking() + store.dispatch(Evaluate(FenixMessageSurfaceId.HOMESCREEN)).joinBlocking() store.waitUntilIdle() assertEquals(messageDisplayed.metadata.displayCount, store.state.messaging.messages[0].metadata.displayCount) @@ -307,12 +308,12 @@ class MessagingMiddlewareTest { every { messagingStorage.getNextMessage( - MessageSurfaceId.HOMESCREEN, + FenixMessageSurfaceId.HOMESCREEN, any(), ) } returns message - store.dispatch(Evaluate(MessageSurfaceId.HOMESCREEN)).joinBlocking() + store.dispatch(Evaluate(FenixMessageSurfaceId.HOMESCREEN)).joinBlocking() store.waitUntilIdle() assertEquals(0, store.state.messaging.messages.size) diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/state/MessagingReducerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/messaging/state/MessagingReducerTest.kt similarity index 83% rename from mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/state/MessagingReducerTest.kt rename to mobile/android/fenix/app/src/test/java/org/mozilla/fenix/messaging/state/MessagingReducerTest.kt index 0ecec8320c22..dbb111eb4339 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/gleanplumb/state/MessagingReducerTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/messaging/state/MessagingReducerTest.kt @@ -2,9 +2,13 @@ * 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 org.mozilla.fenix.gleanplumb.state +package org.mozilla.fenix.messaging.state import io.mockk.mockk +import mozilla.components.service.nimbus.messaging.Message +import mozilla.components.service.nimbus.messaging.MessageData +import mozilla.components.service.nimbus.messaging.MessageSurfaceId +import mozilla.components.service.nimbus.messaging.StyleData import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull @@ -15,11 +19,8 @@ import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMes import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessages import org.mozilla.fenix.components.appstate.AppState import org.mozilla.fenix.components.appstate.AppStoreReducer -import org.mozilla.fenix.gleanplumb.Message -import org.mozilla.fenix.gleanplumb.MessagingState -import org.mozilla.fenix.nimbus.MessageData -import org.mozilla.fenix.nimbus.MessageSurfaceId -import org.mozilla.fenix.nimbus.StyleData +import org.mozilla.fenix.messaging.FenixMessageSurfaceId +import org.mozilla.fenix.messaging.MessagingState class MessagingReducerTest { @@ -45,7 +46,7 @@ class MessagingReducerTest { assertNull(updatedState.messaging.messageToShow[m.surface]) } - private fun createMessage(id: String, action: String = "action-1", surface: MessageSurfaceId = MessageSurfaceId.HOMESCREEN): Message = + private fun createMessage(id: String, action: String = "action-1", surface: MessageSurfaceId = FenixMessageSurfaceId.HOMESCREEN): Message = Message( id = id, data = MessageData(surface = surface), diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/nimbus/NimbusSystemTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/nimbus/NimbusSystemTest.kt index d5a333ea2f6f..3a0dbf22011a 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/nimbus/NimbusSystemTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/nimbus/NimbusSystemTest.kt @@ -10,6 +10,7 @@ import io.mockk.just import io.mockk.mockk import io.mockk.runs import io.mockk.slot +import mozilla.components.service.nimbus.messaging.NimbusSystem import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/settings/studies/StudiesAdapterTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/settings/studies/StudiesAdapterTest.kt index b4f107a3ca8a..610e3144c021 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/settings/studies/StudiesAdapterTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/settings/studies/StudiesAdapterTest.kt @@ -19,13 +19,13 @@ import io.mockk.verify import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertTrue +import mozilla.components.service.nimbus.messaging.MESSAGING_FEATURE_ID import mozilla.components.support.test.robolectric.testContext import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mozilla.experiments.nimbus.internal.EnrolledExperiment import org.mozilla.fenix.R -import org.mozilla.fenix.gleanplumb.MESSAGING_FEATURE_ID import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.settings.studies.CustomViewHolder.SectionViewHolder import org.mozilla.fenix.settings.studies.CustomViewHolder.StudyViewHolder diff --git a/mobile/android/fenix/build.gradle b/mobile/android/fenix/build.gradle index abcdcd0c4235..2119cc42f4b4 100644 --- a/mobile/android/fenix/build.gradle +++ b/mobile/android/fenix/build.gradle @@ -59,6 +59,8 @@ buildscript { } dependencies { + classpath "org.mozilla.appservices:tooling-nimbus-gradle:${Versions.mozilla_appservices}" + classpath FenixDependencies.tools_androidgradle classpath FenixDependencies.tools_kotlingradle classpath FenixDependencies.tools_benchmarkgradle -- 2.11.4.GIT