From f1ff429c8ff2462b55b33f57c8ec3d3c1dc3abcf Mon Sep 17 00:00:00 2001 From: Mobile Ads Developer Relations Date: Mon, 18 May 2026 10:42:11 -0700 Subject: [PATCH] The BUILD file is updated to include the new native ad and native template source paths. PiperOrigin-RevId: 917315040 --- .../google/unity/ads/nextgen/AdWrapper.java | 18 + .../unity/ads/nextgen/UnityBannerAd.java | 19 +- .../ads/nextgen/UnityBannerAdPreloader.java | 163 ++++++ .../nextgen/UnityBannerAdPreloaderTest.java | 166 +++++++ .../unity/ads/nextgen/UnityBannerAdTest.java | 469 ++++++++++++++++++ 5 files changed, 834 insertions(+), 1 deletion(-) create mode 100644 source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityBannerAdPreloader.java create mode 100644 source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/test/java/com/google/unity/ads/nextgen/UnityBannerAdPreloaderTest.java create mode 100644 source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/test/java/com/google/unity/ads/nextgen/UnityBannerAdTest.java diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/AdWrapper.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/AdWrapper.java index e93aac5f4..db4168fe1 100644 --- a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/AdWrapper.java +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/AdWrapper.java @@ -1,6 +1,8 @@ package com.google.unity.ads.nextgen; import com.google.android.libraries.ads.mobile.sdk.appopen.AppOpenAd; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAd; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdRequest; import com.google.android.libraries.ads.mobile.sdk.common.AdLoadCallback; import com.google.android.libraries.ads.mobile.sdk.common.AdRequest; import com.google.android.libraries.ads.mobile.sdk.interstitial.InterstitialAd; @@ -35,6 +37,22 @@ public static AdWrapper forAppOpen() { return new AdWrapper<>(AppOpenAd::load); } + /** Creates a new AdWrapper for loading BannerAds. */ + public static AdWrapper forBanner() { + return new AdWrapper( + new AdLoader() { + @Override + public void load(AdRequest adRequest, AdLoadCallback callback) { + if (adRequest instanceof BannerAdRequest) { + BannerAd.load((BannerAdRequest) adRequest, callback); + } else { + throw new IllegalArgumentException( + "AdRequest must be of type BannerAdRequest for Banner Ads"); + } + } + }); + } + /** Creates a new AdWrapper for loading InterstitialAds. */ public static AdWrapper forInterstitial() { return new AdWrapper<>(InterstitialAd::load); diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityBannerAd.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityBannerAd.java index 3d9a962ed..e3bcabf84 100644 --- a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityBannerAd.java +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityBannerAd.java @@ -26,6 +26,7 @@ import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.libraries.ads.mobile.sdk.banner.BannerAd; import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdEventCallback; import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdRequest; @@ -54,9 +55,13 @@ protected static class Insets { /** The {@link BannerAd} to display to the user. */ private BannerAd bannerAd; + /** The {@link AdWrapper} used to load ads. */ + private final AdWrapper adWrapper; + /** The {@link View} that contains the banner ad. */ private View adView; + /** A code indicating where to place the ad. */ private int positionCode; @@ -91,8 +96,14 @@ protected static class Insets { private View.OnLayoutChangeListener layoutChangeListener; public UnityBannerAd(Activity activity, UnityBannerAdCallback callback) { + this(activity, callback, AdWrapper.forBanner()); + } + + @VisibleForTesting + UnityBannerAd(Activity activity, UnityBannerAdCallback callback, AdWrapper adWrapper) { unityPlayerActivity = activity; this.callback = callback; + this.adWrapper = adWrapper; } /** @@ -121,7 +132,7 @@ public void create(final int positionX, final int positionY) { } protected void load(final BannerAdRequest adRequest) { - BannerAd.load( + adWrapper.load( adRequest, new AdLoadCallback() { @Override @@ -368,6 +379,8 @@ public void destroy() { .getDecorView() .getRootView() .removeOnLayoutChangeListener(layoutChangeListener); + + layoutChangeListener = null; } /** @@ -375,6 +388,10 @@ public void destroy() { * reposition banner ads as required. */ protected void setLayoutChangeListener() { + if (layoutChangeListener != null) { + return; + } + layoutChangeListener = new View.OnLayoutChangeListener() { @Override diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityBannerAdPreloader.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityBannerAdPreloader.java new file mode 100644 index 000000000..28cf3a1aa --- /dev/null +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityBannerAdPreloader.java @@ -0,0 +1,163 @@ +package com.google.unity.ads.nextgen; + +import android.app.Activity; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAd; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdPreloader; +import com.google.android.libraries.ads.mobile.sdk.common.AdLoadCallback; +import com.google.android.libraries.ads.mobile.sdk.common.AdRequest; +import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; +import com.google.android.libraries.ads.mobile.sdk.common.PreloadCallback; +import com.google.android.libraries.ads.mobile.sdk.common.PreloadConfiguration; +import com.google.android.libraries.ads.mobile.sdk.common.ResponseInfo; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** Unity implementation of the {@link BannerAdPreloader}. */ +public class UnityBannerAdPreloader { + + /** The {@code Activity} on which the banner ad will display. */ + private final Activity activity; + + /** An executor used to run the callbacks. */ + private final ExecutorService service; + + /** + * A {@code UnityPreloadCallback} implemented in Unity via {@code AndroidJavaProxy} to receive ad + * events. + */ + private final UnityPreloadCallback preloadCallback; + + private final BannerAdPreloaderWrapper preloaderWrapper; + + public UnityBannerAdPreloader(Activity activity, UnityPreloadCallback preloadCallback) { + this( + activity, + preloadCallback, + new BannerAdPreloaderWrapper(), + Executors.newSingleThreadExecutor()); + } + + @VisibleForTesting + public UnityBannerAdPreloader( + Activity activity, + UnityPreloadCallback preloadCallback, + BannerAdPreloaderWrapper preloaderWrapper, + ExecutorService service) { + this.activity = activity; + this.preloadCallback = preloadCallback; + this.service = service; + this.preloaderWrapper = preloaderWrapper; + } + + public boolean start(String preloadId, PreloadConfiguration preloadConfiguration) { + return preloaderWrapper.start( + preloadId, + preloadConfiguration, + new PreloadCallback() { + @Override + public void onAdPreloaded(@NonNull String preloadId, ResponseInfo responseInfo) { + service.execute( + () -> { + if (preloadCallback != null) { + preloadCallback.onAdPreloaded(preloadId, responseInfo); + } + }); + } + + @Override + public void onAdsExhausted(@NonNull String preloadId) { + service.execute( + () -> { + if (preloadCallback != null) { + preloadCallback.onAdsExhausted(preloadId); + } + }); + } + + @Override + public void onAdFailedToPreload(@NonNull String preloadId, @NonNull LoadAdError adError) { + service.execute( + () -> { + if (preloadCallback != null) { + preloadCallback.onAdFailedToPreload(preloadId, adError); + } + }); + } + }); + } + + public boolean isAdAvailable(String preloadId) { + return preloaderWrapper.isAdAvailable(preloadId); + } + + public int getNumAdsAvailable(String preloadId) { + return preloaderWrapper.getNumAdsAvailable(preloadId); + } + + @SuppressWarnings("VisibleForTests") + @Nullable + public UnityBannerAd pollAd(String preloadId, UnityBannerAdCallback callback) { + final BannerAd pooledAd = preloaderWrapper.pollAd(preloadId); + if (pooledAd == null) { + return null; + } + AdWrapper preloadedWrapper = + new AdWrapper( + new AdWrapper.AdLoader() { + @Override + public void load(AdRequest adRequest, AdLoadCallback loadCallback) { + loadCallback.onAdLoaded(pooledAd); + } + }); + return new UnityBannerAd(activity, callback, preloadedWrapper); + } + + @Nullable + public PreloadConfiguration getConfiguration(String preloadId) { + return preloaderWrapper.getConfiguration(preloadId); + } + + public Map getConfigurations() { + return preloaderWrapper.getConfigurations(); + } + + public void destroy(String preloadId) { + boolean unused = preloaderWrapper.destroy(preloadId); + } + + /** Wrapper for BannerAdPreloader static methods to facilitate testing. */ + @VisibleForTesting + public static class BannerAdPreloaderWrapper { + public boolean start(String preloadId, PreloadConfiguration config, PreloadCallback callback) { + return BannerAdPreloader.start(preloadId, config, callback); + } + + public boolean isAdAvailable(String preloadId) { + return BannerAdPreloader.isAdAvailable(preloadId); + } + + public int getNumAdsAvailable(String preloadId) { + return BannerAdPreloader.getNumAdsAvailable(preloadId); + } + + public BannerAd pollAd(String preloadId) { + return BannerAdPreloader.pollAd(preloadId); + } + + public PreloadConfiguration getConfiguration(String preloadId) { + return BannerAdPreloader.getConfiguration(preloadId); + } + + public Map getConfigurations() { + return BannerAdPreloader.getConfigurations(); + } + + public boolean destroy(String preloadId) { + return BannerAdPreloader.destroy(preloadId); + } + } +} diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/test/java/com/google/unity/ads/nextgen/UnityBannerAdPreloaderTest.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/test/java/com/google/unity/ads/nextgen/UnityBannerAdPreloaderTest.java new file mode 100644 index 000000000..7fb7b7913 --- /dev/null +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/test/java/com/google/unity/ads/nextgen/UnityBannerAdPreloaderTest.java @@ -0,0 +1,166 @@ +package com.google.unity.ads.nextgen; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; +import com.google.android.libraries.ads.mobile.sdk.banner.AdSize; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAd; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdRequest; +import com.google.android.libraries.ads.mobile.sdk.common.AdRequest; +import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; +import com.google.android.libraries.ads.mobile.sdk.common.PreloadCallback; +import com.google.android.libraries.ads.mobile.sdk.common.PreloadConfiguration; +import com.google.android.libraries.ads.mobile.sdk.common.ResponseInfo; +import com.google.unity.ads.nextgen.UnityBannerAdPreloader.BannerAdPreloaderWrapper; +import java.util.ArrayList; +import java.util.Map; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; + +/** Unit tests for {@link UnityBannerAdPreloader}. */ +@RunWith(RobolectricTestRunner.class) +public final class UnityBannerAdPreloaderTest { + + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + + private Activity activity; + private UnityBannerAdPreloader unityBannerAdPreloader; + private PreloadConfiguration preloadConfiguration; + + private static final String PRELOAD_ID = "preloadId"; + + @Mock private UnityPreloadCallback mockPreloadCallback; + @Mock private AdRequest mockAdRequest; + @Mock private BannerAd mockBannerAd; + @Mock private UnityBannerAdCallback mockBannerAdCallback; + @Mock private BannerAdPreloaderWrapper mockWrapper; + @Captor private ArgumentCaptor preloadCallbackCaptor; + + @Before + public void setUp() { + activity = Robolectric.buildActivity(Activity.class).create().get(); + unityBannerAdPreloader = + new UnityBannerAdPreloader( + activity, mockPreloadCallback, mockWrapper, newDirectExecutorService()); + preloadConfiguration = new PreloadConfiguration(mockAdRequest, 1); + } + + @Test + public void testStart() { + boolean unused = unityBannerAdPreloader.start(PRELOAD_ID, preloadConfiguration); + + verify(mockWrapper) + .start(eq(PRELOAD_ID), eq(preloadConfiguration), preloadCallbackCaptor.capture()); + + PreloadCallback callback = preloadCallbackCaptor.getValue(); + ResponseInfo responseInfo = + new ResponseInfo("AdapterName", "responseId", new Bundle(), null, new ArrayList<>()); + + callback.onAdPreloaded(PRELOAD_ID, responseInfo); + verify(mockPreloadCallback).onAdPreloaded(PRELOAD_ID, responseInfo); + + LoadAdError loadAdError = + new LoadAdError(LoadAdError.ErrorCode.INTERNAL_ERROR, "error message", null); + callback.onAdFailedToPreload(PRELOAD_ID, loadAdError); + verify(mockPreloadCallback).onAdFailedToPreload(PRELOAD_ID, loadAdError); + + callback.onAdsExhausted(PRELOAD_ID); + verify(mockPreloadCallback).onAdsExhausted(PRELOAD_ID); + } + + @Test + public void testStart_nullCallback() { + UnityBannerAdPreloader preloader = + new UnityBannerAdPreloader(activity, null, mockWrapper, newDirectExecutorService()); + + boolean unused = preloader.start(PRELOAD_ID, preloadConfiguration); + + verify(mockWrapper) + .start(eq(PRELOAD_ID), eq(preloadConfiguration), preloadCallbackCaptor.capture()); + + PreloadCallback callback = preloadCallbackCaptor.getValue(); + ResponseInfo responseInfo = + new ResponseInfo("AdapterName", "responseId", new Bundle(), null, new ArrayList<>()); + + callback.onAdPreloaded(PRELOAD_ID, responseInfo); + LoadAdError loadAdError = + new LoadAdError(LoadAdError.ErrorCode.INTERNAL_ERROR, "error message", null); + callback.onAdFailedToPreload(PRELOAD_ID, loadAdError); + callback.onAdsExhausted(PRELOAD_ID); + } + + @Test + public void testIsAdAvailable() { + boolean unused = unityBannerAdPreloader.isAdAvailable(PRELOAD_ID); + verify(mockWrapper).isAdAvailable(PRELOAD_ID); + } + + @Test + public void testGetNumAdsAvailable() { + int unused = unityBannerAdPreloader.getNumAdsAvailable(PRELOAD_ID); + verify(mockWrapper).getNumAdsAvailable(PRELOAD_ID); + } + + @Test + public void testPollAd_adAvailable() { + when(mockWrapper.pollAd(PRELOAD_ID)).thenReturn(mockBannerAd); + + UnityBannerAd unityBannerAd = unityBannerAdPreloader.pollAd(PRELOAD_ID, mockBannerAdCallback); + + assertThat(unityBannerAd).isNotNull(); + when(mockBannerAd.getView(activity)).thenReturn(new View(activity)); + + BannerAdRequest adRequest = new BannerAdRequest.Builder("adUnit", AdSize.BANNER).build(); + unityBannerAd.load(adRequest); + verify(mockBannerAdCallback, timeout(1000)).onAdLoaded(); + } + + @Test + public void testPollAd_adNotAvailable() { + when(mockWrapper.pollAd(PRELOAD_ID)).thenReturn(null); + + UnityBannerAd unityBannerAd = unityBannerAdPreloader.pollAd(PRELOAD_ID, mockBannerAdCallback); + + assertThat(unityBannerAd).isNull(); + } + + @Test + public void testGetConfiguration() { + PreloadConfiguration unused = unityBannerAdPreloader.getConfiguration(PRELOAD_ID); + verify(mockWrapper).getConfiguration(PRELOAD_ID); + } + + @Test + public void testGetConfigurations() { + Map unused = unityBannerAdPreloader.getConfigurations(); + verify(mockWrapper).getConfigurations(); + } + + @Test + public void testDestroy() { + unityBannerAdPreloader.destroy(PRELOAD_ID); + verify(mockWrapper).destroy(PRELOAD_ID); + } + + @Test + public void testPublicConstructor() { + UnityBannerAdPreloader preloader = new UnityBannerAdPreloader(activity, mockPreloadCallback); + assertThat(preloader).isNotNull(); + } +} diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/test/java/com/google/unity/ads/nextgen/UnityBannerAdTest.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/test/java/com/google/unity/ads/nextgen/UnityBannerAdTest.java new file mode 100644 index 000000000..35f7de965 --- /dev/null +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/test/java/com/google/unity/ads/nextgen/UnityBannerAdTest.java @@ -0,0 +1,469 @@ +package com.google.unity.ads.nextgen; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; + +import android.app.Activity; +import android.os.Bundle; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; +import com.google.android.libraries.ads.mobile.sdk.banner.AdSize; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAd; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdEventCallback; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdRequest; +import com.google.android.libraries.ads.mobile.sdk.common.AdLoadCallback; +import com.google.android.libraries.ads.mobile.sdk.common.AdValue; +import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; +import com.google.android.libraries.ads.mobile.sdk.common.PrecisionType; +import com.google.android.libraries.ads.mobile.sdk.common.ResponseInfo; +import com.google.unity.ads.PluginUtils; +import java.util.ArrayList; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLooper; + +/** Unit tests for {@link UnityBannerAd}. */ +@RunWith(RobolectricTestRunner.class) +public final class UnityBannerAdTest { + + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + + private Activity activity; + @Mock private UnityBannerAdCallback mockCallback; + @Mock private BannerAd mockBannerAd; + @Mock private AdWrapper mockAdWrapper; + + @Captor private ArgumentCaptor> adLoadCallbackCaptor; + @Captor private ArgumentCaptor adEventCallbackCaptor; + + private UnityBannerAd unityBannerAd; + private BannerAdRequest bannerAdRequest; + + @Before + public void setUp() { + activity = Robolectric.buildActivity(Activity.class).create().start().resume().visible().get(); + unityBannerAd = new UnityBannerAd(activity, mockCallback, mockAdWrapper); + bannerAdRequest = new BannerAdRequest.Builder("test-ad-unit", AdSize.BANNER).build(); + } + + private View simulateAdLoadSuccess() { + return simulateAdLoadSuccess(AdSize.BANNER); + } + + private View simulateAdLoadSuccess(AdSize adSize) { + View fakeView = new View(activity); + when(mockBannerAd.getView(activity)).thenReturn(fakeView); + when(mockBannerAd.getAdSize()).thenReturn(adSize); + + unityBannerAd.load(bannerAdRequest); + verify(mockAdWrapper).load(eq(bannerAdRequest), adLoadCallbackCaptor.capture()); + adLoadCallbackCaptor.getValue().onAdLoaded(mockBannerAd); + return fakeView; + } + + @Test + public void testLoad_onAdLoaded() { + View fakeView = simulateAdLoadSuccess(); + + verify(mockCallback, timeout(1000)).onAdLoaded(); + + ShadowLooper.idleMainLooper(); + verify(mockBannerAd).getView(activity); + assertThat(fakeView.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void testLoad_onAdFailedToLoad() { + unityBannerAd.load(bannerAdRequest); + + verify(mockAdWrapper).load(eq(bannerAdRequest), adLoadCallbackCaptor.capture()); + LoadAdError loadAdError = + new LoadAdError(LoadAdError.ErrorCode.INTERNAL_ERROR, "domain", /* responseInfo= */ null); + adLoadCallbackCaptor.getValue().onAdFailedToLoad(loadAdError); + + verify(mockCallback, timeout(1000)).onAdFailedToLoad(loadAdError); + } + + @Test + public void testShow_whenAdLoaded_setsVisibilityAndPosition() { + View fakeView = simulateAdLoadSuccess(); + + unityBannerAd.show(); + ShadowLooper.idleMainLooper(); + + verify(mockBannerAd, atLeastOnce()).getView(activity); + + assertThat(fakeView.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void testHide_removesViewFromParent() { + View unusedView = simulateAdLoadSuccess(); + + ShadowLooper.idleMainLooper(); + assertThat(unityBannerAd.hidden).isFalse(); + + unityBannerAd.hide(); + ShadowLooper.idleMainLooper(); + + assertThat(unityBannerAd.hidden).isTrue(); + } + + @Test + public void testAdEvents_invokeCallback() { + View unusedView = simulateAdLoadSuccess(); + + verify(mockBannerAd).setAdEventCallback(adEventCallbackCaptor.capture()); + BannerAdEventCallback eventCallback = adEventCallbackCaptor.getValue(); + + eventCallback.onAdImpression(); + verify(mockCallback).onAdImpression(); + + eventCallback.onAdClicked(); + verify(mockCallback).onAdClicked(); + + eventCallback.onAdShowedFullScreenContent(); + verify(mockCallback).onAdOpened(); + + eventCallback.onAdDismissedFullScreenContent(); + verify(mockCallback).onAdClosed(); + + PrecisionType precisionType = PrecisionType.PRECISE; + long valueMicros = 1000000L; + String currencyCode = "USD"; + AdValue adValue = new AdValue(precisionType, valueMicros, currencyCode); + eventCallback.onAdPaid(adValue); + verify(mockCallback) + .onPaidEvent(Util.getAdValuePrecisionType(precisionType), valueMicros, currencyCode); + } + + @Test + public void testGetResponseInfo_whenAdNotLoaded_returnsNull() { + assertThat(unityBannerAd.getResponseInfo()).isNull(); + } + + @Test + public void testGetResponseInfo_whenAdLoaded_returnsResponseInfo() { + ResponseInfo responseInfo = + new ResponseInfo( + "AdapterName", + "responseId", + new Bundle(), + /* loadedAdSourceResponseInfo= */ null, + new ArrayList<>()); + when(mockBannerAd.getResponseInfo()).thenReturn(responseInfo); + + View unusedView = simulateAdLoadSuccess(); + + ResponseInfo actualResponseInfo = unityBannerAd.getResponseInfo(); + verify(mockBannerAd).getResponseInfo(); + assertThat(actualResponseInfo).isEqualTo(responseInfo); + } + + @Test + public void testGetHeightInPixels_whenAdNotLoaded_returnsNegativeOne() { + assertThat(unityBannerAd.getHeightInPixels()).isEqualTo(-1.0f); + } + + @Test + public void testGetHeightInPixels_whenAdLoaded_returnsHeight() { + AdSize customAdSize = new AdSize(300, 250); + View unusedView = simulateAdLoadSuccess(customAdSize); + + int expectedHeight = customAdSize.getHeightInPixels(activity); + assertThat(unityBannerAd.getHeightInPixels()).isEqualTo(expectedHeight); + } + + @Test + public void testGetWidthInPixels_whenAdNotLoaded_returnsNegativeOne() { + assertThat(unityBannerAd.getWidthInPixels()).isEqualTo(-1.0f); + } + + @Test + public void testGetWidthInPixels_whenAdLoaded_returnsWidth() { + AdSize customAdSize = new AdSize(300, 250); + View unusedView = simulateAdLoadSuccess(customAdSize); + + int expectedWidth = customAdSize.getWidthInPixels(activity); + assertThat(unityBannerAd.getWidthInPixels()).isEqualTo(expectedWidth); + } + + @Test + public void testIsCollapsible_whenAdNotLoaded_returnsFalse() { + assertThat(unityBannerAd.isCollapsible()).isFalse(); + } + + @Test + public void testIsCollapsible_whenAdLoaded_returnsCollapsible() { + when(mockBannerAd.isCollapsible()).thenReturn(true); + View unusedView = simulateAdLoadSuccess(); + + assertThat(unityBannerAd.isCollapsible()).isTrue(); + } + + @Test + public void testDestroy_callsBannerAdDestroy() { + View unusedView = simulateAdLoadSuccess(); + + unityBannerAd.destroy(); + ShadowLooper.idleMainLooper(); + + verify(mockBannerAd).destroy(); + } + + @Test + public void testDestroy_whenBannerAdIsNull_doesNothing() { + unityBannerAd.load(bannerAdRequest); + + unityBannerAd.destroy(); + ShadowLooper.idleMainLooper(); + + verify(mockBannerAd, never()).destroy(); + } + + @Test + public void testShow_beforeAdLoaded_doesNothing() { + unityBannerAd.show(); + ShadowLooper.idleMainLooper(); + + verify(mockBannerAd, never()).getView(activity); + } + + @Test + public void testLoad_whenHidden_doesNotShowAutomatically() { + unityBannerAd.hidden = true; + + View unusedView = simulateAdLoadSuccess(); + + ShadowLooper.idleMainLooper(); + + verify(mockBannerAd, never()).getView(activity); + } + + @Test + public void testCreate_withPositionCode_setsLayoutParamsGravity() { + unityBannerAd.create(PluginUtils.POSITION_BOTTOM); + + FrameLayout.LayoutParams params = unityBannerAd.getLayoutParams(); + + assertThat(params.gravity).isEqualTo(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); + } + + @Test + public void testCreate_withPositionTop_setsLayoutParamsGravity() { + unityBannerAd.create(PluginUtils.POSITION_TOP); + + FrameLayout.LayoutParams params = unityBannerAd.getLayoutParams(); + + assertThat(params.gravity).isEqualTo(Gravity.TOP | Gravity.CENTER_HORIZONTAL); + } + + @Test + public void testCreate_withCustomOffset_setsCustomPositionAndOffsets() { + int customX = 100; + int customY = 200; + unityBannerAd.create(customX, customY); + + FrameLayout.LayoutParams params = unityBannerAd.getLayoutParams(); + + assertThat(params.gravity).isEqualTo(Gravity.TOP | Gravity.LEFT); + float density = activity.getResources().getDisplayMetrics().density; + assertThat(params.leftMargin).isEqualTo((int) (customX * density)); + assertThat(params.topMargin).isEqualTo((int) (customY * density)); + } + + @Test + public void testSetPosition_withPositionCode_updatesLayoutParams() { + unityBannerAd.create(PluginUtils.POSITION_TOP); + + unityBannerAd.setPosition(PluginUtils.POSITION_BOTTOM); + ShadowLooper.idleMainLooper(); + + FrameLayout.LayoutParams params = unityBannerAd.getLayoutParams(); + assertThat(params.gravity).isEqualTo(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); + } + + @Test + public void testSetPosition_withCustomOffset_updatesLayoutParams() { + unityBannerAd.create(PluginUtils.POSITION_TOP); + + int customX = 50; + int customY = 60; + unityBannerAd.setPosition(customX, customY); + ShadowLooper.idleMainLooper(); + + FrameLayout.LayoutParams params = unityBannerAd.getLayoutParams(); + assertThat(params.gravity).isEqualTo(Gravity.TOP | Gravity.LEFT); + + float density = activity.getResources().getDisplayMetrics().density; + assertThat(params.leftMargin).isEqualTo((int) (customX * density)); + assertThat(params.topMargin).isEqualTo((int) (customY * density)); + } + + @Test + public void testLayoutChangeListener_onLayoutChange_updatesPositionIfBoundsChanged() { + View fakeView = simulateAdLoadSuccess(); + unityBannerAd.show(); + ShadowLooper.idleMainLooper(); + + FrameLayout.LayoutParams initialParams = (FrameLayout.LayoutParams) fakeView.getLayoutParams(); + assertThat(initialParams).isNotNull(); + int initialGravity = initialParams.gravity; + + unityBannerAd.setPosition(PluginUtils.POSITION_BOTTOM); + + View decorView = activity.getWindow().getDecorView(); + decorView.layout(0, 0, 500, 500); + ShadowLooper.idleMainLooper(); + + FrameLayout.LayoutParams updatedParams = (FrameLayout.LayoutParams) fakeView.getLayoutParams(); + assertThat(updatedParams).isNotNull(); + assertThat(updatedParams.gravity).isNotEqualTo(initialGravity); + } + + @Test + @Config(sdk = 27) + public void testGetSafeInsets_sdkLessThanP_returnsEmptyInsets() { + View fakeView = new View(activity); + when(mockBannerAd.getView(activity)).thenReturn(fakeView); + unityBannerAd.load(bannerAdRequest); + verify(mockAdWrapper).load(eq(bannerAdRequest), adLoadCallbackCaptor.capture()); + adLoadCallbackCaptor.getValue().onAdLoaded(mockBannerAd); + + unityBannerAd.create(PluginUtils.POSITION_TOP); + ShadowLooper.idleMainLooper(); + + FrameLayout.LayoutParams params = unityBannerAd.getLayoutParams(); + assertThat(params.leftMargin).isEqualTo(0); + assertThat(params.topMargin).isEqualTo(0); + assertThat(params.rightMargin).isEqualTo(0); + assertThat(params.bottomMargin).isEqualTo(0); + + // Verify that setLayoutParams was called on the fakeView. + assertThat(fakeView.getLayoutParams()).isInstanceOf(FrameLayout.LayoutParams.class); + FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) fakeView.getLayoutParams(); + assertThat(layoutParams.leftMargin).isEqualTo(0); + assertThat(layoutParams.topMargin).isEqualTo(0); + assertThat(layoutParams.rightMargin).isEqualTo(0); + assertThat(layoutParams.bottomMargin).isEqualTo(0); + } + + @Test + public void testGetHeightInPixels_whenTaskThrowsException_returnsNegativeOne() { + View fakeView = new View(activity); + when(mockBannerAd.getView(activity)).thenReturn(fakeView); + + when(mockBannerAd.getAdSize()).thenThrow(new RuntimeException("Test Exception")); + + unityBannerAd.load(bannerAdRequest); + verify(mockAdWrapper).load(eq(bannerAdRequest), adLoadCallbackCaptor.capture()); + adLoadCallbackCaptor.getValue().onAdLoaded(mockBannerAd); + + assertThat(unityBannerAd.getHeightInPixels()).isEqualTo(-1.0f); + } + + @Test + public void testGetWidthInPixels_whenTaskThrowsException_returnsNegativeOne() { + View fakeView = new View(activity); + when(mockBannerAd.getView(activity)).thenReturn(fakeView); + + when(mockBannerAd.getAdSize()).thenThrow(new RuntimeException("Test Exception")); + + unityBannerAd.load(bannerAdRequest); + verify(mockAdWrapper).load(eq(bannerAdRequest), adLoadCallbackCaptor.capture()); + adLoadCallbackCaptor.getValue().onAdLoaded(mockBannerAd); + + assertThat(unityBannerAd.getWidthInPixels()).isEqualTo(-1.0f); + } + + @Test + public void testDestroy_whenHiddenAndNotShown_doesNotThrowAndCallsDestroy() { + unityBannerAd.hidden = true; + + View unusedView = simulateAdLoadSuccess(); + + ShadowLooper.idleMainLooper(); + unityBannerAd.destroy(); + ShadowLooper.idleMainLooper(); + + verify(mockBannerAd).destroy(); + } + + @Test + @Config(sdk = 28) + public void testGetSafeInsets_whenWindowIsNull_returnsZeroInsets() { + shadowOf(activity).setWindow(null); + UnityBannerAd ad = new UnityBannerAd(activity, mockCallback, mockAdWrapper); + ad.create(PluginUtils.POSITION_TOP); + + FrameLayout.LayoutParams params = ad.getLayoutParams(); + + assertThat(params.leftMargin).isEqualTo(0); + assertThat(params.topMargin).isEqualTo(0); + assertThat(params.rightMargin).isEqualTo(0); + assertThat(params.bottomMargin).isEqualTo(0); + } + + @Test + public void testPublicConstructor_createsInstanceWithoutCrash() { + UnityBannerAd ad = new UnityBannerAd(activity, mockCallback); + assertThat(ad).isNotNull(); + } + + @Test + public void testLoad_multipleTimes_onlyOneLayoutChangeListener() throws Exception { + java.lang.reflect.Field field = UnityBannerAd.class.getDeclaredField("layoutChangeListener"); + field.setAccessible(true); + + View.OnLayoutChangeListener listenerBefore = + (View.OnLayoutChangeListener) field.get(unityBannerAd); + assertThat(listenerBefore).isNull(); + + unityBannerAd.load(bannerAdRequest); + + View.OnLayoutChangeListener firstRegistration = + (View.OnLayoutChangeListener) field.get(unityBannerAd); + assertThat(firstRegistration).isNotNull(); + + unityBannerAd.load(bannerAdRequest); + + View.OnLayoutChangeListener secondRegistration = + (View.OnLayoutChangeListener) field.get(unityBannerAd); + + assertThat(firstRegistration).isSameInstanceAs(secondRegistration); + } + + @Test + public void testDestroy_nullifiesLayoutChangeListener() throws Exception { + + unityBannerAd.load(bannerAdRequest); + + java.lang.reflect.Field field = UnityBannerAd.class.getDeclaredField("layoutChangeListener"); + field.setAccessible(true); + View.OnLayoutChangeListener listener = (View.OnLayoutChangeListener) field.get(unityBannerAd); + assertThat(listener).isNotNull(); + + unityBannerAd.destroy(); + + View.OnLayoutChangeListener listenerAfter = + (View.OnLayoutChangeListener) field.get(unityBannerAd); + assertThat(listenerAfter).isNull(); + } +}