[fenix] Bug 1813085 - If a message notification is being displayed, do not create another notification with the same message.

pull/600/head
t-p-white 1 year ago committed by mergify[bot]
parent d5b1251d82
commit c275034e78

@ -48,6 +48,8 @@ data class Message(
* @param pressed Indicates if a message has been clicked.
* @param dismissed Indicates if a message has been closed.
* @param lastTimeShown A timestamp indicating when was the last time, the message was shown.
* @param latestBootIdentifier A unique boot identifier for when the message was last displayed
* (this may be a boot count or a boot id).
*/
data class Metadata(
val id: String,
@ -55,5 +57,6 @@ data class Message(
val pressed: Boolean = false,
val dismissed: Boolean = false,
val lastTimeShown: Long = 0L,
val latestBootIdentifier: String? = null,
)
}

@ -27,6 +27,7 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.nimbus.MessageSurfaceId
import org.mozilla.fenix.onboarding.MARKETING_CHANNEL_ID
import org.mozilla.fenix.utils.BootUtils
import org.mozilla.fenix.utils.IntentUtils
import org.mozilla.fenix.utils.createBaseNotification
import java.util.concurrent.TimeUnit
@ -54,16 +55,26 @@ class MessageNotificationWorker(
messagingStorage.getNextMessage(MessageSurfaceId.NOTIFICATION, messages)
?: return@launch
val currentBootUniqueIdentifier = BootUtils.getBootIdentifier(context)
val messageMetadata = nextMessage.metadata
// Device has NOT been power cycled.
if (messageMetadata.latestBootIdentifier == currentBootUniqueIdentifier) {
return@launch
}
val nimbusMessagingController = NimbusMessagingController(messagingStorage)
// Update message as displayed.
val updatedMessage =
nimbusMessagingController.updateMessageAsDisplayed(nextMessage)
nimbusMessagingController.updateMessageAsDisplayed(
nextMessage,
currentBootUniqueIdentifier,
)
nimbusMessagingController.onMessageDisplayed(updatedMessage)
NotificationManagerCompat.from(context).notify(
MESSAGE_TAG,
SharedIdsHelper.getNextIdForTag(context, updatedMessage.id),
SharedIdsHelper.getIdForTag(context, updatedMessage.id),
buildNotification(
context,
updatedMessage,

@ -20,12 +20,13 @@ class NimbusMessagingController(
/**
* Called when a message is just about to be shown to the user.
*
* Update the display count and time shown metadata for the given [message].
* Update the display count, time shown and boot identifier metadata for the given [message].
*/
fun updateMessageAsDisplayed(message: Message): Message {
fun updateMessageAsDisplayed(message: Message, bootIdentifier: String? = null): Message {
val updatedMetadata = message.metadata.copy(
displayCount = message.metadata.displayCount + 1,
lastTimeShown = now(),
latestBootIdentifier = bootIdentifier,
)
return message.copy(
metadata = updatedMetadata,

@ -81,7 +81,7 @@ internal fun JSONArray.toMetadataMap(): Map<String, Message.Metadata> {
@Suppress("MaxLineLength") // To avoid adding any extra space to the string.
internal fun Message.Metadata.toJson(): String {
return """{"id":"$id","displayCount":$displayCount,"pressed":$pressed,"dismissed":$dismissed,"lastTimeShown":$lastTimeShown}"""
return """{"id":"$id","displayCount":$displayCount,"pressed":$pressed,"dismissed":$dismissed,"lastTimeShown":$lastTimeShown,"latestBootIdentifier":"$latestBootIdentifier"}"""
}
internal fun JSONObject.toMetadata(): Message.Metadata {
@ -91,5 +91,6 @@ internal fun JSONObject.toMetadata(): Message.Metadata {
pressed = optBoolean("pressed"),
dismissed = optBoolean("dismissed"),
lastTimeShown = optLong("lastTimeShown"),
latestBootIdentifier = optString("latestBootIdentifier"),
)
}

@ -0,0 +1,63 @@
/* 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.utils
import android.content.Context
import android.os.Build
import android.provider.Settings
import androidx.annotation.RequiresApi
import java.io.File
/**
* Provides access to system properties.
*/
interface BootUtils {
/**
* Gets the device boot count.
*
* **Only for Android versions N(24) and above.**
*/
@RequiresApi(Build.VERSION_CODES.N)
fun getDeviceBootCount(context: Context): String
val deviceBootId: String?
val bootIdFileExists: Boolean
companion object {
/**
* @return either the boot count or a boot id depending on the device Android version.
*/
fun getBootIdentifier(context: Context, bootUtils: BootUtils = BootUtilsImpl()): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
bootUtils.getDeviceBootCount(context)
} else {
return if (bootUtils.bootIdFileExists) {
bootUtils.deviceBootId ?: NO_BOOT_IDENTIFIER
} else {
NO_BOOT_IDENTIFIER
}
}
}
}
}
/**
* Implementation of [BootUtils].
*/
class BootUtilsImpl : BootUtils {
private val bootIdFile by lazy { File("/proc/sys/kernel/random/boot_id") }
@RequiresApi(Build.VERSION_CODES.N)
override fun getDeviceBootCount(context: Context): String =
Settings.Global.getString(context.contentResolver, Settings.Global.BOOT_COUNT)
override val deviceBootId: String? by lazy { bootIdFile.readLines().firstOrNull()?.trim() }
override val bootIdFileExists: Boolean by lazy { bootIdFile.exists() }
}
private const val NO_BOOT_IDENTIFIER = "no boot identifier available"

@ -51,19 +51,48 @@ class NimbusMessagingControllerTest {
}
@Test
fun `WHEN calling updateMessageAsDisplayed message THEN message metadata is updated`() =
fun `WHEN calling updateMessageAsDisplayed with message & no boot id THEN metadata for count and lastTimeShown is updated`() =
coroutineScope.runTest {
val message = createMessage("id-1")
assertEquals(0, message.metadata.displayCount)
assertEquals(0L, message.metadata.lastTimeShown)
assertNull(message.metadata.latestBootIdentifier)
val expectedMessage = with(message) {
copy(metadata = metadata.copy(displayCount = 1, lastTimeShown = MOCK_TIME_MILLIS))
copy(
metadata = metadata.copy(
displayCount = 1,
lastTimeShown = MOCK_TIME_MILLIS,
latestBootIdentifier = null,
),
)
}
assertEquals(expectedMessage, controller.updateMessageAsDisplayed(message))
}
@Test
fun `WHEN calling updateMessageAsDisplayed with message & boot id THEN metadata for count, lastTimeShown & latestBootIdentifier is updated`() =
coroutineScope.runTest {
val message = createMessage("id-1")
assertEquals(0, message.metadata.displayCount)
assertEquals(0L, message.metadata.lastTimeShown)
assertNull(message.metadata.latestBootIdentifier)
val bootId = "test boot id"
val expectedMessage = with(message) {
copy(
metadata = metadata.copy(
displayCount = 1,
lastTimeShown = MOCK_TIME_MILLIS,
latestBootIdentifier = bootId,
),
)
}
assertEquals(expectedMessage, controller.updateMessageAsDisplayed(message, bootId))
}
@Test
fun `GIVEN message not expired WHEN calling onMessageDisplayed THEN record a messageShown event and update storage`() =
coroutineScope.runTest {

@ -20,19 +20,12 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.experiments.nimbus.GleanPlumbInterface
import org.mozilla.experiments.nimbus.internal.FeatureHolder
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.nimbus.Messaging
@RunWith(FenixRobolectricTestRunner::class)
class OnDiskMessageMetadataStorageTest {
private lateinit var storage: OnDiskMessageMetadataStorage
private lateinit var metadataStorage: MessageMetadataStorage
private lateinit var gleanPlumb: GleanPlumbInterface
private lateinit var messagingFeature: FeatureHolder<Messaging>
private lateinit var messaging: Messaging
@Before
fun setup() {
@ -98,10 +91,11 @@ class OnDiskMessageMetadataStorageTest {
pressed = false,
dismissed = false,
lastTimeShown = 0L,
latestBootIdentifier = "9",
)
val expected =
"""{"id":"id","displayCount":1,"pressed":false,"dismissed":false,"lastTimeShown":0}"""
"""{"id":"id","displayCount":1,"pressed":false,"dismissed":false,"lastTimeShown":0,"latestBootIdentifier":"9"}"""
assertEquals(expected, metadata.toJson())
}
@ -109,7 +103,7 @@ class OnDiskMessageMetadataStorageTest {
@Test
fun `WHEN calling toMetadata THEN return Metadata representation`() {
val json =
"""{"id":"id","displayCount":1,"pressed":false,"dismissed":false,"lastTimeShown":0}"""
"""{"id":"id","displayCount":1,"pressed":false,"dismissed":false,"lastTimeShown":0,"latestBootIdentifier":"9"}"""
val jsonObject = JSONObject(json)
@ -119,6 +113,7 @@ class OnDiskMessageMetadataStorageTest {
pressed = false,
dismissed = false,
lastTimeShown = 0L,
latestBootIdentifier = "9",
)
assertEquals(metadata, jsonObject.toMetadata())
@ -127,7 +122,7 @@ class OnDiskMessageMetadataStorageTest {
@Test
fun `WHEN calling toMetadataMap THEN return map representation`() {
val json =
"""[{"id":"id","displayCount":1,"pressed":false,"dismissed":false,"lastTimeShown":0}]"""
"""[{"id":"id","displayCount":1,"pressed":false,"dismissed":false,"lastTimeShown":0,"latestBootIdentifier":"9"}]"""
val jsonArray = JSONArray(json)
@ -137,6 +132,7 @@ class OnDiskMessageMetadataStorageTest {
pressed = false,
dismissed = false,
lastTimeShown = 0L,
latestBootIdentifier = "9",
)
assertEquals(metadata, jsonArray.toMetadataMap()[metadata.id])

@ -0,0 +1,79 @@
package org.mozilla.fenix.utils
import android.os.Build
import io.mockk.every
import io.mockk.mockk
import mozilla.components.support.test.robolectric.testContext
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.robolectric.annotation.Config
private const val NO_BOOT_IDENTIFIER = "no boot identifier available"
@RunWith(FenixRobolectricTestRunner::class)
class BootUtilsTest {
private lateinit var bootUtils: BootUtils
@Before
fun setUp() {
bootUtils = mockk(relaxed = true)
}
@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)
assertEquals(NO_BOOT_IDENTIFIER, getBootIdentifier(testContext, bootUtils))
}
@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)
assertEquals(NO_BOOT_IDENTIFIER, getBootIdentifier(testContext, bootUtils))
}
@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)
val bootId = "test"
every { bootUtils.deviceBootId }.returns(bootId)
assertEquals(bootId, getBootIdentifier(testContext, bootUtils))
}
@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)
val bootId = " test "
every { bootUtils.deviceBootId }.returns(bootId)
assertEquals(bootId, getBootIdentifier(testContext, bootUtils))
}
@Test
@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)
assertEquals(bootCount, getBootIdentifier(testContext, bootUtils))
}
@Test
@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)
assertEquals(bootCount, getBootIdentifier(testContext, bootUtils))
}
}
Loading…
Cancel
Save