From bb9f53196b70b4ab04e41ae7508ba15dc9d6b019 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 27 Oct 2022 14:16:30 +1100 Subject: [PATCH 1/7] Android: Make `FlutterNativeAdLoadedListener` more generally usable In `FlutterNativeAdLoadedListener`, make the inner weak reference be generic on the `OnNativeAdLoadedListener` interface instead of the `FlutterNativeAd` concrete class. This allows using the same listener class with any other type that conforms to `OnNativeAdLoadedListener`. Update `FlutterNativeAd` to conform to `OnNativeAdLoadedListener` to allow delegating in the same existing fashion. --- .../plugins/googlemobileads/FlutterAdListener.java | 10 +++++----- .../plugins/googlemobileads/FlutterNativeAd.java | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java index 8f29ee43d..f5353ca2f 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java @@ -97,16 +97,16 @@ public void onAdLoaded() { /** {@link OnNativeAdLoadedListener} for native ads. */ class FlutterNativeAdLoadedListener implements OnNativeAdLoadedListener { - private final WeakReference nativeAdWeakReference; + private final WeakReference listenerWeakReference; - FlutterNativeAdLoadedListener(FlutterNativeAd flutterNativeAd) { - nativeAdWeakReference = new WeakReference<>(flutterNativeAd); + FlutterNativeAdLoadedListener(OnNativeAdLoadedListener listener) { + listenerWeakReference = new WeakReference<>(listener); } @Override public void onNativeAdLoaded(@NonNull NativeAd nativeAd) { - if (nativeAdWeakReference.get() != null) { - nativeAdWeakReference.get().onNativeAdLoaded(nativeAd); + if (listenerWeakReference.get() != null) { + listenerWeakReference.get().onNativeAdLoaded(nativeAd); } } } diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterNativeAd.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterNativeAd.java index 8230b1ddc..9dd04424a 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterNativeAd.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterNativeAd.java @@ -32,7 +32,7 @@ import java.util.Map; /** A wrapper for {@link NativeAd}. */ -class FlutterNativeAd extends FlutterAd { +class FlutterNativeAd extends FlutterAd implements OnNativeAdLoadedListener { private static final String TAG = "FlutterNativeAd"; @NonNull private final AdInstanceManager manager; @@ -248,7 +248,8 @@ public PlatformView getPlatformView() { return null; } - void onNativeAdLoaded(@NonNull NativeAd nativeAd) { + @Override + public void onNativeAdLoaded(@NonNull NativeAd nativeAd) { if (nativeTemplateStyle != null) { templateView = nativeTemplateStyle.asTemplateView(context); templateView.setNativeAd(nativeAd); From 4b2111876093ba7089294ca40424bfff83cbee07 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 27 Oct 2022 17:28:13 +1100 Subject: [PATCH 2/7] iOS: Relax constraints on `FLTAdInstanceManager` methods For the `onBanner*` and `onNative*` methods, relax the type constraint from `FLTBannerAd` and `FLTNativeAd` respectively, to `id` so any instance that conforms to `FLTAd` can be used. --- .../ios/Classes/FLTAdInstanceManager_Internal.h | 16 ++++++++-------- .../ios/Classes/FLTAdInstanceManager_Internal.m | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.h b/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.h index 1e8dd5549..5b187e359 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.h +++ b/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.h @@ -39,10 +39,10 @@ - (void)onAppEvent:(id _Nonnull)ad name:(NSString *_Nullable)name data:(NSString *_Nullable)data; -- (void)onNativeAdImpression:(FLTNativeAd *_Nonnull)ad; -- (void)onNativeAdWillPresentScreen:(FLTNativeAd *_Nonnull)ad; -- (void)onNativeAdDidDismissScreen:(FLTNativeAd *_Nonnull)ad; -- (void)onNativeAdWillDismissScreen:(FLTNativeAd *_Nonnull)ad; +- (void)onNativeAdImpression:(nonnull id)ad; +- (void)onNativeAdWillPresentScreen:(nonnull id)ad; +- (void)onNativeAdDidDismissScreen:(nonnull id)ad; +- (void)onNativeAdWillDismissScreen:(nonnull id)ad; - (void)onRewardedAdUserEarnedReward:(FLTRewardedAd *_Nonnull)ad reward:(FLTRewardItem *_Nonnull)reward; - (void)onRewardedInterstitialAdUserEarnedReward: @@ -50,10 +50,10 @@ reward: (FLTRewardItem *_Nonnull)reward; - (void)onPaidEvent:(id _Nonnull)ad value:(FLTAdValue *_Nonnull)value; -- (void)onBannerImpression:(FLTBannerAd *_Nonnull)ad; -- (void)onBannerWillDismissScreen:(FLTBannerAd *_Nonnull)ad; -- (void)onBannerDidDismissScreen:(FLTBannerAd *_Nonnull)ad; -- (void)onBannerWillPresentScreen:(FLTBannerAd *_Nonnull)ad; +- (void)onBannerImpression:(nonnull id)ad; +- (void)onBannerWillDismissScreen:(nonnull id)ad; +- (void)onBannerDidDismissScreen:(nonnull id)ad; +- (void)onBannerWillPresentScreen:(nonnull id)ad; - (void)adWillPresentFullScreenContent:(id _Nonnull)ad; - (void)adDidDismissFullScreenContent:(id _Nonnull)ad; diff --git a/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.m b/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.m index 6f8e856cc..f57a655ee 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.m +++ b/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.m @@ -110,19 +110,19 @@ - (void)onAppEvent:(id _Nonnull)ad }]; } -- (void)onNativeAdImpression:(FLTNativeAd *_Nonnull)ad { +- (void)onNativeAdImpression:(nonnull id)ad { [self sendAdEvent:@"onNativeAdImpression" ad:ad]; } -- (void)onNativeAdWillPresentScreen:(FLTNativeAd *_Nonnull)ad { +- (void)onNativeAdWillPresentScreen:(nonnull id)ad { [self sendAdEvent:@"onNativeAdWillPresentScreen" ad:ad]; } -- (void)onNativeAdDidDismissScreen:(FLTNativeAd *_Nonnull)ad { +- (void)onNativeAdDidDismissScreen:(nonnull id)ad { [self sendAdEvent:@"onNativeAdDidDismissScreen" ad:ad]; } -- (void)onNativeAdWillDismissScreen:(FLTNativeAd *_Nonnull)ad { +- (void)onNativeAdWillDismissScreen:(nonnull id)ad { [self sendAdEvent:@"onNativeAdWillDismissScreen" ad:ad]; } @@ -159,19 +159,19 @@ - (void)onPaidEvent:(id _Nonnull)ad value:(FLTAdValue *_Nonnull)adValue { }]; } -- (void)onBannerImpression:(FLTBannerAd *_Nonnull)ad { +- (void)onBannerImpression:(nonnull id)ad { [self sendAdEvent:@"onBannerImpression" ad:ad]; } -- (void)onBannerWillDismissScreen:(FLTBannerAd *)ad { +- (void)onBannerWillDismissScreen:(nonnull id)ad { [self sendAdEvent:@"onBannerWillDismissScreen" ad:ad]; } -- (void)onBannerDidDismissScreen:(FLTBannerAd *)ad { +- (void)onBannerDidDismissScreen:(nonnull id)ad { [self sendAdEvent:@"onBannerDidDismissScreen" ad:ad]; } -- (void)onBannerWillPresentScreen:(FLTBannerAd *_Nonnull)ad { +- (void)onBannerWillPresentScreen:(nonnull id)ad { [self sendAdEvent:@"onBannerWillPresentScreen" ad:ad]; } From 2db1c3b351f31f91bb15df6934174151f697f22a Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 7 Apr 2023 15:51:51 +1000 Subject: [PATCH 3/7] Android: Allow passing a `FlutterAdLoader` factory Create a new `GoogleMobileAdsPlugin` constructor which takes an additional `Supplier` argument. This is available solely for testing, and allows the provision of a mock `FlutterAdLoader` for those `FlutterAd`s that need it during their construction. --- .../googlemobileads/GoogleMobileAdsPlugin.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 e9d4b3444..35efab3d5 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 @@ -44,6 +44,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; /** * Flutter plugin accessing Google Mobile Ads API. @@ -70,6 +71,7 @@ private static T requireNonNull(T obj) { private final Map nativeAdFactories = new HashMap<>(); @Nullable private MediationNetworkExtrasProvider mediationNetworkExtrasProvider; private final FlutterMobileAdsWrapper flutterMobileAds; + @Nullable private Supplier adLoaderSupplier; /** * Public constructor for the plugin. Dependency initialization is handled in lifecycle methods * below. @@ -95,6 +97,17 @@ protected GoogleMobileAdsPlugin(@NonNull AppStateNotifier appStateNotifier) { this.flutterMobileAds = new FlutterMobileAdsWrapper(); } + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + protected GoogleMobileAdsPlugin( + @Nullable FlutterPluginBinding pluginBinding, + @Nullable AdInstanceManager instanceManager, + @NonNull FlutterMobileAdsWrapper flutterMobileAds, + @NonNull Supplier adLoaderSupplier) { + this(pluginBinding, instanceManager, flutterMobileAds); + + this.adLoaderSupplier = adLoaderSupplier; + } + /** * Interface used to display a {@link com.google.android.gms.ads.nativead.NativeAd}. * From 1eb2ecdd8660c7131b7c7287b21f3f540b1badc3 Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 23 Oct 2022 11:15:34 +1100 Subject: [PATCH 4/7] Initial `AdLoaderAd` type Add the `AdLoaderAd` component, without the specific configuration items for each of the handled ad types. The component implementation is known as: * `AdLoaderAd` (Flutter) * `FlutterAdLoaderAd` (Android) * `FLTAdLoaderAd` (iOS) --- .../googlemobileads/FlutterAdListener.java | 8 + .../googlemobileads/FlutterAdLoader.java | 17 ++ .../googlemobileads/FlutterAdLoaderAd.java | 173 ++++++++++++ .../GoogleMobileAdsPlugin.java | 51 ++++ .../FlutterAdLoaderAdTest.java | 164 +++++++++++ .../googlemobileads/GoogleMobileAdsTest.java | 100 +++++++ .../ios/Runner.xcodeproj/project.pbxproj | 4 + .../ios/RunnerTests/FLTAdLoaderAdTest.m | 83 ++++++ .../FLTGoogleMobileAdsPluginMethodCallsTest.m | 124 ++++++++ .../Classes/FLTAdInstanceManager_Internal.h | 1 + .../ios/Classes/FLTAd_Internal.h | 17 ++ .../ios/Classes/FLTAd_Internal.m | 62 ++++ .../ios/Classes/FLTGoogleMobileAdsPlugin.m | 43 +++ .../lib/src/ad_containers.dart | 71 +++++ .../lib/src/ad_instance_manager.dart | 56 ++++ .../lib/src/ad_listeners.dart | 36 +++ .../test/ad_containers_test.dart | 1 + .../test/ad_loader_ad_test.dart | 264 ++++++++++++++++++ 18 files changed, 1275 insertions(+) create mode 100644 packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java create mode 100644 packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAdTest.java create mode 100644 packages/google_mobile_ads/example/ios/RunnerTests/FLTAdLoaderAdTest.m create mode 100644 packages/google_mobile_ads/test/ad_loader_ad_test.dart diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java index f5353ca2f..065f74a98 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java @@ -94,6 +94,14 @@ public void onAdLoaded() { } } +/** Listener for adloader ads. */ +class FlutterAdLoaderAdListener extends FlutterAdListener { + + FlutterAdLoaderAdListener(int adId, AdInstanceManager manager) { + super(adId, manager); + } +} + /** {@link OnNativeAdLoadedListener} for native ads. */ class FlutterNativeAdLoadedListener implements OnNativeAdLoadedListener { diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java index 0e864e7d9..d95a6a6b2 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java @@ -137,4 +137,21 @@ public void loadAdManagerNativeAd( .build() .loadAd(adManagerAdRequest); } + + /** 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); + } + + /** 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); + } } diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java new file mode 100644 index 000000000..ac0abc2a9 --- /dev/null +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java @@ -0,0 +1,173 @@ +// 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 android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.ads.AdListener; + +/** + * 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 { + private static final String TAG = "FlutterAdLoaderAd"; + + @NonNull private final AdInstanceManager manager; + @NonNull private final String adUnitId; + @NonNull private final FlutterAdLoader adLoader; + @Nullable private FlutterAdRequest request; + @Nullable private FlutterAdManagerAdRequest adManagerRequest; + @Nullable private AdLoaderAdType type; + @Nullable private String formatId; + + static class Builder { + @Nullable private AdInstanceManager manager; + @Nullable private String adUnitId; + @Nullable private FlutterAdRequest request; + @Nullable private FlutterAdManagerAdRequest adManagerRequest; + @Nullable private Integer id; + @Nullable private FlutterAdLoader adLoader; + + public Builder setId(int id) { + this.id = id; + return this; + } + + public Builder setManager(@NonNull AdInstanceManager manager) { + this.manager = manager; + return this; + } + + public Builder setAdUnitId(@NonNull String adUnitId) { + this.adUnitId = adUnitId; + return this; + } + + public Builder setRequest(@NonNull FlutterAdRequest request) { + this.request = request; + return this; + } + + public Builder setAdManagerRequest(@NonNull FlutterAdManagerAdRequest adManagerRequest) { + this.adManagerRequest = adManagerRequest; + return this; + } + + public Builder setFlutterAdLoader(@NonNull FlutterAdLoader adLoader) { + this.adLoader = adLoader; + return this; + } + + FlutterAdLoaderAd build() { + if (manager == null) { + throw new IllegalStateException("manager must be provided"); + } + + if (adUnitId == null) { + throw new IllegalStateException("adUnitId must be provided"); + } + + if (request == null && adManagerRequest == null) { + throw new IllegalStateException("Either request or adManagerRequest must be provided"); + } + + final FlutterAdLoaderAd adLoaderAd; + + if (request == null) { + adLoaderAd = new FlutterAdLoaderAd(id, manager, adUnitId, adManagerRequest, adLoader); + } else { + adLoaderAd = new FlutterAdLoaderAd(id, manager, adUnitId, request, adLoader); + } + return adLoaderAd; + } + } + + enum AdLoaderAdType { + UNKNOWN, + } + + protected FlutterAdLoaderAd( + int adId, + @NonNull AdInstanceManager manager, + @NonNull String adUnitId, + @NonNull FlutterAdRequest request, + @NonNull FlutterAdLoader adLoader) { + super(adId); + + this.type = AdLoaderAdType.UNKNOWN; + this.formatId = null; + + this.manager = manager; + this.adUnitId = adUnitId; + this.request = request; + this.adLoader = adLoader; + } + + protected FlutterAdLoaderAd( + int adId, + @NonNull AdInstanceManager manager, + @NonNull String adUnitId, + @NonNull FlutterAdManagerAdRequest adManagerRequest, + @NonNull FlutterAdLoader adLoader) { + super(adId); + + this.type = AdLoaderAdType.UNKNOWN; + this.formatId = null; + + this.manager = manager; + this.adUnitId = adUnitId; + this.adManagerRequest = adManagerRequest; + this.adLoader = adLoader; + } + + @Override + void load() { + final AdListener adListener = new FlutterAdLoaderAdListener(adId, manager); + // 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)); + return; + } + + if (adManagerRequest != null) { + adLoader.loadAdManagerAdLoaderAd( + adUnitId, adListener, adManagerRequest.asAdManagerAdRequest(adUnitId)); + return; + } + + Log.e(TAG, "A null or invalid ad request was provided."); + } + + @Nullable + AdLoaderAdType getAdLoaderAdType() { + return type; + } + + @Nullable + FlutterAdSize getAdSize() { + return null; + } + + @Nullable + String getFormatId() { + return formatId; + } + + @Override + void dispose() {} +} 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 35efab3d5..045f67f07 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 @@ -444,6 +444,23 @@ public void onAdInspectorClosed(@Nullable AdInspectorError adInspectorError) { nativeAd.load(); result.success(null); break; + case "loadAdLoaderAd": + final FlutterAdLoaderAd adLoaderAd = + new FlutterAdLoaderAd.Builder() + .setManager(instanceManager) + .setAdUnitId(call.argument("adUnitId")) + .setRequest(call.argument("request")) + .setAdManagerRequest(call.argument("adManagerRequest")) + .setId(call.argument("adId")) + .setFlutterAdLoader( + adLoaderSupplier != null + ? adLoaderSupplier.get() + : new FlutterAdLoader(context)) + .build(); + instanceManager.trackAd(adLoaderAd, call.argument("adId")); + adLoaderAd.load(); + result.success(null); + break; case "loadInterstitialAd": final FlutterInterstitialAd interstitial = new FlutterInterstitialAd( @@ -623,6 +640,22 @@ public void onAdInspectorClosed(@Nullable AdInspectorError adInspectorError) { flutterMobileAds.openDebugMenu(context, adUnitId); result.success(null); break; + case "getAdLoaderAdType": + { + FlutterAd ad = instanceManager.adForId(call.argument("adId")); + if (ad == null) { + // This was called on a dart ad container that hasn't been loaded yet. + result.success(null); + } else if (ad instanceof FlutterAdLoaderAd) { + result.success(((FlutterAdLoaderAd) ad).getAdLoaderAdType().ordinal()); + } else { + result.error( + Constants.ERROR_CODE_UNEXPECTED_AD_TYPE, + "Unexpected ad type for getAdLoaderAdType: " + ad, + null); + } + break; + } case "getAdSize": { FlutterAd ad = instanceManager.adForId(call.argument("adId")); @@ -633,6 +666,8 @@ public void onAdInspectorClosed(@Nullable AdInspectorError adInspectorError) { result.success(((FlutterBannerAd) ad).getAdSize()); } else if (ad instanceof FlutterAdManagerBannerAd) { result.success(((FlutterAdManagerBannerAd) ad).getAdSize()); + } else if (ad instanceof FlutterAdLoaderAd) { + result.success(((FlutterAdLoaderAd) ad).getAdSize()); } else { result.error( Constants.ERROR_CODE_UNEXPECTED_AD_TYPE, @@ -641,6 +676,22 @@ public void onAdInspectorClosed(@Nullable AdInspectorError adInspectorError) { } break; } + case "getFormatId": + { + FlutterAd ad = instanceManager.adForId(call.argument("adId")); + if (ad == null) { + // This was called on a dart ad container that hasn't been loaded yet. + result.success(null); + } else if (ad instanceof FlutterAdLoaderAd) { + result.success(((FlutterAdLoaderAd) ad).getFormatId()); + } else { + result.error( + Constants.ERROR_CODE_UNEXPECTED_AD_TYPE, + "Unexpected ad type for getFormatId: " + ad, + null); + } + break; + } case "setServerSideVerificationOptions": { FlutterAd ad = instanceManager.adForId(call.argument("adId")); 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 new file mode 100644 index 000000000..eb2f61537 --- /dev/null +++ b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAdTest.java @@ -0,0 +1,164 @@ +// 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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +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.LoadAdError; +import com.google.android.gms.ads.admanager.AdManagerAdRequest; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.googlemobileads.FlutterAd.FlutterLoadAdError; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link FlutterAdLoaderAd} */ +@RunWith(RobolectricTestRunner.class) +public class FlutterAdLoaderAdTest { + + private AdInstanceManager testManager; + private final FlutterAdRequest request = new FlutterAdRequest.Builder().build(); + + @Before + public void setup() { + testManager = spy(new AdInstanceManager(mock(MethodChannel.class))); + when(testManager.getActivity()).thenReturn(mock(Activity.class)); + } + + @Test + public void loadAdLoaderAdWithAdManagerAdRequest() { + 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 LoadAdError mockLoadAdError = mock(LoadAdError.class); + when(mockLoadAdError.getCode()).thenReturn(1); + when(mockLoadAdError.getDomain()).thenReturn("2"); + when(mockLoadAdError.getMessage()).thenReturn("3"); + + 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(); + return null; + } + }) + .when(mockLoader) + .loadAdManagerAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest)); + + adLoaderAd.load(); + + verify(mockLoader) + .loadAdManagerAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest)); + + 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 loadAdLoaderAdWithAdRequest() { + final FlutterAdRequest mockFlutterRequest = mock(FlutterAdRequest.class); + final AdRequest mockRequest = mock(AdRequest.class); + when(mockFlutterRequest.asAdRequest(anyString())).thenReturn(mockRequest); + FlutterAdLoader mockLoader = mock(FlutterAdLoader.class); + final FlutterAdLoaderAd adLoaderAd = + new FlutterAdLoaderAd(1, testManager, "testId", mockFlutterRequest, mockLoader); + + final LoadAdError mockLoadAdError = mock(LoadAdError.class); + when(mockLoadAdError.getCode()).thenReturn(1); + when(mockLoadAdError.getDomain()).thenReturn("2"); + when(mockLoadAdError.getMessage()).thenReturn("3"); + + 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(); + return null; + } + }) + .when(mockLoader) + .loadAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest)); + + adLoaderAd.load(); + + verify(mockLoader).loadAdLoaderAd(eq("testId"), any(AdListener.class), eq(mockRequest)); + + 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(expected = IllegalStateException.class) + public void adLoaderAdBuilderNullManager() { + new FlutterAdLoaderAd.Builder() + .setManager(null) + .setAdUnitId("testId") + .setRequest(request) + .build(); + } + + @Test(expected = IllegalStateException.class) + public void adLoaderAdBuilderNullAdUnitId() { + new FlutterAdLoaderAd.Builder() + .setManager(testManager) + .setAdUnitId(null) + .setRequest(request) + .build(); + } + + @Test(expected = IllegalStateException.class) + public void adLoaderAdBuilderNullRequest() { + new FlutterAdLoaderAd.Builder() + .setManager(testManager) + .setAdUnitId("testId") + .setRequest(null) + .build(); + } +} diff --git a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsTest.java b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsTest.java index 832d1d489..01bd12004 100644 --- a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsTest.java +++ b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsTest.java @@ -808,6 +808,73 @@ public void testGetAnchoredAdaptiveBannerAdSize() { verify(result).success(adSize.getHeight()); } + @Test + public void testGetAdLoaderAdType_adLoaderAd() { + // Setup mocks + AdInstanceManager testManagerSpy = spy(testManager); + FlutterMobileAdsWrapper mockMobileAds = mock(FlutterMobileAdsWrapper.class); + FlutterAdLoader mockLoader = mock(FlutterAdLoader.class); + GoogleMobileAdsPlugin plugin = + new GoogleMobileAdsPlugin( + mockFlutterPluginBinding, testManagerSpy, mockMobileAds, () -> mockLoader); + GoogleMobileAdsPlugin pluginSpy = spy(plugin); + + // Load an ad loader ad + Map loadArgs = new HashMap<>(); + loadArgs.put("adId", 1); + loadArgs.put("adUnitId", "test-ad-unit"); + loadArgs.put("request", new FlutterAdRequest.Builder().build()); + + MethodCall loadAdLoaderAdMethodCall = new MethodCall("loadAdLoaderAd", loadArgs); + Result result = mock(Result.class); + pluginSpy.onMethodCall(loadAdLoaderAdMethodCall, result); + verify(result).success(null); + + // Method call for getAdLoaderAdType. + Result getAdLoaderAdTypeResult = mock(Result.class); + Map getAdLoaderAdTypeArgs = Collections.singletonMap("adId", (Object) 1); + MethodCall getAdLoaderAdTypeMethodCall = + new MethodCall("getAdLoaderAdType", getAdLoaderAdTypeArgs); + pluginSpy.onMethodCall(getAdLoaderAdTypeMethodCall, getAdLoaderAdTypeResult); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Integer.class); + verify(getAdLoaderAdTypeResult).success(argumentCaptor.capture()); + assertEquals(0, argumentCaptor.getValue().intValue()); + } + + @Test + public void testGetAdSize_adLoaderAd() { + // Setup mocks + AdInstanceManager testManagerSpy = spy(testManager); + FlutterMobileAdsWrapper mockMobileAds = mock(FlutterMobileAdsWrapper.class); + FlutterAdLoader mockLoader = mock(FlutterAdLoader.class); + GoogleMobileAdsPlugin plugin = + new GoogleMobileAdsPlugin( + mockFlutterPluginBinding, testManagerSpy, mockMobileAds, () -> mockLoader); + GoogleMobileAdsPlugin pluginSpy = spy(plugin); + + // Load a banner ad + Map loadArgs = new HashMap<>(); + loadArgs.put("adId", 1); + loadArgs.put("adUnitId", "test-ad-unit"); + loadArgs.put("request", new FlutterAdRequest.Builder().build()); + + MethodCall loadAdLoaderAdMethodCall = new MethodCall("loadAdLoaderAd", loadArgs); + Result result = mock(Result.class); + pluginSpy.onMethodCall(loadAdLoaderAdMethodCall, result); + verify(result).success(null); + + // Method call for getAdSize. + Result getAdSizeResult = mock(Result.class); + Map getAdSizeArgs = Collections.singletonMap("adId", (Object) 1); + MethodCall getAdSizeMethodCall = new MethodCall("getAdSize", getAdSizeArgs); + pluginSpy.onMethodCall(getAdSizeMethodCall, getAdSizeResult); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(FlutterAdSize.class); + verify(getAdSizeResult).success(argumentCaptor.capture()); + assertEquals(null, argumentCaptor.getValue()); + } + public void testGetAdSize_bannerAd() { // Setup mocks AdInstanceManager testManagerSpy = spy(testManager); @@ -891,6 +958,39 @@ public void testGetAdSize_adManagerBannerAd() { assertEquals(argumentCaptor.getValue().getAdSize(), adSize); } + @Test + public void testGetFormatId_adLoaderAd() { + // Setup mocks + AdInstanceManager testManagerSpy = spy(testManager); + FlutterMobileAdsWrapper mockMobileAds = mock(FlutterMobileAdsWrapper.class); + FlutterAdLoader mockLoader = mock(FlutterAdLoader.class); + GoogleMobileAdsPlugin plugin = + new GoogleMobileAdsPlugin( + mockFlutterPluginBinding, testManagerSpy, mockMobileAds, () -> mockLoader); + GoogleMobileAdsPlugin pluginSpy = spy(plugin); + + // Load an ad loader ad + Map loadArgs = new HashMap<>(); + loadArgs.put("adId", 1); + loadArgs.put("adUnitId", "test-ad-unit"); + loadArgs.put("request", new FlutterAdRequest.Builder().build()); + + MethodCall loadAdLoaderAdMethodCall = new MethodCall("loadAdLoaderAd", loadArgs); + Result result = mock(Result.class); + pluginSpy.onMethodCall(loadAdLoaderAdMethodCall, result); + verify(result).success(null); + + // Method call for getFormatId. + Result getFormatIdResult = mock(Result.class); + Map getFormatIdArgs = Collections.singletonMap("adId", (Object) 1); + MethodCall getFormatIdMethodCall = new MethodCall("getFormatId", getFormatIdArgs); + pluginSpy.onMethodCall(getFormatIdMethodCall, getFormatIdResult); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(String.class); + verify(getFormatIdResult).success(argumentCaptor.capture()); + assertEquals(null, argumentCaptor.getValue()); + } + @Test public void testAppStateNotifyDetachFromEngine() { AppStateNotifier notifier = mock(AppStateNotifier.class); 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 3d588697c..16f145298 100644 --- a/packages/google_mobile_ads/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_mobile_ads/example/ios/Runner.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ 9E61AA6029BBE8FD00801A83 /* FLTNativeTemplateStyleTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E61AA5B29BBE8FD00801A83 /* FLTNativeTemplateStyleTest.m */; }; 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 */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -150,6 +151,7 @@ 9EF4E6FE26392B230007E4FE /* FLTGoogleMobileAdsCollection_Internal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FLTGoogleMobileAdsCollection_Internal.m; path = ../../ios/Classes/FLTGoogleMobileAdsCollection_Internal.m; sourceTree = ""; }; 9EF4E6FF26392B230007E4FE /* FLTMobileAds_Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = FLTMobileAds_Internal.h; path = ../../ios/Classes/FLTMobileAds_Internal.h; 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -272,6 +274,7 @@ children = ( 9E61AA5829BBE8DA00801A83 /* NativeTemplates */, 9E61AA5529BBE8AF00801A83 /* UserMessagingPlatform */, + E276E99129DFB5870052484E /* FLTAdLoaderAdTest.m */, 9E61AA2729BBE66900801A83 /* FLTAdUtilTest.m */, 9E61AA2429BBE66900801A83 /* FLTAppOpenAdTest.m */, 9E61AA2229BBE66900801A83 /* FLTBannerAdTest.m */, @@ -512,6 +515,7 @@ buildActionMask = 2147483647; files = ( 9E61AA5729BBE8D000801A83 /* FLTUserMessagingPlatformManagerTest.m in Sources */, + E276E99229DFB5870052484E /* FLTAdLoaderAdTest.m in Sources */, 9E61AA5329BBE88000801A83 /* FLTUserMessagingPlatformReaderWriterTest.m in Sources */, 9E61AA3229BBE66900801A83 /* FLTAppOpenAdTest.m in Sources */, 9E61AA5F29BBE8FD00801A83 /* FLTNativeTemplateTypeTest.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 new file mode 100644 index 000000000..5ff06833a --- /dev/null +++ b/packages/google_mobile_ads/example/ios/RunnerTests/FLTAdLoaderAdTest.m @@ -0,0 +1,83 @@ +// 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 + +#import "FLTAd_Internal.h" + +@interface FLTAdLoaderAdTest : XCTestCase +@end + +@implementation FLTAdLoaderAdTest +- (void)testDelegates { + UIViewController *viewController = OCMClassMock([UIViewController class]); + FLTAdInstanceManager *manager = OCMClassMock([FLTAdInstanceManager class]); + + FLTAdLoaderAd *ad = + [[FLTAdLoaderAd alloc] initWithAdUnitId:@"testAdUnitId" + request:[[FLTAdRequest alloc] init] + rootViewController:viewController + adId:@0]; + + ad.manager = manager; + + [ad load]; + + XCTAssertEqual(ad.adLoader.delegate, ad); + + // GADAdLoaderDelegate + NSError *error = [NSError errorWithDomain:@"domain" code:1 userInfo:nil]; + [ad.adLoader.delegate adLoader:ad.adLoader.delegate + didFailToReceiveAdWithError:error]; + + OCMVerify([manager onAdFailedToLoad:[OCMArg isEqual:ad] + error:[OCMArg isEqual:error]]); +} + +- (void)testLoadAdLoaderAd { + FLTAdRequest *request = [[FLTAdRequest alloc] init]; + request.keywords = @[ @"apple" ]; + [self testLoadAdLoaderAd:request]; +} + +- (void)testLoadAdLoaderAdWithGAMRequest { + FLTGAMAdRequest *request = [[FLTGAMAdRequest alloc] init]; + request.keywords = @[ @"apple" ]; + [self testLoadAdLoaderAd:request]; +} + +- (void)testLoadAdLoaderAd:(FLTAdRequest *)request { + UIViewController *viewController = OCMClassMock([UIViewController class]); + + FLTAdLoaderAd *ad = [[FLTAdLoaderAd alloc] initWithAdUnitId:@"testAdUnitId" + request:request + rootViewController:viewController + adId:@1]; + + XCTAssertEqual(ad.adLoader.adUnitID, @"testAdUnitId"); + XCTAssertEqual(ad.adLoader.delegate, ad); + + FLTAdLoaderAd *mockAdLoaderAd = OCMPartialMock(ad); + GADAdLoader *mockLoader = OCMPartialMock([ad adLoader]); + OCMStub([mockAdLoaderAd adLoader]).andReturn(mockLoader); + [mockAdLoaderAd load]; + + OCMVerify([mockLoader loadRequest:[OCMArg checkWithBlock:^BOOL(id obj) { + GADRequest *requestArg = obj; + return [requestArg.keywords + isEqualToArray:@[ @"apple" ]]; + }]]); +} +@end diff --git a/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsPluginMethodCallsTest.m b/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsPluginMethodCallsTest.m index a4164b0ce..fc0f46f9d 100644 --- a/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsPluginMethodCallsTest.m +++ b/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsPluginMethodCallsTest.m @@ -551,6 +551,89 @@ - (void)testGetAnchoredAdaptiveBannerAdSize { .size.height); } +- (void)testGetAdLoaderAdType_adLoaderAd { + // Method calls to load an ad loader ad. + FlutterMethodCall *loadAdMethodCall = [FlutterMethodCall + methodCallWithMethodName:@"loadAdLoaderAd" + arguments:@{ + @"adId" : @(1), + @"adUnitId" : @"ad-unit-id", + @"request" : [[FLTAdRequest alloc] init], + }]; + + __block bool loadAdResultInvoked = false; + __block id _Nullable returnedLoadAdResult; + FlutterResult loadAdResult = ^(id _Nullable result) { + loadAdResultInvoked = true; + returnedLoadAdResult = result; + }; + + [_fltGoogleMobileAdsPlugin handleMethodCall:loadAdMethodCall + result:loadAdResult]; + + XCTAssertTrue(loadAdResultInvoked); + XCTAssertNil(returnedLoadAdResult); + + // Method call to get the format id. + __block bool getAdLoaderAdTypeResultInvoked = false; + __block FLTAdLoaderAdType returnedGetAdLoaderAdTypeResult; + FlutterResult getAdLoaderAdTypeResult = ^(id _Nullable result) { + getAdLoaderAdTypeResultInvoked = true; + returnedGetAdLoaderAdTypeResult = result; + }; + + FlutterMethodCall *getAdLoaderAdTypeMethodCall = + [FlutterMethodCall methodCallWithMethodName:@"getAdLoaderAdType" + arguments:@{@"adId" : @(1)}]; + [_fltGoogleMobileAdsPlugin handleMethodCall:getAdLoaderAdTypeMethodCall + result:getAdLoaderAdTypeResult]; + + XCTAssertTrue(getAdLoaderAdTypeResultInvoked); + XCTAssertEqual(returnedGetAdLoaderAdTypeResult, + [[NSNumber alloc] initWithInteger:FLTAdLoaderAdTypeUnknown]); +} + +- (void)testGetAdSize_adLoaderAd { + // Method calls to load an ad loader ad. + FlutterMethodCall *loadAdMethodCall = [FlutterMethodCall + methodCallWithMethodName:@"loadAdLoaderAd" + arguments:@{ + @"adId" : @(1), + @"adUnitId" : @"ad-unit-id", + @"request" : [[FLTAdRequest alloc] init], + }]; + + __block bool loadAdResultInvoked = false; + __block id _Nullable returnedLoadAdResult; + FlutterResult loadAdResult = ^(id _Nullable result) { + loadAdResultInvoked = true; + returnedLoadAdResult = result; + }; + + [_fltGoogleMobileAdsPlugin handleMethodCall:loadAdMethodCall + result:loadAdResult]; + + XCTAssertTrue(loadAdResultInvoked); + XCTAssertNil(returnedLoadAdResult); + + // Method call to get the ad size. + __block bool getAdSizeResultInvoked = false; + __block FLTAdSize *_Nullable returnedGetAdSizeResult; + FlutterResult getAdSizeResult = ^(id _Nullable result) { + getAdSizeResultInvoked = true; + returnedGetAdSizeResult = result; + }; + + FlutterMethodCall *getAdSizeMethodCall = + [FlutterMethodCall methodCallWithMethodName:@"getAdSize" + arguments:@{@"adId" : @(1)}]; + [_fltGoogleMobileAdsPlugin handleMethodCall:getAdSizeMethodCall + result:getAdSizeResult]; + + XCTAssertTrue(getAdSizeResultInvoked); + XCTAssertNil(returnedGetAdSizeResult); +} + - (void)testGetAdSize_bannerAd { // Method calls to load a banner ad. FlutterMethodCall *loadAdMethodCall = [FlutterMethodCall @@ -594,6 +677,47 @@ - (void)testGetAdSize_bannerAd { XCTAssertEqualObjects(returnedGetAdSizeResult.height, @2); } +- (void)testGetFormatId_adLoaderAd { + // Method calls to load an ad loader ad. + FlutterMethodCall *loadAdMethodCall = [FlutterMethodCall + methodCallWithMethodName:@"loadAdLoaderAd" + arguments:@{ + @"adId" : @(1), + @"adUnitId" : @"ad-unit-id", + @"request" : [[FLTAdRequest alloc] init], + }]; + + __block bool loadAdResultInvoked = false; + __block id _Nullable returnedLoadAdResult; + FlutterResult loadAdResult = ^(id _Nullable result) { + loadAdResultInvoked = true; + returnedLoadAdResult = result; + }; + + [_fltGoogleMobileAdsPlugin handleMethodCall:loadAdMethodCall + result:loadAdResult]; + + XCTAssertTrue(loadAdResultInvoked); + XCTAssertNil(returnedLoadAdResult); + + // Method call to get the format id. + __block bool getFormatIdResultInvoked = false; + __block NSString *_Nullable returnedGetFormatIdResult; + FlutterResult getFormatIdResult = ^(id _Nullable result) { + getFormatIdResultInvoked = true; + returnedGetFormatIdResult = result; + }; + + FlutterMethodCall *getFormatIdMethodCall = + [FlutterMethodCall methodCallWithMethodName:@"getFormatId" + arguments:@{@"adId" : @(1)}]; + [_fltGoogleMobileAdsPlugin handleMethodCall:getFormatIdMethodCall + result:getFormatIdResult]; + + XCTAssertTrue(getFormatIdResultInvoked); + XCTAssertNil(returnedGetFormatIdResult); +} + - (void)testServerSideVerificationOptions_rewardedAd { // Mock having already loaded an ad FLTRewardedAd *mockAd = OCMClassMock([FLTRewardedAd class]); diff --git a/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.h b/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.h index 5b187e359..459356b84 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.h +++ b/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.h @@ -18,6 +18,7 @@ #import @protocol FLTAd; +@class FLTAdLoaderAd; @class FLTBannerAd; @class FLTNativeAd; @class FLTRewardedAd; diff --git a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h index 92d0fc151..a907add7e 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h +++ b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h @@ -310,6 +310,23 @@ - (GADAdLoader *_Nonnull)adLoader; @end +typedef NS_ENUM(NSInteger, FLTAdLoaderAdType) { + FLTAdLoaderAdTypeUnknown = 0, +}; + +@interface FLTAdLoaderAd + : 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; +@end + @interface FLTRewardItem : NSObject @property(readonly) NSNumber *_Nonnull amount; @property(readonly) NSString *_Nonnull type; diff --git a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m index 0a82ba796..d4fcb1036 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m +++ b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m @@ -1213,6 +1213,68 @@ - (UIView *)view { @end +#pragma mark - FLTAdLoaderAd + +@implementation FLTAdLoaderAd { + NSString *_adUnitId; + FLTAdRequest *_adRequest; +} + +- (nonnull instancetype)initWithAdUnitId:(nonnull NSString *)adUnitId + request:(nonnull FLTAdRequest *)request + rootViewController: + (nonnull UIViewController *)rootViewController + adId:(nonnull NSNumber *)adId { + 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]; + + _adLoader = [[GADAdLoader alloc] initWithAdUnitID:_adUnitId + rootViewController:rootViewController + adTypes:adTypes + options:options]; + _adLoader.delegate = self; + } + return self; +} + +#pragma mark - FLTAd + +- (void)load { + GADRequest *request; + if ([_adRequest isKindOfClass:[FLTGAMAdRequest class]]) { + request = [(FLTGAMAdRequest *)_adRequest asGAMRequest:_adUnitId]; + } else { + request = [_adRequest asGADRequest:_adUnitId]; + } + [_adLoader loadRequest:request]; +} + +#pragma mark - GADAdLoaderDelegate + +- (void)adLoader:(nonnull GADAdLoader *)adLoader + didFailToReceiveAdWithError:(nonnull NSError *)error { + [manager onAdFailedToLoad:self error:error]; +} + +#pragma mark - FlutterPlatformView + +- (nonnull UIView *)view { + return nil; // TODO not great +} + +@synthesize manager; + +@end + @implementation FLTRewardItem - (instancetype _Nonnull)initWithAmount:(NSNumber *_Nonnull)amount type:(NSString *_Nonnull)type { diff --git a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m index 1defb9db4..056e2f61b 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m +++ b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m @@ -422,6 +422,21 @@ - (void)handleMethodCall:(FlutterMethodCall *)call nativeTemplateStyle:call.arguments[@"nativeTemplateStyle"]]; [_manager loadAd:ad]; result(nil); + } else if ([call.method isEqualToString:@"loadAdLoaderAd"]) { + FLTAdRequest *request; + if ([FLTAdUtil isNotNull:call.arguments[@"request"]]) { + request = call.arguments[@"request"]; + } else if ([FLTAdUtil isNotNull:call.arguments[@"adManagerRequest"]]) { + request = call.arguments[@"adManagerRequest"]; + } + + FLTAdLoaderAd *ad = + [[FLTAdLoaderAd alloc] initWithAdUnitId:call.arguments[@"adUnitId"] + request:request + rootViewController:rootController + adId:call.arguments[@"adId"]]; + [_manager loadAd:ad]; + result(nil); } else if ([call.method isEqualToString:@"loadInterstitialAd"]) { FLTInterstitialAd *ad = [[FLTInterstitialAd alloc] initWithAdUnitId:call.arguments[@"adUnitId"] @@ -517,6 +532,31 @@ - (void)handleMethodCall:(FlutterMethodCall *)call } else { result(nil); } + } else if ([call.method isEqualToString:@"getAdLoaderAdType"]) { + id ad = [_manager adFor:call.arguments[@"adId"]]; + if ([FLTAdUtil isNull:ad]) { + // Called on an ad that hasn't been loaded yet. + result(nil); + } + if ([ad isKindOfClass:[FLTAdLoaderAd class]]) { + FLTAdLoaderAd *adLoaderAd = (FLTAdLoaderAd *)ad; + FLTAdLoaderAdType adLoaderType = [adLoaderAd adLoaderAdType]; + result([[NSNumber alloc] initWithInteger:adLoaderType]); + } else { + result(FlutterMethodNotImplemented); + } + } else if ([call.method isEqualToString:@"getFormatId"]) { + id ad = [_manager adFor:call.arguments[@"adId"]]; + if ([FLTAdUtil isNull:ad]) { + // Called on an ad that hasn't been loaded yet. + result(nil); + } + if ([ad isKindOfClass:[FLTAdLoaderAd class]]) { + FLTAdLoaderAd *adLoaderAd = (FLTAdLoaderAd *)ad; + result([adLoaderAd formatId]); + } else { + result(FlutterMethodNotImplemented); + } } else if ([call.method isEqualToString:@"getAdSize"]) { id ad = [_manager adFor:call.arguments[@"adId"]]; if ([FLTAdUtil isNull:ad]) { @@ -526,6 +566,9 @@ - (void)handleMethodCall:(FlutterMethodCall *)call if ([ad isKindOfClass:[FLTBannerAd class]]) { FLTBannerAd *bannerAd = (FLTBannerAd *)ad; result([bannerAd getAdSize]); + } else if ([ad isKindOfClass:[FLTAdLoaderAd class]]) { + FLTAdLoaderAd *adLoaderAd = (FLTAdLoaderAd *)ad; + result([adLoaderAd adSize]); } else { result(FlutterMethodNotImplemented); } diff --git a/packages/google_mobile_ads/lib/src/ad_containers.dart b/packages/google_mobile_ads/lib/src/ad_containers.dart index 80d679c15..b6f8fa441 100644 --- a/packages/google_mobile_ads/lib/src/ad_containers.dart +++ b/packages/google_mobile_ads/lib/src/ad_containers.dart @@ -1051,6 +1051,77 @@ class NativeAd extends AdWithView { } } +/// Type of ad served by [AdLoaderAd] +enum AdLoaderAdType { + /// Unknown ad type + unknown, +} + +/// An AdLoaderAd. +/// +/// A widget which uses the platforms' ad loader (an [AdLoader] +/// (https://developers.google.com/android/reference/com/google/android/gms/ads/AdLoader) +/// on Android, or a [GADAdLoader] +/// (https://developers.google.com/ad-manager/mobile-ads-sdk/ios/api/reference/Classes/GADAdLoader) +/// on iOS) to allow receiving multiple ad types for a given request. +/// +/// These types are: +/// +/// * A "banner" ad, ([AdManagerAdView] +/// (https://developers.google.com/android/reference/com/google/android/gms/ads/admanager/AdManagerAdView) +/// on Android and [GAMBannerView] +/// (https://developers.google.com/ad-manager/mobile-ads-sdk/ios/api/reference/Classes/GAMBannerView.html) +/// on iOS) +/// +/// * A "custom" ad, ([NativeCustomFormatAd] +/// (https://developers.google.com/android/reference/com/google/android/gms/ads/nativead/NativeCustomFormatAd) +/// on Android and [GADCustomNativeAd] +/// (https://developers.google.com/admob/ios/api/reference/Classes/GADCustomNativeAd.html) on iOS) +/// +/// * A "native" ad, ([NativeAd] +/// (https://developers.google.com/android/reference/com/google/android/gms/ads/nativead/NativeAd) +/// on Android and [GADNativeAd] +/// (https://developers.google.com/admob/ios/api/reference/Classes/GADNativeAd.html) on iOS) +class AdLoaderAd extends AdWithView { + /// Creates an [AdLoaderAd] + /// + /// A valid [adUnitId], nonnull [listener] and nonnull [request] are required. + AdLoaderAd({ + required String adUnitId, + required this.listener, + required AdRequest request, + }) : super(adUnitId: adUnitId, listener: listener) { + if (request is AdManagerAdRequest) { + adManagerRequest = request; + } else { + this.request = request; + } + } + + /// A listener for receiving events in the ad lifecycle. + @override + final AdLoaderAdListener listener; + + /// Targeting information used to fetch an [Ad]. + AdRequest? request; + + /// Targeting information used to fetch an [Ad] with Ad Manager. + AdManagerAdRequest? adManagerRequest; + + @override + Future load() => instanceManager.loadAdLoaderAd(this); + + /// Returns the AdLoaderAdType of the currently served ad. + Future getAdLoaderAdType() => + instanceManager.getAdLoaderAdType(this); + + /// Returns the AdSize of the associated platform ad object. + Future getPlatformAdSize() => instanceManager.getAdSize(this); + + /// Returns the formatId of the served Custom ad. + Future getFormatId() => instanceManager.getFormatId(this); +} + /// A full-screen interstitial ad for the Google Mobile Ads Plugin. class InterstitialAd extends AdWithoutView { /// Creates an [InterstitialAd]. 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 3bb29bcb6..934be2b2c 100644 --- a/packages/google_mobile_ads/lib/src/ad_instance_manager.dart +++ b/packages/google_mobile_ads/lib/src/ad_instance_manager.dart @@ -450,6 +450,41 @@ class AdInstanceManager { }, ); + Future getFormatId(Ad ad) => + instanceManager.channel.invokeMethod( + 'getFormatId', + { + 'adId': adIdFor(ad), + }, + ); + + Future getAdLoaderAdType(Ad ad) async { + int adLoaderAdType = (await instanceManager.channel.invokeMethod( + 'getAdLoaderAdType', + { + 'adId': adIdFor(ad), + }, + ))!; + + if (defaultTargetPlatform == TargetPlatform.iOS) { + switch (adLoaderAdType) { + case 0: + return AdLoaderAdType.unknown; + default: + debugPrint('Error: unknown AdLoaderAdType value: $adLoaderAdType'); + return AdLoaderAdType.unknown; + } + } else { + switch (adLoaderAdType) { + case 0: + return AdLoaderAdType.unknown; + default: + debugPrint('Error: unknown AdLoaderAdType value: $adLoaderAdType'); + return AdLoaderAdType.unknown; + } + } + } + /// Returns null if an invalid [adId] was passed in. Ad? adFor(int adId) => _loadedAds[adId]; @@ -530,6 +565,27 @@ class AdInstanceManager { ); } + /// Starts loading the ad if not previously loaded. + /// + /// Loading also terminates if ad is already in the process of loading. + Future loadAdLoaderAd(AdLoaderAd ad) { + if (adIdFor(ad) != null) { + return Future.value(); + } + + final int adId = _nextAdId++; + _loadedAds[adId] = ad; + return channel.invokeMethod( + 'loadAdLoaderAd', + { + 'adId': adId, + 'adUnitId': ad.adUnitId, + 'request': ad.request, + 'adManagerRequest': ad.adManagerRequest, + }, + ); + } + /// Starts loading the ad if not previously loaded. /// /// Loading also terminates if ad is already in the process of loading. diff --git a/packages/google_mobile_ads/lib/src/ad_listeners.dart b/packages/google_mobile_ads/lib/src/ad_listeners.dart index 5677a2469..9f64e1be3 100644 --- a/packages/google_mobile_ads/lib/src/ad_listeners.dart +++ b/packages/google_mobile_ads/lib/src/ad_listeners.dart @@ -221,6 +221,42 @@ class NativeAdListener extends AdWithViewListener { onAdClicked: onAdClicked); } +/// A listener for receiving notifications for the lifecycle of an [AdLoaderAd] +class AdLoaderAdListener extends AdWithViewListener { + /// Constructs an [AdLoaderAdListener] with the provided event callbacks. + /// + /// Typically you will override [onAdLoaded] and [onAdFailedToLoad]: + /// ```dart + /// AdLoaderAdListener( + /// onAdLoaded: (ad) { + /// // Ad successfully loaded - display an AdWidget with the ad. + /// }, + /// onAdFailedToLoad: (ad, error) { + /// // Ad failed to load - log the error and dispose the ad. + /// }, + /// ... + /// ) + /// ``` + AdLoaderAdListener({ + AdEventCallback? onAdLoaded, + AdLoadErrorCallback? onAdFailedToLoad, + AdEventCallback? onAdOpened, + AdEventCallback? onAdWillDismissScreen, + AdEventCallback? onAdClosed, + AdEventCallback? onAdImpression, + OnPaidEventCallback? onPaidEvent, + AdEventCallback? onAdClicked, + }) : super( + onAdLoaded: onAdLoaded, + onAdFailedToLoad: onAdFailedToLoad, + onAdOpened: onAdOpened, + onAdWillDismissScreen: onAdWillDismissScreen, + onAdClosed: onAdClosed, + onAdImpression: onAdImpression, + onPaidEvent: onPaidEvent, + onAdClicked: onAdClicked); +} + /// Callback events for for full screen ads, such as Rewarded and Interstitial. class FullScreenContentCallback { /// Construct a new [FullScreenContentCallback]. diff --git a/packages/google_mobile_ads/test/ad_containers_test.dart b/packages/google_mobile_ads/test/ad_containers_test.dart index f67ed656f..74a27430e 100644 --- a/packages/google_mobile_ads/test/ad_containers_test.dart +++ b/packages/google_mobile_ads/test/ad_containers_test.dart @@ -49,6 +49,7 @@ void main() { case 'setImmersiveMode': case 'loadBannerAd': case 'loadNativeAd': + case 'loadAdLoaderAd': case 'showAdWithoutView': case 'disposeAd': case 'loadRewardedAd': diff --git a/packages/google_mobile_ads/test/ad_loader_ad_test.dart b/packages/google_mobile_ads/test/ad_loader_ad_test.dart new file mode 100644 index 000000000..ba96b4991 --- /dev/null +++ b/packages/google_mobile_ads/test/ad_loader_ad_test.dart @@ -0,0 +1,264 @@ +// 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 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_mobile_ads/src/ad_containers.dart'; +import 'package:google_mobile_ads/src/ad_instance_manager.dart'; +import 'package:google_mobile_ads/src/ad_listeners.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Ad Loader Ad Tests', () { + final List log = []; + + setUp(() async { + log.clear(); + instanceManager = + AdInstanceManager('plugins.flutter.io/google_mobile_ads'); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(instanceManager.channel, + (MethodCall methodCall) async { + log.add(methodCall); + switch (methodCall.method) { + case 'getAdLoaderAdType': + return Future.value(0); + case 'getAdSize': + return Future.value(AdSize.banner); + case 'getFormatId': + return Future.value('format-id'); + case 'loadAdLoaderAd': + return Future.value(); + default: + assert(false); + return null; + } + }); + }); + + test('load with $AdRequest', () async { + final AdRequest request = AdRequest(); + final AdLoaderAd adLoaderAd = AdLoaderAd( + adUnitId: 'test-ad-unit', + listener: AdLoaderAdListener(), + request: request, + ); + + await adLoaderAd.load(); + + expect(log, [ + isMethodCall('loadAdLoaderAd', arguments: { + 'adId': 0, + 'adUnitId': 'test-ad-unit', + 'request': request, + 'adManagerRequest': null, + }) + ]); + + expect(instanceManager.adFor(0), isNotNull); + }); + + test('load with $AdManagerAdRequest', () async { + final AdManagerAdRequest request = AdManagerAdRequest(); + final AdLoaderAd adLoaderAd = AdLoaderAd( + adUnitId: 'test-ad-unit', + listener: AdLoaderAdListener(), + request: request, + ); + + await adLoaderAd.load(); + + expect(log, [ + isMethodCall('loadAdLoaderAd', arguments: { + 'adId': 0, + 'adUnitId': 'test-ad-unit', + 'request': null, + 'adManagerRequest': request, + }) + ]); + + expect(instanceManager.adFor(0), isNotNull); + }); + + test('getAdLoaderAdType delegates to $MethodChannel', () async { + final AdLoaderAd adLoaderAd = AdLoaderAd( + adUnitId: 'test-ad-unit', + listener: AdLoaderAdListener(), + request: AdRequest(), + ); + + final result = await adLoaderAd.getAdLoaderAdType(); + + expect(result, equals(AdLoaderAdType.unknown)); + + expect(log, [ + isMethodCall('getAdLoaderAdType', arguments: { + 'adId': null, + }) + ]); + }); + + test('getFormatId delegates to $MethodChannel', () async { + final AdLoaderAd adLoaderAd = AdLoaderAd( + adUnitId: 'test-ad-unit', + listener: AdLoaderAdListener(), + request: AdRequest(), + ); + + final result = await adLoaderAd.getFormatId(); + + expect(result, equals('format-id')); + + expect(log, [ + isMethodCall('getFormatId', arguments: { + 'adId': null, + }) + ]); + }); + + test('getPlatformAdSize delegates to $MethodChannel', () async { + final AdLoaderAd adLoaderAd = AdLoaderAd( + adUnitId: 'test-ad-unit', + listener: AdLoaderAdListener(), + request: AdRequest(), + ); + + final result = await adLoaderAd.getPlatformAdSize(); + + expect(result, equals(AdSize.banner)); + + expect(log, [ + isMethodCall('getAdSize', arguments: { + 'adId': null, + }) + ]); + }); + + test('onAdLoaded event', () async { + var testOnAdLoaded = (eventName, adId) async { + final Completer completer = Completer(); + + final AdLoaderAd ad = AdLoaderAd( + adUnitId: 'test-ad-unit', + listener: + AdLoaderAdListener(onAdLoaded: (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 testOnAdLoaded('onAdLoaded', 0); + + debugDefaultTargetPlatformOverride = TargetPlatform.android; + await testOnAdLoaded('onAdLoaded', 1); + }); + + test('onAdFailedToLoad event', () async { + var testOnAdFailedToLoad = (eventName, adId) async { + final Completer completer = Completer(); + + final AdLoaderAd ad = AdLoaderAd( + adUnitId: 'test-ad-unit', + listener: AdLoaderAdListener( + onAdFailedToLoad: (ad, error) => completer.complete(ad)), + request: AdRequest(), + ); + + await ad.load(); + + final MethodCall methodCall = + MethodCall('onAdEvent', { + 'adId': adId, + 'eventName': eventName, + 'loadAdError': LoadAdError(0, '', '', null) + }); + + 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 testOnAdFailedToLoad('onAdFailedToLoad', 0); + + debugDefaultTargetPlatformOverride = TargetPlatform.android; + await testOnAdFailedToLoad('onAdFailedToLoad', 1); + }); + + test('onAdClicked event', () async { + var testOnAdClicked = (eventName, adId) async { + final Completer completer = Completer(); + + final AdLoaderAd ad = AdLoaderAd( + adUnitId: 'test-ad-unit', + listener: + AdLoaderAdListener(onAdClicked: (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 testOnAdClicked('adDidRecordClick', 0); + + debugDefaultTargetPlatformOverride = TargetPlatform.android; + await testOnAdClicked('onAdClicked', 1); + }); + }); +} From ee100193b5713122daf0ff494e0ac2af8c3cdc3f Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 25 Oct 2022 16:50:48 +1100 Subject: [PATCH 5/7] Allow serving `Banner` ads in `AdLoaderAd` Allow the `AdLoaderAd` instance to serve `Banner` ads, which are instances of: * `AdManagerAdView` under Android, and * `GAMBannerView` under iOS --- .../googlemobileads/AdMessageCodec.java | 17 ++ .../googlemobileads/FlutterAdListener.java | 19 ++ .../googlemobileads/FlutterAdLoader.java | 30 ++- .../googlemobileads/FlutterAdLoaderAd.java | 79 ++++++- .../FlutterAdManagerAdViewOptions.java | 37 ++++ .../FlutterBannerParameters.java | 46 ++++ .../GoogleMobileAdsPlugin.java | 1 + .../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 +++++ .../ios/Classes/FLTAd_Internal.h | 33 ++- .../ios/Classes/FLTAd_Internal.m | 142 +++++++++++- .../ios/Classes/FLTGoogleMobileAdsPlugin.m | 3 +- .../FLTGoogleMobileAdsReaderWriter_Internal.m | 24 +- .../lib/src/ad_containers.dart | 51 +++++ .../lib/src/ad_instance_manager.dart | 23 ++ .../test/ad_loader_ad_test.dart | 208 ++++++++++++++++++ .../test/mobile_ads_test.dart | 51 +++++ 23 files changed, 1147 insertions(+), 34 deletions(-) create mode 100644 packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdViewOptions.java create mode 100644 packages/google_mobile_ads/android/src/main/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/main/java/io/flutter/plugins/googlemobileads/AdMessageCodec.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdMessageCodec.java index 1ea1eaa3b..9fd729a3c 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 @@ -68,6 +68,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; @@ -253,6 +255,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); } @@ -434,6 +445,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/FlutterAdListener.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java index 065f74a98..cd0efbefe 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java @@ -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/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java index d95a6a6b2..22241841c 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java @@ -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/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java index ac0abc2a9..0ac49f000 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java @@ -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,12 +107,35 @@ FlutterAdLoaderAd build() { } else { adLoaderAd = new FlutterAdLoaderAd(id, manager, adUnitId, request, adLoader); } + + if (bannerParameters != null) { + adLoaderAd.bannerParameters = + bannerParameters.asBannerParameters( + new FlutterAdManagerAdViewLoadedListener(adLoaderAd)); + } + return adLoaderAd; } } enum AdLoaderAdType { UNKNOWN, + BANNER, + } + + 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( @@ -140,13 +178,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; } @@ -160,7 +199,10 @@ AdLoaderAdType getAdLoaderAdType() { @Nullable FlutterAdSize getAdSize() { - return null; + if (view == null || !(view instanceof AdManagerAdView)) { + return null; + } + return new FlutterAdSize(((AdManagerAdView) view).getAdSize()); } @Nullable @@ -169,5 +211,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/main/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdViewOptions.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdManagerAdViewOptions.java new file mode 100644 index 000000000..03cf55985 --- /dev/null +++ b/packages/google_mobile_ads/android/src/main/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/main/java/io/flutter/plugins/googlemobileads/FlutterBannerParameters.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterBannerParameters.java new file mode 100644 index 000000000..a81562ed4 --- /dev/null +++ b/packages/google_mobile_ads/android/src/main/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/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java index 045f67f07..fb4cf35ad 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 @@ -456,6 +456,7 @@ public void onAdInspectorClosed(@Nullable AdInspectorError adInspectorError) { 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/test/java/io/flutter/plugins/googlemobileads/AdMessageCodecTest.java b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/AdMessageCodecTest.java index 56fb41324..54dfb86f7 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..47cc72115 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(), FlutterAdLoaderAd.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 16f145298..cb76b5109 100644 --- a/packages/google_mobile_ads/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_mobile_ads/example/ios/Runner.xcodeproj/project.pbxproj @@ -38,6 +38,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 */ @@ -152,6 +153,7 @@ 9EF4E6FF26392B230007E4FE /* FLTMobileAds_Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = FLTMobileAds_Internal.h; path = ../../ios/Classes/FLTMobileAds_Internal.h; 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 */ @@ -275,6 +277,7 @@ 9E61AA5829BBE8DA00801A83 /* NativeTemplates */, 9E61AA5529BBE8AF00801A83 /* UserMessagingPlatform */, E276E99129DFB5870052484E /* FLTAdLoaderAdTest.m */, + E2D4625E29DFC2560010C4D0 /* FLTAdManagerAdViewOptionsTest.m */, 9E61AA2729BBE66900801A83 /* FLTAdUtilTest.m */, 9E61AA2429BBE66900801A83 /* FLTAppOpenAdTest.m */, 9E61AA2229BBE66900801A83 /* FLTBannerAdTest.m */, @@ -518,6 +521,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 5ff06833a..62623b135 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..bece32d46 --- /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 "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).enableManualImpressions); +} + +- (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).enableManualImpressions); +} +@end diff --git a/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m b/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m index e60d75356..dc53d1ace 100644 --- a/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m +++ b/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m @@ -777,6 +777,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/Classes/FLTAd_Internal.h b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h index a907add7e..77d53dddd 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h +++ b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h @@ -295,6 +295,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 @@ -312,19 +321,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/ios/Classes/FLTAd_Internal.m b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m index d4fcb1036..49cd24e59 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m +++ b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m @@ -1213,30 +1213,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 @@ -1246,6 +1275,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 { @@ -1265,10 +1301,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; @@ -1447,3 +1554,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.enableManualImpressions = + _manualImpressionsEnabled.boolValue; + [options addObject:bannerViewOptions]; + } + return options; +} +@end diff --git a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m index 056e2f61b..69fc8a903 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m +++ b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m @@ -434,7 +434,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/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m index 24572e60f..ede50dafc 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m +++ b/packages/google_mobile_ads/ios/Classes/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 @@ -341,6 +342,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]; } @@ -524,6 +535,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/lib/src/ad_containers.dart b/packages/google_mobile_ads/lib/src/ad_containers.dart index b6f8fa441..dca90a36d 100644 --- a/packages/google_mobile_ads/lib/src/ad_containers.dart +++ b/packages/google_mobile_ads/lib/src/ad_containers.dart @@ -1055,6 +1055,9 @@ class NativeAd extends AdWithView { enum AdLoaderAdType { /// Unknown ad type unknown, + + /// Banner ad type + banner, } /// An AdLoaderAd. @@ -1090,6 +1093,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; @@ -1108,6 +1112,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); @@ -1524,6 +1531,50 @@ 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 934be2b2c..165203c27 100644 --- a/packages/google_mobile_ads/lib/src/ad_instance_manager.dart +++ b/packages/google_mobile_ads/lib/src/ad_instance_manager.dart @@ -470,6 +470,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; @@ -478,6 +480,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; @@ -582,6 +586,7 @@ class AdInstanceManager { 'adUnitId': ad.adUnitId, 'request': ad.request, 'adManagerRequest': ad.adManagerRequest, + 'banner': ad.banner, }, ); } @@ -915,6 +920,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) { @@ -1051,6 +1058,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); } @@ -1275,6 +1289,15 @@ class AdMessageCodec extends StandardMessageCodec { case _valueNativeTemplateFontStyle: return NativeTemplateFontStyle .values[readValueOfType(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 ba96b4991..263fbd356 100644 --- a/packages/google_mobile_ads/test/ad_loader_ad_test.dart +++ b/packages/google_mobile_ads/test/ad_loader_ad_test.dart @@ -67,6 +67,7 @@ void main() { 'adUnitId': 'test-ad-unit', 'request': request, 'adManagerRequest': null, + 'banner': null, }) ]); @@ -89,6 +90,32 @@ 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, }) ]); @@ -225,6 +252,187 @@ 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 9addf96b7..65c0c0f4c 100644 --- a/packages/google_mobile_ads/test/mobile_ads_test.dart +++ b/packages/google_mobile_ads/test/mobile_ads_test.dart @@ -584,5 +584,56 @@ 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 ded6483e8bb6e29d65fca3ef427d9107e3982466 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 24 Oct 2022 12:57:24 +1100 Subject: [PATCH 6/7] Allow serving `Custom` ads in `AdLoaderAd` Allow the `AdLoaderAd` instance to serve `Custom` ads, which are instances of: * `NativeCustomFormatAd` under Android, and * `GADCustomNativeAd` under iOS --- .../googlemobileads/AdMessageCodec.java | 10 +++ .../googlemobileads/FlutterAdListener.java | 19 +++++ .../googlemobileads/FlutterAdLoader.java | 16 +++- .../googlemobileads/FlutterAdLoaderAd.java | 60 +++++++++++++- .../FlutterCustomParameters.java | 30 +++++++ .../GoogleMobileAdsPlugin.java | 70 ++++++++++++++++ .../googlemobileads/AdMessageCodecTest.java | 16 ++++ .../FlutterAdLoaderAdTest.java | 80 +++++++++++++++++-- .../ios/RunnerTests/FLTAdLoaderAdTest.m | 71 +++++++++++++++- .../FLTGoogleMobileAdsReaderWriterTest.m | 27 +++++++ .../Classes/FLTAdInstanceManager_Internal.h | 4 + .../Classes/FLTAdInstanceManager_Internal.m | 16 ++++ .../ios/Classes/FLTAd_Internal.h | 17 +++- .../ios/Classes/FLTAd_Internal.m | 69 +++++++++++++++- .../ios/Classes/FLTGoogleMobileAdsPlugin.h | 15 ++++ .../ios/Classes/FLTGoogleMobileAdsPlugin.m | 65 ++++++++++++++- .../FLTGoogleMobileAdsReaderWriter_Internal.m | 11 +++ .../lib/src/ad_containers.dart | 34 ++++++++ .../lib/src/ad_instance_manager.dart | 20 +++++ .../test/ad_loader_ad_test.dart | 29 +++++++ .../test/mobile_ads_test.dart | 30 +++++++ 21 files changed, 691 insertions(+), 18 deletions(-) create mode 100644 packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdMessageCodec.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdMessageCodec.java index 9fd729a3c..1717c95c2 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_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; @@ -264,6 +265,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); } @@ -451,6 +457,10 @@ protected Object readValueOfType(byte type, ByteBuffer buffer) { return new FlutterBannerParameters( (List) readValueOfType(buffer.get(), buffer), (FlutterAdManagerAdViewOptions) readValueOfType(buffer.get(), buffer)); + case VALUE_CUSTOM_PARAMETERS: + return new FlutterCustomParameters( + (List) readValueOfType(buffer.get(), buffer), + (Map) readValueOfType(buffer.get(), buffer)); default: return super.readValueOfType(type, buffer); } diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java index cd0efbefe..1133e46e9 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java @@ -20,6 +20,8 @@ import com.google.android.gms.ads.formats.OnAdManagerAdViewLoadedListener; import com.google.android.gms.ads.nativead.NativeAd; import com.google.android.gms.ads.nativead.NativeAd.OnNativeAdLoadedListener; +import com.google.android.gms.ads.nativead.NativeCustomFormatAd; +import com.google.android.gms.ads.nativead.NativeCustomFormatAd.OnCustomFormatAdLoadedListener; import java.lang.ref.WeakReference; /** Callback type to notify when an ad successfully loads. */ @@ -137,3 +139,20 @@ public void onAdManagerAdViewLoaded(AdManagerAdView adView) { } } } + +/** {@link OnCustomFormatAdLoadedListener} for custom ads. */ +class FlutterCustomFormatAdLoadedListener implements OnCustomFormatAdLoadedListener { + + private final WeakReference reference; + + FlutterCustomFormatAdLoadedListener(OnCustomFormatAdLoadedListener listener) { + reference = new WeakReference<>(listener); + } + + @Override + public void onCustomFormatAdLoaded(NativeCustomFormatAd ad) { + if (reference.get() != null) { + reference.get().onCustomFormatAdLoaded(ad); + } + } +} diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java index 22241841c..7efbaccb0 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java @@ -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/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java index 0ac49f000..56d3c89d7 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java @@ -24,13 +24,18 @@ import com.google.android.gms.ads.admanager.AdManagerAdView; import com.google.android.gms.ads.formats.AdManagerAdViewOptions; import com.google.android.gms.ads.formats.OnAdManagerAdViewLoadedListener; +import com.google.android.gms.ads.nativead.NativeCustomFormatAd; +import com.google.android.gms.ads.nativead.NativeCustomFormatAd.OnCustomFormatAdLoadedListener; import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.CustomAdFactory; +import java.util.Map; /** * A central wrapper for {@link AdManagerAdView}, {@link NativeCustomFormatAd} and {@link NativeAd} * instances served for a single {@link AdRequest} or {@link AdManagerAdRequest} */ -class FlutterAdLoaderAd extends FlutterAd implements OnAdManagerAdViewLoadedListener { +class FlutterAdLoaderAd extends FlutterAd + implements OnAdManagerAdViewLoadedListener, OnCustomFormatAdLoadedListener { private static final String TAG = "FlutterAdLoaderAd"; @NonNull private final AdInstanceManager manager; @@ -42,6 +47,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 +57,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 +95,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 +133,12 @@ FlutterAdLoaderAd build() { new FlutterAdManagerAdViewLoadedListener(adLoaderAd)); } + if (customParameters != null) { + adLoaderAd.customParameters = + customParameters.asCustomParameters( + new FlutterCustomFormatAdLoadedListener(adLoaderAd), customFactories); + } + return adLoaderAd; } } @@ -121,6 +146,7 @@ FlutterAdLoaderAd build() { enum AdLoaderAdType { UNKNOWN, BANNER, + CUSTOM, } static class BannerParameters { @@ -138,6 +164,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, @@ -179,13 +220,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; } @@ -227,6 +272,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/main/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java new file mode 100644 index 000000000..3086c7084 --- /dev/null +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java @@ -0,0 +1,30 @@ +package io.flutter.plugins.googlemobileads; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.ads.nativead.NativeCustomFormatAd.OnCustomFormatAdLoadedListener; +import io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.CustomAdFactory; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class FlutterCustomParameters { + @NonNull final List formatIds; + @Nullable final Map viewOptions; + + FlutterCustomParameters( + @NonNull List formatIds, @Nullable Map viewOptions) { + this.formatIds = formatIds; + this.viewOptions = viewOptions; + } + + FlutterAdLoaderAd.CustomParameters asCustomParameters( + @NonNull OnCustomFormatAdLoadedListener listener, + @NonNull Map availableFactories) { + Map factories = new HashMap<>(); + for (String formatId : formatIds) { + factories.put(formatId, availableFactories.get(formatId)); + } + return new FlutterAdLoaderAd.CustomParameters(listener, factories, viewOptions); + } +} diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java index fb4cf35ad..f7b65adb5 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java @@ -16,6 +16,7 @@ import android.content.Context; import android.util.Log; +import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -28,6 +29,7 @@ import com.google.android.gms.ads.initialization.OnInitializationCompleteListener; import com.google.android.gms.ads.nativead.NativeAd; import com.google.android.gms.ads.nativead.NativeAdView; +import com.google.android.gms.ads.nativead.NativeCustomFormatAd; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; @@ -69,6 +71,7 @@ private static T requireNonNull(T obj) { @Nullable private AppStateNotifier appStateNotifier; @Nullable private UserMessagingPlatformManager userMessagingPlatformManager; private final Map nativeAdFactories = new HashMap<>(); + private final Map customAdFactories = new HashMap<>(); @Nullable private MediationNetworkExtrasProvider mediationNetworkExtrasProvider; private final FlutterMobileAdsWrapper flutterMobileAds; @Nullable private Supplier adLoaderSupplier; @@ -129,6 +132,11 @@ public interface NativeAdFactory { NativeAdView createNativeAd(NativeAd nativeAd, Map customOptions); } + public interface CustomAdFactory { + View createCustomAd( + @NonNull NativeCustomFormatAd nativeAd, @Nullable Map customOptions); + } + /** * Registers a {@link io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.NativeAdFactory} * used to create {@link com.google.android.gms.ads.nativead.NativeAdView}s from a Native Ad @@ -148,6 +156,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. @@ -208,6 +223,19 @@ private static boolean registerNativeAdFactory( return plugin.addNativeAdFactory(factoryId, nativeAdFactory); } + private static boolean registerCustomAdFactory( + GoogleMobileAdsPlugin plugin, String formatId, CustomAdFactory customAdFactory) { + if (plugin == null) { + final String message = + String.format( + "Could not find a %s instance. The plugin may have not been registered.", + GoogleMobileAdsPlugin.class.getSimpleName()); + throw new IllegalStateException(message); + } + + return plugin.addCustomAdFactory(formatId, customAdFactory); + } + /** * Unregisters a {@link io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.NativeAdFactory} * used to create {@link com.google.android.gms.ads.nativead.NativeAdView}s from a Native Ad @@ -230,6 +258,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 = @@ -247,6 +285,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; @@ -445,6 +500,19 @@ public void onAdInspectorClosed(@Nullable AdInspectorError adInspectorError) { result.success(null); break; case "loadAdLoaderAd": + final FlutterCustomParameters customParameters = + call.argument("custom"); + if (customParameters != null) { + for (String formatId : customParameters.formatIds) { + if (customAdFactories.get(formatId) == null) { + final String message = + String.format("Can't find CustomAdFactory with id: %s", formatId); + result.error("AdLoaderAdError", message, null); + return; + } + } + } + final FlutterAdLoaderAd adLoaderAd = new FlutterAdLoaderAd.Builder() .setManager(instanceManager) @@ -457,6 +525,8 @@ public void onAdInspectorClosed(@Nullable AdInspectorError adInspectorError) { ? 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/test/java/io/flutter/plugins/googlemobileads/AdMessageCodecTest.java b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/AdMessageCodecTest.java index 54dfb86f7..0517f334f 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 47cc72115..95624efe3 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,11 @@ import com.google.android.gms.ads.ResponseInfo; import com.google.android.gms.ads.admanager.AdManagerAdRequest; import com.google.android.gms.ads.admanager.AdManagerAdView; +import com.google.android.gms.ads.nativead.NativeCustomFormatAd; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.googlemobileads.FlutterAd.FlutterLoadAdError; +import io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.CustomAdFactory; +import java.util.Collections; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -84,12 +87,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 +132,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 +189,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 +203,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 +214,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(), FlutterAdLoaderAd.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 62623b135..483f89c89 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 dc53d1ace..dec432037 100644 --- a/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m +++ b/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m @@ -833,6 +833,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/Classes/FLTAdInstanceManager_Internal.h b/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.h index 459356b84..72f108bf2 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.h +++ b/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.h @@ -44,6 +44,10 @@ - (void)onNativeAdWillPresentScreen:(nonnull id)ad; - (void)onNativeAdDidDismissScreen:(nonnull id)ad; - (void)onNativeAdWillDismissScreen:(nonnull id)ad; +- (void)onCustomNativeAdImpression:(nonnull id)ad; +- (void)onCustomNativeAdWillPresentScreen:(nonnull id)ad; +- (void)onCustomNativeAdDidDismissScreen:(nonnull id)ad; +- (void)onCustomNativeAdWillDismissScreen:(nonnull id)ad; - (void)onRewardedAdUserEarnedReward:(FLTRewardedAd *_Nonnull)ad reward:(FLTRewardItem *_Nonnull)reward; - (void)onRewardedInterstitialAdUserEarnedReward: diff --git a/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.m b/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.m index f57a655ee..c50290ee6 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.m +++ b/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.m @@ -126,6 +126,22 @@ - (void)onNativeAdWillDismissScreen:(nonnull id)ad { [self sendAdEvent:@"onNativeAdWillDismissScreen" ad:ad]; } +- (void)onCustomNativeAdImpression:(nonnull id)ad { + [self sendAdEvent:@"onCustomNativeAdImpression" ad:ad]; +} + +- (void)onCustomNativeAdWillPresentScreen:(nonnull id)ad { + [self sendAdEvent:@"onCustomNativeAdWillPresentScreen" ad:ad]; +} + +- (void)onCustomNativeAdDidDismissScreen:(nonnull id)ad { + [self sendAdEvent:@"onCustomNativeAdDidDismissScreen" ad:ad]; +} + +- (void)onCustomNativeAdWillDismissScreen:(nonnull id)ad { + [self sendAdEvent:@"onCustomNativeAdWillDismissScreen" ad:ad]; +} + - (void)onRewardedAdUserEarnedReward:(FLTRewardedAd *_Nonnull)ad reward:(FLTRewardItem *_Nonnull)reward { [_channel invokeMethod:@"onAdEvent" diff --git a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h index 77d53dddd..924c03852 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h +++ b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h @@ -24,6 +24,7 @@ @class FLTAdInstanceManager; @protocol FLTNativeAdFactory; +@protocol FLTCustomAdFactory; @interface FLTAdSize : NSObject @property(readonly) GADAdSize size; @@ -322,6 +323,7 @@ typedef NS_ENUM(NSInteger, FLTAdLoaderAdType) { FLTAdLoaderAdTypeUnknown = 0, FLTAdLoaderAdTypeBanner = 1, + FLTAdLoaderAdTypeCustom = 2, }; @interface FLTBannerParameters : NSObject @@ -332,10 +334,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; @@ -345,7 +357,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/Classes/FLTAd_Internal.m b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m index 49cd24e59..885786358 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m +++ b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m @@ -1224,6 +1224,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 { @@ -1232,6 +1244,7 @@ @implementation FLTAdLoaderAd { NSMutableArray *_validAdSizes; UIView *_view; FLTBannerParameters *_banner; + FLTCustomParameters *_custom; } - (nonnull instancetype) @@ -1239,7 +1252,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; @@ -1266,6 +1280,12 @@ @implementation FLTAdLoaderAd { } } + if (![FLTAdUtil isNull:customParameters]) { + _custom = customParameters; + + [adTypes addObject:GADAdLoaderAdTypeCustomNative]; + } + _adLoader = [[GADAdLoader alloc] initWithAdUnitID:_adUnitId rootViewController:rootViewController adTypes:adTypes @@ -1372,6 +1392,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/Classes/FLTGoogleMobileAdsPlugin.h b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.h index f01782088..e2c4e1a01 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.h +++ b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.h @@ -46,6 +46,13 @@ (NSDictionary *_Nullable)customOptions; @end +@protocol FLTCustomAdFactory +@required +- (nullable UIView *) + createCustomNativeAd:(nonnull GADCustomNativeAd *)customNativeAd + customOptions:(nullable NSDictionary *)customOptions; +@end + /** * Flutter plugin providing access to the Google Mobile Ads API. */ @@ -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/ios/Classes/FLTGoogleMobileAdsPlugin.m b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m index 69fc8a903..1a2039948 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m +++ b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m @@ -24,6 +24,8 @@ @interface FLTGoogleMobileAdsPlugin () @property(nonatomic, retain) FlutterMethodChannel *channel; @property NSMutableDictionary> *nativeAdFactories; +@property NSMutableDictionary> + *customAdFactories; @end /// Initialization handler for GMASDK. Invokes result at most once. @@ -66,6 +68,7 @@ - (void)handleInitializationComplete:(GADInitializationStatus *_Nonnull)status { @implementation FLTGoogleMobileAdsPlugin { NSMutableDictionary> *_nativeAdFactories; + NSMutableDictionary> *_customAdFactories; FLTAdInstanceManager *_manager; id _mediationNetworkExtrasProvider; FLTGoogleMobileAdsReaderWriter *_readerWriter; @@ -117,6 +120,7 @@ - (instancetype)initWithBinaryMessenger: self = [self init]; if (self) { _nativeAdFactories = [NSMutableDictionary dictionary]; + _customAdFactories = [NSMutableDictionary dictionary]; _manager = [[FLTAdInstanceManager alloc] initWithBinaryMessenger:binaryMessenger]; _appStateNotifier = @@ -198,6 +202,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 { @@ -211,6 +242,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 = UIApplication.sharedApplication.delegate.window.rootViewController; @@ -423,6 +467,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"]; @@ -435,7 +497,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call request:request rootViewController:rootController adId:call.arguments[@"adId"] - banner:call.arguments[@"banner"]]; + banner:call.arguments[@"banner"] + custom:custom]; [_manager loadAd:ad]; result(nil); } else if ([call.method isEqualToString:@"loadInterstitialAd"]) { diff --git a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m index ede50dafc..4972f20e5 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m +++ b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m @@ -50,6 +50,7 @@ typedef NS_ENUM(NSInteger, FLTAdMobField) { FLTAdmobFieldMediationExtras = 154, FLTAdmobFieldAdManagerAdViewOptions = 155, FLTAdmobBannerParameters = 156, + FLTAdmobCustomParameters = 157, }; @interface FLTGoogleMobileAdsWriter : FlutterStandardWriter @@ -352,6 +353,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]; } @@ -544,6 +550,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/lib/src/ad_containers.dart b/packages/google_mobile_ads/lib/src/ad_containers.dart index dca90a36d..cc3544e38 100644 --- a/packages/google_mobile_ads/lib/src/ad_containers.dart +++ b/packages/google_mobile_ads/lib/src/ad_containers.dart @@ -1058,6 +1058,9 @@ enum AdLoaderAdType { /// Banner ad type banner, + + /// Custom ad type + custom, } /// An AdLoaderAd. @@ -1094,6 +1097,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; @@ -1115,6 +1119,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); @@ -1575,6 +1582,33 @@ class BannerParameters { } } +/// Central configuration item for custom format requests served +/// by an [AdLoaderAd] +class CustomParameters { + /// A list of format IDs, corresponding to those in the + /// Google Ad Manager console + final List formatIds; + + /// View options used to create the Platform view + /// + /// These options are passed to the platform's `CustomAdFactory` + Map? viewOptions; + + /// Construct a [CustomParameters] instance, used by an [AdLoaderAd] to + /// configure custom view + CustomParameters({ + required this.formatIds, + this.viewOptions, + }); + + @override + bool operator ==(other) { + return other is CustomParameters && + listEquals(formatIds, other.formatIds) && + mapEquals(viewOptions, other.viewOptions); + } +} + /// Used to configure native ad requests. class NativeAdOptions { /// Where to place the AdChoices icon. diff --git a/packages/google_mobile_ads/lib/src/ad_instance_manager.dart b/packages/google_mobile_ads/lib/src/ad_instance_manager.dart index 165203c27..72ee5931e 100644 --- a/packages/google_mobile_ads/lib/src/ad_instance_manager.dart +++ b/packages/google_mobile_ads/lib/src/ad_instance_manager.dart @@ -99,14 +99,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 { @@ -120,6 +123,7 @@ class AdInstanceManager { case 'onBannerImpression': case 'adDidRecordImpression': // Fall through case 'onNativeAdImpression': // Fall through + case 'onCustomNativeAdImpression': _invokeOnAdImpression(ad, eventName); break; case 'adWillPresentFullScreenContent': @@ -472,6 +476,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; @@ -482,6 +488,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; @@ -587,6 +595,7 @@ class AdInstanceManager { 'request': ad.request, 'adManagerRequest': ad.adManagerRequest, 'banner': ad.banner, + 'custom': ad.custom, }, ); } @@ -922,6 +931,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) { @@ -1065,6 +1075,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); } @@ -1298,6 +1312,12 @@ class AdMessageCodec extends StandardMessageCodec { sizes: readValueOfType(buffer.getUint8(), buffer)?.cast(), adManagerAdViewOptions: readValueOfType(buffer.getUint8(), buffer), ); + case _valueCustomParameters: + return CustomParameters( + formatIds: readValueOfType(buffer.getUint8(), buffer).cast(), + viewOptions: readValueOfType(buffer.getUint8(), buffer) + ?.cast(), + ); default: return super.readValueOfType(type, buffer); } diff --git a/packages/google_mobile_ads/test/ad_loader_ad_test.dart b/packages/google_mobile_ads/test/ad_loader_ad_test.dart index 263fbd356..278e27264 100644 --- a/packages/google_mobile_ads/test/ad_loader_ad_test.dart +++ b/packages/google_mobile_ads/test/ad_loader_ad_test.dart @@ -68,6 +68,7 @@ void main() { 'request': request, 'adManagerRequest': null, 'banner': null, + 'custom': null, }) ]); @@ -91,6 +92,7 @@ void main() { 'request': null, 'adManagerRequest': request, 'banner': null, + 'custom': null, }) ]); @@ -116,6 +118,33 @@ void main() { 'request': adLoaderAd.request, 'adManagerRequest': null, 'banner': banner, + 'custom': null, + }) + ]); + + expect(instanceManager.adFor(0), isNotNull); + }); + + test('load with $CustomParameters', () async { + final CustomParameters custom = CustomParameters( + formatIds: ['test-format-id'], + ); + final AdLoaderAd adLoaderAd = AdLoaderAd( + adUnitId: 'test-ad-unit', + listener: AdLoaderAdListener(), + request: AdRequest(), + custom: custom, + ); + + await adLoaderAd.load(); + expect(log, [ + isMethodCall('loadAdLoaderAd', arguments: { + 'adId': 0, + 'adUnitId': 'test-ad-unit', + 'request': adLoaderAd.request, + 'adManagerRequest': null, + 'banner': null, + 'custom': custom, }) ]); diff --git a/packages/google_mobile_ads/test/mobile_ads_test.dart b/packages/google_mobile_ads/test/mobile_ads_test.dart index 65c0c0f4c..08ab30dfc 100644 --- a/packages/google_mobile_ads/test/mobile_ads_test.dart +++ b/packages/google_mobile_ads/test/mobile_ads_test.dart @@ -635,5 +635,35 @@ void main() { expect(result.adManagerAdViewOptions?.manualImpressionsEnabled, true); } }); + + test('encode/decode minimal $CustomParameters', () { + for (final platform in [TargetPlatform.android, TargetPlatform.iOS]) { + debugDefaultTargetPlatformOverride = platform; + ByteData byteData = codec.encodeMessage(CustomParameters( + formatIds: ['test-format-id'], + ))!; + + CustomParameters result = codec.decodeMessage(byteData); + expect(result.formatIds, ['test-format-id']); + expect(result.viewOptions, null); + } + }); + + test('encode/decode $CustomParameters', () { + for (final platform in [TargetPlatform.android, TargetPlatform.iOS]) { + debugDefaultTargetPlatformOverride = platform; + ByteData byteData = codec.encodeMessage(CustomParameters(formatIds: [ + 'test-format-id' + ], viewOptions: { + 'key': 'value', + }))!; + + CustomParameters result = codec.decodeMessage(byteData); + expect(result.formatIds, ['test-format-id']); + expect(result.viewOptions, { + 'key': 'value', + }); + } + }); }); } From 3705c0f047aabc1b4b68c545a4016803be009431 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 27 Oct 2022 11:52:55 +1100 Subject: [PATCH 7/7] 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/AdMessageCodec.java | 12 ++ .../googlemobileads/FlutterAdLoader.java | 18 ++- .../googlemobileads/FlutterAdLoaderAd.java | 65 ++++++++- .../FlutterCustomParameters.java | 4 +- .../FlutterNativeParameters.java | 48 +++++++ .../GoogleMobileAdsPlugin.java | 13 ++ .../googlemobileads/AdMessageCodecTest.java | 23 +++ .../FlutterAdLoaderAdTest.java | 136 ++++++++++++++++-- .../ios/RunnerTests/FLTAdLoaderAdTest.m | 67 ++++++++- .../FLTGoogleMobileAdsReaderWriterTest.m | 35 +++++ .../ios/Classes/FLTAd_Internal.h | 18 ++- .../ios/Classes/FLTAd_Internal.m | 76 +++++++++- .../ios/Classes/FLTGoogleMobileAdsPlugin.m | 19 ++- .../FLTGoogleMobileAdsReaderWriter_Internal.m | 13 ++ .../lib/src/ad_containers.dart | 38 +++++ .../lib/src/ad_instance_manager.dart | 18 +++ .../test/ad_loader_ad_test.dart | 31 ++++ .../test/mobile_ads_test.dart | 33 +++++ 18 files changed, 644 insertions(+), 23 deletions(-) create mode 100644 packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterNativeParameters.java diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdMessageCodec.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/AdMessageCodec.java index 1717c95c2..3877b85e6 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 @@ -71,6 +71,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; @@ -270,6 +271,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); } @@ -461,6 +468,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/FlutterAdLoader.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java index 7efbaccb0..3fcc804e5 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoader.java @@ -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/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java index 56d3c89d7..ea5b5f496 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java @@ -24,10 +24,14 @@ 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; import io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.CustomAdFactory; +import io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.NativeAdFactory; import java.util.Map; /** @@ -35,7 +39,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; @@ -48,6 +54,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; @@ -59,6 +66,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; @@ -106,6 +115,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"); @@ -139,6 +159,12 @@ FlutterAdLoaderAd build() { new FlutterCustomFormatAdLoadedListener(adLoaderAd), customFactories); } + if (nativeParameters != null) { + adLoaderAd.nativeParameters = + nativeParameters.asNativeParameters( + new FlutterNativeAdLoadedListener(adLoaderAd), nativeFactories); + } + return adLoaderAd; } } @@ -147,6 +173,7 @@ enum AdLoaderAdType { UNKNOWN, BANNER, CUSTOM, + NATIVE, } static class BannerParameters { @@ -179,6 +206,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, @@ -220,7 +265,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; } @@ -230,7 +280,8 @@ void load() { adListener, adManagerRequest.asAdManagerAdRequest(adUnitId), bannerParameters, - customParameters); + customParameters, + nativeParameters); return; } @@ -281,6 +332,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/main/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java index 3086c7084..a3f481bca 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java @@ -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/main/java/io/flutter/plugins/googlemobileads/FlutterNativeParameters.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterNativeParameters.java new file mode 100644 index 000000000..43a6348b7 --- /dev/null +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterNativeParameters.java @@ -0,0 +1,48 @@ +// 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 io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.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 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/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/GoogleMobileAdsPlugin.java index f7b65adb5..a7bff799c 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 @@ -513,6 +513,17 @@ public void onAdInspectorClosed(@Nullable AdInspectorError adInspectorError) { } } + 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) @@ -527,6 +538,8 @@ public void onAdInspectorClosed(@Nullable AdInspectorError adInspectorError) { .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/test/java/io/flutter/plugins/googlemobileads/AdMessageCodecTest.java b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/AdMessageCodecTest.java index 0517f334f..45fc5d994 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 95624efe3..9d42eed32 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,18 +30,22 @@ 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; import io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.CustomAdFactory; +import io.flutter.plugins.googlemobileads.GoogleMobileAdsPlugin.NativeAdFactory; import java.util.Collections; 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; @@ -88,13 +92,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)); @@ -132,12 +136,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)); @@ -189,7 +195,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(); @@ -203,7 +214,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)); @@ -256,7 +272,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(); @@ -266,7 +287,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)); @@ -277,6 +303,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(), FlutterAdLoaderAd.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 483f89c89..a1199e1a5 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 dec432037..3b7dddd66 100644 --- a/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m +++ b/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m @@ -860,6 +860,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/Classes/FLTAd_Internal.h b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h index 924c03852..13b74c76d 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h +++ b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h @@ -324,6 +324,7 @@ typedef NS_ENUM(NSInteger, FLTAdLoaderAdType) { FLTAdLoaderAdTypeUnknown = 0, FLTAdLoaderAdTypeBanner = 1, FLTAdLoaderAdTypeCustom = 2, + FLTAdLoaderAdTypeNative = 3, }; @interface FLTBannerParameters : NSObject @@ -343,11 +344,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; @@ -358,7 +371,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/ios/Classes/FLTAd_Internal.m b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m index 885786358..9762d0bac 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m +++ b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m @@ -1236,6 +1236,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 { @@ -1245,6 +1258,7 @@ @implementation FLTAdLoaderAd { UIView *_view; FLTBannerParameters *_banner; FLTCustomParameters *_custom; + FLTNativeParameters *_native; } - (nonnull instancetype) @@ -1253,7 +1267,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; @@ -1286,6 +1301,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 @@ -1439,6 +1465,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/Classes/FLTGoogleMobileAdsPlugin.m b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m index 1a2039948..c8c8f237d 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m +++ b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m @@ -485,6 +485,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"]; @@ -498,7 +514,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/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m index 4972f20e5..4f1e7360d 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m +++ b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m @@ -51,6 +51,7 @@ typedef NS_ENUM(NSInteger, FLTAdMobField) { FLTAdmobFieldAdManagerAdViewOptions = 155, FLTAdmobBannerParameters = 156, FLTAdmobCustomParameters = 157, + FLTAdmobNativeParameters = 158, }; @interface FLTGoogleMobileAdsWriter : FlutterStandardWriter @@ -358,6 +359,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]; } @@ -555,6 +562,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/lib/src/ad_containers.dart b/packages/google_mobile_ads/lib/src/ad_containers.dart index cc3544e38..0b8d6ea86 100644 --- a/packages/google_mobile_ads/lib/src/ad_containers.dart +++ b/packages/google_mobile_ads/lib/src/ad_containers.dart @@ -1061,6 +1061,9 @@ enum AdLoaderAdType { /// Custom ad type custom, + + /// Native ad type + native, } /// An AdLoaderAd. @@ -1098,6 +1101,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; @@ -1122,6 +1126,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); @@ -1609,6 +1616,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 72ee5931e..dfe8a5904 100644 --- a/packages/google_mobile_ads/lib/src/ad_instance_manager.dart +++ b/packages/google_mobile_ads/lib/src/ad_instance_manager.dart @@ -478,6 +478,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; @@ -490,6 +492,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; @@ -596,6 +600,7 @@ class AdInstanceManager { 'adManagerRequest': ad.adManagerRequest, 'banner': ad.banner, 'custom': ad.custom, + 'native': ad.native, }, ); } @@ -932,6 +937,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) { @@ -1079,6 +1085,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); } @@ -1318,6 +1329,13 @@ class AdMessageCodec extends StandardMessageCodec { viewOptions: readValueOfType(buffer.getUint8(), 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 278e27264..d9bf1cc89 100644 --- a/packages/google_mobile_ads/test/ad_loader_ad_test.dart +++ b/packages/google_mobile_ads/test/ad_loader_ad_test.dart @@ -69,6 +69,7 @@ void main() { 'adManagerRequest': null, 'banner': null, 'custom': null, + 'native': null, }) ]); @@ -93,6 +94,7 @@ void main() { 'adManagerRequest': request, 'banner': null, 'custom': null, + 'native': null, }) ]); @@ -119,6 +121,7 @@ void main() { 'adManagerRequest': null, 'banner': banner, 'custom': null, + 'native': null, }) ]); @@ -145,6 +148,34 @@ 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 08ab30dfc..96595bcac 100644 --- a/packages/google_mobile_ads/test/mobile_ads_test.dart +++ b/packages/google_mobile_ads/test/mobile_ads_test.dart @@ -665,5 +665,38 @@ void main() { }); } }); + + 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', + }); + } + }); }); }