diff --git a/android/build.gradle b/android/build.gradle index 05e0d2d1..ca5b85c0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -3,6 +3,24 @@ plugins { id "org.jetbrains.kotlin.android" } +def omSdkLocalMavenRepo = uri("${project.projectDir}/local-maven") + +rootProject.allprojects { + repositories { + maven { + url omSdkLocalMavenRepo + } + } +} + +repositories { + google() + mavenCentral() + maven { + url omSdkLocalMavenRepo + } +} + android { namespace "so.kontext.sdk.flutter" compileSdk = 34 @@ -27,4 +45,5 @@ android { dependencies { implementation "androidx.webkit:webkit:1.8.0" implementation "com.google.android.gms:play-services-ads-identifier:18.0.1" + implementation "iab:omsdk-android:1.6.4" } diff --git a/android/local-maven/iab/omsdk-android/1.6.4/omsdk-android-1.6.4.aar b/android/local-maven/iab/omsdk-android/1.6.4/omsdk-android-1.6.4.aar new file mode 100644 index 00000000..4f69f041 Binary files /dev/null and b/android/local-maven/iab/omsdk-android/1.6.4/omsdk-android-1.6.4.aar differ diff --git a/android/local-maven/iab/omsdk-android/1.6.4/omsdk-android-1.6.4.pom b/android/local-maven/iab/omsdk-android/1.6.4/omsdk-android-1.6.4.pom new file mode 100644 index 00000000..e2676bcd --- /dev/null +++ b/android/local-maven/iab/omsdk-android/1.6.4/omsdk-android-1.6.4.pom @@ -0,0 +1,8 @@ + + + 4.0.0 + iab + omsdk-android + 1.6.4 + aar + diff --git a/android/src/main/kotlin/so/kontext/sdk/flutter/KontextInAppWebView.kt b/android/src/main/kotlin/so/kontext/sdk/flutter/KontextInAppWebView.kt index 15ea39f4..8fe229d3 100644 --- a/android/src/main/kotlin/so/kontext/sdk/flutter/KontextInAppWebView.kt +++ b/android/src/main/kotlin/so/kontext/sdk/flutter/KontextInAppWebView.kt @@ -6,6 +6,7 @@ import android.graphics.Color import android.os.Build import android.os.Handler import android.os.Looper +import android.util.Log import android.view.View import android.view.ViewGroup import android.webkit.CookieManager @@ -24,6 +25,10 @@ import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.platform.PlatformView +import so.kontext.sdk.flutter.omsdk.OMConstants +import so.kontext.sdk.flutter.omsdk.OMCreativeType +import so.kontext.sdk.flutter.omsdk.WebViewOMLifecycle + private const val CHANNEL_PREFIX = "kontext_flutter_sdk/in_app_webview/" private const val JAVASCRIPT_BRIDGE_NAME = "flutter_inappwebview" private const val MAX_BYPASS_MAIN_FRAME_LOADS = 100 @@ -53,10 +58,22 @@ internal class KontextInAppWebView( private val initialUrl = readInitialUrl(creationParams) private val settings = readSettings(creationParams) private val initialUserScripts = readUserScripts(creationParams) - private var hasLoadedInitialUrl = false - - private val bypassMainFrameLoads = linkedSetOf() private val documentStartSupported = WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT) + private val openMeasurementJavascript = if (documentStartSupported) { + loadOpenMeasurementJavaScript(context) + } else { + null + } + private val omLifecycle = WebViewOMLifecycle( + webView = webView, + initialContentUrl = initialUrl, + initialCreativeType = readInitialOmCreativeType(creationParams), + canUseOpenMeasurement = ::canUseOpenMeasurement, + logUnsupportedOpenMeasurement = ::logUnsupportedOpenMeasurement, + ) + private val bypassMainFrameLoads = linkedSetOf() + + private var hasLoadedInitialUrl = false private val bridgeScript = """ (function() { @@ -129,10 +146,12 @@ internal class KontextInAppWebView( override fun getView(): View = webView override fun dispose() { + omLifecycle.finish() channel.setMethodCallHandler(null) - webView.stopLoading() webView.removeJavascriptInterface(JAVASCRIPT_BRIDGE_NAME) - webView.destroy() + if (!omLifecycle.dispose()) { + destroyWebViewImmediately() + } } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { @@ -155,6 +174,33 @@ internal class KontextInAppWebView( result.success(null) } } + "configureOpenMeasurement" -> { + mainHandler.post { + omLifecycle.configure(call.argument("creativeType")) + result.success(null) + } + } + "startOpenMeasurementSession" -> { + mainHandler.post { + omLifecycle.requestStart() + result.success(null) + } + } + "logOpenMeasurementError" -> { + mainHandler.post { + omLifecycle.logError( + errorType = call.argument("errorType"), + message = call.argument("message"), + ) + result.success(null) + } + } + "finishOpenMeasurementSession" -> { + mainHandler.post { + omLifecycle.finish() + result.success(null) + } + } else -> result.notImplemented() } } @@ -192,6 +238,7 @@ internal class KontextInAppWebView( } if (documentStartSupported) { + openMeasurementJavascript?.let(::addDocumentStartScript) addDocumentStartScript(bridgeScript) // To avoid Android WebView loader for videos, inject JS code with 1x1 transparent pixel. addDocumentStartScript(posterStartScript) @@ -260,6 +307,7 @@ internal class KontextInAppWebView( override fun onPageStarted(view: WebView, url: String?, favicon: android.graphics.Bitmap?) { super.onPageStarted(view, url, favicon) + omLifecycle.markPageStarted(url) if (!documentStartSupported) { // Older WebView versions do not support document-start scripts, so this // fallback injects asynchronously and may still lose the race to early @@ -277,6 +325,7 @@ internal class KontextInAppWebView( .filter { it.injectionTime == "AT_DOCUMENT_END" } .forEach { evaluateJavascript(it.source) } evaluateJavascript(PLATFORM_READY_SCRIPT) + omLifecycle.markPageFinished(url) } override fun onReceivedError( @@ -342,6 +391,7 @@ internal class KontextInAppWebView( return } hasLoadedInitialUrl = true + omLifecycle.markPageStarted(initialUrl) if (!initialUrl.isNullOrBlank()) { webView.loadUrl(initialUrl) } @@ -395,6 +445,30 @@ internal class KontextInAppWebView( """.trimIndent() } + private fun canUseOpenMeasurement(): Boolean = documentStartSupported && openMeasurementJavascript != null + + private fun logUnsupportedOpenMeasurement() { + Log.w( + OMConstants.logTag, + if (!documentStartSupported) { + "DOCUMENT_START_SCRIPT not supported, OM SDK disabled for this WebView" + } else { + "OM SDK JavaScript resource could not be loaded, OM SDK disabled for this WebView" + } + ) + } + + private fun destroyWebViewImmediately() { + try { + (webView.parent as? ViewGroup)?.removeView(webView) + webView.stopLoading() + webView.loadUrl("about:blank") + webView.destroy() + } catch (exception: Throwable) { + Log.w(OMConstants.logTag, "Immediate WebView destroy failed", exception) + } + } + private fun evaluateJavascript(source: String) { webView.evaluateJavascript(source, null) } @@ -464,6 +538,10 @@ private data class AndroidUserScript( val injectionTime: String, ) +private fun readInitialOmCreativeType(creationParams: Map?): OMCreativeType? { + return OMCreativeType.fromRawValue(creationParams?.get("initialOmCreativeType") as? String) +} + private fun readInitialUrl(creationParams: Map?): String? { val initialRequest = creationParams?.get("initialUrlRequest") as? Map<*, *> return initialRequest?.get("url") as? String @@ -494,6 +572,17 @@ private fun readUserScripts(creationParams: Map?): List "ERROR" diff --git a/android/src/main/kotlin/so/kontext/sdk/flutter/KontextSdkPlugin.kt b/android/src/main/kotlin/so/kontext/sdk/flutter/KontextSdkPlugin.kt index e83ad008..b32bd180 100644 --- a/android/src/main/kotlin/so/kontext/sdk/flutter/KontextSdkPlugin.kt +++ b/android/src/main/kotlin/so/kontext/sdk/flutter/KontextSdkPlugin.kt @@ -1,6 +1,7 @@ package so.kontext.sdk.flutter import io.flutter.embedding.engine.plugins.FlutterPlugin +import so.kontext.sdk.flutter.omsdk.OMSDKPlugin class KontextSdkPlugin : FlutterPlugin { private val advertisingId = AdvertisingIdPlugin() @@ -10,6 +11,7 @@ class KontextSdkPlugin : FlutterPlugin { private val inAppWebView = KontextInAppWebViewPlugin() private val network = DeviceNetworkPlugin() private val os = OperationSystemPlugin() + private val omsdk = OMSDKPlugin() private val power = DevicePowerPlugin() private val tcf = TransparencyConsentFramework() @@ -21,6 +23,7 @@ class KontextSdkPlugin : FlutterPlugin { inAppWebView.onAttachedToEngine(binding) network.onAttachedToEngine(binding) os.onAttachedToEngine(binding) + omsdk.onAttachedToEngine(binding) power.onAttachedToEngine(binding) tcf.onAttachedToEngine(binding) } @@ -32,6 +35,7 @@ class KontextSdkPlugin : FlutterPlugin { hardware.onDetachedFromEngine(binding) network.onDetachedFromEngine(binding) os.onDetachedFromEngine(binding) + omsdk.onDetachedFromEngine(binding) power.onDetachedFromEngine(binding) tcf.onDetachedFromEngine(binding) } diff --git a/android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/OMConstants.kt b/android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/OMConstants.kt new file mode 100644 index 00000000..ebc535d7 --- /dev/null +++ b/android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/OMConstants.kt @@ -0,0 +1,9 @@ +package so.kontext.sdk.flutter.omsdk + +internal object OMConstants { + const val channelName = "kontext_flutter_sdk/omsdk" + const val integrationVersion = "1.0.0" + const val logTag = "Kontext SDK" + const val partnerName = "Kontextso" + const val retentionIntervalMillis = 1000L +} diff --git a/android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/OMManager.kt b/android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/OMManager.kt new file mode 100644 index 00000000..e1644e51 --- /dev/null +++ b/android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/OMManager.kt @@ -0,0 +1,96 @@ +package so.kontext.sdk.flutter.omsdk + +import android.content.Context +import android.util.Log +import android.webkit.WebView +import com.iab.omid.library.kontextso.Omid +import com.iab.omid.library.kontextso.adsession.AdSession +import com.iab.omid.library.kontextso.adsession.AdSessionConfiguration +import com.iab.omid.library.kontextso.adsession.AdSessionContext +import com.iab.omid.library.kontextso.adsession.CreativeType +import com.iab.omid.library.kontextso.adsession.ImpressionType +import com.iab.omid.library.kontextso.adsession.Owner +import com.iab.omid.library.kontextso.adsession.Partner + +internal enum class OMCreativeType(val rawValue: String) { + DISPLAY("display"), + VIDEO("video"); + + companion object { + fun fromRawValue(value: String?): OMCreativeType? = values().firstOrNull { it.rawValue == value } + } +} + +internal object OMManager { + private val partner: Partner? by lazy { + try { + Partner.createPartner(OMConstants.partnerName, OMConstants.integrationVersion) + } catch (exception: IllegalArgumentException) { + Log.e(OMConstants.logTag, "OM partner creation failed", exception) + null + } + } + + fun activate(context: Context): Boolean { + if (Omid.isActive()) { + return true + } + + return try { + Omid.activate(context.applicationContext) + Omid.isActive() + } catch (exception: IllegalArgumentException) { + Log.e(OMConstants.logTag, "OM SDK activation failed", exception) + false + } catch (exception: IllegalStateException) { + Log.e(OMConstants.logTag, "OM SDK activation failed", exception) + false + } + } + + fun createSession( + webView: WebView, + contentUrl: String?, + creativeType: OMCreativeType, + ): OMSession? { + if (!Omid.isActive()) { + Log.w(OMConstants.logTag, "OM session creation skipped because the SDK is not active") + return null + } + + val partner = partner + if (partner == null) { + return null + } + + return try { + val context = AdSessionContext.createHtmlAdSessionContext( + partner, + webView, + contentUrl, + "", + ) + val (omCreativeType, mediaEventsOwner) = when (creativeType) { + OMCreativeType.DISPLAY -> CreativeType.HTML_DISPLAY to Owner.NONE + OMCreativeType.VIDEO -> CreativeType.VIDEO to Owner.JAVASCRIPT + } + val configuration = AdSessionConfiguration.createAdSessionConfiguration( + omCreativeType, + ImpressionType.BEGIN_TO_RENDER, + Owner.JAVASCRIPT, + mediaEventsOwner, + false, + ) + val session = AdSession.createAdSession(configuration, context).apply { + registerAdView(webView) + } + OMSession(session = session, webView = webView) + } catch (exception: IllegalArgumentException) { + Log.e(OMConstants.logTag, "OM session creation failed", exception) + null + } catch (exception: IllegalStateException) { + Log.e(OMConstants.logTag, "OM session creation failed", exception) + null + } + } +} diff --git a/android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/OMRetentionPool.kt b/android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/OMRetentionPool.kt new file mode 100644 index 00000000..5fbf4742 --- /dev/null +++ b/android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/OMRetentionPool.kt @@ -0,0 +1,24 @@ +package so.kontext.sdk.flutter.omsdk + +import android.os.Handler +import android.os.Looper +import java.util.UUID + +internal object OMRetentionPool { + private val mainHandler = Handler(Looper.getMainLooper()) + private val retainedWebViews = mutableMapOf() + + fun retain( + webView: OMRetainedWebView, + delayMillis: Long = OMConstants.retentionIntervalMillis, + ) { + val id = UUID.randomUUID().toString() + retainedWebViews[id] = webView + mainHandler.postDelayed( + { + retainedWebViews.remove(id)?.destroy() + }, + delayMillis, + ) + } +} diff --git a/android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/OMSDKPlugin.kt b/android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/OMSDKPlugin.kt new file mode 100644 index 00000000..69493b24 --- /dev/null +++ b/android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/OMSDKPlugin.kt @@ -0,0 +1,28 @@ +package so.kontext.sdk.flutter.omsdk + +import android.content.Context +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel + +class OMSDKPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { + private lateinit var channel: MethodChannel + private lateinit var context: Context + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + context = binding.applicationContext + channel = MethodChannel(binding.binaryMessenger, OMConstants.channelName) + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "activate" -> result.success(OMManager.activate(context)) + else -> result.notImplemented() + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} diff --git a/android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/OMSession.kt b/android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/OMSession.kt new file mode 100644 index 00000000..7c9a9dfe --- /dev/null +++ b/android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/OMSession.kt @@ -0,0 +1,54 @@ +package so.kontext.sdk.flutter.omsdk + +import android.util.Log +import android.view.ViewGroup +import android.webkit.WebView +import com.iab.omid.library.kontextso.adsession.AdSession +import com.iab.omid.library.kontextso.adsession.ErrorType + +internal class OMSession( + private val session: AdSession, + private val webView: WebView, +) { + fun start() { + session.start() + } + + fun retire() { + try { + webView.evaluateJavascript("window.postMessage({ type: 'retire-iframe' }, '*');", null) + } catch (exception: Throwable) { + Log.w(OMConstants.logTag, "OM retire message failed", exception) + } + } + + fun finish() { + session.finish() + } + + fun logError(errorType: String?, message: String?) { + val omErrorType = if (errorType == "video") ErrorType.VIDEO else ErrorType.GENERIC + try { + session.error(omErrorType, message ?: "unknown") + } catch (exception: IllegalStateException) { + Log.e(OMConstants.logTag, "OM error logging failed", exception) + } + } + + fun retainedWebView(): OMRetainedWebView = OMRetainedWebView(webView) +} + +internal class OMRetainedWebView( + private val webView: WebView, +) { + fun destroy() { + try { + (webView.parent as? ViewGroup)?.removeView(webView) + webView.stopLoading() + webView.loadUrl("about:blank") + webView.destroy() + } catch (exception: Throwable) { + Log.w(OMConstants.logTag, "Deferred WebView destroy failed", exception) + } + } +} diff --git a/android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/WebViewOMLifecycle.kt b/android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/WebViewOMLifecycle.kt new file mode 100644 index 00000000..1e392e43 --- /dev/null +++ b/android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/WebViewOMLifecycle.kt @@ -0,0 +1,154 @@ +package so.kontext.sdk.flutter.omsdk + +import android.os.SystemClock +import android.util.Log +import android.webkit.WebView + +internal class WebViewOMLifecycle( + private val webView: WebView, + private val initialContentUrl: String?, + initialCreativeType: OMCreativeType?, + private val canUseOpenMeasurement: () -> Boolean, + private val logUnsupportedOpenMeasurement: () -> Unit, +) { + private var activeOMSession: OMSession? = null + private var hasDeferredDestroy = false + private var hasLoadedPage = false + private var hasLoggedUnsupportedOpenMeasurement = false + private var lastContentUrl: String? = initialContentUrl + private var lastOpenMeasurementFinishTimestampMillis: Long? = null + private var omCreativeType: OMCreativeType? = initialCreativeType + private var pendingOpenMeasurementStart = false + + init { + if (omCreativeType != null && !canUseOpenMeasurement()) { + maybeLogUnsupportedOpenMeasurement() + omCreativeType = null + } + } + + fun configure(creativeTypeRaw: String?) { + val parsedCreativeType = OMCreativeType.fromRawValue(creativeTypeRaw) + if (parsedCreativeType != null && !canUseOpenMeasurement()) { + maybeLogUnsupportedOpenMeasurement() + omCreativeType = null + return + } + + omCreativeType = parsedCreativeType + startOpenMeasurementSessionIfReady() + } + + fun markPageStarted(url: String?) { + hasLoadedPage = false + lastContentUrl = url ?: initialContentUrl + } + + fun markPageFinished(url: String?) { + hasLoadedPage = true + lastContentUrl = url ?: initialContentUrl + startOpenMeasurementSessionIfReady() + } + + fun requestStart() { + if (!canUseOpenMeasurement()) { + if (omCreativeType != null) { + maybeLogUnsupportedOpenMeasurement() + } + return + } + + pendingOpenMeasurementStart = true + startOpenMeasurementSessionIfReady() + } + + fun logError(errorType: String?, message: String?) { + activeOMSession?.logError(errorType = errorType, message = message) + } + + fun finish() { + pendingOpenMeasurementStart = false + + val activeOMSession = activeOMSession ?: return + this.activeOMSession = null + activeOMSession.retire() + activeOMSession.finish() + lastOpenMeasurementFinishTimestampMillis = SystemClock.uptimeMillis() + } + + fun dispose(): Boolean { + if (hasDeferredDestroy) { + return true + } + + val remainingOpenMeasurementRetentionMillis = remainingOpenMeasurementRetentionMillis() + if (remainingOpenMeasurementRetentionMillis <= 0L) { + return false + } + + hasDeferredDestroy = true + OMRetentionPool.retain( + webView = OMRetainedWebView(webView), + delayMillis = remainingOpenMeasurementRetentionMillis, + ) + return true + } + + private fun startOpenMeasurementSessionIfReady() { + if (activeOMSession != null || hasDeferredDestroy) { + return + } + + if (!pendingOpenMeasurementStart) { + return + } + + val omCreativeType = omCreativeType ?: return + if (!hasLoadedPage) { + return + } + + if (!canUseOpenMeasurement()) { + maybeLogUnsupportedOpenMeasurement() + pendingOpenMeasurementStart = false + return + } + + if (!OMManager.activate(webView.context)) { + return + } + + val session = OMManager.createSession( + webView = webView, + contentUrl = lastContentUrl ?: webView.url?.toString() ?: initialContentUrl, + creativeType = omCreativeType, + ) ?: run { + pendingOpenMeasurementStart = false + return + } + + try { + session.start() + activeOMSession = session + lastOpenMeasurementFinishTimestampMillis = null + pendingOpenMeasurementStart = false + } catch (exception: IllegalStateException) { + Log.e(OMConstants.logTag, "OM session start failed", exception) + pendingOpenMeasurementStart = false + } + } + + private fun maybeLogUnsupportedOpenMeasurement() { + if (hasLoggedUnsupportedOpenMeasurement) { + return + } + hasLoggedUnsupportedOpenMeasurement = true + logUnsupportedOpenMeasurement() + } + + private fun remainingOpenMeasurementRetentionMillis(): Long { + val finishedAt = lastOpenMeasurementFinishTimestampMillis ?: return 0L + val elapsedMillis = SystemClock.uptimeMillis() - finishedAt + return (OMConstants.retentionIntervalMillis - elapsedMillis).coerceAtLeast(0L) + } +} diff --git a/android/src/main/res/raw/omsdk_v1.js b/android/src/main/res/raw/omsdk_v1.js new file mode 100644 index 00000000..c84aeb27 --- /dev/null +++ b/android/src/main/res/raw/omsdk_v1.js @@ -0,0 +1,99 @@ +;(function(omidGlobal) { + var n;function aa(a){var b=0;return function(){return bc&&(c=Math.max(c+e,0));c>>0)+'_',e=0;return b}); +u('Symbol.iterator',function(a){if(a)return a;a=Symbol('Symbol.iterator');for(var b='Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array'.split(' '),c=0;cb||1342177279>>=1)c+=c;return d}}); +u('String.prototype.padStart',function(a){return a?a:function(b,c){var d=ma(this,null,'padStart');b-=d.length;c=void 0!==c?String(c):' ';return(0Math.abs(f-c))||(f=b.x,f=f>e||.01>Math.abs(f-e));(e=f)||(e=b.endY,e=eMath.abs(e-d));(d=e)||(b=b.y,d=b>a||.01>Math.abs(b-a));b=!d}return b}function F(a,b){for(var c=!1,d=0;dg&&J.yr){ca=!0;break}}ca&&(f+=Math.round(l)*Math.round(H))}}return c.call(b,0,d-f)};function Ka(){};function La(){} +function Ma(a,b,c,d,e,f){var h=new Ba;b=new z(b,!1);Ea(h,b);Na(a,b,h,d);if(!e)return h.i=['unmeasurable'],h.s=void 0,h.A=0,h.j=[],h.g&&(a=h.g,c={},a=new z((c.x=0,c.y=0,c.width=a.width,c.height=a.height,c),a.g),h.g=a),h.h=Fa(),h;'locked'===f&&F(h,'deviceLocked');if(b.noOutputDevice)F(h,'backgrounded'),F(h,'noOutputDevice');else if('backgrounded'===c)F(h,'backgrounded');else if(void 0!==h.g){for(a=0;ad.time&&(d=b[e]);c=d;a.m=Rb(c.rootBounds);a.g=Rb(c.boundingClientRect);a.C=Rb(c.intersectionRect);a.M=!!c.isIntersecting;Ib(a)}}catch(f){a.A(),ob(a.L,'generic','Problem handling IntersectionObserver callback: '+f.message)}},{root:null,rootMargin:'0px',threshold:[0,.1,.2,.3,.4,.5,.6,.7,.8,.9,1]})} +function Qb(a){a.j.ResizeObserver?a.u||(a.u=Sb(a,function(){return Tb(a)}),a.u.observe(a.h)):(a.v||(a.v=function(){return Tb(a)},(0,a.j.addEventListener)('resize',a.v)),a.s||(a.s=new MutationObserver(function(){return Tb(a)}),a.s.observe(a.h,{childList:!1,attributes:!0,subtree:!1})))}function Tb(a){a.h&&!Pb(a.h)&&(Ob(a),Mb(a))}function Sb(a,b){return new a.j.ResizeObserver(b)}function Rb(a){if(a&&null!==a.x&&null!==a.y&&null!==a.width&&null!==a.height)return new z(a,!1)};function Ub(a){if('object'===typeof a&&'object'===typeof a.webOSSystem)return a.webOSSystem}function Vb(a){if('object'===typeof a&&'object'===typeof a.tizen)return a.tizen}function Yb(a){return'object'===typeof Vb(a)};function Zb(a,b){this.h=a;this.g=b};function $b(){return'undefined'!==typeof crypto&&'function'===typeof crypto.getRandomValues}function ac(){var a=new Uint8Array(16);crypto.getRandomValues(a);a[6]=a[6]&15|64;a[8]=a[8]&63|128;for(var b=[],c=0;16>c;c++)b.push(a[c].toString(16).padStart(2,'0'));return b[0]+b[1]+b[2]+b[3]+'-'+b[4]+b[5]+'-'+b[6]+b[7]+'-'+b[8]+b[9]+'-'+b[10]+b[11]+b[12]+b[13]+b[14]+b[15]} +function bc(){return'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,function(a){var b=16*Math.random()|0;return'y'===a?(b&3|8).toString(16):b.toString(16)})};function cc(a,b){var c=void 0===c?K:c;this.j=a;this.g=c;this.i=b;this.h=[]} +function dc(a){if(!a.g||!a.g.document)throw Error('OMID Service Script is not running within a window.');var b=a.h;a.h=[];b.forEach(function(c){try{var d=a.i.J?'limited':'full',e=R(c.accessMode,sa)?c.accessMode:null;var f=e?'full'==e&&'limited'==d?d:'domain'==e?'limited':e:d;c.accessMode=f;a:{var h=c.resourceUrl,k=a.g.location.origin;try{var g=new URL(h,k);break a}catch(H){}try{g=new URL(h);break a}catch(H){}g=null}if(d=g){var l=$b()?ac():bc();ec(a,l,d,f);var m=c.vendorKey,r=c.verificationParameters; +m=void 0===m?'':m;r=void 0===r?'':r;m&&'string'===typeof m&&''!==m&&r&&'string'===typeof r&&''!==r&&(a.j.o[m]=r);a.i.C.set(l,c)}}catch(H){Pa('OMID verification script '+c.resourceUrl+' failed to load: '+H)}})} +function ec(a,b,c,d){var e=a.g.document,f=e.createElement('iframe');f.id='omid-verification-script-frame-'+b;f.style.display='none';if('full'==d){var h=function(){var k=f.contentWindow;k.omidVerificationProperties={serviceWindow:a.g,injectionSource:'app',injectionId:b};k=k.document;var g=k.createElement('script');g.src=c.href;k.head.appendChild(g);f.removeEventListener('load',h)};f.addEventListener('load',h)}else'limited'==d&&(f.srcdoc="\n