Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: move push click handling to processor #268

Merged
merged 22 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/deploy-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,9 @@ jobs:
id: semantic-release
with:
dry_run: false
# version numbers below can be in many forms: M, M.m, M.m.p
semantic_version: latest
extra_plugins: |
conventional-changelog-conventionalcommits
@semantic-release/changelog
@semantic-release/git
@semantic-release/github
@semantic-release/exec
env:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import android.os.Bundle
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationCompat
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
Expand Down Expand Up @@ -124,7 +125,7 @@
bundle.putInt(NOTIFICATION_REQUEST_CODE, requestCode)

val applicationInfo = try {
context.packageManager.getApplicationInfo(

Check warning on line 128 in messagingpush/src/main/java/io/customer/messagingpush/CustomerIOPushNotificationHandler.kt

View workflow job for this annotation

GitHub Actions / API check

'getApplicationInfo(String, Int): ApplicationInfo' is deprecated. Deprecated in Java

Check warning on line 128 in messagingpush/src/main/java/io/customer/messagingpush/CustomerIOPushNotificationHandler.kt

View workflow job for this annotation

GitHub Actions / API check

'getApplicationInfo(String, Int): ApplicationInfo' is deprecated. Deprecated in Java

Check warning on line 128 in messagingpush/src/main/java/io/customer/messagingpush/CustomerIOPushNotificationHandler.kt

View workflow job for this annotation

GitHub Actions / Android Lint (messagingpush)

'getApplicationInfo(String, Int): ApplicationInfo' is deprecated. Deprecated in Java

Check warning on line 128 in messagingpush/src/main/java/io/customer/messagingpush/CustomerIOPushNotificationHandler.kt

View workflow job for this annotation

GitHub Actions / Android Lint (messagingpush)

'getApplicationInfo(String, Int): ApplicationInfo' is deprecated. Deprecated in Java

Check warning on line 128 in messagingpush/src/main/java/io/customer/messagingpush/CustomerIOPushNotificationHandler.kt

View workflow job for this annotation

GitHub Actions / Unit tests (messagingpush)

'getApplicationInfo(String, Int): ApplicationInfo' is deprecated. Deprecated in Java

Check warning on line 128 in messagingpush/src/main/java/io/customer/messagingpush/CustomerIOPushNotificationHandler.kt

View workflow job for this annotation

GitHub Actions / Unit tests (messagingpush)

'getApplicationInfo(String, Int): ApplicationInfo' is deprecated. Deprecated in Java

Check warning on line 128 in messagingpush/src/main/java/io/customer/messagingpush/CustomerIOPushNotificationHandler.kt

View workflow job for this annotation

GitHub Actions / instrumentation-test (java_layout)

'getApplicationInfo(String, Int): ApplicationInfo' is deprecated. Deprecated in Java

Check warning on line 128 in messagingpush/src/main/java/io/customer/messagingpush/CustomerIOPushNotificationHandler.kt

View workflow job for this annotation

GitHub Actions / instrumentation-test (java_layout)

'getApplicationInfo(String, Int): ApplicationInfo' is deprecated. Deprecated in Java

Check warning on line 128 in messagingpush/src/main/java/io/customer/messagingpush/CustomerIOPushNotificationHandler.kt

View workflow job for this annotation

GitHub Actions / instrumentation-test (kotlin_compose)

'getApplicationInfo(String, Int): ApplicationInfo' is deprecated. Deprecated in Java

Check warning on line 128 in messagingpush/src/main/java/io/customer/messagingpush/CustomerIOPushNotificationHandler.kt

View workflow job for this annotation

GitHub Actions / instrumentation-test (kotlin_compose)

'getApplicationInfo(String, Int): ApplicationInfo' is deprecated. Deprecated in Java
context.packageName,
PackageManager.GET_META_DATA
)
Expand Down Expand Up @@ -205,11 +206,11 @@
payload = payload,
builder = notificationBuilder
)
createIntentForNotificationClick(

Check warning on line 209 in messagingpush/src/main/java/io/customer/messagingpush/CustomerIOPushNotificationHandler.kt

View check run for this annotation

Codecov / codecov/patch

messagingpush/src/main/java/io/customer/messagingpush/CustomerIOPushNotificationHandler.kt#L209

Added line #L209 was not covered by tests
context,
requestCode,
payload
).let { pendingIntent ->

Check warning on line 213 in messagingpush/src/main/java/io/customer/messagingpush/CustomerIOPushNotificationHandler.kt

View check run for this annotation

Codecov / codecov/patch

messagingpush/src/main/java/io/customer/messagingpush/CustomerIOPushNotificationHandler.kt#L213

Added line #L213 was not covered by tests
notificationBuilder.setContentIntent(pendingIntent)
}

