diff --git a/.run/All Tests.run.xml b/.run/All Tests.run.xml
index 8ccf0909a..dbe971eaa 100644
--- a/.run/All Tests.run.xml
+++ b/.run/All Tests.run.xml
@@ -90,11 +90,6 @@
-
-
-
-
-
@@ -160,6 +155,11 @@
+
+
+
+
+
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTWebInterface.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTWebInterface.java
index b03d09065..0462b639e 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTWebInterface.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTWebInterface.java
@@ -2,9 +2,11 @@
import android.os.Bundle;
import android.webkit.JavascriptInterface;
+
import androidx.annotation.RestrictTo;
import com.clevertap.android.sdk.inapp.CTInAppAction;
-import com.clevertap.android.sdk.inapp.fragment.CTInAppBaseFragment;
+import com.clevertap.android.sdk.inapp.CTInAppHost;
+
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
@@ -18,12 +20,13 @@
@SuppressWarnings("WeakerAccess")
public class CTWebInterface {
- private WeakReference cleverTapWr = new WeakReference<>(null);
+ private final WeakReference cleverTapWr;
- private WeakReference fragmentWr = new WeakReference<>(null);
+ private final WeakReference hostWr;
public CTWebInterface(CleverTapAPI instance) {
- this.cleverTapWr = new WeakReference<>(instance);
+ cleverTapWr = new WeakReference<>(instance);
+ hostWr = new WeakReference<>(null);
CleverTapAPI cleverTapAPI = cleverTapWr.get();
if (cleverTapAPI != null) {
CoreState coreState = cleverTapAPI.getCoreState();
@@ -34,9 +37,9 @@ public CTWebInterface(CleverTapAPI instance) {
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public CTWebInterface(CleverTapAPI instance, CTInAppBaseFragment inAppBaseFragment) {
- this.cleverTapWr = new WeakReference<>(instance);
- this.fragmentWr = new WeakReference<>(inAppBaseFragment);
+ public CTWebInterface(CleverTapAPI instance, CTInAppHost host) {
+ cleverTapWr = new WeakReference<>(instance);
+ hostWr = new WeakReference<>(host);
}
/**
@@ -65,9 +68,9 @@ public void dismissInAppNotification() {
Logger.d("CleverTap Instance is null.");
} else {
//Dismisses current IAM and proceeds to call promptForPushPermission()
- CTInAppBaseFragment fragment = fragmentWr.get();
- if (fragment != null) {
- fragment.didDismiss(null);
+ CTInAppHost host = hostWr.get();
+ if (host != null) {
+ host.didDismissInApp(null);
}
}
}
@@ -404,9 +407,9 @@ public void triggerInAppAction(String actionJson, String callToAction, String bu
return;
}
- CTInAppBaseFragment fragment = fragmentWr.get();
- if (fragment == null) {
- Logger.d("CTWebInterface Fragment is null");
+ CTInAppHost host = hostWr.get();
+ if (host == null) {
+ Logger.d("CTWebInterface host is null");
return;
}
@@ -427,7 +430,7 @@ public void triggerInAppAction(String actionJson, String callToAction, String bu
actionData.putString("button_id", buttonId);
}
- fragment.triggerAction(action, callToAction, actionData);
+ host.triggerAction(action, callToAction, actionData);
} catch (JSONException je) {
Logger.d("CTWebInterface invalid action JSON: " + actionJson);
}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHost.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHost.kt
new file mode 100644
index 000000000..03c2baba5
--- /dev/null
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHost.kt
@@ -0,0 +1,120 @@
+package com.clevertap.android.sdk.inapp
+
+import android.content.Context
+import android.os.Bundle
+import android.util.TypedValue
+import com.clevertap.android.sdk.CleverTapInstanceConfig
+import com.clevertap.android.sdk.Constants
+import com.clevertap.android.sdk.utils.UriHelper
+import java.lang.ref.WeakReference
+import java.net.URLDecoder
+import kotlin.text.split
+
+internal class CTInAppHost(
+ inAppListener: InAppListener?,
+ private val config: CleverTapInstanceConfig,
+ private val inAppNotification: CTInAppNotification,
+ private var callbacks: Callbacks?,
+ context: Context?
+) {
+
+ internal interface Callbacks {
+ fun onDismissInApp()
+ }
+
+ private var inAppListenerWeakReference = WeakReference(inAppListener)
+ private val contextWeakReference = WeakReference(context)
+
+ fun getInAppListener(): InAppListener? {
+ val listener = inAppListenerWeakReference.get()
+ if (listener == null) {
+ config.logger.verbose(
+ config.accountId,
+ "InAppListener is null for notification: ${inAppNotification.jsonDescription}"
+ )
+ }
+ return listener
+ }
+
+ fun setInAppListener(listener: InAppListener) {
+ inAppListenerWeakReference = WeakReference(listener)
+ }
+
+ fun getScaledPixels(raw: Int): Int {
+ val context = contextWeakReference.get()
+ if (context == null) {
+ return raw
+ }
+ return TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ raw.toFloat(),
+ context.resources.displayMetrics
+ ).toInt()
+ }
+
+ fun triggerAction(
+ action: CTInAppAction, callToAction: String?, additionalData: Bundle?
+ ) {
+ var additionalData = additionalData
+ var action = action
+ var callToAction = callToAction
+ if (action.type == InAppActionType.OPEN_URL) {
+ //All URL parameters should be tracked as additional data
+ val urlActionData = UriHelper.getAllKeyValuePairs(action.actionUrl, false)
+
+ // callToAction is handled as a parameter
+ var callToActionUrlParam = urlActionData.getString(Constants.KEY_C2A)
+ // no need to keep it in the data bundle
+ urlActionData.remove(Constants.KEY_C2A)
+
+ // add all additional params, overriding the url params if there is a collision
+ if (additionalData != null) {
+ urlActionData.putAll(additionalData)
+ }
+ // Use the merged data for the action
+ additionalData = urlActionData
+ if (callToActionUrlParam != null) {
+ // check if there is a deeplink within the callToAction param
+ val parts = callToActionUrlParam.split(Constants.URL_PARAM_DL_SEPARATOR)
+ if (parts.size == 2) {
+ // Decode it here as it is not decoded by UriHelper
+ try {
+ // Extract the actual callToAction value
+ callToActionUrlParam = URLDecoder.decode(parts[0], "UTF-8")
+ } catch (e: Exception) {
+ config.logger.debug("Error parsing c2a param", e)
+ }
+ // use the url from the callToAction param
+ action = CTInAppAction.CREATOR.createOpenUrlAction(parts[1])
+ }
+ }
+ if (callToAction == null) {
+ // Use the url param value only if no other value is passed
+ callToAction = callToActionUrlParam
+ }
+ }
+ val actionData = notifyActionTriggered(action, callToAction ?: "", additionalData)
+ didDismissInApp(actionData)
+ }
+
+ fun openUrl(url: String) {
+ triggerAction(CTInAppAction.CREATOR.createOpenUrlAction(url), null, null)
+ }
+
+ fun didDismissInApp(data: Bundle?) {
+ callbacks?.onDismissInApp()
+ getInAppListener()?.inAppNotificationDidDismiss(inAppNotification, data)
+ }
+
+ fun didShowInApp(data: Bundle?) {
+ getInAppListener()?.inAppNotificationDidShow(inAppNotification, data)
+ }
+
+ fun notifyActionTriggered(
+ action: CTInAppAction, callToAction: String, additionalData: Bundle?
+ ): Bundle? {
+ return getInAppListener()?.inAppNotificationActionTriggered(
+ inAppNotification, action, callToAction, additionalData, contextWeakReference.get()
+ )
+ }
+}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlBannerOverlay.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlBannerOverlay.kt
new file mode 100644
index 000000000..f93664260
--- /dev/null
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlBannerOverlay.kt
@@ -0,0 +1,197 @@
+package com.clevertap.android.sdk.inapp
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.graphics.PixelFormat
+import android.view.GestureDetector
+import android.view.Gravity
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.FrameLayout
+
+import com.clevertap.android.sdk.CleverTapAPI
+import com.clevertap.android.sdk.CleverTapInstanceConfig
+import com.clevertap.android.sdk.task.MainLooperHandler
+
+import java.lang.ref.WeakReference
+
+/**
+ * Class that renders *custom-html* "header" and "footer" CTInAppNotifications
+ * using an overlay window instead of the normal Fragment-based banner.
+ *
+ * This allows this type of notification to be used in any Activity,
+ * whereas the normal CTInAppHtmlHeaderFragment and CTInAppHtmlFooterFragment
+ * require a FragmentActivity.
+ **/
+internal class CTInAppHtmlBannerOverlay(
+ private val notification: CTInAppNotification,
+ private val config: CleverTapInstanceConfig,
+ inAppListener: InAppListener,
+ activity: Activity
+) : View.OnTouchListener, View.OnLongClickListener,
+ CTInAppHost.Callbacks {
+
+ companion object {
+ fun canDisplay(type: CTInAppType): Boolean {
+ return type == CTInAppType.CTInAppTypeFooterHTML || type == CTInAppType.CTInAppTypeHeaderHTML
+ }
+
+ fun show(
+ notification: CTInAppNotification,
+ config: CleverTapInstanceConfig,
+ inAppListener: InAppListener,
+ activity: Activity
+ ) {
+ CTInAppHtmlBannerOverlay(notification, config, inAppListener, activity).show()
+ }
+ }
+
+ private val activityWeakRef = WeakReference(activity)
+ private val isJsEnabled = notification.isJsEnabled
+ private val mainHandler = MainLooperHandler()
+ private val inAppHost = CTInAppHost(inAppListener, config, notification, this, activity)
+ private val gestureListener = PartialHtmlInAppGestureListener(inAppHost)
+ private val gd = GestureDetector(gestureListener)
+ private var wm: WindowManager? = null
+ private var overlayRoot: View? = null
+ private var webView: CTInAppWebView? = null
+ private var animatingDismiss = false
+
+ fun show() {
+ mainHandler.post(this::build)
+ }
+
+ private fun build() {
+ // this code partially based on CTInAppBasePartialHtmlFragment.displayHTMLView()
+ val activity = activityWeakRef.get() ?: return
+ val root = FrameLayout(activity)
+ root.isClickable = true
+ root.setFocusable(true)
+ overlayRoot = root
+
+ // ---------- WebView ----------
+ val webView = CTInAppWebView(
+ activity,
+ notification.width,
+ notification.height,
+ notification.widthPercentage,
+ notification.heightPercentage,
+ notification.aspectRatio,
+ inAppHost
+ )
+ this.webView = webView
+ gestureListener.webView = webView
+ webView.setOnTouchListener(this)
+ webView.setOnLongClickListener(this)
+
+ // Install our custom JavaScript interface
+ if (isJsEnabled) {
+ val instance = CleverTapAPI.instanceWithConfig(activity, config)
+ webView.enableCTJavaScriptInterface(instance)
+ }
+
+ // load the HTML
+ val html = notification.html ?: return
+ config.getLogger().verbose(
+ config.accountId,
+ "CTInAppHtmlBannerOverlay CTInAppNotification HTML:\n$html"
+ )
+
+ // add to the layout
+ val lp = FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ gravity()
+ )
+ root.addView(webView, lp)
+
+ // add the overlay to the activity's window manager
+ val wmlp = WindowManager.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT, // fit the webview
+ WindowManager.LayoutParams.TYPE_APPLICATION_PANEL, // sit above with own input stream
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
+ PixelFormat.TRANSLUCENT
+ )
+ wmlp.gravity = gravity() // TOP or BOTTOM
+ wmlp.token = activity.window.decorView.windowToken // tie to this activity
+ wm = activity.windowManager
+ wm?.addView(root, wmlp)
+
+ webView.updateDimension()
+ webView.loadInAppHtml(html)
+ inAppHost.didShowInApp(null)
+ }
+
+ override fun onLongClick(v: View): Boolean {
+ return true
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouch(v: View, event: MotionEvent): Boolean {
+ return gd.onTouchEvent(event) || (event.action == MotionEvent.ACTION_MOVE)
+ }
+
+ override fun onDismissInApp() {
+ dismiss()
+ }
+
+ private fun dismiss() {
+ val overlayRoot = this.overlayRoot
+ if (overlayRoot == null || wm == null) {
+ config.getLogger().debug(
+ config.accountId,
+ "CTInAppHtmlBannerOverlay.dismiss() - Missing overlay or window manager"
+ )
+ return
+ }
+ if (!animatingDismiss) {
+ animatingDismiss = true
+ overlayRoot.animate()
+ .alpha(0f)
+ .setDuration(250)
+ .withEndAction(this::finishDismiss)
+ .start()
+ } else {
+ finishDismiss()
+ }
+ }
+
+ private fun finishDismiss() {
+ mainHandler.post {
+ try {
+ wm?.removeViewImmediate(overlayRoot)
+ overlayRoot = null
+ cleanupWebView()
+ } catch (exception: Exception) {
+ config.getLogger().debug(
+ config.accountId,
+ "CTInAppHtmlBannerOverlay: Removing failed!",
+ exception
+ )
+ }
+ }
+ }
+
+ private fun cleanupWebView() {
+ try {
+ webView?.cleanup(isJsEnabled)
+ webView = null
+ } catch (e: Exception) {
+ config.getLogger().debug("cleanupWebView -> there was some crash in cleanup", e)
+ // no-op; we are anyway destroying everything. This is just for safety.
+ }
+ }
+
+ private fun gravity(): Int {
+ return if (CTInAppType.CTInAppTypeFooterHTML == notification.inAppType) {
+ Gravity.BOTTOM
+ } else {
+ Gravity.TOP
+ }
+ }
+}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppWebView.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppWebView.kt
index 24eaea409..11461deb6 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppWebView.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppWebView.kt
@@ -1,16 +1,22 @@
package com.clevertap.android.sdk.inapp
import android.annotation.SuppressLint
+import android.annotation.TargetApi
import android.content.Context
import android.graphics.Point
import android.os.Build
import android.util.TypedValue
import android.view.WindowInsets
import android.view.WindowManager
+import android.webkit.WebResourceRequest
import android.webkit.WebView
+import android.webkit.WebViewClient
import androidx.annotation.Px
import androidx.annotation.RequiresApi
import com.clevertap.android.sdk.CTWebInterface
+import com.clevertap.android.sdk.CleverTapAPI
+import com.clevertap.android.sdk.Logger
+import java.lang.ref.WeakReference
@SuppressLint("ViewConstructor")
internal class CTInAppWebView @SuppressLint("ResourceType") constructor(
@@ -19,7 +25,8 @@ internal class CTInAppWebView @SuppressLint("ResourceType") constructor(
private val heightDp: Int,
private val widthPercentage: Int,
private val heightPercentage: Int,
- private val aspectRatio: Double
+ private val aspectRatio: Double,
+ host: CTInAppHost
) : WebView(context) {
companion object {
@@ -29,6 +36,7 @@ internal class CTInAppWebView @SuppressLint("ResourceType") constructor(
@JvmField
val dim: Point = Point()
+ val hostWr: WeakReference
var isFullscreen = false
@@ -38,8 +46,17 @@ internal class CTInAppWebView @SuppressLint("ResourceType") constructor(
widthDp: Int,
heightDp: Int,
widthPercentage: Int,
- heightPercentage: Int
- ) : this(context, widthDp, heightDp, widthPercentage, heightPercentage, DEFAULT_ASPECT_RATIO)
+ heightPercentage: Int,
+ host: CTInAppHost
+ ) : this(
+ context,
+ widthDp,
+ heightDp,
+ widthPercentage,
+ heightPercentage,
+ DEFAULT_ASPECT_RATIO,
+ host
+ )
init {
isHorizontalScrollBarEnabled = false
@@ -51,6 +68,26 @@ internal class CTInAppWebView @SuppressLint("ResourceType") constructor(
// set the text zoom in order to ignore device font size changes
settings.textZoom = 100
id = 188293
+ hostWr = WeakReference(host)
+ setWebViewClient(InAppWebViewClient())
+ }
+
+ fun loadInAppHtml(html: String) {
+ var mHeight = dim.y
+ var mWidth = dim.x
+
+ val d = resources.displayMetrics.density
+ mHeight = (mHeight / d).toInt()
+ mWidth = (mWidth / d).toInt()
+
+
+ val style =
+ ""
+ val inAppHtml = html.replaceFirst("".toRegex(), "$style")
+ Logger.v("Density appears to be $d")
+
+ setInitialScale((d * 100).toInt())
+ loadDataWithBaseURL(null, inAppHtml, "text/html", "utf-8", null)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
@@ -155,7 +192,8 @@ internal class CTInAppWebView @SuppressLint("ResourceType") constructor(
}
@SuppressLint("SetJavaScriptEnabled")
- fun setJavaScriptInterface(webInterface: CTWebInterface) {
+ fun enableCTJavaScriptInterface(ctInstance: CleverTapAPI) {
+ val host = hostWr.get() ?: return
getSettings().apply {
javaScriptEnabled = true
javaScriptCanOpenWindowsAutomatically = false
@@ -165,7 +203,7 @@ internal class CTInAppWebView @SuppressLint("ResourceType") constructor(
}
addJavascriptInterface(
- webInterface,
+ CTWebInterface(ctInstance, host),
JAVASCRIPT_INTERFACE_NAME
)
}
@@ -180,4 +218,24 @@ internal class CTInAppWebView @SuppressLint("ResourceType") constructor(
clearHistory()
destroy()
}
+
+ private inner class InAppWebViewClient() : WebViewClient() {
+
+ @Deprecated("Deprecated in Java")
+ override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
+ hostWr.get()?.openUrl(url)
+ ?: Logger.v("InAppWebViewClient : Android view is gone, not opening url")
+ return true
+ }
+
+ @SuppressLint("UseRequiresApi")
+ @TargetApi(Build.VERSION_CODES.N)
+ override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
+ request.url?.toString()?.let { url ->
+ hostWr.get()?.openUrl(url)
+ ?: Logger.v("InAppWebViewClient : Android view is gone, not opening url")
+ } ?: Logger.v("InAppWebViewClient : Url to open is null; not processing")
+ return true
+ }
+ }
}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppController.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppController.kt
index 281f5aab5..5cf54f6d5 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppController.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppController.kt
@@ -8,6 +8,7 @@ import android.os.Bundle
import android.os.Looper
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
+import androidx.fragment.app.FragmentActivity
import com.clevertap.android.sdk.AnalyticsManager
import com.clevertap.android.sdk.BaseCallbackManager
import com.clevertap.android.sdk.CleverTapInstanceConfig
@@ -613,7 +614,6 @@ internal class InAppController(
}
private fun showInApp(inAppNotification: CTInAppNotification) {
- val activity = CoreMetaData.getCurrentActivity()
val goFromListener = checkBeforeShowApprovalBeforeDisplay(inAppNotification)
if (!goFromListener) {
logger.verbose(
@@ -638,6 +638,13 @@ internal class InAppController(
return
}
+ val activity = CoreMetaData.getCurrentActivity()
+ if (activity == null) {
+ Logger.v("Current activity reference not found.")
+ Logger.v("Please verify the integration of your app. It is not setup to support in-app notifications yet.")
+ return
+ }
+
if (!canShowInAppOnActivity(activity)) {
pendingNotifications.add(inAppNotification)
Logger.v(
@@ -664,7 +671,6 @@ internal class InAppController(
currentlyDisplayingInApp = inAppNotification
- var inAppFragment: CTInAppBaseFragment? = null
val type = inAppNotification.inAppType
when (type) {
CTInAppTypeCoverHTML,
@@ -677,66 +683,52 @@ internal class InAppController(
CTInAppTypeInterstitialImageOnly,
CTInAppTypeHalfInterstitialImageOnly,
CTInAppTypeCoverImageOnly -> {
+ Logger.d("Displaying In-App: ${inAppNotification.jsonDescription}")
+ InAppNotificationActivity.launchForInAppNotification(
+ activity,
+ inAppNotification,
+ config
+ )
+ }
- try {
- if (activity == null) {
- throw IllegalStateException("Current activity reference not found")
+ CTInAppTypeFooterHTML,
+ CTInAppTypeHeaderHTML,
+ CTInAppTypeFooter,
+ CTInAppTypeHeader -> {
+ if (activity is FragmentActivity) {
+ val inAppFragment: CTInAppBaseFragment = when (type) {
+ CTInAppTypeFooterHTML -> CTInAppHtmlFooterFragment()
+ CTInAppTypeHeaderHTML -> CTInAppHtmlHeaderFragment()
+ CTInAppTypeFooter -> CTInAppNativeFooterFragment()
+ CTInAppTypeHeader -> CTInAppNativeHeaderFragment()
+ else -> return // unreachable
}
Logger.d("Displaying In-App: ${inAppNotification.jsonDescription}")
- InAppNotificationActivity.launchForInAppNotification(
+ val showFragmentSuccess = CTInAppBaseFragment.showOnActivity(
+ inAppFragment,
activity,
inAppNotification,
- config
- )
- } catch (t: Throwable) {
- Logger.v(
- "Please verify the integration of your app. It is not setup to support in-app notifications yet.",
- t
+ config,
+ defaultLogTag
)
+ if (!showFragmentSuccess) {
+ currentlyDisplayingInApp = null
+ }
+ } else if (CTInAppHtmlBannerOverlay.canDisplay(type)) {
+ Logger.d("Displaying In-App: ${inAppNotification.jsonDescription}")
+ CTInAppHtmlBannerOverlay.show(inAppNotification, config, this, activity)
+ } else {
currentlyDisplayingInApp = null
- return
}
}
- CTInAppTypeFooterHTML -> {
- inAppFragment = CTInAppHtmlFooterFragment()
- }
-
- CTInAppTypeHeaderHTML -> {
- inAppFragment = CTInAppHtmlHeaderFragment()
- }
-
- CTInAppTypeFooter -> {
- inAppFragment = CTInAppNativeFooterFragment()
- }
-
- CTInAppTypeHeader -> {
- inAppFragment = CTInAppNativeHeaderFragment()
- }
-
CTInAppTypeCustomCodeTemplate -> {
presentTemplate(inAppNotification)
- return
}
else -> {
Logger.d(defaultLogTag, "Unknown InApp Type found: $type")
currentlyDisplayingInApp = null
- return
- }
- }
-
- if (inAppFragment != null) {
- Logger.d("Displaying In-App: ${inAppNotification.jsonDescription}")
- val showFragmentSuccess = CTInAppBaseFragment.showOnActivity(
- inAppFragment,
- activity,
- inAppNotification,
- config,
- defaultLogTag
- )
- if (!showFragmentSuccess) {
- currentlyDisplayingInApp = null
}
}
}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppWebViewClient.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppWebViewClient.kt
deleted file mode 100644
index 07a79ba3a..000000000
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppWebViewClient.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.clevertap.android.sdk.inapp
-
-import android.annotation.SuppressLint
-import android.annotation.TargetApi
-import android.os.Build
-import android.webkit.WebResourceRequest
-import android.webkit.WebView
-import android.webkit.WebViewClient
-import com.clevertap.android.sdk.Logger
-import com.clevertap.android.sdk.inapp.fragment.CTInAppBaseFragment
-import java.lang.ref.WeakReference
-
-internal class InAppWebViewClient(fragment: CTInAppBaseFragment) : WebViewClient() {
- private val fragmentWr = WeakReference(fragment)
-
- @Deprecated("Deprecated in Java")
- override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
- fragmentWr.get()?.openActionUrl(url) ?: Logger.v("InAppWebViewClient : Android view is gone, not opening url")
- return true
- }
-
- @SuppressLint("UseRequiresApi")
- @TargetApi(Build.VERSION_CODES.N)
- override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
- request.url?.toString()?.let { url ->
- fragmentWr.get()?.openActionUrl(url) ?: Logger.v("InAppWebViewClient : Android view is gone, not opening url")
- } ?: Logger.v("InAppWebViewClient : Url to open is null; not processing")
- return true
- }
-}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/PartialHtmlInAppGestureListener.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/PartialHtmlInAppGestureListener.kt
new file mode 100644
index 000000000..19f13cce3
--- /dev/null
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/PartialHtmlInAppGestureListener.kt
@@ -0,0 +1,64 @@
+package com.clevertap.android.sdk.inapp
+
+import android.view.GestureDetector.SimpleOnGestureListener
+import android.view.MotionEvent
+import android.view.animation.AlphaAnimation
+import android.view.animation.Animation
+import android.view.animation.AnimationSet
+import android.view.animation.TranslateAnimation
+import com.clevertap.android.sdk.inapp.CTInAppAction.CREATOR.createCloseAction
+import com.clevertap.android.sdk.inapp.fragment.CTInAppBasePartialHtmlFragment.Companion.CTA_SWIPE_DISMISS
+import com.clevertap.android.sdk.inapp.fragment.CTInAppBasePartialHtmlFragment.Companion.SWIPE_MIN_DISTANCE
+import com.clevertap.android.sdk.inapp.fragment.CTInAppBasePartialHtmlFragment.Companion.SWIPE_THRESHOLD_VELOCITY
+import kotlin.math.abs
+
+internal class PartialHtmlInAppGestureListener(private val inAppHost: CTInAppHost) :
+ SimpleOnGestureListener() {
+
+ var webView: CTInAppWebView? = null
+
+ override fun onFling(
+ e1: MotionEvent?,
+ e2: MotionEvent,
+ velocityX: Float,
+ velocityY: Float
+ ): Boolean {
+ if (e1 != null) {
+ if (e1.x - e2.x > SWIPE_MIN_DISTANCE && abs(velocityX.toDouble()) > SWIPE_THRESHOLD_VELOCITY) {
+ // Right to left
+ return remove(false)
+ } else if (e2.x - e1.x > SWIPE_MIN_DISTANCE && abs(velocityX.toDouble()) > SWIPE_THRESHOLD_VELOCITY) {
+ // Left to right
+ return remove(true)
+ }
+ }
+ return false
+ }
+
+ private fun remove(ltr: Boolean): Boolean {
+ val animSet = AnimationSet(true)
+ val anim = if (ltr) {
+ TranslateAnimation(0f, inAppHost.getScaledPixels(50).toFloat(), 0f, 0f)
+ } else {
+ TranslateAnimation(0f, -inAppHost.getScaledPixels(50).toFloat(), 0f, 0f)
+ }
+ animSet.addAnimation(anim)
+ animSet.addAnimation(AlphaAnimation(1f, 0f))
+ animSet.setDuration(300)
+ animSet.setFillAfter(true)
+ animSet.isFillEnabled = true
+ animSet.setAnimationListener(object : Animation.AnimationListener {
+ override fun onAnimationEnd(animation: Animation?) {
+ inAppHost.triggerAction(createCloseAction(), CTA_SWIPE_DISMISS, null)
+ }
+
+ override fun onAnimationRepeat(animation: Animation?) {
+ }
+
+ override fun onAnimationStart(animation: Animation?) {
+ }
+ })
+ webView?.startAnimation(animSet)
+ return true
+ }
+}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/fragment/CTInAppBaseFragment.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/fragment/CTInAppBaseFragment.kt
index bbc6841ac..71c8d391d 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/fragment/CTInAppBaseFragment.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/fragment/CTInAppBaseFragment.kt
@@ -3,29 +3,24 @@ package com.clevertap.android.sdk.inapp.fragment
import android.app.Activity
import android.content.Context
import android.os.Bundle
-import android.util.TypedValue
import android.view.View
-
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
-
import com.clevertap.android.sdk.CleverTapInstanceConfig
import com.clevertap.android.sdk.Constants
import com.clevertap.android.sdk.DidClickForHardPermissionListener
import com.clevertap.android.sdk.Logger
import com.clevertap.android.sdk.customviews.CloseImageView
import com.clevertap.android.sdk.inapp.CTInAppAction
+import com.clevertap.android.sdk.inapp.CTInAppHost
import com.clevertap.android.sdk.inapp.CTInAppNotification
import com.clevertap.android.sdk.inapp.CTInAppNotificationButton
import com.clevertap.android.sdk.inapp.InAppActionType
import com.clevertap.android.sdk.inapp.InAppListener
import com.clevertap.android.sdk.inapp.images.FileResourceProvider
-import com.clevertap.android.sdk.utils.UriHelper
-
import java.lang.ref.WeakReference
-import java.net.URLDecoder
-internal abstract class CTInAppBaseFragment : Fragment() {
+internal abstract class CTInAppBaseFragment : Fragment(), CTInAppHost.Callbacks {
companion object {
fun showOnActivity(
@@ -74,8 +69,8 @@ internal abstract class CTInAppBaseFragment : Fragment() {
protected lateinit var config: CleverTapInstanceConfig
protected var currentOrientation: Int = 0
protected var closeImageView: CloseImageView? = null
- private var listenerWeakReference: WeakReference? = null
private var didClickForHardPermissionListener: DidClickForHardPermissionListener? = null
+ internal lateinit var inAppHost: CTInAppHost
protected abstract fun cleanup()
protected abstract fun generateListener()
@@ -84,10 +79,12 @@ internal abstract class CTInAppBaseFragment : Fragment() {
super.onAttach(context)
val bundle = arguments
if (bundle != null) {
- inAppNotification = bundle.getParcelable(Constants.INAPP_KEY)!!
- config = bundle.getParcelable(Constants.KEY_CONFIG)!!
+ inAppNotification = bundle.getParcelable(Constants.INAPP_KEY)!!
+ config = bundle.getParcelable(Constants.KEY_CONFIG)!!
currentOrientation = resources.configuration.orientation
- generateListener()/*Initialize the below listener only when in app has InAppNotification activity as their host activity
+ inAppHost = CTInAppHost(null, config, inAppNotification, this, context)
+ generateListener()
+ /*Initialize the below listener only when in app has InAppNotification activity as their host activity
when requesting permission for notification.*/
if (context is DidClickForHardPermissionListener) {
didClickForHardPermissionListener = context
@@ -97,7 +94,7 @@ internal abstract class CTInAppBaseFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- didShow(null)
+ inAppHost.didShowInApp(null)
}
fun setArguments(inAppNotification: CTInAppNotification, config: CleverTapInstanceConfig) {
@@ -107,84 +104,20 @@ internal abstract class CTInAppBaseFragment : Fragment() {
setArguments(bundle)
}
- fun triggerAction(
- action: CTInAppAction, callToAction: String?, additionalData: Bundle?
- ) {
- var additionalData = additionalData
- var action = action
- var callToAction = callToAction
- if (action.type == InAppActionType.OPEN_URL) {
- //All URL parameters should be tracked as additional data
- val urlActionData = UriHelper.getAllKeyValuePairs(action.actionUrl, false)
-
- // callToAction is handled as a parameter
- var callToActionUrlParam = urlActionData.getString(Constants.KEY_C2A)
- // no need to keep it in the data bundle
- urlActionData.remove(Constants.KEY_C2A)
-
- // add all additional params, overriding the url params if there is a collision
- if (additionalData != null) {
- urlActionData.putAll(additionalData)
- }
- // Use the merged data for the action
- additionalData = urlActionData
- if (callToActionUrlParam != null) {
- // check if there is a deeplink within the callToAction param
- val parts = callToActionUrlParam.split(Constants.URL_PARAM_DL_SEPARATOR)
- if (parts.size == 2) {
- // Decode it here as it is not decoded by UriHelper
- try {
- // Extract the actual callToAction value
- callToActionUrlParam = URLDecoder.decode(parts[0], "UTF-8")
- } catch (e: Exception) {
- config.logger.debug("Error parsing c2a param", e)
- }
- // use the url from the callToAction param
- action = CTInAppAction.CREATOR.createOpenUrlAction(parts[1])
- }
- }
- if (callToAction == null) {
- // Use the url param value only if no other value is passed
- callToAction = callToActionUrlParam
- }
- }
- val actionData = notifyActionTriggered(action, callToAction ?: "", additionalData)
- didDismiss(actionData)
- }
-
- fun openActionUrl(url: String) {
- triggerAction(CTInAppAction.CREATOR.createOpenUrlAction(url), null, null)
- }
-
fun didDismiss(data: Bundle?) {
- cleanup()
- getListener()?.inAppNotificationDidDismiss(inAppNotification, data)
+ inAppHost.didDismissInApp(data)
}
- fun didShow(data: Bundle?) {
- getListener()?.inAppNotificationDidShow(inAppNotification, data)
- }
-
-
- fun getListener(): InAppListener? {
- val listener = listenerWeakReference?.get()
- if (listener == null) {
- config.logger.verbose(
- config.accountId,
- "InAppListener is null for notification: ${inAppNotification.jsonDescription}"
- )
- }
- return listener
+ override fun onDismissInApp() {
+ cleanup()
}
fun setListener(listener: InAppListener) {
- listenerWeakReference = WeakReference(listener)
+ inAppHost.setInAppListener(listener)
}
fun getScaledPixels(raw: Int): Int {
- return TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP, raw.toFloat(), resources.displayMetrics
- ).toInt()
+ return inAppHost.getScaledPixels(raw)
}
fun handleButtonClickAtIndex(index: Int) {
@@ -223,14 +156,6 @@ internal abstract class CTInAppBaseFragment : Fragment() {
if (action == null) {
action = CTInAppAction.CREATOR.createCloseAction()
}
- return notifyActionTriggered(action, button.text, null)
- }
-
- private fun notifyActionTriggered(
- action: CTInAppAction, callToAction: String, additionalData: Bundle?
- ): Bundle? {
- return getListener()?.inAppNotificationActionTriggered(
- inAppNotification, action, callToAction, additionalData, activity
- )
+ return inAppHost.notifyActionTriggered(action, button.text, null)
}
}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/fragment/CTInAppBaseFullHtmlFragment.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/fragment/CTInAppBaseFullHtmlFragment.kt
index fc88d72b8..44bc1f1e5 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/fragment/CTInAppBaseFullHtmlFragment.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/fragment/CTInAppBaseFullHtmlFragment.kt
@@ -10,14 +10,11 @@ import android.view.ViewGroup
import android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN
import android.webkit.WebViewClient
import android.widget.RelativeLayout
-import com.clevertap.android.sdk.CTWebInterface
import com.clevertap.android.sdk.CleverTapAPI
import com.clevertap.android.sdk.Constants
-import com.clevertap.android.sdk.Logger
import com.clevertap.android.sdk.R
import com.clevertap.android.sdk.customviews.CloseImageView
import com.clevertap.android.sdk.inapp.CTInAppWebView
-import com.clevertap.android.sdk.inapp.InAppWebViewClient
internal abstract class CTInAppBaseFullHtmlFragment : CTInAppBaseFullFragment() {
@@ -78,17 +75,15 @@ internal abstract class CTInAppBaseFullHtmlFragment : CTInAppBaseFullFragment()
inAppNotification.width,
inAppNotification.height,
inAppNotification.widthPercentage,
- inAppNotification.heightPercentage
+ inAppNotification.heightPercentage,
+ inAppHost
)
webView.isFullscreen = isFullscreen
this.webView = webView
- val webViewClient = InAppWebViewClient(this)
- webView.setWebViewClient(webViewClient)
if (inAppNotification.isJsEnabled) {
val instance = CleverTapAPI.instanceWithConfig(activity, config)
- val ctWebInterface = CTWebInterface(instance, this)
- webView.setJavaScriptInterface(ctWebInterface)
+ webView.enableCTJavaScriptInterface(instance)
}
if (isDarkenEnabled()) {
@@ -142,22 +137,8 @@ internal abstract class CTInAppBaseFullHtmlFragment : CTInAppBaseFullFragment()
val customUrl = inAppNotification.customInAppUrl
if (customUrl.isNullOrEmpty()) {
- var mHeight = webView.dim.y
- var mWidth = webView.dim.x
-
- val d = resources.displayMetrics.density
- mHeight = (mHeight / d).toInt()
- mWidth = (mWidth / d).toInt()
-
- var html = inAppNotification.html ?: return
-
- val style =
- ""
- html = html.replaceFirst("".toRegex(), "$style")
- Logger.v("Density appears to be $d")
-
- webView.setInitialScale((d * 100).toInt())
- webView.loadDataWithBaseURL(null, html, "text/html", "utf-8", null)
+ val html = inAppNotification.html ?: return
+ webView.loadInAppHtml(html)
} else {
webView.setWebViewClient(WebViewClient())
webView.loadUrl(customUrl)
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/fragment/CTInAppBasePartialHtmlFragment.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/fragment/CTInAppBasePartialHtmlFragment.kt
index 32f38e545..0b507552a 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/fragment/CTInAppBasePartialHtmlFragment.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/fragment/CTInAppBasePartialHtmlFragment.kt
@@ -5,88 +5,33 @@ import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.view.GestureDetector
-import android.view.GestureDetector.SimpleOnGestureListener
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.View.OnLongClickListener
import android.view.View.OnTouchListener
import android.view.ViewGroup
-import android.view.animation.AlphaAnimation
-import android.view.animation.Animation
-import android.view.animation.AnimationSet
-import android.view.animation.TranslateAnimation
-import com.clevertap.android.sdk.CTWebInterface
import com.clevertap.android.sdk.CleverTapAPI
-import com.clevertap.android.sdk.Logger
-import com.clevertap.android.sdk.inapp.CTInAppAction.CREATOR.createCloseAction
import com.clevertap.android.sdk.inapp.CTInAppWebView
-import com.clevertap.android.sdk.inapp.InAppWebViewClient
-import kotlin.math.abs
+import com.clevertap.android.sdk.inapp.PartialHtmlInAppGestureListener
internal abstract class CTInAppBasePartialHtmlFragment : CTInAppBasePartialFragment(),
OnTouchListener,
OnLongClickListener {
- private inner class GestureListener : SimpleOnGestureListener() {
- override fun onFling(
- e1: MotionEvent?,
- e2: MotionEvent,
- velocityX: Float,
- velocityY: Float
- ): Boolean {
- if (e1 != null) {
- if (e1.x - e2.x > SWIPE_MIN_DISTANCE && abs(velocityX.toDouble()) > SWIPE_THRESHOLD_VELOCITY) {
- // Right to left
- return remove(false)
- } else if (e2.x - e1.x > SWIPE_MIN_DISTANCE && abs(velocityX.toDouble()) > SWIPE_THRESHOLD_VELOCITY) {
- // Left to right
- return remove(true)
- }
- }
- return false
- }
-
- fun remove(ltr: Boolean): Boolean {
- val animSet = AnimationSet(true)
- val anim = if (ltr) {
- TranslateAnimation(0f, getScaledPixels(50).toFloat(), 0f, 0f)
- } else {
- TranslateAnimation(0f, -getScaledPixels(50).toFloat(), 0f, 0f)
- }
- animSet.addAnimation(anim)
- animSet.addAnimation(AlphaAnimation(1f, 0f))
- animSet.setDuration(300)
- animSet.setFillAfter(true)
- animSet.isFillEnabled = true
- animSet.setAnimationListener(object : Animation.AnimationListener {
- override fun onAnimationEnd(animation: Animation?) {
- triggerAction(createCloseAction(), CTA_SWIPE_DISMISS, null)
- }
-
- override fun onAnimationRepeat(animation: Animation?) {
- }
-
- override fun onAnimationStart(animation: Animation?) {
- }
- })
- webView?.startAnimation(animSet)
- return true
- }
- }
-
private lateinit var gd: GestureDetector
+ private lateinit var gestureListener: PartialHtmlInAppGestureListener
private var webView: CTInAppWebView? = null
-
abstract fun getLayout(view: View?): ViewGroup?
abstract fun getView(inflater: LayoutInflater, container: ViewGroup?): View
override fun onAttach(context: Context) {
super.onAttach(context)
- gd = GestureDetector(context, GestureListener())
+ gestureListener = PartialHtmlInAppGestureListener(inAppHost)
+ gd = GestureDetector(context, gestureListener)
}
override fun onCreateView(
@@ -140,18 +85,17 @@ internal abstract class CTInAppBasePartialHtmlFragment : CTInAppBasePartialFragm
inAppNotification.height,
inAppNotification.widthPercentage,
inAppNotification.heightPercentage,
- inAppNotification.aspectRatio
+ inAppNotification.aspectRatio,
+ inAppHost
)
this.webView = webView
- val webViewClient = InAppWebViewClient(this)
- webView.setWebViewClient(webViewClient)
+ gestureListener.webView = webView
webView.setOnTouchListener(this)
webView.setOnLongClickListener(this)
if (inAppNotification.isJsEnabled) {
val instance = CleverTapAPI.instanceWithConfig(activity, config)
- val ctWebInterface = CTWebInterface(instance, this)
- webView.setJavaScriptInterface(ctWebInterface)
+ webView.enableCTJavaScriptInterface(instance)
}
layout?.addView(webView)
@@ -165,28 +109,13 @@ internal abstract class CTInAppBasePartialHtmlFragment : CTInAppBasePartialFragm
private fun reDrawInApp() {
val webView = this.webView ?: return
webView.updateDimension()
-
- var mHeight = webView.dim.y
- var mWidth = webView.dim.x
-
- val d = resources.displayMetrics.density
- mHeight = (mHeight / d).toInt()
- mWidth = (mWidth / d).toInt()
-
- var html = inAppNotification.html ?: return
-
- val style =
- ""
- html = html.replaceFirst("".toRegex(), "$style")
- Logger.v("Density appears to be $d")
-
- webView.setInitialScale((d * 100).toInt())
- webView.loadDataWithBaseURL(null, html, "text/html", "utf-8", null)
+ val html = inAppNotification.html ?: return
+ webView.loadInAppHtml(html)
}
companion object {
- private const val CTA_SWIPE_DISMISS = "swipe-dismiss"
- private const val SWIPE_MIN_DISTANCE = 120
- private const val SWIPE_THRESHOLD_VELOCITY = 200
+ const val CTA_SWIPE_DISMISS = "swipe-dismiss"
+ const val SWIPE_MIN_DISTANCE = 120
+ const val SWIPE_THRESHOLD_VELOCITY = 200
}
}
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/CTWebInterfaceTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/CTWebInterfaceTest.kt
index 6524cdd3d..2b9b0fb02 100644
--- a/clevertap-core/src/test/java/com/clevertap/android/sdk/CTWebInterfaceTest.kt
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/CTWebInterfaceTest.kt
@@ -1,14 +1,17 @@
package com.clevertap.android.sdk
-import com.clevertap.android.sdk.inapp.fragment.CTInAppBaseFragment
+import com.clevertap.android.sdk.inapp.CTInAppHost
import com.clevertap.android.sdk.inapp.InAppActionType.CLOSE
import com.clevertap.android.sdk.inapp.InAppActionType.CUSTOM_CODE
import com.clevertap.android.shared.test.BaseTestCase
-import io.mockk.*
+import io.mockk.called
+import io.mockk.clearMocks
+import io.mockk.mockk
+import io.mockk.verify
import org.json.JSONArray
import org.json.JSONObject
-import org.junit.*
-import org.junit.runner.*
+import org.junit.Test
+import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
@@ -403,9 +406,9 @@ class CTWebInterfaceTest : BaseTestCase() {
}
@Test
- fun `triggerAction should call InAppBaseFragment when provided correct parameters`() {
- val fragmentMock = mockk(relaxed = true)
- val webInterface = CTWebInterface(mockk(relaxed = true), fragmentMock)
+ fun `triggerAction should call InAppHost when provided correct parameters`() {
+ val hostMock = mockk(relaxed = true)
+ val webInterface = CTWebInterface(mockk(relaxed = true), hostMock)
val closeActionJson = """{
"type":"$CLOSE",
@@ -417,8 +420,8 @@ class CTWebInterfaceTest : BaseTestCase() {
}"""
webInterface.triggerInAppAction(closeActionJson, "close", null)
- verify { fragmentMock.triggerAction(match { it.type == CLOSE }, "close", any()) }
- clearMocks(fragmentMock)
+ verify { hostMock.triggerAction(match { it.type == CLOSE }, "close", any()) }
+ clearMocks(hostMock)
val customTemplateAction = """{
"type": "$CUSTOM_CODE",
@@ -432,29 +435,29 @@ class CTWebInterfaceTest : BaseTestCase() {
"templateDescription": "Description"
}"""
webInterface.triggerInAppAction(customTemplateAction, "function-a", "buttonId")
- verify { fragmentMock.triggerAction(match { it.type == CUSTOM_CODE }, "function-a", any()) }
+ verify { hostMock.triggerAction(match { it.type == CUSTOM_CODE }, "function-a", any()) }
}
@Test
- fun `triggerAction should not call InAppBaseFragment when provided invalid params`() {
- val fragmentMock = mockk(relaxed = true)
- val webInterface = CTWebInterface(mockk(relaxed = true), fragmentMock)
+ fun `triggerAction should not call InAppHost when provided invalid params`() {
+ val hostMock = mockk(relaxed = true)
+ val webInterface = CTWebInterface(mockk(relaxed = true), hostMock)
webInterface.triggerInAppAction("close", "action", null)
- verify { fragmentMock wasNot called }
+ verify { hostMock wasNot called }
webInterface.triggerInAppAction(null, null, null)
- verify { fragmentMock wasNot called }
+ verify { hostMock wasNot called }
}
@Test
- fun `triggerAction should do nothing when CleverTapAPI or InAppBaseFragment is null`() {
+ fun `triggerAction should do nothing when CleverTapAPI or InAppHost is null`() {
CTWebInterface(null, null).triggerInAppAction(null, null, null)
CTWebInterface(mockk(), null).triggerInAppAction(null, null, null)
- val fragmentMock = mockk(relaxed = true)
- CTWebInterface(null, fragmentMock).triggerInAppAction(null, null, null)
- verify { fragmentMock wasNot called }
+ val hostMock = mockk(relaxed = true)
+ CTWebInterface(null, hostMock).triggerInAppAction(null, null, null)
+ verify { hostMock wasNot called }
}
}
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/CTInAppHostTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/CTInAppHostTest.kt
new file mode 100644
index 000000000..c282ee56e
--- /dev/null
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/CTInAppHostTest.kt
@@ -0,0 +1,207 @@
+package com.clevertap.android.sdk.inapp
+
+import android.net.Uri
+import android.os.Bundle
+import com.clevertap.android.sdk.Constants
+import com.clevertap.android.sdk.utils.configMock
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import io.mockk.verify
+import org.json.JSONObject
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class CTInAppHostTest {
+
+ private lateinit var mockInAppListener: InAppListener
+
+ @Before
+ fun setUp() {
+ mockInAppListener = mockk(relaxed = true)
+ }
+
+ @After
+ fun cleanUp() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `triggerAction should parse url parameters as additionalData`() {
+ val host = createInAppHost()
+
+ val param1 = "value"
+ val param2 = "value 2"
+ val param3 = "5"
+ val url = Uri.parse("https://clevertap.com")
+ .buildUpon()
+ .appendQueryParameter("param1", param1)
+ .appendQueryParameter("param2", param2)
+ .appendQueryParameter("param3", param3)
+ .build().toString()
+
+ host.triggerAction(CTInAppAction.CREATOR.createOpenUrlAction(url), null, null)
+ verify {
+ mockInAppListener.inAppNotificationActionTriggered(
+ inAppNotification = any(),
+ action = match { action ->
+ action.type == InAppActionType.OPEN_URL
+ && action.actionUrl == url
+ },
+ callToAction = any(),
+ additionalData = match { data ->
+ param1 == data.getString("param1")
+ && param2 == data.getString("param2")
+ && param3 == data.getString("param3")
+ },
+ activityContext = any()
+ )
+ }
+ }
+
+ @Test
+ fun `triggerAction should merge url parameters with provided additionalData `() {
+ val host = createInAppHost()
+
+ val urlParam1 = "value"
+ val urlParam2 = "value 2"
+ val urlParam3 = "5"
+ val url = Uri.parse("https://clevertap.com")
+ .buildUpon()
+ .appendQueryParameter("param1", urlParam1)
+ .appendQueryParameter("param2", urlParam2)
+ .appendQueryParameter("param3", urlParam3)
+ .build().toString()
+
+ val dataParam1 = "dataValue"
+ val dataParam2 = "data value 2"
+ val data = Bundle().apply {
+ putString("param1", dataParam1)
+ putString("param2", dataParam2)
+ }
+
+ host.triggerAction(CTInAppAction.CREATOR.createOpenUrlAction(url), null, data)
+ verify {
+ mockInAppListener.inAppNotificationActionTriggered(
+ inAppNotification = any(),
+ action = any(),
+ callToAction = any(),
+ additionalData = match { data ->
+ dataParam1 == data.getString("param1")
+ && dataParam2 == data.getString("param2")
+ && urlParam3 == data.getString("param3")
+ },
+ activityContext = any()
+ )
+ }
+ }
+
+ @Test
+ fun `triggerAction should use callToAction argument or c2a url param`() {
+ val host = createInAppHost()
+
+ val callToActionParam = "c2aParam"
+ val url = Uri.parse("https://clevertap.com")
+ .buildUpon()
+ .appendQueryParameter(Constants.KEY_C2A, callToActionParam)
+ .build().toString()
+
+ host.triggerAction(CTInAppAction.CREATOR.createOpenUrlAction(url), null, null)
+ verify {
+ mockInAppListener.inAppNotificationActionTriggered(
+ inAppNotification = any(),
+ action = any(),
+ callToAction = callToActionParam,
+ additionalData = any(),
+ activityContext = any()
+ )
+ }
+
+ val callToActionArgument = "argument"
+ host.triggerAction(
+ CTInAppAction.CREATOR.createOpenUrlAction(url),
+ callToActionArgument,
+ null
+ )
+ verify {
+ mockInAppListener.inAppNotificationActionTriggered(
+ inAppNotification = any(),
+ action = any(),
+ callToAction = callToActionArgument,
+ additionalData = any(),
+ activityContext = any()
+ )
+ }
+ }
+
+ @Test
+ fun `triggerAction should parse c2a url param with __dl__ data`() {
+ val host = createInAppHost()
+
+ val dl = "https://deeplink.com?param1=asd¶m2=value2"
+ val callToActionParam = "c2aParam"
+ val param1 = "value"
+ val url = Uri.parse("https://clevertap.com")
+ .buildUpon()
+ .appendQueryParameter(Constants.KEY_C2A, "${callToActionParam}__dl__$dl")
+ .appendQueryParameter("param1", param1)
+ .build().toString()
+
+ host.triggerAction(CTInAppAction.CREATOR.createOpenUrlAction(url), null, null)
+ verify {
+ mockInAppListener.inAppNotificationActionTriggered(
+ inAppNotification = any(),
+ action = match { action ->
+ // the open-url action should be performed with the url after __dl__
+ dl == action.actionUrl
+ },
+ callToAction = callToActionParam,
+ additionalData = match { data ->
+ // only the params of the original url should be tracked
+ data.size() == 1
+ && param1 == data.getString("param1")
+ },
+ activityContext = any()
+ )
+ }
+
+ val callToActionArgument = "argument"
+ host.triggerAction(
+ CTInAppAction.CREATOR.createOpenUrlAction(url),
+ callToActionArgument,
+ null
+ )
+ verify {
+ mockInAppListener.inAppNotificationActionTriggered(
+ inAppNotification = any(),
+ action = match { action ->
+ // the open-url action should be performed with the url after __dl__
+ dl == action.actionUrl
+ },
+ callToAction = callToActionArgument,
+ additionalData = match { data ->
+ // only the params of the original url should be tracked
+ data.size() == 1
+ && param1 == data.getString("param1")
+ },
+ activityContext = any()
+ )
+ }
+ }
+
+ private fun createInAppHost(callbacks: CTInAppHost.Callbacks? = null): CTInAppHost {
+ return CTInAppHost(
+ inAppListener = mockInAppListener,
+ config = configMock(),
+ inAppNotification = CTInAppNotification(
+ JSONObject(InAppFixtures.TYPE_ADVANCED_BUILDER_INTERSTITIAL),
+ false
+ ),
+ callbacks = callbacks,
+ context = null
+ )
+ }
+}
\ No newline at end of file
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/fragment/CTInAppBaseFragmentTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/fragment/CTInAppBaseFragmentTest.kt
index 486a79faa..f1f2071df 100644
--- a/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/fragment/CTInAppBaseFragmentTest.kt
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/fragment/CTInAppBaseFragmentTest.kt
@@ -3,20 +3,14 @@ package com.clevertap.android.sdk.inapp.fragment
import android.app.Activity
import android.content.Context
import android.content.res.Resources
-import android.net.Uri
-import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import com.clevertap.android.sdk.CleverTapInstanceConfig
-import com.clevertap.android.sdk.Constants
import com.clevertap.android.sdk.DidClickForHardPermissionListener
-import com.clevertap.android.sdk.inapp.CTInAppAction
import com.clevertap.android.sdk.inapp.CTInAppNotification
import com.clevertap.android.sdk.inapp.CTLocalInApp
-import com.clevertap.android.sdk.inapp.InAppActionType
import com.clevertap.android.sdk.inapp.InAppFixtures
-import com.clevertap.android.sdk.inapp.InAppListener
import com.clevertap.android.sdk.utils.configMock
import io.mockk.confirmVerified
import io.mockk.every
@@ -26,7 +20,6 @@ import io.mockk.unmockkAll
import io.mockk.verify
import org.json.JSONObject
import org.junit.After
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@@ -36,13 +29,6 @@ import kotlin.test.assertTrue
@RunWith(RobolectricTestRunner::class)
class CTInAppBaseFragmentTest {
- private lateinit var mockInAppListener: InAppListener
-
- @Before
- fun setUp() {
- mockInAppListener = mockk(relaxed = true)
- }
-
@After
fun cleanUp() {
unmockkAll()
@@ -176,164 +162,8 @@ class CTInAppBaseFragmentTest {
verify(exactly = 1) { fragment.didDismiss(any()) }
}
- @Test
- fun `triggerAction should parse url parameters as additionalData`() {
- val fragment = createAndAttachFragmentSpy()
-
- val param1 = "value"
- val param2 = "value 2"
- val param3 = "5"
- val url = Uri.parse("https://clevertap.com")
- .buildUpon()
- .appendQueryParameter("param1", param1)
- .appendQueryParameter("param2", param2)
- .appendQueryParameter("param3", param3)
- .build().toString()
-
- fragment.triggerAction(CTInAppAction.CREATOR.createOpenUrlAction(url), null, null)
- verify {
- mockInAppListener.inAppNotificationActionTriggered(
- inAppNotification = any(),
- action = match { action ->
- action.type == InAppActionType.OPEN_URL
- && action.actionUrl == url
- },
- callToAction = any(),
- additionalData = match { data ->
- param1 == data.getString("param1")
- && param2 == data.getString("param2")
- && param3 == data.getString("param3")
- },
- activityContext = any()
- )
- }
- }
-
- @Test
- fun `triggerAction should merge url parameters with provided additionalData `() {
- val fragment = createAndAttachFragmentSpy()
-
- val urlParam1 = "value"
- val urlParam2 = "value 2"
- val urlParam3 = "5"
- val url = Uri.parse("https://clevertap.com")
- .buildUpon()
- .appendQueryParameter("param1", urlParam1)
- .appendQueryParameter("param2", urlParam2)
- .appendQueryParameter("param3", urlParam3)
- .build().toString()
-
- val dataParam1 = "dataValue"
- val dataParam2 = "data value 2"
- val data = Bundle().apply {
- putString("param1", dataParam1)
- putString("param2", dataParam2)
- }
-
- fragment.triggerAction(CTInAppAction.CREATOR.createOpenUrlAction(url), null, data)
- verify {
- mockInAppListener.inAppNotificationActionTriggered(
- inAppNotification = any(),
- action = any(),
- callToAction = any(),
- additionalData = match { data ->
- dataParam1 == data.getString("param1")
- && dataParam2 == data.getString("param2")
- && urlParam3 == data.getString("param3")
- },
- activityContext = any()
- )
- }
- }
-
- @Test
- fun `triggerAction should use callToAction argument or c2a url param`() {
- val fragment = createAndAttachFragmentSpy()
-
- val callToActionParam = "c2aParam"
- val url = Uri.parse("https://clevertap.com")
- .buildUpon()
- .appendQueryParameter(Constants.KEY_C2A, callToActionParam)
- .build().toString()
-
- fragment.triggerAction(CTInAppAction.CREATOR.createOpenUrlAction(url), null, null)
- verify {
- mockInAppListener.inAppNotificationActionTriggered(
- inAppNotification = any(),
- action = any(),
- callToAction = callToActionParam,
- additionalData = any(),
- activityContext = any()
- )
- }
-
- val callToActionArgument = "argument"
- fragment.triggerAction(CTInAppAction.CREATOR.createOpenUrlAction(url), callToActionArgument, null)
- verify {
- mockInAppListener.inAppNotificationActionTriggered(
- inAppNotification = any(),
- action = any(),
- callToAction = callToActionArgument,
- additionalData = any(),
- activityContext = any()
- )
- }
- }
-
- @Test
- fun `triggerAction should parse c2a url param with __dl__ data`() {
- val fragment = createAndAttachFragmentSpy()
-
- val dl = "https://deeplink.com?param1=asd¶m2=value2"
- val callToActionParam = "c2aParam"
- val param1 = "value"
- val url = Uri.parse("https://clevertap.com")
- .buildUpon()
- .appendQueryParameter(Constants.KEY_C2A, "${callToActionParam}__dl__$dl")
- .appendQueryParameter("param1", param1)
- .build().toString()
-
- fragment.triggerAction(CTInAppAction.CREATOR.createOpenUrlAction(url), null, null)
- verify {
- mockInAppListener.inAppNotificationActionTriggered(
- inAppNotification = any(),
- action = match { action ->
- // the open-url action should be performed with the url after __dl__
- dl == action.actionUrl
- },
- callToAction = callToActionParam,
- additionalData = match { data ->
- // only the params of the original url should be tracked
- data.size() == 1
- && param1 == data.getString("param1")
- },
- activityContext = any()
- )
- }
-
- val callToActionArgument = "argument"
- fragment.triggerAction(CTInAppAction.CREATOR.createOpenUrlAction(url), callToActionArgument, null)
- verify {
- mockInAppListener.inAppNotificationActionTriggered(
- inAppNotification = any(),
- action = match { action ->
- // the open-url action should be performed with the url after __dl__
- dl == action.actionUrl
- },
- callToAction = callToActionArgument,
- additionalData = match { data ->
- // only the params of the original url should be tracked
- data.size() == 1
- && param1 == data.getString("param1")
- },
- activityContext = any()
- )
- }
- }
-
private fun createFragmentSpy(): CTInAppBaseFragment {
val fragmentSpy = spyk()
- every { fragmentSpy.getListener() } returns mockInAppListener
val mockResources = mockk(relaxed = true)
every { mockResources.configuration } returns mockk(relaxed = true)
every { fragmentSpy.resources } returns mockResources
diff --git a/gradle-scripts/jacoco_root.gradle b/gradle-scripts/jacoco_root.gradle
index b2077ed31..b5be68edd 100644
--- a/gradle-scripts/jacoco_root.gradle
+++ b/gradle-scripts/jacoco_root.gradle
@@ -40,8 +40,8 @@ ext.excludes = [
'com/clevertap/android/pushtemplates/styles/**',
'com/clevertap/android/sdk/InAppNotificationActivity*.*',
'com/clevertap/android/sdk/inbox/CTInboxActivity*.*',
- 'com/clevertap/android/sdk/inapp/InAppWebViewClient*.*',
'com/clevertap/android/sdk/inapp/fragment/**',
+ 'com/clevertap/android/sdk/inapp/CTInAppHtmlBannerOverlay*.*',
'com/clevertap/android/sdk/inapp/CTInAppWebView*.*',
'com/clevertap/android/sdk/inbox/CTInboxTabAdapter*.*',
'com/clevertap/android/sdk/inbox/CTCarouselViewPager*.*',