From a0b45335fa839304375c5ab423d35fd2828b5e66 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 24 Oct 2022 12:57:24 +1100 Subject: [PATCH] Allow serving `Custom` ads in `AdLoaderAd` Allow the `AdLoaderAd` instance to serve `Custom` ads, which are instances of: * `NativeCustomFormatAd` under Android, and * `GADCustomNativeAd` under iOS --- .../googlemobileads/AdMessageCodec.java | 10 +++ .../googlemobileads/FlutterAdListener.java | 19 +++++ .../googlemobileads/FlutterAdLoader.java | 16 +++- .../googlemobileads/FlutterAdLoaderAd.java | 58 +++++++++++++- .../FlutterCustomParameters.java | 30 ++++++++ .../GoogleMobileAdsPlugin.java | 70 +++++++++++++++++ .../googlemobileads/AdMessageCodecTest.java | 16 ++++ .../FlutterAdLoaderAdTest.java | 76 +++++++++++++++++-- .../Classes/FLTAdInstanceManager_Internal.h | 4 + .../Classes/FLTAdInstanceManager_Internal.m | 16 ++++ .../ios/Classes/FLTAd_Internal.h | 16 +++- .../ios/Classes/FLTAd_Internal.m | 68 ++++++++++++++++- .../ios/Classes/FLTGoogleMobileAdsPlugin.h | 15 ++++ .../ios/Classes/FLTGoogleMobileAdsPlugin.m | 65 +++++++++++++++- .../FLTGoogleMobileAdsReaderWriter_Internal.m | 11 +++ .../ios/Tests/FLTAdLoaderAdTest.m | 65 +++++++++++++++- .../FLTGoogleMobileAdsReaderWriterTest.m | 27 +++++++ .../lib/src/ad_containers.dart | 31 ++++++++ .../lib/src/ad_instance_manager.dart | 16 ++++ .../test/ad_loader_ad_test.dart | 29 +++++++ .../test/mobile_ads_test.dart | 30 ++++++++ 21 files changed, 670 insertions(+), 18 deletions(-) create mode 100644 packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdMessageCodec.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdMessageCodec.java index 248b78955..09fd2a7d4 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdMessageCodec.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdMessageCodec.java @@ -67,6 +67,7 @@ class AdMessageCodec extends StandardMessageCodec { private static final byte VALUE_COLOR = (byte) 153; private static final byte VALUE_AD_MANAGER_AD_VIEW_OPTIONS = (byte) 154; private static final byte VALUE_BANNER_PARAMETERS = (byte) 155; + private static final byte VALUE_CUSTOM_PARAMETERS = (byte) 156; @NonNull Context context; @NonNull final FlutterAdSize.AdSizeFactory adSizeFactory; @@ -253,6 +254,11 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { FlutterBannerParameters bannerParameters = (FlutterBannerParameters) value; writeValue(stream, bannerParameters.sizes); writeValue(stream, bannerParameters.adManagerAdViewOptions); + } else if (value instanceof FlutterCustomParameters) { + stream.write(VALUE_CUSTOM_PARAMETERS); + FlutterCustomParameters customParameters = (FlutterCustomParameters) value; + writeValue(stream, customParameters.formatIds); + writeValue(stream, customParameters.viewOptions); } else { super.writeValue(stream, value); } @@ -419,6 +425,10 @@ protected Object readValueOfType(byte type, ByteBuffer buffer) { return new FlutterBannerParameters( (List) readValueOfType(buffer.get(), buffer), (FlutterAdManagerAdViewOptions) readValueOfType(buffer.get(), buffer)); + case VALUE_CUSTOM_PARAMETERS: + return new FlutterCustomParameters( + (List) readValueOfType(buffer.get(), buffer), + (Map) readValueOfType(buffer.get(), buffer)); default: return super.readValueOfType(type, buffer); } diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java index cd0efbefe..1133e46e9 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java @@ -20,6 +20,8 @@ import com.google.android.gms.ads.formats.OnAdManagerAdViewLoadedListener; import com.google.android.gms.ads.nativead.NativeAd; import com.google.android.gms.ads.nativead.NativeAd.OnNativeAdLoadedListener; +import com.google.android.gms.ads.nativead.NativeCustomFormatAd; +import com.google.android.gms.ads.nativead.NativeCustomFormatAd.OnCustomFormatAdLoadedListener; import java.lang.ref.WeakReference; /** Callback type to notify when an ad successfully loads. */ @@ -137,3 +139,20 @@ public void onAdManagerAdViewLoaded(AdManagerAdView adView) { } } } + +/** {@link OnCustomFormatAdLoadedListener} for custom ads. */ +class FlutterCustomFormatAdLoadedListener implements OnCustomFormatAdLoadedListener { + + private final WeakReference reference; + + FlutterCustomFormatAdLoadedListener(OnCustomFormatAdLoadedListener listener) { + reference = new WeakReference<>(listener); + } + + @Override + public void onCustomFormatAdLoaded(NativeCustomFormatAd ad) { + if (reference.get() != null) { + reference.get().onCustomFormatAdLoaded(ad); + } + } +} diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java index e2f466fe9..89ec55302 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java @@ -146,7 +146,8 @@ public void loadAdLoaderAd( @NonNull String adUnitId, @NonNull AdListener adListener, @NonNull AdRequest request, - @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters) { + @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters, + @Nullable FlutterAdLoaderAd.CustomParameters customParameters) { AdLoader.Builder builder = new AdLoader.Builder(context, adUnitId); if (bannerParameters != null) { builder = builder.forAdManagerAdView(bannerParameters.listener, bannerParameters.adSizes); @@ -154,6 +155,11 @@ public void loadAdLoaderAd( builder.withAdManagerAdViewOptions(bannerParameters.adManagerAdViewOptions); } } + if (customParameters != null) { + for (String formatId : customParameters.factories.keySet()) { + builder = builder.forCustomFormatAd(formatId, customParameters.listener, null); + } + } builder.withAdListener(adListener).build().loadAd(request); } @@ -162,7 +168,8 @@ public void loadAdManagerAdLoaderAd( @NonNull String adUnitId, @NonNull AdListener adListener, @NonNull AdManagerAdRequest adManagerAdRequest, - @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters) { + @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters, + @Nullable FlutterAdLoaderAd.CustomParameters customParameters) { AdLoader.Builder builder = new AdLoader.Builder(context, adUnitId); if (bannerParameters != null) { builder = builder.forAdManagerAdView(bannerParameters.listener, bannerParameters.adSizes); @@ -170,6 +177,11 @@ public void loadAdManagerAdLoaderAd( builder.withAdManagerAdViewOptions(bannerParameters.adManagerAdViewOptions); } } + if (customParameters != null) { + for (String formatId : customParameters.factories.keySet()) { + builder = builder.forCustomFormatAd(formatId, customParameters.listener, null); + } + } builder.withAdListener(adListener).build().loadAd(adManagerAdRequest); } } diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java index 184106fe8..bb758d1e9 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java @@ -24,13 +24,18 @@ import com.google.android.gms.ads.admanager.AdManagerAdView; import com.google.android.gms.ads.formats.AdManagerAdViewOptions; import com.google.android.gms.ads.formats.OnAdManagerAdViewLoadedListener; +import com.google.android.gms.ads.nativead.NativeCustomFormatAd; +import com.google.android.gms.ads.nativead.NativeCustomFormatAd.OnCustomFormatAdLoadedListener; import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.CustomAdFactory; +import java.util.Map; /** * A central wrapper for {@link AdManagerAdView}, {@link NativeCustomFormatAd} and {@link NativeAd} * instances served for a single {@link AdRequest} or {@link AdManagerAdRequest} */ -class FlutterAdLoaderAd extends FlutterAd implements OnAdManagerAdViewLoadedListener { +class FlutterAdLoaderAd extends FlutterAd + implements OnAdManagerAdViewLoadedListener, OnCustomFormatAdLoadedListener { private static final String TAG = "FlutterAdLoaderAd"; @NonNull private final AdInstanceManager manager; @@ -40,6 +45,7 @@ class FlutterAdLoaderAd extends FlutterAd implements OnAdManagerAdViewLoadedList @Nullable private FlutterAdManagerAdRequest adManagerRequest; @Nullable private View view; @Nullable protected BannerParameters bannerParameters; + @Nullable protected CustomParameters customParameters; static class Builder { @Nullable private AdInstanceManager manager; @@ -49,6 +55,8 @@ static class Builder { @Nullable private Integer id; @Nullable private FlutterAdLoader adLoader; @Nullable private FlutterBannerParameters bannerParameters; + @Nullable private FlutterCustomParameters customParameters; + @Nullable private Map customFactories; public Builder setId(int id) { this.id = id; @@ -85,6 +93,17 @@ public Builder setBanner(@Nullable FlutterBannerParameters bannerParameters) { return this; } + public Builder setCustom(@Nullable FlutterCustomParameters customParameters) { + this.customParameters = customParameters; + return this; + } + + public Builder withAvailableCustomFactories( + @NonNull Map customFactories) { + this.customFactories = customFactories; + return this; + } + FlutterAdLoaderAd build() { if (manager == null) { throw new IllegalStateException("manager must be provided"); @@ -112,6 +131,12 @@ FlutterAdLoaderAd build() { new FlutterAdManagerAdViewLoadedListener(adLoaderAd)); } + if (customParameters != null) { + adLoaderAd.customParameters = + customParameters.asCustomParameters( + new FlutterCustomFormatAdLoadedListener(adLoaderAd), customFactories); + } + return adLoaderAd; } } @@ -131,6 +156,21 @@ static class BannerParameters { } } + static class CustomParameters { + @NonNull final OnCustomFormatAdLoadedListener listener; + @NonNull final Map factories; + @Nullable final Map viewOptions; + + CustomParameters( + @NonNull OnCustomFormatAdLoadedListener listener, + @NonNull Map factories, + @Nullable Map viewOptions) { + this.listener = listener; + this.factories = factories; + this.viewOptions = viewOptions; + } + } + protected FlutterAdLoaderAd( int adId, @NonNull AdInstanceManager manager, @@ -164,13 +204,17 @@ void load() { // As of 20.0.0 of GMA, mockito is unable to mock AdLoader. if (request != null) { adLoader.loadAdLoaderAd( - adUnitId, adListener, request.asAdRequest(adUnitId), bannerParameters); + adUnitId, adListener, request.asAdRequest(adUnitId), bannerParameters, customParameters); return; } if (adManagerRequest != null) { adLoader.loadAdManagerAdLoaderAd( - adUnitId, adListener, adManagerRequest.asAdManagerAdRequest(adUnitId), bannerParameters); + adUnitId, + adListener, + adManagerRequest.asAdManagerAdRequest(adUnitId), + bannerParameters, + customParameters); return; } @@ -193,6 +237,14 @@ public void onAdManagerAdViewLoaded(@NonNull AdManagerAdView adView) { manager.onAdLoaded(adId, adView.getResponseInfo()); } + @Override + public void onCustomFormatAdLoaded(@NonNull NativeCustomFormatAd ad) { + String formatId = ad.getCustomFormatId(); + view = + customParameters.factories.get(formatId).createCustomAd(ad, customParameters.viewOptions); + manager.onAdLoaded(adId, null); + } + @Override void dispose() { if (view == null) { diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java new file mode 100644 index 000000000..3086c7084 --- /dev/null +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java @@ -0,0 +1,30 @@ +package io.flutter.plugins.googlemobileads; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.ads.nativead.NativeCustomFormatAd.OnCustomFormatAdLoadedListener; +import io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.CustomAdFactory; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class FlutterCustomParameters { + @NonNull final List formatIds; + @Nullable final Map viewOptions; + + FlutterCustomParameters( + @NonNull List formatIds, @Nullable Map viewOptions) { + this.formatIds = formatIds; + this.viewOptions = viewOptions; + } + + FlutterAdLoaderAd.CustomParameters asCustomParameters( + @NonNull OnCustomFormatAdLoadedListener listener, + @NonNull Map availableFactories) { + Map factories = new HashMap<>(); + for (String formatId : formatIds) { + factories.put(formatId, availableFactories.get(formatId)); + } + return new FlutterAdLoaderAd.CustomParameters(listener, factories, viewOptions); + } +} diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java index e689615bc..4ebdb92b4 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java @@ -16,6 +16,7 @@ import android.content.Context; import android.util.Log; +import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -28,6 +29,7 @@ import com.google.android.gms.ads.initialization.OnInitializationCompleteListener; import com.google.android.gms.ads.nativead.NativeAd; import com.google.android.gms.ads.nativead.NativeAdView; +import com.google.android.gms.ads.nativead.NativeCustomFormatAd; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; @@ -67,6 +69,7 @@ private static T requireNonNull(T obj) { @Nullable private AppStateNotifier appStateNotifier; @Nullable private UserMessagingPlatformManager userMessagingPlatformManager; private final Map nativeAdFactories = new HashMap<>(); + private final Map customAdFactories = new HashMap<>(); @Nullable private MediationNetworkExtrasProvider mediationNetworkExtrasProvider; private final FlutterMobileAdsWrapper flutterMobileAds; /** @@ -115,6 +118,11 @@ public interface NativeAdFactory { NativeAdView createNativeAd(NativeAd nativeAd, Map customOptions); } + public interface CustomAdFactory { + View createCustomAd( + @NonNull NativeCustomFormatAd nativeAd, @Nullable Map customOptions); + } + /** * Registers a {@link io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.NativeAdFactory} * used to create {@link com.google.android.gms.ads.nativead.NativeAdView}s from a Native Ad @@ -134,6 +142,13 @@ public static boolean registerNativeAdFactory( return registerNativeAdFactory(gmaPlugin, factoryId, nativeAdFactory); } + public static boolean registerCustomAdFactory( + FlutterEngine engine, String formatId, CustomAdFactory customAdFactory) { + final GoogleMobileAdsPlugin gmaPlugin = + (GoogleMobileAdsPlugin) engine.getPlugins().get(GoogleMobileAdsPlugin.class); + return registerCustomAdFactory(gmaPlugin, formatId, customAdFactory); + } + /** * Registers a {@link MediationNetworkExtrasProvider} used to provide network extras to the plugin * when it creates ad requests. @@ -190,6 +205,19 @@ private static boolean registerNativeAdFactory( return plugin.addNativeAdFactory(factoryId, nativeAdFactory); } + private static boolean registerCustomAdFactory( + GoogleMobileAdsPlugin plugin, String formatId, CustomAdFactory customAdFactory) { + if (plugin == null) { + final String message = + String.format( + "Could not find a %s instance. The plugin may have not been registered.", + GoogleMobileAdsPlugin.class.getSimpleName()); + throw new IllegalStateException(message); + } + + return plugin.addCustomAdFactory(formatId, customAdFactory); + } + /** * Unregisters a {@link io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.NativeAdFactory} * used to create {@link com.google.android.gms.ads.nativead.NativeAdView}s from a Native Ad @@ -212,6 +240,16 @@ public static NativeAdFactory unregisterNativeAdFactory(FlutterEngine engine, St return null; } + @Nullable + public static CustomAdFactory unregisterCustomAdFactory(FlutterEngine engine, String formatId) { + final FlutterPlugin gmaPlugin = engine.getPlugins().get(GoogleMobileAdsPlugin.class); + if (gmaPlugin != null) { + return ((GoogleMobileAdsPlugin) gmaPlugin).removeCustomAdFactory(formatId); + } + + return null; + } + private boolean addNativeAdFactory(String factoryId, NativeAdFactory nativeAdFactory) { if (nativeAdFactories.containsKey(factoryId)) { final String errorMessage = @@ -229,6 +267,23 @@ private NativeAdFactory removeNativeAdFactory(String factoryId) { return nativeAdFactories.remove(factoryId); } + private boolean addCustomAdFactory(String formatId, CustomAdFactory customAdFactory) { + if (customAdFactories.containsKey(formatId)) { + final String errorMessage = + String.format( + "A CustomAdFactory with the following formatId already exists: %s", formatId); + Log.e(GoogleMobileAdsPlugin.class.getSimpleName(), errorMessage); + return false; + } + + customAdFactories.put(formatId, customAdFactory); + return true; + } + + private CustomAdFactory removeCustomAdFactory(String formatId) { + return customAdFactories.remove(formatId); + } + @Override public void onAttachedToEngine(FlutterPluginBinding binding) { pluginBinding = binding; @@ -420,6 +475,19 @@ public void onAdInspectorClosed(@Nullable AdInspectorError adInspectorError) { result.success(null); break; case "loadAdLoaderAd": + final FlutterCustomParameters customParameters = + call.argument("custom"); + if (customParameters != null) { + for (String formatId : customParameters.formatIds) { + if (customAdFactories.get(formatId) == null) { + final String message = + String.format("Can't find CustomAdFactory with id: %s", formatId); + result.error("AdLoaderAdError", message, null); + return; + } + } + } + final FlutterAdLoaderAd adLoaderAd = new FlutterAdLoaderAd.Builder() .setManager(instanceManager) @@ -429,6 +497,8 @@ public void onAdInspectorClosed(@Nullable AdInspectorError adInspectorError) { .setId(call.argument("adId")) .setFlutterAdLoader(new FlutterAdLoader(context)) .setBanner(call.argument("banner")) + .setCustom(customParameters) + .withAvailableCustomFactories(customAdFactories) .build(); instanceManager.trackAd(adLoaderAd, call.argument("adId")); adLoaderAd.load(); diff --git a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/AdMessageCodecTest.java b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/AdMessageCodecTest.java index adade95aa..a6574dbde 100644 --- a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/AdMessageCodecTest.java +++ b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/AdMessageCodecTest.java @@ -560,4 +560,20 @@ public void encodeBannerParameters() { assertEquals(result.sizes.get(0).height, 2); assertNull(result.adManagerAdViewOptions.manualImpressionsEnabled); } + + @Test + public void encodeCustomParameters() { + final ByteBuffer data = + codec.encodeMessage( + new FlutterCustomParameters( + Collections.singletonList("format-id"), Collections.singletonMap("key", "value"))); + + final FlutterCustomParameters result = + (FlutterCustomParameters) codec.decodeMessage((ByteBuffer) data.position(0)); + + assertEquals(result.formatIds.size(), 1); + assertEquals(result.formatIds.get(0), "format-id"); + assertEquals(result.viewOptions.size(), 1); + assertEquals(result.viewOptions.get("key"), "value"); + } } diff --git a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAdTest.java b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAdTest.java index 8ed1e350d..fba745f2b 100644 --- a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAdTest.java +++ b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAdTest.java @@ -32,8 +32,11 @@ import com.google.android.gms.ads.ResponseInfo; import com.google.android.gms.ads.admanager.AdManagerAdRequest; import com.google.android.gms.ads.admanager.AdManagerAdView; +import com.google.android.gms.ads.nativead.NativeCustomFormatAd; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.googlemobileads.FlutterAd.FlutterLoadAdError; +import io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.CustomAdFactory; +import java.util.Collections; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -82,12 +85,14 @@ public Object answer(InvocationOnMock invocation) { } }) .when(mockLoader) - .loadAdManagerAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest), isNull()); + .loadAdManagerAdLoaderAd( + eq("testId"), any(AdListener.class), eq(mockRequest), isNull(), isNull()); adLoaderAd.load(); verify(mockLoader) - .loadAdManagerAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest), isNull()); + .loadAdManagerAdLoaderAd( + eq("testId"), any(AdListener.class), eq(mockRequest), isNull(), isNull()); verify(testManager).onAdClicked(eq(1)); verify(testManager).onAdClosed(eq(1)); @@ -125,12 +130,12 @@ public Object answer(InvocationOnMock invocation) { } }) .when(mockLoader) - .loadAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest), isNull()); + .loadAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest), isNull(), isNull()); adLoaderAd.load(); verify(mockLoader) - .loadAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest), isNull()); + .loadAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest), isNull(), isNull()); verify(testManager).onAdClicked(eq(1)); verify(testManager).onAdClosed(eq(1)); @@ -181,13 +186,13 @@ public Object answer(InvocationOnMock invocation) { }) .when(mockLoader) .loadAdManagerAdLoaderAd( - eq("testId"), any(AdListener.class), eq(mockRequest), eq(bannerParameters)); + eq("testId"), any(AdListener.class), eq(mockRequest), eq(bannerParameters), isNull()); adLoaderAd.load(); verify(mockLoader) .loadAdManagerAdLoaderAd( - eq("testId"), any(AdListener.class), eq(mockRequest), eq(bannerParameters)); + eq("testId"), any(AdListener.class), eq(mockRequest), eq(bannerParameters), isNull()); verify(testManager).onAdClicked(eq(1)); verify(testManager).onAdClosed(eq(1)); @@ -198,6 +203,65 @@ public Object answer(InvocationOnMock invocation) { verify(testManager).onAdLoaded(eq(1), eq(mockResponseInfo)); } + @Test + public void loadAdLoaderAdCustomWithAdManagerAdRequest() { + final FlutterAdManagerAdRequest mockFlutterRequest = mock(FlutterAdManagerAdRequest.class); + final AdManagerAdRequest mockRequest = mock(AdManagerAdRequest.class); + when(mockFlutterRequest.asAdManagerAdRequest(anyString())).thenReturn(mockRequest); + FlutterAdLoader mockLoader = mock(FlutterAdLoader.class); + final FlutterAdLoaderAd adLoaderAd = + new FlutterAdLoaderAd(1, testManager, "testId", mockFlutterRequest, mockLoader); + final FlutterCustomFormatAdLoadedListener listener = + new FlutterCustomFormatAdLoadedListener(adLoaderAd); + final CustomAdFactory mockCustomAdFactory = mock(CustomAdFactory.class); + final FlutterAdLoaderAd.CustomParameters customParameters = + new FlutterAdLoaderAd.CustomParameters( + listener, Collections.singletonMap("formatId", mockCustomAdFactory), null); + adLoaderAd.customParameters = customParameters; + + final LoadAdError mockLoadAdError = mock(LoadAdError.class); + when(mockLoadAdError.getCode()).thenReturn(1); + when(mockLoadAdError.getDomain()).thenReturn("2"); + when(mockLoadAdError.getMessage()).thenReturn("3"); + + final NativeCustomFormatAd mockNativeCustomFormatAd = mock(NativeCustomFormatAd.class); + when(mockNativeCustomFormatAd.getCustomFormatId()).thenReturn("formatId"); + + doAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) { + AdListener listener = invocation.getArgument(1); + listener.onAdClicked(); + listener.onAdClosed(); + listener.onAdFailedToLoad(mockLoadAdError); + listener.onAdImpression(); + listener.onAdOpened(); + + FlutterAdLoaderAd.CustomParameters customParameters = invocation.getArgument(4); + customParameters.listener.onCustomFormatAdLoaded(mockNativeCustomFormatAd); + return null; + } + }) + .when(mockLoader) + .loadAdManagerAdLoaderAd( + eq("testId"), any(AdListener.class), eq(mockRequest), isNull(), eq(customParameters)); + + adLoaderAd.load(); + + verify(mockLoader) + .loadAdManagerAdLoaderAd( + eq("testId"), any(AdListener.class), eq(mockRequest), isNull(), eq(customParameters)); + + verify(testManager).onAdClicked(eq(1)); + verify(testManager).onAdClosed(eq(1)); + FlutterLoadAdError expectedError = new FlutterLoadAdError(mockLoadAdError); + verify(testManager).onAdFailedToLoad(eq(1), eq(expectedError)); + verify(testManager).onAdImpression(eq(1)); + verify(testManager).onAdOpened(eq(1)); + verify(testManager).onAdLoaded(eq(1), isNull()); + } + @Test(expected = IllegalStateException.class) public void adLoaderAdBuilderNullManager() { new FlutterAdLoaderAd.Builder() diff --git a/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.h b/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.h index 459356b84..72f108bf2 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.h +++ b/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.h @@ -44,6 +44,10 @@ - (void)onNativeAdWillPresentScreen:(nonnull id)ad; - (void)onNativeAdDidDismissScreen:(nonnull id)ad; - (void)onNativeAdWillDismissScreen:(nonnull id)ad; +- (void)onCustomNativeAdImpression:(nonnull id)ad; +- (void)onCustomNativeAdWillPresentScreen:(nonnull id)ad; +- (void)onCustomNativeAdDidDismissScreen:(nonnull id)ad; +- (void)onCustomNativeAdWillDismissScreen:(nonnull id)ad; - (void)onRewardedAdUserEarnedReward:(FLTRewardedAd *_Nonnull)ad reward:(FLTRewardItem *_Nonnull)reward; - (void)onRewardedInterstitialAdUserEarnedReward: diff --git a/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.m b/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.m index f57a655ee..c50290ee6 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.m +++ b/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.m @@ -126,6 +126,22 @@ - (void)onNativeAdWillDismissScreen:(nonnull id)ad { [self sendAdEvent:@"onNativeAdWillDismissScreen" ad:ad]; } +- (void)onCustomNativeAdImpression:(nonnull id)ad { + [self sendAdEvent:@"onCustomNativeAdImpression" ad:ad]; +} + +- (void)onCustomNativeAdWillPresentScreen:(nonnull id)ad { + [self sendAdEvent:@"onCustomNativeAdWillPresentScreen" ad:ad]; +} + +- (void)onCustomNativeAdDidDismissScreen:(nonnull id)ad { + [self sendAdEvent:@"onCustomNativeAdDidDismissScreen" ad:ad]; +} + +- (void)onCustomNativeAdWillDismissScreen:(nonnull id)ad { + [self sendAdEvent:@"onCustomNativeAdWillDismissScreen" ad:ad]; +} + - (void)onRewardedAdUserEarnedReward:(FLTRewardedAd *_Nonnull)ad reward:(FLTRewardItem *_Nonnull)reward { [_channel invokeMethod:@"onAdEvent" diff --git a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h index daaa46979..95fc79473 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h +++ b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h @@ -23,6 +23,7 @@ @class FLTAdInstanceManager; @protocol FLTNativeAdFactory; +@protocol FLTCustomAdFactory; @interface FLTAdSize : NSObject @property(readonly) GADAdSize size; @@ -324,17 +325,28 @@ (nullable FLTAdManagerAdViewOptions *)options; @end +@interface FLTCustomParameters : NSObject +@property(readonly, nonnull) NSArray *formatIds; +@property(readonly, nullable) NSDictionary *viewOptions; +@property(nullable) NSDictionary> *factories; +- (nonnull instancetype) + initWithFormatIds:(nonnull NSArray *)formatIds + viewOptions:(NSDictionary *_Nullable)viewOptions; +@end + @interface FLTAdLoaderAd : FLTBaseAd + GADAppEventDelegate, GADCustomNativeAdLoaderDelegate, + GADCustomNativeAdDelegate> @property(readonly, nonnull) GADAdLoader *adLoader; - (nonnull instancetype) initWithAdUnitId:(nonnull NSString *)adUnitId request:(nonnull FLTAdRequest *)request rootViewController:(nonnull UIViewController *)rootViewController adId:(nonnull NSNumber *)adId - banner:(nullable FLTBannerParameters *)bannerParameters; + banner:(nullable FLTBannerParameters *)bannerParameters + custom:(nullable FLTCustomParameters *)customParameters; @end @interface FLTRewardItem : NSObject diff --git a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m index 062d61808..21ae3233b 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m +++ b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m @@ -1225,6 +1225,18 @@ - (nonnull instancetype)initWithSizes:(nonnull NSArray *)sizes } @end +@implementation FLTCustomParameters +- (nonnull instancetype) + initWithFormatIds:(nonnull NSArray *)formatIds + viewOptions:(nullable NSDictionary *)viewOptions { + self = [super init]; + _formatIds = formatIds; + _viewOptions = viewOptions; + _factories = [NSMutableDictionary dictionary]; + return self; +} +@end + #pragma mark - FLTAdLoaderAd @implementation FLTAdLoaderAd { @@ -1233,6 +1245,7 @@ @implementation FLTAdLoaderAd { NSMutableArray *_validAdSizes; UIView *_view; FLTBannerParameters *_banner; + FLTCustomParameters *_custom; } - (nonnull instancetype) @@ -1240,7 +1253,8 @@ @implementation FLTAdLoaderAd { request:(nonnull FLTAdRequest *)request rootViewController:(nonnull UIViewController *)rootViewController adId:(nonnull NSNumber *)adId - banner:(nullable FLTBannerParameters *)bannerParameters { + banner:(nullable FLTBannerParameters *)bannerParameters + custom:(nullable FLTCustomParameters *)customParameters { self = [super init]; if (self) { self.adId = adId; @@ -1265,6 +1279,12 @@ @implementation FLTAdLoaderAd { } } + if (![FLTAdUtil isNull:customParameters]) { + _custom = customParameters; + + [adTypes addObject:GADAdLoaderAdTypeCustomNative]; + } + _adLoader = [[GADAdLoader alloc] initWithAdUnitID:_adUnitId rootViewController:rootViewController adTypes:adTypes @@ -1363,6 +1383,52 @@ - (void)adView:(nonnull GADBannerView *)banner [self.manager onAppEvent:self name:name data:info]; } +#pragma mark - GADCustomNativeAdLoaderDelegate + +- (nonnull NSArray *)customNativeAdFormatIDsForAdLoader: + (nonnull GADAdLoader *)adLoader { + return _custom.formatIds; +} + +- (void)adLoader:(nonnull GADAdLoader *)adLoader + didReceiveCustomNativeAd:(nonnull GADCustomNativeAd *)customNativeAd { + // Use Nil instead of Null to fix crash with Swift integrations. + NSDictionary *customOptions = + [[NSNull null] isEqual:_custom.viewOptions] ? nil : _custom.viewOptions; + _view = [_custom.factories[customNativeAd.formatID] + createCustomNativeAd:customNativeAd + customOptions:customOptions]; + + customNativeAd.delegate = self; + + [customNativeAd recordImpression]; + + [manager onAdLoaded:self responseInfo:customNativeAd.responseInfo]; +} + +#pragma mark - GADCustomNativeAdDelegate + +- (void)customNativeAdDidRecordImpression: + (nonnull GADCustomNativeAd *)nativeAd { + [manager onCustomNativeAdImpression:self]; +} + +- (void)customNativeAdDidRecordClick:(nonnull GADCustomNativeAd *)nativeAd { + [manager adDidRecordClick:self]; +} + +- (void)customNativeAdWillPresentScreen:(nonnull GADCustomNativeAd *)nativeAd { + [manager onCustomNativeAdWillPresentScreen:self]; +} + +- (void)customNativeAdWillDismissScreen:(nonnull GADCustomNativeAd *)nativeAd { + [manager onCustomNativeAdWillDismissScreen:self]; +} + +- (void)customNativeAdDidDismissScreen:(nonnull GADCustomNativeAd *)nativeAd { + [manager onCustomNativeAdDidDismissScreen:self]; +} + #pragma mark - FlutterPlatformView - (nonnull UIView *)view { diff --git a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.h b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.h index 4a3f2a79d..b46ed385a 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.h +++ b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.h @@ -46,6 +46,13 @@ (NSDictionary *_Nullable)customOptions; @end +@protocol FLTCustomAdFactory +@required +- (nullable UIView *) + createCustomNativeAd:(nonnull GADCustomNativeAd *)customNativeAd + customOptions:(nullable NSDictionary *)customOptions; +@end + /** * Flutter plugin providing access to the Google Mobile Ads API. */ @@ -93,6 +100,10 @@ nativeAdFactory: (id _Nonnull)nativeAdFactory; ++ (BOOL)registerCustomAdFactory:(nonnull id)registry + formatId:(nonnull NSString *)formatId + customAdFactory:(nonnull id)customAdFactory; + /** * Unregisters a `FLTNativeAdFactory` used to create `GADNativeAdView`s from a * Native Ad created in Dart. @@ -106,4 +117,8 @@ + (id _Nullable) unregisterNativeAdFactory:(id _Nonnull)registry factoryId:(NSString *_Nonnull)factoryId; + ++ (nullable id) + unregisterCustomAdFactory:(nonnull id)registry + formatId:(nonnull NSString *)formatId; @end diff --git a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m index 522437359..ab5204564 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m +++ b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m @@ -22,6 +22,8 @@ @interface FLTGoogleMobileAdsPlugin () @property(nonatomic, retain) FlutterMethodChannel *channel; @property NSMutableDictionary> *nativeAdFactories; +@property NSMutableDictionary> + *customAdFactories; @end /// Initialization handler for GMASDK. Invokes result at most once. @@ -56,6 +58,7 @@ - (void)handleInitializationComplete:(GADInitializationStatus *_Nonnull)status { @implementation FLTGoogleMobileAdsPlugin { NSMutableDictionary> *_nativeAdFactories; + NSMutableDictionary> *_customAdFactories; FLTAdInstanceManager *_manager; id _mediationNetworkExtrasProvider; FLTGoogleMobileAdsReaderWriter *_readerWriter; @@ -99,6 +102,7 @@ - (instancetype)initWithBinaryMessenger: self = [self init]; if (self) { _nativeAdFactories = [NSMutableDictionary dictionary]; + _customAdFactories = [NSMutableDictionary dictionary]; _manager = [[FLTAdInstanceManager alloc] initWithBinaryMessenger:binaryMessenger]; _appStateNotifier = @@ -180,6 +184,33 @@ + (BOOL)registerNativeAdFactory:(id)registry return YES; } ++ (BOOL)registerCustomAdFactory:(id)registry + formatId:(NSString *)formatId + customAdFactory:(id)customAdFactory { + NSString *pluginClassName = + NSStringFromClass([FLTGoogleMobileAdsPlugin class]); + FLTGoogleMobileAdsPlugin *adMobPlugin = (FLTGoogleMobileAdsPlugin *)[registry + valuePublishedByPlugin:pluginClassName]; + if (!adMobPlugin) { + NSString *reason = + [NSString stringWithFormat:@"Could not find a %@ instance. The plugin " + @"may have not been registered.", + pluginClassName]; + @throw [NSException exceptionWithName:NSInvalidArgumentException + reason:reason + userInfo:nil]; + } + + if (adMobPlugin.customAdFactories[formatId]) { + NSLog(@"A CustomAdFactory with the following formatId already exists: %@", + formatId); + return NO; + } + + [adMobPlugin.customAdFactories setValue:customAdFactory forKey:formatId]; + return YES; +} + + (id)unregisterNativeAdFactory: (id)registry factoryId:(NSString *)factoryId { @@ -193,6 +224,19 @@ + (BOOL)registerNativeAdFactory:(id)registry return factory; } ++ (id)unregisterCustomAdFactory: + (id)registry + formatId:(NSString *)formatId { + FLTGoogleMobileAdsPlugin *adMobPlugin = (FLTGoogleMobileAdsPlugin *)[registry + valuePublishedByPlugin:NSStringFromClass( + [FLTGoogleMobileAdsPlugin class])]; + + id factory = adMobPlugin.customAdFactories[formatId]; + if (factory) + [adMobPlugin.customAdFactories removeObjectForKey:formatId]; + return factory; +} + - (UIViewController *)rootController { return UIApplication.sharedApplication.delegate.window.rootViewController; } @@ -377,6 +421,24 @@ - (void)handleMethodCall:(FlutterMethodCall *)call [_manager loadAd:ad]; result(nil); } else if ([call.method isEqualToString:@"loadAdLoaderAd"]) { + FLTCustomParameters *custom = call.arguments[@"custom"]; + if ([FLTAdUtil isNotNull:custom]) { + for (NSString *formatId in custom.formatIds) { + id factory = _customAdFactories[formatId]; + if (!factory) { + NSString *message = [NSString + stringWithFormat:@"Can't find CustomAdFactory with id: %@", + formatId]; + result([FlutterError errorWithCode:@"AdLoaderAdError" + message:message + details:nil]); + return; + } + + [custom.factories setValue:factory forKey:formatId]; + } + } + FLTAdRequest *request; if ([FLTAdUtil isNotNull:call.arguments[@"request"]]) { request = call.arguments[@"request"]; @@ -389,7 +451,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call request:request rootViewController:rootController adId:call.arguments[@"adId"] - banner:call.arguments[@"banner"]]; + banner:call.arguments[@"banner"] + custom:custom]; [_manager loadAd:ad]; result(nil); } else if ([call.method isEqualToString:@"loadInterstitialAd"]) { diff --git a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m index 0192078f6..5728fd355 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m +++ b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m @@ -48,6 +48,7 @@ typedef NS_ENUM(NSInteger, FLTAdMobField) { FLTAdmobFieldNativeTemplateColor = 153, FLTAdmobFieldAdManagerAdViewOptions = 154, FLTAdmobBannerParameters = 155, + FLTAdmobCustomParameters = 156, }; @interface FLTGoogleMobileAdsWriter : FlutterStandardWriter @@ -340,6 +341,11 @@ - (id _Nullable)readValueOfType:(UInt8)type { initWithSizes:[self readValueOfType:[self readByte]] options:[self readValueOfType:[self readByte]]]; } + case FLTAdmobCustomParameters: { + return [[FLTCustomParameters alloc] + initWithFormatIds:[self readValueOfType:[self readByte]] + viewOptions:[self readValueOfType:[self readByte]]]; + } } return [super readValueOfType:type]; } @@ -528,6 +534,11 @@ - (void)writeValue:(id)value { FLTBannerParameters *bannerParameters = value; [self writeValue:bannerParameters.sizes]; [self writeValue:bannerParameters.options]; + } else if ([value isKindOfClass:[FLTCustomParameters class]]) { + [self writeByte:FLTAdmobCustomParameters]; + FLTCustomParameters *customParameters = value; + [self writeValue:customParameters.formatIds]; + [self writeValue:customParameters.viewOptions]; } else { [super writeValue:value]; } diff --git a/packages/google_mobile_ads/ios/Tests/FLTAdLoaderAdTest.m b/packages/google_mobile_ads/ios/Tests/FLTAdLoaderAdTest.m index ce063c072..8fcd9c54a 100644 --- a/packages/google_mobile_ads/ios/Tests/FLTAdLoaderAdTest.m +++ b/packages/google_mobile_ads/ios/Tests/FLTAdLoaderAdTest.m @@ -30,7 +30,8 @@ - (void)testDelegates { request:[[FLTAdRequest alloc] init] rootViewController:viewController adId:@0 - banner:nil]; + banner:nil + custom:nil]; ad.manager = manager; @@ -58,7 +59,8 @@ - (void)testBannerDelegates { rootViewController:viewController adId:@0 banner:[[FLTBannerParameters alloc] initWithSizes:@[ adSize ] - options:nil]]; + options:nil] + custom:nil]; ad.manager = manager; @@ -105,6 +107,62 @@ - (void)testBannerDelegates { data:[OCMArg isEqual:@"info"]]); } +- (void)testCustomDelegates { + UIViewController *viewController = OCMClassMock([UIViewController class]); + FLTAdInstanceManager *manager = OCMClassMock([FLTAdInstanceManager class]); + + FLTCustomParameters *custom = + [[FLTCustomParameters alloc] initWithFormatIds:@[ @"12345678" ] + viewOptions:nil]; + id factory = + OCMProtocolMock(@protocol(FLTCustomAdFactory)); + [custom.factories setValue:factory forKey:@"12345678"]; + + FLTAdLoaderAd *ad = + [[FLTAdLoaderAd alloc] initWithAdUnitId:@"testAdUnitId" + request:[[FLTAdRequest alloc] init] + rootViewController:viewController + adId:@0 + banner:nil + custom:custom]; + + ad.manager = manager; + + [ad load]; + + // GADCustomNativeAdLoaderDelegate + NSArray *formatIds = + [ad customNativeAdFormatIDsForAdLoader:ad.adLoader]; + XCTAssertEqual(formatIds.count, 1); + XCTAssertEqualObjects(formatIds[0], @"12345678"); + + GADCustomNativeAd *customNativeAd = OCMClassMock([GADCustomNativeAd class]); + OCMStub([customNativeAd formatID]).andReturn(@"12345678"); + + [ad adLoader:ad.adLoader didReceiveCustomNativeAd:customNativeAd]; + OCMVerify([customNativeAd setDelegate:[OCMArg isEqual:ad]]); + OCMVerify([factory createCustomNativeAd:[OCMArg isEqual:customNativeAd] + customOptions:[OCMArg isEqual:nil]]); + OCMVerify([manager onAdLoaded:[OCMArg isEqual:ad] + responseInfo:[OCMArg isEqual:nil]]); + + // GADCustomNativeAdDelegate + [ad customNativeAdDidRecordImpression:customNativeAd]; + OCMVerify([manager onCustomNativeAdImpression:[OCMArg isEqual:ad]]); + + [ad customNativeAdDidRecordClick:customNativeAd]; + OCMVerify([manager adDidRecordClick:[OCMArg isEqual:ad]]); + + [ad customNativeAdWillPresentScreen:customNativeAd]; + OCMVerify([manager onCustomNativeAdWillPresentScreen:[OCMArg isEqual:ad]]); + + [ad customNativeAdWillDismissScreen:customNativeAd]; + OCMVerify([manager onCustomNativeAdWillDismissScreen:[OCMArg isEqual:ad]]); + + [ad customNativeAdDidDismissScreen:customNativeAd]; + OCMVerify([manager onCustomNativeAdDidDismissScreen:[OCMArg isEqual:ad]]); +} + - (void)testLoadAdLoaderAd { FLTAdRequest *request = [[FLTAdRequest alloc] init]; request.keywords = @[ @"apple" ]; @@ -124,7 +182,8 @@ - (void)testLoadAdLoaderAd:(FLTAdRequest *)request { request:request rootViewController:viewController adId:@1 - banner:nil]; + banner:nil + custom:nil]; XCTAssertEqual(ad.adLoader.adUnitID, @"testAdUnitId"); XCTAssertEqual(ad.adLoader.delegate, ad); diff --git a/packages/google_mobile_ads/ios/Tests/FLTGoogleMobileAdsReaderWriterTest.m b/packages/google_mobile_ads/ios/Tests/FLTGoogleMobileAdsReaderWriterTest.m index 5a655caba..71f394193 100644 --- a/packages/google_mobile_ads/ios/Tests/FLTGoogleMobileAdsReaderWriterTest.m +++ b/packages/google_mobile_ads/ios/Tests/FLTGoogleMobileAdsReaderWriterTest.m @@ -802,6 +802,33 @@ - (void)testEncodeDecodeBannerParameters { XCTAssertNil(options.manualImpressionsEnabled); } +- (void)testEncodeDecodeCustomParameters { + FLTCustomParameters *parameters = [[FLTCustomParameters alloc] + initWithFormatIds:@[ @"formatId0", @"formatId1" ] + viewOptions:@{@"key" : @"value"}]; + + NSData *encodedMessage = [_messageCodec encode:parameters]; + + FLTCustomParameters *decodedParameters = + [_messageCodec decode:encodedMessage]; + + NSArray *formatIds = decodedParameters.formatIds; + + XCTAssertEqual(formatIds.count, 2); + XCTAssertEqualObjects(formatIds[0], @"formatId0"); + XCTAssertEqualObjects(formatIds[1], @"formatId1"); + + NSDictionary *viewOptions = decodedParameters.viewOptions; + + XCTAssertNotNil(viewOptions); + XCTAssertEqualObjects(viewOptions[@"key"], @"value"); + + NSDictionary> *factories = + decodedParameters.factories; + + XCTAssertEqual(factories.count, 0); +} + @end @implementation FLTTestAdSizeFactory diff --git a/packages/google_mobile_ads/lib/src/ad_containers.dart b/packages/google_mobile_ads/lib/src/ad_containers.dart index c06864bf6..65a30eb85 100644 --- a/packages/google_mobile_ads/lib/src/ad_containers.dart +++ b/packages/google_mobile_ads/lib/src/ad_containers.dart @@ -1109,6 +1109,7 @@ class AdLoaderAd extends AdWithView { required this.listener, required AdRequest request, this.banner, + this.custom, }) : super(adUnitId: adUnitId, listener: listener) { if (request is AdManagerAdRequest) { adManagerRequest = request; @@ -1130,6 +1131,9 @@ class AdLoaderAd extends AdWithView { /// Optional parameters used to configure served "banner" ads final BannerParameters? banner; + /// Optional parameters used to configure served "custom" ads + final CustomParameters? custom; + @override Future load() async { await instanceManager.loadAdLoaderAd(this); @@ -1609,6 +1613,33 @@ class BannerParameters { } } +/// Central configuration item for custom format requests served +/// by an [AdLoaderAd] +class CustomParameters { + /// A list of format IDs, corresponding to those in the + /// Google Ad Manager console + final List formatIds; + + /// View options used to create the Platform view + /// + /// These options are passed to the platform's `CustomAdFactory` + Map? viewOptions; + + /// Construct a [CustomParameters] instance, used by an [AdLoaderAd] to + /// configure custom view + CustomParameters({ + required this.formatIds, + this.viewOptions, + }); + + @override + bool operator ==(other) { + return other is CustomParameters && + listEquals(formatIds, other.formatIds) && + mapEquals(viewOptions, other.viewOptions); + } +} + /// Used to configure native ad requests. class NativeAdOptions { /// Where to place the AdChoices icon. diff --git a/packages/google_mobile_ads/lib/src/ad_instance_manager.dart b/packages/google_mobile_ads/lib/src/ad_instance_manager.dart index 086a34a0f..f44b822ee 100644 --- a/packages/google_mobile_ads/lib/src/ad_instance_manager.dart +++ b/packages/google_mobile_ads/lib/src/ad_instance_manager.dart @@ -91,14 +91,17 @@ class AdInstanceManager { break; case 'onNativeAdWillPresentScreen': // Fall through case 'onBannerWillPresentScreen': + case 'onCustomNativeWillPresentScreen': _invokeOnAdOpened(ad, eventName); break; case 'onNativeAdDidDismissScreen': // Fall through case 'onBannerDidDismissScreen': + case 'onCustomNativeAdDidDismissScreen': _invokeOnAdClosed(ad, eventName); break; case 'onBannerWillDismissScreen': // Fall through case 'onNativeAdWillDismissScreen': + case 'onCustomNativeAdWillDismissScreen': if (ad is AdWithView) { ad.listener.onAdWillDismissScreen?.call(ad); } else { @@ -112,6 +115,7 @@ class AdInstanceManager { case 'onBannerImpression': case 'adDidRecordImpression': // Fall through case 'onNativeAdImpression': // Fall through + case 'onCustomNativeAdImpression': _invokeOnAdImpression(ad, eventName); break; case 'adWillPresentFullScreenContent': @@ -541,6 +545,7 @@ class AdInstanceManager { 'request': ad.request, 'adManagerRequest': ad.adManagerRequest, 'banner': ad.banner, + 'custom': ad.custom, }, ); } @@ -855,6 +860,7 @@ class AdMessageCodec extends StandardMessageCodec { static const int _valueColor = 153; static const int _valueAdManagerAdViewOptions = 154; static const int _valueBannerParameters = 155; + static const int _valueCustomParameters = 156; @override void writeValue(WriteBuffer buffer, dynamic value) { @@ -988,6 +994,10 @@ class AdMessageCodec extends StandardMessageCodec { buffer.putUint8(_valueBannerParameters); writeValue(buffer, value.sizes); writeValue(buffer, value.adManagerAdViewOptions); + } else if (value is CustomParameters) { + buffer.putUint8(_valueCustomParameters); + writeValue(buffer, value.formatIds); + writeValue(buffer, value.viewOptions); } else { super.writeValue(buffer, value); } @@ -1216,6 +1226,12 @@ class AdMessageCodec extends StandardMessageCodec { sizes: readValueOfType(buffer.getUint8(), buffer)?.cast(), adManagerAdViewOptions: readValueOfType(buffer.getUint8(), buffer), ); + case _valueCustomParameters: + return CustomParameters( + formatIds: readValueOfType(buffer.getUint8(), buffer).cast(), + viewOptions: readValueOfType(buffer.getUint8(), buffer) + ?.cast(), + ); default: return super.readValueOfType(type, buffer); } diff --git a/packages/google_mobile_ads/test/ad_loader_ad_test.dart b/packages/google_mobile_ads/test/ad_loader_ad_test.dart index f403c0de8..cad895fb2 100644 --- a/packages/google_mobile_ads/test/ad_loader_ad_test.dart +++ b/packages/google_mobile_ads/test/ad_loader_ad_test.dart @@ -61,6 +61,7 @@ void main() { 'request': request, 'adManagerRequest': null, 'banner': null, + 'custom': null, }) ]); @@ -84,6 +85,7 @@ void main() { 'request': null, 'adManagerRequest': request, 'banner': null, + 'custom': null, }) ]); @@ -109,6 +111,33 @@ void main() { 'request': adLoaderAd.request, 'adManagerRequest': null, 'banner': banner, + 'custom': null, + }) + ]); + + expect(instanceManager.adFor(0), isNotNull); + }); + + test('load with $CustomParameters', () async { + final CustomParameters custom = CustomParameters( + formatIds: ['test-format-id'], + ); + final AdLoaderAd adLoaderAd = AdLoaderAd( + adUnitId: 'test-ad-unit', + listener: AdLoaderAdListener(), + request: AdRequest(), + custom: custom, + ); + + await adLoaderAd.load(); + expect(log, [ + isMethodCall('loadAdLoaderAd', arguments: { + 'adId': 0, + 'adUnitId': 'test-ad-unit', + 'request': adLoaderAd.request, + 'adManagerRequest': null, + 'banner': null, + 'custom': custom, }) ]); diff --git a/packages/google_mobile_ads/test/mobile_ads_test.dart b/packages/google_mobile_ads/test/mobile_ads_test.dart index 75123bee3..279fbb695 100644 --- a/packages/google_mobile_ads/test/mobile_ads_test.dart +++ b/packages/google_mobile_ads/test/mobile_ads_test.dart @@ -577,5 +577,35 @@ void main() { expect(result.adManagerAdViewOptions?.manualImpressionsEnabled, true); } }); + + test('encode/decode minimal $CustomParameters', () { + for (final platform in [TargetPlatform.android, TargetPlatform.iOS]) { + debugDefaultTargetPlatformOverride = platform; + ByteData byteData = codec.encodeMessage(CustomParameters( + formatIds: ['test-format-id'], + ))!; + + CustomParameters result = codec.decodeMessage(byteData); + expect(result.formatIds, ['test-format-id']); + expect(result.viewOptions, null); + } + }); + + test('encode/decode $CustomParameters', () { + for (final platform in [TargetPlatform.android, TargetPlatform.iOS]) { + debugDefaultTargetPlatformOverride = platform; + ByteData byteData = codec.encodeMessage(CustomParameters(formatIds: [ + 'test-format-id' + ], viewOptions: { + 'key': 'value', + }))!; + + CustomParameters result = codec.decodeMessage(byteData); + expect(result.formatIds, ['test-format-id']); + expect(result.viewOptions, { + 'key': 'value', + }); + } + }); }); }