Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>iab</groupId>
<artifactId>omsdk-android</artifactId>
<version>1.6.4</version>
<packaging>aar</packaging>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<String>()
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<String>()

private var hasLoadedInitialUrl = false

private val bridgeScript = """
(function() {
Expand Down Expand Up @@ -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) {
Expand All @@ -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()
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -342,6 +391,7 @@ internal class KontextInAppWebView(
return
}
hasLoadedInitialUrl = true
omLifecycle.markPageStarted(initialUrl)
if (!initialUrl.isNullOrBlank()) {
webView.loadUrl(initialUrl)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -464,6 +538,10 @@ private data class AndroidUserScript(
val injectionTime: String,
)

private fun readInitialOmCreativeType(creationParams: Map<String, Any?>?): OMCreativeType? {
return OMCreativeType.fromRawValue(creationParams?.get("initialOmCreativeType") as? String)
}

private fun readInitialUrl(creationParams: Map<String, Any?>?): String? {
val initialRequest = creationParams?.get("initialUrlRequest") as? Map<*, *>
return initialRequest?.get("url") as? String
Expand Down Expand Up @@ -494,6 +572,17 @@ private fun readUserScripts(creationParams: Map<String, Any?>?): List<AndroidUse
}
}

private fun loadOpenMeasurementJavaScript(context: Context): String? {
return try {
context.resources.openRawResource(R.raw.omsdk_v1)
.bufferedReader(Charsets.UTF_8)
.use { it.readText() }
} catch (exception: Exception) {
Log.e(OMConstants.logTag, "Failed to load OM SDK JavaScript", exception)
null
}
}

private fun consoleLevelName(level: android.webkit.ConsoleMessage.MessageLevel): String {
return when (level) {
android.webkit.ConsoleMessage.MessageLevel.ERROR -> "ERROR"
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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()

Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
96 changes: 96 additions & 0 deletions android/src/main/kotlin/so/kontext/sdk/flutter/omsdk/OMManager.kt
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, OMRetainedWebView>()

fun retain(
webView: OMRetainedWebView,
delayMillis: Long = OMConstants.retentionIntervalMillis,
) {
val id = UUID.randomUUID().toString()
retainedWebViews[id] = webView
mainHandler.postDelayed(
{
retainedWebViews.remove(id)?.destroy()
},
delayMillis,
)
}
}
Loading