diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6da7d098..cee7fbd3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# Changelog
+## 7.13.0
+
+- Implementation for Overlay Messaging channel. Check optimove developer docs for more.
+
## 7.12.5
- Wrap `pendingResult.finish()` in PushBroadcastReceiver in try/catch to prevent crash.
@@ -10,11 +14,11 @@
## 7.12.3
-- Wraps `isLaunchActivity()` in try/catch and safely returns false on failure to prevent crashes.
+- Wraps `isLaunchActivity()` in try/catch and safely returns false on failure to prevent crashes.
## 7.12.2
-- Introduces a lastShownByInterceptorId field that tracks which message was last shown through the interceptor, preventing duplicate interception of the same head message.
+- Introduces a lastShownByInterceptorId field that tracks which message was last shown through the interceptor, preventing duplicate interception of the same head message.
## 7.12.1
@@ -26,12 +30,10 @@
- Fixed dismissed in-app messages re-appearing on fast consecutive app foregrounds
-
## 7.11.1
- Fix: expiryDate is always absent in EmbeddedMessage due to inconsistent format given by v2 endpoint
-
## 7.11.0
- Updated delayed configuration to no longer require a redundant region parameter. Region is now inferred from credentials. Deprecated `OptimoveConfig.Builder(Region, FeatureSet)` — use `Builder(FeatureSet)` instead.
@@ -40,7 +42,6 @@
- Minor bug fixes for Embedded Messaging: correct field mapping
-
## 7.10.1
- Bumped GSON version number to fix a vulnerability issue
@@ -60,7 +61,8 @@
## 7.8.0
-Add In-App Message Interceptor API `OptimoveInApp.getInstance().setInAppMessageInterceptor`, basic usage:
+Add In-App Message Interceptor API `OptimoveInApp.getInstance().setInAppMessageInterceptor`, basic usage:
+
```java
OptimoveInApp.getInstance().setInAppMessageInterceptor((message, decision) -> {
// Example: decide based on your own logic
diff --git a/OptimoveSDK/app/src/main/AndroidManifest.xml b/OptimoveSDK/app/src/main/AndroidManifest.xml
index 1756682c..dce17306 100644
--- a/OptimoveSDK/app/src/main/AndroidManifest.xml
+++ b/OptimoveSDK/app/src/main/AndroidManifest.xml
@@ -28,6 +28,10 @@
android:name="com.optimove.android.optimovemobilesdk.GamifyWidgetActivity"
android:exported="false"
android:parentActivityName=".MainActivity"/>
+
+
@@ -115,16 +128,18 @@ class MainActivity : AppCompatActivity() {
}
Optimove.getInstance().sendLocationUpdate(location)
outputText = "Location sent: ($lat, $lng)"
- },
+ },
onResetToken = {},
onOpenDeeplinkTest = ::openDeeplinkTest,
onRegisterPush = {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
- ContextCompat.checkSelfPermission(this@MainActivity, Manifest.permission.POST_NOTIFICATIONS)
- == PackageManager.PERMISSION_DENIED &&
- !shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(
+ this@MainActivity, Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_DENIED && !shouldShowRequestPermissionRationale(
+ Manifest.permission.POST_NOTIFICATIONS
+ )
) {
- outputText = "Notification permission permanently denied. Opening settings..."
+ outputText =
+ "Notification permission permanently denied. Opening settings..."
startActivity(Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, this@MainActivity.packageName)
})
@@ -143,6 +158,7 @@ class MainActivity : AppCompatActivity() {
onDelayedInitToggle = { enabled ->
isDelayedInit = enabled
qaPrefs.edit().putBoolean(MyApplication.KEY_DELAYED_INIT, enabled).apply()
+
outputText = if (enabled)
"Delayed init enabled — restart app to apply"
else
@@ -174,6 +190,7 @@ class MainActivity : AppCompatActivity() {
runFromWorker { Optimove.getInstance().reportEvent(SimpleCustomEvent()) }
runFromWorker { Optimove.getInstance().reportEvent("Event_No ParaMs ") }
}
+
private fun clearAppData() {
AlertDialog.Builder(this)
.setTitle("Clear App Data")
@@ -224,30 +241,27 @@ class MainActivity : AppCompatActivity() {
outputText = "Calling setUserId"
Optimove.getInstance().setUserId(userId)
}
+
userId.isEmpty() -> {
outputText = "Calling setUserEmail"
Optimove.getInstance().setUserEmail(userEmail)
}
+
else -> {
outputText = "Calling registerUser"
Optimove.getInstance().registerUser(userId, userEmail)
}
}
- identityPrefs.edit()
- .putString(KEY_USER_ID, userId)
- .putString(KEY_USER_EMAIL, userEmail)
+ identityPrefs.edit().putString(KEY_USER_ID, userId).putString(KEY_USER_EMAIL, userEmail)
.apply()
}
private fun clearIdentity() {
- identityPrefs.edit()
- .remove(KEY_USER_ID)
- .remove(KEY_USER_EMAIL)
- .apply()
+ identityPrefs.edit().remove(KEY_USER_ID).remove(KEY_USER_EMAIL).apply()
persistedUserId = ""
persistedUserEmail = ""
outputText = "Identity cleared (saved values removed)"
- }
+ }
private fun readInbox() {
val items = OptimoveInApp.getInstance().inboxItems
@@ -274,18 +288,21 @@ class MainActivity : AppCompatActivity() {
items.forEach { OptimoveInApp.getInstance().deleteMessageFromInbox(it) }
}
+
private fun getPreferences() {
OptimovePreferenceCenter.getInstance().getPreferencesAsync { result, preferences ->
when (result) {
- OptimovePreferenceCenter.ResultType.ERROR_USER_NOT_SET,
- OptimovePreferenceCenter.ResultType.ERROR,
- OptimovePreferenceCenter.ResultType.ERROR_CREDENTIALS_NOT_SET -> Log.d(PC_TAG, result.toString())
+ OptimovePreferenceCenter.ResultType.ERROR_USER_NOT_SET, OptimovePreferenceCenter.ResultType.ERROR, OptimovePreferenceCenter.ResultType.ERROR_CREDENTIALS_NOT_SET -> Log.d(
+ PC_TAG, result.toString()
+ )
+
OptimovePreferenceCenter.ResultType.SUCCESS -> preferences?.let { prefs ->
Log.d(PC_TAG, "configured: ${prefs.configuredChannels}")
prefs.customerPreferences.forEach { topic ->
Log.d(PC_TAG, "${topic.id} ${topic.name} ${topic.subscribedChannels}")
}
}
+
else -> Log.d(PC_TAG, "unknown res type")
}
}
@@ -294,9 +311,10 @@ class MainActivity : AppCompatActivity() {
private fun setPreferences() {
OptimovePreferenceCenter.getInstance().getPreferencesAsync { result, preferences ->
when (result) {
- OptimovePreferenceCenter.ResultType.ERROR_USER_NOT_SET,
- OptimovePreferenceCenter.ResultType.ERROR,
- OptimovePreferenceCenter.ResultType.ERROR_CREDENTIALS_NOT_SET -> Log.d(PC_TAG, result.toString())
+ OptimovePreferenceCenter.ResultType.ERROR_USER_NOT_SET, OptimovePreferenceCenter.ResultType.ERROR, OptimovePreferenceCenter.ResultType.ERROR_CREDENTIALS_NOT_SET -> Log.d(
+ PC_TAG, result.toString()
+ )
+
OptimovePreferenceCenter.ResultType.SUCCESS -> preferences?.let { prefs ->
Log.d(PC_TAG, "loaded prefs for set: good")
val configuredChannels: List = prefs.configuredChannels
@@ -305,10 +323,10 @@ class MainActivity : AppCompatActivity() {
PreferenceUpdate(topic.id, configuredChannels.subList(0, 1))
}
OptimovePreferenceCenter.getInstance().setCustomerPreferencesAsync(
- { setResult -> Log.d(PC_TAG, setResult.toString()) },
- updates
+ { setResult -> Log.d(PC_TAG, setResult.toString()) }, updates
)
}
+
else -> Log.d(PC_TAG, "unknown res type")
}
}
@@ -318,12 +336,22 @@ class MainActivity : AppCompatActivity() {
startActivity(Intent(this, EmbeddedMessagingActivity::class.java))
}
+
+ private fun viewOverlayMessaging() {
+ startActivity(Intent(this, OverlayMessagingActivity::class.java))
+ }
+
private fun openGamifyWidget() {
startActivity(Intent(this, GamifyWidgetActivity::class.java))
+
}
private fun openDeeplinkTest() {
- startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(DeeplinkTargetActivity.DEEPLINK_TEST_URI)))
+ startActivity(
+ Intent(
+ Intent.ACTION_VIEW, Uri.parse(DeeplinkTargetActivity.DEEPLINK_TEST_URI)
+ )
+ )
}
private fun setCredentials(optimove: String?, optimobile: String?, prefCenter: String?) {
@@ -354,19 +382,22 @@ class MainActivity : AppCompatActivity() {
val options = arrayOf("Default (5000 ms)", "12,000 ms")
var selected = 0
- AlertDialog.Builder(this)
- .setTitle("Enable In-App Interception")
+ AlertDialog.Builder(this).setTitle("Enable In-App Interception")
.setSingleChoiceItems(options, 0) { _, which -> selected = which }
.setPositiveButton("Enable") { d, _ ->
val timeoutMs = if (selected == 0) 5000L else 12000L
enableInAppInterception(timeoutMs)
isInterceptingInApp = true
+
outputText = "In-App interception enabled ($timeoutMs ms timeout per message)"
- Toast.makeText(this, "In-App interception enabled ($timeoutMs ms)", Toast.LENGTH_SHORT).show()
+ Toast.makeText(
+ this,
+ "In-App interception enabled ($timeoutMs ms)",
+ Toast.LENGTH_SHORT
+ ).show()
+
d.dismiss()
- }
- .setNegativeButton("Cancel", null)
- .show()
+ }.setNegativeButton("Cancel", null).show()
}
private fun disableInAppInterception() {
@@ -379,10 +410,13 @@ class MainActivity : AppCompatActivity() {
private fun enableInAppInterception(timeoutMs: Long) {
OptimoveInApp.getInstance().setInAppMessageInterceptor(object : InAppMessageInterceptor {
- override fun processMessage(messageData: JSONObject?, decision: InAppMessageInterceptorCallback) {
+ override fun processMessage(
+ messageData: JSONObject?, decision: InAppMessageInterceptorCallback
+ ) {
runOnUiThread {
inAppDecisionDialog?.takeIf { it.isShowing }?.dismiss()
val dataText = messageData?.toString() ?: "No data provided"
+
inAppDecisionDialog = AlertDialog.Builder(this@MainActivity)
.setTitle("QA: In-App Message")
.setMessage("$dataText\nChoose Show, Postpone, or Suppress.")
@@ -400,6 +434,7 @@ class MainActivity : AppCompatActivity() {
}
.setOnCancelListener { decision.suppress() }
.create()
+
inAppDecisionDialog?.show()
}
}
@@ -411,8 +446,7 @@ class MainActivity : AppCompatActivity() {
private class SimpleCustomEvent : OptimoveEvent() {
override fun getName(): String = "Simple cUSTOM_Event "
override fun getParameters(): Map = mapOf(
- "strinG_param" to " some_string ",
- "number_param" to 42
+ "strinG_param" to " some_string ", "number_param" to 42
)
}
diff --git a/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/MyApplication.kt b/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/MyApplication.kt
index 08cb45f1..426814e0 100644
--- a/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/MyApplication.kt
+++ b/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/MyApplication.kt
@@ -26,6 +26,7 @@ class MyApplication : Application() {
.enableEmbeddedMessaging("embedded_config_string")
.setPushSmallIconId(R.drawable.small_icon)
.setPushAccentColor(Color.parseColor("#FF0000"))
+ .enableOverlayMessaging(1)
.build()
} else {
OptimoveConfig.Builder(
@@ -35,6 +36,7 @@ class MyApplication : Application() {
.enableInAppMessaging(OptimoveConfig.InAppConsentStrategy.AUTO_ENROLL)
.setPushSmallIconId(R.drawable.small_icon)
.setPushAccentColor(Color.parseColor("#FF0000"))
+ .enableOverlayMessaging(1)
.build()
}
diff --git a/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/OverlayMessagingActivity.kt b/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/OverlayMessagingActivity.kt
new file mode 100644
index 00000000..f9c6447d
--- /dev/null
+++ b/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/OverlayMessagingActivity.kt
@@ -0,0 +1,210 @@
+package com.optimove.android.optimovemobilesdk
+
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import com.optimove.android.optimobile.OptimoveOverlayMessaging
+import com.optimove.android.optimovemobilesdk.ui.theme.AppTheme
+
+class OverlayMessagingActivity : AppCompatActivity() {
+
+ private var isInterceptorSet by mutableStateOf(false)
+ private var pendingCallback by mutableStateOf(
+ null
+ )
+ private var timeoutSeconds by mutableStateOf("30")
+ private var lastOutcome by mutableStateOf(null)
+ private val handler = Handler(Looper.getMainLooper())
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ AppTheme {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.Top
+ ) {
+ Button(
+ onClick = { OptimoveOverlayMessaging.getInstance().resetSession() },
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(10.dp)
+ ) {
+ Text("Reset Session")
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Button(
+ onClick = { setInterceptor() },
+ enabled = !isInterceptorSet,
+ modifier = Modifier.weight(1f),
+ shape = RoundedCornerShape(10.dp)
+ ) {
+ Text("Set Interceptor")
+ }
+ Button(
+ onClick = { unsetInterceptor() },
+ enabled = isInterceptorSet,
+ modifier = Modifier.weight(1f),
+ shape = RoundedCornerShape(10.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.secondary
+ )
+ ) {
+ Text("Unset Interceptor")
+ }
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Text(
+ text = "Interceptor timeout",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.weight(1f)
+ )
+ OutlinedTextField(
+ value = timeoutSeconds,
+ onValueChange = { timeoutSeconds = it.filter { c -> c.isDigit() } },
+ suffix = { Text("s") },
+ singleLine = true,
+ enabled = !isInterceptorSet,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ modifier = Modifier.width(88.dp),
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = MaterialTheme.colorScheme.primary,
+ focusedLabelColor = MaterialTheme.colorScheme.primary
+ )
+ )
+ }
+
+ if (isInterceptorSet) {
+ Spacer(modifier = Modifier.height(16.dp))
+ val hasMessage = pendingCallback != null
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Button(
+ onClick = {
+ pendingCallback?.show(); pendingCallback = null; lastOutcome =
+ "show"
+ },
+ enabled = hasMessage,
+ modifier = Modifier.weight(1f),
+ shape = RoundedCornerShape(10.dp)
+ ) {
+ Text("Show")
+ }
+ Button(
+ onClick = {
+ pendingCallback?.discard(); pendingCallback =
+ null; lastOutcome = "discard"
+ },
+ enabled = hasMessage,
+ modifier = Modifier.weight(1f),
+ shape = RoundedCornerShape(10.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Text("Discard")
+ }
+ Button(
+ onClick = {
+ pendingCallback?.defer(); pendingCallback = null; lastOutcome =
+ "defer"
+ },
+ enabled = hasMessage,
+ modifier = Modifier.weight(1f),
+ shape = RoundedCornerShape(10.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.secondary
+ )
+ ) {
+ Text("Defer")
+ }
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = "Outcome: ${lastOutcome ?: "—"}",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+ }
+ }
+
+ private fun setInterceptor() {
+ val timeoutMs = (timeoutSeconds.toLongOrNull() ?: 30L) * 1000L
+ OptimoveOverlayMessaging.getInstance()
+ .setInterceptor(object : OptimoveOverlayMessaging.OverlayMessagingInterceptor {
+ override fun onMessageLoaded(
+ message: com.optimove.android.optimobile.OverlayMessagingMessage,
+ callback: OptimoveOverlayMessaging.OverlayMessagingInterceptorCallback
+ ) {
+ runOnUiThread {
+ pendingCallback = callback
+ handler.postDelayed(
+ { pendingCallback = null; lastOutcome = "timeout" }, timeoutMs
+ )
+ }
+ }
+
+ override fun getTimeoutMs(): Long = timeoutMs
+ })
+ isInterceptorSet = true
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ unsetInterceptor()
+ }
+
+ private fun unsetInterceptor() {
+ OptimoveOverlayMessaging.getInstance().setInterceptor(null)
+ isInterceptorSet = false
+ pendingCallback = null
+ }
+}
diff --git a/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/ui/MainScreen.kt b/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/ui/MainScreen.kt
index 1f3671a0..ce76e925 100644
--- a/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/ui/MainScreen.kt
+++ b/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/ui/MainScreen.kt
@@ -43,6 +43,7 @@ fun MainScreen(
outputText: String,
showPreferenceCenter: Boolean,
showEmbeddedMessaging: Boolean,
+ showOverlayMessaging: Boolean,
showDelayedConfig: Boolean,
credentialsSubmitted: Boolean,
isInterceptingInApp: Boolean,
@@ -61,6 +62,7 @@ fun MainScreen(
onGetPreferences: () -> Unit,
onSetPreferences: () -> Unit,
onViewEmbeddedMessaging: () -> Unit,
+ onViewOverlayMessaging: () -> Unit,
onSetCredentials: (optimove: String?, optimobile: String?, prefCenter: String?) -> Unit,
onInAppInterceptionClicked: () -> Unit,
onResetToken: () -> Unit,
@@ -272,6 +274,17 @@ fun MainScreen(
}
}
+ if (showOverlayMessaging) {
+ Spacer(modifier = Modifier.height(4.dp))
+ Button(
+ onClick = onViewOverlayMessaging,
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(10.dp)
+ ) {
+ Text("Overlay Messaging")
+ }
+ }
+
if (showDelayedConfig) {
Spacer(modifier = Modifier.height(16.dp))
Surface(modifier = Modifier.fillMaxWidth(), shape = CardShape, color = MaterialTheme.colorScheme.surfaceVariant) {
diff --git a/OptimoveSDK/gradle.properties b/OptimoveSDK/gradle.properties
index 9e26ea2c..cbfe81ec 100644
--- a/OptimoveSDK/gradle.properties
+++ b/OptimoveSDK/gradle.properties
@@ -7,8 +7,10 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
-sdk_version=7.12.5
-sdk_version_code=71205
+
+sdk_version=7.13.0
+sdk_version_code=71300
+
sdk_platform=Android
android.useAndroidX=true
android.enableJetifier=true
diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/OptimoveConfig.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/OptimoveConfig.java
index daf9d5e8..af52f559 100644
--- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/OptimoveConfig.java
+++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/OptimoveConfig.java
@@ -73,6 +73,9 @@ public final class OptimoveConfig {
private @Nullable EmbeddedMessagingConfig embeddedMessagingConfig;
+ private boolean overlayMessagingEnabled;
+ private @Nullable Integer overlayMessagingSessionLengthHours;
+
public enum InAppConsentStrategy {
AUTO_ENROLL,
EXPLICIT_BY_USER
@@ -138,6 +141,7 @@ public FeatureSet withEmbeddedMessaging() {
}
+
boolean has(Feature feature) {
return features.contains(feature);
}
@@ -415,6 +419,14 @@ public boolean usesDelayedConfiguration() {
return this.embeddedMessagingConfig;
}
+ public boolean isOverlayMessagingEnabled() {
+ return this.overlayMessagingEnabled;
+ }
+
+ public int getOverlayMessagingSessionLengthHours() {
+ return this.overlayMessagingSessionLengthHours;
+ }
+
private boolean hasFinishedInitialisation() {
boolean hasOptimoveCreds = optimoveToken != null && configFileName != null;
boolean hasOptimobileCreds = apiKey != null && secretKey != null;
@@ -474,6 +486,8 @@ public static class Builder {
private @Nullable LogLevel minLogLevel;
+ private @Nullable Integer overlayMessagingSessionLengthHours;
+
/**
* @deprecated Use {@link Builder#Builder(FeatureSet)} instead
*/
@@ -575,6 +589,17 @@ public Builder enableEmbeddedMessaging(@NonNull String embeddedMessagingConfigur
return this;
}
+ public Builder enableOverlayMessaging(int sessionLengthHours) {
+ if (sessionLengthHours <= 0) {
+ throw new IllegalArgumentException("OverlayMessaging: sessionLengthHours must be greater than 0");
+ }
+ if (!this.featureSet.has(FeatureSet.Feature.OPTIMOBILE)) {
+ throw new IllegalArgumentException("OverlayMessaging: optimobile feature required");
+ }
+ this.overlayMessagingSessionLengthHours = sessionLengthHours;
+ return this;
+ }
+
/**
* The minimum amount of time the user has to have left the app for a session end event to be
* recorded.
@@ -665,6 +690,9 @@ public OptimoveConfig build() {
newConfig.setMinLogLevel(this.minLogLevel);
+ newConfig.overlayMessagingEnabled = this.overlayMessagingSessionLengthHours != null;
+ newConfig.overlayMessagingSessionLengthHours = this.overlayMessagingSessionLengthHours;
+
return newConfig;
}
}
diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/AnalyticsContract.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/AnalyticsContract.java
index 30ed26f1..7ddad2a2 100644
--- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/AnalyticsContract.java
+++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/AnalyticsContract.java
@@ -45,6 +45,8 @@ final class AnalyticsContract {
static final String EVENT_TYPE_MESSAGE_DELIVERED = "k.message.delivered";
static final String EVENT_TYPE_MESSAGE_READ = "k.message.read";
static final String MESSAGE_DELETED_FROM_INBOX = "k.message.inbox.deleted";
+ static final String EVENT_TYPE_OM_INTERCEPTED = "optimove.om.intercepted";
+ static final String EVENT_TYPE_OM_DISMISSED = "optimove.om.dismissed";
static final String EVENT_TYPE_DEEP_LINK_MATCHED = "k.deepLink.matched";
static final String EVENT_TYPE_LOCATION_UPDATED = "k.engage.locationUpdated";
static final String EVENT_TYPE_ENTERED_BEACON_PROXIMITY = "k.engage.beaconEnteredProximity";
diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/BaseMessageView.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/BaseMessageView.java
new file mode 100644
index 00000000..bc5688f0
--- /dev/null
+++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/BaseMessageView.java
@@ -0,0 +1,505 @@
+package com.optimove.android.optimobile;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.net.http.SslError;
+import android.os.Build;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.Pair;
+import android.view.Display;
+import android.view.DisplayCutout;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.webkit.JavascriptInterface;
+import android.webkit.RenderProcessGoneDetail;
+import android.webkit.SslErrorHandler;
+import android.webkit.WebResourceError;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.UiThread;
+
+import com.optimove.android.BuildConfig;
+import com.optimove.android.R;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+abstract class BaseMessageView extends WebViewClient {
+
+ protected enum MessageCloseSource {
+ HARDWARE,
+ CLIENT
+ }
+
+ private enum State {
+ INITIAL,
+ LOADING,
+ READY,
+ DISPOSED
+ }
+ private static final String TAG = BaseMessageView.class.getName();
+
+ private static final String HOST_MESSAGE_TYPE_PRESENT_MESSAGE = "PRESENT_MESSAGE";
+ private static final String HOST_MESSAGE_TYPE_CLOSE_MESSAGE = "CLOSE_MESSAGE";
+ private static final String HOST_MESSAGE_TYPE_SET_NOTCH_INSETS = "SET_NOTCH_INSETS";
+
+ private static final String JS_NAME = "Android";
+
+ private State state;
+ private boolean pageFinished;
+
+ @NonNull
+ protected final Activity currentActivity;
+
+ @Nullable
+ private WebView wv;
+ @Nullable
+ private Dialog dialog;
+ @Nullable
+ private ProgressBar spinner;
+
+ private int prevStatusBarColor;
+ private boolean prevFlagTranslucentStatus;
+ private boolean prevFlagDrawsSystemBarBackgrounds;
+ private final boolean manageStatusBarColor;
+
+
+ @UiThread
+ BaseMessageView(@NonNull Activity currentActivity, boolean manageStatusBarColor) {
+ this.state = State.INITIAL;
+ pageFinished = false;
+
+ this.currentActivity = currentActivity;
+ this.manageStatusBarColor = manageStatusBarColor;
+ }
+
+ @UiThread
+ void dispose() {
+ if (state == State.DISPOSED) return;
+ closeDialog(currentActivity);
+ state = State.DISPOSED;
+ }
+
+ @UiThread
+ void handleViewError(){
+ dispose();
+
+ onViewError();
+ }
+
+ @UiThread
+ protected void sendCurrentMessageToClient() {
+ if (state == State.READY && pageFinished) {
+ JSONObject content = this.getCurrentMessageContent();
+
+ sendToClient(HOST_MESSAGE_TYPE_PRESENT_MESSAGE, content);
+ }
+ }
+
+ @UiThread
+ private void setSpinnerVisibility(int visibility) {
+ if (spinner != null) {
+ spinner.setVisibility(visibility);
+ }
+ }
+
+ @UiThread
+ protected void sendToClient(String type, JSONObject data) {
+ if (wv == null) {
+ return;
+ }
+
+ JSONObject j = new JSONObject();
+ try {
+ j.put("data", data);
+ j.put("type", type);
+ } catch (JSONException e) {
+ Log.d(TAG, "Could not create client message");
+ return;
+ }
+
+ String script = "window.postHostMessage(" + j.toString() + ")";
+
+ wv.evaluateJavascript(script, null);
+ }
+
+ @UiThread
+ private void setStatusBarColorForDialog(Activity currentActivity) {
+ if (currentActivity == null) {
+ return;
+ }
+
+ Window window = currentActivity.getWindow();
+
+ prevStatusBarColor = window.getStatusBarColor();
+
+ int flags = window.getAttributes().flags;
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ prevFlagTranslucentStatus = (flags & WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) != 0;
+ window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ }
+
+ prevFlagDrawsSystemBarBackgrounds = (flags & WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0;
+ window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+ int statusBarColor;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ statusBarColor = currentActivity.getResources().getColor(R.color.statusBarColorForNotch, null);
+ } else {
+ statusBarColor = currentActivity.getResources().getColor(R.color.statusBarColorForNotch);
+ }
+
+ window.setStatusBarColor(statusBarColor);
+ }
+
+ @UiThread
+ private void unsetStatusBarColorForDialog(Activity dialogActivity) {
+ if (dialogActivity == null) {
+ return;
+ }
+
+ Window window = dialogActivity.getWindow();
+ window.setStatusBarColor(prevStatusBarColor);
+
+ if (prevFlagTranslucentStatus) {
+ window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ }
+
+ if (!prevFlagDrawsSystemBarBackgrounds) {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+ }
+ }
+
+ @UiThread
+ private void closeDialog(Activity dialogActivity) {
+ if (dialog != null) {
+ dialog.setOnKeyListener(null);
+ dialog.dismiss();
+
+ if (manageStatusBarColor) {
+ unsetStatusBarColorForDialog(dialogActivity);
+ }
+ }
+
+ if (null != wv) {
+ wv.destroy();
+ }
+
+ dialog = null;
+ wv = null;
+ spinner = null;
+ }
+
+ @SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface", "InflateParams"})
+ @UiThread
+ protected void showWebView(@NonNull final Activity currentActivity, @NonNull String iarUrl) {
+ try {
+ if (BuildConfig.DEBUG) {
+ WebView.setWebContentsDebuggingEnabled(true);
+ }
+
+ RelativeLayout.LayoutParams paramsWebView = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
+ dialog = new Dialog(currentActivity, android.R.style.Theme_Translucent_NoTitleBar_Fullscreen);
+
+ Window window = dialog.getWindow();
+ if (window != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ WindowManager.LayoutParams windowAttributes = dialog.getWindow().getAttributes();
+ windowAttributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+
+ View view = window.getDecorView();
+ view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+ }
+
+ LayoutInflater inflater = (LayoutInflater) currentActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ dialog.setContentView(inflater.inflate(R.layout.optimobile_dialog_view, null), paramsWebView);
+ dialog.setOnKeyListener((dialog, keyCode, event) -> {
+ if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() != KeyEvent.ACTION_DOWN) {
+ closeCurrentMessage(MessageCloseSource.HARDWARE);
+ }
+ return true;
+ });
+
+ wv = dialog.findViewById(R.id.optimobile_webview);
+ spinner = dialog.findViewById(R.id.optimobile_progressBar);
+
+ if (null == wv || null == spinner) {
+ dispose();
+ return;
+ }
+
+ int cacheMode = WebSettings.LOAD_DEFAULT;
+ if (BuildConfig.DEBUG) {
+ cacheMode = WebSettings.LOAD_NO_CACHE;
+ }
+ wv.getSettings().setCacheMode(cacheMode);
+
+ wv.setBackgroundColor(android.graphics.Color.TRANSPARENT);
+
+ WebSettings settings = wv.getSettings();
+ settings.setJavaScriptEnabled(true);
+ settings.setAllowFileAccess(false);
+ settings.setAllowContentAccess(false);
+
+ settings.setMediaPlaybackRequiresUserGesture(false);
+
+
+ wv.addJavascriptInterface(this, JS_NAME);
+ wv.setWebViewClient(this);
+
+ dialog.show();
+ setSpinnerVisibility(View.VISIBLE);
+ wv.loadUrl(iarUrl);
+ state = State.LOADING;
+ } catch (Exception e) {
+ Optimobile.log(TAG, e.getMessage());
+ }
+ }
+
+ protected void closeCurrentMessage(MessageCloseSource source) {
+ sendToClient(HOST_MESSAGE_TYPE_CLOSE_MESSAGE, null);
+
+ onMessageCloseRequested(source);
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ view.setBackgroundColor(android.graphics.Color.TRANSPARENT);
+ if (manageStatusBarColor) {
+ setStatusBarColorForDialog(currentActivity);
+ }
+ pageFinished = true;
+
+ sendCurrentMessageToClient();
+
+ super.onPageFinished(view, url);
+ }
+
+ @Override
+ public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
+ super.onReceivedHttpError(view, request, errorResponse);
+
+ String url = request.getUrl().toString();
+
+ try {
+ // Only consider handling for failures of our renderer assets
+ // 3rd-party fonts/images etc. shouldn't trigger this
+ String iarBaseUrl = Optimobile.urlForService(UrlBuilder.Service.IAR, "");
+ if (!url.startsWith(iarBaseUrl)) {
+ return;
+ }
+
+ // Cached index page may refer to stale JS/CSS file hashes
+ // Evict the cache to allow next presentation to re-fetch
+ if (404 == errorResponse.getStatusCode()) {
+ view.clearCache(true);
+ }
+
+ handleViewError();
+ } catch (Optimobile.PartialInitialisationException e) {
+ Optimobile.log(TAG, "Cannot handle HTTP error: credentials not yet available");
+ }
+ }
+
+ @Override
+ public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
+ handler.cancel();
+ handleViewError();
+ }
+
+ @Override
+ public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) {
+ handleViewError();
+
+ // Allow app to keep running, don't terminate
+ return true;
+ }
+
+ @Override
+ public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+ Optimobile.log(TAG, "Error code: " + errorCode + ". " + description + " " + failingUrl);
+
+ String extension = failingUrl.substring(failingUrl.length() - 4);
+ boolean isVideo = extension.matches(".mp4|.m4a|.m4p|.m4b|.m4r|.m4v");
+ if (errorCode == -1 && "net::ERR_FAILED".equals(description) && isVideo) {
+ // This is a workaround for a bug in the WebView.
+ // See these chromium issues for more context:
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=1023678
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=1050635
+
+ //We encountered the issue only with some (and not other) videos, but possibly not limited to other file types
+ return;
+ }
+
+ handleViewError();
+ }
+
+ @Override
+ @RequiresApi(Build.VERSION_CODES.M)
+ public void onReceivedError(WebView view, WebResourceRequest req, WebResourceError rerr) {
+ onReceivedError(view, rerr.getErrorCode(), rerr.getDescription().toString(), req.getUrl().toString());
+ }
+
+ @JavascriptInterface
+ @AnyThread
+ public void postClientMessage(String msg) {
+ String messageType;
+ JSONObject data;
+ try {
+ JSONObject message = new JSONObject(msg);
+ messageType = message.getString("type");
+ data = message.optJSONObject("data");
+ } catch (JSONException e) {
+ Log.d(TAG, "Incorrect message format: " + msg);
+ return;
+ }
+
+ switch (messageType) {
+ case "READY":
+ currentActivity.runOnUiThread(() -> {
+ if (state == State.DISPOSED) {
+ return;
+ }
+
+ state = State.READY;
+
+ maybeSetNotchInsets(currentActivity);
+ sendCurrentMessageToClient();
+ });
+ return;
+ case "MESSAGE_OPENED":
+ currentActivity.runOnUiThread(() -> {
+ if (state == State.DISPOSED) {
+ return;
+ }
+
+ setSpinnerVisibility(View.GONE);
+ onMessageOpened();
+ });
+ return;
+ case "MESSAGE_CLOSED":
+ currentActivity.runOnUiThread(() -> onMessageClosedByClient());
+ return;
+ case "EXECUTE_ACTIONS":
+ onExecuteActions(data);
+ return;
+ case "COMMAND":
+ onCommand(data);
+ return;
+ case "PRESENTATION_ERROR":
+ currentActivity.runOnUiThread(this::handleViewError);
+ return;
+ default:
+ Log.d(TAG, "Unknown message type: " + messageType);
+ }
+ }
+
+
+ @UiThread
+ private void maybeSetNotchInsets(Context context) {
+ if (dialog == null) {
+ return;
+ }
+
+ Window window = dialog.getWindow();
+ if (window == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ return;
+ }
+
+ WindowInsets insets = window.getDecorView().getRootWindowInsets();
+ if (insets == null) {
+ return;
+ }
+
+ DisplayCutout displayCutout = insets.getDisplayCutout();
+ if (displayCutout == null) {
+ return;
+ }
+
+ List cutoutBoundingRectangles = displayCutout.getBoundingRects();
+ if (cutoutBoundingRectangles.isEmpty()) {
+ return;
+ }
+
+ Pair notchPositions = determineNotchPositions(window, cutoutBoundingRectangles);
+ float density = context.getResources().getDisplayMetrics().density;
+
+ JSONObject notchData = new JSONObject();
+ try {
+ notchData.put("hasNotchOnTheLeft", notchPositions.first);
+ notchData.put("hasNotchOnTheRight", notchPositions.second);
+ notchData.put("insetTop", displayCutout.getSafeInsetTop() / density);
+ notchData.put("insetRight", displayCutout.getSafeInsetRight() / density);
+ notchData.put("insetBottom", displayCutout.getSafeInsetBottom() / density);
+ notchData.put("insetLeft", displayCutout.getSafeInsetLeft() / density);
+
+ sendToClient(HOST_MESSAGE_TYPE_SET_NOTCH_INSETS, notchData);
+ } catch (JSONException e) {
+ Optimobile.log(TAG, e.getMessage());
+ }
+ }
+
+ @UiThread
+ private Pair determineNotchPositions(Window window, List cutoutBoundingRectangles) {
+ Display display = window.getWindowManager().getDefaultDisplay();
+ DisplayMetrics outMetrics = new DisplayMetrics();
+ display.getMetrics(outMetrics);
+
+ boolean hasNotchOnTheRight = false;
+ boolean hasNotchOnTheLeft = false;
+ for (Rect rect : cutoutBoundingRectangles) {
+ if (rect.top == 0) {
+ if (rect.left > outMetrics.widthPixels - rect.right) {
+ hasNotchOnTheRight = true;
+ } else if (rect.left < outMetrics.widthPixels - rect.right) {
+ hasNotchOnTheLeft = true;
+ }
+ } else if (rect.right >= outMetrics.widthPixels) {
+ hasNotchOnTheRight = true;
+ } else if (rect.left == 0) {
+ hasNotchOnTheLeft = true;
+ }
+ }
+
+ return new Pair<>(hasNotchOnTheLeft, hasNotchOnTheRight);
+ }
+
+ // - Abstracts
+ abstract protected JSONObject getCurrentMessageContent();
+
+ abstract protected void onMessageClosedByClient();
+
+ abstract protected void onMessageCloseRequested(MessageCloseSource source);
+
+ abstract protected void onMessageOpened();
+
+ abstract protected void onExecuteActions(JSONObject data);
+
+ abstract protected void onCommand(JSONObject data);
+
+ @UiThread
+ abstract protected void onViewError();
+}
diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/InAppMessagePresenter.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/InAppMessagePresenter.java
index 3c82683a..fafc99d4 100644
--- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/InAppMessagePresenter.java
+++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/InAppMessagePresenter.java
@@ -145,6 +145,11 @@ void messageClosed() {
presentMessageToClient();
}
+ @UiThread
+ void onViewError() {
+ disposeView();
+ }
+
@UiThread
private void disposeView() {
if (view == null) {
diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/InAppMessageView.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/InAppMessageView.java
index ef7e3b84..bf928b1a 100644
--- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/InAppMessageView.java
+++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/InAppMessageView.java
@@ -1,47 +1,14 @@
package com.optimove.android.optimobile;
-import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
import android.app.Activity;
-import android.app.Dialog;
-import android.content.Context;
import android.content.Intent;
-import android.graphics.Rect;
import android.net.Uri;
-import android.net.http.SslError;
-import android.os.Build;
-import android.util.DisplayMetrics;
import android.util.Log;
-import android.util.Pair;
-import android.view.Display;
-import android.view.DisplayCutout;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.Window;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-import android.webkit.JavascriptInterface;
-import android.webkit.RenderProcessGoneDetail;
-import android.webkit.SslErrorHandler;
-import android.webkit.WebResourceError;
-import android.webkit.WebResourceRequest;
-import android.webkit.WebResourceResponse;
-import android.webkit.WebSettings;
-import android.webkit.WebView;
-import android.webkit.WebViewClient;
-import android.widget.ProgressBar;
-import android.widget.RelativeLayout;
-
-import androidx.annotation.AnyThread;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
import androidx.annotation.UiThread;
-import com.optimove.android.BuildConfig;
-import com.optimove.android.R;
-
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -49,14 +16,7 @@
import java.util.ArrayList;
import java.util.List;
-class InAppMessageView extends WebViewClient {
-
- private enum State {
- INITIAL,
- LOADING,
- READY,
- DISPOSED
- }
+class InAppMessageView extends BaseMessageView {
private static final String TAG = InAppMessageView.class.getName();
@@ -67,29 +27,6 @@ private enum State {
private static final String BUTTON_ACTION_REQUEST_APP_STORE_RATING = "requestAppStoreRating";
private static final String BUTTON_ACTION_PUSH_REGISTER = "promptPushPermission";
- private static final String HOST_MESSAGE_TYPE_PRESENT_MESSAGE = "PRESENT_MESSAGE";
- private static final String HOST_MESSAGE_TYPE_CLOSE_MESSAGE = "CLOSE_MESSAGE";
- private static final String HOST_MESSAGE_TYPE_SET_NOTCH_INSETS = "SET_NOTCH_INSETS";
-
- private static final String JS_NAME = "Android";
-
- private State state;
- private boolean pageFinished;
-
- @NonNull
- private final Activity currentActivity;
-
- @Nullable
- private WebView wv;
- @Nullable
- private Dialog dialog;
- @Nullable
- private ProgressBar spinner;
-
- private int prevStatusBarColor;
- private boolean prevFlagTranslucentStatus;
- private boolean prevFlagDrawsSystemBarBackgrounds;
-
@NonNull
private final InAppMessagePresenter presenter;
@NonNull
@@ -98,11 +35,14 @@ private enum State {
private final String region;
@UiThread
- InAppMessageView(@NonNull InAppMessagePresenter presenter, @NonNull InAppMessage message, @NonNull Activity currentActivity, @NonNull String iarUrl, @Nullable String region) {
- this.state = State.INITIAL;
- pageFinished = false;
+ InAppMessageView(@NonNull InAppMessagePresenter presenter,
+ @NonNull InAppMessage message,
+ @NonNull Activity currentActivity,
+ @NonNull String iarUrl,
+ @Nullable String region) {
+ super(currentActivity, true);
+
this.presenter = presenter;
- this.currentActivity = currentActivity;
this.currentMessage = message;
this.region = region;
@@ -118,403 +58,13 @@ void showMessage(@NonNull InAppMessage message) {
sendCurrentMessageToClient();
}
- @UiThread
- void dispose() {
- closeDialog(currentActivity);
- state = State.DISPOSED;
- }
-
- @UiThread
- private void sendCurrentMessageToClient() {
- if (state == State.READY && pageFinished) {
- JSONObject content = currentMessage.getContent();
- if (region != null) {
- try {
- content.put("region", region);
- } catch (JSONException e) {
- Log.w(TAG, "Could not pass region to In-App renderer");
- }
- }
-
- sendToClient(HOST_MESSAGE_TYPE_PRESENT_MESSAGE, content);
- }
- }
-
- @UiThread
- private void setSpinnerVisibility(int visibility) {
- if (spinner != null) {
- spinner.setVisibility(visibility);
- }
- }
-
- @UiThread
- private void sendToClient(String type, JSONObject data) {
- if (wv == null) {
- return;
- }
-
- JSONObject j = new JSONObject();
- try {
- j.put("data", data);
- j.put("type", type);
- } catch (JSONException e) {
- Log.d(TAG, "Could not create client message");
- return;
- }
-
- String script = "window.postHostMessage(" + j.toString() + ")";
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- wv.evaluateJavascript(script, null);
- } else {
- wv.loadUrl("javascript:" + script);
- }
- }
-
- @UiThread
- @SuppressWarnings("deprecation")
- private void setStatusBarColorForDialog(Activity currentActivity) {
- if (currentActivity == null) {
- return;
- }
-
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
- return;
- }
-
- Window window = currentActivity.getWindow();
-
- prevStatusBarColor = window.getStatusBarColor();
-
- int flags = window.getAttributes().flags;
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
- prevFlagTranslucentStatus = (flags & WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) != 0;
- window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
- }
-
- prevFlagDrawsSystemBarBackgrounds = (flags & WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0;
- window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
- int statusBarColor;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- statusBarColor = currentActivity.getResources().getColor(R.color.statusBarColorForNotch, null);
- } else {
- statusBarColor = currentActivity.getResources().getColor(R.color.statusBarColorForNotch);
- }
-
- window.setStatusBarColor(statusBarColor);
- }
-
- @UiThread
- @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
- private void unsetStatusBarColorForDialog(Activity dialogActivity) {
- if (dialogActivity == null) {
- return;
- }
-
- Window window = dialogActivity.getWindow();
- window.setStatusBarColor(prevStatusBarColor);
-
- if (prevFlagTranslucentStatus) {
- window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
- }
-
- if (!prevFlagDrawsSystemBarBackgrounds) {
- window.clearFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
- }
- }
-
- @UiThread
- private void closeDialog(Activity dialogActivity) {
- if (dialog != null) {
- dialog.setOnKeyListener(null);
- dialog.dismiss();
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- unsetStatusBarColorForDialog(dialogActivity);
- }
- }
-
- if (null != wv) {
- wv.destroy();
- }
-
- dialog = null;
- wv = null;
- spinner = null;
- }
-
- @SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface", "InflateParams"})
- @UiThread
- private void showWebView(@NonNull final Activity currentActivity, @NonNull String iarUrl) {
- try {
- if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- WebView.setWebContentsDebuggingEnabled(true);
- }
-
- RelativeLayout.LayoutParams paramsWebView = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
- dialog = new Dialog(currentActivity, android.R.style.Theme_Translucent_NoTitleBar_Fullscreen);
-
- Window window = dialog.getWindow();
- if (window != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- WindowManager.LayoutParams windowAttributes = dialog.getWindow().getAttributes();
- windowAttributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
-
- View view = window.getDecorView();
- view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
- }
-
- LayoutInflater inflater = (LayoutInflater) currentActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- dialog.setContentView(inflater.inflate(R.layout.optimobile_dialog_view, null), paramsWebView);
- dialog.setOnKeyListener((dialog, keyCode, event) -> {
- if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() != KeyEvent.ACTION_DOWN) {
- closeCurrentMessage();
- }
- return true;
- });
-
- wv = dialog.findViewById(R.id.optimobile_webview);
- spinner = dialog.findViewById(R.id.optimobile_progressBar);
-
- if (null == wv || null == spinner) {
- dispose();
- return;
- }
-
- int cacheMode = WebSettings.LOAD_DEFAULT;
- if (BuildConfig.DEBUG) {
- cacheMode = WebSettings.LOAD_NO_CACHE;
- }
- wv.getSettings().setCacheMode(cacheMode);
-
- wv.setBackgroundColor(android.graphics.Color.TRANSPARENT);
-
- WebSettings settings = wv.getSettings();
- settings.setJavaScriptEnabled(true);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- settings.setMediaPlaybackRequiresUserGesture(false);
- }
-
- wv.addJavascriptInterface(this, JS_NAME);
- wv.setWebViewClient(this);
-
- dialog.show();
- setSpinnerVisibility(View.VISIBLE);
- wv.loadUrl(iarUrl);
- state = State.LOADING;
- } catch (Exception e) {
- Optimobile.log(TAG, e.getMessage());
- }
- }
-
- private void closeCurrentMessage() {
- sendToClient(HOST_MESSAGE_TYPE_CLOSE_MESSAGE, null);
- InAppMessageService.handleMessageClosed(currentActivity, currentMessage);
- }
-
- @Override
- public void onPageFinished(WebView view, String url) {
- view.setBackgroundColor(android.graphics.Color.TRANSPARENT);
- setStatusBarColorForDialog(currentActivity);
- pageFinished = true;
-
- sendCurrentMessageToClient();
-
- super.onPageFinished(view, url);
- }
-
- @Override
- @TargetApi(Build.VERSION_CODES.LOLLIPOP)
- public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
- super.onReceivedHttpError(view, request, errorResponse);
-
- String url = request.getUrl().toString();
-
- try {
- // Only consider handling for failures of our renderer assets
- // 3rd-party fonts/images etc. shouldn't trigger this
- String iarBaseUrl = Optimobile.urlForService(UrlBuilder.Service.IAR, "");
- if (!url.startsWith(iarBaseUrl)) {
- return;
- }
-
- // Cached index page may refer to stale JS/CSS file hashes
- // Evict the cache to allow next presentation to re-fetch
- if (404 == errorResponse.getStatusCode()) {
- view.clearCache(true);
- }
-
- closeDialog(currentActivity);
- } catch (Optimobile.PartialInitialisationException e) {
- Optimobile.log(TAG, "Cannot handle HTTP error: credentials not yet available");
- }
- }
-
- @Override
- public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
- handler.cancel();
- closeDialog(currentActivity);
- }
-
- @Override
- public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) {
- closeDialog(currentActivity);
-
- // Allow app to keep running, don't terminate
- return true;
- }
-
- @Override
- public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
- Optimobile.log(TAG, "Error code: " + errorCode + ". " + description + " " + failingUrl);
-
- String extension = failingUrl.substring(failingUrl.length() - 4);
- boolean isVideo = extension.matches(".mp4|.m4a|.m4p|.m4b|.m4r|.m4v");
- if (errorCode == -1 && "net::ERR_FAILED".equals(description) && isVideo) {
- // This is a workaround for a bug in the WebView.
- // See these chromium issues for more context:
- // https://bugs.chromium.org/p/chromium/issues/detail?id=1023678
- // https://bugs.chromium.org/p/chromium/issues/detail?id=1050635
-
- //We encountered the issue only with some (and not other) videos, but possibly not limited to other file types
- return;
- }
-
- closeDialog(currentActivity);
- }
-
- @Override
- @TargetApi(Build.VERSION_CODES.M)
- public void onReceivedError(WebView view, WebResourceRequest req, WebResourceError rerr) {
- onReceivedError(view, rerr.getErrorCode(), rerr.getDescription().toString(), req.getUrl().toString());
- }
-
- @JavascriptInterface
- @AnyThread
- public void postClientMessage(String msg) {
- String messageType;
- JSONObject data;
- try {
- JSONObject message = new JSONObject(msg);
- messageType = message.getString("type");
- data = message.optJSONObject("data");
- } catch (JSONException e) {
- Log.d(TAG, "Incorrect message format: " + msg);
- return;
- }
-
- switch (messageType) {
- case "READY":
- currentActivity.runOnUiThread(() -> {
- if (state == State.DISPOSED) {
- return;
- }
-
- state = State.READY;
-
- maybeSetNotchInsets(currentActivity);
- sendCurrentMessageToClient();
- });
- return;
- case "MESSAGE_OPENED":
- currentActivity.runOnUiThread(() -> {
- if (state == State.DISPOSED) {
- return;
- }
-
- setSpinnerVisibility(View.GONE);
- InAppMessageService.handleMessageOpened(currentActivity, currentMessage);
- });
- return;
- case "MESSAGE_CLOSED":
- currentActivity.runOnUiThread(presenter::messageClosed);
- return;
- case "EXECUTE_ACTIONS":
- if (null == data) {
- return;
- }
- List actions = this.parseButtonActionData(data);
- currentActivity.runOnUiThread(() -> this.executeActions(currentActivity, actions));
- return;
- default:
- Log.d(TAG, "Unknown message type: " + messageType);
- }
- }
-
- @UiThread
- private void maybeSetNotchInsets(Context context) {
- if (dialog == null) {
- return;
- }
-
- Window window = dialog.getWindow();
- if (window == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
- return;
- }
-
- WindowInsets insets = window.getDecorView().getRootWindowInsets();
- if (insets == null) {
- return;
- }
-
- DisplayCutout displayCutout = insets.getDisplayCutout();
- if (displayCutout == null) {
- return;
- }
-
- List cutoutBoundingRectangles = displayCutout.getBoundingRects();
- if (cutoutBoundingRectangles.size() == 0) {
- return;
- }
-
- Pair notchPositions = determineNotchPositions(window, cutoutBoundingRectangles);
- float density = context.getResources().getDisplayMetrics().density;
-
- JSONObject notchData = new JSONObject();
- try {
- notchData.put("hasNotchOnTheLeft", notchPositions.first);
- notchData.put("hasNotchOnTheRight", notchPositions.second);
- notchData.put("insetTop", displayCutout.getSafeInsetTop() / density);
- notchData.put("insetRight", displayCutout.getSafeInsetRight() / density);
- notchData.put("insetBottom", displayCutout.getSafeInsetBottom() / density);
- notchData.put("insetLeft", displayCutout.getSafeInsetLeft() / density);
-
- sendToClient(HOST_MESSAGE_TYPE_SET_NOTCH_INSETS, notchData);
- } catch (JSONException e) {
- Optimobile.log(TAG, e.getMessage());
- }
- }
-
- @UiThread
- private Pair determineNotchPositions(Window window, List cutoutBoundingRectangles) {
- Display display = window.getWindowManager().getDefaultDisplay();
- DisplayMetrics outMetrics = new DisplayMetrics();
- display.getMetrics(outMetrics);
-
- boolean hasNotchOnTheRight = false;
- boolean hasNotchOnTheLeft = false;
- for (Rect rect : cutoutBoundingRectangles) {
- if (rect.top == 0) {
- if (rect.left > outMetrics.widthPixels - rect.right) {
- hasNotchOnTheRight = true;
- } else if (rect.left < outMetrics.widthPixels - rect.right) {
- hasNotchOnTheLeft = true;
- }
- } else if (rect.right >= outMetrics.widthPixels) {
- hasNotchOnTheRight = true;
- } else if (rect.left == 0) {
- hasNotchOnTheLeft = true;
- }
- }
-
- return new Pair<>(hasNotchOnTheLeft, hasNotchOnTheRight);
- }
-
@UiThread
private void executeActions(Activity currentActivity, List actions) {
// Handle 'secondary' actions
for (ExecutableAction action : actions) {
switch (action.getType()) {
case BUTTON_ACTION_CLOSE_MESSAGE:
- closeCurrentMessage();
+ closeCurrentMessage(MessageCloseSource.CLIENT);
break;
case BUTTON_ACTION_TRACK_CONVERSION_EVENT:
Optimobile.trackEventImmediately(currentActivity, action.getEventType(), action.getConversionEventData());
@@ -684,4 +234,60 @@ JSONObject getDeepLink() {
}
}
+
+ // - Implementations for abstracts
+
+ @Override
+ protected JSONObject getCurrentMessageContent() {
+ JSONObject content = currentMessage.getContent();
+ if (region != null) {
+ try {
+ content.put("region", region);
+ } catch (JSONException e) {
+ Log.w(TAG, "Could not pass region to In-App renderer");
+ }
+ }
+
+ return content;
+ }
+
+ @Override
+ protected void onMessageClosedByClient() {
+ // this happens when IAR client closed message
+ presenter.messageClosed();
+ }
+
+ @Override
+ protected void onViewError() {
+ presenter.onViewError();
+ }
+
+ @Override
+ protected void onCommand(JSONObject data) {
+ // noop: in-app uses EXECUTE_ACTIONS, not COMMAND
+ }
+
+ @Override
+ protected void onMessageCloseRequested(MessageCloseSource source) {
+ // this happens when we told IAR to close message
+ // TODO: the split keeps existing behaviour, but simpler would be to run this when message closed by client as well (or was there a reason?)
+ InAppMessageService.handleMessageClosed(currentActivity, currentMessage);
+ }
+
+ @Override
+ protected void onMessageOpened() {
+ InAppMessageService.handleMessageOpened(currentActivity, currentMessage);
+ }
+
+ @Override
+ protected void onExecuteActions(JSONObject data) {
+ if (null == data) {
+ return;
+ }
+
+ List actions = this.parseButtonActionData(data);
+ currentActivity.runOnUiThread(() -> this.executeActions(currentActivity, actions));
+ }
+
+
}
diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/Optimobile.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/Optimobile.java
index 74a937a5..6de753bb 100644
--- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/Optimobile.java
+++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/Optimobile.java
@@ -117,6 +117,10 @@ public static synchronized void initialize(final Application application, Optimo
OptimoveInApp.initialize(application, config);
+ if (config.isOverlayMessagingEnabled()) {
+ OptimoveOverlayMessaging.initialize(application, config);
+ }
+
if (config.getDeferredDeepLinkHandler() != null) {
deepLinkHelper = new DeferredDeepLinkHelper();
}
@@ -598,6 +602,9 @@ public static synchronized void completeDelayedConfiguration(Context context, Op
urlBuilder = new UrlBuilder(config.getBaseUrlMap());
persistMediaBaseUrl(context, config.getBaseUrlMap());
OptimoveInApp.getInstance().onCredentialsAvailable();
+ if (config.isOverlayMessagingEnabled()) {
+ OptimoveOverlayMessaging.getInstance().onCredentialsAvailable();
+ }
}
flushEvents(context);
diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OptimoveOverlayMessaging.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OptimoveOverlayMessaging.java
new file mode 100644
index 00000000..d4b980e5
--- /dev/null
+++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OptimoveOverlayMessaging.java
@@ -0,0 +1,94 @@
+package com.optimove.android.optimobile;
+
+import android.app.Application;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import com.optimove.android.OptimoveConfig;
+
+public class OptimoveOverlayMessaging {
+
+ private static OptimoveOverlayMessaging shared;
+
+ @Nullable
+ private OverlayMessagingSessionManager sessionManager;
+ private final OverlayMessagingManager manager;
+ private final Application application;
+ private final long sessionLengthHours;
+
+ public interface OverlayMessagingInterceptorCallback {
+ @UiThread
+ void show();
+
+ @UiThread
+ void discard();
+
+ @UiThread
+ void defer();
+ }
+
+ public interface OverlayMessagingInterceptor {
+ @UiThread
+ void onMessageLoaded(@NonNull OverlayMessagingMessage message, @NonNull OverlayMessagingInterceptorCallback callback);
+
+ default long getTimeoutMs() {
+ return 5000L;
+ }
+ }
+
+ private OptimoveOverlayMessaging(@NonNull Application application, long sessionLengthHours) {
+ this.application = application;
+ this.sessionLengthHours = sessionLengthHours;
+ this.manager = new OverlayMessagingManager(application);
+ }
+
+ private void startSessionManager() {
+ OverlayMessagingSessionManager.Listener sessionListener = () ->
+ manager.onTriggerReceived(OverlayMessagingMessage.MessageType.SESSION);
+ this.sessionManager = new OverlayMessagingSessionManager(application, sessionLengthHours, sessionListener);
+ }
+
+ //==============================================================================================
+ //-- Public API
+
+ public static OptimoveOverlayMessaging getInstance() {
+ if (shared == null) {
+ throw new IllegalStateException("OptimoveOverlayMessaging is not initialized");
+ }
+ return shared;
+ }
+
+ public void setInterceptor(@Nullable OverlayMessagingInterceptor interceptor) {
+ manager.setInterceptor(interceptor);
+ }
+
+ @UiThread
+ public void resetSession() {
+ if (sessionManager != null) {
+ sessionManager.resetSession();
+ }
+ }
+
+ //==============================================================================================
+ //-- Internal
+
+ @UiThread
+ void onPushTriggerReceived() {
+ manager.onTriggerReceived(OverlayMessagingMessage.MessageType.IMMEDIATE);
+ }
+
+ static void initialize(@NonNull Application application, @NonNull OptimoveConfig config) {
+ shared = new OptimoveOverlayMessaging(application, config.getOverlayMessagingSessionLengthHours());
+ if (!config.usesDelayedOptimobileConfiguration()) {
+ shared.startSessionManager();
+ }
+ }
+
+ @AnyThread
+ void onCredentialsAvailable() {
+ Optimobile.handler.post(this::startSessionManager);
+ }
+}
diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingManager.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingManager.java
new file mode 100644
index 00000000..b7164433
--- /dev/null
+++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingManager.java
@@ -0,0 +1,310 @@
+package com.optimove.android.optimobile;
+
+import android.app.Activity;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.List;
+
+import java.util.ArrayDeque;
+import java.util.HashSet;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+class OverlayMessagingManager implements AppStateWatcher.AppStateChangedListener {
+
+ private static final int SESSION_SLOT_CAPACITY = 1;
+ private static final int IMMEDIATE_SLOT_CAPACITY = 1;
+
+ enum InterceptorOutcome {
+ SHOW, DISCARD, DEFER, TIMEOUT;
+
+ String toEventValue() {
+ switch (this) {
+ case SHOW:
+ return "show";
+ case DISCARD:
+ return "discard";
+ case DEFER:
+ return "defer";
+ case TIMEOUT:
+ return "timeout";
+ default:
+ throw new IllegalStateException("Unhandled outcome: " + this);
+ }
+ }
+ }
+
+ private final Context context;
+ private final Queue displayQueue = new ArrayDeque<>();
+ private final ScheduledExecutorService interceptorExecutor = Executors.newSingleThreadScheduledExecutor();
+
+ @Nullable
+ private OptimoveOverlayMessaging.OverlayMessagingInterceptor interceptor;
+ @Nullable
+ private OverlayMessagingView currentView;
+ @Nullable
+ private Activity currentActivity;
+
+ private int sessionSlotCount = 0;
+ private int immediateSlotCount = 0;
+ // Prevents re-processing the same message when triggers arrive before backend events propagate
+ private final Set seenMessageIds = new HashSet<>();
+
+ OverlayMessagingManager(Context context) {
+ this.context = context.getApplicationContext();
+ OptimobileInitProvider.getAppStateWatcher().registerListener(this);
+ }
+
+
+ @UiThread
+ void setInterceptor(@Nullable OptimoveOverlayMessaging.OverlayMessagingInterceptor interceptor) {
+ this.interceptor = interceptor;
+ }
+
+ @UiThread
+ void onTriggerReceived(OverlayMessagingMessage.MessageType type) {
+ switch (type) {
+ case SESSION:
+ if (sessionSlotCount >= SESSION_SLOT_CAPACITY) {
+ return;
+ }
+ sessionSlotCount++;
+ loadMessage(type);
+ break;
+ case IMMEDIATE:
+ if (immediateSlotCount >= IMMEDIATE_SLOT_CAPACITY) {
+ return;
+ }
+ immediateSlotCount++;
+ loadMessage(type);
+ break;
+ }
+ }
+
+ @UiThread
+ void onSlotCleared(OverlayMessagingMessage.MessageType type) {
+ switch (type) {
+ case SESSION:
+ sessionSlotCount = Math.max(0, sessionSlotCount - 1);
+ break;
+ case IMMEDIATE:
+ immediateSlotCount = Math.max(0, immediateSlotCount - 1);
+ break;
+ }
+ }
+
+ private void loadMessage(OverlayMessagingMessage.MessageType type) {
+ Optimobile.executorService.submit(() -> {
+ OverlayMessagingMessage message = OverlayMessagingRequestService.readOverlayMessage(context, type);
+ Optimobile.handler.post(() -> onMessageLoaded(type, message));
+ });
+ }
+
+ @UiThread
+ private void onMessageLoaded(OverlayMessagingMessage.MessageType type, @Nullable OverlayMessagingMessage message) {
+ if (message == null || seenMessageIds.contains(message.getId())) {
+ onSlotCleared(type);
+ return;
+ }
+
+ seenMessageIds.add(message.getId());
+ processMessage(message);
+ }
+
+ @UiThread
+ private void processMessage(OverlayMessagingMessage message) {
+ if (interceptor == null) {
+ displayQueue.add(message);
+ maybeShowNext();
+ return;
+ }
+
+ AtomicBoolean processed = new AtomicBoolean(false);
+
+ OptimoveOverlayMessaging.OverlayMessagingInterceptorCallback callback = new OptimoveOverlayMessaging.OverlayMessagingInterceptorCallback() {
+ @Override
+ public void show() {
+ if (!processed.compareAndSet(false, true)) return;
+ Optimobile.handler.post(() -> handleInterceptorOutcome(message, InterceptorOutcome.SHOW));
+ }
+
+ @Override
+ public void discard() {
+ if (!processed.compareAndSet(false, true)) return;
+ Optimobile.handler.post(() -> handleInterceptorOutcome(message, InterceptorOutcome.DISCARD));
+ }
+
+ @Override
+ public void defer() {
+ if (!processed.compareAndSet(false, true)) return;
+ Optimobile.handler.post(() -> handleInterceptorOutcome(message, InterceptorOutcome.DEFER));
+ }
+ };
+
+ interceptorExecutor.schedule(() -> {
+ if (!processed.compareAndSet(false, true)) return;
+ Optimobile.handler.post(() -> handleInterceptorOutcome(message, InterceptorOutcome.TIMEOUT));
+ }, interceptor.getTimeoutMs(), TimeUnit.MILLISECONDS);
+
+ interceptor.onMessageLoaded(message, callback);
+ }
+
+ @UiThread
+ private void handleInterceptorOutcome(@NonNull OverlayMessagingMessage message, @NonNull InterceptorOutcome outcome) {
+ switch (outcome) {
+ case SHOW:
+ displayQueue.add(message);
+ maybeShowNext();
+ trackInterceptedEvent(message.getId(), outcome);
+ break;
+ case DEFER:
+ case TIMEOUT:
+ seenMessageIds.remove(message.getId());
+ onSlotCleared(message.getType());
+ trackInterceptedEvent(message.getId(), outcome);
+ break;
+ case DISCARD:
+ onSlotCleared(message.getType());
+ trackInterceptedEvent(message.getId(), outcome);
+ break;
+ }
+ }
+
+ @UiThread
+ private void maybeShowNext() {
+ OverlayMessagingMessage next = displayQueue.peek();
+
+ if (next == null) {
+ if (currentView != null) {
+ currentView.dispose();
+ currentView = null;
+ }
+ return;
+ }
+
+ if (currentView != null) {
+ currentView.showMessage(next);
+ return;
+ }
+
+ if (currentActivity == null) {
+ return;
+ }
+
+ String iarUrl;
+ try {
+ iarUrl = Optimobile.urlForService(UrlBuilder.Service.IAR, "");
+ } catch (Optimobile.PartialInitialisationException e) {
+ return;
+ }
+
+ currentView = new OverlayMessagingView(next, currentActivity, iarUrl, new OverlayMessagingView.Listener() {
+ @Override
+ public void onMessageClosed(OverlayMessagingMessage closedMessage) {
+ displayQueue.poll();
+ onSlotCleared(closedMessage.getType());
+ maybeShowNext();
+ }
+
+ @Override
+ public void onEvents(OverlayMessagingMessage message, List events) {
+ trackOverlayMessagingRendererEvents(message.getId(), events);
+ }
+
+ @Override
+ public void onDismissed(OverlayMessagingMessage message) {
+ trackDismissedEvent(message.getId());
+ }
+
+ @Override
+ public void onViewError(OverlayMessagingMessage failedMessage) {
+ currentView.dispose();
+ currentView = null;
+ // Immediate messages are short-lived. In case of an error we dont want them to stay on queue and surface later
+ displayQueue.poll();
+ onSlotCleared(failedMessage.getType());
+ maybeShowNext();
+ }
+ });
+ }
+
+ private void trackOverlayMessagingRendererEvents(long messageId, List events) {
+ for (OverlayMessagingRendererEvent event : events) {
+ try {
+ JSONObject data = event.data != null ? event.data : new JSONObject();
+ data.put("id", messageId);
+ Optimobile.trackEvent(context, event.type, data, System.currentTimeMillis(), event.immediateFlush);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void trackDismissedEvent(long messageId) {
+ try {
+ JSONObject props = new JSONObject();
+ props.put("id", messageId);
+ Optimobile.trackEventImmediately(context, AnalyticsContract.EVENT_TYPE_OM_DISMISSED, props);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void trackInterceptedEvent(long messageId, @NonNull InterceptorOutcome outcome) {
+ try {
+ JSONObject props = new JSONObject();
+ props.put("outcome", outcome.toEventValue());
+ props.put("id", messageId);
+ Optimobile.trackEventImmediately(context, AnalyticsContract.EVENT_TYPE_OM_INTERCEPTED, props);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ //==============================================================================================
+ //-- AppStateChangedListener
+
+ @Override
+ @UiThread
+ public void activityAvailable(@NonNull Activity activity) {
+ if (currentActivity != activity) {
+ if (currentView != null) {
+ currentView.dispose();
+ currentView = null;
+ }
+ currentActivity = activity;
+ }
+ maybeShowNext();
+ }
+
+ @Override
+ @UiThread
+ public void activityUnavailable(@NonNull Activity activity) {
+ if (activity != currentActivity) {
+ return;
+ }
+ currentActivity = null;
+ if (currentView != null) {
+ currentView.dispose();
+ currentView = null;
+ }
+ }
+
+ @Override
+ public void appEnteredForeground() { /* noop */ }
+
+ @Override
+ public void appEnteredBackground() { /* noop */ }
+
+}
diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingMessage.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingMessage.java
new file mode 100644
index 00000000..39944ab6
--- /dev/null
+++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingMessage.java
@@ -0,0 +1,42 @@
+package com.optimove.android.optimobile;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.json.JSONObject;
+
+public class OverlayMessagingMessage {
+
+ public enum MessageType {
+ SESSION, IMMEDIATE
+ }
+
+ private final long id;
+ private final JSONObject content;
+ private final JSONObject data;
+ private final MessageType type;
+
+ OverlayMessagingMessage(long id, @NonNull JSONObject content, @Nullable JSONObject data, @NonNull MessageType type) {
+ this.id = id;
+ this.content = content;
+ this.data = data;
+ this.type = type;
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public JSONObject getContent() {
+ return content;
+ }
+
+ @Nullable
+ public JSONObject getData() {
+ return data;
+ }
+
+ public MessageType getType() {
+ return type;
+ }
+}
diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingRendererEvent.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingRendererEvent.java
new file mode 100644
index 00000000..aed53804
--- /dev/null
+++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingRendererEvent.java
@@ -0,0 +1,38 @@
+package com.optimove.android.optimobile;
+
+import androidx.annotation.Nullable;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class OverlayMessagingRendererEvent {
+ final String type;
+ final boolean immediateFlush;
+ @Nullable
+ final JSONObject data;
+
+ private OverlayMessagingRendererEvent(String type, boolean immediateFlush, @Nullable JSONObject data) {
+ this.type = type;
+ this.immediateFlush = immediateFlush;
+ this.data = data;
+ }
+
+ static List parseAll(@Nullable JSONArray raw) {
+ List result = new ArrayList<>();
+ if (raw == null) return result;
+
+ for (int i = 0; i < raw.length(); i++) {
+ JSONObject obj = raw.optJSONObject(i);
+ if (obj == null) continue;
+ result.add(new OverlayMessagingRendererEvent(
+ obj.optString("type"),
+ obj.optBoolean("immediateFlush", true),
+ obj.optJSONObject("data")
+ ));
+ }
+ return result;
+ }
+}
diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingRequestService.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingRequestService.java
new file mode 100644
index 00000000..ae757df4
--- /dev/null
+++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingRequestService.java
@@ -0,0 +1,84 @@
+package com.optimove.android.optimobile;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+
+import okhttp3.Response;
+
+class OverlayMessagingRequestService {
+
+ private static final String TAG = OverlayMessagingRequestService.class.getName();
+
+ static @Nullable OverlayMessagingMessage readOverlayMessage(Context c, OverlayMessagingMessage.MessageType type) {
+ OptimobileHttpClient httpClient = OptimobileHttpClient.getInstance();
+ String userIdentifier = Optimobile.getCurrentUserIdentifier(c);
+
+ String encodedIdentifier = Uri.encode(userIdentifier);
+
+ try {
+ String messageType = type == OverlayMessagingMessage.MessageType.SESSION ? "session" : "immediate";
+
+ String url = Optimobile.urlForService(UrlBuilder.Service.OVERLAY_MESSAGING, "/api/v1/users/" + encodedIdentifier + "/messages/mobile?messageType=" + messageType);
+
+ try (Response response = httpClient.getSync(url)) {
+
+ if (!response.isSuccessful()) {
+ logFailedResponse(response);
+ return null;
+ }
+
+ if (response.code() == 204) {
+ return null;
+ }
+
+ return buildMessage(response, type);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (Optimobile.PartialInitialisationException e) {
+ // noop -- will skip loading until credentials are set
+ }
+
+ return null;
+ }
+
+ private static void logFailedResponse(Response response) {
+ switch (response.code()) {
+ case 404:
+ Optimobile.log(TAG, "User not found");
+ break;
+ case 422:
+ try {
+ Optimobile.log(TAG, response.body().string());
+ } catch (NullPointerException | IOException e) {
+ Optimobile.log(TAG, e.getMessage());
+ }
+ break;
+ default:
+ Optimobile.log(TAG, response.message());
+ break;
+ }
+ }
+
+ private static @Nullable OverlayMessagingMessage buildMessage(Response response, OverlayMessagingMessage.MessageType type) {
+ try {
+ JSONObject json = new JSONObject(response.body().string());
+ long id = json.getLong("id");
+ JSONObject content = json.getJSONObject("content");
+ JSONObject data = json.optJSONObject("data");
+
+ return new OverlayMessagingMessage(id, content, data, type);
+ } catch (NullPointerException | JSONException | IOException e) {
+ Optimobile.log(TAG, e.getMessage());
+ return null;
+ }
+ }
+}
diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingSessionManager.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingSessionManager.java
new file mode 100644
index 00000000..b83773cd
--- /dev/null
+++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingSessionManager.java
@@ -0,0 +1,98 @@
+package com.optimove.android.optimobile;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.UiThread;
+
+class OverlayMessagingSessionManager implements AppStateWatcher.AppStateChangedListener {
+
+ interface Listener {
+ @UiThread
+ void onSessionStarted();
+ }
+
+ private static final long SCHEDULE_BUFFER_MS = 1_000L;
+ private static final String PREFS_FILE = "optimove_overlay_messaging";
+ private static final String KEY_LAST_SESSION_START = "last_session_start";
+
+ private final Handler handler;
+ private final long sessionLengthMs;
+ private final Listener listener;
+ private final SharedPreferences prefs;
+ private boolean appInForeground;
+
+ private final Runnable ticker = new Runnable() {
+ @Override
+ public void run() {
+ startNewSession();
+ scheduleNextTick();
+ }
+ };
+
+ OverlayMessagingSessionManager(@NonNull Context context, long sessionLengthHours, @NonNull Listener listener) {
+ this.handler = new Handler(Looper.getMainLooper());
+ this.sessionLengthMs = sessionLengthHours * 3_600_000L;
+ this.listener = listener;
+ this.prefs = context.getApplicationContext().getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
+
+ OptimobileInitProvider.getAppStateWatcher().registerListener(this);
+ }
+
+ @UiThread
+ void resetSession() {
+ prefs.edit().remove(KEY_LAST_SESSION_START).apply();
+ if (!appInForeground) {
+ return;
+ }
+ handler.removeCallbacks(ticker);
+ startNewSession();
+ scheduleNextTick();
+ }
+
+ @Override
+ @UiThread
+ public void appEnteredForeground() {
+ appInForeground = true;
+ long lastSessionStart = prefs.getLong(KEY_LAST_SESSION_START, 0L);
+ boolean noPreviousSession = lastSessionStart == 0L;
+ boolean sessionExpired = (System.currentTimeMillis() - lastSessionStart) >= sessionLengthMs;
+
+ if (noPreviousSession || sessionExpired) {
+ startNewSession();
+ }
+ scheduleNextTick();
+ }
+
+ @Override
+ @UiThread
+ public void appEnteredBackground() {
+ appInForeground = false;
+ handler.removeCallbacks(ticker);
+ }
+
+ private void scheduleNextTick() {
+
+ long lastSessionStart = prefs.getLong(KEY_LAST_SESSION_START, 0L);
+ long nextSessionAt = lastSessionStart + sessionLengthMs + SCHEDULE_BUFFER_MS;
+ long delay = Math.max(0, nextSessionAt - System.currentTimeMillis());
+ handler.removeCallbacks(ticker);
+ handler.postDelayed(ticker, delay);
+ }
+
+ private void startNewSession() {
+ prefs.edit().putLong(KEY_LAST_SESSION_START, System.currentTimeMillis()).apply();
+ listener.onSessionStarted();
+ }
+
+
+ @Override
+ public void activityAvailable(@NonNull Activity activity) { /* noop */ }
+
+ @Override
+ public void activityUnavailable(@NonNull Activity activity) { /* noop */ }
+}
diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingView.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingView.java
new file mode 100644
index 00000000..c3f04303
--- /dev/null
+++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingView.java
@@ -0,0 +1,160 @@
+package com.optimove.android.optimobile;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.List;
+
+class OverlayMessagingView extends BaseMessageView {
+
+ private static final String SDK_ACTION_OPEN_DEEP_LINK = "OPEN_DEEP_LINK";
+
+ private static class RendererCommand {
+ final boolean close;
+ @Nullable
+ final List events;
+ @Nullable
+ final JSONArray sdkActions;
+
+ private RendererCommand(boolean close, @Nullable List events, @Nullable JSONArray sdkActions) {
+ this.close = close;
+ this.events = events;
+ this.sdkActions = sdkActions;
+ }
+
+ @Nullable
+ static RendererCommand parse(@Nullable JSONObject data) {
+ if (data == null) return null;
+ List events = OverlayMessagingRendererEvent.parseAll(data.optJSONArray("events"));
+ return new RendererCommand(data.optBoolean("close", false), events.isEmpty() ? null : events, data.optJSONArray("executeSdkActions"));
+ }
+ }
+
+ interface Listener {
+ @UiThread
+ void onMessageClosed(OverlayMessagingMessage message);
+
+ @UiThread
+ void onEvents(OverlayMessagingMessage message, List events);
+
+ @UiThread
+ void onDismissed(OverlayMessagingMessage message);
+
+ @UiThread
+ void onViewError(OverlayMessagingMessage message);
+ }
+
+ @NonNull
+ private OverlayMessagingMessage currentMessage;
+ @NonNull
+ private final Listener listener;
+
+ @UiThread
+ OverlayMessagingView(@NonNull OverlayMessagingMessage message, @NonNull Activity currentActivity, @NonNull String iarUrl, @NonNull Listener listener) {
+ super(currentActivity, false);
+
+ this.currentMessage = message;
+ this.listener = listener;
+
+ showWebView(currentActivity, iarUrl);
+ }
+
+ @UiThread
+ void showMessage(@NonNull OverlayMessagingMessage message) {
+ if (currentMessage.getId() == message.getId()) {
+ return;
+ }
+ currentMessage = message;
+ sendCurrentMessageToClient();
+ }
+
+ private void openUrl(Activity currentActivity, String uri) {
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
+ if (browserIntent.resolveActivity(currentActivity.getPackageManager()) != null) {
+ currentActivity.startActivity(browserIntent);
+ }
+ }
+
+
+ // - Implementations for abstracts
+
+ @Override
+ protected JSONObject getCurrentMessageContent() {
+ return currentMessage.getContent();
+ }
+
+ @Override
+ protected void onViewError() {
+ listener.onViewError(currentMessage);
+ }
+
+ @Override
+ protected void onMessageClosedByClient() {
+ listener.onMessageClosed(currentMessage);
+ }
+
+ @Override
+ protected void onMessageCloseRequested(MessageCloseSource source) {
+ switch (source) {
+ case CLIENT:
+ // events already tracked when COMMAND handled
+ break;
+ case HARDWARE:
+ listener.onDismissed(currentMessage);
+ break;
+ }
+ }
+
+ @Override
+ protected void onMessageOpened() {
+ // noop: no event needed yet
+ }
+
+ @Override
+ protected void onExecuteActions(JSONObject data) {
+ // noop: OM uses COMMAND, not EXECUTE_ACTIONS
+ }
+
+ @Override
+ protected void onCommand(JSONObject data) {
+ RendererCommand command = RendererCommand.parse(data);
+ if (command == null) {
+ return;
+ }
+
+ currentActivity.runOnUiThread(() -> {
+ if (command.events != null) {
+ listener.onEvents(currentMessage, command.events);
+ }
+
+ if (command.close) {
+ closeCurrentMessage(MessageCloseSource.CLIENT);
+ }
+
+ if (command.sdkActions == null) {
+ return;
+ }
+ for (int i = 0; i < command.sdkActions.length(); i++) {
+ JSONObject action = command.sdkActions.optJSONObject(i);
+ if (action == null) continue;
+ String type = action.optString("type");
+ JSONObject actionData = action.optJSONObject("data");
+ switch (type) {
+ case SDK_ACTION_OPEN_DEEP_LINK:
+ if (actionData != null) {
+ openUrl(currentActivity, actionData.optString("url"));
+ }
+ break;
+ }
+ }
+ });
+ }
+}
diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/PushBroadcastReceiver.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/PushBroadcastReceiver.java
index 1ab6fdb8..f575cdd4 100644
--- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/PushBroadcastReceiver.java
+++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/PushBroadcastReceiver.java
@@ -90,6 +90,8 @@ protected void onPushReceived(Context context, PushMessage pushMessage) {
this.pushTrackDelivered(context, pushMessage);
+ this.maybeTriggerOverlayMessagingSync(context, pushMessage);
+
this.maybeTriggerInAppSync(context, pushMessage);
if (pushMessage.runBackgroundHandler()) {
@@ -101,6 +103,19 @@ protected void onPushReceived(Context context, PushMessage pushMessage) {
}
}
+ protected void maybeTriggerOverlayMessagingSync(Context context, PushMessage pushMessage) {
+ if (!Optimove.getConfig().isOverlayMessagingEnabled()) {
+ return;
+ }
+
+ if (!pushMessage.isOverlayMessagingTrigger()) {
+ return;
+ }
+
+ Optimobile.handler.post(() -> OptimoveOverlayMessaging.getInstance().onPushTriggerReceived());
+ }
+
+
private void processPushMessage(Context context, PushMessage pushMessage) {
Notification.Builder builder = getNotificationBuilder(context, pushMessage);
if (null == builder) {
diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/PushMessage.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/PushMessage.java
index 1b5eea47..0abe2313 100644
--- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/PushMessage.java
+++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/PushMessage.java
@@ -18,6 +18,7 @@ public final class PushMessage implements Parcelable {
public static final String EXTRAS_KEY = "com.optimove.push.message";
private static final int DEEP_LINK_TYPE_IN_APP = 1;
+ private static final int DEEP_LINK_TYPE_OVERLAY_MESSAGE = 2;
public static final String TAG = PushMessage.class.getName();
private final int id;
@@ -31,6 +32,7 @@ public final class PushMessage implements Parcelable {
Uri url;
private final boolean runBackgroundHandler;
private final int tickleId;
+ private final boolean isOverlayMessagingTrigger;
private final @Nullable
String pictureUrl;
private @Nullable
@@ -59,6 +61,7 @@ public final class PushMessage implements Parcelable {
this.message = message;
this.data = data;
this.tickleId = this.getTickleId(data);
+ this.isOverlayMessagingTrigger = this.isOverlayMessagingTrigger(data);
this.timeSent = timeSent;
this.url = url;
this.runBackgroundHandler = runBackgroundHandler;
@@ -90,6 +93,7 @@ private PushMessage(Parcel in) {
url = Uri.parse(urlString);
}
tickleId = in.readInt();
+ isOverlayMessagingTrigger = (in.readInt() == 1);
pictureUrl = in.readString();
String buttonsString = in.readString();
@@ -142,6 +146,22 @@ private Integer getTickleId(JSONObject data) {
}
}
+ boolean isOverlayMessagingTrigger(JSONObject data) {
+ JSONObject deepLink = data.optJSONObject("k.deepLink");
+
+ if (deepLink == null) {
+ return false;
+ }
+
+ int linkType = deepLink.optInt("type", -1);
+
+ if (linkType != DEEP_LINK_TYPE_OVERLAY_MESSAGE) {
+ return false;
+ }
+
+ return true;
+ }
+
public static final Creator CREATOR = new Creator() {
@Override
public PushMessage createFromParcel(Parcel in) {
@@ -173,6 +193,7 @@ public void writeToParcel(Parcel dest, int flags) {
dest.writeString(dataString);
dest.writeString(urlString);
dest.writeInt(tickleId);
+ dest.writeInt(isOverlayMessagingTrigger ? 1 : 0);
dest.writeString(pictureUrl);
dest.writeString(buttonsString);
dest.writeString(sound);
@@ -215,6 +236,10 @@ int getTickleId() {
return tickleId;
}
+ boolean isOverlayMessagingTrigger() {
+ return isOverlayMessagingTrigger;
+ }
+
public boolean runBackgroundHandler() {
return runBackgroundHandler;
}
diff --git a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/UrlBuilder.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/UrlBuilder.java
index cea0feb6..708f5328 100644
--- a/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/UrlBuilder.java
+++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/UrlBuilder.java
@@ -14,6 +14,7 @@ public enum Service {
IAR,
MEDIA,
PUSH,
+ OVERLAY_MESSAGING
}
private final Map baseUrlMap;
@@ -39,13 +40,15 @@ public static Map defaultMapping(@NonNull String region) {
baseUrlMap.put(Service.IAR, "https://iar.app.delivery");
+
baseUrlMap.put(Service.PUSH, "https://push-" + region + ".kumulos.com");
baseUrlMap.put(Service.CRM, "https://crm-" + region + ".kumulos.com");
baseUrlMap.put(Service.EVENTS, "https://events-" + region + ".kumulos.com");
baseUrlMap.put(Service.DDL, "https://links-" + region + ".kumulos.com");
baseUrlMap.put(Service.MEDIA, "https://i-" + region + ".app.delivery");
+ baseUrlMap.put(Service.OVERLAY_MESSAGING, "https://optimobile-overlay-srv-" + region + ".optimove.net");
+
return baseUrlMap;
}
-
}