From 9093c78253dc54fb1cc7663594c217199e881114 Mon Sep 17 00:00:00 2001 From: jjliu15 <58007281+jjliu15@users.noreply.github.com> Date: Thu, 29 Jul 2021 23:58:49 -0700 Subject: [PATCH] Native ad options (#311) * Support the native ad options API. --- packages/google_mobile_ads/CHANGELOG.md | 4 + packages/google_mobile_ads/README.md | 92 ++++++++++++ .../googlemobileads/AdMessageCodec.java | 30 ++++ .../plugins/googlemobileads/Constants.java | 2 +- .../googlemobileads/FlutterNativeAd.java | 33 ++++- .../FlutterNativeAdOptions.java | 68 +++++++++ .../googlemobileads/FlutterVideoOptions.java | 49 +++++++ .../GoogleMobileAdsPlugin.java | 1 + .../googlemobileads/AdMessageCodecTest.java | 48 +++++++ .../FlutterNativeAdOptionsTest.java | 64 +++++++++ .../googlemobileads/FlutterNativeAdTest.java | 16 ++- .../FlutterVideoOptionsTest.java | 50 +++++++ .../ios/Runner.xcodeproj/project.pbxproj | 6 + .../google_mobile_ads/ios/Classes/FLTAdUtil.h | 23 +++ .../google_mobile_ads/ios/Classes/FLTAdUtil.m | 27 ++++ .../ios/Classes/FLTAd_Internal.h | 33 ++++- .../ios/Classes/FLTAd_Internal.m | 131 +++++++++++++++++- .../ios/Classes/FLTConstants.h | 2 +- .../ios/Classes/FLTGoogleMobileAdsPlugin.m | 3 +- .../FLTGoogleMobileAdsReaderWriter_Internal.m | 33 +++++ .../ios/Tests/FLTGoogleMobileAdsTest.m | 15 +- .../ios/Tests/FLTNativeAdTest.m | 10 +- .../lib/src/ad_containers.dart | 128 +++++++++++++++++ .../lib/src/ad_instance_manager.dart | 107 +++++++++++++- packages/google_mobile_ads/pubspec.yaml | 2 +- .../test/ad_containers_test.dart | 15 +- .../test/mobile_ads_test.dart | 76 ++++++++++ 27 files changed, 1041 insertions(+), 27 deletions(-) create mode 100644 packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterNativeAdOptions.java create mode 100644 packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterVideoOptions.java create mode 100644 packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterNativeAdOptionsTest.java create mode 100644 packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterVideoOptionsTest.java create mode 100644 packages/google_mobile_ads/ios/Classes/FLTAdUtil.h create mode 100644 packages/google_mobile_ads/ios/Classes/FLTAdUtil.m diff --git a/packages/google_mobile_ads/CHANGELOG.md b/packages/google_mobile_ads/CHANGELOG.md index e9f786584..c7ac2b5a5 100644 --- a/packages/google_mobile_ads/CHANGELOG.md +++ b/packages/google_mobile_ads/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.13.3 + +* Adds support for NativeAdOptions. More documentation also available for [Android](https://developers.google.com/admob/android/native/options) and [iOS](https://developers.google.com/admob/ios/native/options) + ## 0.13.2+1 * Fixes [Issue #130](https://github.com/googleads/googleads-mobile-flutter/issues/130) diff --git a/packages/google_mobile_ads/README.md b/packages/google_mobile_ads/README.md index 5d94b4ed0..92364561b 100644 --- a/packages/google_mobile_ads/README.md +++ b/packages/google_mobile_ads/README.md @@ -663,6 +663,52 @@ final NativeAdListener listener = NativeAdListener( ); ``` +#### NativeAdOptions + +`NativeAds` have an optional argument, `nativeAdOptions`, which can be used to set specific options on the native ad. + +`shouldRequestMultipleImages` +

If set to `true`, the SDK will not load image asset content and native ad +image URLs can be used to fetch content. Defaults to false.

+ +`shouldRequestMultipleImages` +

+Some image assets will contain a series of images rather than just one. By setting this value to true, +your app indicates that it's prepared to display all the images for any assets that have more than one. +By setting it to false (the default) your app instructs the SDK to provide just the first image for any assets that contain a series. + +If no `NativeadOptions` are passed in when initializing a `NativeAd`, the default value for each property will be used. +

+ +`adChoicesPlacement` +

+The [AdChoices overlay](https://developers.google.com/admob/android/native/advanced#adchoices_overlay) is set to the top right corner by default. +Apps can change which corner this overlay is rendered in by setting this property to one of the following: + +* AdChoicesPlacement.topRightCorner +* AdChoicesPlacement.topLeftCorner +* AdChoicesPlacement.bottomRightCorner +* AdChoicesPlacement.bottomLeftCorner +

+ +`videoOptions` +

+Can be used to set video options for video assets returned as part of a native ad. +

+ +`mediaAspectRatio` +

+This sets the aspect ratio for image or video to be returned for the native ad. +Setting NativeMediaAspectRatio to one of the following constants will cause only ads with media of the specified aspect ratio to be returned: + +* MediaAspectRatio.landscape +* MediaAspectRatio.portrait +* MediaAspectRatio.square +* MediaAspectRatio.any + +If not set, ads with any aspect ratio will be returned. +

+ ### Load Native Ad After a `NativeAd` is instantiated, `load()` must be called before it can be shown on the screen. @@ -1339,6 +1385,52 @@ final NativeAdListener listener = NativeAdListener( ); ``` +#### NativeAdOptions + +`NativeAds` have an optional argument, `nativeAdOptions`, which can be used to set specific options on the native ad. + +`shouldRequestMultipleImages` +

If set to `true`, the SDK will not load image asset content and native ad +image URLs can be used to fetch content. Defaults to false.

+ +`shouldRequestMultipleImages` +

+Some image assets will contain a series of images rather than just one. By setting this value to true, +your app indicates that it's prepared to display all the images for any assets that have more than one. +By setting it to false (the default) your app instructs the SDK to provide just the first image for any assets that contain a series. + +If no `NativeadOptions` are passed in when initializing a `NativeAd`, the default value for each property will be used. +

+ +`adChoicesPlacement` +

+The [AdChoices overlay](https://developers.google.com/admob/android/native/advanced#adchoices_overlay) is set to the top right corner by default. +Apps can change which corner this overlay is rendered in by setting this property to one of the following: + +* AdChoicesPlacement.topRightCorner +* AdChoicesPlacement.topLeftCorner +* AdChoicesPlacement.bottomRightCorner +* AdChoicesPlacement.bottomLeftCorner +

+ +`videoOptions` +

+Can be used to set video options for video assets returned as part of a native ad. +

+ +`mediaAspectRatio` +

+This sets the aspect ratio for image or video to be returned for the native ad. +Setting NativeMediaAspectRatio to one of the following constants will cause only ads with media of the specified aspect ratio to be returned: + +* MediaAspectRatio.landscape +* MediaAspectRatio.portrait +* MediaAspectRatio.square +* MediaAspectRatio.any + +If not set, ads with any aspect ratio will be returned. +

+ ### Load Native Ad After a `NativeAd` is instantiated, `load()` must be called before it can be shown on the screen. 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 694431017..382017450 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 @@ -47,6 +47,8 @@ class AdMessageCodec extends StandardMessageCodec { private static final byte VALUE_ADAPTER_RESPONSE_INFO = (byte) 141; static final byte VALUE_ANCHORED_ADAPTIVE_BANNER_AD_SIZE = (byte) 142; static final byte VALUE_SMART_BANNER_AD_SIZE = (byte) 143; + static final byte VALUE_NATIVE_AD_OPTIONS = (byte) 144; + static final byte VALUE_VIDEO_OPTIONS = (byte) 145; @NonNull final Context context; @NonNull final FlutterAdSize.AdSizeFactory adSizeFactory; @@ -141,6 +143,21 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { FlutterServerSideVerificationOptions options = (FlutterServerSideVerificationOptions) value; writeValue(stream, options.getUserId()); writeValue(stream, options.getCustomData()); + } else if (value instanceof FlutterNativeAdOptions) { + stream.write(VALUE_NATIVE_AD_OPTIONS); + FlutterNativeAdOptions options = (FlutterNativeAdOptions) value; + writeValue(stream, options.adChoicesPlacement); + writeValue(stream, options.mediaAspectRatio); + writeValue(stream, options.videoOptions); + writeValue(stream, options.requestCustomMuteThisAd); + writeValue(stream, options.shouldRequestMultipleImages); + writeValue(stream, options.shouldReturnUrlsForImageAssets); + } else if (value instanceof FlutterVideoOptions) { + stream.write(VALUE_VIDEO_OPTIONS); + FlutterVideoOptions options = (FlutterVideoOptions) value; + writeValue(stream, options.clickToExpandRequested); + writeValue(stream, options.customControlsRequested); + writeValue(stream, options.startMuted); } else { super.writeValue(stream, value); } @@ -226,6 +243,19 @@ protected Object readValueOfType(byte type, ByteBuffer buffer) { return new FlutterServerSideVerificationOptions( (String) readValueOfType(buffer.get(), buffer), (String) readValueOfType(buffer.get(), buffer)); + case VALUE_NATIVE_AD_OPTIONS: + return new FlutterNativeAdOptions( + (Integer) readValueOfType(buffer.get(), buffer), + (Integer) readValueOfType(buffer.get(), buffer), + (FlutterVideoOptions) readValueOfType(buffer.get(), buffer), + (Boolean) readValueOfType(buffer.get(), buffer), + (Boolean) readValueOfType(buffer.get(), buffer), + (Boolean) readValueOfType(buffer.get(), buffer)); + case VALUE_VIDEO_OPTIONS: + return new FlutterVideoOptions( + (Boolean) readValueOfType(buffer.get(), buffer), + (Boolean) readValueOfType(buffer.get(), buffer), + (Boolean) 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/Constants.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/Constants.java index 6417b1813..1c411b86a 100644 --- a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/Constants.java +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/Constants.java @@ -17,5 +17,5 @@ /** Constants used in the plugin. */ public class Constants { /** Version request agent. Should be bumped alongside plugin versions. */ - public static final String REQUEST_AGENT_PREFIX_VERSIONED = "Flutter-GMA-0.13.2+1"; + public static final String REQUEST_AGENT_PREFIX_VERSIONED = "Flutter-GMA-0.13.3"; } 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 d05237915..c476107a4 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 @@ -38,6 +38,7 @@ class FlutterNativeAd extends FlutterAd { @Nullable private FlutterAdManagerAdRequest adManagerRequest; @Nullable private Map customOptions; @Nullable private NativeAdView nativeAdView; + @Nullable private FlutterNativeAdOptions nativeAdOptions; static class Builder { @Nullable private AdInstanceManager manager; @@ -47,6 +48,7 @@ static class Builder { @Nullable private FlutterAdManagerAdRequest adManagerRequest; @Nullable private Map customOptions; @Nullable private Integer id; + @Nullable private FlutterNativeAdOptions nativeAdOptions; public Builder setAdFactory(@NonNull NativeAdFactory adFactory) { this.adFactory = adFactory; @@ -83,6 +85,11 @@ public Builder setAdManagerRequest(@NonNull FlutterAdManagerAdRequest request) { return this; } + public Builder setNativeAdOptions(@Nullable FlutterNativeAdOptions nativeAdOptions) { + this.nativeAdOptions = nativeAdOptions; + return this; + } + FlutterNativeAd build() { if (manager == null) { throw new IllegalStateException("AdInstanceManager cannot not be null."); @@ -104,11 +111,19 @@ FlutterNativeAd build() { adFactory, adManagerRequest, new FlutterAdLoader(), - customOptions); + customOptions, + nativeAdOptions); } else { nativeAd = new FlutterNativeAd( - id, manager, adUnitId, adFactory, request, new FlutterAdLoader(), customOptions); + id, + manager, + adUnitId, + adFactory, + request, + new FlutterAdLoader(), + customOptions, + nativeAdOptions); } return nativeAd; } @@ -121,7 +136,8 @@ protected FlutterNativeAd( @NonNull NativeAdFactory adFactory, @NonNull FlutterAdRequest request, @NonNull FlutterAdLoader flutterAdLoader, - @Nullable Map customOptions) { + @Nullable Map customOptions, + @Nullable FlutterNativeAdOptions nativeAdOptions) { super(adId); this.manager = manager; this.adUnitId = adUnitId; @@ -129,6 +145,7 @@ protected FlutterNativeAd( this.request = request; this.flutterAdLoader = flutterAdLoader; this.customOptions = customOptions; + this.nativeAdOptions = nativeAdOptions; } protected FlutterNativeAd( @@ -138,7 +155,8 @@ protected FlutterNativeAd( @NonNull NativeAdFactory adFactory, @NonNull FlutterAdManagerAdRequest adManagerRequest, @NonNull FlutterAdLoader flutterAdLoader, - @Nullable Map customOptions) { + @Nullable Map customOptions, + @Nullable FlutterNativeAdOptions nativeAdOptions) { super(adId); this.manager = manager; this.adUnitId = adUnitId; @@ -146,16 +164,19 @@ protected FlutterNativeAd( this.adManagerRequest = adManagerRequest; this.flutterAdLoader = flutterAdLoader; this.customOptions = customOptions; + this.nativeAdOptions = nativeAdOptions; } @Override void load() { final OnNativeAdLoadedListener loadedListener = new FlutterNativeAdLoadedListener(this); final AdListener adListener = new FlutterNativeAdListener(adId, manager); - final NativeAdOptions options = new NativeAdOptions.Builder().build(); - // 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. + final NativeAdOptions options = + this.nativeAdOptions == null + ? new NativeAdOptions.Builder().build() + : nativeAdOptions.asNativeAdOptions(); if (request != null) { flutterAdLoader.loadNativeAd( manager.activity, adUnitId, loadedListener, options, adListener, request.asAdRequest()); diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterNativeAdOptions.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterNativeAdOptions.java new file mode 100644 index 000000000..fe2bb7751 --- /dev/null +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterNativeAdOptions.java @@ -0,0 +1,68 @@ +// Copyright 2021 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.nativead.NativeAdOptions; + +/** A wrapper for {@link com.google.android.gms.ads.nativead.NativeAdOptions}. */ +class FlutterNativeAdOptions { + + @Nullable final Integer adChoicesPlacement; + @Nullable final Integer mediaAspectRatio; + @Nullable final FlutterVideoOptions videoOptions; + @Nullable final Boolean requestCustomMuteThisAd; + @Nullable final Boolean shouldRequestMultipleImages; + @Nullable final Boolean shouldReturnUrlsForImageAssets; + + FlutterNativeAdOptions( + @Nullable Integer adChoicesPlacement, + @Nullable Integer mediaAspectRatio, + @Nullable FlutterVideoOptions videoOptions, + @Nullable Boolean requestCustomMuteThisAd, + @Nullable Boolean shouldRequestMultipleImages, + @Nullable Boolean shouldReturnUrlsForImageAssets) { + this.adChoicesPlacement = adChoicesPlacement; + this.mediaAspectRatio = mediaAspectRatio; + this.videoOptions = videoOptions; + this.requestCustomMuteThisAd = requestCustomMuteThisAd; + this.shouldRequestMultipleImages = shouldRequestMultipleImages; + this.shouldReturnUrlsForImageAssets = shouldReturnUrlsForImageAssets; + } + + NativeAdOptions asNativeAdOptions() { + NativeAdOptions.Builder builder = new NativeAdOptions.Builder(); + if (adChoicesPlacement != null) { + builder.setAdChoicesPlacement(adChoicesPlacement); + } + if (mediaAspectRatio != null) { + builder.setMediaAspectRatio(mediaAspectRatio); + } + if (videoOptions != null) { + builder.setVideoOptions(videoOptions.asVideoOptions()); + } + if (requestCustomMuteThisAd != null) { + builder.setRequestCustomMuteThisAd(requestCustomMuteThisAd); + } + if (shouldRequestMultipleImages != null) { + builder.setRequestMultipleImages(shouldRequestMultipleImages); + } + if (shouldReturnUrlsForImageAssets != null) { + builder.setReturnUrlsForImageAssets(shouldReturnUrlsForImageAssets); + } + return builder.build(); + } +} diff --git a/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterVideoOptions.java b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterVideoOptions.java new file mode 100644 index 000000000..25a29e6bb --- /dev/null +++ b/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterVideoOptions.java @@ -0,0 +1,49 @@ +// Copyright 2021 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.VideoOptions; + +/** A wrapper for {@link com.google.android.gms.ads.VideoOptions}. */ +class FlutterVideoOptions { + @Nullable final Boolean clickToExpandRequested; + @Nullable final Boolean customControlsRequested; + @Nullable final Boolean startMuted; + + FlutterVideoOptions( + @Nullable Boolean clickToExpandRequested, + @Nullable Boolean customControlsRequested, + @Nullable Boolean startMuted) { + this.clickToExpandRequested = clickToExpandRequested; + this.customControlsRequested = customControlsRequested; + this.startMuted = startMuted; + } + + VideoOptions asVideoOptions() { + VideoOptions.Builder builder = new VideoOptions.Builder(); + if (clickToExpandRequested != null) { + builder.setClickToExpandRequested(clickToExpandRequested); + } + if (customControlsRequested != null) { + builder.setCustomControlsRequested(customControlsRequested); + } + if (startMuted != null) { + builder.setStartMuted(startMuted); + } + return builder.build(); + } +} 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 cc733a38f..783484048 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 @@ -286,6 +286,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) .setAdManagerRequest(call.argument("adManagerRequest")) .setCustomOptions(call.>argument("customOptions")) .setId(call.argument("adId")) + .setNativeAdOptions(call.argument("nativeAdOptions")) .build(); instanceManager.trackAd(nativeAd, call.argument("adId")); nativeAd.load(); diff --git a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/AdMessageCodecTest.java b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/AdMessageCodecTest.java index 9a641a378..92d43b72d 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 @@ -15,6 +15,9 @@ package io.flutter.plugins.googlemobileads; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import android.content.Context; @@ -178,4 +181,49 @@ public void adMessageCodec_encodeSmartBannerAdSize() { (FlutterAdSize.SmartBannerAdSize) testCodec.decodeMessage((ByteBuffer) data.position(0)); assertEquals(result.size, AdSize.SMART_BANNER); } + + @Test + public void adMessageCodec_nativeAdOptionsNull() { + final ByteBuffer data = + testCodec.encodeMessage(new FlutterNativeAdOptions(null, null, null, null, null, null)); + + final FlutterNativeAdOptions result = + (FlutterNativeAdOptions) testCodec.decodeMessage((ByteBuffer) data.position(0)); + assertNull(result.adChoicesPlacement); + assertNull(result.mediaAspectRatio); + assertNull(result.videoOptions); + assertNull(result.requestCustomMuteThisAd); + assertNull(result.shouldRequestMultipleImages); + assertNull(result.shouldReturnUrlsForImageAssets); + } + + @Test + public void adMessageCodec_nativeAdOptions() { + FlutterVideoOptions videoOptions = new FlutterVideoOptions(true, false, true); + + final ByteBuffer data = + testCodec.encodeMessage(new FlutterNativeAdOptions(1, 2, videoOptions, false, true, false)); + + final FlutterNativeAdOptions result = + (FlutterNativeAdOptions) testCodec.decodeMessage((ByteBuffer) data.position(0)); + assertEquals(result.adChoicesPlacement, (Integer) 1); + assertEquals(result.mediaAspectRatio, (Integer) 2); + assertEquals(result.videoOptions.clickToExpandRequested, true); + assertEquals(result.videoOptions.customControlsRequested, false); + assertEquals(result.videoOptions.startMuted, true); + assertFalse(result.requestCustomMuteThisAd); + assertTrue(result.shouldRequestMultipleImages); + assertFalse(result.shouldReturnUrlsForImageAssets); + } + + @Test + public void adMessageCodec_videoOptionsNull() { + final ByteBuffer data = testCodec.encodeMessage(new FlutterVideoOptions(null, null, null)); + + final FlutterVideoOptions result = + (FlutterVideoOptions) testCodec.decodeMessage((ByteBuffer) data.position(0)); + assertNull(result.clickToExpandRequested); + assertNull(result.customControlsRequested); + assertNull(result.startMuted); + } } diff --git a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterNativeAdOptionsTest.java b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterNativeAdOptionsTest.java new file mode 100644 index 000000000..2afbba275 --- /dev/null +++ b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterNativeAdOptionsTest.java @@ -0,0 +1,64 @@ +// Copyright 2021 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 static org.mockito.Mockito.doReturn; + +import com.google.android.gms.ads.VideoOptions; +import com.google.android.gms.ads.nativead.NativeAdOptions; +import org.junit.Test; +import org.mockito.Mockito; + +/** Tests for {@link FlutterNativeAdOptions}. */ +public class FlutterNativeAdOptionsTest { + + @Test + public void testAsAdOptions_null() { + FlutterNativeAdOptions flutterNativeAdOptions = + new FlutterNativeAdOptions(null, null, null, null, null, null); + + NativeAdOptions nativeAdOptions = flutterNativeAdOptions.asNativeAdOptions(); + NativeAdOptions defaultOptions = new NativeAdOptions.Builder().build(); + assertEquals(nativeAdOptions.getAdChoicesPlacement(), defaultOptions.getAdChoicesPlacement()); + assertEquals(nativeAdOptions.getMediaAspectRatio(), defaultOptions.getMediaAspectRatio()); + assertEquals(nativeAdOptions.getVideoOptions(), defaultOptions.getVideoOptions()); + assertEquals( + nativeAdOptions.shouldRequestMultipleImages(), + defaultOptions.shouldRequestMultipleImages()); + assertEquals( + nativeAdOptions.shouldReturnUrlsForImageAssets(), + defaultOptions.shouldReturnUrlsForImageAssets()); + } + + @Test + public void testAsAdOptions() { + FlutterVideoOptions mockFlutterVideoOptions = Mockito.mock(FlutterVideoOptions.class); + VideoOptions mockVideoOptions = Mockito.mock(VideoOptions.class); + doReturn(mockVideoOptions).when(mockFlutterVideoOptions).asVideoOptions(); + + FlutterNativeAdOptions flutterNativeAdOptions = + new FlutterNativeAdOptions(1, 2, mockFlutterVideoOptions, true, false, true); + + NativeAdOptions nativeAdOptions = flutterNativeAdOptions.asNativeAdOptions(); + assertEquals(nativeAdOptions.getAdChoicesPlacement(), 1); + assertEquals(nativeAdOptions.getMediaAspectRatio(), 2); + assertFalse(nativeAdOptions.shouldRequestMultipleImages()); + assertTrue(nativeAdOptions.shouldReturnUrlsForImageAssets()); + assertEquals(nativeAdOptions.getVideoOptions(), mockVideoOptions); + } +} diff --git a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterNativeAdTest.java b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterNativeAdTest.java index d13e198ee..2ce30542e 100644 --- a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterNativeAdTest.java +++ b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterNativeAdTest.java @@ -69,6 +69,9 @@ public void loadNativeAdWithAdManagerAdRequest() { NativeAdFactory mockNativeAdFactory = mock(NativeAdFactory.class); @SuppressWarnings("unchecked") Map mockOptions = mock(Map.class); + FlutterNativeAdOptions mockFlutterNativeAdOptions = mock(FlutterNativeAdOptions.class); + NativeAdOptions mockNativeAdOptions = mock(NativeAdOptions.class); + doReturn(mockNativeAdOptions).when(mockFlutterNativeAdOptions).asNativeAdOptions(); final FlutterNativeAd nativeAd = new FlutterNativeAd( 1, @@ -77,7 +80,8 @@ public void loadNativeAdWithAdManagerAdRequest() { mockNativeAdFactory, mockFlutterRequest, mockLoader, - mockOptions); + mockOptions, + mockFlutterNativeAdOptions); final ResponseInfo responseInfo = mock(ResponseInfo.class); final NativeAd mockNativeAd = mock(NativeAd.class); @@ -135,7 +139,7 @@ public Object answer(InvocationOnMock invocation) { eq(testManager.activity), eq("testId"), any(OnNativeAdLoadedListener.class), - any(NativeAdOptions.class), + eq(mockNativeAdOptions), any(AdListener.class), eq(mockRequest)); @@ -169,6 +173,9 @@ public void loadNativeAdWithAdRequest() { .createNativeAd(any(NativeAd.class), any(Map.class)); @SuppressWarnings("unchecked") Map mockOptions = mock(Map.class); + FlutterNativeAdOptions mockFlutterNativeAdOptions = mock(FlutterNativeAdOptions.class); + NativeAdOptions mockNativeAdOptions = mock(NativeAdOptions.class); + doReturn(mockNativeAdOptions).when(mockFlutterNativeAdOptions).asNativeAdOptions(); final FlutterNativeAd nativeAd = new FlutterNativeAd( 1, @@ -177,7 +184,8 @@ public void loadNativeAdWithAdRequest() { mockNativeAdFactory, mockFlutterRequest, mockLoader, - mockOptions); + mockOptions, + mockFlutterNativeAdOptions); final ResponseInfo responseInfo = mock(ResponseInfo.class); final NativeAd mockNativeAd = mock(NativeAd.class); @@ -220,7 +228,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { eq(testManager.activity), eq("testId"), any(OnNativeAdLoadedListener.class), - any(NativeAdOptions.class), + eq(mockNativeAdOptions), any(AdListener.class), eq(mockRequest)); diff --git a/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterVideoOptionsTest.java b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterVideoOptionsTest.java new file mode 100644 index 000000000..55efe9bce --- /dev/null +++ b/packages/google_mobile_ads/android/src/test/java/io/flutter/plugins/googlemobileads/FlutterVideoOptionsTest.java @@ -0,0 +1,50 @@ +// Copyright 2021 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.VideoOptions; +import org.junit.Test; + +/** Tests for {@link FlutterVideoOptions}. */ +public class FlutterVideoOptionsTest { + + @Test + public void testVideoOptions_null() { + FlutterVideoOptions flutterVideoOptions = new FlutterVideoOptions(null, null, null); + + VideoOptions videoOptions = flutterVideoOptions.asVideoOptions(); + VideoOptions defaultOptions = new VideoOptions.Builder().build(); + assertEquals( + videoOptions.getClickToExpandRequested(), defaultOptions.getClickToExpandRequested()); + assertEquals( + videoOptions.getCustomControlsRequested(), defaultOptions.getCustomControlsRequested()); + assertEquals(videoOptions.getStartMuted(), defaultOptions.getStartMuted()); + } + + @Test + public void testVideoOptions() { + FlutterVideoOptions flutterVideoOptions = new FlutterVideoOptions(true, false, true); + + VideoOptions videoOptions = flutterVideoOptions.asVideoOptions(); + + assertTrue(videoOptions.getClickToExpandRequested()); + assertFalse(videoOptions.getCustomControlsRequested()); + assertTrue(videoOptions.getStartMuted()); + } +} 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 9984a2c00..0de46aa16 100644 --- a/packages/google_mobile_ads/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_mobile_ads/example/ios/Runner.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 9E5A534926AA235D00B9438D /* FLTAdUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E5A534826AA235D00B9438D /* FLTAdUtil.m */; }; 9EA7213725BB6517008D57E3 /* GoogleMobileAds.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9EA7213525BB6464008D57E3 /* GoogleMobileAds.xcframework */; }; 9EA7213825BB6530008D57E3 /* GoogleMobileAds.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9EA7213525BB6464008D57E3 /* GoogleMobileAds.xcframework */; }; 9EA7474E2637CD6800E0B0E4 /* FLTBannerAdTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 9EA7474C2637CD6800E0B0E4 /* FLTBannerAdTest.m */; }; @@ -89,6 +90,8 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9E5A534726AA235D00B9438D /* FLTAdUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FLTAdUtil.h; path = ../../ios/Classes/FLTAdUtil.h; sourceTree = ""; }; + 9E5A534826AA235D00B9438D /* FLTAdUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FLTAdUtil.m; path = ../../ios/Classes/FLTAdUtil.m; sourceTree = ""; }; 9EA7213525BB6464008D57E3 /* GoogleMobileAds.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = GoogleMobileAds.xcframework; path = "Pods/Google-Mobile-Ads-SDK/Frameworks/GoogleMobileAdsFramework-Current/GoogleMobileAds.xcframework"; sourceTree = ""; }; 9EA7474C2637CD6800E0B0E4 /* FLTBannerAdTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FLTBannerAdTest.m; path = ../../../ios/Tests/FLTBannerAdTest.m; sourceTree = ""; }; 9EA7474D2637CD6800E0B0E4 /* FLTGAMBannerAdTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FLTGAMBannerAdTest.m; path = ../../../ios/Tests/FLTGAMBannerAdTest.m; sourceTree = ""; }; @@ -191,6 +194,8 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( + 9E5A534726AA235D00B9438D /* FLTAdUtil.h */, + 9E5A534826AA235D00B9438D /* FLTAdUtil.m */, 9EF4E6FB26392B230007E4FE /* FLTAd_Internal.h */, 9EF4E6F426392B230007E4FE /* FLTAd_Internal.m */, 9EF4E6FD26392B230007E4FE /* FLTAdInstanceManager_Internal.h */, @@ -431,6 +436,7 @@ buildActionMask = 2147483647; files = ( 9EF4E70026392B230007E4FE /* FLTGoogleMobileAdsCollection_Internal.h in Sources */, + 9E5A534926AA235D00B9438D /* FLTAdUtil.m in Sources */, 9EF4E70126392B230007E4FE /* FLTAd_Internal.m in Sources */, 9EF4E70226392B230007E4FE /* FLTConstants.h in Sources */, 9EF4E70326392B230007E4FE /* FLTMobileAds_Internal.m in Sources */, diff --git a/packages/google_mobile_ads/ios/Classes/FLTAdUtil.h b/packages/google_mobile_ads/ios/Classes/FLTAdUtil.h new file mode 100644 index 000000000..d8b88eb28 --- /dev/null +++ b/packages/google_mobile_ads/ios/Classes/FLTAdUtil.h @@ -0,0 +1,23 @@ +// Copyright 2021 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 + +@interface FLTAdUtil : NSObject + ++ (BOOL)isNull:(id)object; + ++ (BOOL)isNotNull:(id)object; + +@end diff --git a/packages/google_mobile_ads/ios/Classes/FLTAdUtil.m b/packages/google_mobile_ads/ios/Classes/FLTAdUtil.m new file mode 100644 index 000000000..1cc50d211 --- /dev/null +++ b/packages/google_mobile_ads/ios/Classes/FLTAdUtil.m @@ -0,0 +1,27 @@ +// Copyright 2021 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 "FLTAdUtil.h" + +@implementation FLTAdUtil + ++ (BOOL)isNull:(id)object { + return object == nil || [[NSNull null] isEqual:object]; +} + ++ (BOOL)isNotNull:(id)object { + return ![FLTAdUtil isNull:object]; +} + +@end diff --git a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h index a98cb0b05..c2b4f602e 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h +++ b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.h @@ -164,6 +164,36 @@ - (GADRewardedAd *_Nullable)rewardedAd; @end +@interface FLTVideoOptions : NSObject +@property(readonly) NSNumber *_Nullable clickToExpandRequested; +@property(readonly) NSNumber *_Nullable customControlsRequested; +@property(readonly) NSNumber *_Nullable startMuted; +- (instancetype _Nonnull)initWithClickToExpandRequested:(NSNumber *_Nullable)clickToExpandRequested + customControlsRequested:(NSNumber *_Nullable)customControlsRequested + startMuted:(NSNumber *_Nullable)startMuted; +- (GADVideoOptions *_Nonnull)asGADVideoOptions; + +@end + +@interface FLTNativeAdOptions : NSObject +@property(readonly) NSNumber *_Nullable adChoicesPlacement; +@property(readonly) NSNumber *_Nullable mediaAspectRatio; +@property(readonly) FLTVideoOptions *_Nullable videoOptions; +@property(readonly) NSNumber *_Nullable requestCustomMuteThisAd; +@property(readonly) NSNumber *_Nullable shouldRequestMultipleImages; +@property(readonly) NSNumber *_Nullable shouldReturnUrlsForImageAssets; + +- (instancetype _Nonnull)initWithAdChoicesPlacement:(NSNumber *_Nullable)adChoicesPlacement + mediaAspectRatio:(NSNumber *_Nullable)mediaAspectRatio + videoOptions:(FLTVideoOptions *_Nullable)videoOptions + requestCustomMuteThisAd:(NSNumber *_Nullable)requestCustomMuteThisAd + shouldRequestMultipleImages:(NSNumber *_Nullable)shouldRequestMultipleImages + shouldReturnUrlsForImageAssets: + (NSNumber *_Nullable)shouldReturnUrlsForImageAssets; + +- (NSArray *_Nonnull)asGADAdLoaderOptions; +@end + @interface FLTNativeAd : FLTBaseAd - (instancetype _Nonnull)initWithAdUnitId:(NSString *_Nonnull)adUnitId @@ -171,7 +201,8 @@ nativeAdFactory:(NSObject *_Nonnull)nativeAdFactory customOptions:(NSDictionary *_Nullable)customOptions rootViewController:(UIViewController *_Nonnull)rootViewController - adId:(NSNumber *_Nonnull)adId; + adId:(NSNumber *_Nonnull)adId + nativeAdOptions:(FLTNativeAdOptions *_Nullable)nativeAdOptions; - (GADAdLoader *_Nonnull)adLoader; @end diff --git a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m index 61579bedb..32dca9fd5 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m +++ b/packages/google_mobile_ads/ios/Classes/FLTAd_Internal.m @@ -13,6 +13,7 @@ // limitations under the License. #import "FLTAd_Internal.h" +#import "FLTAdUtil.h" #import "FLTConstants.h" @implementation FLTAdSize @@ -604,6 +605,7 @@ @implementation FLTNativeAd { NSDictionary *_customOptions; GADNativeAdView *_view; GADAdLoader *_adLoader; + FLTNativeAdOptions *_nativeAdOptions; } - (instancetype _Nonnull)initWithAdUnitId:(NSString *_Nonnull)adUnitId @@ -611,7 +613,8 @@ - (instancetype _Nonnull)initWithAdUnitId:(NSString *_Nonnull)adUnitId nativeAdFactory:(NSObject *_Nonnull)nativeAdFactory customOptions:(NSDictionary *_Nullable)customOptions rootViewController:(UIViewController *_Nonnull)rootViewController - adId:(NSNumber *_Nonnull)adId { + adId:(NSNumber *_Nonnull)adId + nativeAdOptions:(FLTNativeAdOptions *_Nullable)nativeAdOptions { self = [super init]; if (self) { self.adId = adId; @@ -619,10 +622,16 @@ - (instancetype _Nonnull)initWithAdUnitId:(NSString *_Nonnull)adUnitId _adRequest = request; _nativeAdFactory = nativeAdFactory; _customOptions = customOptions; + NSArray *adLoaderOptions = + (nativeAdOptions == nil || [[NSNull null] isEqual:nativeAdOptions]) + ? @[] + : nativeAdOptions.asGADAdLoaderOptions; + _adLoader = [[GADAdLoader alloc] initWithAdUnitID:_adUnitId rootViewController:rootViewController adTypes:@[ kGADAdLoaderAdTypeNative ] - options:@[]]; + options:adLoaderOptions]; + _nativeAdOptions = nativeAdOptions; self.adLoader.delegate = self; } return self; @@ -741,3 +750,121 @@ - (instancetype _Nonnull)initWithValue:(NSDecimalNumber *_Nonnull)value return self; } @end + +@implementation FLTVideoOptions +- (instancetype _Nonnull)initWithClickToExpandRequested:(NSNumber *_Nullable)clickToExpandRequested + customControlsRequested:(NSNumber *_Nullable)customControlsRequested + startMuted:(NSNumber *_Nullable)startMuted { + self = [super init]; + if (self) { + _clickToExpandRequested = clickToExpandRequested; + _customControlsRequested = customControlsRequested; + _startMuted = startMuted; + } + return self; +} + +- (GADVideoOptions *_Nonnull)asGADVideoOptions { + GADVideoOptions *options = [[GADVideoOptions alloc] init]; + if ([FLTAdUtil isNotNull:_clickToExpandRequested]) { + options.clickToExpandRequested = _clickToExpandRequested.boolValue; + } + if ([FLTAdUtil isNotNull:_customControlsRequested]) { + options.customControlsRequested = _customControlsRequested.boolValue; + } + if ([FLTAdUtil isNotNull:_startMuted]) { + options.startMuted = _startMuted.boolValue; + } + return options; +} + +@end + +@implementation FLTNativeAdOptions +- (instancetype _Nonnull)initWithAdChoicesPlacement:(NSNumber *_Nullable)adChoicesPlacement + mediaAspectRatio:(NSNumber *_Nullable)mediaAspectRatio + videoOptions:(FLTVideoOptions *_Nullable)videoOptions + requestCustomMuteThisAd:(NSNumber *_Nullable)requestCustomMuteThisAd + shouldRequestMultipleImages:(NSNumber *_Nullable)shouldRequestMultipleImages + shouldReturnUrlsForImageAssets: + (NSNumber *_Nullable)shouldReturnUrlsForImageAssets { + self = [super init]; + if (self) { + _adChoicesPlacement = adChoicesPlacement; + _mediaAspectRatio = mediaAspectRatio; + _videoOptions = videoOptions; + _requestCustomMuteThisAd = requestCustomMuteThisAd; + _shouldRequestMultipleImages = shouldRequestMultipleImages; + _shouldReturnUrlsForImageAssets = shouldReturnUrlsForImageAssets; + } + return self; +} + +- (NSArray *_Nonnull)asGADAdLoaderOptions { + NSMutableArray *options = [NSMutableArray array]; + + GADNativeAdImageAdLoaderOptions *imageOptions = [[GADNativeAdImageAdLoaderOptions alloc] init]; + if ([FLTAdUtil isNotNull:_shouldReturnUrlsForImageAssets]) { + imageOptions.disableImageLoading = _shouldReturnUrlsForImageAssets.boolValue; + } + if ([FLTAdUtil isNotNull:_shouldRequestMultipleImages]) { + imageOptions.shouldRequestMultipleImages = _shouldRequestMultipleImages.boolValue; + } + [options addObject:imageOptions]; + + if ([FLTAdUtil isNotNull:_adChoicesPlacement]) { + GADNativeAdViewAdOptions *adViewOptions = [[GADNativeAdViewAdOptions alloc] init]; + switch (_adChoicesPlacement.intValue) { + case 0: + adViewOptions.preferredAdChoicesPosition = GADAdChoicesPositionTopRightCorner; + break; + case 1: + adViewOptions.preferredAdChoicesPosition = GADAdChoicesPositionTopLeftCorner; + break; + case 2: + adViewOptions.preferredAdChoicesPosition = GADAdChoicesPositionBottomRightCorner; + break; + case 3: + adViewOptions.preferredAdChoicesPosition = GADAdChoicesPositionBottomLeftCorner; + break; + default: + NSLog(@"AdChoicesPlacement should be an int in the range [0, 3]: %d", + _adChoicesPlacement.intValue); + break; + } + [options addObject:adViewOptions]; + } + + if ([FLTAdUtil isNotNull:_mediaAspectRatio]) { + GADNativeAdMediaAdLoaderOptions *mediaOptions = [[GADNativeAdMediaAdLoaderOptions alloc] init]; + switch (_mediaAspectRatio.intValue) { + case 0: + mediaOptions.mediaAspectRatio = GADMediaAspectRatioUnknown; + break; + case 1: + mediaOptions.mediaAspectRatio = GADMediaAspectRatioAny; + break; + case 2: + mediaOptions.mediaAspectRatio = GADMediaAspectRatioLandscape; + break; + case 3: + mediaOptions.mediaAspectRatio = GADMediaAspectRatioPortrait; + break; + case 4: + mediaOptions.mediaAspectRatio = GADMediaAspectRatioSquare; + break; + default: + NSLog(@"MediaAspectRatio should be an int in the range [0, 4]: %d", + _mediaAspectRatio.intValue); + break; + } + [options addObject:mediaOptions]; + } + + if ([FLTAdUtil isNotNull:_videoOptions]) { + [options addObject:_videoOptions.asGADVideoOptions]; + } + return options; +} + +@end diff --git a/packages/google_mobile_ads/ios/Classes/FLTConstants.h b/packages/google_mobile_ads/ios/Classes/FLTConstants.h index e9de7cb43..684803217 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTConstants.h +++ b/packages/google_mobile_ads/ios/Classes/FLTConstants.h @@ -13,4 +13,4 @@ // limitations under the License. /** Versioned request agent string. */ -#define FLT_REQUEST_AGENT_VERSIONED @"Flutter-GMA-0.13.2+1" +#define FLT_REQUEST_AGENT_VERSIONED @"Flutter-GMA-0.13.3" diff --git a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m index 0bdf99acd..eeb4dc5fc 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m +++ b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsPlugin.m @@ -224,7 +224,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result nativeAdFactory:(id)factory customOptions:call.arguments[@"customOptions"] rootViewController:rootController - adId:call.arguments[@"adId"]]; + adId:call.arguments[@"adId"] + nativeAdOptions:call.arguments[@"nativeAdOptions"]]; [_manager loadAd:ad]; result(nil); } else if ([call.method isEqualToString:@"loadInterstitialAd"]) { diff --git a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m index 4d4e55866..5d68e7b4a 100644 --- a/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m +++ b/packages/google_mobile_ads/ios/Classes/FLTGoogleMobileAdsReaderWriter_Internal.m @@ -30,6 +30,8 @@ typedef NS_ENUM(NSInteger, FLTAdMobField) { FLTAdmobFieldGADAdNetworkResponseInfo = 141, FLTAdmobFieldAnchoredAdaptiveBannerAdSize = 142, FLTAdmobFieldSmartBannerAdSize = 143, + FLTAdmobFieldNativeAdOptions = 144, + FLTAdmobFieldVideoOptions = 145, }; @interface FLTGoogleMobileAdsWriter : FlutterStandardWriter @@ -185,9 +187,25 @@ - (id _Nullable)readValueOfType:(UInt8)type { case FLTAdmobFieldSmartBannerAdSize: return [[FLTSmartBannerSize alloc] initWithOrientation:[self readValueOfType:[self readByte]]]; + case FLTAdmobFieldNativeAdOptions: { + return [[FLTNativeAdOptions alloc] + initWithAdChoicesPlacement:[self readValueOfType:[self readByte]] + mediaAspectRatio:[self readValueOfType:[self readByte]] + videoOptions:[self readValueOfType:[self readByte]] + requestCustomMuteThisAd:[self readValueOfType:[self readByte]] + shouldRequestMultipleImages:[self readValueOfType:[self readByte]] + shouldReturnUrlsForImageAssets:[self readValueOfType:[self readByte]]]; + } + case FLTAdmobFieldVideoOptions: { + return [[FLTVideoOptions alloc] + initWithClickToExpandRequested:[self readValueOfType:[self readByte]] + customControlsRequested:[self readValueOfType:[self readByte]] + startMuted:[self readValueOfType:[self readByte]]]; + } } return [super readValueOfType:type]; } + @end @implementation FLTGoogleMobileAdsWriter @@ -282,6 +300,21 @@ - (void)writeValue:(id _Nonnull)value { FLTServerSideVerificationOptions *options = value; [self writeValue:options.userIdentifier]; [self writeValue:options.customRewardString]; + } else if ([value isKindOfClass:[FLTNativeAdOptions class]]) { + [self writeByte:FLTAdmobFieldNativeAdOptions]; + FLTNativeAdOptions *options = value; + [self writeValue:options.adChoicesPlacement]; + [self writeValue:options.mediaAspectRatio]; + [self writeValue:options.videoOptions]; + [self writeValue:options.requestCustomMuteThisAd]; + [self writeValue:options.shouldRequestMultipleImages]; + [self writeValue:options.shouldReturnUrlsForImageAssets]; + } else if ([value isKindOfClass:[FLTVideoOptions class]]) { + [self writeByte:FLTAdmobFieldVideoOptions]; + FLTVideoOptions *options = value; + [self writeValue:options.clickToExpandRequested]; + [self writeValue:options.customControlsRequested]; + [self writeValue:options.startMuted]; } else { [super writeValue:value]; } diff --git a/packages/google_mobile_ads/ios/Tests/FLTGoogleMobileAdsTest.m b/packages/google_mobile_ads/ios/Tests/FLTGoogleMobileAdsTest.m index 3226109c6..5b8d452f1 100644 --- a/packages/google_mobile_ads/ios/Tests/FLTGoogleMobileAdsTest.m +++ b/packages/google_mobile_ads/ios/Tests/FLTGoogleMobileAdsTest.m @@ -114,7 +114,8 @@ - (void)testAdInstanceManagerOnAdLoaded { nativeAdFactory:OCMProtocolMock(@protocol(FLTNativeAdFactory)) customOptions:nil rootViewController:OCMClassMock([UIViewController class]) - adId:@1]; + adId:@1 + nativeAdOptions:nil]; [_manager loadAd:ad]; GADResponseInfo *responseInfo = OCMClassMock([GADResponseInfo class]); @@ -138,7 +139,8 @@ - (void)testAdInstanceManagerOnAdFailedToLoad { nativeAdFactory:OCMProtocolMock(@protocol(FLTNativeAdFactory)) customOptions:nil rootViewController:OCMClassMock([UIViewController class]) - adId:@1]; + adId:@1 + nativeAdOptions:nil]; [_manager loadAd:ad]; NSDictionary *userInfo = @{NSLocalizedDescriptionKey : @"message"}; @@ -184,7 +186,8 @@ - (void)testAdInstanceManagerOnAppEvent { nativeAdFactory:OCMProtocolMock(@protocol(FLTNativeAdFactory)) customOptions:nil rootViewController:OCMClassMock([UIViewController class]) - adId:@1]; + adId:@1 + nativeAdOptions:nil]; [_manager loadAd:ad]; [_manager onAppEvent:ad name:@"color" data:@"red"]; @@ -207,7 +210,8 @@ - (void)testAdInstanceManagerOnNativeAdEvents { nativeAdFactory:OCMProtocolMock(@protocol(FLTNativeAdFactory)) customOptions:nil rootViewController:OCMClassMock([UIViewController class]) - adId:@1]; + adId:@1 + nativeAdOptions:nil]; [_manager loadAd:ad]; [_manager onNativeAdClicked:ad]; @@ -261,7 +265,8 @@ - (void)testAdInstanceManagerOnPaidEvent { nativeAdFactory:OCMProtocolMock(@protocol(FLTNativeAdFactory)) customOptions:nil rootViewController:OCMClassMock([UIViewController class]) - adId:@1]; + adId:@1 + nativeAdOptions:nil]; [_manager loadAd:ad]; NSDecimalNumber *valueDecimal = [[NSDecimalNumber alloc] initWithInt:1]; diff --git a/packages/google_mobile_ads/ios/Tests/FLTNativeAdTest.m b/packages/google_mobile_ads/ios/Tests/FLTNativeAdTest.m index 8536d615a..959a696a4 100644 --- a/packages/google_mobile_ads/ios/Tests/FLTNativeAdTest.m +++ b/packages/google_mobile_ads/ios/Tests/FLTNativeAdTest.m @@ -44,17 +44,23 @@ - (void)testLoadNativeAdWithGAMRequest { - (void)testLoadNativeAd:(FLTAdRequest *)gadOrGAMRequest customOptions:(NSDictionary *_Nullable)customOptions { id mockNativeAdFactory = OCMProtocolMock(@protocol(FLTNativeAdFactory)); + FLTNativeAdOptions *mockNativeAdOptions = OCMClassMock([FLTNativeAdOptions class]); + GADAdLoaderOptions *mockGADAdLoaderOptions = OCMClassMock([GADAdLoaderOptions class]); + OCMStub([mockNativeAdOptions asGADAdLoaderOptions]).andReturn(mockGADAdLoaderOptions); + UIViewController *mockViewController = OCMClassMock([UIViewController class]); FLTNativeAd *ad = [[FLTNativeAd alloc] initWithAdUnitId:@"testAdUnitId" request:gadOrGAMRequest nativeAdFactory:mockNativeAdFactory customOptions:customOptions - rootViewController:OCMClassMock([UIViewController class]) - adId:@1]; + rootViewController:mockViewController + adId:@1 + nativeAdOptions:mockNativeAdOptions]; ad.manager = mockManager; XCTAssertEqual(ad.adLoader.adUnitID, @"testAdUnitId"); XCTAssertEqual(ad.adLoader.delegate, ad); + OCMVerify([mockNativeAdOptions asGADAdLoaderOptions]); FLTNativeAd *mockNativeAd = OCMPartialMock(ad); GADAdLoader *mockLoader = OCMPartialMock([ad adLoader]); diff --git a/packages/google_mobile_ads/lib/src/ad_containers.dart b/packages/google_mobile_ads/lib/src/ad_containers.dart index f831d7f8e..08dfa6fd8 100644 --- a/packages/google_mobile_ads/lib/src/ad_containers.dart +++ b/packages/google_mobile_ads/lib/src/ad_containers.dart @@ -604,11 +604,14 @@ class NativeAd extends AdWithView { /// /// A valid [adUnitId], nonnull [listener], nonnull [request], and nonnull /// [factoryId] is required. + /// Use [nativeAdOptions] to customize the native ad request. + /// Use [customOptions] to pass data to your native ad factory. NativeAd({ required String adUnitId, required this.factoryId, required this.listener, required this.request, + this.nativeAdOptions, this.customOptions, }) : adManagerRequest = null, assert(request != null), @@ -618,11 +621,14 @@ class NativeAd extends AdWithView { /// /// A valid [adUnitId], nonnull [listener], nonnull [adManagerRequest], and /// nonnull [factoryId] is required. + /// Use [nativeAdOptions] to customize the native ad request. + /// Use [customOptions] to pass data to your native ad factory. NativeAd.fromAdManagerRequest({ required String adUnitId, required this.factoryId, required this.listener, required this.adManagerRequest, + this.nativeAdOptions, this.customOptions, }) : request = null, assert(adManagerRequest != null), @@ -646,6 +652,9 @@ class NativeAd extends AdWithView { /// Targeting information used to fetch an [Ad] with Ad Manager. final AdManagerAdRequest? adManagerRequest; + /// Options to configure the native ad request. + final NativeAdOptions? nativeAdOptions; + /// {@template google_mobile_ads.testAdUnitId} /// A platform-specific AdMob test ad unit ID. /// @@ -892,3 +901,122 @@ class ServerSideVerificationOptions { customData == other.customData; } } + +/// Media aspect ratio for native ads. +enum MediaAspectRatio { + /// Unknown media aspect ratio. + unknown, + + /// Any media aspect ratio. + any, + + /// Landscape media aspect ratio. + landscape, + + /// Portrait media aspect ratio. + portrait, + + /// Close to square media aspect ratio. This is not a strict 1:1 aspect ratio. + square +} + +/// Indicates preferred location of AdChoices icon. +enum AdChoicesPlacement { + /// Top right corner. + topRightCorner, + + /// Top left corner. + topLeftCorner, + + /// Bottom right corner. + bottomRightCorner, + + /// Bottom left corner. + bottomLeftCorner +} + +/// Used to configure native ad requests. +class NativeAdOptions { + /// Where to place the AdChoices icon. + /// + /// Default is top right. + final AdChoicesPlacement? adChoicesPlacement; + + /// The media aspect ratio. + /// + /// Default is unknown, which will apply no restrictions. + final MediaAspectRatio? mediaAspectRatio; + + /// Options for video. + final VideoOptions? videoOptions; + + /// Whether to request a custom implementation for the Mute This Ad feature. + /// + /// Default value is false. + final bool? requestCustomMuteThisAd; + + /// Sets whether multiple images should be requested or not. + /// + /// Default value is false. + final bool? shouldRequestMultipleImages; + + /// Indicates whether image asset content should be loaded by the SDK. + /// + /// If set to true, the SDK will not load image asset content and native ad + /// image URLs can be used to fetch content. Defaults to false. + final bool? shouldReturnUrlsForImageAssets; + + /// Construct a NativeAdOptions, an optional class used to further customize + /// native ad requests. + NativeAdOptions({ + this.adChoicesPlacement, + this.mediaAspectRatio, + this.videoOptions, + this.requestCustomMuteThisAd, + this.shouldRequestMultipleImages, + this.shouldReturnUrlsForImageAssets, + }); + + @override + bool operator ==(other) { + return other is NativeAdOptions && + adChoicesPlacement == other.adChoicesPlacement && + mediaAspectRatio == other.mediaAspectRatio && + videoOptions == other.videoOptions && + requestCustomMuteThisAd == other.requestCustomMuteThisAd && + shouldRequestMultipleImages == other.shouldRequestMultipleImages && + shouldReturnUrlsForImageAssets == other.shouldReturnUrlsForImageAssets; + } +} + +/// Options for controlling video playback in supported ad formats. +class VideoOptions { + /// Indicates whether the requested video should have the click to expand + /// behavior. + final bool? clickToExpandRequested; + + /// Indicates whether the requested video should have custom controls enabled for + /// play/pause/mute/unmute. + final bool? customControlsRequested; + + /// Indicates whether videos should start muted. By default this property value is YES. + final bool? startMuted; + + /// Constructs a VideoOptions to further customize a native ad request. + /// + /// This is only necessary if you wish to further customize your native ad + /// integration. + VideoOptions({ + this.clickToExpandRequested, + this.customControlsRequested, + this.startMuted, + }); + + @override + bool operator ==(other) { + return other is VideoOptions && + clickToExpandRequested == other.clickToExpandRequested && + customControlsRequested == other.customControlsRequested && + startMuted == other.startMuted; + } +} 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 c522d1a9d..6b3a49f14 100644 --- a/packages/google_mobile_ads/lib/src/ad_instance_manager.dart +++ b/packages/google_mobile_ads/lib/src/ad_instance_manager.dart @@ -424,6 +424,7 @@ class AdInstanceManager { 'request': ad.request, 'adManagerRequest': ad.adManagerRequest, 'factoryId': ad.factoryId, + 'nativeAdOptions': ad.nativeAdOptions, 'customOptions': ad.customOptions, }, ); @@ -553,8 +554,6 @@ class AdInstanceManager { @visibleForTesting class AdMessageCodec extends StandardMessageCodec { - const AdMessageCodec(); - // The type values below must be consistent for each platform. static const int _valueAdSize = 128; static const int _valueAdRequest = 129; @@ -570,6 +569,8 @@ class AdMessageCodec extends StandardMessageCodec { static const int _valueAdapterResponseInfo = 141; static const int _valueAnchoredAdaptiveBannerAdSize = 142; static const int _valueSmartBannerAdSize = 143; + static const int _valueNativeAdOptions = 144; + static const int _valueVideoOptions = 145; @override void writeValue(WriteBuffer buffer, dynamic value) { @@ -629,6 +630,19 @@ class AdMessageCodec extends StandardMessageCodec { buffer.putUint8(_valueServerSideVerificationOptions); writeValue(buffer, value.userId); writeValue(buffer, value.customData); + } else if (value is NativeAdOptions) { + buffer.putUint8(_valueNativeAdOptions); + writeValue(buffer, value.adChoicesPlacement?.intValue); + writeValue(buffer, value.mediaAspectRatio?.intValue); + writeValue(buffer, value.videoOptions); + writeValue(buffer, value.requestCustomMuteThisAd); + writeValue(buffer, value.shouldRequestMultipleImages); + writeValue(buffer, value.shouldReturnUrlsForImageAssets); + } else if (value is VideoOptions) { + buffer.putUint8(_valueVideoOptions); + writeValue(buffer, value.clickToExpandRequested); + writeValue(buffer, value.customControlsRequested); + writeValue(buffer, value.startMuted); } else { super.writeValue(buffer, value); } @@ -741,6 +755,25 @@ class AdMessageCodec extends StandardMessageCodec { return ServerSideVerificationOptions( userId: readValueOfType(buffer.getUint8(), buffer), customData: readValueOfType(buffer.getUint8(), buffer)); + case _valueNativeAdOptions: + int? adChoices = readValueOfType(buffer.getUint8(), buffer); + int? mediaAspectRatio = readValueOfType(buffer.getUint8(), buffer); + return NativeAdOptions( + adChoicesPlacement: AdChoicesPlacementExtension.fromInt(adChoices), + mediaAspectRatio: MediaAspectRatioExtension.fromInt(mediaAspectRatio), + videoOptions: readValueOfType(buffer.getUint8(), buffer), + requestCustomMuteThisAd: readValueOfType(buffer.getUint8(), buffer), + shouldRequestMultipleImages: + readValueOfType(buffer.getUint8(), buffer), + shouldReturnUrlsForImageAssets: + readValueOfType(buffer.getUint8(), buffer), + ); + case _valueVideoOptions: + return VideoOptions( + clickToExpandRequested: readValueOfType(buffer.getUint8(), buffer), + customControlsRequested: readValueOfType(buffer.getUint8(), buffer), + startMuted: readValueOfType(buffer.getUint8(), buffer), + ); default: return super.readValueOfType(type, buffer); } @@ -774,6 +807,76 @@ class AdMessageCodec extends StandardMessageCodec { } } +/// An extension that maps each [MediaAspectRatio] to an int. +extension MediaAspectRatioExtension on MediaAspectRatio { + /// Gets the int mapping to pass to platform channel. + int get intValue { + switch (this) { + case MediaAspectRatio.unknown: + return 0; + case MediaAspectRatio.any: + return 1; + case MediaAspectRatio.landscape: + return 2; + case MediaAspectRatio.portrait: + return 3; + case MediaAspectRatio.square: + return 4; + } + } + + /// Maps an int back to [MediaAspectRatio]. + static MediaAspectRatio? fromInt(int? intValue) { + switch (intValue) { + case 0: + return MediaAspectRatio.unknown; + case 1: + return MediaAspectRatio.any; + case 2: + return MediaAspectRatio.landscape; + case 3: + return MediaAspectRatio.portrait; + case 4: + return MediaAspectRatio.square; + default: + return null; + } + } +} + +/// An extension that maps each [AdChoicesPlacement] to an int. +extension AdChoicesPlacementExtension on AdChoicesPlacement { + /// Gets the int mapping to pass to platform channel. + int get intValue { + switch (this) { + case AdChoicesPlacement.topRightCorner: + return 0; + case AdChoicesPlacement.topLeftCorner: + return 1; + case AdChoicesPlacement.bottomRightCorner: + return 2; + case AdChoicesPlacement.bottomLeftCorner: + return 3; + } + } + + /// Maps an int back to [AdChoicesPlacement]. + static AdChoicesPlacement? fromInt(int? intValue) { + switch (intValue) { + case 0: + return AdChoicesPlacement.topRightCorner; + case 1: + return AdChoicesPlacement.topLeftCorner; + case 2: + return AdChoicesPlacement.bottomRightCorner; + case 3: + return AdChoicesPlacement.bottomLeftCorner; + default: + return null; + } + } +} + class _BiMap extends MapBase { _BiMap() { _inverse = _BiMap._inverse(this); diff --git a/packages/google_mobile_ads/pubspec.yaml b/packages/google_mobile_ads/pubspec.yaml index 2eb605f32..ebb7dbe61 100644 --- a/packages/google_mobile_ads/pubspec.yaml +++ b/packages/google_mobile_ads/pubspec.yaml @@ -16,7 +16,7 @@ name: google_mobile_ads description: Flutter plugin for Google Mobile Ads, supporting banner, interstitial (full-screen), rewarded and native ads homepage: https://github.com/googleads/googleads-mobile-flutter/tree/master/packages/google_mobile_ads -version: 0.13.2+1 +version: 0.13.3 flutter: plugin: diff --git a/packages/google_mobile_ads/test/ad_containers_test.dart b/packages/google_mobile_ads/test/ad_containers_test.dart index cf6e8336e..344041079 100644 --- a/packages/google_mobile_ads/test/ad_containers_test.dart +++ b/packages/google_mobile_ads/test/ad_containers_test.dart @@ -157,13 +157,24 @@ void main() { test('load native', () async { final Map options = {'a': 1, 'b': 2}; - + final NativeAdOptions nativeAdOptions = NativeAdOptions( + adChoicesPlacement: AdChoicesPlacement.bottomLeftCorner, + mediaAspectRatio: MediaAspectRatio.any, + videoOptions: VideoOptions( + clickToExpandRequested: true, + customControlsRequested: true, + startMuted: true, + ), + requestCustomMuteThisAd: false, + shouldRequestMultipleImages: true, + shouldReturnUrlsForImageAssets: false); final NativeAd native = NativeAd( adUnitId: NativeAd.testAdUnitId, factoryId: '0', customOptions: options, listener: NativeAdListener(), request: AdRequest(), + nativeAdOptions: nativeAdOptions, ); await native.load(); @@ -174,6 +185,7 @@ void main() { 'request': native.request, 'adManagerRequest': null, 'factoryId': '0', + 'nativeAdOptions': nativeAdOptions, 'customOptions': options, }) ]); @@ -200,6 +212,7 @@ void main() { 'request': null, 'adManagerRequest': native.adManagerRequest, 'factoryId': '0', + 'nativeAdOptions': null, 'customOptions': options, }) ]); diff --git a/packages/google_mobile_ads/test/mobile_ads_test.dart b/packages/google_mobile_ads/test/mobile_ads_test.dart index 3c124fd58..b600f800a 100644 --- a/packages/google_mobile_ads/test/mobile_ads_test.dart +++ b/packages/google_mobile_ads/test/mobile_ads_test.dart @@ -149,5 +149,81 @@ void main() { arguments: {'isEnabled': false}) ]); }); + + test('encode/decode empty native ad options', () { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + ByteData byteData = codec.encodeMessage(NativeAdOptions())!; + + NativeAdOptions result = codec.decodeMessage(byteData); + expect(result.mediaAspectRatio, null); + expect(result.adChoicesPlacement, null); + expect(result.requestCustomMuteThisAd, null); + expect(result.shouldRequestMultipleImages, null); + expect(result.shouldReturnUrlsForImageAssets, null); + expect(result.videoOptions, null); + + byteData = + codec.encodeMessage(NativeAdOptions(videoOptions: VideoOptions()))!; + result = codec.decodeMessage(byteData); + expect(result.mediaAspectRatio, null); + expect(result.adChoicesPlacement, null); + expect(result.requestCustomMuteThisAd, null); + expect(result.shouldRequestMultipleImages, null); + expect(result.shouldReturnUrlsForImageAssets, null); + expect(result.videoOptions!.clickToExpandRequested, null); + expect(result.videoOptions!.customControlsRequested, null); + expect(result.videoOptions!.startMuted, null); + }); + + test('encode/decode native ad options', () { + NativeAdOptions nativeAdOptions = NativeAdOptions( + adChoicesPlacement: AdChoicesPlacement.topRightCorner, + mediaAspectRatio: MediaAspectRatio.unknown, + videoOptions: VideoOptions( + clickToExpandRequested: false, + customControlsRequested: false, + startMuted: false, + ), + requestCustomMuteThisAd: false, + shouldRequestMultipleImages: false, + shouldReturnUrlsForImageAssets: false); + + debugDefaultTargetPlatformOverride = TargetPlatform.android; + ByteData byteData = codec.encodeMessage(nativeAdOptions)!; + + NativeAdOptions result = codec.decodeMessage(byteData); + expect(result.mediaAspectRatio, MediaAspectRatio.unknown); + expect(result.adChoicesPlacement, AdChoicesPlacement.topRightCorner); + expect(result.requestCustomMuteThisAd, false); + expect(result.shouldRequestMultipleImages, false); + expect(result.shouldReturnUrlsForImageAssets, false); + expect(result.videoOptions!.startMuted, false); + expect(result.videoOptions!.customControlsRequested, false); + expect(result.videoOptions!.clickToExpandRequested, false); + + nativeAdOptions = NativeAdOptions( + adChoicesPlacement: AdChoicesPlacement.bottomLeftCorner, + mediaAspectRatio: MediaAspectRatio.landscape, + videoOptions: VideoOptions( + clickToExpandRequested: true, + customControlsRequested: true, + startMuted: true, + ), + requestCustomMuteThisAd: true, + shouldRequestMultipleImages: true, + shouldReturnUrlsForImageAssets: true); + + byteData = codec.encodeMessage(nativeAdOptions)!; + result = codec.decodeMessage(byteData); + + expect(result.mediaAspectRatio, MediaAspectRatio.landscape); + expect(result.adChoicesPlacement, AdChoicesPlacement.bottomLeftCorner); + expect(result.requestCustomMuteThisAd, true); + expect(result.shouldRequestMultipleImages, true); + expect(result.shouldReturnUrlsForImageAssets, true); + expect(result.videoOptions!.startMuted, true); + expect(result.videoOptions!.customControlsRequested, true); + expect(result.videoOptions!.clickToExpandRequested, true); + }); }); }