@ -4,10 +4,14 @@
package org.mozilla.fenix.gleanplumb
import android.app.Activity
import android.app.Notification
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.IBinder
import androidx.core.app.NotificationManagerCompat
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequest
@ -27,6 +31,9 @@ import org.mozilla.fenix.utils.IntentUtils
import org.mozilla.fenix.utils.createBaseNotification
import java.util.concurrent.TimeUnit
const val CLICKED _MESSAGE _ID = " clickedMessageId "
const val DISMISSED _MESSAGE _ID = " dismissedMessageId "
/ * *
* Background [ Worker ] that polls Nimbus for available [ Message ] s at a given interval .
* A [ Notification ] will be created using the configuration of the next highest priority [ Message ]
@ -37,7 +44,7 @@ class MessageNotificationWorker(
workerParameters : WorkerParameters ,
) : Worker ( context , workerParameters ) {
@OptIn ( DelicateCoroutinesApi :: class ) // GlobalScope usage
@OptIn ( DelicateCoroutinesApi :: class ) // GlobalScope usage .
override fun doWork ( ) : Result {
GlobalScope . launch ( Dispatchers . IO ) {
val context = applicationContext
@ -45,51 +52,76 @@ class MessageNotificationWorker(
val messages = messagingStorage . getMessages ( )
val nextMessage =
messagingStorage . getNextMessage ( MessageSurfaceId . NOTIFICATION , messages )
if ( nextMessage == null || ! nextMessage . shouldDisplayMessage ( ) ) {
return @launch
}
?: return @launch
val nimbusMessagingController = NimbusMessagingController ( messagingStorage )
// Update message as displayed.
val messageAsDisplayed =
val updatedMessage =
nimbusMessagingController . updateMessageAsDisplayed ( nextMessage )
nimbusMessagingController . onMessageDisplayed ( messageAsDisplayed )
// Generate the processed Message action
val processedAction = nimbusMessagingController . processMessageActionToUri ( nextMessage )
val actionIntent = Intent ( Intent . ACTION _VIEW , processedAction )
nimbusMessagingController . onMessageDisplayed ( updatedMessage )
NotificationManagerCompat . from ( context ) . notify (
MESSAGE _TAG ,
SharedIdsHelper . getNextIdForTag ( context , nextMessage . id ) ,
buildNotification ( nextMessage , actionIntent ) ,
SharedIdsHelper . getNextIdForTag ( context , updatedMessage . id ) ,
buildNotification (
context ,
updatedMessage ,
) ,
)
}
return Result . success ( )
}
private fun Message . shouldDisplayMessage ( ) = metadata . displayCount == 0
private fun buildNotification (
context : Context ,
message : Message ,
) : Notification {
val onClickPendingIntent = createOnClickPendingIntent ( context , message )
val onDismissPendingIntent = createOnDismissPendingIntent ( context , message )
return createBaseNotification (
context ,
MARKETING _CHANNEL _ID ,
message . data . title ,
message . data . text ,
onClickPendingIntent ,
onDismissPendingIntent ,
)
}
private fun buildNotification ( message : Message , intent : Intent ) : Notification {
with ( applicationContext ) {
val pendingIntent = PendingIntent . getActivity (
this ,
SharedIdsHelper . getNextIdForTag ( this , NOTIFICATION _PENDING _INTENT _TAG ) ,
intent ,
IntentUtils . defaultIntentPendingFlags ,
)
private fun createOnClickPendingIntent (
context : Context ,
message : Message ,
) : PendingIntent {
val intent = Intent ( context , NotificationClickedReceiverActivity :: class . java )
intent . putExtra ( CLICKED _MESSAGE _ID , message . id )
intent . addFlags ( Intent . FLAG _ACTIVITY _EXCLUDE _FROM _RECENTS )
// Activity intent.
return PendingIntent . getActivity (
context ,
SharedIdsHelper . getNextIdForTag ( context , NOTIFICATION _PENDING _INTENT _TAG ) ,
intent ,
IntentUtils . defaultIntentPendingFlags ,
)
}
return createBaseNotification (
this ,
MARKETING _CHANNEL _ID ,
message . data . title ,
message . data . text ,
pendingIntent ,
)
}
private fun createOnDismissPendingIntent (
context : Context ,
message : Message ,
) : PendingIntent {
val intent = Intent ( context , NotificationDismissedService :: class . java )
intent . putExtra ( DISMISSED _MESSAGE _ID , message . id )
// Service intent.
return PendingIntent . getService (
context ,
SharedIdsHelper . getNextIdForTag ( context , NOTIFICATION _PENDING _INTENT _TAG ) ,
intent ,
IntentUtils . defaultIntentPendingFlags ,
)
}
companion object {
@ -109,17 +141,91 @@ class MessageNotificationWorker(
MessageNotificationWorker :: class . java ,
pollingInterval ,
TimeUnit . MINUTES ,
) // Only start polling after the given interval
) // Only start polling after the given interval .
. setInitialDelay ( pollingInterval , TimeUnit . MINUTES )
. build ( )
val instanceWorkManager = WorkManager . getInstance ( context )
instanceWorkManager . enqueueUniquePeriodicWork (
MESSAGE _WORK _NAME ,
// We want to keep any existing scheduled work
// We want to keep any existing scheduled work .
ExistingPeriodicWorkPolicy . KEEP ,
messageWorkRequest ,
)
}
}
}
/ * *
* When a [ Message ] [ Notification ] is dismissed by the user record telemetry data and update the
* [ Message . metadata ] .
*
* This [ Service ] is only intended to be used by the [ MessageNotificationWorker . createOnDismissPendingIntent ] function .
* /
class NotificationDismissedService : Service ( ) {
/ * *
* This service cannot be bound to .
* /
override fun onBind ( intent : Intent ? ) : IBinder ? = null
@OptIn ( DelicateCoroutinesApi :: class ) // GlobalScope usage.
override fun onStartCommand ( intent : Intent ? , flags : Int , startId : Int ) : Int {
GlobalScope . launch {
if ( intent != null ) {
val nimbusMessagingController =
NimbusMessagingController ( applicationContext . components . analytics . messagingStorage )
// Get the relevant message.
val messageId = intent . getStringExtra ( DISMISSED _MESSAGE _ID ) !!
val message = nimbusMessagingController . getMessage ( messageId )
if ( message != null ) {
// Update message as 'dismissed'.
nimbusMessagingController . onMessageDismissed ( message . metadata )
}
}
}
return START _REDELIVER _INTENT
}
}
/ * *
* When a [ Message ] [ Notification ] is clicked by the user record telemetry data and update the
* [ Message . metadata ] .
*
* This [ Activity ] is only intended to be used by the [ MessageNotificationWorker . createOnClickPendingIntent ] function .
* /
class NotificationClickedReceiverActivity : Activity ( ) {
@OptIn ( DelicateCoroutinesApi :: class ) // GlobalScope usage.
override fun onCreate ( savedInstanceState : Bundle ? ) {
super . onCreate ( savedInstanceState )
GlobalScope . launch {
val nimbusMessagingController =
NimbusMessagingController ( components . analytics . messagingStorage )
// Get the relevant message.
val messageId = intent . getStringExtra ( CLICKED _MESSAGE _ID ) !!
val message = nimbusMessagingController . getMessage ( messageId )
if ( message != null ) {
// Update message as 'clicked'.
nimbusMessagingController . onMessageClicked ( message . metadata )
// Create the intent.
val intent = nimbusMessagingController . getIntentForMessageAction ( message . action )
intent . addFlags ( Intent . FLAG _ACTIVITY _NEW _TASK )
intent . addFlags ( Intent . FLAG _ACTIVITY _CLEAR _TASK )
// Start the message intent.
startActivity ( intent )
}
}
// End this activity.
finish ( )
}
}