From 3d4656a6c059cce1a299af3f8dfc9c143dcc8d44 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 25 Oct 2022 16:50:48 +1100 Subject: [PATCH 1/6] Allow serving `Banner` ads in `AdLoaderAd` Allow the `AdLoaderAd` instance to serve `Banner` ads, which are instances of: * `AdManagerAdView`/`BannerAd` under Android, and * `GAMBannerView` under iOS --- .../googlemobileads/FlutterAdListener.java | 7 + .../googlemobileads/FlutterAdLoader.java | 25 +- .../googlemobileads/FlutterAdLoaderAd.java | 74 +++++- .../FlutterAdManagerAdViewOptions.java | 12 + .../FlutterBannerParameters.java | 29 +++ .../googlemobileads/AdLoaderAdType.java | 1 + .../googlemobileads/AdMessageCodec.java | 17 ++ .../GoogleMobileAdsPlugin.java | 1 + .../googlemobileads/FlutterAdListener.java | 19 ++ .../googlemobileads/FlutterAdLoader.java | 30 ++- .../googlemobileads/FlutterAdLoaderAd.java | 78 +++++- .../FlutterAdManagerAdViewOptions.java | 37 +++ .../FlutterBannerParameters.java | 46 ++++ .../googlemobileads/AdMessageCodecTest.java | 44 ++++ .../FlutterAdLoaderAdTest.java | 82 ++++++- .../FlutterAdManagerAdViewOptionsTest.java | 59 +++++ .../FlutterBannerParametersTest.java | 49 ++++ .../ios/Runner.xcodeproj/project.pbxproj | 4 + .../ios/RunnerTests/FLTAdLoaderAdTest.m | 74 +++++- .../FLTAdManagerAdViewOptionsTest.m | 49 ++++ .../FLTGoogleMobileAdsReaderWriterTest.m | 56 +++++ .../google_mobile_ads/FLTAd_Internal.m | 142 ++++++++++- .../FLTGoogleMobileAdsPlugin.m | 3 +- .../FLTGoogleMobileAdsReaderWriter_Internal.m | 24 +- .../google_mobile_ads/FLTAd_Internal.h | 33 ++- .../lib/src/ad_containers.dart | 46 ++++ .../lib/src/ad_instance_manager.dart | 23 ++ .../test/ad_loader_ad_test.dart | 227 ++++++++++++++++++ .../test/mobile_ads_test.dart | 54 +++++ 29 files changed, 1302 insertions(+), 43 deletions(-) create mode 100644 packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdViewOptions.java create mode 100644 packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterBannerParameters.java create mode 100644 packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdViewOptions.java create mode 100644 packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterBannerParameters.java create mode 100644 packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdViewOptionsTest.java create mode 100644 packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterBannerParametersTest.java create mode 100644 packages/google_mobile_ads/example/ios/RunnerTests/FLTAdManagerAdViewOptionsTest.m diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java index b7ea8ff13..ac81ae1b3 100644 --- a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java @@ -165,4 +165,11 @@ public void onAdFailedToLoad(@NonNull LoadAdError loadAdError) { callbackWeakReference.get().onAdFailedToLoad(loadAdError); } } + + @Override + public void onBannerAdLoaded(@NonNull BannerAd bannerAd) { + if (callbackWeakReference.get() != null) { + callbackWeakReference.get().onBannerAdLoaded(bannerAd); + } + } } diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java index c582a3070..35a9f122f 100644 --- a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java @@ -17,6 +17,7 @@ import android.content.Context; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.gms.ads.nativead.NativeAdOptions; import com.google.android.libraries.ads.mobile.sdk.appopen.AppOpenAd; import com.google.android.libraries.ads.mobile.sdk.common.AdLoadCallback; @@ -124,19 +125,33 @@ public void loadAdManagerNativeAd( /** Load an ad loader ad. */ public void loadAdLoaderAd( @NonNull String adUnitId, - @NonNull NativeAdLoaderCallback adLoaderAdLoaderCallback) { + @NonNull NativeAdLoaderCallback adLoaderAdLoaderCallback, + @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters) { List nativeAdTypes = new ArrayList<>(); + if (bannerParameters != null) { + nativeAdTypes.add(NativeAdType.BANNER); + } + + NativeAdRequest.Builder builder = new NativeAdRequest.Builder(adUnitId, nativeAdTypes); + + if (bannerParameters != null) { + builder.setAdSizes(bannerParameters.adSizes); + if (bannerParameters.adManagerAdViewOptions != null) { + builder.setManualImpressionEnabled(bannerParameters.adManagerAdViewOptions.manualImpressionsEnabled); + } + } + NativeAdLoader.load( - new NativeAdRequest.Builder(adUnitId, nativeAdTypes) - .build(), + builder.build(), adLoaderAdLoaderCallback); } /** Load an ad manager ad loader ad. */ public void loadAdManagerAdLoaderAd( @NonNull String adUnitId, - @NonNull NativeAdLoaderCallback adLoaderAdLoaderCallback) { - loadAdLoaderAd(adUnitId, adLoaderAdLoaderCallback); + @NonNull NativeAdLoaderCallback adLoaderAdLoaderCallback, + @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters) { + loadAdLoaderAd(adUnitId, adLoaderAdLoaderCallback, bannerParameters); } } diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java index 8f9bce672..970a33090 100644 --- a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java @@ -1,10 +1,16 @@ package io.flutter.plugins.googlemobileads; import android.util.Log; +import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.android.libraries.ads.mobile.sdk.banner.AdSize; +import com.google.android.libraries.ads.mobile.sdk.banner.AdView; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAd; import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdLoaderCallback; +import io.flutter.plugin.platform.PlatformView; +import java.util.List; class FlutterAdLoaderAd extends FlutterAd implements FlutterAdLoadedListener, NativeAdLoaderCallback { private static final String TAG = "FlutterAdLoaderAd"; @@ -16,6 +22,8 @@ class FlutterAdLoaderAd extends FlutterAd implements FlutterAdLoadedListener, Na @Nullable private FlutterAdManagerAdRequest adManagerRequest; @Nullable private AdLoaderAdType type; @Nullable private String formatId; + @Nullable private View view; + @Nullable protected BannerParameters bannerParameters; static class Builder { @Nullable private AdInstanceManager manager; @@ -24,6 +32,7 @@ static class Builder { @Nullable private FlutterAdManagerAdRequest adManagerRequest; @Nullable private Integer id; @Nullable private FlutterAdLoader adLoader; + @Nullable private FlutterBannerParameters bannerParameters; public Builder setId(int id) { this.id = id; @@ -50,6 +59,11 @@ public Builder setAdManagerRequest(@NonNull FlutterAdManagerAdRequest adManagerR return this; } + public Builder setBanner(@Nullable FlutterBannerParameters bannerParameters) { + this.bannerParameters = bannerParameters; + return this; + } + public Builder setFlutterAdLoader(@NonNull FlutterAdLoader adLoader) { this.adLoader = adLoader; return this; @@ -75,10 +89,27 @@ FlutterAdLoaderAd build() { } else { adLoaderAd = new FlutterAdLoaderAd(id, manager, adUnitId, request, adLoader); } + + if (bannerParameters != null) { + adLoaderAd.bannerParameters = bannerParameters.asBannerParameters(); + } + return adLoaderAd; } } + static class BannerParameters { + @NonNull final List adSizes; + @Nullable final FlutterAdManagerAdViewOptions adManagerAdViewOptions; + + BannerParameters( + @NonNull List adSizes, + @Nullable FlutterAdManagerAdViewOptions adManagerAdViewOptions) { + this.adSizes = adSizes; + this.adManagerAdViewOptions = adManagerAdViewOptions; + } + } + protected FlutterAdLoaderAd( int adId, @NonNull AdInstanceManager manager, @@ -119,9 +150,9 @@ void load() { // Note we delegate loading the ad to FlutterAdLoader mainly for testing purposes. // As of 20.0.0 of GMA, mockito is unable to mock AdLoader. if (request != null) { - adLoader.loadAdLoaderAd(adUnitId, loadedListener); + adLoader.loadAdLoaderAd(adUnitId, loadedListener, bannerParameters); } else if (adManagerRequest != null) { - adLoader.loadAdManagerAdLoaderAd(adUnitId, loadedListener); + adLoader.loadAdManagerAdLoaderAd(adUnitId, loadedListener, bannerParameters); } else { Log.e(TAG, "A null or invalid ad request was provided."); } @@ -143,7 +174,19 @@ String getFormatId() { } @Override - public void onAdLoaded() {} + @Nullable + public PlatformView getPlatformView() { + if (view == null) { + return null; + } + + return new FlutterPlatformView(view); + } + + @Override + public void onAdLoaded() { + // handled by onBannerAdLoaded + } @Override public void onAdFailedToLoad(@NonNull LoadAdError loadAdError) { @@ -151,5 +194,28 @@ public void onAdFailedToLoad(@NonNull LoadAdError loadAdError) { } @Override - void dispose() {} + public void onBannerAdLoaded(@NonNull BannerAd bannerAd) { + AdView adView = new AdView(manager.getActivity()); + adView.registerBannerAd(bannerAd, manager.getActivity()); + + view = adView; + type = AdLoaderAdType.BANNER; + + bannerAd.setAdEventCallback(new FlutterBannerAdListener(adId, manager, this)); + + manager.onAdLoaded(adId, bannerAd.getResponseInfo()); + } + + @Override + void dispose() { + if (view == null) { + return; + } + + if (view instanceof AdView) { + ((AdView) view).destroy(); + } + + view = null; + } } diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdViewOptions.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdViewOptions.java new file mode 100644 index 000000000..dc6f39064 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdViewOptions.java @@ -0,0 +1,12 @@ +package io.flutter.plugins.googlemobileads; + +import androidx.annotation.Nullable; + +class FlutterAdManagerAdViewOptions { + + @Nullable final Boolean manualImpressionsEnabled; + + FlutterAdManagerAdViewOptions(@Nullable Boolean manualImpressionsEnabled) { + this.manualImpressionsEnabled = manualImpressionsEnabled; + } +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterBannerParameters.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterBannerParameters.java new file mode 100644 index 000000000..77e19c4e6 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterBannerParameters.java @@ -0,0 +1,29 @@ +package io.flutter.plugins.googlemobileads; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.libraries.ads.mobile.sdk.banner.AdSize; +import java.util.ArrayList; +import java.util.List; + +class FlutterBannerParameters { + @NonNull final List sizes; + @Nullable final FlutterAdManagerAdViewOptions adManagerAdViewOptions; + + FlutterBannerParameters( + @NonNull List sizes, + @Nullable FlutterAdManagerAdViewOptions adManagerAdViewOptions) { + this.sizes = sizes; + this.adManagerAdViewOptions = adManagerAdViewOptions; + } + + FlutterAdLoaderAd.BannerParameters asBannerParameters() { + List adSizes = new ArrayList<>(sizes.size()); + for (FlutterAdSize size : sizes) { + adSizes.add(size.getAdSize()); + } + return new FlutterAdLoaderAd.BannerParameters( + adSizes, + adManagerAdViewOptions); + } +} diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdLoaderAdType.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdLoaderAdType.java index f972e319f..23d6838dd 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdLoaderAdType.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdLoaderAdType.java @@ -2,4 +2,5 @@ enum AdLoaderAdType { UNKNOWN, + BANNER, } 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 77c5382a9..491a4fec7 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,8 @@ class AdMessageCodec extends StandardMessageCodec { private static final byte VALUE_NATIVE_TEMPLATE_TYPE = (byte) 152; private static final byte VALUE_COLOR = (byte) 153; private static final byte VALUE_MEDIATION_EXTRAS = (byte) 154; + private static final byte VALUE_AD_MANAGER_AD_VIEW_OPTIONS = (byte) 155; + private static final byte VALUE_BANNER_PARAMETERS = (byte) 156; @NonNull Context context; @NonNull final FlutterAdSize.AdSizeFactory adSizeFactory; @@ -257,6 +259,15 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { writeValue(stream, Color.red(colorValue)); writeValue(stream, Color.green(colorValue)); writeValue(stream, Color.blue(colorValue)); + } else if (value instanceof FlutterAdManagerAdViewOptions) { + stream.write(VALUE_AD_MANAGER_AD_VIEW_OPTIONS); + FlutterAdManagerAdViewOptions options = (FlutterAdManagerAdViewOptions) value; + writeValue(stream, options.manualImpressionsEnabled); + } else if (value instanceof FlutterBannerParameters) { + stream.write(VALUE_BANNER_PARAMETERS); + FlutterBannerParameters bannerParameters = (FlutterBannerParameters) value; + writeValue(stream, bannerParameters.sizes); + writeValue(stream, bannerParameters.adManagerAdViewOptions); } else { super.writeValue(stream, value); } @@ -443,6 +454,12 @@ protected Object readValueOfType(byte type, ByteBuffer buffer) { final Integer green = (Integer) readValueOfType(buffer.get(), buffer); final Integer blue = (Integer) readValueOfType(buffer.get(), buffer); return new ColorDrawable(Color.argb(alpha, red, green, blue)); + case VALUE_AD_MANAGER_AD_VIEW_OPTIONS: + return new FlutterAdManagerAdViewOptions((Boolean) readValueOfType(buffer.get(), buffer)); + case VALUE_BANNER_PARAMETERS: + return new FlutterBannerParameters( + (List) readValueOfType(buffer.get(), buffer), + (FlutterAdManagerAdViewOptions) 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/GoogleMobileAdsPlugin.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java index a1b3645a5..9189276c6 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 @@ -401,6 +401,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) adLoaderSupplier != null ? adLoaderSupplier.get() : new FlutterAdLoader(context)) + .setBanner(call.argument("banner")) .build(); instanceManager.trackAd(adLoaderAd, call.argument("adId")); adLoaderAd.load(); diff --git a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java index 065f74a98..cd0efbefe 100644 --- a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java @@ -16,6 +16,8 @@ import androidx.annotation.NonNull; import com.google.android.gms.ads.AdListener; import com.google.android.gms.ads.LoadAdError; +import com.google.android.gms.ads.admanager.AdManagerAdView; +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 java.lang.ref.WeakReference; @@ -118,3 +120,20 @@ public void onNativeAdLoaded(@NonNull NativeAd nativeAd) { } } } + +/** {@link OnAdManagerAdViewLoadedListener} for banner ads. */ +class FlutterAdManagerAdViewLoadedListener implements OnAdManagerAdViewLoadedListener { + + private final WeakReference reference; + + FlutterAdManagerAdViewLoadedListener(OnAdManagerAdViewLoadedListener listener) { + reference = new WeakReference<>(listener); + } + + @Override + public void onAdManagerAdViewLoaded(AdManagerAdView adView) { + if (reference.get() != null) { + reference.get().onAdManagerAdViewLoaded(adView); + } + } +} diff --git a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java index d95a6a6b2..22241841c 100644 --- a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java @@ -16,6 +16,7 @@ import android.content.Context; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.gms.ads.AdListener; import com.google.android.gms.ads.AdLoader; import com.google.android.gms.ads.AdRequest; @@ -140,18 +141,33 @@ public void loadAdManagerNativeAd( /** Load an ad loader ad. */ public void loadAdLoaderAd( - @NonNull String adUnitId, @NonNull AdListener adListener, @NonNull AdRequest request) { - new AdLoader.Builder(context, adUnitId).withAdListener(adListener).build().loadAd(request); + @NonNull String adUnitId, + @NonNull AdListener adListener, + @NonNull AdRequest request, + @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters) { + AdLoader.Builder builder = new AdLoader.Builder(context, adUnitId); + if (bannerParameters != null) { + builder = builder.forAdManagerAdView(bannerParameters.listener, bannerParameters.adSizes); + if (bannerParameters.adManagerAdViewOptions != null) { + builder.withAdManagerAdViewOptions(bannerParameters.adManagerAdViewOptions); + } + } + builder.withAdListener(adListener).build().loadAd(request); } /** Load an ad manager ad loader ad. */ public void loadAdManagerAdLoaderAd( @NonNull String adUnitId, @NonNull AdListener adListener, - @NonNull AdManagerAdRequest adManagerAdRequest) { - new AdLoader.Builder(context, adUnitId) - .withAdListener(adListener) - .build() - .loadAd(adManagerAdRequest); + @NonNull AdManagerAdRequest adManagerAdRequest, + @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters) { + AdLoader.Builder builder = new AdLoader.Builder(context, adUnitId); + if (bannerParameters != null) { + builder = builder.forAdManagerAdView(bannerParameters.listener, bannerParameters.adSizes); + if (bannerParameters.adManagerAdViewOptions != null) { + builder.withAdManagerAdViewOptions(bannerParameters.adManagerAdViewOptions); + } + } + builder.withAdListener(adListener).build().loadAd(adManagerAdRequest); } } diff --git a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java index a329ee2c6..2a5629478 100644 --- a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java @@ -15,15 +15,22 @@ package io.flutter.plugins.googlemobileads; import android.util.Log; +import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.gms.ads.AdListener; +import com.google.android.gms.ads.AdSize; +import com.google.android.gms.ads.BaseAdView; +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 io.flutter.plugin.platform.PlatformView; /** * 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 { +class FlutterAdLoaderAd extends FlutterAd implements OnAdManagerAdViewLoadedListener { private static final String TAG = "FlutterAdLoaderAd"; @NonNull private final AdInstanceManager manager; @@ -33,6 +40,8 @@ class FlutterAdLoaderAd extends FlutterAd { @Nullable private FlutterAdManagerAdRequest adManagerRequest; @Nullable private AdLoaderAdType type; @Nullable private String formatId; + @Nullable private View view; + @Nullable protected BannerParameters bannerParameters; static class Builder { @Nullable private AdInstanceManager manager; @@ -41,6 +50,7 @@ static class Builder { @Nullable private FlutterAdManagerAdRequest adManagerRequest; @Nullable private Integer id; @Nullable private FlutterAdLoader adLoader; + @Nullable private FlutterBannerParameters bannerParameters; public Builder setId(int id) { this.id = id; @@ -72,6 +82,11 @@ public Builder setFlutterAdLoader(@NonNull FlutterAdLoader adLoader) { return this; } + public Builder setBanner(@Nullable FlutterBannerParameters bannerParameters) { + this.bannerParameters = bannerParameters; + return this; + } + FlutterAdLoaderAd build() { if (manager == null) { throw new IllegalStateException("manager must be provided"); @@ -92,10 +107,32 @@ FlutterAdLoaderAd build() { } else { adLoaderAd = new FlutterAdLoaderAd(id, manager, adUnitId, request, adLoader); } + + if (bannerParameters != null) { + adLoaderAd.bannerParameters = + bannerParameters.asBannerParameters( + new FlutterAdManagerAdViewLoadedListener(adLoaderAd)); + } + return adLoaderAd; } } + static class BannerParameters { + @NonNull final OnAdManagerAdViewLoadedListener listener; + @NonNull final AdSize[] adSizes; + @Nullable final AdManagerAdViewOptions adManagerAdViewOptions; + + BannerParameters( + @NonNull OnAdManagerAdViewLoadedListener listener, + @NonNull AdSize[] adSizes, + @Nullable AdManagerAdViewOptions adManagerAdViewOptions) { + this.listener = listener; + this.adSizes = adSizes; + this.adManagerAdViewOptions = adManagerAdViewOptions; + } + } + protected FlutterAdLoaderAd( int adId, @NonNull AdInstanceManager manager, @@ -136,13 +173,14 @@ void load() { // Note we delegate loading the ad to FlutterAdLoader mainly for testing purposes. // As of 20.0.0 of GMA, mockito is unable to mock AdLoader. if (request != null) { - adLoader.loadAdLoaderAd(adUnitId, adListener, request.asAdRequest(adUnitId)); + adLoader.loadAdLoaderAd( + adUnitId, adListener, request.asAdRequest(adUnitId), bannerParameters); return; } if (adManagerRequest != null) { adLoader.loadAdManagerAdLoaderAd( - adUnitId, adListener, adManagerRequest.asAdManagerAdRequest(adUnitId)); + adUnitId, adListener, adManagerRequest.asAdManagerAdRequest(adUnitId), bannerParameters); return; } @@ -156,7 +194,10 @@ AdLoaderAdType getAdLoaderAdType() { @Nullable FlutterAdSize getAdSize() { - return null; + if (view == null || !(view instanceof AdManagerAdView)) { + return null; + } + return new FlutterAdSize(((AdManagerAdView) view).getAdSize()); } @Nullable @@ -165,5 +206,32 @@ String getFormatId() { } @Override - void dispose() {} + @Nullable + public PlatformView getPlatformView() { + if (view == null) { + return null; + } + + return new FlutterPlatformView(view); + } + + @Override + public void onAdManagerAdViewLoaded(@NonNull AdManagerAdView adView) { + view = adView; + type = AdLoaderAdType.BANNER; + manager.onAdLoaded(adId, adView.getResponseInfo()); + } + + @Override + void dispose() { + if (view == null) { + return; + } + + if (view instanceof BaseAdView) { + ((BaseAdView) view).destroy(); + } + + view = null; + } } diff --git a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdViewOptions.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdViewOptions.java new file mode 100644 index 000000000..03cf55985 --- /dev/null +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdViewOptions.java @@ -0,0 +1,37 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.c language governing permissions and +// limitations under the License. + +package io.flutter.plugins.googlemobileads; + +import androidx.annotation.Nullable; +import com.google.android.gms.ads.formats.AdManagerAdViewOptions; + +/** A wrapper for {@link com.google.android.gms.ads.formats.AdManagerAdViewOptions}. */ +class FlutterAdManagerAdViewOptions { + + @Nullable final Boolean manualImpressionsEnabled; + + FlutterAdManagerAdViewOptions(@Nullable Boolean manualImpressionsEnabled) { + this.manualImpressionsEnabled = manualImpressionsEnabled; + } + + AdManagerAdViewOptions asAdManagerAdViewOptions() { + AdManagerAdViewOptions.Builder builder = new AdManagerAdViewOptions.Builder(); + if (manualImpressionsEnabled != null) { + builder.setManualImpressionsEnabled(manualImpressionsEnabled); + } + return builder.build(); + } +} diff --git a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterBannerParameters.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterBannerParameters.java new file mode 100644 index 000000000..a81562ed4 --- /dev/null +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterBannerParameters.java @@ -0,0 +1,46 @@ +// Copyright 20222 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io.flutter.plugins.googlemobileads; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.ads.AdSize; +import com.google.android.gms.ads.formats.OnAdManagerAdViewLoadedListener; +import java.util.List; + +class FlutterBannerParameters { + @NonNull final List sizes; + @Nullable final FlutterAdManagerAdViewOptions adManagerAdViewOptions; + + FlutterBannerParameters( + @NonNull List sizes, + @Nullable FlutterAdManagerAdViewOptions adManagerAdViewOptions) { + this.sizes = sizes; + this.adManagerAdViewOptions = adManagerAdViewOptions; + } + + FlutterAdLoaderAd.BannerParameters asBannerParameters( + @NonNull OnAdManagerAdViewLoadedListener listener) { + AdSize[] adSizes = new AdSize[sizes.size()]; + int i = 0; + for (FlutterAdSize size : sizes) { + adSizes[i++] = size.getAdSize(); + } + return new FlutterAdLoaderAd.BannerParameters( + listener, + adSizes, + adManagerAdViewOptions != null ? adManagerAdViewOptions.asAdManagerAdViewOptions() : null); + } +} 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 f1b80ac62..e4a91dc45 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 @@ -537,6 +537,50 @@ public void encodeRequestConfiguration() { RequestConfiguration.TAG_FOR_UNDER_AGE_OF_CONSENT_FALSE); assertEquals(result.getTestDeviceIds(), Arrays.asList("test-device-id")); } + + @Test + public void encodeAdManagerAdViewOptionsNull() { + final ByteBuffer data = codec.encodeMessage(new FlutterAdManagerAdViewOptions(null)); + + final FlutterAdManagerAdViewOptions result = + (FlutterAdManagerAdViewOptions) codec.decodeMessage((ByteBuffer) data.position(0)); + assertNull(result.manualImpressionsEnabled); + } + + @Test + public void encodeAdManagerAdViewOptionsTrue() { + final ByteBuffer data = codec.encodeMessage(new FlutterAdManagerAdViewOptions(true)); + + final FlutterAdManagerAdViewOptions result = + (FlutterAdManagerAdViewOptions) codec.decodeMessage((ByteBuffer) data.position(0)); + assertTrue(result.manualImpressionsEnabled); + } + + @Test + public void encodeAdManagerAdViewOptionsFalse() { + final ByteBuffer data = codec.encodeMessage(new FlutterAdManagerAdViewOptions(false)); + + final FlutterAdManagerAdViewOptions result = + (FlutterAdManagerAdViewOptions) codec.decodeMessage((ByteBuffer) data.position(0)); + assertFalse(result.manualImpressionsEnabled); + } + + @Test + public void encodeBannerParameters() { + final ByteBuffer data = + codec.encodeMessage( + new FlutterBannerParameters( + Collections.singletonList(new FlutterAdSize(1, 2)), + new FlutterAdManagerAdViewOptions(null))); + + final FlutterBannerParameters result = + (FlutterBannerParameters) codec.decodeMessage((ByteBuffer) data.position(0)); + + assertEquals(result.sizes.size(), 1); + assertEquals(result.sizes.get(0).width, 1); + assertEquals(result.sizes.get(0).height, 2); + assertNull(result.adManagerAdViewOptions.manualImpressionsEnabled); + } } class DummyMediationExtras extends FlutterMediationExtras { 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 eb2f61537..ec828faed 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 @@ -14,20 +14,26 @@ package io.flutter.plugins.googlemobileads; +import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Activity; import com.google.android.gms.ads.AdListener; import com.google.android.gms.ads.AdRequest; +import com.google.android.gms.ads.AdSize; import com.google.android.gms.ads.LoadAdError; +import com.google.android.gms.ads.ResponseInfo; import com.google.android.gms.ads.admanager.AdManagerAdRequest; +import com.google.android.gms.ads.admanager.AdManagerAdView; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.googlemobileads.FlutterAd.FlutterLoadAdError; import org.junit.Before; @@ -78,12 +84,12 @@ public Object answer(InvocationOnMock invocation) { } }) .when(mockLoader) - .loadAdManagerAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest)); + .loadAdManagerAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest), isNull()); adLoaderAd.load(); verify(mockLoader) - .loadAdManagerAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest)); + .loadAdManagerAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest), isNull()); verify(testManager).onAdClicked(eq(1)); verify(testManager).onAdClosed(eq(1)); @@ -121,11 +127,78 @@ public Object answer(InvocationOnMock invocation) { } }) .when(mockLoader) - .loadAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest)); + .loadAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest), isNull()); adLoaderAd.load(); - verify(mockLoader).loadAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest)); + verify(mockLoader) + .loadAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest), isNull()); + + 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)); + } + + @Test + public void loadAdLoaderAdBannerWithAdManagerAdRequest() { + 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 FlutterAdManagerAdViewLoadedListener listener = + new FlutterAdManagerAdViewLoadedListener(adLoaderAd); + final FlutterAdLoaderAd.BannerParameters bannerParameters = + new FlutterAdLoaderAd.BannerParameters(listener, new AdSize[] {AdSize.BANNER}, null); + adLoaderAd.bannerParameters = bannerParameters; + + final LoadAdError mockLoadAdError = mock(LoadAdError.class); + when(mockLoadAdError.getCode()).thenReturn(1); + when(mockLoadAdError.getDomain()).thenReturn("2"); + when(mockLoadAdError.getMessage()).thenReturn("3"); + + final AdManagerAdView mockAdView = mock(AdManagerAdView.class); + when(mockAdView.getAdSize()).thenReturn(new AdSize(0, 0)); + final ResponseInfo mockResponseInfo = mock(ResponseInfo.class); + when(mockAdView.getResponseInfo()).thenReturn(mockResponseInfo); + + 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.BannerParameters bannerParameters = invocation.getArgument(3); + bannerParameters.listener.onAdManagerAdViewLoaded(mockAdView); + return null; + } + }) + .when(mockLoader) + .loadAdManagerAdLoaderAd( + eq("testId"), any(AdListener.class), eq(mockRequest), eq(bannerParameters)); + + adLoaderAd.load(); + + assertEquals(adLoaderAd.getAdLoaderAdType(), AdLoaderAdType.BANNER); + + final FlutterAdSize adSize = adLoaderAd.getAdSize(); + assertEquals(adSize.width, 0); + assertEquals(adSize.height, 0); + + verify(mockAdView, times(1)).getAdSize(); + + verify(mockLoader) + .loadAdManagerAdLoaderAd( + eq("testId"), any(AdListener.class), eq(mockRequest), eq(bannerParameters)); verify(testManager).onAdClicked(eq(1)); verify(testManager).onAdClosed(eq(1)); @@ -133,6 +206,7 @@ public Object answer(InvocationOnMock invocation) { verify(testManager).onAdFailedToLoad(eq(1), eq(expectedError)); verify(testManager).onAdImpression(eq(1)); verify(testManager).onAdOpened(eq(1)); + verify(testManager).onAdLoaded(eq(1), eq(mockResponseInfo)); } @Test(expected = IllegalStateException.class) diff --git a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdViewOptionsTest.java b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdViewOptionsTest.java new file mode 100644 index 000000000..f5b911c6a --- /dev/null +++ b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdViewOptionsTest.java @@ -0,0 +1,59 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io.flutter.plugins.googlemobileads; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.android.gms.ads.formats.AdManagerAdViewOptions; +import org.junit.Test; + +/** Tests for {@link FlutterAdManagerAdViewOptions}. */ +public class FlutterAdManagerAdViewOptionsTest { + + @Test + public void testAsAdManagerAdViewOptions_null() { + FlutterAdManagerAdViewOptions flutterAdManagerAdViewOptions = + new FlutterAdManagerAdViewOptions(null); + + AdManagerAdViewOptions adManagerAdViewOptions = + flutterAdManagerAdViewOptions.asAdManagerAdViewOptions(); + AdManagerAdViewOptions defaultOptions = new AdManagerAdViewOptions.Builder().build(); + assertEquals( + adManagerAdViewOptions.getManualImpressionsEnabled(), + defaultOptions.getManualImpressionsEnabled()); + } + + @Test + public void testAsAdManagerAdViewOptions_true() { + FlutterAdManagerAdViewOptions flutterAdManagerAdViewOptions = + new FlutterAdManagerAdViewOptions(true); + + AdManagerAdViewOptions adManagerAdViewOptions = + flutterAdManagerAdViewOptions.asAdManagerAdViewOptions(); + assertTrue(adManagerAdViewOptions.getManualImpressionsEnabled()); + } + + @Test + public void testAsAdManagerAdViewOptions_false() { + FlutterAdManagerAdViewOptions flutterAdManagerAdViewOptions = + new FlutterAdManagerAdViewOptions(false); + + AdManagerAdViewOptions adManagerAdViewOptions = + flutterAdManagerAdViewOptions.asAdManagerAdViewOptions(); + assertFalse(adManagerAdViewOptions.getManualImpressionsEnabled()); + } +} diff --git a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterBannerParametersTest.java b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterBannerParametersTest.java new file mode 100644 index 000000000..2193ccb63 --- /dev/null +++ b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterBannerParametersTest.java @@ -0,0 +1,49 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io.flutter.plugins.googlemobileads; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import com.google.android.gms.ads.admanager.AdManagerAdView; +import com.google.android.gms.ads.formats.OnAdManagerAdViewLoadedListener; +import java.util.Collections; +import java.util.List; +import org.junit.Test; + +/** Tests for {@link FlutterBannerParameters}. */ +public class FlutterBannerParametersTest { + + @Test + public void testAsBannerParameters() { + List sizes = Collections.singletonList(new FlutterAdSize(100, 200)); + FlutterBannerParameters flutterBannerParameters = new FlutterBannerParameters(sizes, null); + + OnAdManagerAdViewLoadedListener listener = + new OnAdManagerAdViewLoadedListener() { + @Override + public void onAdManagerAdViewLoaded(AdManagerAdView adView) {} + }; + + FlutterAdLoaderAd.BannerParameters bannerParameters = + flutterBannerParameters.asBannerParameters(listener); + + assertEquals(bannerParameters.adSizes.length, 1); + assertEquals(bannerParameters.adSizes[0].getWidth(), 100); + assertEquals(bannerParameters.adSizes[0].getHeight(), 200); + assertNull(bannerParameters.adManagerAdViewOptions); + assertEquals(bannerParameters.listener, listener); + } +} diff --git a/packages/google_mobile_ads/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_mobile_ads/example/ios/Runner.xcodeproj/project.pbxproj index cea7d5c1b..5c8f4762b 100644 --- a/packages/google_mobile_ads/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_mobile_ads/example/ios/Runner.xcodeproj/project.pbxproj @@ -41,6 +41,7 @@ 9E61AA6129BBE8FD00801A83 /* FLTNativeTemplateFontStyleTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E61AA5C29BBE8FD00801A83 /* FLTNativeTemplateFontStyleTest.m */; }; 9E61AA6229BBE8FD00801A83 /* FLTNativeTemplateColorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E61AA5D29BBE8FD00801A83 /* FLTNativeTemplateColorTest.m */; }; E276E99229DFB5870052484E /* FLTAdLoaderAdTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E276E99129DFB5870052484E /* FLTAdLoaderAdTest.m */; }; + E2D4625F29DFC2560010C4D0 /* FLTAdManagerAdViewOptionsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E2D4625E29DFC2560010C4D0 /* FLTAdManagerAdViewOptionsTest.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -111,6 +112,7 @@ 9EB9FE57280F853D00DDBB4F /* UserMessagingPlatform.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = UserMessagingPlatform.xcframework; path = Pods/GoogleUserMessagingPlatform/Frameworks/Release/UserMessagingPlatform.xcframework; sourceTree = ""; }; 9EFEAB4E29B0019F000A063B /* GoogleMobileAds.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = GoogleMobileAds.xcframework; path = "Pods/Google-Mobile-Ads-SDK/Frameworks/GoogleMobileAdsFramework/GoogleMobileAds.xcframework"; sourceTree = ""; }; E276E99129DFB5870052484E /* FLTAdLoaderAdTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTAdLoaderAdTest.m; sourceTree = ""; }; + E2D4625E29DFC2560010C4D0 /* FLTAdManagerAdViewOptionsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTAdManagerAdViewOptionsTest.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -216,6 +218,7 @@ 9E61AA5829BBE8DA00801A83 /* NativeTemplates */, 9E61AA5529BBE8AF00801A83 /* UserMessagingPlatform */, E276E99129DFB5870052484E /* FLTAdLoaderAdTest.m */, + E2D4625E29DFC2560010C4D0 /* FLTAdManagerAdViewOptionsTest.m */, 9E61AA2729BBE66900801A83 /* FLTAdUtilTest.m */, 9E61AA2429BBE66900801A83 /* FLTAppOpenAdTest.m */, 9E61AA2229BBE66900801A83 /* FLTBannerAdTest.m */, @@ -418,6 +421,7 @@ E276E99229DFB5870052484E /* FLTAdLoaderAdTest.m in Sources */, 9E61AA5329BBE88000801A83 /* FLTUserMessagingPlatformReaderWriterTest.m in Sources */, 9E61AA3229BBE66900801A83 /* FLTAppOpenAdTest.m in Sources */, + E2D4625F29DFC2560010C4D0 /* FLTAdManagerAdViewOptionsTest.m in Sources */, 9E61AA5F29BBE8FD00801A83 /* FLTNativeTemplateTypeTest.m in Sources */, 9E61AA3429BBE66900801A83 /* FLTGamInterstitialAdTest.m in Sources */, 9E61AA6029BBE8FD00801A83 /* FLTNativeTemplateStyleTest.m in Sources */, diff --git a/packages/google_mobile_ads/example/ios/RunnerTests/FLTAdLoaderAdTest.m b/packages/google_mobile_ads/example/ios/RunnerTests/FLTAdLoaderAdTest.m index da591c26c..30469d733 100644 --- a/packages/google_mobile_ads/example/ios/RunnerTests/FLTAdLoaderAdTest.m +++ b/packages/google_mobile_ads/example/ios/RunnerTests/FLTAdLoaderAdTest.m @@ -29,7 +29,8 @@ - (void)testDelegates { [[FLTAdLoaderAd alloc] initWithAdUnitId:@"testAdUnitId" request:[[FLTAdRequest alloc] init] rootViewController:viewController - adId:@0]; + adId:@0 + banner:nil]; ad.manager = manager; @@ -46,6 +47,74 @@ - (void)testDelegates { error:[OCMArg isEqual:error]]); } +- (void)testBannerDelegates { + UIViewController *viewController = OCMClassMock([UIViewController class]); + FLTAdInstanceManager *manager = OCMClassMock([FLTAdInstanceManager class]); + + FLTAdSize *adSize = [[FLTAdSize alloc] initWithWidth:@(1) height:@(2)]; + FLTAdLoaderAd *ad = [[FLTAdLoaderAd alloc] + initWithAdUnitId:@"testAdUnitId" + request:[[FLTAdRequest alloc] init] + rootViewController:viewController + adId:@0 + banner:[[FLTBannerParameters alloc] initWithSizes:@[ adSize ] + options:nil]]; + + ad.manager = manager; + + [ad load]; + + // GAMBannerAdLoaderDelegate + NSArray *validSizes = [ad validBannerSizesForAdLoader:ad.adLoader]; + XCTAssertEqual(validSizes.count, 1); + XCTAssertEqualObjects(validSizes[0], NSValueFromGADAdSize(adSize.size)); + + GAMBannerView *bannerView = OCMClassMock([GAMBannerView class]); + OCMStub([bannerView adSize]).andReturn(GADAdSizeFromCGSize(CGSizeMake(0, 0))); + OCMStub([bannerView recordImpression]); + + [ad adLoader:ad.adLoader didReceiveGAMBannerView:bannerView]; + + XCTAssertEqual([ad adLoaderAdType], FLTAdLoaderAdTypeBanner); + + OCMVerify([bannerView setAppEventDelegate:[OCMArg isEqual:ad]]); + OCMVerify([bannerView setDelegate:[OCMArg isEqual:ad]]); + OCMVerify([manager onAdLoaded:[OCMArg isEqual:ad] + responseInfo:[OCMArg isEqual:nil]]); + + FLTAdSize *size = [ad adSize]; + XCTAssertEqualObjects(size.width, @0); + XCTAssertEqualObjects(size.height, @0); + + OCMVerify([bannerView adSize]); + + // GADBannerViewDelegate + NSError *error = [NSError errorWithDomain:@"domain" code:1 userInfo:nil]; + [ad bannerView:bannerView didFailToReceiveAdWithError:error]; + OCMVerify([manager onAdFailedToLoad:[OCMArg isEqual:ad] error:error]); + + [ad bannerViewDidRecordImpression:bannerView]; + OCMVerify([manager onBannerImpression:[OCMArg isEqual:ad]]); + + [ad bannerViewDidRecordClick:bannerView]; + OCMVerify([manager adDidRecordClick:[OCMArg isEqual:ad]]); + + [ad bannerViewWillPresentScreen:bannerView]; + OCMVerify([manager onBannerWillPresentScreen:[OCMArg isEqual:ad]]); + + [ad bannerViewWillDismissScreen:bannerView]; + OCMVerify([manager onBannerWillDismissScreen:[OCMArg isEqual:ad]]); + + [ad bannerViewDidDismissScreen:bannerView]; + OCMVerify([manager onBannerDidDismissScreen:[OCMArg isEqual:ad]]); + + // GADAppEventDelegate + [ad adView:bannerView didReceiveAppEvent:@"name" withInfo:@"info"]; + OCMVerify([manager onAppEvent:[OCMArg isEqual:ad] + name:[OCMArg isEqual:@"name"] + data:[OCMArg isEqual:@"info"]]); +} + - (void)testLoadAdLoaderAd { FLTAdRequest *request = [[FLTAdRequest alloc] init]; request.keywords = @[ @"apple" ]; @@ -64,7 +133,8 @@ - (void)testLoadAdLoaderAd:(FLTAdRequest *)request { FLTAdLoaderAd *ad = [[FLTAdLoaderAd alloc] initWithAdUnitId:@"testAdUnitId" request:request rootViewController:viewController - adId:@1]; + adId:@1 + banner:nil]; XCTAssertEqual(ad.adLoader.adUnitID, @"testAdUnitId"); XCTAssertEqual(ad.adLoader.delegate, ad); diff --git a/packages/google_mobile_ads/example/ios/RunnerTests/FLTAdManagerAdViewOptionsTest.m b/packages/google_mobile_ads/example/ios/RunnerTests/FLTAdManagerAdViewOptionsTest.m new file mode 100644 index 000000000..c42015c18 --- /dev/null +++ b/packages/google_mobile_ads/example/ios/RunnerTests/FLTAdManagerAdViewOptionsTest.m @@ -0,0 +1,49 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +#import "google_mobile_ads/FLTAd_Internal.h" + +@interface FLTAdManagerAdViewOptionsTest : XCTestCase +@end + +@implementation FLTAdManagerAdViewOptionsTest +- (void)testAsGADAdLoaderOptionsManualImpressionsEnabledUnset { + FLTAdManagerAdViewOptions *options = + [[FLTAdManagerAdViewOptions alloc] initWithManualImpressionsEnabled:nil]; + + XCTAssertEqual(options.asGADAdLoaderOptions.count, 0); +} + +- (void)testAsGADAdLoaderOptionsManualImpressionsEnabledYes { + FLTAdManagerAdViewOptions *options = + [[FLTAdManagerAdViewOptions alloc] initWithManualImpressionsEnabled:@YES]; + + XCTAssertEqual(options.asGADAdLoaderOptions.count, 1); + GADAdLoaderOptions *option = options.asGADAdLoaderOptions[0]; + XCTAssert([option isKindOfClass:[GAMBannerViewOptions class]]); + XCTAssertTrue(((GAMBannerViewOptions *)option).manualImpressionEnabled); +} + +- (void)testAsGADAdLoaderOptionsManualImpressionsEnabledNo { + FLTAdManagerAdViewOptions *options = + [[FLTAdManagerAdViewOptions alloc] initWithManualImpressionsEnabled:@NO]; + + XCTAssertEqual(options.asGADAdLoaderOptions.count, 1); + GADAdLoaderOptions *option = options.asGADAdLoaderOptions[0]; + XCTAssert([option isKindOfClass:[GAMBannerViewOptions class]]); + XCTAssertFalse(((GAMBannerViewOptions *)option).manualImpressionEnabled); +} +@end diff --git a/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m b/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m index bf273249e..87a4b80e6 100644 --- a/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m +++ b/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m @@ -780,6 +780,62 @@ - (void)assertEqualTemplateTypes:(FLTNativeTemplateType *)first XCTAssertEqual(first.intValue, second.intValue); } +- (void)testEncodeDecodeAdManagerAdViewOptionsNil { + FLTAdManagerAdViewOptions *options = + [[FLTAdManagerAdViewOptions alloc] initWithManualImpressionsEnabled:nil]; + + NSData *encodedMessage = [_messageCodec encode:options]; + + FLTAdManagerAdViewOptions *decodedOptions = + [_messageCodec decode:encodedMessage]; + XCTAssertEqual(decodedOptions.manualImpressionsEnabled, nil); +} + +- (void)testEncodeDecodeAdManagerAdViewOptionsYes { + FLTAdManagerAdViewOptions *options = [[FLTAdManagerAdViewOptions alloc] + initWithManualImpressionsEnabled:@(YES)]; + + NSData *encodedMessage = [_messageCodec encode:options]; + + FLTAdManagerAdViewOptions *decodedOptions = + [_messageCodec decode:encodedMessage]; + XCTAssertEqual(decodedOptions.manualImpressionsEnabled, @(YES)); +} + +- (void)testEncodeDecodeAdManagerAdViewOptionsNo { + FLTAdManagerAdViewOptions *options = [[FLTAdManagerAdViewOptions alloc] + initWithManualImpressionsEnabled:@(NO)]; + + NSData *encodedMessage = [_messageCodec encode:options]; + + FLTAdManagerAdViewOptions *decodedOptions = + [_messageCodec decode:encodedMessage]; + XCTAssertEqual(decodedOptions.manualImpressionsEnabled, @(NO)); +} + +- (void)testEncodeDecodeBannerParameters { + FLTBannerParameters *parameters = [[FLTBannerParameters alloc] + initWithSizes:@[ [[FLTAdSize alloc] initWithWidth:@(1) height:@(2)] ] + options:[[FLTAdManagerAdViewOptions alloc] + initWithManualImpressionsEnabled:nil]]; + + NSData *encodedMessage = [_messageCodec encode:parameters]; + + FLTBannerParameters *decodedParameters = + [_messageCodec decode:encodedMessage]; + + NSArray *sizes = decodedParameters.sizes; + + XCTAssertEqual(sizes.count, 1); + XCTAssertEqualObjects(sizes[0].width, @(1)); + XCTAssertEqualObjects(sizes[0].height, @(2)); + + FLTAdManagerAdViewOptions *options = decodedParameters.options; + + XCTAssertNotNil(options); + XCTAssertNil(options.manualImpressionsEnabled); +} + @end @implementation FLTTestAdSizeFactory diff --git a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTAd_Internal.m b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTAd_Internal.m index f889603e1..fc97fd903 100644 --- a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTAd_Internal.m +++ b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTAd_Internal.m @@ -1233,30 +1233,59 @@ - (UIView *)view { @end +@implementation FLTBannerParameters +- (nonnull instancetype)initWithSizes:(nonnull NSArray *)sizes + options:(nullable FLTAdManagerAdViewOptions *) + options { + self = [super init]; + _sizes = sizes; + _options = options; + return self; +} +@end + #pragma mark - FLTAdLoaderAd @implementation FLTAdLoaderAd { NSString *_adUnitId; FLTAdRequest *_adRequest; + NSMutableArray *_validAdSizes; + UIView *_view; + FLTBannerParameters *_banner; } -- (nonnull instancetype)initWithAdUnitId:(nonnull NSString *)adUnitId - request:(nonnull FLTAdRequest *)request - rootViewController: - (nonnull UIViewController *)rootViewController - adId:(nonnull NSNumber *)adId { +- (nonnull instancetype) + initWithAdUnitId:(nonnull NSString *)adUnitId + request:(nonnull FLTAdRequest *)request + rootViewController:(nonnull UIViewController *)rootViewController + adId:(nonnull NSNumber *)adId + banner:(nullable FLTBannerParameters *)bannerParameters { self = [super init]; if (self) { self.adId = adId; _adUnitId = adUnitId; _adRequest = request; _adLoaderAdType = FLTAdLoaderAdTypeUnknown; - _adSize = nil; _formatId = nil; NSMutableArray *adTypes = [[NSMutableArray alloc] init]; NSMutableArray *options = [[NSMutableArray alloc] init]; + if (![FLTAdUtil isNull:bannerParameters]) { + _banner = bannerParameters; + _validAdSizes = + [NSMutableArray arrayWithCapacity:bannerParameters.sizes.count]; + for (FLTAdSize *size in bannerParameters.sizes) { + [_validAdSizes addObject:NSValueFromGADAdSize(size.size)]; + } + + [adTypes addObject:GADAdLoaderAdTypeGAMBanner]; + + if (![FLTAdUtil isNull:_banner.options]) { + [options addObjectsFromArray:_banner.options.asGADAdLoaderOptions]; + } + } + _adLoader = [[GADAdLoader alloc] initWithAdUnitID:_adUnitId rootViewController:rootViewController adTypes:adTypes @@ -1266,6 +1295,13 @@ - (nonnull instancetype)initWithAdUnitId:(nonnull NSString *)adUnitId return self; } +- (FLTAdSize *)adSize { + if (_view && [_view isKindOfClass:[GADBannerView class]]) { + return [[FLTAdSize alloc] initWithAdSize:((GADBannerView *)_view).adSize]; + } + return nil; +} + #pragma mark - FLTAd - (void)load { @@ -1285,10 +1321,81 @@ - (void)adLoader:(nonnull GADAdLoader *)adLoader [manager onAdFailedToLoad:self error:error]; } +#pragma mark - GAMBannerAdLoaderDelegate + +- (nonnull NSArray *)validBannerSizesForAdLoader: + (nonnull GADAdLoader *)adLoader { + return _validAdSizes; +} + +- (void)adLoader:(nonnull GADAdLoader *)adLoader + didReceiveGAMBannerView:(nonnull GAMBannerView *)bannerView { + _adLoaderAdType = FLTAdLoaderAdTypeBanner; + _view = bannerView; + + bannerView.appEventDelegate = self; + bannerView.delegate = self; + + __weak FLTAdLoaderAd *weakSelf = self; + bannerView.paidEventHandler = ^(GADAdValue *_Nonnull value) { + if (weakSelf.manager == nil) { + return; + } + [weakSelf.manager + onPaidEvent:weakSelf + value:[[FLTAdValue alloc] initWithValue:value.value + precision:(NSInteger)value.precision + currencyCode:value.currencyCode]]; + }; + + [bannerView recordImpression]; + + [manager onAdLoaded:self responseInfo:bannerView.responseInfo]; +} + +#pragma mark - GADBannerViewDelegate + +- (void)bannerViewDidReceiveAd:(nonnull GADBannerView *)bannerView { + // TODO handled by adLoader:didReceiveGAMBannerView: ? +} + +- (void)bannerView:(nonnull GADBannerView *)bannerView + didFailToReceiveAdWithError:(nonnull NSError *)error { + [manager onAdFailedToLoad:self error:error]; +} + +- (void)bannerViewDidRecordImpression:(nonnull GADBannerView *)bannerView { + [manager onBannerImpression:self]; +} + +- (void)bannerViewDidRecordClick:(nonnull GADBannerView *)bannerView { + [manager adDidRecordClick:self]; +} + +- (void)bannerViewWillPresentScreen:(nonnull GADBannerView *)bannerView { + [manager onBannerWillPresentScreen:self]; +} + +- (void)bannerViewWillDismissScreen:(nonnull GADBannerView *)bannerView { + [manager onBannerWillDismissScreen:self]; +} + +- (void)bannerViewDidDismissScreen:(nonnull GADBannerView *)bannerView { + [manager onBannerDidDismissScreen:self]; +} + +#pragma mark - GADAppEventDelegate + +- (void)adView:(nonnull GADBannerView *)banner + didReceiveAppEvent:(nonnull NSString *)name + withInfo:(nullable NSString *)info { + [self.manager onAppEvent:self name:name data:info]; +} + #pragma mark - FlutterPlatformView - (nonnull UIView *)view { - return nil; // TODO not great + return _view; } @synthesize manager; @@ -1467,3 +1574,24 @@ @implementation FLTNativeAdOptions } @end + +@implementation FLTAdManagerAdViewOptions +- (nonnull instancetype)initWithManualImpressionsEnabled: + (nullable NSNumber *)manualImpressionsEnabled { + self = [super init]; + _manualImpressionsEnabled = manualImpressionsEnabled; + return self; +} + +- (nonnull NSArray *)asGADAdLoaderOptions { + NSMutableArray *options = [NSMutableArray array]; + if ([FLTAdUtil isNotNull:_manualImpressionsEnabled]) { + GAMBannerViewOptions *bannerViewOptions = + [[GAMBannerViewOptions alloc] init]; + bannerViewOptions.manualImpressionEnabled = + _manualImpressionsEnabled.boolValue; + [options addObject:bannerViewOptions]; + } + return options; +} +@end diff --git a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsPlugin.m b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsPlugin.m index 4e33ec14f..ddf90d843 100644 --- a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsPlugin.m +++ b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsPlugin.m @@ -453,7 +453,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call [[FLTAdLoaderAd alloc] initWithAdUnitId:call.arguments[@"adUnitId"] request:request rootViewController:rootController - adId:call.arguments[@"adId"]]; + adId:call.arguments[@"adId"] + banner:call.arguments[@"banner"]]; [_manager loadAd:ad]; result(nil); } else if ([call.method isEqualToString:@"loadInterstitialAd"]) { diff --git a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsReaderWriter_Internal.m b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsReaderWriter_Internal.m index fa8bcb5b3..139e92b4d 100644 --- a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsReaderWriter_Internal.m +++ b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsReaderWriter_Internal.m @@ -47,8 +47,9 @@ typedef NS_ENUM(NSInteger, FLTAdMobField) { FLTAdmobFieldNativeTemplateFontStyle = 151, FLTAdmobFieldNativeTemplateType = 152, FLTAdmobFieldNativeTemplateColor = 153, - FLTAdmobFieldMediationExtras = 154 - + FLTAdmobFieldMediationExtras = 154, + FLTAdmobFieldAdManagerAdViewOptions = 155, + FLTAdmobBannerParameters = 156, }; @interface FLTGoogleMobileAdsWriter : FlutterStandardWriter @@ -342,6 +343,16 @@ - (id _Nullable)readValueOfType:(UInt8)type { green:green blue:blue]; } + case FLTAdmobFieldAdManagerAdViewOptions: { + return [[FLTAdManagerAdViewOptions alloc] + initWithManualImpressionsEnabled:[self + readValueOfType:[self readByte]]]; + } + case FLTAdmobBannerParameters: { + return [[FLTBannerParameters alloc] + initWithSizes:[self readValueOfType:[self readByte]] + options:[self readValueOfType:[self readByte]]]; + } } return [super readValueOfType:type]; } @@ -525,6 +536,15 @@ - (void)writeValue:(id)value { [self writeValue:templateStyle.secondaryTextStyle]; [self writeValue:templateStyle.tertiaryTextStyle]; [self writeValue:templateStyle.cornerRadius]; + } else if ([value isKindOfClass:[FLTAdManagerAdViewOptions class]]) { + [self writeByte:FLTAdmobFieldAdManagerAdViewOptions]; + FLTAdManagerAdViewOptions *options = value; + [self writeValue:options.manualImpressionsEnabled]; + } else if ([value isKindOfClass:[FLTBannerParameters class]]) { + [self writeByte:FLTAdmobBannerParameters]; + FLTBannerParameters *bannerParameters = value; + [self writeValue:bannerParameters.sizes]; + [self writeValue:bannerParameters.options]; } else { [super writeValue:value]; } diff --git a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/FLTAd_Internal.h b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/FLTAd_Internal.h index abdd33aba..f6d6cb274 100644 --- a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/FLTAd_Internal.h +++ b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/FLTAd_Internal.h @@ -287,6 +287,15 @@ - (NSArray *_Nonnull)asGADAdLoaderOptions; @end +@interface FLTAdManagerAdViewOptions : NSObject +@property(readonly, nullable) NSNumber *manualImpressionsEnabled; + +- (nonnull instancetype)initWithManualImpressionsEnabled: + (nullable NSNumber *)manualImpressionsEnabled; + +- (nonnull NSArray *)asGADAdLoaderOptions; +@end + @interface FLTNativeAd : FLTBaseAd @@ -304,19 +313,31 @@ typedef NS_ENUM(NSInteger, FLTAdLoaderAdType) { FLTAdLoaderAdTypeUnknown = 0, + FLTAdLoaderAdTypeBanner = 1, }; +@interface FLTBannerParameters : NSObject +@property(readonly, nonnull) NSArray *sizes; +@property(readonly, nullable) FLTAdManagerAdViewOptions *options; +- (nonnull instancetype)initWithSizes:(nonnull NSArray *)sizes + options: + (nullable FLTAdManagerAdViewOptions *)options; +@end + @interface FLTAdLoaderAd - : FLTBaseAd + : FLTBaseAd @property(readonly, nonnull) GADAdLoader *adLoader; @property(readonly) FLTAdLoaderAdType adLoaderAdType; @property(readonly, nullable) FLTAdSize *adSize; @property(readonly, nullable) NSString *formatId; -- (nonnull instancetype)initWithAdUnitId:(nonnull NSString *)adUnitId - request:(nonnull FLTAdRequest *)request - rootViewController: - (nonnull UIViewController *)rootViewController - adId:(nonnull NSNumber *)adId; +- (nonnull instancetype) + initWithAdUnitId:(nonnull NSString *)adUnitId + request:(nonnull FLTAdRequest *)request + rootViewController:(nonnull UIViewController *)rootViewController + adId:(nonnull NSNumber *)adId + banner:(nullable FLTBannerParameters *)bannerParameters; @end @interface FLTRewardItem : NSObject diff --git a/packages/google_mobile_ads/lib/src/ad_containers.dart b/packages/google_mobile_ads/lib/src/ad_containers.dart index 075415bd8..cafb6e4b9 100644 --- a/packages/google_mobile_ads/lib/src/ad_containers.dart +++ b/packages/google_mobile_ads/lib/src/ad_containers.dart @@ -1149,6 +1149,9 @@ class NativeAd extends AdWithView { enum AdLoaderAdType { /// Unknown ad type unknown, + + /// Banner ad type + banner, } /// An AdLoaderAd. @@ -1184,6 +1187,7 @@ class AdLoaderAd extends AdWithView { required String adUnitId, required this.listener, required AdRequest request, + this.banner, }) : super(adUnitId: adUnitId, listener: listener) { if (request is AdManagerAdRequest) { adManagerRequest = request; @@ -1202,6 +1206,9 @@ class AdLoaderAd extends AdWithView { /// Targeting information used to fetch an [Ad] with Ad Manager. AdManagerAdRequest? adManagerRequest; + /// Optional parameters used to configure served "banner" ads + final BannerParameters? banner; + @override Future load() => instanceManager.loadAdLoaderAd(this); @@ -1631,6 +1638,45 @@ enum AdChoicesPlacement { bottomLeftCorner, } +/// Used to configure ad manager ad view requests. +class AdManagerAdViewOptions { + /// Whether manual impression reporting is enabled + /// + /// Default value is false. + final bool? manualImpressionsEnabled; + + /// Construct an [AdManagerAdViewOptions], an optional class used to further customize + /// ad manager ad view requests. + AdManagerAdViewOptions({this.manualImpressionsEnabled}); + + @override + bool operator ==(other) { + return other is AdManagerAdViewOptions && + manualImpressionsEnabled == other.manualImpressionsEnabled; + } +} + +/// Central configuration item for ad manager ad view requests served by +/// an [AdLoaderAd]. +class BannerParameters { + /// List of sizes the [AdLoaderAd] should expect + final List sizes; + + /// Additional options used when configuring the ad manager ad view + final AdManagerAdViewOptions? adManagerAdViewOptions; + + /// Construct a [BannerParameters], used by an [AdLoaderAd] to configure + /// ad manager ad views + BannerParameters({required this.sizes, this.adManagerAdViewOptions}); + + @override + bool operator ==(other) { + return other is BannerParameters && + listEquals(sizes, other.sizes) && + adManagerAdViewOptions == other.adManagerAdViewOptions; + } +} + /// 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 22cdf1a0f..bfd8d2651 100644 --- a/packages/google_mobile_ads/lib/src/ad_instance_manager.dart +++ b/packages/google_mobile_ads/lib/src/ad_instance_manager.dart @@ -509,6 +509,8 @@ class AdInstanceManager { switch (adLoaderAdType) { case 0: return AdLoaderAdType.unknown; + case 1: + return AdLoaderAdType.banner; default: debugPrint('Error: unknown AdLoaderAdType value: $adLoaderAdType'); return AdLoaderAdType.unknown; @@ -517,6 +519,8 @@ class AdInstanceManager { switch (adLoaderAdType) { case 0: return AdLoaderAdType.unknown; + case 1: + return AdLoaderAdType.banner; default: debugPrint('Error: unknown AdLoaderAdType value: $adLoaderAdType'); return AdLoaderAdType.unknown; @@ -617,6 +621,7 @@ class AdInstanceManager { 'adUnitId': ad.adUnitId, 'request': ad.request, 'adManagerRequest': ad.adManagerRequest, + 'banner': ad.banner, }); } @@ -928,6 +933,8 @@ class AdMessageCodec extends StandardMessageCodec { static const int _valueNativeTemplateType = 152; static const int _valueColor = 153; static const int _valueMediationExtras = 154; + static const int _valueAdManagerAdViewOptions = 155; + static const int _valueBannerParameters = 156; @override void writeValue(WriteBuffer buffer, dynamic value) { @@ -1064,6 +1071,13 @@ class AdMessageCodec extends StandardMessageCodec { } else if (value is NativeTemplateFontStyle) { buffer.putUint8(_valueNativeTemplateFontStyle); writeValue(buffer, value.index); + } else if (value is AdManagerAdViewOptions) { + buffer.putUint8(_valueAdManagerAdViewOptions); + writeValue(buffer, value.manualImpressionsEnabled); + } else if (value is BannerParameters) { + buffer.putUint8(_valueBannerParameters); + writeValue(buffer, value.sizes); + writeValue(buffer, value.adManagerAdViewOptions); } else { super.writeValue(buffer, value); } @@ -1330,6 +1344,15 @@ class AdMessageCodec extends StandardMessageCodec { buffer.getUint8(), buffer, )]; + case _valueAdManagerAdViewOptions: + return AdManagerAdViewOptions( + manualImpressionsEnabled: readValueOfType(buffer.getUint8(), buffer), + ); + case _valueBannerParameters: + return BannerParameters( + sizes: readValueOfType(buffer.getUint8(), buffer)?.cast(), + adManagerAdViewOptions: readValueOfType(buffer.getUint8(), buffer), + ); 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 516b0ac17..c89071e38 100644 --- a/packages/google_mobile_ads/test/ad_loader_ad_test.dart +++ b/packages/google_mobile_ads/test/ad_loader_ad_test.dart @@ -71,6 +71,7 @@ void main() { 'adUnitId': 'test-ad-unit', 'request': request, 'adManagerRequest': null, + 'banner': null, }, ), ]); @@ -96,6 +97,33 @@ void main() { 'adUnitId': 'test-ad-unit', 'request': null, 'adManagerRequest': request, + 'banner': null, + }, + ), + ]); + + expect(instanceManager.adFor(0), isNotNull); + }); + + test('load with $BannerParameters', () async { + final BannerParameters banner = BannerParameters(sizes: [AdSize.banner]); + final AdLoaderAd adLoaderAd = AdLoaderAd( + adUnitId: 'test-ad-unit', + listener: AdLoaderAdListener(), + request: AdRequest(), + banner: banner, + ); + + await adLoaderAd.load(); + expect(log, [ + isMethodCall( + 'loadAdLoaderAd', + arguments: { + 'adId': 0, + 'adUnitId': 'test-ad-unit', + 'request': adLoaderAd.request, + 'adManagerRequest': null, + 'banner': banner, }, ), ]); @@ -238,6 +266,205 @@ void main() { await testOnAdFailedToLoad('onAdFailedToLoad', 1); }); + test('onAdOpened event', () async { + var testOnAdOpened = (eventName, adId) async { + final Completer completer = Completer(); + + final AdLoaderAd ad = AdLoaderAd( + adUnitId: 'test-ad-unit', + listener: AdLoaderAdListener( + onAdOpened: (ad) => completer.complete(ad), + ), + request: AdRequest(), + ); + + await ad.load(); + + final MethodCall methodCall = MethodCall( + 'onAdEvent', + {'adId': adId, 'eventName': eventName}, + ); + + final ByteData data = instanceManager.channel.codec.encodeMethodCall( + methodCall, + ); + + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + 'plugins.flutter.io/google_mobile_ads', + data, + (ByteData? data) {}, + ); + + expect(completer.future, completion(ad)); + }; + + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + await testOnAdOpened('onBannerWillPresentScreen', 0); + + debugDefaultTargetPlatformOverride = TargetPlatform.android; + await testOnAdOpened('onAdOpened', 1); + }); + + test('onAdWillDismissScreen event', () async { + var testOnAdWillDismissScreen = (eventName, adId) async { + final Completer completer = Completer(); + + final AdLoaderAd ad = AdLoaderAd( + adUnitId: 'test-ad-unit', + listener: AdLoaderAdListener( + onAdWillDismissScreen: (ad) => completer.complete(ad), + ), + request: AdRequest(), + ); + + await ad.load(); + + final MethodCall methodCall = MethodCall( + 'onAdEvent', + {'adId': adId, 'eventName': eventName}, + ); + + final ByteData data = instanceManager.channel.codec.encodeMethodCall( + methodCall, + ); + + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + 'plugins.flutter.io/google_mobile_ads', + data, + (ByteData? data) {}, + ); + + expect(completer.future, completion(ad)); + }; + + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + await testOnAdWillDismissScreen('onBannerWillDismissScreen', 0); + }); + + test('onAdClosed event', () async { + var testOnAdClosed = (eventName, adId) async { + final Completer completer = Completer(); + + final AdLoaderAd ad = AdLoaderAd( + adUnitId: 'test-ad-unit', + listener: AdLoaderAdListener( + onAdClosed: (ad) => completer.complete(ad), + ), + request: AdRequest(), + ); + + await ad.load(); + + final MethodCall methodCall = MethodCall( + 'onAdEvent', + {'adId': adId, 'eventName': eventName}, + ); + + final ByteData data = instanceManager.channel.codec.encodeMethodCall( + methodCall, + ); + + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + 'plugins.flutter.io/google_mobile_ads', + data, + (ByteData? data) {}, + ); + + expect(completer.future, completion(ad)); + }; + + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + await testOnAdClosed('onBannerDidDismissScreen', 0); + + debugDefaultTargetPlatformOverride = TargetPlatform.android; + await testOnAdClosed('onAdClosed', 1); + }); + + test('onAdImpression event', () async { + var testOnAdImpression = (eventName, adId) async { + final Completer completer = Completer(); + + final AdLoaderAd ad = AdLoaderAd( + adUnitId: 'test-ad-unit', + listener: AdLoaderAdListener( + onAdImpression: (ad) => completer.complete(ad), + ), + request: AdRequest(), + ); + + await ad.load(); + + final MethodCall methodCall = MethodCall( + 'onAdEvent', + {'adId': adId, 'eventName': eventName}, + ); + + final ByteData data = instanceManager.channel.codec.encodeMethodCall( + methodCall, + ); + + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + 'plugins.flutter.io/google_mobile_ads', + data, + (ByteData? data) {}, + ); + + expect(completer.future, completion(ad)); + }; + + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + await testOnAdImpression('onBannerImpression', 0); + + debugDefaultTargetPlatformOverride = TargetPlatform.android; + await testOnAdImpression('onAdImpression', 1); + }); + + test('onPaidEvent event', () async { + var testOnPaidEvent = (eventName, adId) async { + final Completer completer = Completer(); + + final AdLoaderAd ad = AdLoaderAd( + adUnitId: 'test-ad-unit', + listener: AdLoaderAdListener( + onPaidEvent: (ad, micros, precision, currency) => + completer.complete(ad), + ), + request: AdRequest(), + ); + + await ad.load(); + + final MethodCall methodCall = + MethodCall('onAdEvent', { + 'adId': adId, + 'eventName': eventName, + 'valueMicros': 0, + 'precision': 0, + 'currencyCode': 'AUD', + }); + + final ByteData data = instanceManager.channel.codec.encodeMethodCall( + methodCall, + ); + + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + 'plugins.flutter.io/google_mobile_ads', + data, + (ByteData? data) {}, + ); + + expect(completer.future, completion(ad)); + }; + + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + await testOnPaidEvent('onPaidEvent', 0); + }); + test('onAdClicked event', () async { var testOnAdClicked = (eventName, adId) async { final Completer completer = Completer(); diff --git a/packages/google_mobile_ads/test/mobile_ads_test.dart b/packages/google_mobile_ads/test/mobile_ads_test.dart index ab7eddad7..15489f006 100644 --- a/packages/google_mobile_ads/test/mobile_ads_test.dart +++ b/packages/google_mobile_ads/test/mobile_ads_test.dart @@ -634,5 +634,59 @@ void main() { expect(result.tertiaryTextStyle, templateStyle.tertiaryTextStyle); expect(result.mainBackgroundColor, templateStyle.mainBackgroundColor); }); + + test('encode/decode minimal $AdManagerAdViewOptions', () { + for (final platform in [TargetPlatform.android, TargetPlatform.iOS]) { + debugDefaultTargetPlatformOverride = platform; + ByteData byteData = codec.encodeMessage(AdManagerAdViewOptions())!; + + AdManagerAdViewOptions result = codec.decodeMessage(byteData); + expect(result.manualImpressionsEnabled, null); + } + }); + + test('encode/decode $AdManagerAdViewOptions', () { + for (final platform in [TargetPlatform.android, TargetPlatform.iOS]) { + debugDefaultTargetPlatformOverride = platform; + ByteData byteData = codec.encodeMessage( + AdManagerAdViewOptions(manualImpressionsEnabled: true), + )!; + + AdManagerAdViewOptions result = codec.decodeMessage(byteData); + expect(result.manualImpressionsEnabled, true); + + byteData = codec.encodeMessage( + AdManagerAdViewOptions(manualImpressionsEnabled: false), + )!; + + result = codec.decodeMessage(byteData); + expect(result.manualImpressionsEnabled, false); + + byteData = codec.encodeMessage( + AdManagerAdViewOptions(manualImpressionsEnabled: null), + )!; + + result = codec.decodeMessage(byteData); + expect(result.manualImpressionsEnabled, null); + } + }); + + test('encode/decode $BannerParameters', () { + for (final platform in [TargetPlatform.android, TargetPlatform.iOS]) { + debugDefaultTargetPlatformOverride = platform; + ByteData byteData = codec.encodeMessage( + BannerParameters( + sizes: [AdSize.banner], + adManagerAdViewOptions: AdManagerAdViewOptions( + manualImpressionsEnabled: true, + ), + ), + )!; + + BannerParameters result = codec.decodeMessage(byteData); + expect(result.sizes, [AdSize.banner]); + expect(result.adManagerAdViewOptions?.manualImpressionsEnabled, true); + } + }); }); } From 2451350c79f22de806dac38e7dc024dde66f977f Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 24 Oct 2022 12:57:24 +1100 Subject: [PATCH 2/6] Allow serving `Custom` ads in `AdLoaderAd` Allow the `AdLoaderAd` instance to serve `Custom` ads, which are instances of: * `NativeCustomFormatAd`/`CustomNativeAd` under Android, and * `GADCustomNativeAd` under iOS --- .../googlemobileads/CustomAdFactory.java | 12 +++ .../googlemobileads/FlutterAdListener.java | 8 ++ .../googlemobileads/FlutterAdLoader.java | 16 +++- .../googlemobileads/FlutterAdLoaderAd.java | 52 +++++++++++- .../FlutterCustomParameters.java | 28 +++++++ .../googlemobileads/AdLoaderAdType.java | 1 + .../googlemobileads/AdMessageCodec.java | 10 +++ .../GoogleMobileAdsPlugin.java | 63 +++++++++++++++ .../googlemobileads/CustomAdFactory.java | 12 +++ .../googlemobileads/FlutterAdListener.java | 19 +++++ .../googlemobileads/FlutterAdLoader.java | 16 +++- .../googlemobileads/FlutterAdLoaderAd.java | 58 +++++++++++++- .../FlutterCustomParameters.java | 30 +++++++ .../googlemobileads/AdMessageCodecTest.java | 16 ++++ .../FlutterAdLoaderAdTest.java | 79 +++++++++++++++++-- .../ios/RunnerTests/FLTAdLoaderAdTest.m | 71 ++++++++++++++++- .../FLTGoogleMobileAdsReaderWriterTest.m | 27 +++++++ .../FLTAdInstanceManager_Internal.m | 16 ++++ .../google_mobile_ads/FLTAd_Internal.m | 69 +++++++++++++++- .../FLTGoogleMobileAdsPlugin.m | 65 ++++++++++++++- .../FLTGoogleMobileAdsReaderWriter_Internal.m | 11 +++ .../FLTAdInstanceManager_Internal.h | 4 + .../google_mobile_ads/FLTAd_Internal.h | 17 +++- .../FLTGoogleMobileAdsPlugin.h | 15 ++++ .../lib/src/ad_containers.dart | 31 ++++++++ .../lib/src/ad_instance_manager.dart | 22 ++++++ .../test/ad_loader_ad_test.dart | 32 ++++++++ .../test/mobile_ads_test.dart | 29 +++++++ 28 files changed, 806 insertions(+), 23 deletions(-) create mode 100644 packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/CustomAdFactory.java create mode 100644 packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java create mode 100644 packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/CustomAdFactory.java create mode 100644 packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/CustomAdFactory.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/CustomAdFactory.java new file mode 100644 index 000000000..404753dd1 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/CustomAdFactory.java @@ -0,0 +1,12 @@ +package io.flutter.plugins.googlemobileads; + +import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.libraries.ads.mobile.sdk.nativead.CustomNativeAd; +import java.util.Map; + +public interface CustomAdFactory { + View createCustomAd( + @NonNull CustomNativeAd customNativeAd, @Nullable Map customOptions); +} diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java index ac81ae1b3..34d68c91d 100644 --- a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java @@ -21,6 +21,7 @@ import com.google.android.libraries.ads.mobile.sdk.common.AdValue; import com.google.android.libraries.ads.mobile.sdk.common.FullScreenContentError; import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; +import com.google.android.libraries.ads.mobile.sdk.nativead.CustomNativeAd; import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAd; import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdEventCallback; import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdLoaderCallback; @@ -172,4 +173,11 @@ public void onBannerAdLoaded(@NonNull BannerAd bannerAd) { callbackWeakReference.get().onBannerAdLoaded(bannerAd); } } + + @Override + public void onCustomNativeAdLoaded(@NonNull CustomNativeAd customNativeAd) { + if (callbackWeakReference.get() != null) { + callbackWeakReference.get().onCustomNativeAdLoaded(customNativeAd); + } + } } diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java index 35a9f122f..5c6455820 100644 --- a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java @@ -126,13 +126,18 @@ public void loadAdManagerNativeAd( public void loadAdLoaderAd( @NonNull String adUnitId, @NonNull NativeAdLoaderCallback adLoaderAdLoaderCallback, - @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters) { + @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters, + @Nullable FlutterAdLoaderAd.CustomParameters customParameters) { List nativeAdTypes = new ArrayList<>(); if (bannerParameters != null) { nativeAdTypes.add(NativeAdType.BANNER); } + if (customParameters != null) { + nativeAdTypes.add(NativeAdType.CUSTOM_NATIVE); + } + NativeAdRequest.Builder builder = new NativeAdRequest.Builder(adUnitId, nativeAdTypes); if (bannerParameters != null) { @@ -142,6 +147,10 @@ public void loadAdLoaderAd( } } + if (customParameters != null) { + builder.setCustomFormatIds(new ArrayList<>(customParameters.factories.keySet())); + } + NativeAdLoader.load( builder.build(), adLoaderAdLoaderCallback); @@ -151,7 +160,8 @@ public void loadAdLoaderAd( public void loadAdManagerAdLoaderAd( @NonNull String adUnitId, @NonNull NativeAdLoaderCallback adLoaderAdLoaderCallback, - @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters) { - loadAdLoaderAd(adUnitId, adLoaderAdLoaderCallback, bannerParameters); + @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters, + @Nullable FlutterAdLoaderAd.CustomParameters customParameters) { + loadAdLoaderAd(adUnitId, adLoaderAdLoaderCallback, bannerParameters, customParameters); } } diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java index 970a33090..5b92f38f5 100644 --- a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java @@ -8,9 +8,11 @@ import com.google.android.libraries.ads.mobile.sdk.banner.AdView; import com.google.android.libraries.ads.mobile.sdk.banner.BannerAd; import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; +import com.google.android.libraries.ads.mobile.sdk.nativead.CustomNativeAd; import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdLoaderCallback; import io.flutter.plugin.platform.PlatformView; import java.util.List; +import java.util.Map; class FlutterAdLoaderAd extends FlutterAd implements FlutterAdLoadedListener, NativeAdLoaderCallback { private static final String TAG = "FlutterAdLoaderAd"; @@ -24,6 +26,7 @@ class FlutterAdLoaderAd extends FlutterAd implements FlutterAdLoadedListener, Na @Nullable private String formatId; @Nullable private View view; @Nullable protected BannerParameters bannerParameters; + @Nullable protected CustomParameters customParameters; static class Builder { @Nullable private AdInstanceManager manager; @@ -33,6 +36,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; @@ -64,11 +69,22 @@ public Builder setBanner(@Nullable FlutterBannerParameters bannerParameters) { return this; } + public Builder setCustom(@Nullable FlutterCustomParameters customParameters) { + this.customParameters = customParameters; + return this; + } + public Builder setFlutterAdLoader(@NonNull FlutterAdLoader adLoader) { this.adLoader = adLoader; 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"); @@ -94,6 +110,10 @@ FlutterAdLoaderAd build() { adLoaderAd.bannerParameters = bannerParameters.asBannerParameters(); } + if (customParameters != null) { + adLoaderAd.customParameters = customParameters.asCustomParameters(customFactories); + } + return adLoaderAd; } } @@ -110,6 +130,18 @@ static class BannerParameters { } } + static class CustomParameters { + @NonNull final Map factories; + @Nullable final Map viewOptions; + + CustomParameters( + @NonNull Map factories, + @Nullable Map viewOptions) { + this.factories = factories; + this.viewOptions = viewOptions; + } + } + protected FlutterAdLoaderAd( int adId, @NonNull AdInstanceManager manager, @@ -150,9 +182,10 @@ void load() { // Note we delegate loading the ad to FlutterAdLoader mainly for testing purposes. // As of 20.0.0 of GMA, mockito is unable to mock AdLoader. if (request != null) { - adLoader.loadAdLoaderAd(adUnitId, loadedListener, bannerParameters); + adLoader.loadAdLoaderAd(adUnitId, loadedListener, bannerParameters, customParameters); } else if (adManagerRequest != null) { - adLoader.loadAdManagerAdLoaderAd(adUnitId, loadedListener, bannerParameters); + adLoader.loadAdManagerAdLoaderAd( + adUnitId, loadedListener, bannerParameters, customParameters); } else { Log.e(TAG, "A null or invalid ad request was provided."); } @@ -206,6 +239,21 @@ public void onBannerAdLoaded(@NonNull BannerAd bannerAd) { manager.onAdLoaded(adId, bannerAd.getResponseInfo()); } + @Override + public void onCustomNativeAdLoaded(@NonNull CustomNativeAd customNativeAd) { + formatId = customNativeAd.getCustomFormatId(); + view = + customParameters + .factories + .get(formatId) + .createCustomAd(customNativeAd, customParameters.viewOptions); + type = AdLoaderAdType.CUSTOM; + + customNativeAd.setAdEventCallback(new FlutterNativeAdListener(adId, manager)); + + manager.onAdLoaded(adId, customNativeAd.getResponseInfo()); + } + @Override void dispose() { if (view == null) { diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java new file mode 100644 index 000000000..7dec7bffb --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java @@ -0,0 +1,28 @@ +package io.flutter.plugins.googlemobileads; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugins.googlemobileads.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 Map availableFactories) { + Map factories = new HashMap<>(); + for (String formatId : formatIds) { + factories.put(formatId, availableFactories.get(formatId)); + } + return new FlutterAdLoaderAd.CustomParameters(factories, viewOptions); + } +} diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdLoaderAdType.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdLoaderAdType.java index 23d6838dd..84022879e 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdLoaderAdType.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdLoaderAdType.java @@ -3,4 +3,5 @@ enum AdLoaderAdType { UNKNOWN, BANNER, + CUSTOM, } 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 491a4fec7..753e0b783 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 @@ -69,6 +69,7 @@ class AdMessageCodec extends StandardMessageCodec { private static final byte VALUE_MEDIATION_EXTRAS = (byte) 154; private static final byte VALUE_AD_MANAGER_AD_VIEW_OPTIONS = (byte) 155; private static final byte VALUE_BANNER_PARAMETERS = (byte) 156; + private static final byte VALUE_CUSTOM_PARAMETERS = (byte) 157; @NonNull Context context; @NonNull final FlutterAdSize.AdSizeFactory adSizeFactory; @@ -268,6 +269,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); } @@ -460,6 +466,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/GoogleMobileAdsPlugin.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java index 9189276c6..e7e0de5ba 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 @@ -60,6 +60,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<>(); @SuppressWarnings("deprecation") // Keeping for compatibility @Nullable @@ -123,6 +124,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. @@ -183,6 +191,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 NativeAdFactory} * used to create {@link NativeAdView}s from a Native Ad @@ -205,6 +226,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 = @@ -222,6 +253,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; @@ -390,6 +438,19 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) 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) @@ -402,6 +463,8 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) ? adLoaderSupplier.get() : 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/playServices/java/io/flutter/plugins/googlemobileads/CustomAdFactory.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/CustomAdFactory.java new file mode 100644 index 000000000..2fb47bce6 --- /dev/null +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/CustomAdFactory.java @@ -0,0 +1,12 @@ +package io.flutter.plugins.googlemobileads; + +import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.ads.nativead.NativeCustomFormatAd; +import java.util.Map; + +public interface CustomAdFactory { + View createCustomAd( + @NonNull NativeCustomFormatAd nativeAd, @Nullable Map customOptions); +} diff --git a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java index cd0efbefe..1133e46e9 100644 --- a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java +++ b/packages/google_mobile_ads/android/src/playServices/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/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java index 22241841c..7efbaccb0 100644 --- a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java @@ -144,7 +144,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); @@ -152,6 +153,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); } @@ -160,7 +166,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); @@ -168,6 +175,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/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java index 2a5629478..7a944dc8d 100644 --- a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java @@ -24,13 +24,17 @@ 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 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; @@ -42,6 +46,7 @@ class FlutterAdLoaderAd extends FlutterAd implements OnAdManagerAdViewLoadedList @Nullable private String formatId; @Nullable private View view; @Nullable protected BannerParameters bannerParameters; + @Nullable protected CustomParameters customParameters; static class Builder { @Nullable private AdInstanceManager manager; @@ -51,6 +56,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; @@ -87,6 +94,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"); @@ -114,6 +132,12 @@ FlutterAdLoaderAd build() { new FlutterAdManagerAdViewLoadedListener(adLoaderAd)); } + if (customParameters != null) { + adLoaderAd.customParameters = + customParameters.asCustomParameters( + new FlutterCustomFormatAdLoadedListener(adLoaderAd), customFactories); + } + return adLoaderAd; } } @@ -133,6 +157,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, @@ -174,13 +213,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; } @@ -222,6 +265,15 @@ public void onAdManagerAdViewLoaded(@NonNull AdManagerAdView adView) { manager.onAdLoaded(adId, adView.getResponseInfo()); } + @Override + public void onCustomFormatAdLoaded(@NonNull NativeCustomFormatAd ad) { + formatId = ad.getCustomFormatId(); + view = + customParameters.factories.get(formatId).createCustomAd(ad, customParameters.viewOptions); + type = AdLoaderAdType.CUSTOM; + manager.onAdLoaded(adId, null); + } + @Override void dispose() { if (view == null) { diff --git a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java new file mode 100644 index 000000000..3142d7d57 --- /dev/null +++ b/packages/google_mobile_ads/android/src/playServices/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.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/test/java/io/flutter/plugins/googlemobileads/AdMessageCodecTest.java b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/AdMessageCodecTest.java index e4a91dc45..9e5d73ee6 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 @@ -581,6 +581,22 @@ 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"); + } } class DummyMediationExtras extends FlutterMediationExtras { 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 ec828faed..418a86a88 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 @@ -34,8 +34,10 @@ 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 java.util.Collections; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -84,12 +86,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)); @@ -127,12 +131,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)); @@ -184,7 +188,7 @@ 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(); @@ -198,7 +202,7 @@ public Object answer(InvocationOnMock invocation) { 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)); @@ -209,6 +213,69 @@ 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(); + + assertEquals(adLoaderAd.getAdLoaderAdType(), AdLoaderAdType.CUSTOM); + + assertEquals(adLoaderAd.getFormatId(), "formatId"); + + 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/example/ios/RunnerTests/FLTAdLoaderAdTest.m b/packages/google_mobile_ads/example/ios/RunnerTests/FLTAdLoaderAdTest.m index 30469d733..6ab7164e9 100644 --- a/packages/google_mobile_ads/example/ios/RunnerTests/FLTAdLoaderAdTest.m +++ b/packages/google_mobile_ads/example/ios/RunnerTests/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; @@ -115,6 +117,68 @@ - (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]; + + XCTAssertEqual([ad adLoaderAdType], FLTAdLoaderAdTypeCustom); + + 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]]); + + NSString *formatId = [ad formatId]; + XCTAssertEqual([ad formatId], @"12345678"); + + // 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" ]; @@ -134,7 +198,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/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m b/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m index 87a4b80e6..b91359591 100644 --- a/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m +++ b/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m @@ -836,6 +836,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/ios/google_mobile_ads/Sources/google_mobile_ads/FLTAdInstanceManager_Internal.m b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTAdInstanceManager_Internal.m index f57a655ee..c50290ee6 100644 --- a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTAdInstanceManager_Internal.m +++ b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/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/google_mobile_ads/Sources/google_mobile_ads/FLTAd_Internal.m b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTAd_Internal.m index fc97fd903..e4e4f9e91 100644 --- a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTAd_Internal.m +++ b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTAd_Internal.m @@ -1244,6 +1244,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 { @@ -1252,6 +1264,7 @@ @implementation FLTAdLoaderAd { NSMutableArray *_validAdSizes; UIView *_view; FLTBannerParameters *_banner; + FLTCustomParameters *_custom; } - (nonnull instancetype) @@ -1259,7 +1272,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; @@ -1286,6 +1300,12 @@ @implementation FLTAdLoaderAd { } } + if (![FLTAdUtil isNull:customParameters]) { + _custom = customParameters; + + [adTypes addObject:GADAdLoaderAdTypeCustomNative]; + } + _adLoader = [[GADAdLoader alloc] initWithAdUnitID:_adUnitId rootViewController:rootViewController adTypes:adTypes @@ -1392,6 +1412,53 @@ - (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; + _adLoaderAdType = FLTAdLoaderAdTypeCustom; + _formatId = customNativeAd.formatID; + _view = [_custom.factories[_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/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsPlugin.m b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsPlugin.m index ddf90d843..b84ecead3 100644 --- a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsPlugin.m +++ b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsPlugin.m @@ -25,6 +25,8 @@ @interface FLTGoogleMobileAdsPlugin () @property NSObject *registrar; @property NSMutableDictionary> *nativeAdFactories; +@property NSMutableDictionary> + *customAdFactories; @end /// Initialization handler for GMASDK. Invokes result at most once. @@ -67,6 +69,7 @@ - (void)handleInitializationComplete:(GADInitializationStatus *_Nonnull)status { @implementation FLTGoogleMobileAdsPlugin { NSMutableDictionary> *_nativeAdFactories; + NSMutableDictionary> *_customAdFactories; FLTAdInstanceManager *_manager; id _mediationNetworkExtrasProvider; FLTGoogleMobileAdsReaderWriter *_readerWriter; @@ -119,6 +122,7 @@ - (instancetype)initWithRegistrar: (NSObject*)registrar if (self) { _registrar = registrar; _nativeAdFactories = [NSMutableDictionary dictionary]; + _customAdFactories = [NSMutableDictionary dictionary]; _manager = [[FLTAdInstanceManager alloc] initWithBinaryMessenger:binaryMessenger]; _appStateNotifier = @@ -200,6 +204,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 { @@ -213,6 +244,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 { UIViewController* root = _registrar.viewController; if ([FLTAdUtil isNull:root]) { @@ -442,6 +486,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"]; @@ -454,7 +516,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/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsReaderWriter_Internal.m b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsReaderWriter_Internal.m index 139e92b4d..a400dc723 100644 --- a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsReaderWriter_Internal.m +++ b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsReaderWriter_Internal.m @@ -50,6 +50,7 @@ typedef NS_ENUM(NSInteger, FLTAdMobField) { FLTAdmobFieldMediationExtras = 154, FLTAdmobFieldAdManagerAdViewOptions = 155, FLTAdmobBannerParameters = 156, + FLTAdmobCustomParameters = 157, }; @interface FLTGoogleMobileAdsWriter : FlutterStandardWriter @@ -353,6 +354,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]; } @@ -545,6 +551,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/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/FLTAdInstanceManager_Internal.h b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/FLTAdInstanceManager_Internal.h index 459356b84..72f108bf2 100644 --- a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/FLTAdInstanceManager_Internal.h +++ b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/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/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/FLTAd_Internal.h b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/FLTAd_Internal.h index f6d6cb274..aa3f12be6 100644 --- a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/FLTAd_Internal.h +++ b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/FLTAd_Internal.h @@ -24,6 +24,7 @@ @class FLTAdInstanceManager; @protocol FLTNativeAdFactory; +@protocol FLTCustomAdFactory; @interface FLTAdSize : NSObject @property(readonly) GADAdSize size; @@ -314,6 +315,7 @@ typedef NS_ENUM(NSInteger, FLTAdLoaderAdType) { FLTAdLoaderAdTypeUnknown = 0, FLTAdLoaderAdTypeBanner = 1, + FLTAdLoaderAdTypeCustom = 2, }; @interface FLTBannerParameters : NSObject @@ -324,10 +326,20 @@ typedef NS_ENUM(NSInteger, FLTAdLoaderAdType) { (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; @property(readonly) FLTAdLoaderAdType adLoaderAdType; @property(readonly, nullable) FLTAdSize *adSize; @@ -337,7 +349,8 @@ typedef NS_ENUM(NSInteger, FLTAdLoaderAdType) { 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/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/FLTGoogleMobileAdsPlugin.h b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/FLTGoogleMobileAdsPlugin.h index 9da745da3..d1c3abb36 100644 --- a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/FLTGoogleMobileAdsPlugin.h +++ b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/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. */ @@ -95,6 +102,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. @@ -108,4 +119,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/lib/src/ad_containers.dart b/packages/google_mobile_ads/lib/src/ad_containers.dart index cafb6e4b9..a1c6c8ffb 100644 --- a/packages/google_mobile_ads/lib/src/ad_containers.dart +++ b/packages/google_mobile_ads/lib/src/ad_containers.dart @@ -1152,6 +1152,9 @@ enum AdLoaderAdType { /// Banner ad type banner, + + /// Custom ad type + custom, } /// An AdLoaderAd. @@ -1188,6 +1191,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; @@ -1209,6 +1213,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() => instanceManager.loadAdLoaderAd(this); @@ -1677,6 +1684,30 @@ 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 bfd8d2651..b8aae9bc7 100644 --- a/packages/google_mobile_ads/lib/src/ad_instance_manager.dart +++ b/packages/google_mobile_ads/lib/src/ad_instance_manager.dart @@ -103,14 +103,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 { @@ -124,6 +127,7 @@ class AdInstanceManager { case 'onBannerImpression': case 'adDidRecordImpression': // Fall through case 'onNativeAdImpression': // Fall through + case 'onCustomNativeAdImpression': _invokeOnAdImpression(ad, eventName); break; case 'adWillPresentFullScreenContent': @@ -511,6 +515,8 @@ class AdInstanceManager { return AdLoaderAdType.unknown; case 1: return AdLoaderAdType.banner; + case 2: + return AdLoaderAdType.custom; default: debugPrint('Error: unknown AdLoaderAdType value: $adLoaderAdType'); return AdLoaderAdType.unknown; @@ -521,6 +527,8 @@ class AdInstanceManager { return AdLoaderAdType.unknown; case 1: return AdLoaderAdType.banner; + case 2: + return AdLoaderAdType.custom; default: debugPrint('Error: unknown AdLoaderAdType value: $adLoaderAdType'); return AdLoaderAdType.unknown; @@ -622,6 +630,7 @@ class AdInstanceManager { 'request': ad.request, 'adManagerRequest': ad.adManagerRequest, 'banner': ad.banner, + 'custom': ad.custom, }); } @@ -935,6 +944,7 @@ class AdMessageCodec extends StandardMessageCodec { static const int _valueMediationExtras = 154; static const int _valueAdManagerAdViewOptions = 155; static const int _valueBannerParameters = 156; + static const int _valueCustomParameters = 157; @override void writeValue(WriteBuffer buffer, dynamic value) { @@ -1078,6 +1088,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); } @@ -1353,6 +1367,14 @@ 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 c89071e38..57ce53cad 100644 --- a/packages/google_mobile_ads/test/ad_loader_ad_test.dart +++ b/packages/google_mobile_ads/test/ad_loader_ad_test.dart @@ -72,6 +72,7 @@ void main() { 'request': request, 'adManagerRequest': null, 'banner': null, + 'custom': null, }, ), ]); @@ -98,6 +99,7 @@ void main() { 'request': null, 'adManagerRequest': request, 'banner': null, + 'custom': null, }, ), ]); @@ -124,6 +126,36 @@ 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 15489f006..949b98212 100644 --- a/packages/google_mobile_ads/test/mobile_ads_test.dart +++ b/packages/google_mobile_ads/test/mobile_ads_test.dart @@ -688,5 +688,34 @@ 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'}); + } + }); }); } From 60b56b3daa84a6d06e55f364d8dc07d6758efe91 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 27 Oct 2022 11:52:55 +1100 Subject: [PATCH 3/6] Allow serving `Native` ads in `AdLoaderAd` Allow the `AdLoaderAd` instance to serve `Native` ads, which are instances of: * `NativeAd` under Android, and * `GADNativeAd` under iOS --- .../googlemobileads/FlutterAdListener.java | 7 + .../googlemobileads/FlutterAdLoader.java | 63 +++++++- .../googlemobileads/FlutterAdLoaderAd.java | 50 ++++++- .../FlutterNativeParameters.java | 32 +++++ .../googlemobileads/AdLoaderAdType.java | 1 + .../googlemobileads/AdMessageCodec.java | 12 ++ .../GoogleMobileAdsPlugin.java | 13 ++ .../googlemobileads/FlutterAdLoader.java | 18 ++- .../googlemobileads/FlutterAdLoaderAd.java | 63 +++++++- .../FlutterCustomParameters.java | 4 +- .../FlutterNativeParameters.java | 47 ++++++ .../googlemobileads/AdMessageCodecTest.java | 23 +++ .../FlutterAdLoaderAdTest.java | 135 ++++++++++++++++-- .../ios/RunnerTests/FLTAdLoaderAdTest.m | 67 ++++++++- .../FLTGoogleMobileAdsReaderWriterTest.m | 35 +++++ .../google_mobile_ads/FLTAd_Internal.m | 76 +++++++++- .../FLTGoogleMobileAdsPlugin.m | 19 ++- .../FLTGoogleMobileAdsReaderWriter_Internal.m | 13 ++ .../google_mobile_ads/FLTAd_Internal.h | 18 ++- .../lib/src/ad_containers.dart | 38 +++++ .../lib/src/ad_instance_manager.dart | 20 +++ .../test/ad_loader_ad_test.dart | 34 +++++ .../test/mobile_ads_test.dart | 32 +++++ 23 files changed, 792 insertions(+), 28 deletions(-) create mode 100644 packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterNativeParameters.java create mode 100644 packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterNativeParameters.java diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java index 34d68c91d..ac3fad6b7 100644 --- a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java @@ -180,4 +180,11 @@ public void onCustomNativeAdLoaded(@NonNull CustomNativeAd customNativeAd) { callbackWeakReference.get().onCustomNativeAdLoaded(customNativeAd); } } + + @Override + public void onNativeAdLoaded(@NonNull NativeAd nativeAd) { + if (callbackWeakReference.get() != null) { + callbackWeakReference.get().onNativeAdLoaded(nativeAd); + } + } } diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java index 5c6455820..4f24c2cb2 100644 --- a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java @@ -20,11 +20,13 @@ import androidx.annotation.Nullable; import com.google.android.gms.ads.nativead.NativeAdOptions; import com.google.android.libraries.ads.mobile.sdk.appopen.AppOpenAd; +import com.google.android.libraries.ads.mobile.sdk.common.AdChoicesPlacement; 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.VideoOptions; import com.google.android.libraries.ads.mobile.sdk.interstitial.InterstitialAd; import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAd.NativeAdType; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAd.NativeMediaAspectRatio; import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdLoader; import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdLoaderCallback; import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdRequest; @@ -127,7 +129,8 @@ public void loadAdLoaderAd( @NonNull String adUnitId, @NonNull NativeAdLoaderCallback adLoaderAdLoaderCallback, @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters, - @Nullable FlutterAdLoaderAd.CustomParameters customParameters) { + @Nullable FlutterAdLoaderAd.CustomParameters customParameters, + @Nullable FlutterAdLoaderAd.NativeParameters nativeParameters) { List nativeAdTypes = new ArrayList<>(); if (bannerParameters != null) { @@ -138,6 +141,10 @@ public void loadAdLoaderAd( nativeAdTypes.add(NativeAdType.CUSTOM_NATIVE); } + if (nativeParameters != null) { + nativeAdTypes.add(NativeAdType.NATIVE); + } + NativeAdRequest.Builder builder = new NativeAdRequest.Builder(adUnitId, nativeAdTypes); if (bannerParameters != null) { @@ -151,6 +158,15 @@ public void loadAdLoaderAd( builder.setCustomFormatIds(new ArrayList<>(customParameters.factories.keySet())); } + if (nativeParameters != null && nativeParameters.nativeAdOptions != null) { + builder.setAdChoicesPlacement(convertAdChoicesPlacement(nativeParameters.nativeAdOptions.getAdChoicesPlacement())); + builder.setMediaAspectRatio(convertMediaAspectRatio(nativeParameters.nativeAdOptions.getMediaAspectRatio())); + com.google.android.gms.ads.VideoOptions videoOptions = nativeParameters.nativeAdOptions.getVideoOptions(); + if (videoOptions != null) { + builder.setVideoOptions(convertVideoOptions(videoOptions)); + } + } + NativeAdLoader.load( builder.build(), adLoaderAdLoaderCallback); @@ -161,7 +177,48 @@ public void loadAdManagerAdLoaderAd( @NonNull String adUnitId, @NonNull NativeAdLoaderCallback adLoaderAdLoaderCallback, @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters, - @Nullable FlutterAdLoaderAd.CustomParameters customParameters) { - loadAdLoaderAd(adUnitId, adLoaderAdLoaderCallback, bannerParameters, customParameters); + @Nullable FlutterAdLoaderAd.CustomParameters customParameters, + @Nullable FlutterAdLoaderAd.NativeParameters nativeParameters) { + loadAdLoaderAd(adUnitId, adLoaderAdLoaderCallback, bannerParameters, customParameters, nativeParameters); + } + + static AdChoicesPlacement convertAdChoicesPlacement(int adChoicesPlacement) { + switch(adChoicesPlacement) { + case NativeAdOptions.ADCHOICES_BOTTOM_LEFT: + return AdChoicesPlacement.BOTTOM_LEFT; + case NativeAdOptions.ADCHOICES_BOTTOM_RIGHT: + return AdChoicesPlacement.BOTTOM_RIGHT; + case NativeAdOptions.ADCHOICES_TOP_LEFT: + return AdChoicesPlacement.TOP_LEFT; + case NativeAdOptions.ADCHOICES_TOP_RIGHT: + return AdChoicesPlacement.TOP_RIGHT; + default: + return AdChoicesPlacement.TOP_RIGHT; + } + } + + static NativeMediaAspectRatio convertMediaAspectRatio(int mediaAspectRatio) { + switch(mediaAspectRatio) { + case NativeAdOptions.NATIVE_MEDIA_ASPECT_RATIO_ANY: + return NativeMediaAspectRatio.ANY; + case NativeAdOptions.NATIVE_MEDIA_ASPECT_RATIO_LANDSCAPE: + return NativeMediaAspectRatio.LANDSCAPE; + case NativeAdOptions.NATIVE_MEDIA_ASPECT_RATIO_PORTRAIT: + return NativeMediaAspectRatio.PORTRAIT; + case NativeAdOptions.NATIVE_MEDIA_ASPECT_RATIO_SQUARE: + return NativeMediaAspectRatio.SQUARE; + case NativeAdOptions.NATIVE_MEDIA_ASPECT_RATIO_UNKNOWN: + return NativeMediaAspectRatio.UNKNOWN; + default: + return NativeMediaAspectRatio.UNKNOWN; + } + } + + static VideoOptions convertVideoOptions(com.google.android.gms.ads.VideoOptions videoOptions) { + return new VideoOptions.Builder() + .setClickToExpandRequested(videoOptions.getClickToExpandRequested()) + .setCustomControlsRequested(videoOptions.getCustomControlsRequested()) + .setStartMuted(videoOptions.getStartMuted()) + .build(); } } diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java index 5b92f38f5..1f5a0d874 100644 --- a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java @@ -4,11 +4,13 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.android.gms.ads.nativead.NativeAdOptions; import com.google.android.libraries.ads.mobile.sdk.banner.AdSize; import com.google.android.libraries.ads.mobile.sdk.banner.AdView; import com.google.android.libraries.ads.mobile.sdk.banner.BannerAd; import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; import com.google.android.libraries.ads.mobile.sdk.nativead.CustomNativeAd; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAd; import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdLoaderCallback; import io.flutter.plugin.platform.PlatformView; import java.util.List; @@ -27,6 +29,7 @@ class FlutterAdLoaderAd extends FlutterAd implements FlutterAdLoadedListener, Na @Nullable private View view; @Nullable protected BannerParameters bannerParameters; @Nullable protected CustomParameters customParameters; + @Nullable protected NativeParameters nativeParameters; static class Builder { @Nullable private AdInstanceManager manager; @@ -38,6 +41,8 @@ static class Builder { @Nullable private FlutterBannerParameters bannerParameters; @Nullable private FlutterCustomParameters customParameters; @Nullable private Map customFactories; + @Nullable private FlutterNativeParameters nativeParameters; + @Nullable private Map nativeFactories; public Builder setId(int id) { this.id = id; @@ -85,6 +90,17 @@ public Builder withAvailableCustomFactories( return this; } + public Builder setNative(@Nullable FlutterNativeParameters nativeParameters) { + this.nativeParameters = nativeParameters; + return this; + } + + public Builder withAvailableNativeFactories( + @NonNull Map nativeFactories) { + this.nativeFactories = nativeFactories; + return this; + } + FlutterAdLoaderAd build() { if (manager == null) { throw new IllegalStateException("manager must be provided"); @@ -114,6 +130,10 @@ FlutterAdLoaderAd build() { adLoaderAd.customParameters = customParameters.asCustomParameters(customFactories); } + if (nativeParameters != null) { + adLoaderAd.nativeParameters = nativeParameters.asNativeParameters(nativeFactories); + } + return adLoaderAd; } } @@ -142,6 +162,21 @@ static class CustomParameters { } } + static class NativeParameters { + @NonNull final NativeAdFactory factory; + @Nullable final NativeAdOptions nativeAdOptions; + @Nullable final Map viewOptions; + + NativeParameters( + @NonNull NativeAdFactory factory, + @Nullable NativeAdOptions nativeAdOptions, + @Nullable Map viewOptions) { + this.factory = factory; + this.nativeAdOptions = nativeAdOptions; + this.viewOptions = viewOptions; + } + } + protected FlutterAdLoaderAd( int adId, @NonNull AdInstanceManager manager, @@ -182,10 +217,11 @@ void load() { // Note we delegate loading the ad to FlutterAdLoader mainly for testing purposes. // As of 20.0.0 of GMA, mockito is unable to mock AdLoader. if (request != null) { - adLoader.loadAdLoaderAd(adUnitId, loadedListener, bannerParameters, customParameters); + adLoader.loadAdLoaderAd( + adUnitId, loadedListener, bannerParameters, customParameters, nativeParameters); } else if (adManagerRequest != null) { adLoader.loadAdManagerAdLoaderAd( - adUnitId, loadedListener, bannerParameters, customParameters); + adUnitId, loadedListener, bannerParameters, customParameters, nativeParameters); } else { Log.e(TAG, "A null or invalid ad request was provided."); } @@ -254,6 +290,16 @@ public void onCustomNativeAdLoaded(@NonNull CustomNativeAd customNativeAd) { manager.onAdLoaded(adId, customNativeAd.getResponseInfo()); } + @Override + public void onNativeAdLoaded(@NonNull NativeAd nativeAd) { + view = nativeParameters.factory.createNativeAd(nativeAd, nativeParameters.viewOptions); + type = AdLoaderAdType.NATIVE; + + nativeAd.setAdEventCallback(new FlutterNativeAdListener(adId, manager)); + + manager.onAdLoaded(adId, nativeAd.getResponseInfo()); + } + @Override void dispose() { if (view == null) { diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterNativeParameters.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterNativeParameters.java new file mode 100644 index 000000000..50abe3ea0 --- /dev/null +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterNativeParameters.java @@ -0,0 +1,32 @@ +package io.flutter.plugins.googlemobileads; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.ads.nativead.NativeAdOptions; +import io.flutter.plugins.googlemobileads.NativeAdFactory; +import java.util.Map; + +class FlutterNativeParameters { + @NonNull final String factoryId; + @Nullable final FlutterNativeAdOptions nativeAdOptions; + @Nullable final Map viewOptions; + + FlutterNativeParameters( + @NonNull String factoryId, + @Nullable FlutterNativeAdOptions nativeAdOptions, + @Nullable Map viewOptions) { + this.factoryId = factoryId; + this.nativeAdOptions = nativeAdOptions; + this.viewOptions = viewOptions; + } + + FlutterAdLoaderAd.NativeParameters asNativeParameters( + @NonNull Map registeredFactories) { + NativeAdOptions nativeAdOptions = null; + if (this.nativeAdOptions != null) { + nativeAdOptions = this.nativeAdOptions.asNativeAdOptions(); + } + return new FlutterAdLoaderAd.NativeParameters( + registeredFactories.get(factoryId), nativeAdOptions, viewOptions); + } +} diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdLoaderAdType.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdLoaderAdType.java index 84022879e..799f0c3af 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdLoaderAdType.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdLoaderAdType.java @@ -4,4 +4,5 @@ enum AdLoaderAdType { UNKNOWN, BANNER, CUSTOM, + NATIVE, } 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 753e0b783..92f248347 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 @@ -70,6 +70,7 @@ class AdMessageCodec extends StandardMessageCodec { private static final byte VALUE_AD_MANAGER_AD_VIEW_OPTIONS = (byte) 155; private static final byte VALUE_BANNER_PARAMETERS = (byte) 156; private static final byte VALUE_CUSTOM_PARAMETERS = (byte) 157; + private static final byte VALUE_NATIVE_PARAMETERS = (byte) 158; @NonNull Context context; @NonNull final FlutterAdSize.AdSizeFactory adSizeFactory; @@ -274,6 +275,12 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { FlutterCustomParameters customParameters = (FlutterCustomParameters) value; writeValue(stream, customParameters.formatIds); writeValue(stream, customParameters.viewOptions); + } else if (value instanceof FlutterNativeParameters) { + stream.write(VALUE_NATIVE_PARAMETERS); + FlutterNativeParameters nativeParameters = (FlutterNativeParameters) value; + writeValue(stream, nativeParameters.factoryId); + writeValue(stream, nativeParameters.nativeAdOptions); + writeValue(stream, nativeParameters.viewOptions); } else { super.writeValue(stream, value); } @@ -470,6 +477,11 @@ protected Object readValueOfType(byte type, ByteBuffer buffer) { return new FlutterCustomParameters( (List) readValueOfType(buffer.get(), buffer), (Map) readValueOfType(buffer.get(), buffer)); + case VALUE_NATIVE_PARAMETERS: + return new FlutterNativeParameters( + (String) readValueOfType(buffer.get(), buffer), + (FlutterNativeAdOptions) 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/GoogleMobileAdsPlugin.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java index e7e0de5ba..4f1ccf903 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 @@ -451,6 +451,17 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) } } + final FlutterNativeParameters nativeParameters = + call.argument("native"); + if (nativeParameters != null) { + if (nativeAdFactories.get(nativeParameters.factoryId) == null) { + final String message = + String.format("Can't find NativeAdFactory with id: %s", nativeParameters.factoryId); + result.error("AdLoaderAdError", message, null); + return; + } + } + final FlutterAdLoaderAd adLoaderAd = new FlutterAdLoaderAd.Builder() .setManager(instanceManager) @@ -465,6 +476,8 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) .setBanner(call.argument("banner")) .setCustom(customParameters) .withAvailableCustomFactories(customAdFactories) + .setNative(nativeParameters) + .withAvailableNativeFactories(nativeAdFactories) .build(); instanceManager.trackAd(adLoaderAd, call.argument("adId")); adLoaderAd.load(); diff --git a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java index 7efbaccb0..3fcc804e5 100644 --- a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java @@ -145,7 +145,8 @@ public void loadAdLoaderAd( @NonNull AdListener adListener, @NonNull AdRequest request, @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters, - @Nullable FlutterAdLoaderAd.CustomParameters customParameters) { + @Nullable FlutterAdLoaderAd.CustomParameters customParameters, + @Nullable FlutterAdLoaderAd.NativeParameters nativeParameters) { AdLoader.Builder builder = new AdLoader.Builder(context, adUnitId); if (bannerParameters != null) { builder = builder.forAdManagerAdView(bannerParameters.listener, bannerParameters.adSizes); @@ -158,6 +159,12 @@ public void loadAdLoaderAd( builder = builder.forCustomFormatAd(formatId, customParameters.listener, null); } } + if (nativeParameters != null) { + builder = builder.forNativeAd(nativeParameters.listener); + if (nativeParameters.nativeAdOptions != null) { + builder = builder.withNativeAdOptions(nativeParameters.nativeAdOptions); + } + } builder.withAdListener(adListener).build().loadAd(request); } @@ -167,7 +174,8 @@ public void loadAdManagerAdLoaderAd( @NonNull AdListener adListener, @NonNull AdManagerAdRequest adManagerAdRequest, @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters, - @Nullable FlutterAdLoaderAd.CustomParameters customParameters) { + @Nullable FlutterAdLoaderAd.CustomParameters customParameters, + @Nullable FlutterAdLoaderAd.NativeParameters nativeParameters) { AdLoader.Builder builder = new AdLoader.Builder(context, adUnitId); if (bannerParameters != null) { builder = builder.forAdManagerAdView(bannerParameters.listener, bannerParameters.adSizes); @@ -180,6 +188,12 @@ public void loadAdManagerAdLoaderAd( builder = builder.forCustomFormatAd(formatId, customParameters.listener, null); } } + if (nativeParameters != null) { + builder = builder.forNativeAd(nativeParameters.listener); + if (nativeParameters.nativeAdOptions != null) { + builder = builder.withNativeAdOptions(nativeParameters.nativeAdOptions); + } + } builder.withAdListener(adListener).build().loadAd(adManagerAdRequest); } } diff --git a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java index 7a944dc8d..4f036c88d 100644 --- a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java @@ -24,6 +24,9 @@ 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.NativeAd; +import com.google.android.gms.ads.nativead.NativeAd.OnNativeAdLoadedListener; +import com.google.android.gms.ads.nativead.NativeAdOptions; import com.google.android.gms.ads.nativead.NativeCustomFormatAd; import com.google.android.gms.ads.nativead.NativeCustomFormatAd.OnCustomFormatAdLoadedListener; import io.flutter.plugin.platform.PlatformView; @@ -34,7 +37,9 @@ * instances served for a single {@link AdRequest} or {@link AdManagerAdRequest} */ class FlutterAdLoaderAd extends FlutterAd - implements OnAdManagerAdViewLoadedListener, OnCustomFormatAdLoadedListener { + implements OnAdManagerAdViewLoadedListener, + OnCustomFormatAdLoadedListener, + OnNativeAdLoadedListener { private static final String TAG = "FlutterAdLoaderAd"; @NonNull private final AdInstanceManager manager; @@ -47,6 +52,7 @@ class FlutterAdLoaderAd extends FlutterAd @Nullable private View view; @Nullable protected BannerParameters bannerParameters; @Nullable protected CustomParameters customParameters; + @Nullable protected NativeParameters nativeParameters; static class Builder { @Nullable private AdInstanceManager manager; @@ -58,6 +64,8 @@ static class Builder { @Nullable private FlutterBannerParameters bannerParameters; @Nullable private FlutterCustomParameters customParameters; @Nullable private Map customFactories; + @Nullable private FlutterNativeParameters nativeParameters; + @Nullable private Map nativeFactories; public Builder setId(int id) { this.id = id; @@ -105,6 +113,17 @@ public Builder withAvailableCustomFactories( return this; } + public Builder setNative(@Nullable FlutterNativeParameters nativeParameters) { + this.nativeParameters = nativeParameters; + return this; + } + + public Builder withAvailableNativeFactories( + @NonNull Map nativeFactories) { + this.nativeFactories = nativeFactories; + return this; + } + FlutterAdLoaderAd build() { if (manager == null) { throw new IllegalStateException("manager must be provided"); @@ -138,6 +157,12 @@ FlutterAdLoaderAd build() { new FlutterCustomFormatAdLoadedListener(adLoaderAd), customFactories); } + if (nativeParameters != null) { + adLoaderAd.nativeParameters = + nativeParameters.asNativeParameters( + new FlutterNativeAdLoadedListener(adLoaderAd), nativeFactories); + } + return adLoaderAd; } } @@ -172,6 +197,24 @@ static class CustomParameters { } } + static class NativeParameters { + @NonNull final OnNativeAdLoadedListener listener; + @NonNull final NativeAdFactory factory; + @Nullable final NativeAdOptions nativeAdOptions; + @Nullable final Map viewOptions; + + NativeParameters( + @NonNull OnNativeAdLoadedListener listener, + @NonNull NativeAdFactory factory, + @Nullable NativeAdOptions nativeAdOptions, + @Nullable Map viewOptions) { + this.listener = listener; + this.factory = factory; + this.nativeAdOptions = nativeAdOptions; + this.viewOptions = viewOptions; + } + } + protected FlutterAdLoaderAd( int adId, @NonNull AdInstanceManager manager, @@ -213,7 +256,12 @@ 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, customParameters); + adUnitId, + adListener, + request.asAdRequest(adUnitId), + bannerParameters, + customParameters, + nativeParameters); return; } @@ -223,7 +271,8 @@ void load() { adListener, adManagerRequest.asAdManagerAdRequest(adUnitId), bannerParameters, - customParameters); + customParameters, + nativeParameters); return; } @@ -274,6 +323,14 @@ public void onCustomFormatAdLoaded(@NonNull NativeCustomFormatAd ad) { manager.onAdLoaded(adId, null); } + @Override + public void onNativeAdLoaded(@NonNull NativeAd ad) { + view = nativeParameters.factory.createNativeAd(ad, nativeParameters.viewOptions); + type = AdLoaderAdType.NATIVE; + ad.setOnPaidEventListener(new FlutterPaidEventListener(manager, this)); + manager.onAdLoaded(adId, ad.getResponseInfo()); + } + @Override void dispose() { if (view == null) { diff --git a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java index 3142d7d57..de0792ab3 100644 --- a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java @@ -20,10 +20,10 @@ class FlutterCustomParameters { FlutterAdLoaderAd.CustomParameters asCustomParameters( @NonNull OnCustomFormatAdLoadedListener listener, - @NonNull Map availableFactories) { + @NonNull Map registeredFactories) { Map factories = new HashMap<>(); for (String formatId : formatIds) { - factories.put(formatId, availableFactories.get(formatId)); + factories.put(formatId, registeredFactories.get(formatId)); } return new FlutterAdLoaderAd.CustomParameters(listener, factories, viewOptions); } diff --git a/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterNativeParameters.java b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterNativeParameters.java new file mode 100644 index 000000000..d1c144de2 --- /dev/null +++ b/packages/google_mobile_ads/android/src/playServices/java/io/flutter/plugins/googlemobileads/FlutterNativeParameters.java @@ -0,0 +1,47 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io.flutter.plugins.googlemobileads; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.ads.nativead.NativeAd.OnNativeAdLoadedListener; +import com.google.android.gms.ads.nativead.NativeAdOptions; +import java.util.Map; + +class FlutterNativeParameters { + @NonNull final String factoryId; + @Nullable final FlutterNativeAdOptions nativeAdOptions; + @Nullable final Map viewOptions; + + FlutterNativeParameters( + @NonNull String factoryId, + @Nullable FlutterNativeAdOptions nativeAdOptions, + @Nullable Map viewOptions) { + this.factoryId = factoryId; + this.nativeAdOptions = nativeAdOptions; + this.viewOptions = viewOptions; + } + + FlutterAdLoaderAd.NativeParameters asNativeParameters( + @NonNull OnNativeAdLoadedListener listener, + @NonNull Map registeredFactories) { + NativeAdOptions nativeAdOptions = null; + if (this.nativeAdOptions != null) { + nativeAdOptions = this.nativeAdOptions.asNativeAdOptions(); + } + return new FlutterAdLoaderAd.NativeParameters( + listener, registeredFactories.get(factoryId), nativeAdOptions, viewOptions); + } +} 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 9e5d73ee6..6bb9b3663 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 @@ -597,6 +597,29 @@ public void encodeCustomParameters() { assertEquals(result.viewOptions.size(), 1); assertEquals(result.viewOptions.get("key"), "value"); } + + @Test + public void encodeNativeParameters() { + final ByteBuffer data = + codec.encodeMessage( + new FlutterNativeParameters( + "factory-id", + new FlutterNativeAdOptions(1, 1, null, true, true, true), + Collections.singletonMap("key", "value"))); + + final FlutterNativeParameters result = + (FlutterNativeParameters) codec.decodeMessage((ByteBuffer) data.position(0)); + + assertEquals(result.factoryId, "factory-id"); + assertEquals(result.nativeAdOptions.adChoicesPlacement, Integer.valueOf(1)); + assertEquals(result.nativeAdOptions.mediaAspectRatio, Integer.valueOf(1)); + assertNull(result.nativeAdOptions.videoOptions); + assertTrue(result.nativeAdOptions.requestCustomMuteThisAd); + assertTrue(result.nativeAdOptions.shouldRequestMultipleImages); + assertTrue(result.nativeAdOptions.shouldReturnUrlsForImageAssets); + assertEquals(result.viewOptions.size(), 1); + assertEquals(result.viewOptions.get("key"), "value"); + } } class DummyMediationExtras extends FlutterMediationExtras { 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 418a86a88..3a8ec903e 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 @@ -30,10 +30,12 @@ import com.google.android.gms.ads.AdListener; import com.google.android.gms.ads.AdRequest; import com.google.android.gms.ads.AdSize; +import com.google.android.gms.ads.AdValue; import com.google.android.gms.ads.LoadAdError; 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.NativeAd; import com.google.android.gms.ads.nativead.NativeCustomFormatAd; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.googlemobileads.FlutterAd.FlutterLoadAdError; @@ -41,6 +43,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; @@ -87,13 +90,13 @@ public Object answer(InvocationOnMock invocation) { }) .when(mockLoader) .loadAdManagerAdLoaderAd( - eq("testId"), any(AdListener.class), eq(mockRequest), isNull(), isNull()); + eq("testId"), any(AdListener.class), eq(mockRequest), isNull(), isNull(), isNull()); adLoaderAd.load(); verify(mockLoader) .loadAdManagerAdLoaderAd( - eq("testId"), any(AdListener.class), eq(mockRequest), isNull(), isNull()); + eq("testId"), any(AdListener.class), eq(mockRequest), isNull(), isNull(), isNull()); verify(testManager).onAdClicked(eq(1)); verify(testManager).onAdClosed(eq(1)); @@ -131,12 +134,14 @@ public Object answer(InvocationOnMock invocation) { } }) .when(mockLoader) - .loadAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest), isNull(), isNull()); + .loadAdLoaderAd( + eq("testId"), any(AdListener.class), eq(mockRequest), isNull(), isNull(), isNull()); adLoaderAd.load(); verify(mockLoader) - .loadAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest), isNull(), isNull()); + .loadAdLoaderAd( + eq("testId"), any(AdListener.class), eq(mockRequest), isNull(), isNull(), isNull()); verify(testManager).onAdClicked(eq(1)); verify(testManager).onAdClosed(eq(1)); @@ -188,7 +193,12 @@ public Object answer(InvocationOnMock invocation) { }) .when(mockLoader) .loadAdManagerAdLoaderAd( - eq("testId"), any(AdListener.class), eq(mockRequest), eq(bannerParameters), isNull()); + eq("testId"), + any(AdListener.class), + eq(mockRequest), + eq(bannerParameters), + isNull(), + isNull()); adLoaderAd.load(); @@ -202,7 +212,12 @@ public Object answer(InvocationOnMock invocation) { verify(mockLoader) .loadAdManagerAdLoaderAd( - eq("testId"), any(AdListener.class), eq(mockRequest), eq(bannerParameters), isNull()); + eq("testId"), + any(AdListener.class), + eq(mockRequest), + eq(bannerParameters), + isNull(), + isNull()); verify(testManager).onAdClicked(eq(1)); verify(testManager).onAdClosed(eq(1)); @@ -255,7 +270,12 @@ public Object answer(InvocationOnMock invocation) { }) .when(mockLoader) .loadAdManagerAdLoaderAd( - eq("testId"), any(AdListener.class), eq(mockRequest), isNull(), eq(customParameters)); + eq("testId"), + any(AdListener.class), + eq(mockRequest), + isNull(), + eq(customParameters), + isNull()); adLoaderAd.load(); @@ -265,7 +285,12 @@ public Object answer(InvocationOnMock invocation) { verify(mockLoader) .loadAdManagerAdLoaderAd( - eq("testId"), any(AdListener.class), eq(mockRequest), isNull(), eq(customParameters)); + eq("testId"), + any(AdListener.class), + eq(mockRequest), + isNull(), + eq(customParameters), + isNull()); verify(testManager).onAdClicked(eq(1)); verify(testManager).onAdClosed(eq(1)); @@ -276,6 +301,100 @@ public Object answer(InvocationOnMock invocation) { verify(testManager).onAdLoaded(eq(1), isNull()); } + @Test + public void loadAdLoaderAdNativeWithAdManagerAdRequest() { + 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 FlutterNativeAdLoadedListener listener = new FlutterNativeAdLoadedListener(adLoaderAd); + final NativeAdFactory mockNativeAdFactory = mock(NativeAdFactory.class); + final FlutterAdLoaderAd.NativeParameters nativeParameters = + new FlutterAdLoaderAd.NativeParameters(listener, mockNativeAdFactory, null, null); + adLoaderAd.nativeParameters = nativeParameters; + + final LoadAdError mockLoadAdError = mock(LoadAdError.class); + when(mockLoadAdError.getCode()).thenReturn(1); + when(mockLoadAdError.getDomain()).thenReturn("2"); + when(mockLoadAdError.getMessage()).thenReturn("3"); + + final ResponseInfo mockResponseInfo = mock(ResponseInfo.class); + final NativeAd mockNativeAd = mock(NativeAd.class); + when(mockNativeAd.getResponseInfo()).thenReturn(mockResponseInfo); + + 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.NativeParameters nativeParameters = invocation.getArgument(5); + nativeParameters.listener.onNativeAdLoaded(mockNativeAd); + return null; + } + }) + .when(mockLoader) + .loadAdManagerAdLoaderAd( + eq("testId"), + any(AdListener.class), + eq(mockRequest), + isNull(), + isNull(), + eq(nativeParameters)); + + final AdValue mockAdValue = mock(AdValue.class); + when(mockAdValue.getCurrencyCode()).thenReturn("Dollars"); + when(mockAdValue.getPrecisionType()).thenReturn(1); + when(mockAdValue.getValueMicros()).thenReturn(1000L); + + doAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) { + FlutterPaidEventListener listener = invocation.getArgument(0); + listener.onPaidEvent(mockAdValue); + return null; + } + }) + .when(mockNativeAd) + .setOnPaidEventListener(any(FlutterPaidEventListener.class)); + + adLoaderAd.load(); + + assertEquals(adLoaderAd.getAdLoaderAdType(), AdLoaderAdType.NATIVE); + + verify(mockLoader) + .loadAdManagerAdLoaderAd( + eq("testId"), + any(AdListener.class), + eq(mockRequest), + isNull(), + isNull(), + eq(nativeParameters)); + + 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), eq(mockResponseInfo)); + + final ArgumentCaptor adValueCaptor = + ArgumentCaptor.forClass(FlutterAdValue.class); + verify(testManager).onPaidEvent(eq(adLoaderAd), adValueCaptor.capture()); + assertEquals(adValueCaptor.getValue().currencyCode, "Dollars"); + assertEquals(adValueCaptor.getValue().precisionType, 1); + assertEquals(adValueCaptor.getValue().valueMicros, 1000L); + } + @Test(expected = IllegalStateException.class) public void adLoaderAdBuilderNullManager() { new FlutterAdLoaderAd.Builder() diff --git a/packages/google_mobile_ads/example/ios/RunnerTests/FLTAdLoaderAdTest.m b/packages/google_mobile_ads/example/ios/RunnerTests/FLTAdLoaderAdTest.m index 6ab7164e9..5fc1dffa5 100644 --- a/packages/google_mobile_ads/example/ios/RunnerTests/FLTAdLoaderAdTest.m +++ b/packages/google_mobile_ads/example/ios/RunnerTests/FLTAdLoaderAdTest.m @@ -31,7 +31,8 @@ - (void)testDelegates { rootViewController:viewController adId:@0 banner:nil - custom:nil]; + custom:nil + native:nil]; ad.manager = manager; @@ -60,7 +61,8 @@ - (void)testBannerDelegates { adId:@0 banner:[[FLTBannerParameters alloc] initWithSizes:@[ adSize ] options:nil] - custom:nil]; + custom:nil + native:nil]; ad.manager = manager; @@ -134,7 +136,8 @@ - (void)testCustomDelegates { rootViewController:viewController adId:@0 banner:nil - custom:custom]; + custom:custom + native:nil]; ad.manager = manager; @@ -179,6 +182,61 @@ - (void)testCustomDelegates { OCMVerify([manager onCustomNativeAdDidDismissScreen:[OCMArg isEqual:ad]]); } +- (void)testNativeDelegates { + UIViewController *viewController = OCMClassMock([UIViewController class]); + FLTAdInstanceManager *manager = OCMClassMock([FLTAdInstanceManager class]); + + FLTNativeParameters *native = + [[FLTNativeParameters alloc] initWithFactoryId:@"factoryId" + nativeAdOptions:nil + viewOptions:nil]; + id factory = + OCMProtocolMock(@protocol(FLTNativeAdFactory)); + native.factory = factory; + + FLTAdLoaderAd *ad = + [[FLTAdLoaderAd alloc] initWithAdUnitId:@"testAdUnitId" + request:[[FLTAdRequest alloc] init] + rootViewController:viewController + adId:@0 + banner:nil + custom:nil + native:native]; + + ad.manager = manager; + + [ad load]; + + // GADNativeAdLoaderDelegate + GADNativeAd *nativeAd = OCMClassMock([GADNativeAd class]); + + [ad adLoader:ad.adLoader didReceiveNativeAd:nativeAd]; + + XCTAssertEqual([ad adLoaderAdType], FLTAdLoaderAdTypeNative); + + OCMVerify([nativeAd setDelegate:[OCMArg isEqual:ad]]); + OCMVerify([factory createNativeAd:[OCMArg isEqual:nativeAd] + customOptions:[OCMArg isEqual:nil]]); + OCMVerify([manager onAdLoaded:[OCMArg isEqual:ad] + responseInfo:[OCMArg isEqual:nil]]); + + // GADNativeAdDelegate + [ad nativeAdDidRecordImpression:nativeAd]; + OCMVerify([manager onNativeAdImpression:[OCMArg isEqual:ad]]); + + [ad nativeAdDidRecordClick:nativeAd]; + OCMVerify([manager adDidRecordClick:[OCMArg isEqual:ad]]); + + [ad nativeAdWillPresentScreen:nativeAd]; + OCMVerify([manager onNativeAdWillPresentScreen:[OCMArg isEqual:ad]]); + + [ad nativeAdWillDismissScreen:nativeAd]; + OCMVerify([manager onNativeAdWillDismissScreen:[OCMArg isEqual:ad]]); + + [ad nativeAdDidDismissScreen:nativeAd]; + OCMVerify([manager onNativeAdDidDismissScreen:[OCMArg isEqual:ad]]); +} + - (void)testLoadAdLoaderAd { FLTAdRequest *request = [[FLTAdRequest alloc] init]; request.keywords = @[ @"apple" ]; @@ -199,7 +257,8 @@ - (void)testLoadAdLoaderAd:(FLTAdRequest *)request { rootViewController:viewController adId:@1 banner:nil - custom:nil]; + custom:nil + native:nil]; XCTAssertEqual(ad.adLoader.adUnitID, @"testAdUnitId"); XCTAssertEqual(ad.adLoader.delegate, ad); diff --git a/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m b/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m index b91359591..68ccb119c 100644 --- a/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m +++ b/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m @@ -863,6 +863,41 @@ - (void)testEncodeDecodeCustomParameters { XCTAssertEqual(factories.count, 0); } +- (void)testEncodeDecodeNativeParameters { + FLTNativeParameters *parameters = [[FLTNativeParameters alloc] + initWithFactoryId:@"factory-id" + nativeAdOptions:[[FLTNativeAdOptions alloc] + initWithAdChoicesPlacement:@(1) + mediaAspectRatio:@(1) + videoOptions:nil + requestCustomMuteThisAd:@YES + shouldRequestMultipleImages:@YES + shouldReturnUrlsForImageAssets:@YES] + viewOptions:@{@"key" : @"value"}]; + + NSData *encodedMessage = [_messageCodec encode:parameters]; + + FLTNativeParameters *decodedParameters = + [_messageCodec decode:encodedMessage]; + + XCTAssertEqualObjects(decodedParameters.factoryId, @"factory-id"); + + XCTAssertEqualObjects(parameters.nativeAdOptions.adChoicesPlacement, @(1)); + XCTAssertEqualObjects(parameters.nativeAdOptions.mediaAspectRatio, @(1)); + XCTAssertNil(parameters.nativeAdOptions.videoOptions); + XCTAssertEqualObjects(parameters.nativeAdOptions.requestCustomMuteThisAd, + @(YES)); + XCTAssertEqualObjects(parameters.nativeAdOptions.shouldRequestMultipleImages, + @(YES)); + XCTAssertEqualObjects( + parameters.nativeAdOptions.shouldReturnUrlsForImageAssets, @(YES)); + + NSDictionary *viewOptions = decodedParameters.viewOptions; + + XCTAssertNotNil(viewOptions); + XCTAssertEqualObjects(viewOptions[@"key"], @"value"); +} + @end @implementation FLTTestAdSizeFactory diff --git a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTAd_Internal.m b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTAd_Internal.m index e4e4f9e91..fa2a12deb 100644 --- a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTAd_Internal.m +++ b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTAd_Internal.m @@ -1256,6 +1256,19 @@ @implementation FLTCustomParameters } @end +@implementation FLTNativeParameters +- (nonnull instancetype) + initWithFactoryId:(nonnull NSString *)factoryId + nativeAdOptions:(nullable FLTNativeAdOptions *)nativeAdOptions + viewOptions:(nullable NSDictionary *)viewOptions { + self = [super init]; + _factoryId = factoryId; + _nativeAdOptions = nativeAdOptions; + _viewOptions = viewOptions; + return self; +} +@end + #pragma mark - FLTAdLoaderAd @implementation FLTAdLoaderAd { @@ -1265,6 +1278,7 @@ @implementation FLTAdLoaderAd { UIView *_view; FLTBannerParameters *_banner; FLTCustomParameters *_custom; + FLTNativeParameters *_native; } - (nonnull instancetype) @@ -1273,7 +1287,8 @@ @implementation FLTAdLoaderAd { rootViewController:(nonnull UIViewController *)rootViewController adId:(nonnull NSNumber *)adId banner:(nullable FLTBannerParameters *)bannerParameters - custom:(nullable FLTCustomParameters *)customParameters { + custom:(nullable FLTCustomParameters *)customParameters + native:(nullable FLTNativeParameters *)nativeParameters { self = [super init]; if (self) { self.adId = adId; @@ -1306,6 +1321,17 @@ @implementation FLTAdLoaderAd { [adTypes addObject:GADAdLoaderAdTypeCustomNative]; } + if (![FLTAdUtil isNull:nativeParameters]) { + _native = nativeParameters; + + [adTypes addObject:GADAdLoaderAdTypeNative]; + + if (![FLTAdUtil isNull:_native.nativeAdOptions]) { + [options + addObjectsFromArray:_native.nativeAdOptions.asGADAdLoaderOptions]; + } + } + _adLoader = [[GADAdLoader alloc] initWithAdUnitID:_adUnitId rootViewController:rootViewController adTypes:adTypes @@ -1459,6 +1485,54 @@ - (void)customNativeAdDidDismissScreen:(nonnull GADCustomNativeAd *)nativeAd { [manager onCustomNativeAdDidDismissScreen:self]; } +#pragma mark - GADNativeAdLoaderDelegate + +- (void)adLoader:(nonnull GADAdLoader *)adLoader + didReceiveNativeAd:(nonnull GADNativeAd *)nativeAd { + // Use Nil instead of Null to fix crash with Swift integrations. + NSDictionary *customOptions = + [[NSNull null] isEqual:_native.viewOptions] ? nil : _native.viewOptions; + _adLoaderAdType = FLTAdLoaderAdTypeNative; + _view = [_native.factory createNativeAd:nativeAd customOptions:customOptions]; + nativeAd.delegate = self; + + __weak FLTAdLoaderAd *weakSelf = self; + nativeAd.paidEventHandler = ^(GADAdValue *_Nonnull value) { + if (weakSelf.manager == nil) { + return; + } + [weakSelf.manager + onPaidEvent:weakSelf + value:[[FLTAdValue alloc] initWithValue:value.value + precision:(NSInteger)value.precision + currencyCode:value.currencyCode]]; + }; + + [manager onAdLoaded:self responseInfo:nativeAd.responseInfo]; +} + +#pragma mark - GADNativeAdDelegate + +- (void)nativeAdDidRecordImpression:(nonnull GADNativeAd *)nativeAd { + [manager onNativeAdImpression:self]; +} + +- (void)nativeAdDidRecordClick:(nonnull GADNativeAd *)nativeAd { + [manager adDidRecordClick:self]; +} + +- (void)nativeAdWillPresentScreen:(nonnull GADNativeAd *)nativeAd { + [manager onNativeAdWillPresentScreen:self]; +} + +- (void)nativeAdWillDismissScreen:(nonnull GADNativeAd *)nativeAd { + [manager onNativeAdWillDismissScreen:self]; +} + +- (void)nativeAdDidDismissScreen:(nonnull GADNativeAd *)nativeAd { + [manager onNativeAdDidDismissScreen:self]; +} + #pragma mark - FlutterPlatformView - (nonnull UIView *)view { diff --git a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsPlugin.m b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsPlugin.m index b84ecead3..f6dbe71e2 100644 --- a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsPlugin.m +++ b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsPlugin.m @@ -504,6 +504,22 @@ - (void)handleMethodCall:(FlutterMethodCall *)call } } + FLTNativeParameters *native = call.arguments[@"native"]; + if ([FLTAdUtil isNotNull:native]) { + id factory = _nativeAdFactories[native.factoryId]; + if (!factory) { + NSString *message = [NSString + stringWithFormat:@"Can't find NativeAdFactory with id: %@", + native.factoryId]; + result([FlutterError errorWithCode:@"AdLoaderAdError" + message:message + details:nil]); + return; + } + + native.factory = factory; + } + FLTAdRequest *request; if ([FLTAdUtil isNotNull:call.arguments[@"request"]]) { request = call.arguments[@"request"]; @@ -517,7 +533,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call rootViewController:rootController adId:call.arguments[@"adId"] banner:call.arguments[@"banner"] - custom:custom]; + custom:custom + native:native]; [_manager loadAd:ad]; result(nil); } else if ([call.method isEqualToString:@"loadInterstitialAd"]) { diff --git a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsReaderWriter_Internal.m b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsReaderWriter_Internal.m index a400dc723..d6e68dc11 100644 --- a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsReaderWriter_Internal.m +++ b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/FLTGoogleMobileAdsReaderWriter_Internal.m @@ -51,6 +51,7 @@ typedef NS_ENUM(NSInteger, FLTAdMobField) { FLTAdmobFieldAdManagerAdViewOptions = 155, FLTAdmobBannerParameters = 156, FLTAdmobCustomParameters = 157, + FLTAdmobNativeParameters = 158, }; @interface FLTGoogleMobileAdsWriter : FlutterStandardWriter @@ -359,6 +360,12 @@ - (id _Nullable)readValueOfType:(UInt8)type { initWithFormatIds:[self readValueOfType:[self readByte]] viewOptions:[self readValueOfType:[self readByte]]]; } + case FLTAdmobNativeParameters: { + return [[FLTNativeParameters alloc] + initWithFactoryId:[self readValueOfType:[self readByte]] + nativeAdOptions:[self readValueOfType:[self readByte]] + viewOptions:[self readValueOfType:[self readByte]]]; + } } return [super readValueOfType:type]; } @@ -556,6 +563,12 @@ - (void)writeValue:(id)value { FLTCustomParameters *customParameters = value; [self writeValue:customParameters.formatIds]; [self writeValue:customParameters.viewOptions]; + } else if ([value isKindOfClass:[FLTNativeParameters class]]) { + [self writeByte:FLTAdmobNativeParameters]; + FLTNativeParameters *nativeParameters = value; + [self writeValue:nativeParameters.factoryId]; + [self writeValue:nativeParameters.nativeAdOptions]; + [self writeValue:nativeParameters.viewOptions]; } else { [super writeValue:value]; } diff --git a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/FLTAd_Internal.h b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/FLTAd_Internal.h index aa3f12be6..5d1916e9b 100644 --- a/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/FLTAd_Internal.h +++ b/packages/google_mobile_ads/ios/google_mobile_ads/Sources/google_mobile_ads/include/google_mobile_ads/FLTAd_Internal.h @@ -316,6 +316,7 @@ typedef NS_ENUM(NSInteger, FLTAdLoaderAdType) { FLTAdLoaderAdTypeUnknown = 0, FLTAdLoaderAdTypeBanner = 1, FLTAdLoaderAdTypeCustom = 2, + FLTAdLoaderAdTypeNative = 3, }; @interface FLTBannerParameters : NSObject @@ -335,11 +336,23 @@ typedef NS_ENUM(NSInteger, FLTAdLoaderAdType) { viewOptions:(NSDictionary *_Nullable)viewOptions; @end +@interface FLTNativeParameters : NSObject +@property(readonly, nonnull) NSString *factoryId; +@property(readonly, nullable) FLTNativeAdOptions *nativeAdOptions; +@property(readonly, nullable) NSDictionary *viewOptions; +@property(nullable) id factory; +- (nonnull instancetype) + initWithFactoryId:(nonnull NSString *)factoryId + nativeAdOptions:(nullable FLTNativeAdOptions *)nativeAdOptions + viewOptions:(nullable NSDictionary *)viewOptions; +@end + @interface FLTAdLoaderAd : FLTBaseAd + GADCustomNativeAdDelegate, GADNativeAdLoaderDelegate, + GADNativeAdDelegate> @property(readonly, nonnull) GADAdLoader *adLoader; @property(readonly) FLTAdLoaderAdType adLoaderAdType; @property(readonly, nullable) FLTAdSize *adSize; @@ -350,7 +363,8 @@ typedef NS_ENUM(NSInteger, FLTAdLoaderAdType) { rootViewController:(nonnull UIViewController *)rootViewController adId:(nonnull NSNumber *)adId banner:(nullable FLTBannerParameters *)bannerParameters - custom:(nullable FLTCustomParameters *)customParameters; + custom:(nullable FLTCustomParameters *)customParameters + native:(nullable FLTNativeParameters *)nativeParameters; @end @interface FLTRewardItem : NSObject diff --git a/packages/google_mobile_ads/lib/src/ad_containers.dart b/packages/google_mobile_ads/lib/src/ad_containers.dart index a1c6c8ffb..fa37f249c 100644 --- a/packages/google_mobile_ads/lib/src/ad_containers.dart +++ b/packages/google_mobile_ads/lib/src/ad_containers.dart @@ -1155,6 +1155,9 @@ enum AdLoaderAdType { /// Custom ad type custom, + + /// Native ad type + native, } /// An AdLoaderAd. @@ -1192,6 +1195,7 @@ class AdLoaderAd extends AdWithView { required AdRequest request, this.banner, this.custom, + this.native, }) : super(adUnitId: adUnitId, listener: listener) { if (request is AdManagerAdRequest) { adManagerRequest = request; @@ -1216,6 +1220,9 @@ class AdLoaderAd extends AdWithView { /// Optional parameters used to configure served "custom" ads final CustomParameters? custom; + /// Optional parameters used to configure served "native" ads + final NativeParameters? native; + @override Future load() => instanceManager.loadAdLoaderAd(this); @@ -1708,6 +1715,37 @@ class CustomParameters { } } +/// Central configuration item for native view requests served by an +/// [AdLoaderAd]. +class NativeParameters { + /// An identifier for the factory that creates the Platform view. + final String factoryId; + + /// Options to configure the native ad request. + final NativeAdOptions? nativeAdOptions; + + /// Optional options used to create the Platform view. + /// + /// These options are passed to the platform's `NativeAdFactory`. + final Map? viewOptions; + + /// Construct a [NativeParameters] instance, used by an [AdLoaderAd] to + /// configure native views. + NativeParameters({ + required this.factoryId, + this.nativeAdOptions, + this.viewOptions, + }); + + @override + bool operator ==(other) { + return other is NativeParameters && + factoryId == other.factoryId && + nativeAdOptions == other.nativeAdOptions && + 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 b8aae9bc7..9c5471b58 100644 --- a/packages/google_mobile_ads/lib/src/ad_instance_manager.dart +++ b/packages/google_mobile_ads/lib/src/ad_instance_manager.dart @@ -517,6 +517,8 @@ class AdInstanceManager { return AdLoaderAdType.banner; case 2: return AdLoaderAdType.custom; + case 3: + return AdLoaderAdType.native; default: debugPrint('Error: unknown AdLoaderAdType value: $adLoaderAdType'); return AdLoaderAdType.unknown; @@ -529,6 +531,8 @@ class AdInstanceManager { return AdLoaderAdType.banner; case 2: return AdLoaderAdType.custom; + case 3: + return AdLoaderAdType.native; default: debugPrint('Error: unknown AdLoaderAdType value: $adLoaderAdType'); return AdLoaderAdType.unknown; @@ -631,6 +635,7 @@ class AdInstanceManager { 'adManagerRequest': ad.adManagerRequest, 'banner': ad.banner, 'custom': ad.custom, + 'native': ad.native, }); } @@ -945,6 +950,7 @@ class AdMessageCodec extends StandardMessageCodec { static const int _valueAdManagerAdViewOptions = 155; static const int _valueBannerParameters = 156; static const int _valueCustomParameters = 157; + static const int _valueNativeParameters = 158; @override void writeValue(WriteBuffer buffer, dynamic value) { @@ -1092,6 +1098,11 @@ class AdMessageCodec extends StandardMessageCodec { buffer.putUint8(_valueCustomParameters); writeValue(buffer, value.formatIds); writeValue(buffer, value.viewOptions); + } else if (value is NativeParameters) { + buffer.putUint8(_valueNativeParameters); + writeValue(buffer, value.factoryId); + writeValue(buffer, value.nativeAdOptions); + writeValue(buffer, value.viewOptions); } else { super.writeValue(buffer, value); } @@ -1375,6 +1386,15 @@ class AdMessageCodec extends StandardMessageCodec { buffer, )?.cast(), ); + case _valueNativeParameters: + return NativeParameters( + factoryId: readValueOfType(buffer.getUint8(), buffer), + nativeAdOptions: readValueOfType(buffer.getUint8(), buffer), + 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 57ce53cad..5cde4e6fd 100644 --- a/packages/google_mobile_ads/test/ad_loader_ad_test.dart +++ b/packages/google_mobile_ads/test/ad_loader_ad_test.dart @@ -73,6 +73,7 @@ void main() { 'adManagerRequest': null, 'banner': null, 'custom': null, + 'native': null, }, ), ]); @@ -100,6 +101,7 @@ void main() { 'adManagerRequest': request, 'banner': null, 'custom': null, + 'native': null, }, ), ]); @@ -127,6 +129,7 @@ void main() { 'adManagerRequest': null, 'banner': banner, 'custom': null, + 'native': null, }, ), ]); @@ -156,6 +159,37 @@ void main() { 'adManagerRequest': null, 'banner': null, 'custom': custom, + 'native': null, + }, + ), + ]); + + expect(instanceManager.adFor(0), isNotNull); + }); + + test('load with $NativeParameters', () async { + final NativeParameters native = NativeParameters( + factoryId: 'test-factory-id', + ); + final AdLoaderAd adLoaderAd = AdLoaderAd( + adUnitId: 'test-ad-unit', + listener: AdLoaderAdListener(), + request: AdRequest(), + native: native, + ); + + await adLoaderAd.load(); + expect(log, [ + isMethodCall( + 'loadAdLoaderAd', + arguments: { + 'adId': 0, + 'adUnitId': 'test-ad-unit', + 'request': adLoaderAd.request, + 'adManagerRequest': null, + 'banner': null, + 'custom': null, + 'native': native, }, ), ]); diff --git a/packages/google_mobile_ads/test/mobile_ads_test.dart b/packages/google_mobile_ads/test/mobile_ads_test.dart index 949b98212..c99bfd69f 100644 --- a/packages/google_mobile_ads/test/mobile_ads_test.dart +++ b/packages/google_mobile_ads/test/mobile_ads_test.dart @@ -717,5 +717,37 @@ void main() { expect(result.viewOptions, {'key': 'value'}); } }); + + test('encode/decode minimal $NativeParameters', () { + for (final platform in [TargetPlatform.android, TargetPlatform.iOS]) { + debugDefaultTargetPlatformOverride = platform; + ByteData byteData = codec.encodeMessage( + NativeParameters(factoryId: 'test-factory-id'), + )!; + + NativeParameters result = codec.decodeMessage(byteData); + expect(result.factoryId, 'test-factory-id'); + expect(result.nativeAdOptions, null); + expect(result.viewOptions, null); + } + }); + + test('encode/decode $NativeParameters', () { + for (final platform in [TargetPlatform.android, TargetPlatform.iOS]) { + debugDefaultTargetPlatformOverride = platform; + ByteData byteData = codec.encodeMessage( + NativeParameters( + factoryId: 'test-factory-id', + nativeAdOptions: NativeAdOptions(), + viewOptions: {'key': 'value'}, + ), + )!; + + NativeParameters result = codec.decodeMessage(byteData); + expect(result.factoryId, 'test-factory-id'); + expect(result.nativeAdOptions, NativeAdOptions()); + expect(result.viewOptions, {'key': 'value'}); + } + }); }); } From 2abb74310d13c7e3238301dd8cde38581543cbb7 Mon Sep 17 00:00:00 2001 From: Felipe Fuzaro Date: Tue, 1 Sep 2026 13:38:36 +0200 Subject: [PATCH 4/6] fix: request-forward on `AdLoaderAd` --- .../googlemobileads/FlutterAdLoader.java | 6 +++++- .../googlemobileads/FlutterAdLoaderAd.java | 4 ++-- .../FlutterAdManagerAdRequest.java | 21 +++++++++++++++++++ .../googlemobileads/FlutterAdRequest.java | 19 +++++++++++++++++ 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java index 4f24c2cb2..4e796f3dd 100644 --- a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java @@ -127,6 +127,7 @@ public void loadAdManagerNativeAd( /** Load an ad loader ad. */ public void loadAdLoaderAd( @NonNull String adUnitId, + @NonNull FlutterAdRequest request, @NonNull NativeAdLoaderCallback adLoaderAdLoaderCallback, @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters, @Nullable FlutterAdLoaderAd.CustomParameters customParameters, @@ -167,6 +168,8 @@ public void loadAdLoaderAd( } } + request.configureNativeAdRequestBuilder(builder, adUnitId); + NativeAdLoader.load( builder.build(), adLoaderAdLoaderCallback); @@ -175,11 +178,12 @@ public void loadAdLoaderAd( /** Load an ad manager ad loader ad. */ public void loadAdManagerAdLoaderAd( @NonNull String adUnitId, + @NonNull FlutterAdManagerAdRequest adManagerRequest, @NonNull NativeAdLoaderCallback adLoaderAdLoaderCallback, @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters, @Nullable FlutterAdLoaderAd.CustomParameters customParameters, @Nullable FlutterAdLoaderAd.NativeParameters nativeParameters) { - loadAdLoaderAd(adUnitId, adLoaderAdLoaderCallback, bannerParameters, customParameters, nativeParameters); + loadAdLoaderAd(adUnitId, adManagerRequest, adLoaderAdLoaderCallback, bannerParameters, customParameters, nativeParameters); } static AdChoicesPlacement convertAdChoicesPlacement(int adChoicesPlacement) { diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java index 1f5a0d874..dcb6e3248 100644 --- a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java @@ -218,10 +218,10 @@ void load() { // As of 20.0.0 of GMA, mockito is unable to mock AdLoader. if (request != null) { adLoader.loadAdLoaderAd( - adUnitId, loadedListener, bannerParameters, customParameters, nativeParameters); + adUnitId, request, loadedListener, bannerParameters, customParameters, nativeParameters); } else if (adManagerRequest != null) { adLoader.loadAdManagerAdLoaderAd( - adUnitId, loadedListener, bannerParameters, customParameters, nativeParameters); + adUnitId, adManagerRequest, loadedListener, bannerParameters, customParameters, nativeParameters); } else { Log.e(TAG, "A null or invalid ad request was provided."); } diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdRequest.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdRequest.java index da53ddd1a..f34c578a5 100644 --- a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdRequest.java +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdRequest.java @@ -19,6 +19,7 @@ import com.google.android.libraries.ads.mobile.sdk.banner.AdSize; 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.nativead.NativeAdRequest; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.List; import java.util.Map; @@ -149,6 +150,26 @@ protected BannerAdRequest.Builder toBannerAdRequestBuilder(String adUnitId, return builder; } + @Override + protected NativeAdRequest.Builder configureNativeAdRequestBuilder( + NativeAdRequest.Builder builder, String adUnitId) { + super.configureNativeAdRequestBuilder(builder, adUnitId); + if (customTargeting != null) { + for (final Map.Entry entry : customTargeting.entrySet()) { + builder.putCustomTargeting(entry.getKey(), entry.getValue()); + } + } + if (customTargetingLists != null) { + for (final Map.Entry> entry : customTargetingLists.entrySet()) { + builder.putCustomTargeting(entry.getKey(), entry.getValue()); + } + } + if (publisherProvidedId != null) { + builder.setPublisherProvidedId(publisherProvidedId); + } + return builder; + } + @Nullable protected Map getCustomTargeting() { return customTargeting; diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdRequest.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdRequest.java index 95ccf9b56..6fdda788a 100644 --- a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdRequest.java +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdRequest.java @@ -23,6 +23,7 @@ import com.google.android.libraries.ads.mobile.sdk.banner.AdSize; 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.nativead.NativeAdRequest; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.HashMap; import java.util.HashSet; @@ -324,6 +325,24 @@ protected BannerAdRequest.Builder toBannerAdRequestBuilder(String adUnitId, List return builder; } + /** Applies the common request properties onto a next-gen {@link NativeAdRequest.Builder}. */ + protected NativeAdRequest.Builder configureNativeAdRequestBuilder( + NativeAdRequest.Builder builder, String adUnitId) { + if (keywords != null) { + for (final String keyword : keywords) { + builder.addKeyword(keyword); + } + } + if (contentUrl != null) { + builder.setContentUrl(contentUrl); + } + if (neighboringContentUrls != null) { + builder.setNeighboringContentUrls(new HashSet(neighboringContentUrls)); + } + builder.setRequestAgent(requestAgent); + return builder; + } + AdRequest asAdRequest(String adUnitId) { return ((AdRequest.Builder) updateAdRequestBuilder(new AdRequest.Builder(adUnitId), adUnitId)) .build(); From 330fa5a8be38f83986defa826ead2074626403b3 Mon Sep 17 00:00:00 2001 From: LTphantom Date: Tue, 28 Jul 2026 12:16:19 -0700 Subject: [PATCH 5/6] Made domain nullsafe in LoadAdError for Android Next-Gen (#1461) --- .../plugins/googlemobileads/FlutterAd.java | 5 +++- .../lib/src/ad_instance_manager.dart | 6 ++--- .../test/ad_containers_test.dart | 25 +++++++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAd.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAd.java index a6fd9b86b..7ce6baa1b 100644 --- a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAd.java +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAd.java @@ -335,7 +335,10 @@ static class FlutterLoadAdError { if (error.getResponseInfo() != null) { responseInfo = new FlutterResponseInfo(error.getResponseInfo()); - domain = responseInfo.getMediationAdapterClassName(); + String adapterClassName = responseInfo.getMediationAdapterClassName(); + domain = adapterClassName != null + ? adapterClassName + : "com.google.android.libraries.ads.mobile.sdk"; } else { domain = "com.google.android.libraries.ads.mobile.sdk"; } 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 9c5471b58..48d69b8da 100644 --- a/packages/google_mobile_ads/lib/src/ad_instance_manager.dart +++ b/packages/google_mobile_ads/lib/src/ad_instance_manager.dart @@ -1192,8 +1192,8 @@ class AdMessageCodec extends StandardMessageCodec { ); case _valueRewardItem: return RewardItem( - readValueOfType(buffer.getUint8(), buffer), - readValueOfType(buffer.getUint8(), buffer), + readValueOfType(buffer.getUint8(), buffer) ?? 0, + _safeReadString(buffer), ); case _valueResponseInfo: return ResponseInfo( @@ -1278,7 +1278,7 @@ class AdMessageCodec extends StandardMessageCodec { buffer.getUint8(), buffer, ); - final String description = readValueOfType(buffer.getUint8(), buffer); + final String description = _safeReadString(buffer); double latency = readValueOfType(buffer.getUint8(), buffer).toDouble(); // Android provides this value as an int in milliseconds while iOS diff --git a/packages/google_mobile_ads/test/ad_containers_test.dart b/packages/google_mobile_ads/test/ad_containers_test.dart index ef0652449..d6c615756 100644 --- a/packages/google_mobile_ads/test/ad_containers_test.dart +++ b/packages/google_mobile_ads/test/ad_containers_test.dart @@ -1196,6 +1196,31 @@ void main() { expect(result.type, 'type'); }); + test('encode/decode $RewardItem with null values', () async { + final WriteBuffer buffer = WriteBuffer(); + buffer.putUint8(132); + codec.writeValue(buffer, null); + codec.writeValue(buffer, null); + + final RewardItem result = codec.decodeMessage(buffer.done()); + expect(result.amount, 0); + expect(result.type, ''); + }); + + test('encode/decode $AdapterStatus with null description', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + final WriteBuffer buffer = WriteBuffer(); + buffer.putUint8(136); + codec.writeValue(buffer, AdapterInitializationState.ready); + codec.writeValue(buffer, null); + codec.writeValue(buffer, 1000); + + final AdapterStatus result = codec.decodeMessage(buffer.done()); + expect(result.state, AdapterInitializationState.ready); + expect(result.description, ''); + expect(result.latency, 1.0); + }); + test('encode/decode $InlineAdaptiveSize', () async { ByteData byteData = codec.encodeMessage( AdSize.getCurrentOrientationInlineAdaptiveBannerAdSize(100), From 2ed0274d118efe108cc066cf041840a186b59b00 Mon Sep 17 00:00:00 2001 From: Felipe Fuzaro Date: Wed, 23 Sep 2026 11:12:25 +0200 Subject: [PATCH 6/6] fix(android): return ad size for `AdLoaderAd` banners on Next-Gen SDK --- .../plugins/googlemobileads/FlutterAdLoaderAd.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java index dcb6e3248..81060789a 100644 --- a/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java +++ b/packages/google_mobile_ads/android/src/adsNextGenSdk/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java @@ -234,7 +234,14 @@ AdLoaderAdType getAdLoaderAdType() { @Nullable FlutterAdSize getAdSize() { - return null; + if (!(view instanceof AdView)) { + return null; + } + final BannerAd bannerAd = ((AdView) view).getBannerAd(); + if (bannerAd == null) { + return null; + } + return new FlutterAdSize(bannerAd.getAdSize()); } @Nullable