[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 pressed Indicates if a message has been clicked.
* @param dismissed Indicates if a message has been closed. * @param dismissed Indicates if a message has been closed.
* @param lastTimeShown A timestamp indicating when was the last time, the message was shown. * @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( data class Metadata(
val id: String, val id: String,
@ -55,5 +57,6 @@ data class Message(
val pressed: Boolean = false, val pressed: Boolean = false,
val dismissed: Boolean = false, val dismissed: Boolean = false,
val lastTimeShown: Long = 0L, 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.FxNimbus
import org.mozilla.fenix.nimbus.MessageSurfaceId import org.mozilla.fenix.nimbus.MessageSurfaceId
import org.mozilla.fenix.onboarding.MARKETING_CHANNEL_ID 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.IntentUtils
import org.mozilla.fenix.utils.createBaseNotification import org.mozilla.fenix.utils.createBaseNotification
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -54,16 +55,26 @@ class MessageNotificationWorker(
messagingStorage.getNextMessage(MessageSurfaceId.NOTIFICATION, messages) messagingStorage.getNextMessage(MessageSurfaceId.NOTIFICATION, messages)
?: return@launch ?: 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) val nimbusMessagingController = NimbusMessagingController(messagingStorage)
// Update message as displayed. // Update message as displayed.
val updatedMessage = val updatedMessage =
nimbusMessagingController.updateMessageAsDisplayed(nextMessage) nimbusMessagingController.updateMessageAsDisplayed(
nextMessage,
currentBootUniqueIdentifier,
)
nimbusMessagingController.onMessageDisplayed(updatedMessage) nimbusMessagingController.onMessageDisplayed(updatedMessage)
NotificationManagerCompat.from(context).notify( NotificationManagerCompat.from(context).notify(
MESSAGE_TAG, MESSAGE_TAG,
SharedIdsHelper.getNextIdForTag(context, updatedMessage.id), SharedIdsHelper.getIdForTag(context, updatedMessage.id),
buildNotification( buildNotification(
context, context,
updatedMessage, updatedMessage,

@ -20,12 +20,13 @@ class NimbusMessagingController(
/** /**
* Called when a message is just about to be shown to the user. * 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( val updatedMetadata = message.metadata.copy(
displayCount = message.metadata.displayCount + 1, displayCount = message.metadata.displayCount + 1,
lastTimeShown = now(), lastTimeShown = now(),
latestBootIdentifier = bootIdentifier,
) )
return message.copy( return message.copy(
metadata = updatedMetadata, 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. @Suppress("MaxLineLength") // To avoid adding any extra space to the string.
internal fun Message.Metadata.toJson(): 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 { internal fun JSONObject.toMetadata(): Message.Metadata {
@ -91,5 +91,6 @@ internal fun JSONObject.toMetadata(): Message.Metadata {
pressed = optBoolean("pressed"), pressed = optBoolean("pressed"),
dismissed = optBoolean("dismissed"), dismissed = optBoolean("dismissed"),
lastTimeShown = optLong("lastTimeShown"), 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 @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 { coroutineScope.runTest {
val message = createMessage("id-1") val message = createMessage("id-1")
assertEquals(0, message.metadata.displayCount) assertEquals(0, message.metadata.displayCount)
assertEquals(0L, message.metadata.lastTimeShown) assertEquals(0L, message.metadata.lastTimeShown)
assertNull(message.metadata.latestBootIdentifier)
val expectedMessage = with(message) { 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)) 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 @Test
fun `GIVEN message not expired WHEN calling onMessageDisplayed THEN record a messageShown event and update storage`() = fun `GIVEN message not expired WHEN calling onMessageDisplayed THEN record a messageShown event and update storage`() =
coroutineScope.runTest { coroutineScope.runTest {

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