Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased
- Replaced the deprecated `AsyncTask`-based push notification handling with `WorkManager` for improved reliability and compatibility with modern Android versions. No action is required.
- Added `onEmbeddedMessagingSyncSucceeded()` and `onEmbeddedMessagingSyncFailed()` callbacks to `IterableEmbeddedUpdateHandler` for monitoring embedded message sync results.

## [3.6.6]
### Fixed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.iterable.iterableapi

import android.content.Context
import com.iterable.iterableapi.IterableHelper.SuccessHandler
import org.json.JSONException
import org.json.JSONObject

Expand All @@ -22,12 +21,9 @@ public class IterableEmbeddedManager : IterableActivityMonitor.AppStateCallback
private var embeddedSessionManager = EmbeddedSessionManager()

private var activityMonitor: IterableActivityMonitor? = null

// endregion

// region constructor

//Constructor of this class with actionHandler and updateHandler
public constructor(
iterableApi: IterableApi
) {
Expand All @@ -38,23 +34,19 @@ public class IterableEmbeddedManager : IterableActivityMonitor.AppStateCallback
activityMonitor?.addCallback(this)
}
}

// endregion

// region getters and setters
// region public methods

//Add updateHandler to the list
public fun addUpdateListener(updateHandler: IterableEmbeddedUpdateHandler) {
updateHandleListeners.add(updateHandler)
}

//Remove updateHandler from the list
public fun removeUpdateListener(updateHandler: IterableEmbeddedUpdateHandler) {
updateHandleListeners.remove(updateHandler)
embeddedSessionManager.endSession()
}

//Get the list of updateHandlers
public fun getUpdateHandlers(): List<IterableEmbeddedUpdateHandler> {
return updateHandleListeners
}
Expand All @@ -63,11 +55,6 @@ public class IterableEmbeddedManager : IterableActivityMonitor.AppStateCallback
return embeddedSessionManager
}

// endregion

// region public methods

//Gets the list of embedded messages in memory without syncing
fun getMessages(placementId: Long): List<IterableEmbeddedMessage>? {
return localPlacementMessagesMap[placementId]
}
Expand All @@ -80,101 +67,40 @@ public class IterableEmbeddedManager : IterableActivityMonitor.AppStateCallback
return localPlacementIds
}

fun syncMessages() {
syncMessages(emptyArray<Long>())
}

