v18.0.0 `InAppMessageListener` missing from API
Hey, Just looking at the v18.0.0 migration guide but couldn't see anything relating to InAppMessageListener no longer been available.
Could you please provide instructions on how to migrate these imports in v18.0.0?
Hi @wezley98, would you be able to share a snippet (or description) of how you're using those APIs in SDK 17? If you'd rather not post it publicly, my email is: josh.yaganeh @ airship.com
@jyaganeh Here you go.
package com.xxxx.xxxxx.xxxxx
import com.xxxx.xxxxx.interfaces.notifications.InAppNotificationMessage
import com.xxxx.xxxx.logging.data.Log
import com.urbanairship.channel.AirshipChannelListener
import com.urbanairship.iam.DisplayContent
import com.urbanairship.iam.InAppMessage
import com.urbanairship.iam.InAppMessageListener
import com.urbanairship.iam.ResolutionInfo
import com.urbanairship.iam.banner.BannerDisplayContent
import com.urbanairship.iam.fullscreen.FullScreenDisplayContent
import com.urbanairship.iam.html.HtmlDisplayContent
import com.urbanairship.iam.modal.ModalDisplayContent
import com.urbanairship.push.NotificationActionButtonInfo
import com.urbanairship.push.NotificationInfo
import com.urbanairship.push.NotificationListener
import com.urbanairship.push.PushListener
import com.urbanairship.push.PushMessage
import com.urbanairship.push.PushTokenListener
/**
* Listener for push, notifications, and registrations events.
*/
@Suppress("TooManyFunctions", "FunctionOnlyReturningConstant", "UnusedPrivateMember")
class AirshipListener(
private val notificationHandler: NotificationHandler,
private val inAppNotification: InAppNotificationMessage
) : PushListener, NotificationListener, PushTokenListener, AirshipChannelListener, InAppMessageListener {
override fun onNotificationPosted(notificationInfo: NotificationInfo) {
Log.i("AirshipListener Notification posted: $notificationInfo")
}
override fun onNotificationOpened(notificationInfo: NotificationInfo): Boolean {
notificationHandler.handleNotificationInfo(notificationInfo)
return true
}
override fun onNotificationForegroundAction(notificationInfo: NotificationInfo, actionButtonInfo: NotificationActionButtonInfo): Boolean {
Log.i("AirshipListener Notification action: $notificationInfo $actionButtonInfo")
// Return false here to allow Airship to auto launch the launcher
// activity for foreground notification action buttons
notificationHandler.handleNotificationInfo(notificationInfo)
return true
}
override fun onNotificationBackgroundAction(notificationInfo: NotificationInfo, actionButtonInfo: NotificationActionButtonInfo) {
Log.i("AirshipListener Notification action: $notificationInfo $actionButtonInfo")
}
override fun onNotificationDismissed(notificationInfo: NotificationInfo) {
Log.i(
"AirshipListener Notification dismissed. Alert: ${notificationInfo.message.alert}. " +
"Notification ID: ${notificationInfo.notificationId}"
)
}
override fun onPushReceived(message: PushMessage, notificationPosted: Boolean) {
Log.i(
"AirshipListener Received push message. Alert: ${message.alert}. Posted notification: $notificationPosted"
)
}
override fun onChannelCreated(channelId: String) {
Log.i("AirshipListener Channel created $channelId")
}
override fun onPushTokenUpdated(token: String) {
Log.i("AirshipListener Push token updated $token")
}
override fun onMessageDisplayed(scheduleId: String, message: InAppMessage) {
Log.i("AirshipListener InAppAutomation message displayed:\nscheduleId:$scheduleId,\nmessage:$message")
val contentTitle: String = getInAppMessageTitle(message)
val contentMessage: String = getInAppMessageText(message)
val buttons: List<String> = getInAppMessageButtons(message)
inAppNotification.onMessageDisplayed(
scheduleId = scheduleId,
title = contentTitle,
message = contentMessage,
buttons = buttons
)
}
override fun onMessageFinished(scheduleId: String, message: InAppMessage, resolutionInfo: ResolutionInfo) {
Log.i(
"AirshipListener InAppAutomation message display finished:" +
"\nscheduleId:$scheduleId," +
"\nmessage:$message," +
"\nresolutionType:${resolutionInfo.type}" +
"\nresolutionButton:${resolutionInfo.buttonInfo}" +
"\nmessage extras:${message.extras}" +
"\nmessage toJsonValue:${message.toJsonValue()}"
)
val contentTitle: String = getInAppMessageTitle(message)
val contentMessage: String = getInAppMessageText(message)
val buttons: List<String> = getInAppMessageButtons(message)
val buttonInfo = resolutionInfo.buttonInfo
val buttonText = buttonInfo?.label?.text.orEmpty()
val buttonActions: MutableMap<String, String> = mutableMapOf()
buttonInfo?.actions?.forEach {
buttonActions[it.key] = it.value.toString()
}
inAppNotification.onMessageFinished(
scheduleId = scheduleId,
title = contentTitle,
message = contentMessage,
buttons = buttons,
selected = buttonText,
actions = buttonActions
)
}
private fun getInAppMessageText(message: InAppMessage): String {
val messageText = when (message.type) {
InAppMessage.TYPE_MODAL -> getMessageContent<ModalDisplayContent>(message)?.body?.text.orEmpty()
InAppMessage.TYPE_BANNER -> getMessageContent<BannerDisplayContent>(message)?.body?.text.orEmpty()
InAppMessage.TYPE_FULLSCREEN -> getMessageContent<FullScreenDisplayContent>(message)?.body?.text.orEmpty()
InAppMessage.TYPE_HTML -> getMessageContent<HtmlDisplayContent>(message)?.url.orEmpty()
else -> "" // This is for CustomDisplayContent
}
Log.d("AirshipListener InAppAutomation message getInAppMessageText message is : $messageText")
return messageText
}
private fun getInAppMessageTitle(message: InAppMessage): String {
val title = when (message.type) {
InAppMessage.TYPE_MODAL -> getMessageContent<ModalDisplayContent>(message)?.heading?.text.orEmpty()
InAppMessage.TYPE_BANNER -> getMessageContent<BannerDisplayContent>(message)?.heading?.text.orEmpty()
InAppMessage.TYPE_FULLSCREEN -> getMessageContent<FullScreenDisplayContent>(
message
)?.heading?.text.orEmpty()
else -> "" // This is for CustomDisplayContent and HtmlDisplayContent
}
Log.d("AirshipListener InAppAutomation message getInAppMessageTitle title is : $title")
return title
}
private fun getInAppMessageButtons(message: InAppMessage): List<String> {
val buttons: MutableList<String> = mutableListOf()
val contentButtons = when (message.type) {
InAppMessage.TYPE_MODAL -> getMessageContent<ModalDisplayContent>(message)?.buttons
InAppMessage.TYPE_BANNER -> getMessageContent<BannerDisplayContent>(message)?.buttons
InAppMessage.TYPE_FULLSCREEN -> getMessageContent<FullScreenDisplayContent>(message)?.buttons
else -> null // This is for CustomDisplayContent and HtmlDisplayContent
}
contentButtons?.forEach { buttonInfo ->
buttonInfo.label.text?.let {
buttons.add(it)
}
}
return buttons
}
private fun <T : DisplayContent> getMessageContent(message: InAppMessage): T? {
var content: T? = null
try {
content = message.getDisplayContent<T>()
} catch (_: ClassCastException) {
Log.d(
"AirshipListener InAppAutomation message getMessageModalContent not applicable for type : ${message.type}"
)
}
return content
}
}
Thanks @wezley98!
The DisplayContent class was restructured into a new sealed class: InAppMessageDisplayContent. Here's how to migrate the helpers at the bottom of your listener (the getMessageContent helper should no longer be needed):
private fun getInAppMessageText(message: InAppMessage): String {
val messageText = when (val content = message.displayContent) {
is InAppMessageDisplayContent.ModalContent -> content.modal.body?.text.orEmpty()
is InAppMessageDisplayContent.BannerContent -> content.banner.body?.text.orEmpty()
is InAppMessageDisplayContent.FullscreenContent -> content.fullscreen.body?.text.orEmpty()
is InAppMessageDisplayContent.HTMLContent -> content.html.url
else -> "" // Scenes & Surveys, and Custom display content
}
Log.d("AirshipListener InAppAutomation message getInAppMessageText message is : $messageText")
return messageText
}
private fun getInAppMessageTitle(message: InAppMessage): String {
val title = when (val content = message.displayContent) {
is InAppMessageDisplayContent.ModalContent -> content.modal.heading?.text.orEmpty()
is InAppMessageDisplayContent.BannerContent -> content.banner.heading?.text.orEmpty()
is InAppMessageDisplayContent.FullscreenContent -> content.fullscreen.heading?.text.orEmpty()
else -> "" // Scenes & Surveys, HTML, and Custom display content
}
Log.d("AirshipListener InAppAutomation message getInAppMessageTitle title is : $title")
return title
}
private fun getInAppMessageButtons(message: InAppMessage): List<String> {
val buttons: MutableList<String> = mutableListOf()
val contentButtons = when (val content = message.displayContent) {
is InAppMessageDisplayContent.ModalContent -> content.modal.buttons
is InAppMessageDisplayContent.BannerContent -> content.banner.buttons
is InAppMessageDisplayContent.FullscreenContent -> content.fullscreen.buttons
else -> null // Scenes & Surveys, HTML, and Custom display content
}
contentButtons?.forEach { buttonInfo ->
buttons.add(buttonInfo.label.text)
}
return buttons
}
The replacement for InAppMessageListener is InAppMessageDisplayDelegate, which has the following methods:
fun isMessageReadyToDisplay(): BooleanCalled to check if the message is ready to be displayed. Returntrueto use the SDK's default logic for determining if messages are ready to display.fun messageWillDisplay(message: InAppMessage, scheduleId: String)- called when a message will be displayedfun messageFinishedDisplaying(message: InAppMessage, scheduleId: String)- called when a message finishes displaying
You can register the display delegate like this:
InAppAutomation.shared().inAppMessaging.displayDelegate = AirshipListener(...)
In the new display delegate, the messageFinishedDisplaying() callback doesn't receive the resolution info, but it's still possible to listen to the events Flow on the analytics class to get info about InApp resolutions. Here's an example:
scope.launch {
airship.analytics.events
.filter { it.type == EventType.IN_APP_RESOLUTION }
.collect { event ->
val body = event.body.optMap()
val messageId = body.opt("id").optMap().opt("message_id").string
val resolution = body.opt("resolution").optMap()
Log.i("InApp Message ($messageId) resolution: $resolution")
}
}
The full event body looks like:
{"context":{"button":{"identifier":"dismiss_button"},"reporting_context":{"iax_linking_id":"8eefb821-4bac-4db0-b170-1275d9881c85","experiment_id":"","content_types":["scene"]},"pager":{"count":1,"identifier":"59748c2d-236e-4ca6-9a7b-8bf79260071a","completed":true,"page_identifier":"1a365d16-cb46-42ab-9bee-64b0bf5aabaf","page_index":0},"display":{"is_first_display":false,"is_first_display_trigger_session":true,"trigger_session_id":"88b0bd79-2c00-43c4-83d6-24c740d86d9b"}},"id":{"message_id":"8eefb821-4bac-4db0-b170-1275d9881c85"},"source":"urban-airship","resolution":{"button_id":"dismiss_button","type":"button_click","display_time":2,"button_description":"dismiss_button"}}
And the resolution object looks like:
{"button_id":"dismiss_button","type":"button_click","display_time":2,"button_description":"dismiss_button"}
I'm not sure what the onMessageFinished(...) call on your InAppNotificationMessage class is doing with the resolution info that used to be provided on the InAppMessageListener. onMessageFinished callback, so let me know if using the analytics event feed doesn't fit your use case.
According to the posted code resolution is used to get the actions on the buttons and one which was selected. Which can be useful to know I guess.
val buttonInfo = resolutionInfo.buttonInfo
val buttonText = buttonInfo?.label?.text.orEmpty()
val buttonActions: MutableMap<String, String> = mutableMapOf()
buttonInfo?.actions?.forEach {
buttonActions[it.key] = it.value.toString()
}
Proposed airship.analytics.events Flow observing is a bit different and can happen at different pace and not be available at time when delegate call happens fun messageFinishedDisplaying(message: InAppMessage, scheduleId: String).
Also app crashes when takeoff is called and we did not disable analytics with builder AirshipConfigOptions.
App crashes at AppForegroundEvent.getEventData. With it disabled it setup fine AirshipConfigOptions.Builder().setAnalyticsEnabled(false).
java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String com.urbanairship.analytics.Analytics.getConversionSendId()' on a null object reference
at com.urbanairship.analytics.AppForegroundEvent.getEventData(AppForegroundEvent.java:49)
at com.urbanairship.analytics.Analytics.addEvent(Analytics.kt:318)
at com.urbanairship.analytics.Analytics.onForeground(Analytics.kt:354)
at com.urbanairship.analytics.Analytics.<init>(Analytics.kt:203)
at com.urbanairship.analytics.Analytics.<init>(Analytics.kt:52)
at com.urbanairship.analytics.Analytics.<init>(Analytics.kt:176)
at com.urbanairship.UAirship.init(UAirship.java:732)
at com.urbanairship.UAirship.executeTakeOff(UAirship.java:428)
at com.urbanairship.UAirship.access$000(UAirship.java:72)
at com.urbanairship.UAirship$2.run(UAirship.java:387)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
at com.urbanairship.util.AirshipThreadFactory$1.run(AirshipThreadFactory.java:50)
at java.lang.Thread.run(Thread.java:1012)
thanks for the report, looking into it
@DejanMedicSKY @wezley98 Are you working on the same implementation? Any chance we can sync on a call to figure out how we can provide the right info for you?
I am mostly curious how you are using this data? Yes the flow changes when you get it but if you are just generating reporting from the data then its probably fine and you can replace the listener with the flow. If you can provide more details on these methods then we can hopefully unblock you:
inAppNotification.onMessageDisplayed(
scheduleId = scheduleId,
title = contentTitle,
message = contentMessage,
buttons = buttons
)
inAppNotification.onMessageFinished(
scheduleId = scheduleId,
title = contentTitle,
message = contentMessage,
buttons = buttons,
selected = buttonText,
actions = buttonActions
)
@rlepinski Basically we have a business request to intercept the callbacks and send this information to another analytics system. eg which button was pressed by the user. I'm checking with my colleague @DejanMedicSKY to see if we have everything we need now. Looks like v18.1.0 also fixes the crash we were seeing.
@wezley98 @DejanMedicSKY, thanks for the email! would one or both of you be able to join a short call to discuss this further? We'd like to make sure that we fully understand your needs, so that we can either make the necessary changes in an upcoming SDK release, or help with an alternate solution that satisfies your use case.
@jyaganeh We've dropped you an email on 21st June, yes happy to jump on a call to discuss further.
I didn't hear back from you re: availability, so I'm going to close this out for now. If you'd still like to schedule a call or have any other questions or concerns, feel free to email me or re-open this issue.