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..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 @@ -68,6 +68,10 @@ 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; + 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; @@ -253,6 +257,26 @@ 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 if (value instanceof FlutterCustomParameters) { + stream.write(VALUE_CUSTOM_PARAMETERS); + 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); } @@ -434,6 +458,21 @@ 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)); + case VALUE_CUSTOM_PARAMETERS: + 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/FlutterAdListener.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdListener.java index 8f29ee43d..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 @@ -16,8 +16,12 @@ 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 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. */ @@ -94,19 +98,61 @@ 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 { - 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); + } + } +} + +/** {@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); + } + } +} + +/** {@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 0e864e7d9..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 @@ -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; @@ -137,4 +138,62 @@ public void loadAdManagerNativeAd( .build() .loadAd(adManagerAdRequest); } + + /** Load an ad loader ad. */ + public void loadAdLoaderAd( + @NonNull String adUnitId, + @NonNull AdListener adListener, + @NonNull AdRequest request, + @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters, + @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); + if (bannerParameters.adManagerAdViewOptions != null) { + builder.withAdManagerAdViewOptions(bannerParameters.adManagerAdViewOptions); + } + } + if (customParameters != null) { + for (String formatId : customParameters.factories.keySet()) { + 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); + } + + /** Load an ad manager ad loader ad. */ + public void loadAdManagerAdLoaderAd( + @NonNull String adUnitId, + @NonNull AdListener adListener, + @NonNull AdManagerAdRequest adManagerAdRequest, + @Nullable FlutterAdLoaderAd.BannerParameters bannerParameters, + @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); + if (bannerParameters.adManagerAdViewOptions != null) { + builder.withAdManagerAdViewOptions(bannerParameters.adManagerAdViewOptions); + } + } + if (customParameters != null) { + for (String formatId : customParameters.factories.keySet()) { + 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 new file mode 100644 index 000000000..ea5b5f496 --- /dev/null +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAd.java @@ -0,0 +1,355 @@ +// 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 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 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; + +/** + * 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, + OnCustomFormatAdLoadedListener, + OnNativeAdLoadedListener { + 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; + @Nullable private View view; + @Nullable protected BannerParameters bannerParameters; + @Nullable protected CustomParameters customParameters; + @Nullable protected NativeParameters nativeParameters; + + 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; + @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; + 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; + } + + public Builder setBanner(@Nullable FlutterBannerParameters bannerParameters) { + this.bannerParameters = 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; + } + + 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"); + } + + 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); + } + + if (bannerParameters != null) { + adLoaderAd.bannerParameters = + bannerParameters.asBannerParameters( + new FlutterAdManagerAdViewLoadedListener(adLoaderAd)); + } + + if (customParameters != null) { + adLoaderAd.customParameters = + customParameters.asCustomParameters( + new FlutterCustomFormatAdLoadedListener(adLoaderAd), customFactories); + } + + if (nativeParameters != null) { + adLoaderAd.nativeParameters = + nativeParameters.asNativeParameters( + new FlutterNativeAdLoadedListener(adLoaderAd), nativeFactories); + } + + return adLoaderAd; + } + } + + enum AdLoaderAdType { + UNKNOWN, + BANNER, + CUSTOM, + NATIVE, + } + + 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; + } + } + + 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; + } + } + + 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, + @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), + bannerParameters, + customParameters, + nativeParameters); + return; + } + + if (adManagerRequest != null) { + adLoader.loadAdManagerAdLoaderAd( + adUnitId, + adListener, + adManagerRequest.asAdManagerAdRequest(adUnitId), + bannerParameters, + customParameters, + nativeParameters); + return; + } + + Log.e(TAG, "A null or invalid ad request was provided."); + } + + @Nullable + AdLoaderAdType getAdLoaderAdType() { + return type; + } + + @Nullable + FlutterAdSize getAdSize() { + if (view == null || !(view instanceof AdManagerAdView)) { + return null; + } + return new FlutterAdSize(((AdManagerAdView) view).getAdSize()); + } + + @Nullable + String getFormatId() { + return formatId; + } + + @Override + @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 + 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 + 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) { + 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/FlutterCustomParameters.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterCustomParameters.java new file mode 100644 index 000000000..a3f481bca --- /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 registeredFactories) { + Map factories = new HashMap<>(); + for (String formatId : formatIds) { + 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/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); 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 e9d4b3444..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 @@ -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; @@ -44,6 +46,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. @@ -68,8 +71,10 @@ 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; /** * Public constructor for the plugin. Dependency initialization is handled in lifecycle methods * below. @@ -95,6 +100,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}. * @@ -116,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 @@ -135,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. @@ -195,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 @@ -217,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 = @@ -234,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; @@ -431,6 +499,52 @@ public void onAdInspectorClosed(@Nullable AdInspectorError adInspectorError) { nativeAd.load(); 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 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) + .setAdUnitId(call.argument("adUnitId")) + .setRequest(call.argument("request")) + .setAdManagerRequest(call.argument("adManagerRequest")) + .setId(call.argument("adId")) + .setFlutterAdLoader( + adLoaderSupplier != null + ? adLoaderSupplier.get() + : new FlutterAdLoader(context)) + .setBanner(call.argument("banner")) + .setCustom(customParameters) + .withAvailableCustomFactories(customAdFactories) + .setNative(nativeParameters) + .withAvailableNativeFactories(nativeAdFactories) + .build(); + instanceManager.trackAd(adLoaderAd, call.argument("adId")); + adLoaderAd.load(); + result.success(null); + break; case "loadInterstitialAd": final FlutterInterstitialAd interstitial = new FlutterInterstitialAd( @@ -610,6 +724,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")); @@ -620,6 +750,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, @@ -628,6 +760,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/AdMessageCodecTest.java b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/AdMessageCodecTest.java index 56fb41324..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 @@ -537,6 +537,89 @@ 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); + } + + @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"); + } + + @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 new file mode 100644 index 000000000..9d42eed32 --- /dev/null +++ b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterAdLoaderAdTest.java @@ -0,0 +1,426 @@ +// 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.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.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; + +/** 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), isNull(), isNull(), isNull()); + + adLoaderAd.load(); + + verify(mockLoader) + .loadAdManagerAdLoaderAd( + eq("testId"), any(AdListener.class), eq(mockRequest), isNull(), isNull(), 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 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), isNull(), isNull(), isNull()); + + adLoaderAd.load(); + + verify(mockLoader) + .loadAdLoaderAd( + eq("testId"), any(AdListener.class), eq(mockRequest), isNull(), isNull(), 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), + isNull(), + isNull()); + + 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), + isNull(), + 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)); + 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), + isNull()); + + 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), + 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)); + 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() + .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/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/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..cb76b5109 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,8 @@ 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 */; }; + E2D4625F29DFC2560010C4D0 /* FLTAdManagerAdViewOptionsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E2D4625E29DFC2560010C4D0 /* FLTAdManagerAdViewOptionsTest.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -150,6 +152,8 @@ 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 = ""; }; + E2D4625E29DFC2560010C4D0 /* FLTAdManagerAdViewOptionsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTAdManagerAdViewOptionsTest.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -272,6 +276,8 @@ children = ( 9E61AA5829BBE8DA00801A83 /* NativeTemplates */, 9E61AA5529BBE8AF00801A83 /* UserMessagingPlatform */, + E276E99129DFB5870052484E /* FLTAdLoaderAdTest.m */, + E2D4625E29DFC2560010C4D0 /* FLTAdManagerAdViewOptionsTest.m */, 9E61AA2729BBE66900801A83 /* FLTAdUtilTest.m */, 9E61AA2429BBE66900801A83 /* FLTAppOpenAdTest.m */, 9E61AA2229BBE66900801A83 /* FLTBannerAdTest.m */, @@ -512,8 +518,10 @@ buildActionMask = 2147483647; files = ( 9E61AA5729BBE8D000801A83 /* FLTUserMessagingPlatformManagerTest.m in Sources */, + 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 new file mode 100644 index 000000000..a1199e1a5 --- /dev/null +++ b/packages/google_mobile_ads/example/ios/RunnerTests/FLTAdLoaderAdTest.m @@ -0,0 +1,277 @@ +// 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 + banner:nil + custom:nil + native:nil]; + + 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)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] + custom:nil + native: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)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 + native:nil]; + + 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)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" ]; + [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 + banner:nil + custom:nil + native:nil]; + + 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/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/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/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m b/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m index e60d75356..3b7dddd66 100644 --- a/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m +++ b/packages/google_mobile_ads/example/ios/RunnerTests/FLTGoogleMobileAdsReaderWriterTest.m @@ -777,6 +777,124 @@ - (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); +} + +- (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); +} + +- (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/FLTAdInstanceManager_Internal.h b/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.h index 1e8dd5549..72f108bf2 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; @@ -39,10 +40,14 @@ - (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)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: @@ -50,10 +55,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..c50290ee6 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.m +++ b/packages/google_mobile_ads/ios/Classes/FLTAdInstanceManager_Internal.m @@ -110,22 +110,38 @@ - (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]; } +- (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" @@ -159,19 +175,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]; } diff --git a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h index 92d0fc151..13b74c76d 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; @@ -295,6 +296,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 @@ -310,6 +320,61 @@ - (GADAdLoader *_Nonnull)adLoader; @end +typedef NS_ENUM(NSInteger, FLTAdLoaderAdType) { + FLTAdLoaderAdTypeUnknown = 0, + FLTAdLoaderAdTypeBanner = 1, + FLTAdLoaderAdTypeCustom = 2, + FLTAdLoaderAdTypeNative = 3, +}; + +@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 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 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 +@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 + banner:(nullable FLTBannerParameters *)bannerParameters + custom:(nullable FLTCustomParameters *)customParameters + native:(nullable FLTNativeParameters *)nativeParameters; +@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..9762d0bac 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m +++ b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m @@ -1213,6 +1213,316 @@ - (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 + +@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 + +@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 { + NSString *_adUnitId; + FLTAdRequest *_adRequest; + NSMutableArray *_validAdSizes; + UIView *_view; + FLTBannerParameters *_banner; + FLTCustomParameters *_custom; + FLTNativeParameters *_native; +} + +- (nonnull instancetype) + initWithAdUnitId:(nonnull NSString *)adUnitId + request:(nonnull FLTAdRequest *)request + rootViewController:(nonnull UIViewController *)rootViewController + adId:(nonnull NSNumber *)adId + banner:(nullable FLTBannerParameters *)bannerParameters + custom:(nullable FLTCustomParameters *)customParameters + native:(nullable FLTNativeParameters *)nativeParameters { + self = [super init]; + if (self) { + self.adId = adId; + _adUnitId = adUnitId; + _adRequest = request; + _adLoaderAdType = FLTAdLoaderAdTypeUnknown; + _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]; + } + } + + if (![FLTAdUtil isNull:customParameters]) { + _custom = customParameters; + + [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 + options:options]; + _adLoader.delegate = self; + } + return self; +} + +- (FLTAdSize *)adSize { + if (_view && [_view isKindOfClass:[GADBannerView class]]) { + return [[FLTAdSize alloc] initWithAdSize:((GADBannerView *)_view).adSize]; + } + return nil; +} + +#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 - 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 - 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 - 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 { + return _view; +} + +@synthesize manager; + +@end + @implementation FLTRewardItem - (instancetype _Nonnull)initWithAmount:(NSNumber *_Nonnull)amount type:(NSString *_Nonnull)type { @@ -1385,3 +1695,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.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 1defb9db4..c8c8f237d 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; @@ -422,6 +466,58 @@ - (void)handleMethodCall:(FlutterMethodCall *)call nativeTemplateStyle:call.arguments[@"nativeTemplateStyle"]]; [_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]; + } + } + + 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"]; + } 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"] + banner:call.arguments[@"banner"] + custom:custom + native:native]; + [_manager loadAd:ad]; + result(nil); } else if ([call.method isEqualToString:@"loadInterstitialAd"]) { FLTInterstitialAd *ad = [[FLTInterstitialAd alloc] initWithAdUnitId:call.arguments[@"adUnitId"] @@ -517,6 +613,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 +647,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/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m index 24572e60f..4f1e7360d 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m +++ b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m @@ -47,8 +47,11 @@ typedef NS_ENUM(NSInteger, FLTAdMobField) { FLTAdmobFieldNativeTemplateFontStyle = 151, FLTAdmobFieldNativeTemplateType = 152, FLTAdmobFieldNativeTemplateColor = 153, - FLTAdmobFieldMediationExtras = 154 - + FLTAdmobFieldMediationExtras = 154, + FLTAdmobFieldAdManagerAdViewOptions = 155, + FLTAdmobBannerParameters = 156, + FLTAdmobCustomParameters = 157, + FLTAdmobNativeParameters = 158, }; @interface FLTGoogleMobileAdsWriter : FlutterStandardWriter @@ -341,6 +344,27 @@ - (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]]]; + } + case FLTAdmobCustomParameters: { + return [[FLTCustomParameters alloc] + 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]; } @@ -524,6 +548,26 @@ - (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 if ([value isKindOfClass:[FLTCustomParameters class]]) { + [self writeByte:FLTAdmobCustomParameters]; + 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 80d679c15..0b8d6ea86 100644 --- a/packages/google_mobile_ads/lib/src/ad_containers.dart +++ b/packages/google_mobile_ads/lib/src/ad_containers.dart @@ -1051,6 +1051,98 @@ class NativeAd extends AdWithView { } } +/// Type of ad served by [AdLoaderAd] +enum AdLoaderAdType { + /// Unknown ad type + unknown, + + /// Banner ad type + banner, + + /// Custom ad type + custom, + + /// Native ad type + native, +} + +/// 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, + this.banner, + this.custom, + this.native, + }) : 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; + + /// Optional parameters used to configure served "banner" ads + final BannerParameters? banner; + + /// 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); + + /// 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]. @@ -1453,6 +1545,108 @@ 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; + } +} + +/// 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); + } +} + +/// 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 3bb29bcb6..dfe8a5904 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': @@ -450,6 +454,53 @@ 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; + case 1: + return AdLoaderAdType.banner; + case 2: + return AdLoaderAdType.custom; + case 3: + return AdLoaderAdType.native; + default: + debugPrint('Error: unknown AdLoaderAdType value: $adLoaderAdType'); + return AdLoaderAdType.unknown; + } + } else { + switch (adLoaderAdType) { + case 0: + return AdLoaderAdType.unknown; + case 1: + return AdLoaderAdType.banner; + case 2: + return AdLoaderAdType.custom; + case 3: + return AdLoaderAdType.native; + 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 +581,30 @@ 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, + 'banner': ad.banner, + 'custom': ad.custom, + 'native': ad.native, + }, + ); + } + /// Starts loading the ad if not previously loaded. /// /// Loading also terminates if ad is already in the process of loading. @@ -859,6 +934,10 @@ 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; + static const int _valueCustomParameters = 157; + static const int _valueNativeParameters = 158; @override void writeValue(WriteBuffer buffer, dynamic value) { @@ -995,6 +1074,22 @@ 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 if (value is CustomParameters) { + 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); } @@ -1219,6 +1314,28 @@ 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), + ); + case _valueCustomParameters: + return CustomParameters( + formatIds: readValueOfType(buffer.getUint8(), buffer).cast(), + 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/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..d9bf1cc89 --- /dev/null +++ b/packages/google_mobile_ads/test/ad_loader_ad_test.dart @@ -0,0 +1,532 @@ +// 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, + 'banner': null, + 'custom': null, + 'native': 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, + 'banner': null, + 'custom': null, + 'native': 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, + 'custom': null, + 'native': 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, + '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, + }) + ]); + + 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('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(); + + 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); + }); + }); +} diff --git a/packages/google_mobile_ads/test/mobile_ads_test.dart b/packages/google_mobile_ads/test/mobile_ads_test.dart index 9addf96b7..96595bcac 100644 --- a/packages/google_mobile_ads/test/mobile_ads_test.dart +++ b/packages/google_mobile_ads/test/mobile_ads_test.dart @@ -584,5 +584,119 @@ 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); + } + }); + + 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', + }); + } + }); + + 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', + }); + } + }); }); }