From 2958676b652196b116e525c8dcb83af29a94995c Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Sun, 29 Mar 2026 00:46:53 +0000 Subject: [PATCH 01/29] session manager + pass tickled trigger + slots + request --- .../com/optimove/android/OptimoveConfig.java | 33 +++++++ .../android/optimobile/Optimobile.java | 5 + .../optimobile/OptimoveOverlayMessaging.java | 73 ++++++++++++++ .../optimobile/OverlayMessagingManager.java | 69 +++++++++++++ .../optimobile/OverlayMessagingMessage.java | 13 +++ .../OverlayMessagingRequestService.java | 82 ++++++++++++++++ .../OverlayMessagingSessionManager.java | 98 +++++++++++++++++++ .../optimobile/OverlayMessagingView.java | 4 + .../optimobile/PushBroadcastReceiver.java | 15 +++ 9 files changed, 392 insertions(+) create mode 100644 OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OptimoveOverlayMessaging.java create mode 100644 OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingManager.java create mode 100644 OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingMessage.java create mode 100644 OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingRequestService.java create mode 100644 OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingSessionManager.java create mode 100644 OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingView.java 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..306750a1 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,8 @@ public final class OptimoveConfig { private @Nullable EmbeddedMessagingConfig embeddedMessagingConfig; + private @Nullable Integer overlayMessagingSessionLengthMinutes; + public enum InAppConsentStrategy { AUTO_ENROLL, EXPLICIT_BY_USER @@ -109,6 +111,7 @@ private enum Feature { OPTIMOBILE, PREFERENCE_CENTER, EMBEDDED_MESSAGING, + OVERLAY_MESSAGING, } Set features = new HashSet<>(); @@ -137,6 +140,12 @@ public FeatureSet withEmbeddedMessaging() { return this; } + public FeatureSet withOverlayMessaging() { + features.add(Feature.OVERLAY_MESSAGING); + + return this; + } + boolean has(Feature feature) { return features.contains(feature); @@ -415,6 +424,14 @@ public boolean usesDelayedConfiguration() { return this.embeddedMessagingConfig; } + public boolean isOverlayMessagingEnabled() { + return this.featureSet.has(FeatureSet.Feature.OVERLAY_MESSAGING); + } + + public int getOverlayMessagingSessionLengthMinutes() { + return this.overlayMessagingSessionLengthMinutes; + } + private boolean hasFinishedInitialisation() { boolean hasOptimoveCreds = optimoveToken != null && configFileName != null; boolean hasOptimobileCreds = apiKey != null && secretKey != null; @@ -474,6 +491,8 @@ public static class Builder { private @Nullable LogLevel minLogLevel; + private @Nullable Integer overlayMessagingSessionLengthMinutes; + /** * @deprecated Use {@link Builder#Builder(FeatureSet)} instead */ @@ -575,6 +594,18 @@ public Builder enableEmbeddedMessaging(@NonNull String embeddedMessagingConfigur return this; } + public Builder enableOverlayMessaging(int sessionLengthMinutes) { + if (sessionLengthMinutes <= 0) { + throw new IllegalArgumentException("OverlayMessaging: sessionLengthMinutes must be greater than 0"); + } + if (!this.featureSet.has(FeatureSet.Feature.OPTIMOBILE)) { + throw new IllegalArgumentException("OverlayMmessaging: optimobile feature required"); + } + this.overlayMessagingSessionLengthMinutes = sessionLengthMinutes; + this.featureSet.withOverlayMessaging(); + 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 +696,8 @@ public OptimoveConfig build() { newConfig.setMinLogLevel(this.minLogLevel); + newConfig.overlayMessagingSessionLengthMinutes = this.overlayMessagingSessionLengthMinutes; + return newConfig; } } 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..5455fe49 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,11 @@ public static synchronized void initialize(final Application application, Optimo OptimoveInApp.initialize(application, config); + //TODO: move to optimove? + if (config.isOverlayMessagingEnabled()) { + OptimoveOverlayMessaging.initialize(application, config.getOverlayMessagingSessionLengthMinutes()); + } + if (config.getDeferredDeepLinkHandler() != null) { deepLinkHelper = new DeferredDeepLinkHelper(); } 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..29a4e905 --- /dev/null +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OptimoveOverlayMessaging.java @@ -0,0 +1,73 @@ +package com.optimove.android.optimobile; + +import android.app.Application; +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; + +public class OptimoveOverlayMessaging { + + private static OptimoveOverlayMessaging shared; + + private final OverlayMessagingSessionManager sessionManager; + private final OverlayMessagingManager manager; + + @Nullable + private OverlayMessagingInterceptor interceptor; + + public interface OverlayMessagingInterceptor { + @UiThread + OverlayMessagingInterceptorOutcome onMessageLoaded(@NonNull OverlayMessagingMessage message); + } + + public enum OverlayMessagingInterceptorOutcome { + SHOW, + DISCARD, + HOLD + } + + private OptimoveOverlayMessaging(@NonNull Application application, long sessionLengthMinutes) { + this.manager = new OverlayMessagingManager(application); + OverlayMessagingSessionManager.Listener sessionListener = () -> + manager.onTriggerReceived(OverlayMessagingManager.MessageType.SESSION); + this.sessionManager = new OverlayMessagingSessionManager(application, sessionLengthMinutes, 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) { + this.interceptor = interceptor; + } + + @UiThread + public void resetSession() { + sessionManager.resetSession(); + } + + //============================================================================================== + //-- Internal + + @UiThread + void onPushTriggerReceived() { + manager.onTriggerReceived(OverlayMessagingManager.MessageType.IMMEDIATE); + } + + static void initialize(@NonNull Application application, long sessionLengthMinutes) { + shared = new OptimoveOverlayMessaging(application, sessionLengthMinutes); + } + + boolean isOverlayMessagingEnabled() { + return shared != null; + } +} 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..1e3ff533 --- /dev/null +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingManager.java @@ -0,0 +1,69 @@ +package com.optimove.android.optimobile; + +import android.content.Context; + +import androidx.annotation.UiThread; + +class OverlayMessagingManager { + + private static final int SESSION_SLOT_CAPACITY = 1; + private static final int IMMEDIATE_SLOT_CAPACITY = 1; + + enum MessageType { + SESSION, + IMMEDIATE + } + + private final Context context; + + private int sessionSlotCount = 0; + private int immediateSlotCount = 0; + + OverlayMessagingManager(Context context) { + this.context = context.getApplicationContext(); + } + + @UiThread + void onTriggerReceived(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(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(MessageType type) { + Optimobile.executorService.submit(() -> { + OverlayMessagingMessage message = OverlayMessagingRequestService.readOverlayMessage(context, type); + Optimobile.handler.post(() -> onMessageLoaded(type, message)); + }); + } + + @UiThread + private void onMessageLoaded(MessageType type, OverlayMessagingMessage message) { + if (message == null) { + onSlotCleared(type); + return; + } + // TODO: run interceptor, add to display queue + } +} 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..ecc9f065 --- /dev/null +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingMessage.java @@ -0,0 +1,13 @@ +package com.optimove.android.optimobile; + +import androidx.annotation.NonNull; + +public class OverlayMessagingMessage { + public final long id; + public final String html; + + OverlayMessagingMessage(long id, @NonNull String html) { + this.id = id; + this.html = html; + } +} 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..0f649c30 --- /dev/null +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingRequestService.java @@ -0,0 +1,82 @@ +package com.optimove.android.optimobile; + +import android.content.Context; +import android.net.Uri; + +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, OverlayMessagingManager.MessageType type) { + OptimobileHttpClient httpClient = OptimobileHttpClient.getInstance(); + String userIdentifier = Optimobile.getCurrentUserIdentifier(c); + + String encodedIdentifier = Uri.encode(userIdentifier); + + try { + //TODO: derive region, tenantId, brandId from credentials + take from urlBuilder + String region = "dev"; + int tenantId = 3013; + String brandId = "9abb8d6d-62ed-42d1-97d1-c82d15f9c1fc"; + + String messageType = type == OverlayMessagingManager.MessageType.SESSION ? "session-start" : "immediate"; + + String url = String.format( + "http://optimobile-overlay-srv-%s.optimove.net/mobile/%s/messages?tenantId=%s&brandId=%s&messageType=%s", + region, encodedIdentifier, tenantId, brandId, messageType); + + try (Response response = httpClient.getSync(url)) { + if (!response.isSuccessful()) { + logFailedResponse(response); + return null; + } + return buildMessage(response); + } + } 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) { + try { + JSONObject json = new JSONObject(response.body().string()); + long id = json.getLong("id"); + String html = json.getString("html"); + return new OverlayMessagingMessage(id, html); + } 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..435c3306 --- /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 sessionLengthMinutes, @NonNull Listener listener) { + this.handler = new Handler(Looper.getMainLooper()); + this.sessionLengthMs = sessionLengthMinutes * 60_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..89c8a7f8 --- /dev/null +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingView.java @@ -0,0 +1,4 @@ +package com.optimove.android.optimobile; + +class OverlayMessagingView { +} 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 e96abb91..45b7505d 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 (!OptimoveOverlayMessaging.getInstance().isOverlayMessagingEnabled()) { + return; + } + // TODO -- where in PushMessage does it actually sit? + if (!pushMessage.getTitle().equals("OM")){ + return; + } + + Optimobile.handler.post(() -> OptimoveOverlayMessaging.getInstance().onPushTriggerReceived()); + } + + private void processPushMessage(Context context, PushMessage pushMessage) { Notification.Builder builder = getNotificationBuilder(context, pushMessage); if (null == builder) { From e899095310bc4f22cbc4c2f0e2c51edd246ba06d Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Mon, 30 Mar 2026 00:28:21 +0100 Subject: [PATCH 02/29] split InAppMessageView for Base to be reusable by OM --- .../android/optimobile/AnalyticsContract.java | 1 + .../android/optimobile/BaseMessageView.java | 486 +++++++++++++++++ .../android/optimobile/InAppMessageView.java | 514 ++---------------- .../optimobile/OptimoveOverlayMessaging.java | 19 +- .../optimobile/OverlayMessagingManager.java | 119 +++- 5 files changed, 669 insertions(+), 470 deletions(-) create mode 100644 OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/BaseMessageView.java 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..4f386ebe 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,7 @@ 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_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..6eb3a9d6 --- /dev/null +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/BaseMessageView.java @@ -0,0 +1,486 @@ +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 { + + 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; + + + @UiThread + BaseMessageView(@NonNull Activity currentActivity) { + this.state = State.INITIAL; + pageFinished = false; + + this.currentActivity = currentActivity; + } + + @UiThread + void dispose() { + closeDialog(currentActivity); + state = State.DISPOSED; + } + + @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() + ")"; + + 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 + protected 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()); + } + } + + protected void closeCurrentMessage() { + sendToClient(HOST_MESSAGE_TYPE_CLOSE_MESSAGE, null); + + onMessageCloseRequested(); + } + + @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); + onMessageOpened(); + }); + return; + case "MESSAGE_CLOSED": + currentActivity.runOnUiThread(() -> onMessageClosedByClient()); + return; + case "EXECUTE_ACTIONS": + onExecuteActions(data); + + 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); + } + + // - Abstracts + abstract protected JSONObject getCurrentMessageContent(); + + abstract protected void onMessageClosedByClient(); + + abstract protected void onMessageCloseRequested(); + + abstract protected void onMessageOpened(); + + abstract protected void onExecuteActions(JSONObject data); +} 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..e533b22f 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); + this.presenter = presenter; - this.currentActivity = currentActivity; this.currentMessage = message; this.region = region; @@ -118,396 +58,6 @@ 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 @@ -684,4 +234,50 @@ 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 onMessageCloseRequested() { + // 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/OptimoveOverlayMessaging.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OptimoveOverlayMessaging.java index 29a4e905..393b5271 100644 --- 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 @@ -15,18 +15,19 @@ public class OptimoveOverlayMessaging { private final OverlayMessagingSessionManager sessionManager; private final OverlayMessagingManager manager; - @Nullable - private OverlayMessagingInterceptor interceptor; + public interface OverlayMessagingInterceptorCallback { + @UiThread void show(); + @UiThread void discard(); + @UiThread void hold(); + } public interface OverlayMessagingInterceptor { @UiThread - OverlayMessagingInterceptorOutcome onMessageLoaded(@NonNull OverlayMessagingMessage message); - } + void onMessageLoaded(@NonNull OverlayMessagingMessage message, @NonNull OverlayMessagingInterceptorCallback callback); - public enum OverlayMessagingInterceptorOutcome { - SHOW, - DISCARD, - HOLD + default long getTimeoutMs() { + return 5000L; + } } private OptimoveOverlayMessaging(@NonNull Application application, long sessionLengthMinutes) { @@ -47,7 +48,7 @@ public static OptimoveOverlayMessaging getInstance() { } public void setInterceptor(@Nullable OverlayMessagingInterceptor interceptor) { - this.interceptor = interceptor; + manager.setInterceptor(interceptor); } @UiThread 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 index 1e3ff533..79848c38 100644 --- 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 @@ -2,8 +2,20 @@ 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.ArrayDeque; +import java.util.Queue; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + class OverlayMessagingManager { private static final int SESSION_SLOT_CAPACITY = 1; @@ -14,7 +26,29 @@ enum MessageType { IMMEDIATE } + enum InterceptorOutcome { + SHOW, + DISCARD, + HOLD, + TIMEOUT; + + String toEventValue() { + switch (this) { + case SHOW: return "shown"; + case DISCARD: return "discarded"; + case HOLD: return "held"; + 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; private int sessionSlotCount = 0; private int immediateSlotCount = 0; @@ -23,6 +57,11 @@ enum MessageType { this.context = context.getApplicationContext(); } + @UiThread + void setInterceptor(@Nullable OptimoveOverlayMessaging.OverlayMessagingInterceptor interceptor) { + this.interceptor = interceptor; + } + @UiThread void onTriggerReceived(MessageType type) { switch (type) { @@ -59,11 +98,87 @@ private void loadMessage(MessageType type) { } @UiThread - private void onMessageLoaded(MessageType type, OverlayMessagingMessage message) { + private void onMessageLoaded(MessageType type, @Nullable OverlayMessagingMessage message) { if (message == null) { onSlotCleared(type); return; } - // TODO: run interceptor, add to display queue + + processMessage(type, message); + } + + @UiThread + private void processMessage(MessageType type, OverlayMessagingMessage message) { + if (interceptor == null) { + displayQueue.add(message); + // TODO: notify OverlayMessagingView to display next message + 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(type, message, InterceptorOutcome.SHOW)); + } + + @Override + public void discard() { + if (!processed.compareAndSet(false, true)) return; + Optimobile.handler.post(() -> handleInterceptorOutcome(type, message, InterceptorOutcome.DISCARD)); + } + + @Override + public void hold() { + if (!processed.compareAndSet(false, true)) return; + Optimobile.handler.post(() -> handleInterceptorOutcome(type, message, InterceptorOutcome.HOLD)); + } + }; + + interceptorExecutor.schedule(() -> { + if (!processed.compareAndSet(false, true)) return; + Optimobile.handler.post(() -> handleInterceptorOutcome(type, message, InterceptorOutcome.TIMEOUT)); + }, interceptor.getTimeoutMs(), TimeUnit.MILLISECONDS); + + interceptor.onMessageLoaded(message, callback); + } + + @UiThread + private void handleInterceptorOutcome( + @NonNull MessageType type, + @NonNull OverlayMessagingMessage message, + @NonNull InterceptorOutcome outcome) { + switch (outcome) { + case SHOW: + displayQueue.add(message); + // TODO: notify OverlayMessagingView to display next message + trackInterceptedEvent(message.id, outcome); + break; + case DISCARD: + onSlotCleared(type); + trackInterceptedEvent(message.id, outcome); + break; + case HOLD: + onSlotCleared(type); + trackInterceptedEvent(message.id, outcome); + break; + case TIMEOUT: + onSlotCleared(type); + trackInterceptedEvent(message.id, outcome); + break; + } + } + + 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(); + } } } From 69ea2a64cb31c586aacf8c6398e75476e83b0979 Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Mon, 30 Mar 2026 00:37:22 +0100 Subject: [PATCH 03/29] overlay messaging view variant --- .../android/optimobile/BaseMessageView.java | 13 +- .../android/optimobile/InAppMessageView.java | 4 +- .../optimobile/OverlayMessagingMessage.java | 16 +- .../OverlayMessagingRequestService.java | 8 +- .../optimobile/OverlayMessagingView.java | 171 +++++++++++++++++- 5 files changed, 199 insertions(+), 13 deletions(-) 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 index 6eb3a9d6..2d6801b1 100644 --- 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 @@ -51,6 +51,11 @@ abstract class BaseMessageView extends WebViewClient { + protected enum MessageCloseSource { + HARDWARE, + CLICK + } + private enum State { INITIAL, LOADING, @@ -232,7 +237,7 @@ protected void showWebView(@NonNull final Activity currentActivity, @NonNull Str 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(); + closeCurrentMessage(MessageCloseSource.HARDWARE); } return true; }); @@ -271,10 +276,10 @@ protected void showWebView(@NonNull final Activity currentActivity, @NonNull Str } } - protected void closeCurrentMessage() { + protected void closeCurrentMessage(MessageCloseSource source) { sendToClient(HOST_MESSAGE_TYPE_CLOSE_MESSAGE, null); - onMessageCloseRequested(); + onMessageCloseRequested(source); } @Override @@ -478,7 +483,7 @@ private Pair determineNotchPositions(Window window, List abstract protected void onMessageClosedByClient(); - abstract protected void onMessageCloseRequested(); + abstract protected void onMessageCloseRequested(MessageCloseSource source); abstract protected void onMessageOpened(); 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 e533b22f..2568d1f0 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 @@ -64,7 +64,7 @@ private void executeActions(Activity currentActivity, List act for (ExecutableAction action : actions) { switch (action.getType()) { case BUTTON_ACTION_CLOSE_MESSAGE: - closeCurrentMessage(); + closeCurrentMessage(MessageCloseSource.CLICK); break; case BUTTON_ACTION_TRACK_CONVERSION_EVENT: Optimobile.trackEventImmediately(currentActivity, action.getEventType(), action.getConversionEventData()); @@ -258,7 +258,7 @@ protected void onMessageClosedByClient() { } @Override - protected void onMessageCloseRequested() { + 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); 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 index ecc9f065..5cd82414 100644 --- 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 @@ -2,12 +2,18 @@ import androidx.annotation.NonNull; -public class OverlayMessagingMessage { - public final long id; - public final String html; +import org.json.JSONObject; - OverlayMessagingMessage(long id, @NonNull String html) { +class OverlayMessagingMessage { + final long id; + final JSONObject content; + + OverlayMessagingMessage(long id, @NonNull JSONObject content) { this.id = id; - this.html = html; + this.content = content; + } + + public JSONObject getContent() { + return content; } } 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 index 0f649c30..60dfa332 100644 --- 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 @@ -73,7 +73,13 @@ private static void logFailedResponse(Response response) { JSONObject json = new JSONObject(response.body().string()); long id = json.getLong("id"); String html = json.getString("html"); - return new OverlayMessagingMessage(id, html); + + // TODO: this should be under message content in response already + JSONObject content = new JSONObject(); + content.put("ver", 1); + content.put("html", html); + + return new OverlayMessagingMessage(id, content); } 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/OverlayMessagingView.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingView.java index 89c8a7f8..948c4b95 100644 --- 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 @@ -1,4 +1,173 @@ package com.optimove.android.optimobile; -class OverlayMessagingView { +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.UiThread; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +class OverlayMessagingView extends BaseMessageView { + + private static final String TAG = OverlayMessagingView.class.getName(); + + private static final String BUTTON_ACTION_CLOSE_MESSAGE = "closeMessage"; + private static final String BUTTON_ACTION_OPEN_URL = "openUrl"; + + @NonNull + private OverlayMessagingMessage currentMessage; + + @UiThread + OverlayMessagingView(@NonNull OverlayMessagingMessage message, + @NonNull Activity currentActivity, + @NonNull String iarUrl) { + super(currentActivity); + + this.currentMessage = message; + + showWebView(currentActivity, iarUrl); + } + +// @UiThread +// void showMessage(@NonNull OverlayMessagingMessage message) { +// if (currentMessage.getInAppId() == message.getInAppId()) { +// return; +// } +// currentMessage = message; +// sendCurrentMessageToClient(); +// } + + @UiThread + private void executeActions(Activity currentActivity, List actions) { + // Handle 'secondary' actions + for (ExecutableAction action : actions) { + switch (action.getType()) { + case BUTTON_ACTION_CLOSE_MESSAGE: + closeCurrentMessage(MessageCloseSource.CLICK); + break; + } + } + + // Handle 'terminating' actions + for (ExecutableAction action : actions) { + switch (action.getType()) { + case BUTTON_ACTION_OPEN_URL: + //presenter.cancelCurrentPresentationQueue(); + + this.openUrl(currentActivity, action.getUrl()); + return; + } + } + } + + + 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); + } + } + + private List parseButtonActionData(@NonNull JSONObject data) { + List actions = new ArrayList<>(); + JSONArray rawActions = data.optJSONArray("actions"); + + if (null == rawActions) { + return actions; + } + + for (int i = 0; i < rawActions.length(); i++) { + JSONObject rawAction = rawActions.optJSONObject(i); + + String actionType = rawAction.optString("type"); + JSONObject rawActionData = rawAction.optJSONObject("data"); + + ExecutableAction action = new ExecutableAction(); + action.setType(actionType); + + switch (actionType) { + case BUTTON_ACTION_OPEN_URL: + if (null == rawActionData) { + continue; + } + String url = rawActionData.optString("url"); + action.setUrl(url); + break; + default: + break; + } + actions.add(action); + } + + return actions; + } + + private static class ExecutableAction { + String type; + + String url; + + void setType(String type) { + this.type = type; + } + + void setUrl(String url) { + this.url = url; + } + + + String getType() { + return type; + } + + String getUrl() { + return url; + } + + } + + + // - Implementations for abstracts + + @Override + protected JSONObject getCurrentMessageContent() { + return currentMessage.getContent(); + } + + @Override + protected void onMessageClosedByClient() { + // TODO: free slot, present next message if have + } + + @Override + protected void onMessageCloseRequested(MessageCloseSource source) { + // TODO: do we need to differentiate these? + // 1. back button -> dismissed + // 2. close action -> clicked + + //TODO: report event + } + + @Override + protected void onMessageOpened() { + // noop: no event needed yet + } + + @Override + protected void onExecuteActions(JSONObject data) { + if (null == data) { + return; + } + + List actions = this.parseButtonActionData(data); + currentActivity.runOnUiThread(() -> this.executeActions(currentActivity, actions)); + } + + } From d8d0a4825a24b05aab3ceaf5c61ddd7aeb938798 Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Mon, 30 Mar 2026 00:57:53 +0100 Subject: [PATCH 04/29] store type on message -- to clear right slot later + can be used in interceptor --- .../optimobile/OptimoveOverlayMessaging.java | 4 +-- .../optimobile/OverlayMessagingManager.java | 32 ++++++++----------- .../optimobile/OverlayMessagingMessage.java | 24 +++++++++++--- .../OverlayMessagingRequestService.java | 10 +++--- 4 files changed, 40 insertions(+), 30 deletions(-) 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 index 393b5271..98c1ecc1 100644 --- 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 @@ -33,7 +33,7 @@ default long getTimeoutMs() { private OptimoveOverlayMessaging(@NonNull Application application, long sessionLengthMinutes) { this.manager = new OverlayMessagingManager(application); OverlayMessagingSessionManager.Listener sessionListener = () -> - manager.onTriggerReceived(OverlayMessagingManager.MessageType.SESSION); + manager.onTriggerReceived(OverlayMessagingMessage.MessageType.SESSION); this.sessionManager = new OverlayMessagingSessionManager(application, sessionLengthMinutes, sessionListener); } @@ -61,7 +61,7 @@ public void resetSession() { @UiThread void onPushTriggerReceived() { - manager.onTriggerReceived(OverlayMessagingManager.MessageType.IMMEDIATE); + manager.onTriggerReceived(OverlayMessagingMessage.MessageType.IMMEDIATE); } static void initialize(@NonNull Application application, long sessionLengthMinutes) { 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 index 79848c38..995d0c37 100644 --- 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 @@ -21,11 +21,6 @@ class OverlayMessagingManager { private static final int SESSION_SLOT_CAPACITY = 1; private static final int IMMEDIATE_SLOT_CAPACITY = 1; - enum MessageType { - SESSION, - IMMEDIATE - } - enum InterceptorOutcome { SHOW, DISCARD, @@ -63,7 +58,7 @@ void setInterceptor(@Nullable OptimoveOverlayMessaging.OverlayMessagingIntercept } @UiThread - void onTriggerReceived(MessageType type) { + void onTriggerReceived(OverlayMessagingMessage.MessageType type) { switch (type) { case SESSION: if (sessionSlotCount >= SESSION_SLOT_CAPACITY) return; @@ -79,7 +74,7 @@ void onTriggerReceived(MessageType type) { } @UiThread - void onSlotCleared(MessageType type) { + void onSlotCleared(OverlayMessagingMessage.MessageType type) { switch (type) { case SESSION: sessionSlotCount = Math.max(0, sessionSlotCount - 1); @@ -90,7 +85,7 @@ void onSlotCleared(MessageType type) { } } - private void loadMessage(MessageType type) { + private void loadMessage(OverlayMessagingMessage.MessageType type) { Optimobile.executorService.submit(() -> { OverlayMessagingMessage message = OverlayMessagingRequestService.readOverlayMessage(context, type); Optimobile.handler.post(() -> onMessageLoaded(type, message)); @@ -98,17 +93,17 @@ private void loadMessage(MessageType type) { } @UiThread - private void onMessageLoaded(MessageType type, @Nullable OverlayMessagingMessage message) { + private void onMessageLoaded(OverlayMessagingMessage.MessageType type, @Nullable OverlayMessagingMessage message) { if (message == null) { onSlotCleared(type); return; } - processMessage(type, message); + processMessage(message); } @UiThread - private void processMessage(MessageType type, OverlayMessagingMessage message) { + private void processMessage(OverlayMessagingMessage message) { if (interceptor == null) { displayQueue.add(message); // TODO: notify OverlayMessagingView to display next message @@ -121,25 +116,25 @@ private void processMessage(MessageType type, OverlayMessagingMessage message) { @Override public void show() { if (!processed.compareAndSet(false, true)) return; - Optimobile.handler.post(() -> handleInterceptorOutcome(type, message, InterceptorOutcome.SHOW)); + Optimobile.handler.post(() -> handleInterceptorOutcome(message, InterceptorOutcome.SHOW)); } @Override public void discard() { if (!processed.compareAndSet(false, true)) return; - Optimobile.handler.post(() -> handleInterceptorOutcome(type, message, InterceptorOutcome.DISCARD)); + Optimobile.handler.post(() -> handleInterceptorOutcome(message, InterceptorOutcome.DISCARD)); } @Override public void hold() { if (!processed.compareAndSet(false, true)) return; - Optimobile.handler.post(() -> handleInterceptorOutcome(type, message, InterceptorOutcome.HOLD)); + Optimobile.handler.post(() -> handleInterceptorOutcome(message, InterceptorOutcome.HOLD)); } }; interceptorExecutor.schedule(() -> { if (!processed.compareAndSet(false, true)) return; - Optimobile.handler.post(() -> handleInterceptorOutcome(type, message, InterceptorOutcome.TIMEOUT)); + Optimobile.handler.post(() -> handleInterceptorOutcome(message, InterceptorOutcome.TIMEOUT)); }, interceptor.getTimeoutMs(), TimeUnit.MILLISECONDS); interceptor.onMessageLoaded(message, callback); @@ -147,7 +142,6 @@ public void hold() { @UiThread private void handleInterceptorOutcome( - @NonNull MessageType type, @NonNull OverlayMessagingMessage message, @NonNull InterceptorOutcome outcome) { switch (outcome) { @@ -157,15 +151,15 @@ private void handleInterceptorOutcome( trackInterceptedEvent(message.id, outcome); break; case DISCARD: - onSlotCleared(type); + onSlotCleared(message.type); trackInterceptedEvent(message.id, outcome); break; case HOLD: - onSlotCleared(type); + onSlotCleared(message.type); trackInterceptedEvent(message.id, outcome); break; case TIMEOUT: - onSlotCleared(type); + onSlotCleared(message.type); trackInterceptedEvent(message.id, outcome); break; } 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 index 5cd82414..6903b3bb 100644 --- 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 @@ -4,16 +4,32 @@ import org.json.JSONObject; -class OverlayMessagingMessage { - final long id; - final JSONObject content; +public class OverlayMessagingMessage { - OverlayMessagingMessage(long id, @NonNull JSONObject content) { + public enum MessageType { + SESSION, + IMMEDIATE + } + + private final long id; + private final JSONObject content; + private final MessageType type; + + OverlayMessagingMessage(long id, @NonNull JSONObject content, @NonNull MessageType type) { this.id = id; this.content = content; + this.type = type; + } + + public long getId() { + return id; } public JSONObject getContent() { return content; } + + public MessageType getType() { + return type; + } } 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 index 60dfa332..0d0f518a 100644 --- 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 @@ -16,7 +16,7 @@ class OverlayMessagingRequestService { private static final String TAG = OverlayMessagingRequestService.class.getName(); - static @Nullable OverlayMessagingMessage readOverlayMessage(Context c, OverlayMessagingManager.MessageType type) { + static @Nullable OverlayMessagingMessage readOverlayMessage(Context c, OverlayMessagingMessage.MessageType type) { OptimobileHttpClient httpClient = OptimobileHttpClient.getInstance(); String userIdentifier = Optimobile.getCurrentUserIdentifier(c); @@ -28,7 +28,7 @@ class OverlayMessagingRequestService { int tenantId = 3013; String brandId = "9abb8d6d-62ed-42d1-97d1-c82d15f9c1fc"; - String messageType = type == OverlayMessagingManager.MessageType.SESSION ? "session-start" : "immediate"; + String messageType = type == OverlayMessagingMessage.MessageType.SESSION ? "session-start" : "immediate"; String url = String.format( "http://optimobile-overlay-srv-%s.optimove.net/mobile/%s/messages?tenantId=%s&brandId=%s&messageType=%s", @@ -39,7 +39,7 @@ class OverlayMessagingRequestService { logFailedResponse(response); return null; } - return buildMessage(response); + return buildMessage(response, type); } } catch (IOException e) { e.printStackTrace(); @@ -68,7 +68,7 @@ private static void logFailedResponse(Response response) { } } - private static @Nullable OverlayMessagingMessage buildMessage(Response response) { + private static @Nullable OverlayMessagingMessage buildMessage(Response response, OverlayMessagingMessage.MessageType type) { try { JSONObject json = new JSONObject(response.body().string()); long id = json.getLong("id"); @@ -79,7 +79,7 @@ private static void logFailedResponse(Response response) { content.put("ver", 1); content.put("html", html); - return new OverlayMessagingMessage(id, content); + return new OverlayMessagingMessage(id, content, type); } catch (NullPointerException | JSONException | IOException e) { Optimobile.log(TAG, e.getMessage()); return null; From 5441f91256053b7af4a6efbded0f32273500cde1 Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Mon, 30 Mar 2026 14:19:22 +0100 Subject: [PATCH 05/29] wire OM view to manager + edge fixes --- .../android/optimobile/BaseMessageView.java | 12 +- .../optimobile/InAppMessagePresenter.java | 5 + .../android/optimobile/InAppMessageView.java | 5 + .../optimobile/OverlayMessagingManager.java | 107 ++++++++++++++++-- .../optimobile/OverlayMessagingView.java | 36 ++++-- 5 files changed, 140 insertions(+), 25 deletions(-) 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 index 2d6801b1..ae5af569 100644 --- 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 @@ -98,6 +98,7 @@ private enum State { @UiThread void dispose() { + if (state == State.DISPOSED) return; closeDialog(currentActivity); state = State.DISPOSED; } @@ -314,7 +315,7 @@ public void onReceivedHttpError(WebView view, WebResourceRequest request, WebRes view.clearCache(true); } - closeDialog(currentActivity); + onViewError(); } catch (Optimobile.PartialInitialisationException e) { Optimobile.log(TAG, "Cannot handle HTTP error: credentials not yet available"); } @@ -323,12 +324,12 @@ public void onReceivedHttpError(WebView view, WebResourceRequest request, WebRes @Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { handler.cancel(); - closeDialog(currentActivity); + onViewError(); } @Override public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) { - closeDialog(currentActivity); + onViewError(); // Allow app to keep running, don't terminate return true; @@ -350,7 +351,7 @@ public void onReceivedError(WebView view, int errorCode, String description, Str return; } - closeDialog(currentActivity); + onViewError(); } @Override @@ -488,4 +489,7 @@ private Pair determineNotchPositions(Window window, List abstract protected void onMessageOpened(); abstract protected void onExecuteActions(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 37c629f1..0b6ecf1d 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 @@ -141,6 +141,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 2568d1f0..1a5f47e3 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 @@ -257,6 +257,11 @@ protected void onMessageClosedByClient() { presenter.messageClosed(); } + @Override + protected void onViewError() { + presenter.onViewError(); + } + @Override protected void onMessageCloseRequested(MessageCloseSource source) { // this happens when we told IAR to close message 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 index 995d0c37..73820ebc 100644 --- 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 @@ -1,5 +1,6 @@ package com.optimove.android.optimobile; +import android.app.Activity; import android.content.Context; import androidx.annotation.NonNull; @@ -16,7 +17,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -class OverlayMessagingManager { +class OverlayMessagingManager implements AppStateWatcher.AppStateChangedListener { private static final int SESSION_SLOT_CAPACITY = 1; private static final int IMMEDIATE_SLOT_CAPACITY = 1; @@ -44,14 +45,20 @@ String toEventValue() { @Nullable private OptimoveOverlayMessaging.OverlayMessagingInterceptor interceptor; + @Nullable + private OverlayMessagingView currentView; + @Nullable + private Activity currentActivity; private int sessionSlotCount = 0; private int immediateSlotCount = 0; OverlayMessagingManager(Context context) { this.context = context.getApplicationContext(); + OptimobileInitProvider.getAppStateWatcher().registerListener(this); } + @UiThread void setInterceptor(@Nullable OptimoveOverlayMessaging.OverlayMessagingInterceptor interceptor) { this.interceptor = interceptor; @@ -106,7 +113,7 @@ private void onMessageLoaded(OverlayMessagingMessage.MessageType type, @Nullable private void processMessage(OverlayMessagingMessage message) { if (interceptor == null) { displayQueue.add(message); - // TODO: notify OverlayMessagingView to display next message + maybeShowNext(); return; } @@ -147,24 +154,68 @@ private void handleInterceptorOutcome( switch (outcome) { case SHOW: displayQueue.add(message); - // TODO: notify OverlayMessagingView to display next message - trackInterceptedEvent(message.id, outcome); + maybeShowNext(); + trackInterceptedEvent(message.getId(), outcome); break; case DISCARD: - onSlotCleared(message.type); - trackInterceptedEvent(message.id, outcome); + onSlotCleared(message.getType()); + trackInterceptedEvent(message.getId(), outcome); break; case HOLD: - onSlotCleared(message.type); - trackInterceptedEvent(message.id, outcome); + onSlotCleared(message.getType()); + trackInterceptedEvent(message.getId(), outcome); break; case TIMEOUT: - onSlotCleared(message.type); - trackInterceptedEvent(message.id, outcome); + 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 onViewError(OverlayMessagingMessage failedMessage) { + currentView.dispose(); + currentView = null; + } + }); + } + private void trackInterceptedEvent(long messageId, @NonNull InterceptorOutcome outcome) { try { JSONObject props = new JSONObject(); @@ -175,4 +226,40 @@ private void trackInterceptedEvent(long messageId, @NonNull InterceptorOutcome o 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/OverlayMessagingView.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingView.java index 948c4b95..0f689513 100644 --- 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 @@ -20,28 +20,37 @@ class OverlayMessagingView extends BaseMessageView { private static final String BUTTON_ACTION_CLOSE_MESSAGE = "closeMessage"; private static final String BUTTON_ACTION_OPEN_URL = "openUrl"; + interface Listener { + @UiThread void onMessageClosed(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 Activity currentActivity, + @NonNull String iarUrl, + @NonNull Listener listener) { super(currentActivity); this.currentMessage = message; + this.listener = listener; showWebView(currentActivity, iarUrl); } -// @UiThread -// void showMessage(@NonNull OverlayMessagingMessage message) { -// if (currentMessage.getInAppId() == message.getInAppId()) { -// return; -// } -// currentMessage = message; -// sendCurrentMessageToClient(); -// } + @UiThread + void showMessage(@NonNull OverlayMessagingMessage message) { + if (currentMessage.getId() == message.getId()) { + return; + } + currentMessage = message; + sendCurrentMessageToClient(); + } @UiThread private void executeActions(Activity currentActivity, List actions) { @@ -142,7 +151,12 @@ protected JSONObject getCurrentMessageContent() { @Override protected void onMessageClosedByClient() { - // TODO: free slot, present next message if have + listener.onMessageClosed(currentMessage); + } + + @Override + protected void onViewError() { + listener.onViewError(currentMessage); } @Override From 61d11f0ee84d5e793e2928f87b9475352f67f4ee Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Mon, 30 Mar 2026 14:44:42 +0100 Subject: [PATCH 06/29] close dialog as soon as error, dont rely on children --- .../android/optimobile/BaseMessageView.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) 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 index ae5af569..99e7ecff 100644 --- 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 @@ -103,6 +103,13 @@ void dispose() { state = State.DISPOSED; } + @UiThread + void handleViewError(){ + dispose(); + + onViewError(); + } + @UiThread protected void sendCurrentMessageToClient() { if (state == State.READY && pageFinished) { @@ -315,7 +322,7 @@ public void onReceivedHttpError(WebView view, WebResourceRequest request, WebRes view.clearCache(true); } - onViewError(); + handleViewError(); } catch (Optimobile.PartialInitialisationException e) { Optimobile.log(TAG, "Cannot handle HTTP error: credentials not yet available"); } @@ -324,12 +331,12 @@ public void onReceivedHttpError(WebView view, WebResourceRequest request, WebRes @Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { handler.cancel(); - onViewError(); + handleViewError(); } @Override public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) { - onViewError(); + handleViewError(); // Allow app to keep running, don't terminate return true; @@ -351,7 +358,7 @@ public void onReceivedError(WebView view, int errorCode, String description, Str return; } - onViewError(); + handleViewError(); } @Override From 0273ba265ab0b20b5688d5352b41613d116a0b9b Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Mon, 30 Mar 2026 15:58:38 +0100 Subject: [PATCH 07/29] track clicked and dismissed events --- .../android/optimobile/AnalyticsContract.java | 2 + .../optimobile/OverlayMessagingManager.java | 29 +++++++++ .../optimobile/OverlayMessagingView.java | 61 ++++++++++--------- 3 files changed, 64 insertions(+), 28 deletions(-) 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 4f386ebe..c3d3c5f3 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 @@ -46,6 +46,8 @@ final class AnalyticsContract { 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_CLICKED = "optimove.om.clicked"; + 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/OverlayMessagingManager.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingManager.java index 73820ebc..273a1c82 100644 --- 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 @@ -208,6 +208,16 @@ public void onMessageClosed(OverlayMessagingMessage closedMessage) { maybeShowNext(); } + @Override + public void onClicked(OverlayMessagingMessage message, JSONObject props) { + trackClickedEvent(message.getId(), props); + } + + @Override + public void onDismissed(OverlayMessagingMessage message) { + trackDismissedEvent(message.getId()); + } + @Override public void onViewError(OverlayMessagingMessage failedMessage) { currentView.dispose(); @@ -216,6 +226,25 @@ public void onViewError(OverlayMessagingMessage failedMessage) { }); } + private void trackClickedEvent(long messageId, JSONObject props) { + try { + props.put("id", messageId); + Optimobile.trackEventImmediately(context, AnalyticsContract.EVENT_TYPE_OM_CLICKED, props); + } 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(); 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 index 0f689513..b59b8462 100644 --- 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 @@ -8,6 +8,7 @@ import androidx.annotation.UiThread; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; @@ -22,6 +23,8 @@ class OverlayMessagingView extends BaseMessageView { interface Listener { @UiThread void onMessageClosed(OverlayMessagingMessage message); + @UiThread void onClicked(OverlayMessagingMessage message, JSONObject props); + @UiThread void onDismissed(OverlayMessagingMessage message); @UiThread void onViewError(OverlayMessagingMessage message); } @@ -58,6 +61,7 @@ private void executeActions(Activity currentActivity, List act for (ExecutableAction action : actions) { switch (action.getType()) { case BUTTON_ACTION_CLOSE_MESSAGE: + fireClickedEvent(true); closeCurrentMessage(MessageCloseSource.CLICK); break; } @@ -67,14 +71,24 @@ private void executeActions(Activity currentActivity, List act for (ExecutableAction action : actions) { switch (action.getType()) { case BUTTON_ACTION_OPEN_URL: - //presenter.cancelCurrentPresentationQueue(); - + // TODO: this should close current message? + fireClickedEvent(false); this.openUrl(currentActivity, action.getUrl()); return; } } } + private void fireClickedEvent(boolean closing) { + try { + JSONObject props = new JSONObject(); + props.put("closing", closing); + listener.onClicked(currentMessage, props); + } catch (JSONException e) { + e.printStackTrace(); + } + } + private void openUrl(Activity currentActivity, String uri) { Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri)); @@ -119,26 +133,13 @@ private List parseButtonActionData(@NonNull JSONObject data) { private static class ExecutableAction { String type; - String url; - void setType(String type) { - this.type = type; - } - - void setUrl(String url) { - this.url = url; - } - - - String getType() { - return type; - } - - String getUrl() { - return url; - } + void setType(String type) { this.type = type; } + void setUrl(String url) { this.url = url; } + String getType() { return type; } + String getUrl() { return url; } } @@ -150,22 +151,26 @@ protected JSONObject getCurrentMessageContent() { } @Override - protected void onMessageClosedByClient() { - listener.onMessageClosed(currentMessage); + protected void onViewError() { + listener.onViewError(currentMessage); } + @Override - protected void onViewError() { - listener.onViewError(currentMessage); + protected void onMessageClosedByClient() { + listener.onMessageClosed(currentMessage); } @Override protected void onMessageCloseRequested(MessageCloseSource source) { - // TODO: do we need to differentiate these? - // 1. back button -> dismissed - // 2. close action -> clicked - - //TODO: report event + switch (source) { + case CLICK: + // event already tracked when closing click action executed + break; + case HARDWARE: + listener.onDismissed(currentMessage); + break; + } } @Override From 5abfe8141aecc90be62070f348bc79d6525a3a20 Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Mon, 30 Mar 2026 16:04:59 +0100 Subject: [PATCH 08/29] bump version + changelog --- CHANGELOG.md | 11 +++++++---- OptimoveSDK/gradle.properties | 6 ++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffc410df..7b58bfca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,19 @@ # Changelog + +## 7.13.0 + +- Implementation for Overlay Messaging channel. Check optimove developer docs for more. + ## 7.12.0 - Add `postpone()` to the In-App Message Interceptor API. When called, the message is not displayed and not marked as dismissed — it is moved to the back of the queue and will be re-intercepted on the next natural presentation trigger (e.g. app foreground, push tickle). Existing `show()` and `suppress()` behavior is unchanged. - 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. @@ -19,7 +22,6 @@ - Minor bug fixes for Embedded Messaging: correct field mapping - ## 7.10.1 - Bumped GSON version number to fix a vulnerability issue @@ -39,7 +41,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/gradle.properties b/OptimoveSDK/gradle.properties index a13127ed..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.0 -sdk_version_code=71200 + +sdk_version=7.13.0 +sdk_version_code=71300 + sdk_platform=Android android.useAndroidX=true android.enableJetifier=true From e449f3653d1b6b0473d0948b4740ea3d7047a8c3 Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Mon, 30 Mar 2026 16:30:57 +0100 Subject: [PATCH 09/29] OM is not a standalone feature + make inapp vs OM mutually exclusive --- .../java/com/optimove/android/OptimoveConfig.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) 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 306750a1..4b5c9714 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,7 @@ public final class OptimoveConfig { private @Nullable EmbeddedMessagingConfig embeddedMessagingConfig; + private boolean overlayMessagingEnabled; private @Nullable Integer overlayMessagingSessionLengthMinutes; public enum InAppConsentStrategy { @@ -111,7 +112,6 @@ private enum Feature { OPTIMOBILE, PREFERENCE_CENTER, EMBEDDED_MESSAGING, - OVERLAY_MESSAGING, } Set features = new HashSet<>(); @@ -140,11 +140,6 @@ public FeatureSet withEmbeddedMessaging() { return this; } - public FeatureSet withOverlayMessaging() { - features.add(Feature.OVERLAY_MESSAGING); - - return this; - } boolean has(Feature feature) { @@ -425,7 +420,7 @@ public boolean usesDelayedConfiguration() { } public boolean isOverlayMessagingEnabled() { - return this.featureSet.has(FeatureSet.Feature.OVERLAY_MESSAGING); + return this.overlayMessagingEnabled; } public int getOverlayMessagingSessionLengthMinutes() { @@ -602,7 +597,6 @@ public Builder enableOverlayMessaging(int sessionLengthMinutes) { throw new IllegalArgumentException("OverlayMmessaging: optimobile feature required"); } this.overlayMessagingSessionLengthMinutes = sessionLengthMinutes; - this.featureSet.withOverlayMessaging(); return this; } @@ -660,6 +654,10 @@ public Builder setBaseUrlMapping(Map baseUrlMap) { } public OptimoveConfig build() { + if (this.consentStrategy != null && this.overlayMessagingSessionLengthMinutes != null) { + throw new IllegalStateException("enableInAppMessaging and enableOverlayMessaging are mutually exclusive"); + } + OptimoveConfig newConfig = new OptimoveConfig(); newConfig.setFeatureSet(this.featureSet); newConfig.setDelayedInitialisation(delayedInitialisation); @@ -696,6 +694,7 @@ public OptimoveConfig build() { newConfig.setMinLogLevel(this.minLogLevel); + newConfig.overlayMessagingEnabled = this.overlayMessagingSessionLengthMinutes != null; newConfig.overlayMessagingSessionLengthMinutes = this.overlayMessagingSessionLengthMinutes; return newConfig; From d053b5d0db966266e635dece91ed052e74b0f955 Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Mon, 30 Mar 2026 16:36:00 +0100 Subject: [PATCH 10/29] fix warnings in base view -- we are minsdk 21 long ago --- .../android/optimobile/BaseMessageView.java | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) 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 index 99e7ecff..837415e4 100644 --- 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 @@ -143,24 +143,15 @@ protected void sendToClient(String type, JSONObject data) { String script = "window.postHostMessage(" + j.toString() + ")"; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - wv.evaluateJavascript(script, null); - } else { - wv.loadUrl("javascript:" + script); - } + wv.evaluateJavascript(script, null); } @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(); @@ -184,7 +175,6 @@ private void setStatusBarColorForDialog(Activity currentActivity) { } @UiThread - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private void unsetStatusBarColorForDialog(Activity dialogActivity) { if (dialogActivity == null) { return; @@ -207,9 +197,8 @@ private void closeDialog(Activity dialogActivity) { if (dialog != null) { dialog.setOnKeyListener(null); dialog.dismiss(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - unsetStatusBarColorForDialog(dialogActivity); - } + + unsetStatusBarColorForDialog(dialogActivity); } if (null != wv) { @@ -225,7 +214,7 @@ private void closeDialog(Activity dialogActivity) { @UiThread protected void showWebView(@NonNull final Activity currentActivity, @NonNull String iarUrl) { try { - if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (BuildConfig.DEBUG) { WebView.setWebContentsDebuggingEnabled(true); } @@ -268,9 +257,9 @@ protected void showWebView(@NonNull final Activity currentActivity, @NonNull Str WebSettings settings = wv.getSettings(); settings.setJavaScriptEnabled(true); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - settings.setMediaPlaybackRequiresUserGesture(false); - } + + settings.setMediaPlaybackRequiresUserGesture(false); + wv.addJavascriptInterface(this, JS_NAME); wv.setWebViewClient(this); @@ -302,7 +291,6 @@ public void onPageFinished(WebView view, String url) { } @Override - @TargetApi(Build.VERSION_CODES.LOLLIPOP) public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { super.onReceivedHttpError(view, request, errorResponse); @@ -362,7 +350,7 @@ public void onReceivedError(WebView view, int errorCode, String description, Str } @Override - @TargetApi(Build.VERSION_CODES.M) + @RequiresApi(Build.VERSION_CODES.M) public void onReceivedError(WebView view, WebResourceRequest req, WebResourceError rerr) { onReceivedError(view, rerr.getErrorCode(), rerr.getDescription().toString(), req.getUrl().toString()); } @@ -439,7 +427,7 @@ private void maybeSetNotchInsets(Context context) { } List cutoutBoundingRectangles = displayCutout.getBoundingRects(); - if (cutoutBoundingRectangles.size() == 0) { + if (cutoutBoundingRectangles.isEmpty()) { return; } From e1ad284f7c032b6d90c9867596c6746b4b161e6c Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Mon, 30 Mar 2026 16:46:16 +0100 Subject: [PATCH 11/29] apply security suggestion from wiz --- .../java/com/optimove/android/optimobile/BaseMessageView.java | 2 ++ 1 file changed, 2 insertions(+) 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 index 837415e4..f863ecd7 100644 --- 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 @@ -257,6 +257,8 @@ protected void showWebView(@NonNull final Activity currentActivity, @NonNull Str WebSettings settings = wv.getSettings(); settings.setJavaScriptEnabled(true); + settings.setAllowFileAccess(false); + settings.setAllowContentAccess(false); settings.setMediaPlaybackRequiresUserGesture(false); From 6898f3cf74552064cc161dc23626bca8c585b6a6 Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Wed, 1 Apr 2026 00:24:25 +0100 Subject: [PATCH 12/29] ui for OM in test app + rename hold->defer + explicitly handle 204 --- .../android/optimovemobilesdk/MainActivity.kt | 10 + .../OverlayMessagingActivity.kt | 191 ++++++++++++++++++ .../optimovemobilesdk/ui/MainScreen.kt | 13 ++ .../optimobile/OptimoveOverlayMessaging.java | 4 +- .../optimobile/OverlayMessagingManager.java | 16 +- .../OverlayMessagingRequestService.java | 3 + 6 files changed, 223 insertions(+), 14 deletions(-) create mode 100644 OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/OverlayMessagingActivity.kt diff --git a/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/MainActivity.kt b/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/MainActivity.kt index 7f0f7acb..9394fe96 100644 --- a/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/MainActivity.kt +++ b/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/MainActivity.kt @@ -60,6 +60,7 @@ class MainActivity : AppCompatActivity() { val config = Optimove.getConfig() val showPreferenceCenter = config.isPreferenceCenterConfigured val showEmbeddedMessaging = config.isEmbeddedMessagingConfigured + val showOverlayMessaging = config.isOverlayMessagingEnabled val showDelayedConfig = config.usesDelayedConfiguration() if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { @@ -82,6 +83,7 @@ class MainActivity : AppCompatActivity() { outputText = outputText, showPreferenceCenter = showPreferenceCenter, showEmbeddedMessaging = showEmbeddedMessaging, + showOverlayMessaging = showOverlayMessaging, showDelayedConfig = showDelayedConfig, credentialsSubmitted = credentialsSubmitted, isInterceptingInApp = isInterceptingInApp, @@ -99,6 +101,7 @@ class MainActivity : AppCompatActivity() { onGetPreferences = ::getPreferences, onSetPreferences = ::setPreferences, onViewEmbeddedMessaging = ::viewEmbeddedMessaging, + onViewOverlayMessaging = ::viewOverlayMessaging, onSetCredentials = ::setCredentials, onEnableInAppInterceptionClicked = ::enableInAppInterceptionClicked, onResetToken = {}, @@ -220,6 +223,9 @@ class MainActivity : AppCompatActivity() { items.forEach { OptimoveInApp.getInstance().deleteMessageFromInbox(it) } } + + + private fun getPreferences() { OptimovePreferenceCenter.getInstance().getPreferencesAsync { result, preferences -> when (result) { @@ -264,6 +270,10 @@ class MainActivity : AppCompatActivity() { startActivity(Intent(this, EmbeddedMessagingActivity::class.java)) } + private fun viewOverlayMessaging() { + startActivity(Intent(this, OverlayMessagingActivity::class.java)) + } + private fun openDeeplinkTest() { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(DeeplinkTargetActivity.DEEPLINK_TEST_URI))) } 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..c32ed1ff --- /dev/null +++ b/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/OverlayMessagingActivity.kt @@ -0,0 +1,191 @@ +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 + } + + 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 945ad81b..29ee40a0 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 @@ -40,6 +40,7 @@ fun MainScreen( outputText: String, showPreferenceCenter: Boolean, showEmbeddedMessaging: Boolean, + showOverlayMessaging: Boolean, showDelayedConfig: Boolean, credentialsSubmitted: Boolean, isInterceptingInApp: Boolean, @@ -57,6 +58,7 @@ fun MainScreen( onGetPreferences: () -> Unit, onSetPreferences: () -> Unit, onViewEmbeddedMessaging: () -> Unit, + onViewOverlayMessaging: () -> Unit, onSetCredentials: (optimove: String?, optimobile: String?, prefCenter: String?) -> Unit, onEnableInAppInterceptionClicked: () -> Unit, onResetToken: () -> Unit, @@ -258,6 +260,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/optimove-sdk/src/main/java/com/optimove/android/optimobile/OptimoveOverlayMessaging.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OptimoveOverlayMessaging.java index 98c1ecc1..535276d4 100644 --- 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 @@ -1,8 +1,6 @@ package com.optimove.android.optimobile; import android.app.Application; -import android.content.Context; -import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -18,7 +16,7 @@ public class OptimoveOverlayMessaging { public interface OverlayMessagingInterceptorCallback { @UiThread void show(); @UiThread void discard(); - @UiThread void hold(); + @UiThread void defer(); } public interface OverlayMessagingInterceptor { 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 index 273a1c82..46c1d9ef 100644 --- 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 @@ -25,14 +25,14 @@ class OverlayMessagingManager implements AppStateWatcher.AppStateChangedListener enum InterceptorOutcome { SHOW, DISCARD, - HOLD, + DEFER, TIMEOUT; String toEventValue() { switch (this) { case SHOW: return "shown"; case DISCARD: return "discarded"; - case HOLD: return "held"; + case DEFER: return "deferred"; case TIMEOUT: return "timeout"; default: throw new IllegalStateException("Unhandled outcome: " + this); } @@ -133,9 +133,9 @@ public void discard() { } @Override - public void hold() { + public void defer() { if (!processed.compareAndSet(false, true)) return; - Optimobile.handler.post(() -> handleInterceptorOutcome(message, InterceptorOutcome.HOLD)); + Optimobile.handler.post(() -> handleInterceptorOutcome(message, InterceptorOutcome.DEFER)); } }; @@ -158,13 +158,7 @@ private void handleInterceptorOutcome( trackInterceptedEvent(message.getId(), outcome); break; case DISCARD: - onSlotCleared(message.getType()); - trackInterceptedEvent(message.getId(), outcome); - break; - case HOLD: - onSlotCleared(message.getType()); - trackInterceptedEvent(message.getId(), outcome); - break; + case DEFER: case TIMEOUT: onSlotCleared(message.getType()); trackInterceptedEvent(message.getId(), outcome); 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 index 0d0f518a..5f7eb494 100644 --- 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 @@ -35,6 +35,9 @@ class OverlayMessagingRequestService { region, encodedIdentifier, tenantId, brandId, messageType); try (Response response = httpClient.getSync(url)) { + if (response.code() == 204) { + return null; + } if (!response.isSuccessful()) { logFailedResponse(response); return null; From ee7d76937811d8ebe78f36f2cddaff2fec7c72a9 Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Wed, 1 Apr 2026 17:17:37 +0100 Subject: [PATCH 13/29] events coming from IAR --- .../android/optimobile/BaseMessageView.java | 8 +- .../android/optimobile/InAppMessageView.java | 7 +- .../optimobile/OverlayMessagingManager.java | 29 ++-- .../OverlayMessagingRendererEvent.java | 37 +++++ .../optimobile/OverlayMessagingView.java | 152 +++++++----------- 5 files changed, 127 insertions(+), 106 deletions(-) create mode 100644 OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingRendererEvent.java 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 index f863ecd7..298b7477 100644 --- 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 @@ -53,7 +53,7 @@ abstract class BaseMessageView extends WebViewClient { protected enum MessageCloseSource { HARDWARE, - CLICK + CLIENT } private enum State { @@ -399,7 +399,9 @@ public void postClientMessage(String msg) { return; case "EXECUTE_ACTIONS": onExecuteActions(data); - + return; + case "COMMAND": + onCommand(data); return; default: Log.d(TAG, "Unknown message type: " + messageType); @@ -487,6 +489,8 @@ private Pair determineNotchPositions(Window window, List 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/InAppMessageView.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/InAppMessageView.java index 1a5f47e3..c14e998e 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 @@ -64,7 +64,7 @@ private void executeActions(Activity currentActivity, List act for (ExecutableAction action : actions) { switch (action.getType()) { case BUTTON_ACTION_CLOSE_MESSAGE: - closeCurrentMessage(MessageCloseSource.CLICK); + closeCurrentMessage(MessageCloseSource.CLIENT); break; case BUTTON_ACTION_TRACK_CONVERSION_EVENT: Optimobile.trackEventImmediately(currentActivity, action.getEventType(), action.getConversionEventData()); @@ -262,6 +262,11 @@ 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 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 index 46c1d9ef..54e713c1 100644 --- 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 @@ -10,6 +10,8 @@ import org.json.JSONException; import org.json.JSONObject; +import java.util.List; + import java.util.ArrayDeque; import java.util.Queue; import java.util.concurrent.Executors; @@ -68,12 +70,16 @@ void setInterceptor(@Nullable OptimoveOverlayMessaging.OverlayMessagingIntercept void onTriggerReceived(OverlayMessagingMessage.MessageType type) { switch (type) { case SESSION: - if (sessionSlotCount >= SESSION_SLOT_CAPACITY) return; + if (sessionSlotCount >= SESSION_SLOT_CAPACITY) { + return; + } sessionSlotCount++; loadMessage(type); break; case IMMEDIATE: - if (immediateSlotCount >= IMMEDIATE_SLOT_CAPACITY) return; + if (immediateSlotCount >= IMMEDIATE_SLOT_CAPACITY) { + return; + } immediateSlotCount++; loadMessage(type); break; @@ -203,8 +209,8 @@ public void onMessageClosed(OverlayMessagingMessage closedMessage) { } @Override - public void onClicked(OverlayMessagingMessage message, JSONObject props) { - trackClickedEvent(message.getId(), props); + public void onEvents(OverlayMessagingMessage message, List events) { + trackOverlayMessagingRendererEvents(message.getId(), events); } @Override @@ -220,12 +226,15 @@ public void onViewError(OverlayMessagingMessage failedMessage) { }); } - private void trackClickedEvent(long messageId, JSONObject props) { - try { - props.put("id", messageId); - Optimobile.trackEventImmediately(context, AnalyticsContract.EVENT_TYPE_OM_CLICKED, props); - } catch (JSONException e) { - e.printStackTrace(); + 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(); + } } } 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..32c30f14 --- /dev/null +++ b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingRendererEvent.java @@ -0,0 +1,37 @@ +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/OverlayMessagingView.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingView.java index b59b8462..e50cfb29 100644 --- 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 @@ -5,25 +5,44 @@ import android.net.Uri; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.UiThread; import org.json.JSONArray; -import org.json.JSONException; import org.json.JSONObject; -import java.util.ArrayList; import java.util.List; class OverlayMessagingView extends BaseMessageView { - private static final String TAG = OverlayMessagingView.class.getName(); + private static final String SDK_ACTION_OPEN_DEEP_LINK = "OPEN_DEEP_LINK"; - private static final String BUTTON_ACTION_CLOSE_MESSAGE = "closeMessage"; - private static final String BUTTON_ACTION_OPEN_URL = "openUrl"; + 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 onClicked(OverlayMessagingMessage message, JSONObject props); + @UiThread void onEvents(OverlayMessagingMessage message, List events); @UiThread void onDismissed(OverlayMessagingMessage message); @UiThread void onViewError(OverlayMessagingMessage message); } @@ -55,41 +74,6 @@ void showMessage(@NonNull OverlayMessagingMessage message) { sendCurrentMessageToClient(); } - @UiThread - private void executeActions(Activity currentActivity, List actions) { - // Handle 'secondary' actions - for (ExecutableAction action : actions) { - switch (action.getType()) { - case BUTTON_ACTION_CLOSE_MESSAGE: - fireClickedEvent(true); - closeCurrentMessage(MessageCloseSource.CLICK); - break; - } - } - - // Handle 'terminating' actions - for (ExecutableAction action : actions) { - switch (action.getType()) { - case BUTTON_ACTION_OPEN_URL: - // TODO: this should close current message? - fireClickedEvent(false); - this.openUrl(currentActivity, action.getUrl()); - return; - } - } - } - - private void fireClickedEvent(boolean closing) { - try { - JSONObject props = new JSONObject(); - props.put("closing", closing); - listener.onClicked(currentMessage, props); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - private void openUrl(Activity currentActivity, String uri) { Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri)); if (browserIntent.resolveActivity(currentActivity.getPackageManager()) != null) { @@ -97,51 +81,6 @@ private void openUrl(Activity currentActivity, String uri) { } } - private List parseButtonActionData(@NonNull JSONObject data) { - List actions = new ArrayList<>(); - JSONArray rawActions = data.optJSONArray("actions"); - - if (null == rawActions) { - return actions; - } - - for (int i = 0; i < rawActions.length(); i++) { - JSONObject rawAction = rawActions.optJSONObject(i); - - String actionType = rawAction.optString("type"); - JSONObject rawActionData = rawAction.optJSONObject("data"); - - ExecutableAction action = new ExecutableAction(); - action.setType(actionType); - - switch (actionType) { - case BUTTON_ACTION_OPEN_URL: - if (null == rawActionData) { - continue; - } - String url = rawActionData.optString("url"); - action.setUrl(url); - break; - default: - break; - } - actions.add(action); - } - - return actions; - } - - private static class ExecutableAction { - String type; - String url; - - void setType(String type) { this.type = type; } - void setUrl(String url) { this.url = url; } - - String getType() { return type; } - String getUrl() { return url; } - } - // - Implementations for abstracts @@ -155,7 +94,6 @@ protected void onViewError() { listener.onViewError(currentMessage); } - @Override protected void onMessageClosedByClient() { listener.onMessageClosed(currentMessage); @@ -164,8 +102,8 @@ protected void onMessageClosedByClient() { @Override protected void onMessageCloseRequested(MessageCloseSource source) { switch (source) { - case CLICK: - // event already tracked when closing click action executed + case CLIENT: + // events already tracked when COMMAND handled break; case HARDWARE: listener.onDismissed(currentMessage); @@ -180,13 +118,41 @@ protected void onMessageOpened() { @Override protected void onExecuteActions(JSONObject data) { - if (null == data) { + // noop: OM uses COMMAND, not EXECUTE_ACTIONS + } + + @Override + protected void onCommand(JSONObject data) { + RendererCommand command = RendererCommand.parse(data); + if (command == null) { return; } - List actions = this.parseButtonActionData(data); - currentActivity.runOnUiThread(() -> this.executeActions(currentActivity, actions)); - } + 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; + } + } + }); + } } From 812224ce2dfa54720cfcc535c28b67fd7ddb1074 Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Thu, 2 Apr 2026 00:07:28 +0100 Subject: [PATCH 14/29] dispose view on hard crashes in IAR -- to prevent highly improbably stuck LOADING state --- .../java/com/optimove/android/optimobile/BaseMessageView.java | 3 +++ 1 file changed, 3 insertions(+) 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 index 298b7477..6b47da0a 100644 --- 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 @@ -403,6 +403,9 @@ public void postClientMessage(String msg) { case "COMMAND": onCommand(data); return; + case "PRESENTATION_ERROR": + currentActivity.runOnUiThread(this::handleViewError); + return; default: Log.d(TAG, "Unknown message type: " + messageType); } From a06569282e52f68d4100409e9e12de4d78fcf871 Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Thu, 2 Apr 2026 13:07:04 +0100 Subject: [PATCH 15/29] proper push triggers for immediate messages --- .../optimobile/PushBroadcastReceiver.java | 4 ++-- .../android/optimobile/PushMessage.java | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) 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 45b7505d..8dc4e542 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 @@ -107,8 +107,8 @@ protected void maybeTriggerOverlayMessagingSync(Context context, PushMessage pus if (!OptimoveOverlayMessaging.getInstance().isOverlayMessagingEnabled()) { return; } - // TODO -- where in PushMessage does it actually sit? - if (!pushMessage.getTitle().equals("OM")){ + + if (!pushMessage.isOverlayMessagingTrigger()){ return; } 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..0c60fa52 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 = false; 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) { @@ -215,6 +235,10 @@ int getTickleId() { return tickleId; } + boolean isOverlayMessagingTrigger() { + return isOverlayMessagingTrigger; + } + public boolean runBackgroundHandler() { return runBackgroundHandler; } From 59d91f53deba15bd8887f158fbf2dbc84658ff0e Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Mon, 6 Apr 2026 13:17:54 +0100 Subject: [PATCH 16/29] fix typo + match OM messages endpoint changes + session length min 1h + fix immedate tickle + remove hardcoded values --- .../com/optimove/android/OptimoveConfig.java | 26 +++++++-------- .../android/optimobile/Optimobile.java | 2 +- .../optimobile/OptimoveOverlayMessaging.java | 8 ++--- .../optimobile/OverlayMessagingMessage.java | 10 +++++- .../OverlayMessagingRequestService.java | 32 ++++++++----------- .../OverlayMessagingSessionManager.java | 5 +-- .../android/optimobile/PushMessage.java | 3 +- .../android/optimobile/UrlBuilder.java | 26 ++++++++++++++- 8 files changed, 68 insertions(+), 44 deletions(-) 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 4b5c9714..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 @@ -74,7 +74,7 @@ public final class OptimoveConfig { private @Nullable EmbeddedMessagingConfig embeddedMessagingConfig; private boolean overlayMessagingEnabled; - private @Nullable Integer overlayMessagingSessionLengthMinutes; + private @Nullable Integer overlayMessagingSessionLengthHours; public enum InAppConsentStrategy { AUTO_ENROLL, @@ -423,8 +423,8 @@ public boolean isOverlayMessagingEnabled() { return this.overlayMessagingEnabled; } - public int getOverlayMessagingSessionLengthMinutes() { - return this.overlayMessagingSessionLengthMinutes; + public int getOverlayMessagingSessionLengthHours() { + return this.overlayMessagingSessionLengthHours; } private boolean hasFinishedInitialisation() { @@ -486,7 +486,7 @@ public static class Builder { private @Nullable LogLevel minLogLevel; - private @Nullable Integer overlayMessagingSessionLengthMinutes; + private @Nullable Integer overlayMessagingSessionLengthHours; /** * @deprecated Use {@link Builder#Builder(FeatureSet)} instead @@ -589,14 +589,14 @@ public Builder enableEmbeddedMessaging(@NonNull String embeddedMessagingConfigur return this; } - public Builder enableOverlayMessaging(int sessionLengthMinutes) { - if (sessionLengthMinutes <= 0) { - throw new IllegalArgumentException("OverlayMessaging: sessionLengthMinutes must be greater than 0"); + 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("OverlayMmessaging: optimobile feature required"); + throw new IllegalArgumentException("OverlayMessaging: optimobile feature required"); } - this.overlayMessagingSessionLengthMinutes = sessionLengthMinutes; + this.overlayMessagingSessionLengthHours = sessionLengthHours; return this; } @@ -654,10 +654,6 @@ public Builder setBaseUrlMapping(Map baseUrlMap) { } public OptimoveConfig build() { - if (this.consentStrategy != null && this.overlayMessagingSessionLengthMinutes != null) { - throw new IllegalStateException("enableInAppMessaging and enableOverlayMessaging are mutually exclusive"); - } - OptimoveConfig newConfig = new OptimoveConfig(); newConfig.setFeatureSet(this.featureSet); newConfig.setDelayedInitialisation(delayedInitialisation); @@ -694,8 +690,8 @@ public OptimoveConfig build() { newConfig.setMinLogLevel(this.minLogLevel); - newConfig.overlayMessagingEnabled = this.overlayMessagingSessionLengthMinutes != null; - newConfig.overlayMessagingSessionLengthMinutes = this.overlayMessagingSessionLengthMinutes; + newConfig.overlayMessagingEnabled = this.overlayMessagingSessionLengthHours != null; + newConfig.overlayMessagingSessionLengthHours = this.overlayMessagingSessionLengthHours; return newConfig; } 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 5455fe49..0479d1a2 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 @@ -119,7 +119,7 @@ public static synchronized void initialize(final Application application, Optimo //TODO: move to optimove? if (config.isOverlayMessagingEnabled()) { - OptimoveOverlayMessaging.initialize(application, config.getOverlayMessagingSessionLengthMinutes()); + OptimoveOverlayMessaging.initialize(application, config.getOverlayMessagingSessionLengthHours()); } if (config.getDeferredDeepLinkHandler() != null) { 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 index 535276d4..c804e987 100644 --- 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 @@ -28,11 +28,11 @@ default long getTimeoutMs() { } } - private OptimoveOverlayMessaging(@NonNull Application application, long sessionLengthMinutes) { + private OptimoveOverlayMessaging(@NonNull Application application, long sessionLengthHours) { this.manager = new OverlayMessagingManager(application); OverlayMessagingSessionManager.Listener sessionListener = () -> manager.onTriggerReceived(OverlayMessagingMessage.MessageType.SESSION); - this.sessionManager = new OverlayMessagingSessionManager(application, sessionLengthMinutes, sessionListener); + this.sessionManager = new OverlayMessagingSessionManager(application, sessionLengthHours, sessionListener); } //============================================================================================== @@ -62,8 +62,8 @@ void onPushTriggerReceived() { manager.onTriggerReceived(OverlayMessagingMessage.MessageType.IMMEDIATE); } - static void initialize(@NonNull Application application, long sessionLengthMinutes) { - shared = new OptimoveOverlayMessaging(application, sessionLengthMinutes); + static void initialize(@NonNull Application application, long sessionLengthHours) { + shared = new OptimoveOverlayMessaging(application, sessionLengthHours); } boolean isOverlayMessagingEnabled() { 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 index 6903b3bb..c00379e3 100644 --- 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 @@ -1,6 +1,7 @@ package com.optimove.android.optimobile; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.json.JSONObject; @@ -13,11 +14,13 @@ public enum MessageType { private final long id; private final JSONObject content; + private final JSONObject data; private final MessageType type; - OverlayMessagingMessage(long id, @NonNull JSONObject content, @NonNull 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; } @@ -29,6 +32,11 @@ 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/OverlayMessagingRequestService.java b/OptimoveSDK/optimove-sdk/src/main/java/com/optimove/android/optimobile/OverlayMessagingRequestService.java index 5f7eb494..d117f60b 100644 --- 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 @@ -2,6 +2,7 @@ import android.content.Context; import android.net.Uri; +import android.util.Log; import androidx.annotation.Nullable; @@ -23,25 +24,22 @@ class OverlayMessagingRequestService { String encodedIdentifier = Uri.encode(userIdentifier); try { - //TODO: derive region, tenantId, brandId from credentials + take from urlBuilder - String region = "dev"; - int tenantId = 3013; - String brandId = "9abb8d6d-62ed-42d1-97d1-c82d15f9c1fc"; + String messageType = type == OverlayMessagingMessage.MessageType.SESSION ? "session" : "immediate"; - String messageType = type == OverlayMessagingMessage.MessageType.SESSION ? "session-start" : "immediate"; - - String url = String.format( - "http://optimobile-overlay-srv-%s.optimove.net/mobile/%s/messages?tenantId=%s&brandId=%s&messageType=%s", - region, encodedIdentifier, tenantId, brandId, messageType); + String url = Optimobile.urlForService(UrlBuilder.Service.OVERLAY_MESSAGING, + "/api/v1/users/" + encodedIdentifier + "/messages/mobile?messageType=" + messageType); try (Response response = httpClient.getSync(url)) { - if (response.code() == 204) { - return null; - } + if (!response.isSuccessful()) { logFailedResponse(response); return null; } + + if (response.code() == 204) { + return null; + } + return buildMessage(response, type); } } catch (IOException e) { @@ -75,14 +73,10 @@ private static void logFailedResponse(Response response) { try { JSONObject json = new JSONObject(response.body().string()); long id = json.getLong("id"); - String html = json.getString("html"); - - // TODO: this should be under message content in response already - JSONObject content = new JSONObject(); - content.put("ver", 1); - content.put("html", html); + JSONObject content = json.getJSONObject("content"); + JSONObject data = json.optJSONObject("data"); - return new OverlayMessagingMessage(id, content, type); + 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 index 435c3306..6bd9c8a4 100644 --- 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 @@ -34,9 +34,9 @@ public void run() { } }; - OverlayMessagingSessionManager(@NonNull Context context, long sessionLengthMinutes, @NonNull Listener listener) { + OverlayMessagingSessionManager(@NonNull Context context, long sessionLengthHours, @NonNull Listener listener) { this.handler = new Handler(Looper.getMainLooper()); - this.sessionLengthMs = sessionLengthMinutes * 60_000L; + this.sessionLengthMs = sessionLengthHours * 3_600_000L; this.listener = listener; this.prefs = context.getApplicationContext().getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE); @@ -76,6 +76,7 @@ public void appEnteredBackground() { } 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()); 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 0c60fa52..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 @@ -93,7 +93,7 @@ private PushMessage(Parcel in) { url = Uri.parse(urlString); } tickleId = in.readInt(); - isOverlayMessagingTrigger = false; + isOverlayMessagingTrigger = (in.readInt() == 1); pictureUrl = in.readString(); String buttonsString = in.readString(); @@ -193,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); 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..9defb03c 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 @@ -1,6 +1,7 @@ package com.optimove.android.optimobile; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.HashMap; import java.util.Map; @@ -14,6 +15,7 @@ public enum Service { IAR, MEDIA, PUSH, + OVERLAY_MESSAGING } private final Map baseUrlMap; @@ -37,7 +39,11 @@ String urlForService(Service service, String path) { public static Map defaultMapping(@NonNull String region) { Map baseUrlMap = new HashMap<>(Service.values().length); - baseUrlMap.put(Service.IAR, "https://iar.app.delivery"); + //baseUrlMap.put(Service.IAR, "https://iar.app.delivery"); + // TODO + baseUrlMap.put(Service.IAR, "https://optimobile-iar-dev.optimove.net"); + + baseUrlMap.put(Service.PUSH, "https://push-" + region + ".kumulos.com"); baseUrlMap.put(Service.CRM, "https://crm-" + region + ".kumulos.com"); @@ -45,7 +51,25 @@ public static Map defaultMapping(@NonNull String region) { baseUrlMap.put(Service.DDL, "https://links-" + region + ".kumulos.com"); baseUrlMap.put(Service.MEDIA, "https://i-" + region + ".app.delivery"); + // TODO: http -> https + String omRegion = mapRegionForOverlayMessaging(region); + if (omRegion != null) { + baseUrlMap.put(Service.OVERLAY_MESSAGING, "http://optimobile-overlay-srv-" + omRegion + ".optimove.net"); + } + return baseUrlMap; } + + // TODO: region. mapping? crashing app? + @Nullable + private static String mapRegionForOverlayMessaging(@NonNull String region) { + switch (region) { + case "eu-central-2": return "eu"; + case "us-east-1": return "us"; + case "uk-1": return "dev"; + default: return null; + } + } + } From 9de3319e84f232e42a24d181cd930523845b6430 Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Mon, 6 Apr 2026 13:36:57 +0100 Subject: [PATCH 17/29] bugfix: OM enablement check --- .../optimove/android/optimobile/OptimoveOverlayMessaging.java | 4 ---- .../optimove/android/optimobile/PushBroadcastReceiver.java | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) 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 index c804e987..522a3c17 100644 --- 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 @@ -65,8 +65,4 @@ void onPushTriggerReceived() { static void initialize(@NonNull Application application, long sessionLengthHours) { shared = new OptimoveOverlayMessaging(application, sessionLengthHours); } - - boolean isOverlayMessagingEnabled() { - return shared != null; - } } 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 8dc4e542..a9f61786 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 @@ -104,7 +104,7 @@ protected void onPushReceived(Context context, PushMessage pushMessage) { } protected void maybeTriggerOverlayMessagingSync(Context context, PushMessage pushMessage) { - if (!OptimoveOverlayMessaging.getInstance().isOverlayMessagingEnabled()) { + if (!Optimove.getConfig().isOverlayMessagingEnabled()) { return; } From 50d9b27f47a86f648cb71bc16e0ee2037897a69d Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Mon, 6 Apr 2026 14:27:52 +0100 Subject: [PATCH 18/29] wont use mapping in the end --- .../android/optimobile/Optimobile.java | 1 - .../android/optimobile/UrlBuilder.java | 19 ++++--------------- 2 files changed, 4 insertions(+), 16 deletions(-) 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 0479d1a2..84b004e1 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,7 +117,6 @@ public static synchronized void initialize(final Application application, Optimo OptimoveInApp.initialize(application, config); - //TODO: move to optimove? if (config.isOverlayMessagingEnabled()) { OptimoveOverlayMessaging.initialize(application, config.getOverlayMessagingSessionLengthHours()); } 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 9defb03c..b0f50b40 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 @@ -1,7 +1,6 @@ package com.optimove.android.optimobile; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import java.util.HashMap; import java.util.Map; @@ -52,24 +51,14 @@ public static Map defaultMapping(@NonNull String region) { baseUrlMap.put(Service.MEDIA, "https://i-" + region + ".app.delivery"); // TODO: http -> https - String omRegion = mapRegionForOverlayMessaging(region); - if (omRegion != null) { - baseUrlMap.put(Service.OVERLAY_MESSAGING, "http://optimobile-overlay-srv-" + omRegion + ".optimove.net"); - } + // TODO: use region once cnmaes ok + baseUrlMap.put(Service.OVERLAY_MESSAGING, "http://optimobile-overlay-srv-" + "dev" + ".optimove.net"); + return baseUrlMap; } - // TODO: region. mapping? crashing app? - @Nullable - private static String mapRegionForOverlayMessaging(@NonNull String region) { - switch (region) { - case "eu-central-2": return "eu"; - case "us-east-1": return "us"; - case "uk-1": return "dev"; - default: return null; - } - } + } From b62a51174fd1d6543b21fdaa67c908d8b271ac73 Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Mon, 6 Apr 2026 14:44:44 +0100 Subject: [PATCH 19/29] format code --- .../android/optimovemobilesdk/MainActivity.kt | 118 ++++++++++-------- .../OverlayMessagingActivity.kt | 44 ++++--- .../optimobile/OptimoveOverlayMessaging.java | 11 +- .../optimobile/OverlayMessagingManager.java | 24 ++-- .../optimobile/OverlayMessagingMessage.java | 3 +- .../OverlayMessagingRendererEvent.java | 9 +- .../OverlayMessagingRequestService.java | 3 +- .../OverlayMessagingSessionManager.java | 1 - .../optimobile/OverlayMessagingView.java | 32 ++--- .../optimobile/PushBroadcastReceiver.java | 2 +- 10 files changed, 137 insertions(+), 110 deletions(-) diff --git a/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/MainActivity.kt b/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/MainActivity.kt index 9394fe96..d8dde9d3 100644 --- a/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/MainActivity.kt +++ b/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/MainActivity.kt @@ -51,7 +51,10 @@ class MainActivity : AppCompatActivity() { super.onCreate(savedInstanceState) OptimoveInApp.getInstance().setDeepLinkHandler(object : InAppDeepLinkHandlerInterface { - override fun handle(context: android.content.Context, buttonPress: InAppDeepLinkHandlerInterface.InAppButtonPress) { + override fun handle( + context: android.content.Context, + buttonPress: InAppDeepLinkHandlerInterface.InAppButtonPress + ) { Log.d(TAG, "DeepLink handler invoked") startActivity(Intent(this@MainActivity, MainActivity::class.java)) } @@ -63,8 +66,15 @@ class MainActivity : AppCompatActivity() { val showOverlayMessaging = config.isOverlayMessagingEnabled val showDelayedConfig = config.usesDelayedConfiguration() - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), WRITE_EXTERNAL_PERMISSION_REQUEST_CODE) + if (ContextCompat.checkSelfPermission( + this, Manifest.permission.WRITE_EXTERNAL_STORAGE + ) != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), + WRITE_EXTERNAL_PERMISSION_REQUEST_CODE + ) } Optimove.getInstance().seeIntent(intent, savedInstanceState) @@ -107,12 +117,14 @@ class MainActivity : AppCompatActivity() { 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) }) @@ -131,12 +143,9 @@ 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 - "Immediate init enabled — restart app to apply" - } - ) + outputText = if (enabled) "Delayed init enabled — restart app to apply" + else "Immediate init enabled — restart app to apply" + }) } } } @@ -173,30 +182,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 @@ -224,20 +230,20 @@ class MainActivity : AppCompatActivity() { } - - 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") } } @@ -246,9 +252,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 @@ -257,10 +264,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") } } @@ -275,7 +282,11 @@ class MainActivity : AppCompatActivity() { } 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?) { @@ -298,39 +309,37 @@ 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 - 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 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\nShow this message?") - .setPositiveButton("Show") { dialog, _ -> - decision.show() - dialog.dismiss() - } - .setNegativeButton("Suppress") { dialog, _ -> - decision.suppress() - dialog.dismiss() - } - .setOnCancelListener { decision.suppress() } - .create() + inAppDecisionDialog = + AlertDialog.Builder(this@MainActivity).setTitle("QA: In-App Message") + .setMessage("$dataText\nShow this message?") + .setPositiveButton("Show") { dialog, _ -> + decision.show() + dialog.dismiss() + }.setNegativeButton("Suppress") { dialog, _ -> + decision.suppress() + dialog.dismiss() + }.setOnCancelListener { decision.suppress() }.create() inAppDecisionDialog?.show() } } @@ -342,8 +351,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/OverlayMessagingActivity.kt b/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/OverlayMessagingActivity.kt index c32ed1ff..2dedfdef 100644 --- a/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/OverlayMessagingActivity.kt +++ b/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/OverlayMessagingActivity.kt @@ -35,7 +35,9 @@ 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 pendingCallback by mutableStateOf( + null + ) private var timeoutSeconds by mutableStateOf("30") private var lastOutcome by mutableStateOf(null) private val handler = Handler(Looper.getMainLooper()) @@ -122,7 +124,10 @@ class OverlayMessagingActivity : AppCompatActivity() { horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Button( - onClick = { pendingCallback?.show(); pendingCallback = null; lastOutcome = "show" }, + onClick = { + pendingCallback?.show(); pendingCallback = null; lastOutcome = + "show" + }, enabled = hasMessage, modifier = Modifier.weight(1f), shape = RoundedCornerShape(10.dp) @@ -130,7 +135,10 @@ class OverlayMessagingActivity : AppCompatActivity() { Text("Show") } Button( - onClick = { pendingCallback?.discard(); pendingCallback = null; lastOutcome = "discard" }, + onClick = { + pendingCallback?.discard(); pendingCallback = + null; lastOutcome = "discard" + }, enabled = hasMessage, modifier = Modifier.weight(1f), shape = RoundedCornerShape(10.dp), @@ -141,7 +149,10 @@ class OverlayMessagingActivity : AppCompatActivity() { Text("Discard") } Button( - onClick = { pendingCallback?.defer(); pendingCallback = null; lastOutcome = "defer" }, + onClick = { + pendingCallback?.defer(); pendingCallback = null; lastOutcome = + "defer" + }, enabled = hasMessage, modifier = Modifier.weight(1f), shape = RoundedCornerShape(10.dp), @@ -167,19 +178,22 @@ class OverlayMessagingActivity : AppCompatActivity() { 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) + 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 - }) + override fun getTimeoutMs(): Long = timeoutMs + }) isInterceptorSet = true } 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 index 522a3c17..267a513f 100644 --- 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 @@ -14,9 +14,14 @@ public class OptimoveOverlayMessaging { private final OverlayMessagingManager manager; public interface OverlayMessagingInterceptorCallback { - @UiThread void show(); - @UiThread void discard(); - @UiThread void defer(); + @UiThread + void show(); + + @UiThread + void discard(); + + @UiThread + void defer(); } public interface OverlayMessagingInterceptor { 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 index 54e713c1..a7aa3c00 100644 --- 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 @@ -25,18 +25,20 @@ class OverlayMessagingManager implements AppStateWatcher.AppStateChangedListener private static final int IMMEDIATE_SLOT_CAPACITY = 1; enum InterceptorOutcome { - SHOW, - DISCARD, - DEFER, - TIMEOUT; + SHOW, DISCARD, DEFER, TIMEOUT; String toEventValue() { switch (this) { - case SHOW: return "shown"; - case DISCARD: return "discarded"; - case DEFER: return "deferred"; - case TIMEOUT: return "timeout"; - default: throw new IllegalStateException("Unhandled outcome: " + this); + case SHOW: + return "shown"; + case DISCARD: + return "discarded"; + case DEFER: + return "deferred"; + case TIMEOUT: + return "timeout"; + default: + throw new IllegalStateException("Unhandled outcome: " + this); } } } @@ -154,9 +156,7 @@ public void defer() { } @UiThread - private void handleInterceptorOutcome( - @NonNull OverlayMessagingMessage message, - @NonNull InterceptorOutcome outcome) { + private void handleInterceptorOutcome(@NonNull OverlayMessagingMessage message, @NonNull InterceptorOutcome outcome) { switch (outcome) { case SHOW: displayQueue.add(message); 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 index c00379e3..39944ab6 100644 --- 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 @@ -8,8 +8,7 @@ public class OverlayMessagingMessage { public enum MessageType { - SESSION, - IMMEDIATE + SESSION, IMMEDIATE } private final long id; 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 index 32c30f14..aed53804 100644 --- 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 @@ -11,7 +11,8 @@ class OverlayMessagingRendererEvent { final String type; final boolean immediateFlush; - @Nullable final JSONObject data; + @Nullable + final JSONObject data; private OverlayMessagingRendererEvent(String type, boolean immediateFlush, @Nullable JSONObject data) { this.type = type; @@ -27,9 +28,9 @@ static List parseAll(@Nullable JSONArray raw) { JSONObject obj = raw.optJSONObject(i); if (obj == null) continue; result.add(new OverlayMessagingRendererEvent( - obj.optString("type"), - obj.optBoolean("immediateFlush", true), - obj.optJSONObject("data") + 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 index d117f60b..ae757df4 100644 --- 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 @@ -26,8 +26,7 @@ class OverlayMessagingRequestService { 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); + String url = Optimobile.urlForService(UrlBuilder.Service.OVERLAY_MESSAGING, "/api/v1/users/" + encodedIdentifier + "/messages/mobile?messageType=" + messageType); try (Response response = httpClient.getSync(url)) { 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 index 6bd9c8a4..b83773cd 100644 --- 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 @@ -90,7 +90,6 @@ private void startNewSession() { } - @Override public void activityAvailable(@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 index e50cfb29..378ec84b 100644 --- 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 @@ -19,8 +19,10 @@ class OverlayMessagingView extends BaseMessageView { private static class RendererCommand { final boolean close; - @Nullable final List events; - @Nullable final JSONArray sdkActions; + @Nullable + final List events; + @Nullable + final JSONArray sdkActions; private RendererCommand(boolean close, @Nullable List events, @Nullable JSONArray sdkActions) { this.close = close; @@ -32,19 +34,22 @@ private RendererCommand(boolean close, @Nullable List events = OverlayMessagingRendererEvent.parseAll(data.optJSONArray("events")); - return new RendererCommand( - data.optBoolean("close", false), - events.isEmpty() ? null : events, - data.optJSONArray("executeSdkActions") - ); + 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); + @UiThread + void onMessageClosed(OverlayMessagingMessage message); + + @UiThread + void onEvents(OverlayMessagingMessage message, List events); + + @UiThread + void onDismissed(OverlayMessagingMessage message); + + @UiThread + void onViewError(OverlayMessagingMessage message); } @NonNull @@ -53,10 +58,7 @@ interface Listener { private final Listener listener; @UiThread - OverlayMessagingView(@NonNull OverlayMessagingMessage message, - @NonNull Activity currentActivity, - @NonNull String iarUrl, - @NonNull Listener listener) { + OverlayMessagingView(@NonNull OverlayMessagingMessage message, @NonNull Activity currentActivity, @NonNull String iarUrl, @NonNull Listener listener) { super(currentActivity); this.currentMessage = message; 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 a9f61786..43f42d5f 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 @@ -108,7 +108,7 @@ protected void maybeTriggerOverlayMessagingSync(Context context, PushMessage pus return; } - if (!pushMessage.isOverlayMessagingTrigger()){ + if (!pushMessage.isOverlayMessagingTrigger()) { return; } From 002677e06aa862488ad24c5b8ca0903d94ca6b9c Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Tue, 7 Apr 2026 12:36:40 +0100 Subject: [PATCH 20/29] status bar color funs --- .../android/optimobile/BaseMessageView.java | 14 ++++++++++---- .../android/optimobile/InAppMessageView.java | 2 +- .../android/optimobile/OverlayMessagingView.java | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) 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 index 6b47da0a..bc5688f0 100644 --- 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 @@ -86,14 +86,16 @@ private enum State { private int prevStatusBarColor; private boolean prevFlagTranslucentStatus; private boolean prevFlagDrawsSystemBarBackgrounds; + private final boolean manageStatusBarColor; @UiThread - BaseMessageView(@NonNull Activity currentActivity) { + BaseMessageView(@NonNull Activity currentActivity, boolean manageStatusBarColor) { this.state = State.INITIAL; pageFinished = false; this.currentActivity = currentActivity; + this.manageStatusBarColor = manageStatusBarColor; } @UiThread @@ -197,8 +199,10 @@ private void closeDialog(Activity dialogActivity) { if (dialog != null) { dialog.setOnKeyListener(null); dialog.dismiss(); - - unsetStatusBarColorForDialog(dialogActivity); + + if (manageStatusBarColor) { + unsetStatusBarColorForDialog(dialogActivity); + } } if (null != wv) { @@ -284,7 +288,9 @@ protected void closeCurrentMessage(MessageCloseSource source) { @Override public void onPageFinished(WebView view, String url) { view.setBackgroundColor(android.graphics.Color.TRANSPARENT); - setStatusBarColorForDialog(currentActivity); + if (manageStatusBarColor) { + setStatusBarColorForDialog(currentActivity); + } pageFinished = true; sendCurrentMessageToClient(); 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 c14e998e..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 @@ -40,7 +40,7 @@ class InAppMessageView extends BaseMessageView { @NonNull Activity currentActivity, @NonNull String iarUrl, @Nullable String region) { - super(currentActivity); + super(currentActivity, true); this.presenter = presenter; this.currentMessage = message; 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 index 378ec84b..c3f04303 100644 --- 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 @@ -59,7 +59,7 @@ interface Listener { @UiThread OverlayMessagingView(@NonNull OverlayMessagingMessage message, @NonNull Activity currentActivity, @NonNull String iarUrl, @NonNull Listener listener) { - super(currentActivity); + super(currentActivity, false); this.currentMessage = message; this.listener = listener; From 60e47c9b8bdcfb56e9f61f6c3b290ad4121bd2db Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Tue, 7 Apr 2026 13:51:11 +0100 Subject: [PATCH 21/29] delayed init vs sessions --- .../android/optimobile/Optimobile.java | 5 +++- .../optimobile/OptimoveOverlayMessaging.java | 29 ++++++++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) 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 84b004e1..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 @@ -118,7 +118,7 @@ public static synchronized void initialize(final Application application, Optimo OptimoveInApp.initialize(application, config); if (config.isOverlayMessagingEnabled()) { - OptimoveOverlayMessaging.initialize(application, config.getOverlayMessagingSessionLengthHours()); + OptimoveOverlayMessaging.initialize(application, config); } if (config.getDeferredDeepLinkHandler() != null) { @@ -602,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 index 267a513f..d4b980e5 100644 --- 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 @@ -2,16 +2,22 @@ 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; - private final OverlayMessagingSessionManager sessionManager; + @Nullable + private OverlayMessagingSessionManager sessionManager; private final OverlayMessagingManager manager; + private final Application application; + private final long sessionLengthHours; public interface OverlayMessagingInterceptorCallback { @UiThread @@ -34,7 +40,12 @@ default long getTimeoutMs() { } 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); @@ -56,7 +67,9 @@ public void setInterceptor(@Nullable OverlayMessagingInterceptor interceptor) { @UiThread public void resetSession() { - sessionManager.resetSession(); + if (sessionManager != null) { + sessionManager.resetSession(); + } } //============================================================================================== @@ -67,7 +80,15 @@ void onPushTriggerReceived() { manager.onTriggerReceived(OverlayMessagingMessage.MessageType.IMMEDIATE); } - static void initialize(@NonNull Application application, long sessionLengthHours) { - shared = new OptimoveOverlayMessaging(application, sessionLengthHours); + 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); } } From 4df7e980c1ae5dc78706920bb9e0ebb1d34454be Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Wed, 8 Apr 2026 12:51:15 +0100 Subject: [PATCH 22/29] remove clicked event, it's given by IAR directly --- .../java/com/optimove/android/optimobile/AnalyticsContract.java | 1 - 1 file changed, 1 deletion(-) 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 c3d3c5f3..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 @@ -46,7 +46,6 @@ final class AnalyticsContract { 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_CLICKED = "optimove.om.clicked"; 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"; From a038a5bc0cb5c72c1d16c6964a4c886206be1b0b Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Sun, 12 Apr 2026 21:55:27 +0100 Subject: [PATCH 23/29] clean up message after view error --- .../optimove/android/optimobile/OverlayMessagingManager.java | 4 ++++ 1 file changed, 4 insertions(+) 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 index a7aa3c00..49861989 100644 --- 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 @@ -222,6 +222,10 @@ public void onDismissed(OverlayMessagingMessage message) { 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(); } }); } From 56192e005603666cce656f5d8cec0bcfe270f2ce Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Mon, 13 Apr 2026 12:35:16 +0100 Subject: [PATCH 24/29] align interceptor outcomes --- .../android/optimobile/OverlayMessagingManager.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 49861989..5c0f55c2 100644 --- 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 @@ -30,11 +30,11 @@ enum InterceptorOutcome { String toEventValue() { switch (this) { case SHOW: - return "shown"; + return "show"; case DISCARD: - return "discarded"; + return "discard"; case DEFER: - return "deferred"; + return "defer"; case TIMEOUT: return "timeout"; default: From 812c7ec6caf9bfffbbb459a236213f7cf0d34ba9 Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Mon, 13 Apr 2026 14:41:23 +0100 Subject: [PATCH 25/29] iar was released --- .../java/com/optimove/android/optimobile/UrlBuilder.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 b0f50b40..fd0b423e 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 @@ -38,9 +38,8 @@ String urlForService(Service service, String path) { public static Map defaultMapping(@NonNull String region) { Map baseUrlMap = new HashMap<>(Service.values().length); - //baseUrlMap.put(Service.IAR, "https://iar.app.delivery"); - // TODO - baseUrlMap.put(Service.IAR, "https://optimobile-iar-dev.optimove.net"); + baseUrlMap.put(Service.IAR, "https://iar.app.delivery"); + From 311278a96d4aed6aa1fa80ddd1cbcede10cc5493 Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Wed, 15 Apr 2026 09:07:39 +0100 Subject: [PATCH 26/29] use public messages url --- .../com/optimove/android/optimobile/UrlBuilder.java | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) 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 fd0b423e..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 @@ -38,9 +38,7 @@ String urlForService(Service service, String path) { public static Map defaultMapping(@NonNull String region) { Map baseUrlMap = new HashMap<>(Service.values().length); - baseUrlMap.put(Service.IAR, "https://iar.app.delivery"); - - + baseUrlMap.put(Service.IAR, "https://iar.app.delivery"); baseUrlMap.put(Service.PUSH, "https://push-" + region + ".kumulos.com"); @@ -49,15 +47,8 @@ public static Map defaultMapping(@NonNull String region) { baseUrlMap.put(Service.DDL, "https://links-" + region + ".kumulos.com"); baseUrlMap.put(Service.MEDIA, "https://i-" + region + ".app.delivery"); - // TODO: http -> https - // TODO: use region once cnmaes ok - baseUrlMap.put(Service.OVERLAY_MESSAGING, "http://optimobile-overlay-srv-" + "dev" + ".optimove.net"); - + baseUrlMap.put(Service.OVERLAY_MESSAGING, "https://optimobile-overlay-srv-" + region + ".optimove.net"); return baseUrlMap; } - - - - } From 29b28a6dee7449f4905627074d3e2119fa9be4e9 Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Thu, 23 Apr 2026 15:59:19 +0100 Subject: [PATCH 27/29] port in-memory seen ids set from web --- .../android/optimobile/OverlayMessagingManager.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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 index 5c0f55c2..b7164433 100644 --- 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 @@ -13,7 +13,9 @@ 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; @@ -56,6 +58,8 @@ String toEventValue() { 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(); @@ -109,11 +113,12 @@ private void loadMessage(OverlayMessagingMessage.MessageType type) { @UiThread private void onMessageLoaded(OverlayMessagingMessage.MessageType type, @Nullable OverlayMessagingMessage message) { - if (message == null) { + if (message == null || seenMessageIds.contains(message.getId())) { onSlotCleared(type); return; } + seenMessageIds.add(message.getId()); processMessage(message); } @@ -163,9 +168,13 @@ private void handleInterceptorOutcome(@NonNull OverlayMessagingMessage message, maybeShowNext(); trackInterceptedEvent(message.getId(), outcome); break; - case DISCARD: 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; From da79ceb277e2b5fe86f50fa6f163a68da52ee8e5 Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Thu, 23 Apr 2026 16:39:40 +0100 Subject: [PATCH 28/29] unset interceptor when activity destroyed to fix memory leak, test app issue --- .../android/optimovemobilesdk/OverlayMessagingActivity.kt | 5 +++++ 1 file changed, 5 insertions(+) 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 index 2dedfdef..f9c6447d 100644 --- a/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/OverlayMessagingActivity.kt +++ b/OptimoveSDK/app/src/main/java/com/optimove/android/optimovemobilesdk/OverlayMessagingActivity.kt @@ -197,6 +197,11 @@ class OverlayMessagingActivity : AppCompatActivity() { isInterceptorSet = true } + override fun onDestroy() { + super.onDestroy() + unsetInterceptor() + } + private fun unsetInterceptor() { OptimoveOverlayMessaging.getInstance().setInterceptor(null) isInterceptorSet = false From ec86bb6ffe827f681da5ccc633e2afa1d6770988 Mon Sep 17 00:00:00 2001 From: Vladislav Voicehovich Date: Thu, 23 Apr 2026 17:00:21 +0100 Subject: [PATCH 29/29] overlay changes for test app --- OptimoveSDK/app/src/main/AndroidManifest.xml | 4 ++++ .../com/optimove/android/optimovemobilesdk/MyApplication.kt | 2 ++ 2 files changed, 6 insertions(+) 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"/> + +