Expand All @@ -217,7 +218,8 @@
notificationManager.notify(requestCode, notification)
}

private fun createIntentForNotificationClick(
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun createIntentForNotificationClick(
context: Context,
requestCode: Int,
payload: CustomerIOParsedPushPayload
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,9 @@
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.core.app.TaskStackBuilder
import io.customer.messagingpush.MessagingPushModuleConfig
import io.customer.messagingpush.config.PushClickBehavior
import io.customer.messagingpush.data.model.CustomerIOParsedPushPayload
import io.customer.messagingpush.di.deepLinkUtil
import io.customer.messagingpush.di.moduleConfig
import io.customer.messagingpush.extensions.parcelable
import io.customer.messagingpush.util.DeepLinkUtil
import io.customer.messagingpush.di.pushMessageProcessor
import io.customer.sdk.CustomerIO
import io.customer.sdk.CustomerIOShared
import io.customer.sdk.data.request.MetricEvent
import io.customer.sdk.extensions.takeIfNotBlank
import io.customer.sdk.tracking.TrackableScreen
import io.customer.sdk.util.Logger

Expand All @@ -24,117 +15,42 @@
* This activity is launched when a notification is clicked. It tracks opened
* metrics, handles the deep link and opens the desired activity in the host app.
*/
class NotificationClickReceiverActivity : Activity(), TrackableScreen {
val logger: Logger by lazy { CustomerIOShared.instance().diStaticGraph.logger }

Check warning on line 19 in messagingpush/src/main/java/io/customer/messagingpush/activity/NotificationClickReceiverActivity.kt

View check run for this annotation

Codecov / codecov/patch

messagingpush/src/main/java/io/customer/messagingpush/activity/NotificationClickReceiverActivity.kt#L18-L19

Added lines #L18 - L19 were not covered by tests

override fun getScreenName(): String? {
// Return null to prevent this screen from being tracked
return null

Check warning on line 23 in messagingpush/src/main/java/io/customer/messagingpush/activity/NotificationClickReceiverActivity.kt

View check run for this annotation

Codecov / codecov/patch

messagingpush/src/main/java/io/customer/messagingpush/activity/NotificationClickReceiverActivity.kt#L23

Added line #L23 was not covered by tests
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleIntent(data = intent)
}

Check warning on line 29 in messagingpush/src/main/java/io/customer/messagingpush/activity/NotificationClickReceiverActivity.kt

View check run for this annotation

Codecov / codecov/patch

messagingpush/src/main/java/io/customer/messagingpush/activity/NotificationClickReceiverActivity.kt#L27-L29

Added lines #L27 - L29 were not covered by tests

override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
handleIntent(data = intent)
}

Check warning on line 34 in messagingpush/src/main/java/io/customer/messagingpush/activity/NotificationClickReceiverActivity.kt

View check run for this annotation

Codecov / codecov/patch

messagingpush/src/main/java/io/customer/messagingpush/activity/NotificationClickReceiverActivity.kt#L32-L34

Added lines #L32 - L34 were not covered by tests

private fun handleIntent(data: Intent?) {
kotlin.runCatching {
val payload: CustomerIOParsedPushPayload? =
data?.extras?.parcelable(NOTIFICATION_PAYLOAD_EXTRA)
if (payload == null) {
logger.error("Payload is null, cannot handle notification intent")
if (data == null) {
// This should never happen ideally
logger.error("Intent is null, cannot process notification click")

Check warning on line 39 in messagingpush/src/main/java/io/customer/messagingpush/activity/NotificationClickReceiverActivity.kt

View check run for this annotation

Codecov / codecov/patch

messagingpush/src/main/java/io/customer/messagingpush/activity/NotificationClickReceiverActivity.kt#L39

Added line #L39 was not covered by tests
} else {
levibostian marked this conversation as resolved.
Show resolved Hide resolved
val sdkInstance = CustomerIO.instanceOrNull(context = this)

Check warning on line 41 in messagingpush/src/main/java/io/customer/messagingpush/activity/NotificationClickReceiverActivity.kt

View check run for this annotation

Codecov / codecov/patch

messagingpush/src/main/java/io/customer/messagingpush/activity/NotificationClickReceiverActivity.kt#L41

Added line #L41 was not covered by tests
if (sdkInstance == null) {
logger.error("SDK is not initialized, cannot handle notification intent")

Check warning on line 43 in messagingpush/src/main/java/io/customer/messagingpush/activity/NotificationClickReceiverActivity.kt

View check run for this annotation

Codecov / codecov/patch

messagingpush/src/main/java/io/customer/messagingpush/activity/NotificationClickReceiverActivity.kt#L43

Added line #L43 was not covered by tests
} else {
processNotificationIntent(payload = payload)
sdkInstance.diGraph.pushMessageProcessor.processNotificationClick(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

think this should be covered by test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can. But I avoided because I don't think this adds a lot of value. It mostly relies on Android OS lifecycle and couple of other methods that are already being tested. Also, it requires setting up Activity lifecycle for testing which I plan to improve later to gain more confidence in testing. However, if you think we should still cover this, I can write a test for it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it still alots value because the whole logic revolves around what flags to expect and what behaviour it would display.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have pushed more tests, hope it helps.

PS: Github is having issues in PRs, so it might take some time to show new changes.

activityContext = this,
intent = data

Check warning on line 47 in messagingpush/src/main/java/io/customer/messagingpush/activity/NotificationClickReceiverActivity.kt

View check run for this annotation

Codecov / codecov/patch

messagingpush/src/main/java/io/customer/messagingpush/activity/NotificationClickReceiverActivity.kt#L45-L47

Added lines #L45 - L47 were not covered by tests
)
}
}.onFailure { ex ->
logger.error("Failed to process notification intent: ${ex.message}")
}
finish()
}

Check warning on line 52 in messagingpush/src/main/java/io/customer/messagingpush/activity/NotificationClickReceiverActivity.kt

View check run for this annotation

Codecov / codecov/patch

messagingpush/src/main/java/io/customer/messagingpush/activity/NotificationClickReceiverActivity.kt#L51-L52

Added lines #L51 - L52 were not covered by tests

private fun processNotificationIntent(payload: CustomerIOParsedPushPayload) {
val sdkInstance = CustomerIO.instanceOrNull(context = this)
if (sdkInstance == null) {
logger.error("SDK is not initialized, cannot handle notification intent")
return
}

val moduleConfig: MessagingPushModuleConfig = sdkInstance.diGraph.moduleConfig
trackMetrics(moduleConfig, payload)
handleDeepLink(moduleConfig, payload)
}

private fun trackMetrics(
moduleConfig: MessagingPushModuleConfig,
payload: CustomerIOParsedPushPayload
) {
if (moduleConfig.autoTrackPushEvents) {
CustomerIO.instance().trackMetric(
payload.cioDeliveryId,
MetricEvent.opened,
payload.cioDeliveryToken
)
}
}

private fun handleDeepLink(
moduleConfig: MessagingPushModuleConfig,
payload: CustomerIOParsedPushPayload
) {
val deepLinkUtil: DeepLinkUtil = CustomerIO.instance().diGraph.deepLinkUtil
val deepLink = payload.deepLink?.takeIfNotBlank()

// check if host app overrides the handling of deeplink
val notificationCallback = moduleConfig.notificationCallback
val taskStackFromPayload = notificationCallback?.createTaskStackFromPayload(this, payload)
if (taskStackFromPayload != null) {
logger.info("Notification target overridden by createTaskStackFromPayload, starting new stack for link $deepLink")
taskStackFromPayload.startActivities()
return
}

// Get the default intent for the host app
val defaultHostAppIntent = deepLinkUtil.createDefaultHostAppIntent(context = this)
// Check if the deep links are handled within the host app
val deepLinkHostAppIntent = deepLink?.let { link ->
deepLinkUtil.createDeepLinkHostAppIntent(context = this, link = link)
}
// Check if the deep links can be opened outside the host app
val deepLinkExternalIntent = deepLink?.let { link ->
deepLinkUtil.createDeepLinkExternalIntent(context = this, link = link)
}
val deepLinkIntent: Intent = deepLinkHostAppIntent
?: deepLinkExternalIntent
?: defaultHostAppIntent
?: return
deepLinkIntent.putExtra(NOTIFICATION_PAYLOAD_EXTRA, payload)
logger.info("Dispatching notification with link $deepLink to intent: $deepLinkIntent with behavior: ${moduleConfig.pushClickBehavior}")

when (moduleConfig.pushClickBehavior) {
PushClickBehavior.RESET_TASK_STACK -> {
val taskStackBuilder = TaskStackBuilder.create(this).apply {
addNextIntentWithParentStack(deepLinkIntent)
}
taskStackBuilder.startActivities()
}

PushClickBehavior.ACTIVITY_PREVENT_RESTART -> {
deepLinkIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_SINGLE_TOP
startActivity(deepLinkIntent)
}

PushClickBehavior.ACTIVITY_NO_FLAGS -> {
startActivity(deepLinkIntent)
}
}
}

companion object {
const val NOTIFICATION_PAYLOAD_EXTRA = "CIO_NotificationPayloadExtras"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ internal val CustomerIOComponent.pushMessageProcessor: PushMessageProcessor
PushMessageProcessorImpl(
logger = logger,
moduleConfig = moduleConfig,
deepLinkUtil = deepLinkUtil,
trackRepository = trackRepository
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.customer.messagingpush.processor

import android.content.Context
import android.content.Intent
import com.google.firebase.messaging.RemoteMessage
import io.customer.base.internal.InternalCustomerIOApi
Expand Down Expand Up @@ -45,6 +46,21 @@ interface PushMessageProcessor {
*/
fun processRemoteMessageDeliveredMetrics(deliveryId: String, deliveryToken: String)

/**
* Executes the necessary actions when a notification is clicked by the user.
*
* This method performs the following tasks:
* 1. Tracks 'opened' metrics for the notification.
* 2. Resolves the deep link, if available in the notification payload, and navigates to the corresponding screen.
* 3. If no deep link is provided, opens the default launcher screen.
*
* This method may only be called from `onCreate` or `onNewIntent` methods of notification handler activity.
*
* @param activityContext context should be from activity as this will be used for launching activity
* @param intent intent received by the activity
*/
fun processNotificationClick(activityContext: Context, intent: Intent)

companion object {
// Count of messages stored in memory
const val RECENT_MESSAGES_MAX_SIZE = 10
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
package io.customer.messagingpush.processor

import android.content.Context
import android.content.Intent
import androidx.annotation.VisibleForTesting
import androidx.core.app.TaskStackBuilder
import io.customer.messagingpush.MessagingPushModuleConfig
import io.customer.messagingpush.activity.NotificationClickReceiverActivity
import io.customer.messagingpush.config.PushClickBehavior
import io.customer.messagingpush.data.model.CustomerIOParsedPushPayload
import io.customer.messagingpush.extensions.parcelable
import io.customer.messagingpush.util.DeepLinkUtil
import io.customer.messagingpush.util.PushTrackingUtil
import io.customer.sdk.data.request.MetricEvent
import io.customer.sdk.extensions.takeIfNotBlank
import io.customer.sdk.repository.TrackRepository
import io.customer.sdk.util.Logger

internal class PushMessageProcessorImpl(
private val logger: Logger,
private val moduleConfig: MessagingPushModuleConfig,
private val deepLinkUtil: DeepLinkUtil,
private val trackRepository: TrackRepository
) : PushMessageProcessor {

Expand Down Expand Up @@ -85,4 +94,95 @@
)
}
}

override fun processNotificationClick(activityContext: Context, intent: Intent) {
kotlin.runCatching {
val payload: CustomerIOParsedPushPayload? =
intent.extras?.parcelable(NotificationClickReceiverActivity.NOTIFICATION_PAYLOAD_EXTRA)
if (payload == null) {
logger.error("Payload is null, cannot handle notification intent")
} else {
handleNotificationClickIntent(activityContext, payload)
}
}.onFailure { ex ->
logger.error("Failed to process notification intent: ${ex.message}")
}
}

private fun handleNotificationClickIntent(
activityContext: Context,
payload: CustomerIOParsedPushPayload
) {
trackNotificationClickMetrics(payload)
handleNotificationDeepLink(activityContext, payload)
}

private fun trackNotificationClickMetrics(payload: CustomerIOParsedPushPayload) {
if (moduleConfig.autoTrackPushEvents) {
trackRepository.trackMetric(
deliveryID = payload.cioDeliveryId,
event = MetricEvent.opened,
deviceToken = payload.cioDeliveryToken
)
}
}

private fun handleNotificationDeepLink(
activityContext: Context,
payload: CustomerIOParsedPushPayload
) {
val deepLink = payload.deepLink?.takeIfNotBlank()

// check if host app overrides the handling of deeplink
val notificationCallback = moduleConfig.notificationCallback
val taskStackFromPayload = notificationCallback?.createTaskStackFromPayload(
context = activityContext,
payload = payload
)
if (taskStackFromPayload != null) {
logger.info("Notification target overridden by createTaskStackFromPayload, starting new stack for link $deepLink")
taskStackFromPayload.startActivities()
return

Check warning on line 145 in messagingpush/src/main/java/io/customer/messagingpush/processor/PushMessageProcessorImpl.kt

View check run for this annotation

Codecov / codecov/patch

messagingpush/src/main/java/io/customer/messagingpush/processor/PushMessageProcessorImpl.kt#L144-L145

Added lines #L144 - L145 were not covered by tests
}

// Get the default intent for the host app
val defaultHostAppIntent =
deepLinkUtil.createDefaultHostAppIntent(context = activityContext)
// Check if the deep links are handled within the host app
val deepLinkHostAppIntent = deepLink?.let { link ->
deepLinkUtil.createDeepLinkHostAppIntent(context = activityContext, link = link)
}
// Check if the deep links can be opened outside the host app
val deepLinkExternalIntent = deepLink?.let { link ->
deepLinkUtil.createDeepLinkExternalIntent(context = activityContext, link = link)
}
val deepLinkIntent: Intent = deepLinkHostAppIntent
?: deepLinkExternalIntent
?: defaultHostAppIntent
?: return
deepLinkIntent.putExtra(
NotificationClickReceiverActivity.NOTIFICATION_PAYLOAD_EXTRA,
payload
)
logger.info("Dispatching notification with link $deepLink to intent: $deepLinkIntent with behavior: ${moduleConfig.pushClickBehavior}")

when (moduleConfig.pushClickBehavior) {
PushClickBehavior.RESET_TASK_STACK -> {
val taskStackBuilder = TaskStackBuilder.create(activityContext).apply {
addNextIntentWithParentStack(deepLinkIntent)
}
taskStackBuilder.startActivities()
}

PushClickBehavior.ACTIVITY_PREVENT_RESTART -> {
deepLinkIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_SINGLE_TOP
activityContext.startActivity(deepLinkIntent)
}

PushClickBehavior.ACTIVITY_NO_FLAGS -> {
activityContext.startActivity(deepLinkIntent)

Check warning on line 184 in messagingpush/src/main/java/io/customer/messagingpush/processor/PushMessageProcessorImpl.kt

View check run for this annotation

Codecov / codecov/patch

messagingpush/src/main/java/io/customer/messagingpush/processor/PushMessageProcessorImpl.kt#L184

Added line #L184 was not covered by tests
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.customer.messagingpush

import android.os.Bundle
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.firebase.messaging.RemoteMessage
import io.customer.commontest.BaseTest
import io.customer.messagingpush.activity.NotificationClickReceiverActivity
import io.customer.messagingpush.data.model.CustomerIOParsedPushPayload
import io.customer.messagingpush.extensions.parcelable
import io.customer.sdk.extensions.random
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.robolectric.Shadows

@RunWith(AndroidJUnit4::class)
internal class CustomerIOPushNotificationHandlerTest : BaseTest() {

private lateinit var pushNotificationHandler: CustomerIOPushNotificationHandler
private lateinit var pushNotificationPayload: CustomerIOParsedPushPayload

@Before
override fun setup() {
super.setup()

val extras = Bundle.EMPTY
pushNotificationHandler = CustomerIOPushNotificationHandler(mock(), RemoteMessage(extras))
pushNotificationPayload = CustomerIOParsedPushPayload(
extras = extras,
deepLink = String.random,
cioDeliveryId = String.random,
cioDeliveryToken = String.random,
title = String.random,
body = String.random
)
}

@Test
fun createIntentForNotificationClick_givenAnyPayload_shouldStartNotificationClickReceiverActivity() {
val actualPendingIntent = pushNotificationHandler.createIntentForNotificationClick(
context,
Int.random(1000, 9999),
pushNotificationPayload
)

actualPendingIntent.send()
val nextStartedActivity = Shadows.shadowOf(application).nextStartedActivity
val nextStartedActivityIntent = Shadows.shadowOf(nextStartedActivity)
val nextStartedActivityPayload: CustomerIOParsedPushPayload? =
nextStartedActivity.extras?.parcelable(NotificationClickReceiverActivity.NOTIFICATION_PAYLOAD_EXTRA)

nextStartedActivityIntent.intentClass shouldBeEqualTo NotificationClickReceiverActivity::class.java
nextStartedActivityPayload shouldBeEqualTo pushNotificationPayload
}
}
Loading
Loading