//Network call to get the embedded messages
fun syncMessages(placementIds: Array<Long>) {
@JvmOverloads
fun syncMessages(placementIds: Array<Long> = emptyArray()) {
if (iterableApi.config.enableEmbeddedMessaging) {
IterableLogger.v(TAG, "Syncing messages...")

IterableApi.sharedInstance.getEmbeddedMessages(placementIds, { data ->
IterableLogger.v(TAG, "Got response from network call to get embedded messages")
try {
val previousPlacementIds = getPlacementIds()
val currentPlacementIds: MutableList<Long> = mutableListOf()

val placementsArray =
data.optJSONArray(IterableConstants.ITERABLE_EMBEDDED_MESSAGE_PLACEMENTS)
if (placementsArray != null) {
//if there are no placements in the payload
//reset the local message storage and trigger a UI update
if (placementsArray.length() == 0) {
reset()
if (previousPlacementIds.isNotEmpty()) {
updateHandleListeners.forEach {
IterableLogger.d(TAG, "Calling updateHandler")
it.onMessagesUpdated()
}
}
} else {
for (i in 0 until placementsArray.length()) {
val placementJson = placementsArray.optJSONObject(i)
val placement =
IterableEmbeddedPlacement.fromJSONObject(placementJson)
val placementId = placement.placementId
val messages = placement.messages

currentPlacementIds.add(placementId)
updateLocalMessageMap(placementId, messages)
}
}
}

// compare previous placements to the current placement payload
val removedPlacementIds =
previousPlacementIds.subtract(currentPlacementIds.toSet())

//if there are placements removed, update the local storage and trigger UI update
if (removedPlacementIds.isNotEmpty()) {
removedPlacementIds.forEach {
localPlacementMessagesMap.remove(it)
}

updateHandleListeners.forEach {
IterableLogger.d(TAG, "Calling updateHandler")
it.onMessagesUpdated()
}
}

//store placements from payload for next comparison
localPlacementIds = currentPlacementIds

} catch (e: JSONException) {
IterableLogger.e(TAG, e.toString())
}
}, object : IterableHelper.FailureHandler {
override fun onFailure(reason: String, data: JSONObject?) {
if (reason.equals(
"SUBSCRIPTION_INACTIVE",
ignoreCase = true
) || reason.equals("Invalid API Key", ignoreCase = true)
) {
IterableLogger.e(TAG, "Subscription is inactive. Stopping sync")
broadcastSubscriptionInactive()
return
} else {
//TODO: Instead of generic condition, have the retry only in certain situation
IterableLogger.e(TAG, "Error while fetching embedded messages: $reason")
IterableApi.sharedInstance.getEmbeddedMessages(
placementIds,
{ data ->
try {
processEmbeddedMessagesResponse(data)
} catch (e: JSONException) {
IterableLogger.e(TAG, e.toString())
}
notifySyncSucceeded()
Comment thread
franco-zalamena-iterable marked this conversation as resolved.
Outdated
},
{ reason, data ->
handleSyncFailure(reason, data)
notifySyncFailed(reason)
}
})
)
}
}

fun handleEmbeddedClick(message: IterableEmbeddedMessage, buttonIdentifier: String?, clickedUrl: String?) {
if ((clickedUrl != null) && clickedUrl.toString().isNotEmpty()) {
if (clickedUrl.startsWith(IterableConstants.URL_SCHEME_ACTION)) {
// This is an action:// URL, pass that to the custom action handler
val actionName: String = clickedUrl.replace(IterableConstants.URL_SCHEME_ACTION, "")
IterableActionRunner.executeAction(
context,
IterableAction.actionCustomAction(actionName),
IterableActionSource.EMBEDDED
)
} else if (clickedUrl.startsWith(IterableConstants.URL_SCHEME_ITBL)) {
// Handle itbl:// URLs, pass that to the custom action handler for compatibility
val actionName: String = clickedUrl.replace(IterableConstants.URL_SCHEME_ITBL, "")
IterableActionRunner.executeAction(
context,
Expand All @@ -191,6 +117,82 @@ public class IterableEmbeddedManager : IterableActivityMonitor.AppStateCallback
}
}

// endregion

// region private methods

private fun processEmbeddedMessagesResponse(data: JSONObject) {
IterableLogger.v(TAG, "Got response from network call to get embedded messages")
val previousPlacementIds = getPlacementIds()
val currentPlacementIds: MutableList<Long> = mutableListOf()

val placementsArray =
data.optJSONArray(IterableConstants.ITERABLE_EMBEDDED_MESSAGE_PLACEMENTS)
if (placementsArray != null) {
if (placementsArray.length() == 0) {
reset()
if (previousPlacementIds.isNotEmpty()) {
updateHandleListeners.forEach {
IterableLogger.d(TAG, "Calling updateHandler")
it.onMessagesUpdated()
}
}
} else {
for (i in 0 until placementsArray.length()) {
val placementJson = placementsArray.optJSONObject(i)
val placement =
IterableEmbeddedPlacement.fromJSONObject(placementJson)
val placementId = placement.placementId
val messages = placement.messages

currentPlacementIds.add(placementId)
updateLocalMessageMap(placementId, messages)
}
}
}

val removedPlacementIds =
previousPlacementIds.subtract(currentPlacementIds.toSet())

if (removedPlacementIds.isNotEmpty()) {
removedPlacementIds.forEach {
localPlacementMessagesMap.remove(it)
}

updateHandleListeners.forEach {
IterableLogger.d(TAG, "Calling updateHandler")
it.onMessagesUpdated()
}
}

localPlacementIds = currentPlacementIds
}

private fun handleSyncFailure(reason: String, data: JSONObject?) {
if (reason.equals(
"SUBSCRIPTION_INACTIVE",
ignoreCase = true
) || reason.equals("Invalid API Key", ignoreCase = true)
) {
IterableLogger.e(TAG, "Subscription is inactive. Stopping sync")
broadcastSubscriptionInactive()
} else {
IterableLogger.e(TAG, "Error while fetching embedded messages: $reason")
}
}

private fun notifySyncSucceeded() {
updateHandleListeners.forEach {
it.onEmbeddedMessagingSyncSucceeded()
}
}

private fun notifySyncFailed(reason: String?) {
updateHandleListeners.forEach {
it.onEmbeddedMessagingSyncFailed(reason)
}
}

private fun broadcastSubscriptionInactive() {
updateHandleListeners.forEach {
IterableLogger.d(TAG, "Broadcasting subscription inactive to the views")
Expand All @@ -205,23 +207,18 @@ public class IterableEmbeddedManager : IterableActivityMonitor.AppStateCallback
IterableLogger.printInfo()
var localMessagesChanged = false

// Get local messages in a mutable list
val localMessageMap = mutableMapOf<String, IterableEmbeddedMessage>()
getMessages(placementId)?.toMutableList()?.forEach {
localMessageMap[it.metadata.messageId] = it
}

// Compare the remote list to local list
// if there are new messages, trigger a message update in UI and send out received events
remoteMessageList.forEach {
if (!localMessageMap.containsKey(it.metadata.messageId)) {
localMessagesChanged = true
IterableApi.getInstance().trackEmbeddedMessageReceived(it)
}
}

// Compare the local list to remote list
// if there are messages to remove, trigger a message update in UI
val remoteMessageMap = mutableMapOf<String, IterableEmbeddedMessage>()
remoteMessageList.forEach {
remoteMessageMap[it.metadata.messageId] = it
Expand All @@ -233,19 +230,20 @@ public class IterableEmbeddedManager : IterableActivityMonitor.AppStateCallback
}
}

// update local message map for placement with remote message list
localPlacementMessagesMap[placementId] = remoteMessageList

//if local messages changed, trigger a message update in UI
if (localMessagesChanged) {
updateHandleListeners.forEach {
IterableLogger.d(TAG, "Calling updateHandler")
it.onMessagesUpdated()
}
}
}

// endregion

// region AppStateCallback overrides

override fun onSwitchToForeground() {
IterableLogger.printInfo()
embeddedSessionManager.startSession()
Expand All @@ -256,14 +254,17 @@ public class IterableEmbeddedManager : IterableActivityMonitor.AppStateCallback
override fun onSwitchToBackground() {
embeddedSessionManager.endSession()
}

// endregion
}

// region interfaces

public interface IterableEmbeddedUpdateHandler {
fun onMessagesUpdated()
fun onEmbeddedMessagingDisabled()
fun onEmbeddedMessagingSyncSucceeded() {}
fun onEmbeddedMessagingSyncFailed(reason: String?) {}
}

// endregion

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static android.os.Looper.getMainLooper;
import static junit.framework.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
Expand Down Expand Up @@ -265,4 +266,35 @@ public void testOnEmbeddedMessagingDisabled() throws Exception {
verify(mockHandler1).onEmbeddedMessagingDisabled();
}

@Test
public void testOnEmbeddedMessagingSyncSucceeded() throws Exception {
dispatcher.enqueueResponse("/embedded-messaging/messages", new MockResponse().setBody(IterableTestUtils.getResourceString("embedded_payload_single_1.json")));
IterableEmbeddedManager embeddedManager = IterableApi.getInstance().getEmbeddedManager();

IterableEmbeddedUpdateHandler mockHandler = mock(IterableEmbeddedUpdateHandler.class);
embeddedManager.addUpdateListener(mockHandler);

embeddedManager.syncMessages();
shadowOf(getMainLooper()).idle();

verify(mockHandler).onEmbeddedMessagingSyncSucceeded();
verify(mockHandler, never()).onEmbeddedMessagingSyncFailed(anyString());
assertEquals(1, embeddedManager.getMessages(0L).size());
}

@Test
public void testOnEmbeddedMessagingSyncFailed() throws Exception {
dispatcher.enqueueResponse("/embedded-messaging/messages", new MockResponse().setResponseCode(401).setBody(IterableTestUtils.getResourceString("embedded_payload_bad_api_key.json")));
IterableEmbeddedManager embeddedManager = IterableApi.getInstance().getEmbeddedManager();

IterableEmbeddedUpdateHandler mockHandler = mock(IterableEmbeddedUpdateHandler.class);
embeddedManager.addUpdateListener(mockHandler);

embeddedManager.syncMessages();
shadowOf(getMainLooper()).idle();

verify(mockHandler).onEmbeddedMessagingSyncFailed(anyString());
verify(mockHandler, never()).onEmbeddedMessagingSyncSucceeded();
}

}
Loading