diff --git a/e2e/android/app/src/compose/java/com/example/androidobservability/masking/ComposeWebActivity.kt b/e2e/android/app/src/compose/java/com/example/androidobservability/masking/ComposeWebActivity.kt index efa1184e0e..82705cbc2e 100644 --- a/e2e/android/app/src/compose/java/com/example/androidobservability/masking/ComposeWebActivity.kt +++ b/e2e/android/app/src/compose/java/com/example/androidobservability/masking/ComposeWebActivity.kt @@ -8,6 +8,8 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -20,8 +22,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.key +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -46,75 +50,63 @@ class ComposeWebActivity : ComponentActivity() { val context = LocalContext.current val webView = remember(context) { WebView(context) } val customWebView = remember(context) { CustomWebView(context) } - val geckoView = remember(context) { GeckoView(context) } - val customGeckoView = remember(context) { CustomGeckoView(context) } - Column( modifier = Modifier .fillMaxSize() .padding(innerPadding) - .verticalScroll(rememberScrollState()) ) { - Text( - text = "android.webkit.WebView", - fontSize = 16.sp, - modifier = Modifier - .background(Color.Yellow) - .align(Alignment.CenterHorizontally) - ) - WebViewItem( - url = "https://www.google.com", - webView = webView, + Column( modifier = Modifier - .fillMaxWidth() - .height(450.dp) - ) + .weight(1f) + .verticalScroll(rememberScrollState()) + ) { + Text( + text = "android.webkit.WebView", + fontSize = 16.sp, + modifier = Modifier + .background(Color.Yellow) + .align(Alignment.CenterHorizontally) + ) + WebViewItem( + url = "https://www.google.com", + webView = webView, + modifier = Modifier + .fillMaxWidth() + .height(450.dp) + ) - Text( - text = "CustomWebView", - fontSize = 16.sp, - modifier = Modifier - .background(Color.Yellow) - .align(Alignment.CenterHorizontally) - ) - WebViewItem( - url = "https://www.google.com", - webView = customWebView, - modifier = Modifier - .fillMaxWidth() - .height(450.dp) - ) + Text( + text = "CustomWebView", + fontSize = 16.sp, + modifier = Modifier + .background(Color.Yellow) + .align(Alignment.CenterHorizontally) + ) + WebViewItem( + url = "https://www.google.com", + webView = customWebView, + modifier = Modifier + .fillMaxWidth() + .height(450.dp) + ) + } - Text( - text = "org.mozilla.geckoview.GeckoView", - fontSize = 16.sp, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .background(Color.Yellow) - .padding(top = 8.dp) - ) - GeckoViewItem( + LazyGeckoViewItem( + label = "org.mozilla.geckoview.GeckoView (device)", url = "https://www.google.com", - geckoView = geckoView, + geckoViewFactory = { GeckoView(it) }, modifier = Modifier .fillMaxWidth() - .height(450.dp) + .height(200.dp) ) - Text( - text = "CustomGeckoView", - fontSize = 16.sp, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .background(Color.Yellow) - .padding(top = 8.dp) - ) - GeckoViewItem( + LazyGeckoViewItem( + label = "CustomGeckoView (device)", url = "https://www.google.com", - geckoView = customGeckoView, + geckoViewFactory = { CustomGeckoView(it) }, modifier = Modifier .fillMaxWidth() - .height(450.dp) + .height(200.dp) ) } } @@ -152,36 +144,52 @@ fun WebViewItem(url: String, webView: WebView, modifier: Modifier = Modifier) { } @Composable -fun GeckoViewItem(url: String, geckoView: GeckoView, modifier: Modifier = Modifier) { - val context = LocalContext.current - val runtime = remember { - GeckoRuntime.getDefault(context.applicationContext) - } - val session = remember(runtime) { - GeckoSession().apply { - setContentDelegate(object : ContentDelegate {}) - open(runtime) +fun LazyGeckoViewItem( + label: String, + url: String, + geckoViewFactory: (android.content.Context) -> GeckoView, + modifier: Modifier = Modifier +) { + var loaded by remember { mutableStateOf(false) } + + Text( + text = if (loaded) label else "Tap to load $label", + fontSize = 16.sp, + modifier = Modifier + .background(Color.Yellow) + .padding(top = 8.dp) + .then(if (!loaded) Modifier.clickable { loaded = true } else Modifier) + ) + + if (loaded) { + val context = LocalContext.current + val geckoView = remember(context) { geckoViewFactory(context) } + val runtime = remember { GeckoRuntime.getDefault(context.applicationContext) } + val session = remember(runtime) { + GeckoSession().apply { + setContentDelegate(object : ContentDelegate {}) + open(runtime) + } } - } - DisposableEffect(session) { - onDispose { - session.close() + DisposableEffect(session) { + onDispose { session.close() } } - } - key(geckoView) { AndroidView( modifier = modifier, - factory = { _ -> - geckoView.apply { - setSession(session) - } - } + factory = { _ -> geckoView.apply { setSession(session) } } ) - } - LaunchedEffect(url) { - session.loadUri(url) + LaunchedEffect(url) { session.loadUri(url) } + } else { + Box( + modifier = modifier + .background(Color.LightGray) + .clickable { loaded = true }, + contentAlignment = Alignment.Center + ) { + Text("Tap to load", color = Color.DarkGray) + } } } diff --git a/e2e/android/app/src/main/AndroidManifest.xml b/e2e/android/app/src/main/AndroidManifest.xml index cec3ae2c59..87d34f6b77 100644 --- a/e2e/android/app/src/main/AndroidManifest.xml +++ b/e2e/android/app/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ android:supportsRtl="true" android:theme="@style/Theme.AndroidObservability" android:usesCleartextTraffic="true" + android:extractNativeLibs="true" tools:targetApi="31" > (geckoViewId) - val session = GeckoSession() + private fun setupLazyGeckoView( + @IdRes labelId: Int, + @IdRes containerId: Int, + factory: () -> GeckoView, + label: String + ) { + val labelView = findViewById(labelId) + val container = findViewById(containerId) - session.setContentDelegate(object : ContentDelegate {}) + labelView.setOnClickListener { loadGeckoView(container, factory(), label, labelView) } + container.setOnClickListener { loadGeckoView(container, factory(), label, labelView) } + } - GeckoRuntime.getDefault(application).let { - session.open(it) - } - view?.setSession(session) + private fun loadGeckoView( + container: FrameLayout, + geckoView: GeckoView, + label: String, + labelView: TextView + ) { + container.setOnClickListener(null) + labelView.setOnClickListener(null) + labelView.text = label + + val session = GeckoSession() + session.setContentDelegate(object : ContentDelegate {}) + GeckoRuntime.getDefault(application).let { session.open(it) } + geckoView.setSession(session) session.loadUri(url) + + container.addView( + geckoView, + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) + ) } } diff --git a/e2e/android/app/src/main/res/layout/activity_webview.xml b/e2e/android/app/src/main/res/layout/activity_webview.xml index 4997b98044..21dd48fd1a 100644 --- a/e2e/android/app/src/main/res/layout/activity_webview.xml +++ b/e2e/android/app/src/main/res/layout/activity_webview.xml @@ -1,67 +1,79 @@ - + android:layout_height="match_parent" + android:orientation="vertical"> - + android:layout_height="0dp" + android:layout_weight="1"> - + android:orientation="vertical"> - + - + - + - + - + + - + + + - + + + - - + diff --git a/sdk/@launchdarkly/mobile-dotnet/android/LDObserve.Android.Binding/LDObserve.Android.Binding.csproj b/sdk/@launchdarkly/mobile-dotnet/android/LDObserve.Android.Binding/LDObserve.Android.Binding.csproj index 1b40498296..6c90d0f0ed 100644 --- a/sdk/@launchdarkly/mobile-dotnet/android/LDObserve.Android.Binding/LDObserve.Android.Binding.csproj +++ b/sdk/@launchdarkly/mobile-dotnet/android/LDObserve.Android.Binding/LDObserve.Android.Binding.csproj @@ -10,7 +10,7 @@ To learn more, see: https://learn.microsoft.com/dotnet/core/deploying/trimming/prepare-libraries-for-trimming --> true - $(NoWarn);NU1605;NU1608 + $(NoWarn);NU1605;NU1608;BG8605;BG8606;BG8400 LDObserveAndroid + + + diff --git a/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/build.gradle.kts b/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/build.gradle.kts index 2022791182..9ec534c444 100644 --- a/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/build.gradle.kts +++ b/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/build.gradle.kts @@ -24,8 +24,10 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = "1.8" + kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8) + } } } diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj b/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj index efb601b03b..3f80489dd6 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj +++ b/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.Fat.csproj @@ -20,7 +20,7 @@ - + diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.csproj b/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.csproj index 428bafb56f..bd8aeb5a16 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.csproj +++ b/sdk/@launchdarkly/mobile-dotnet/observability/LDObservability.csproj @@ -59,7 +59,7 @@ - + diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/bridge/DictionaryTypeConverters.cs b/sdk/@launchdarkly/mobile-dotnet/observability/bridge/DictionaryTypeConverters.cs index 2f12b8cd7f..303a44c1e7 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/bridge/DictionaryTypeConverters.cs +++ b/sdk/@launchdarkly/mobile-dotnet/observability/bridge/DictionaryTypeConverters.cs @@ -59,7 +59,11 @@ internal static NSObject ToNSObject(object? value) } #elif ANDROID - internal static IDictionary? ToJavaDictionary(IDictionary? src) + // Returns a managed Dictionary (not Java.Util.HashMap) intentionally. + // The auto-generated Android binding marshals any IDictionary into a + // java.util.HashMap via JavaDictionary.ToLocalJniHandle() at JNI call + // time — changing this to Java.Util.HashMap causes double-marshaling bugs. + internal static IDictionary? ToJavaDictionary(IDictionary? src) { if (src is null) return null; @@ -79,21 +83,21 @@ internal static NSObject ToNSObject(object? value) return value switch { string s => new Java.Lang.String(s), - bool b => new Java.Lang.Boolean(b), - int i => new Java.Lang.Integer(i), - long l => new Java.Lang.Long(l), - double d => new Java.Lang.Double(d), - float f => new Java.Lang.Float(f), - decimal m => new Java.Lang.Double((double)m), + bool b => Java.Lang.Boolean.ValueOf(b), + int i => Java.Lang.Integer.ValueOf(i), + long l => Java.Lang.Long.ValueOf(l), + double d => Java.Lang.Double.ValueOf(d), + float f => Java.Lang.Float.ValueOf(f), + decimal m => Java.Lang.Double.ValueOf((double)m), IDictionary dict => ToJavaHashMap(dict), IEnumerable arr => ToJavaList(arr.Select(s => (Java.Lang.Object)new Java.Lang.String(s))), - IEnumerable arr => ToJavaList(arr.Select(b => (Java.Lang.Object)new Java.Lang.Boolean(b))), - IEnumerable arr => ToJavaList(arr.Select(i => (Java.Lang.Object)new Java.Lang.Integer(i))), - IEnumerable arr => ToJavaList(arr.Select(l => (Java.Lang.Object)new Java.Lang.Long(l))), - IEnumerable arr => ToJavaList(arr.Select(d => (Java.Lang.Object)new Java.Lang.Double(d))), - IEnumerable arr => ToJavaList(arr.Select(f => (Java.Lang.Object)new Java.Lang.Float(f))), + IEnumerable arr => ToJavaList(arr.Select(b => (Java.Lang.Object)Java.Lang.Boolean.ValueOf(b))), + IEnumerable arr => ToJavaList(arr.Select(i => (Java.Lang.Object)Java.Lang.Integer.ValueOf(i))), + IEnumerable arr => ToJavaList(arr.Select(l => (Java.Lang.Object)Java.Lang.Long.ValueOf(l))), + IEnumerable arr => ToJavaList(arr.Select(d => (Java.Lang.Object)Java.Lang.Double.ValueOf(d))), + IEnumerable arr => ToJavaList(arr.Select(f => (Java.Lang.Object)Java.Lang.Float.ValueOf(f))), _ => new Java.Lang.String(value.ToString() ?? string.Empty) }; diff --git a/sdk/@launchdarkly/mobile-dotnet/sample/MauiProgram.cs b/sdk/@launchdarkly/mobile-dotnet/sample/MauiProgram.cs index 60cd325da3..372d5451e3 100644 --- a/sdk/@launchdarkly/mobile-dotnet/sample/MauiProgram.cs +++ b/sdk/@launchdarkly/mobile-dotnet/sample/MauiProgram.cs @@ -98,7 +98,7 @@ public static MauiApp CreateMauiApp() #endif otlpEndpoint: otlpEndpoint, backendUrl: backendUrl, - attributes: new Dictionary { { "test-options-attribute", "maui-sample-value" } } + attributes: new Dictionary { { "test-options-attribute", "maui-sample-value" } } ))) .Add(new SessionReplayPlugin(new SessionReplayOptions( isEnabled: true, diff --git a/sdk/@launchdarkly/observability-android/gradle.properties b/sdk/@launchdarkly/observability-android/gradle.properties index 5e3559e215..0729d4f809 100644 --- a/sdk/@launchdarkly/observability-android/gradle.properties +++ b/sdk/@launchdarkly/observability-android/gradle.properties @@ -2,6 +2,7 @@ # https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties android.useAndroidX=true +org.jetbrains.dokka.experimental.gradle.pluginMode=V2EnabledWithHelpers #x-release-please-start-version version=0.33.0 diff --git a/sdk/@launchdarkly/observability-android/lib/build.gradle.kts b/sdk/@launchdarkly/observability-android/lib/build.gradle.kts index 314e8ec4c9..04dac3bf86 100644 --- a/sdk/@launchdarkly/observability-android/lib/build.gradle.kts +++ b/sdk/@launchdarkly/observability-android/lib/build.gradle.kts @@ -9,7 +9,8 @@ plugins { alias(libs.plugins.kotlin.serialization) // Apply Dokka plugin for documentation generation - id("org.jetbrains.dokka") version "2.0.0" + id("org.jetbrains.dokka") version "2.1.0" + id("org.jetbrains.dokka-javadoc") version "2.1.0" } allprojects { @@ -106,8 +107,10 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = "1.8" + kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8) + } } publishing { @@ -177,15 +180,15 @@ signing { sign(publishing.publications["release"]) } -// Dokka configuration for Android library documentation -tasks.dokkaJavadoc.configure { +dokka { moduleName.set("launchdarkly-observability-android") moduleVersion.set(project.version.toString()) - outputDirectory.set(layout.projectDirectory.dir("docs")) - dokkaSourceSets { - configureEach { - includes.from("doc-module.md") - } + dokkaPublications.javadoc { + outputDirectory.set(layout.projectDirectory.dir("docs")) + } + + dokkaSourceSets.configureEach { + includes.from("doc-module.md") } } diff --git a/sdk/@launchdarkly/observability-android/lib/src/main/cpp/CMakeLists.txt b/sdk/@launchdarkly/observability-android/lib/src/main/cpp/CMakeLists.txt index 89bc22cbd4..9832b63c9f 100644 --- a/sdk/@launchdarkly/observability-android/lib/src/main/cpp/CMakeLists.txt +++ b/sdk/@launchdarkly/observability-android/lib/src/main/cpp/CMakeLists.txt @@ -7,7 +7,8 @@ add_library(tile_hash SHARED tile_hash_jni.c ) -target_compile_options(tile_hash PRIVATE -O2) +target_compile_options(tile_hash PRIVATE -O3) +target_link_options(tile_hash PRIVATE -Wl,-z,max-page-size=16384) find_library(jnigraphics-lib jnigraphics)