diff --git a/VERSIONS.md b/VERSIONS.md index a4c7a3d..97799fb 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -35,4 +35,5 @@ Here's a list of the Basic-Ads dependency versions for each release after 0.2.0: | 1.1.1 | 2.3.0 | 1.10.0 | 1.9.1 | 24.9.0 / 12.14.0 | 4.0.0 / 3.1.0 | | 1.2.0-beta01 | 2.3.21 | 1.10.3 | 1.9.1 | 25.2.0 / 13.3.0 | 4.0.0 / 3.1.0 | | 1.2.0-beta02 | 2.4.0 | 1.11.1 | 1.10.0 | 25.4.0 / 13.3.0 | 4.0.0 / 3.1.0 | -| 1.2.0 | 2.4.0 | 1.11.1 | 1.10.0 | 25.4.0 / 13.3.0 | 4.0.0 / 3.1.0 | \ No newline at end of file +| 1.2.0 | 2.4.0 | 1.11.1 | 1.10.0 | 25.4.0 / 13.3.0 | 4.0.0 / 3.1.0 | +| 1.2.1 | 2.4.10 | 1.11.1 | 1.10.0 | 25.4.0 / 13.8.0 | 4.0.0 / 3.1.0 | \ No newline at end of file diff --git a/basic-ads/build.gradle.kts b/basic-ads/build.gradle.kts index 8439416..9dd3f04 100644 --- a/basic-ads/build.gradle.kts +++ b/basic-ads/build.gradle.kts @@ -49,6 +49,9 @@ kotlin { implementation(libs.annotations) implementation(libs.lexilabs.basic.logging) } + commonTest.dependencies { + implementation(kotlin("test")) + } androidMain.dependencies { compileOnly(libs.google.play.services.ads) compileOnly(libs.android.core) @@ -76,9 +79,10 @@ kotlin { compileSdk = libs.versions.build.sdk.compile.get().toInt() minSdk = libs.versions.build.sdk.min.get().toInt() withJava() + withHostTest {} compilerOptions { - jvmTarget.set(JvmTarget.JVM_17) + jvmTarget.set(JvmTarget.JVM_26) } } } \ No newline at end of file diff --git a/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/AdSize.kt b/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/AdSize.kt index f3a40d0..96103bc 100644 --- a/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/AdSize.kt +++ b/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/AdSize.kt @@ -10,10 +10,36 @@ import android.content.Context */ public actual class AdSize public actual constructor(public actual val width: Int, public actual val height: Int) { + /** + * The size handed over by the Google Mobile Ads SDK, when this instance came from one of its + * factories rather than from a pair of dimensions. + */ + private var nativeAdSize: com.google.android.gms.ads.AdSize? = null + + /** + * Wraps a size produced by the Google Mobile Ads SDK. + * + * The adaptive factories return sizes carrying request metadata that goes beyond width and + * height, and the SDK exposes no public constructor accepting it. Rebuilding such a size from + * its dimensions alone silently turns an adaptive request into a fixed-size one, so the + * original instance is kept here and handed back unchanged by [toNative]. + */ + internal constructor(nativeAdSize: com.google.android.gms.ads.AdSize) : + this(nativeAdSize.width, nativeAdSize.height) { + this.nativeAdSize = nativeAdSize + } + init { com.google.android.gms.ads.AdSize(width, height) } + /** + * Returns the Google Mobile Ads size this represents, preserving the original instance when + * there is one. + */ + internal fun toNative(): com.google.android.gms.ads.AdSize = + nativeAdSize ?: com.google.android.gms.ads.AdSize(width, height) + public actual companion object { /** A constant for full-width ads. */ public actual val FULL_WIDTH: Int = com.google.android.gms.ads.AdSize.FULL_WIDTH diff --git a/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/Converters.kt b/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/Converters.kt index d624634..09b0b7a 100644 --- a/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/Converters.kt +++ b/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/Converters.kt @@ -7,14 +7,12 @@ import android.content.ContextWrapper /** * Converts a common [AdSize] to a [com.google.android.gms.ads.AdSize]. */ -public fun AdSize.toAndroid(): com.google.android.gms.ads.AdSize = - com.google.android.gms.ads.AdSize(this.width, this.height) +public fun AdSize.toAndroid(): com.google.android.gms.ads.AdSize = this.toNative() /** * Converts a [com.google.android.gms.ads.AdSize] to a common [AdSize]. */ -public fun com.google.android.gms.ads.AdSize.toCommon(): AdSize = - AdSize(width = this.width, height = this.height) +public fun com.google.android.gms.ads.AdSize.toCommon(): AdSize = AdSize(this) /** * Converts a common [RequestConfiguration] to a [com.google.android.gms.ads.RequestConfiguration]. @@ -72,11 +70,14 @@ public fun com.google.android.gms.ads.RequestConfiguration.PublisherPrivacyPerso */ public fun Context.getActivity(): Activity? { var context = this - var i = 5 - while (context is ContextWrapper && i > 0) { + while (context is ContextWrapper) { if (context is Activity) return context - context = context.baseContext - i -= 1 + val base = context.baseContext + // A wrapper whose base is itself would otherwise spin forever. This replaces a previous + // fixed limit of five unwraps, which gave up before reaching the Activity on deeply + // wrapped chains and returned null as though no Activity existed. + if (base === context) return null + context = base } return null } \ No newline at end of file diff --git a/basic-ads/src/commonTest/kotlin/app/lexilabs/basic/ads/AdExceptionTest.kt b/basic-ads/src/commonTest/kotlin/app/lexilabs/basic/ads/AdExceptionTest.kt new file mode 100644 index 0000000..25d5a19 --- /dev/null +++ b/basic-ads/src/commonTest/kotlin/app/lexilabs/basic/ads/AdExceptionTest.kt @@ -0,0 +1,19 @@ +package app.lexilabs.basic.ads + +import kotlin.test.Test +import kotlin.test.assertEquals + +class AdExceptionTest { + + @Test + fun testAdExceptionMessage() { + val exception = AdException("Network error") + assertEquals("AdMob failed to obtain, load, or show an ad: Network error", exception.message) + } + + @Test + fun testAdExceptionNullMessage() { + val exception = AdException(null) + assertEquals("AdMob failed to obtain, load, or show an ad: null", exception.message) + } +} diff --git a/basic-ads/src/commonTest/kotlin/app/lexilabs/basic/ads/AdStateTest.kt b/basic-ads/src/commonTest/kotlin/app/lexilabs/basic/ads/AdStateTest.kt new file mode 100644 index 0000000..aac285f --- /dev/null +++ b/basic-ads/src/commonTest/kotlin/app/lexilabs/basic/ads/AdStateTest.kt @@ -0,0 +1,22 @@ +package app.lexilabs.basic.ads + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class AdStateTest { + + @Test + fun testAdStateValues() { + val states = AdState.entries.toTypedArray() + assertNotNull(states) + assertEquals(7, states.size) + assertEquals(AdState.NONE, AdState.valueOf("NONE")) + assertEquals(AdState.LOADING, AdState.valueOf("LOADING")) + assertEquals(AdState.READY, AdState.valueOf("READY")) + assertEquals(AdState.SHOWING, AdState.valueOf("SHOWING")) + assertEquals(AdState.SHOWN, AdState.valueOf("SHOWN")) + assertEquals(AdState.DISMISSED, AdState.valueOf("DISMISSED")) + assertEquals(AdState.FAILING, AdState.valueOf("FAILING")) + } +} diff --git a/basic-ads/src/commonTest/kotlin/app/lexilabs/basic/ads/AdUnitIdTest.kt b/basic-ads/src/commonTest/kotlin/app/lexilabs/basic/ads/AdUnitIdTest.kt new file mode 100644 index 0000000..71e7e30 --- /dev/null +++ b/basic-ads/src/commonTest/kotlin/app/lexilabs/basic/ads/AdUnitIdTest.kt @@ -0,0 +1,23 @@ +package app.lexilabs.basic.ads + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class AdUnitIdTest { + + @Test + fun testDefaults() { + assertNotNull(AdUnitId.BANNER_DEFAULT) + assertNotNull(AdUnitId.INTERSTITIAL_DEFAULT) + assertNotNull(AdUnitId.REWARDED_DEFAULT) + assertNotNull(AdUnitId.REWARDED_INTERSTITIAL_DEFAULT) + assertNotNull(AdUnitId.NATIVE_DEFAULT) + } + + @Test + fun testAutoSelect() { + val selected = AdUnitId.autoSelect(androidAdUnitId = "test_id", iosAdUnitId = "test_id") + assertEquals("test_id", selected) + } +} diff --git a/basic-ads/src/commonTest/kotlin/app/lexilabs/basic/ads/ConsentExceptionTest.kt b/basic-ads/src/commonTest/kotlin/app/lexilabs/basic/ads/ConsentExceptionTest.kt new file mode 100644 index 0000000..0d2938c --- /dev/null +++ b/basic-ads/src/commonTest/kotlin/app/lexilabs/basic/ads/ConsentExceptionTest.kt @@ -0,0 +1,14 @@ +package app.lexilabs.basic.ads + +import kotlin.test.Test +import kotlin.test.assertEquals + +class ConsentExceptionTest { + + @Test + @OptIn(DependsOnGoogleUserMessagingPlatform::class) + fun testConsentExceptionMessage() { + val exception = ConsentException("Configuration error") + assertEquals("UserMessagingPlatform failed to obtain, load, or show ConsentInformation: Configuration error", exception.message) + } +} diff --git a/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/FullScreenContentDelegate.kt b/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/FullScreenContentDelegate.kt index b5d23c0..9840be7 100644 --- a/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/FullScreenContentDelegate.kt +++ b/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/FullScreenContentDelegate.kt @@ -48,8 +48,10 @@ public class FullScreenContentDelegate( override fun adWillDismissFullScreenContent(ad: GADFullScreenPresentingAdProtocol) { superclass + // Deliberately does not call onDismissed(). iOS splits dismissal into a will/did pair + // while Android reports it once, via onAdDismissedFullScreenContent, which corresponds to + // adDidDismissFullScreenContent above. Calling it here too fired the callback twice. Log.d(tag, "ad being dismissed soon") - onDismissed() } override fun adWillPresentFullScreenContent(ad: GADFullScreenPresentingAdProtocol) { diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties index 93ee2e5..5e9bc38 100644 --- a/gradle/gradle-daemon-jvm.properties +++ b/gradle/gradle-daemon-jvm.properties @@ -1,12 +1,12 @@ #This file is generated by updateDaemonJvm -toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/402983f310a88ac68b3e883c7c91c760/redirect -toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/e50b80b5a11d194a898bc3e6211b7c4b/redirect -toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/402983f310a88ac68b3e883c7c91c760/redirect -toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/e50b80b5a11d194a898bc3e6211b7c4b/redirect -toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/f257be9f04bfdf169051808541767806/redirect -toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/1dcbacacca32618bd21ec5465779ade1/redirect -toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/402983f310a88ac68b3e883c7c91c760/redirect -toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/e50b80b5a11d194a898bc3e6211b7c4b/redirect -toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/476d3c08f4989328dee56d22e202d98d/redirect -toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/5a88b04b5e582b332d2e6bc12b45f1b9/redirect -toolchainVersion=21 +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e2d49876ee3db33100d35f5be9838c3b/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/6af2df4e19bc2c444a6c6acc6a01a6ae/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e2d49876ee3db33100d35f5be9838c3b/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/6af2df4e19bc2c444a6c6acc6a01a6ae/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/452dfc1cc9013972dc33eb709577c24f/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/8e89327856a245266587332a292623ce/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e2d49876ee3db33100d35f5be9838c3b/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/6af2df4e19bc2c444a6c6acc6a01a6ae/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/06247aa9c110fdcc3afc4c1b4da3a10a/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/496ddee07c4f214fc64845d4c1f671df/redirect +toolchainVersion=26 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 85a1651..24b92e7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,14 +1,14 @@ [versions] # BUILD INFO -ads = "1.2.0" +ads = "1.2.1" build-sdk-compile = "37" build-sdk-min = "24" build-sdk-target = "37" build-ios-target-deployment = "13.0" -kotlin = "2.4.0" -agp = "9.2.1" +kotlin = "2.4.10" +agp = "9.5.0-alpha02" # COCOAPODS DEPENDENCIES -cocoapods-admob = "13.3.0" +cocoapods-admob = "13.8.0" cocoapods-ump = "3.1.0" # DEPENDENCIES bcv = "0.18.1" @@ -17,8 +17,8 @@ compose = "1.11.1" google-play-services-ads = "25.4.0" android-core = "1.19.0" annotations = "1.10.0" -kover = "0.9.8" -logging = "0.2.6" +kover = "0.9.9" +logging = "0.3.1" android-ump = "4.0.0" maven-publish = "0.37.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a9db115..ad7845b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500