diff --git a/source/plugin/Assets/GoogleMobileAds/Editor/GoogleMobileAdsDependencies.xml b/source/plugin/Assets/GoogleMobileAds/Editor/GoogleMobileAdsDependencies.xml index 245be949c..c59645a7b 100644 --- a/source/plugin/Assets/GoogleMobileAds/Editor/GoogleMobileAdsDependencies.xml +++ b/source/plugin/Assets/GoogleMobileAds/Editor/GoogleMobileAdsDependencies.xml @@ -1,6 +1,6 @@ - + https://maven.google.com/ diff --git a/source/plugin/Assets/GoogleMobileAds/Platforms/Android/GoogleMobileAdsClientFactory.cs b/source/plugin/Assets/GoogleMobileAds/Platforms/Android/GoogleMobileAdsClientFactory.cs index c532f3f1f..c6aa3e4db 100644 --- a/source/plugin/Assets/GoogleMobileAds/Platforms/Android/GoogleMobileAdsClientFactory.cs +++ b/source/plugin/Assets/GoogleMobileAds/Platforms/Android/GoogleMobileAdsClientFactory.cs @@ -124,6 +124,10 @@ public IRewardedInterstitialAdClient BuildRewardedInterstitialAdClient() public INativeOverlayAdClient BuildNativeOverlayAdClient() { if (Application.platform == RuntimePlatform.Android) { + if (IsNextGenEnabled()) + { + return new GoogleMobileAds.Android.NextGenNativeOverlayAdClient(); + } return new GoogleMobileAds.Android.NativeOverlayAdClient(); } throw new InvalidOperationException(@"Called " + MethodBase.GetCurrentMethod().Name + diff --git a/source/plugin/Assets/GoogleMobileAds/Platforms/Android/NextGenNativeOverlayAdClient.cs b/source/plugin/Assets/GoogleMobileAds/Platforms/Android/NextGenNativeOverlayAdClient.cs new file mode 100644 index 000000000..14eb2c182 --- /dev/null +++ b/source/plugin/Assets/GoogleMobileAds/Platforms/Android/NextGenNativeOverlayAdClient.cs @@ -0,0 +1,342 @@ +// Copyright (C) 2026 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 +// +// http://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. + +using System; +using UnityEngine; + +using GoogleMobileAds.Api; +using GoogleMobileAds.Common; + +namespace GoogleMobileAds.Android +{ + public class NextGenNativeOverlayAdClient : AndroidJavaProxy, INativeOverlayAdClient + { + private AndroidJavaObject nativeOverlayAd; + + public NextGenNativeOverlayAdClient() : base(NextGenUtils.UnityNativeTemplateAdCallbackClassName) + { + AndroidJavaClass playerClass = new AndroidJavaClass(Utils.UnityActivityClassName); + AndroidJavaObject activity = playerClass.GetStatic("currentActivity"); + this.nativeOverlayAd = + new AndroidJavaObject(NextGenUtils.UnityNativeTemplateAdClassName, activity, this); + } + +#region INativeOverlayAdClient implementation + // Ad event fired when the native ad has loaded. + public event EventHandler OnAdLoaded; + // Ad event fired when the native ad has failed to load. + public event EventHandler OnAdFailedToLoad; + // Ad event fired when an ad impression has been recorded. + public event EventHandler OnAdDidRecordImpression; + // Ad event fired when the full screen content has been presented. + public event EventHandler OnAdDidPresentFullScreenContent; + // Ad event fired when the full screen content has been dismissed. + public event EventHandler OnAdDidDismissFullScreenContent; + // Ad event fired when an ad has been clicked. + public event Action OnAdClicked; + + public event Action OnPaidEvent; + + // A long integer provided by the AdMob UI for the configured placement. + public long PlacementId + { + get + { + return this.nativeOverlayAd.Call("getPlacementId"); + } + set + { + this.nativeOverlayAd.Call("setPlacementId", value); + } + } + + // Loads a native ad + public void Load(string adUnitId, AdRequest request, NativeAdOptions options) + { + AndroidJavaObject nativeAdOptionsJava = new AndroidJavaObject("com.google.unity.ads.nextgen.NativeAdOptions"); + nativeAdOptionsJava.Call("setMediaAspectRatio", (int)options.MediaAspectRatio); + nativeAdOptionsJava.Call("setAdChoicesPlacement", (int)options.AdChoicesPlacement); + + if (options.VideoOptions != null) + { + AndroidJavaObject videoOptionsBuilder = new AndroidJavaObject("com.google.android.libraries.ads.mobile.sdk.common.VideoOptions$Builder"); + videoOptionsBuilder.Call("setStartMuted", (bool)options.VideoOptions.StartMuted); + videoOptionsBuilder.Call("setCustomControlsRequested", (bool)options.VideoOptions.CustomControlsRequested); + videoOptionsBuilder.Call("setClickToExpandRequested", (bool)options.VideoOptions.ClickToExpandRequested); + AndroidJavaObject videoOptionsJava = videoOptionsBuilder.Call("build"); + nativeAdOptionsJava.Call("setVideoOptions", videoOptionsJava); + } + + this.nativeOverlayAd.Call("loadAd", adUnitId, nativeAdOptionsJava, + NextGenUtils.GetAdRequestJavaObject(request, adUnitId)); + } + + // Hides the native overlay from the screen. + public void Hide() + { + this.nativeOverlayAd.Call("hide"); + } + + // Shows the Native overlay on the screen. + public void Show() + { + this.nativeOverlayAd.Call("show"); + } + + // Set the position of the Native overlay using standard position. + public void SetPosition(AdPosition position) + { + this.nativeOverlayAd.Call("setPositionCode", (int)position); + } + + // Set the position of the Native overlay using custom position. + public void SetPosition(int x, int y) + { + this.nativeOverlayAd.Call("setPosition", x, y); + } + + // Renders the Native overlay ad on the screen using the provided style, size and + // AdPosition. + public void Render(NativeTemplateStyle templateViewStyle, AdSize adSize, + AdPosition adPosition) + { + this.nativeOverlayAd.Call("renderCustomSizeAtPositionCode", + GetNativeTemplateStyleJavaObject(templateViewStyle), + NextGenUtils.GetAdSizeJavaObject(adSize), (int)adPosition); + } + + // Renders the Native overlay ad on the screen using the provided style, size and + // coordinates. + public void Render(NativeTemplateStyle templateViewStyle, AdSize adSize, int x, int y) + { + this.nativeOverlayAd.Call("renderCustomSizeAtPosition", + GetNativeTemplateStyleJavaObject(templateViewStyle), + NextGenUtils.GetAdSizeJavaObject(adSize), x, y); + } + + // Renders the Native overlay ad on the screen using default size at preset position. + public void Render(NativeTemplateStyle templateViewStyle, AdPosition adPosition) + { + this.nativeOverlayAd.Call("renderDefaultSizeAtPositionCode", + GetNativeTemplateStyleJavaObject(templateViewStyle), + (int)adPosition); + } + + // Renders the Native overlay ad on the screen using default size at (x,y) coordinates. + public void Render(NativeTemplateStyle templateViewStyle, int x, int y) + { + this.nativeOverlayAd.Call("renderDefaultSizeAtPosition", + GetNativeTemplateStyleJavaObject(templateViewStyle), x, + y); + } + + // Destroys the native ad. + public void DestroyAd() + { + this.nativeOverlayAd.Call("destroy"); + } + + // Returns ad request Response info client. + public IResponseInfoClient GetResponseInfoClient() + { + var responseInfoJavaObject = nativeOverlayAd.Call("getResponseInfo"); + return new NextGenResponseInfoClient(responseInfoJavaObject); + } + + // Returns the height of the NativeTemplateView in pixels. + public float GetHeightInPixels() + { + return this.nativeOverlayAd.Call("getHeightInPixels"); + } + + // Returns the width of the NativeTemplateView in pixels. + public float GetWidthInPixels() + { + return this.nativeOverlayAd.Call("getWidthInPixels"); + } + +#endregion + +#region Callbacks from UnityNativeTemplateAdCallbackNextgen. + void onNativeAdLoaded() + { + if (this.OnAdLoaded != null) + { + this.OnAdLoaded(this, EventArgs.Empty); + } + } + + void onNativeAdFailedToLoad(AndroidJavaObject error) + { + if (this.OnAdFailedToLoad != null) + { + LoadAdErrorClientEventArgs args = new LoadAdErrorClientEventArgs() + { + LoadAdErrorClient = new NextGenLoadAdErrorClient(error) + }; + this.OnAdFailedToLoad(this, args); + } + } + + void onAdImpression() + { + if (this.OnAdDidRecordImpression != null) + { + this.OnAdDidRecordImpression(this, EventArgs.Empty); + } + } + + void onAdClicked() + { + if (this.OnAdClicked != null) + { + this.OnAdClicked(); + } + } + + void onAdShowedFullScreenContent() + { + if (this.OnAdDidPresentFullScreenContent != null) + { + this.OnAdDidPresentFullScreenContent(this, EventArgs.Empty); + } + } + + void onAdDismissedFullScreenContent() + { + if (this.OnAdDidDismissFullScreenContent != null) + { + this.OnAdDidDismissFullScreenContent(this, EventArgs.Empty); + } + } + + void onAdFailedToShowFullScreenContent(AndroidJavaObject error) + { + // No-op + } + + void onPaidEvent(int precision, long valueInMicros, string currencyCode) + { + if (this.OnPaidEvent != null) + { + AdValue adValue = new AdValue() + { + Precision = (AdValue.PrecisionType)precision, + Value = valueInMicros, + CurrencyCode = currencyCode + }; + this.OnPaidEvent(adValue); + } + } +#endregion + +#region Native Template Styling Utilities + + private AndroidJavaObject GetNativeTemplateStyleJavaObject(NativeTemplateStyle tmplStyle) + { + AndroidJavaClass nativeTemplateTypeClass = + new AndroidJavaClass(Utils.UnityNativeTemplateTypeClassName); + AndroidJavaObject nativeTemplateType = null; + if (tmplStyle.TemplateId == "small") + { + nativeTemplateType = + nativeTemplateTypeClass.CallStatic("fromIntValue", 0); + } + else + { + nativeTemplateType = + nativeTemplateTypeClass.CallStatic("fromIntValue", 1); + } + + AndroidJavaObject mainBgColor = null; + if (!tmplStyle.MainBackgroundColor.Equals(Color.clear)) + { + AndroidJavaClass colorClass = new AndroidJavaClass(Utils.ColorClassName); + int color = colorClass.CallStatic( + "argb", (int)(255 * tmplStyle.MainBackgroundColor.a), + (int)(255 * tmplStyle.MainBackgroundColor.r), + (int)(255 * tmplStyle.MainBackgroundColor.g), + (int)(255 * tmplStyle.MainBackgroundColor.b)); + mainBgColor = new AndroidJavaObject(Utils.ColorDrawableClassName, color); + } + + AndroidJavaObject primaryTextStyle = null; + if (tmplStyle.PrimaryText != null) + { + primaryTextStyle = GetNativeTemplateTextStyleJavaObject(tmplStyle.PrimaryText); + } + + AndroidJavaObject secondaryTextStyle = null; + if (tmplStyle.SecondaryText != null) + { + secondaryTextStyle = GetNativeTemplateTextStyleJavaObject(tmplStyle.SecondaryText); + } + + AndroidJavaObject tertiaryTextStyle = null; + if (tmplStyle.TertiaryText != null) + { + tertiaryTextStyle = GetNativeTemplateTextStyleJavaObject(tmplStyle.TertiaryText); + } + + AndroidJavaObject c2aTextStyle = null; + if (tmplStyle.CallToActionText != null) + { + c2aTextStyle = GetNativeTemplateTextStyleJavaObject(tmplStyle.CallToActionText); + } + + AndroidJavaObject nativeAdTemplateStyle = new AndroidJavaObject( + Utils.UnityNativeTemplateStyleClassName, nativeTemplateType, mainBgColor, + c2aTextStyle, primaryTextStyle, secondaryTextStyle, tertiaryTextStyle); + + return nativeAdTemplateStyle; + } + + private AndroidJavaObject GetNativeTemplateTextStyleJavaObject(NativeTemplateTextStyle text) + { + AndroidJavaClass colorClass = new AndroidJavaClass(Utils.ColorClassName); + AndroidJavaObject textColorDrawable = null; + AndroidJavaObject bgColorDrawable = null; + + if (!text.TextColor.Equals(Color.clear)) + { + int color = colorClass.CallStatic( + "argb", (int)(255 * text.TextColor.a), (int)(255 * text.TextColor.r), + (int)(255 * text.TextColor.g), (int)(255 * text.TextColor.b)); + textColorDrawable = new AndroidJavaObject(Utils.ColorDrawableClassName, color); + } + + if (!text.BackgroundColor.Equals(Color.clear)) + { + int bgColor = colorClass.CallStatic( + "argb", (int)(255 * text.BackgroundColor.a), (int)(255 * text.BackgroundColor.r), + (int)(255 * text.BackgroundColor.g), (int)(255 * text.BackgroundColor.b)); + bgColorDrawable = new AndroidJavaObject(Utils.ColorDrawableClassName, bgColor); + } + + AndroidJavaClass fontStyleClass = + new AndroidJavaClass(Utils.UnityNativeTemplateFontStyleClassName); + AndroidJavaObject fontStyle = fontStyleClass.CallStatic( + "fromIntValue", (int)text.Style); + + AndroidJavaObject fontSize = + new AndroidJavaObject(Utils.DoubleClassName, (double)text.FontSize); + + AndroidJavaObject templateTextStyle = + new AndroidJavaObject(Utils.UnityNativeTemplateTextStyleClassName, + textColorDrawable, bgColorDrawable, fontStyle, fontSize); + return templateTextStyle; + } +#endregion + } +} diff --git a/source/plugin/Assets/GoogleMobileAds/Platforms/Android/NextGenUtils.cs b/source/plugin/Assets/GoogleMobileAds/Platforms/Android/NextGenUtils.cs index 483748e34..86c37d7ae 100644 --- a/source/plugin/Assets/GoogleMobileAds/Platforms/Android/NextGenUtils.cs +++ b/source/plugin/Assets/GoogleMobileAds/Platforms/Android/NextGenUtils.cs @@ -108,6 +108,11 @@ internal class NextGenUtils { "com.google.unity.ads.nextgen.UnityRewardedInterstitialAdCallback"; public const string UnityInterstitialAdPreloaderClassName = "com.google.unity.ads.nextgen.UnityInterstitialAdPreloader"; + + public const string UnityNativeTemplateAdClassName = + "com.google.unity.ads.nextgen.UnityNativeTemplateAdNextgen"; + public const string UnityNativeTemplateAdCallbackClassName = + "com.google.unity.ads.nextgen.UnityNativeTemplateAdCallbackNextgen"; #endregion /// diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nativead/UnityNativeTemplateStyle.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nativead/UnityNativeTemplateStyle.java index 879679c48..de1c1702c 100644 --- a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nativead/UnityNativeTemplateStyle.java +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nativead/UnityNativeTemplateStyle.java @@ -138,7 +138,8 @@ public int hashCode() { tertiaryTextStyle); } - private NativeTemplateStyle asNativeTemplateStyle() { + @NonNull + public NativeTemplateStyle asNativeTemplateStyle() { NativeTemplateStyle.Builder builder = new NativeTemplateStyle.Builder(); if (mainBackgroundColor != null) { builder.withMainBackgroundColor(mainBackgroundColor); diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/AdWrapper.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/AdWrapper.java index e93aac5f4..db4168fe1 100644 --- a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/AdWrapper.java +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/AdWrapper.java @@ -1,6 +1,8 @@ package com.google.unity.ads.nextgen; import com.google.android.libraries.ads.mobile.sdk.appopen.AppOpenAd; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAd; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdRequest; import com.google.android.libraries.ads.mobile.sdk.common.AdLoadCallback; import com.google.android.libraries.ads.mobile.sdk.common.AdRequest; import com.google.android.libraries.ads.mobile.sdk.interstitial.InterstitialAd; @@ -35,6 +37,22 @@ public static AdWrapper forAppOpen() { return new AdWrapper<>(AppOpenAd::load); } + /** Creates a new AdWrapper for loading BannerAds. */ + public static AdWrapper forBanner() { + return new AdWrapper( + new AdLoader() { + @Override + public void load(AdRequest adRequest, AdLoadCallback callback) { + if (adRequest instanceof BannerAdRequest) { + BannerAd.load((BannerAdRequest) adRequest, callback); + } else { + throw new IllegalArgumentException( + "AdRequest must be of type BannerAdRequest for Banner Ads"); + } + } + }); + } + /** Creates a new AdWrapper for loading InterstitialAds. */ public static AdWrapper forInterstitial() { return new AdWrapper<>(InterstitialAd::load); diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/NativeAdOptions.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/NativeAdOptions.java new file mode 100644 index 000000000..52cccd6e0 --- /dev/null +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/NativeAdOptions.java @@ -0,0 +1,49 @@ +package com.google.unity.ads.nextgen; + +import com.google.android.libraries.ads.mobile.sdk.common.VideoOptions; + +/** + * DECAGON CONTEXT NOTE FOR PORTING + * + *

In legacy Nonagon (GMS), configuring Native Ads uses an `AdLoader.Builder` taking a Decagon + * separate `NativeAdOptions` class. However, Decagon removes this Class completely! + * + *

All properties are now set directly onto `NativeAdRequest.Builder`. + * + *

To avoid breaking reflection signatures where the Unity C# layer still attempts to instantiate + * and pass a POJO for options, we create this local Adapter Shim + * (`com.google.unity.ads.nextgen.NativeAdOptions`). + * + *

We mimic the properties provided by `GoogleMobileAds/Api/Core/NativeAdOptions.cs` and pipe + * them during the `loadAd` phase inside `UnityNativeTemplateAdNextgen`. + */ +public class NativeAdOptions { + + private int mediaAspectRatio; + private int adChoicesPlacement; + private VideoOptions videoOptions; + + public int getMediaAspectRatio() { + return mediaAspectRatio; + } + + public void setMediaAspectRatio(int mediaAspectRatio) { + this.mediaAspectRatio = mediaAspectRatio; + } + + public int getAdChoicesPlacement() { + return adChoicesPlacement; + } + + public void setAdChoicesPlacement(int adChoicesPlacement) { + this.adChoicesPlacement = adChoicesPlacement; + } + + public VideoOptions getVideoOptions() { + return videoOptions; + } + + public void setVideoOptions(VideoOptions videoOptions) { + this.videoOptions = videoOptions; + } +} diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/TemplateViewNextgen.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/TemplateViewNextgen.java new file mode 100644 index 000000000..45b24ec8d --- /dev/null +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/TemplateViewNextgen.java @@ -0,0 +1,353 @@ +// Copyright 2019 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 +// +// http://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 com.google.unity.ads.nextgen; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.RatingBar; +import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.constraintlayout.widget.ConstraintLayout; +import com.google.android.ads.nativetemplates.NativeTemplateStyle; +import com.google.android.libraries.ads.mobile.sdk.nativead.MediaView; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAd; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdView; +import com.google.unity.ads.R; + +/** Forked TemplateView for Nextgen SDK (Decagon). */ +public final class TemplateViewNextgen extends FrameLayout { + + private static final String TAG = TemplateViewNextgen.class.getSimpleName(); + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + static final String MEDIUM_TEMPLATE = "medium_template"; + + private static final String SMALL_TEMPLATE = "small_template"; + + private int templateType; + private NativeTemplateStyle styles; + private NativeAd nativeAd; + private NativeAdView nativeAdView; + + private TextView primaryView; + private TextView secondaryView; + private RatingBar ratingBar; + private TextView tertiaryView; + private ImageView iconView; + private MediaView mediaView; + private Button callToActionView; + private ConstraintLayout background; + private LayoutInflater layoutInflater; + + public TemplateViewNextgen(Context context) { + super(context); + } + + public TemplateViewNextgen(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initView(context, attrs); + } + + public TemplateViewNextgen(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initView(context, attrs); + } + + public TemplateViewNextgen( + Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initView(context, attrs); + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public TemplateViewNextgen( + Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, + int defStyleRes, + LayoutInflater layoutInflater) { + super(context, attrs, defStyleAttr, defStyleRes); + this.layoutInflater = layoutInflater; + initView(context, attrs); + } + + private void initView(Context context, @Nullable AttributeSet attributeSet) { + if (attributeSet == null) { + return; + } + TypedArray typedArray = + context + .getTheme() + .obtainStyledAttributes( + /* set= */ attributeSet, + /* attrs= */ R.styleable.TemplateView, + /* defStyleAttr= */ 0, + /* defStyleRes= */ 0); + if (typedArray == null) { + return; + } + int templateTypeResource = R.styleable.TemplateView_gnt_template_type; + int templateViewResource = R.layout.nextgen_medium_template_view; + try { + templateType = typedArray.getResourceId(templateTypeResource, templateViewResource); + } catch (RuntimeException e) { + Log.e( + TAG, + String.format( + "Failed to get template type from attribute resources (templateTypeResource: %d, " + + "templateViewResource: %d).", + templateTypeResource, templateViewResource), + e); + throw e; + } finally { + typedArray.recycle(); + } + + if (layoutInflater == null) { + setLayoutInflater(context); + } + layoutInflater.inflate(templateType, this); + } + + private void setLayoutInflater(Context context) { + layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + public NativeTemplateStyle getStyles() { + return styles; + } + + public void setStyles(NativeTemplateStyle styles) { + this.styles = styles; + this.applyStyles(); + } + + public NativeAdView getNativeAdView() { + return nativeAdView; + } + + private void applyStyles() { + Drawable mainBackground = styles.getMainBackgroundColor(); + if (mainBackground != null) { + if (background != null) { + background.setBackground(mainBackground); + } + if (primaryView != null) { + primaryView.setBackground(mainBackground); + } + if (secondaryView != null) { + secondaryView.setBackground(mainBackground); + } + if (tertiaryView != null) { + tertiaryView.setBackground(mainBackground); + } + } + + Typeface primary = styles.getPrimaryTextTypeface(); + if (primary != null && primaryView != null) { + primaryView.setTypeface(primary); + } + + Typeface secondary = styles.getSecondaryTextTypeface(); + if (secondary != null && secondaryView != null) { + secondaryView.setTypeface(secondary); + } + + Typeface tertiary = styles.getTertiaryTextTypeface(); + if (tertiary != null && tertiaryView != null) { + tertiaryView.setTypeface(tertiary); + } + + Typeface ctaTypeface = styles.getCallToActionTextTypeface(); + if (ctaTypeface != null && callToActionView != null) { + callToActionView.setTypeface(ctaTypeface); + } + + if (styles.getPrimaryTextTypefaceColor() != null && primaryView != null) { + primaryView.setTextColor(styles.getPrimaryTextTypefaceColor()); + } + + if (styles.getSecondaryTextTypefaceColor() != null && secondaryView != null) { + secondaryView.setTextColor(styles.getSecondaryTextTypefaceColor()); + } + + if (styles.getTertiaryTextTypefaceColor() != null && tertiaryView != null) { + tertiaryView.setTextColor(styles.getTertiaryTextTypefaceColor()); + } + + if (styles.getCallToActionTypefaceColor() != null && callToActionView != null) { + callToActionView.setTextColor(styles.getCallToActionTypefaceColor()); + } + + float ctaTextSize = styles.getCallToActionTextSize(); + if (ctaTextSize > 0 && callToActionView != null) { + callToActionView.setTextSize(ctaTextSize); + } + + float primaryTextSize = styles.getPrimaryTextSize(); + if (primaryTextSize > 0 && primaryView != null) { + primaryView.setTextSize(primaryTextSize); + } + + float secondaryTextSize = styles.getSecondaryTextSize(); + if (secondaryTextSize > 0 && secondaryView != null) { + secondaryView.setTextSize(secondaryTextSize); + } + + float tertiaryTextSize = styles.getTertiaryTextSize(); + if (tertiaryTextSize > 0 && tertiaryView != null) { + tertiaryView.setTextSize(tertiaryTextSize); + } + + Drawable ctaBackground = styles.getCallToActionBackgroundColor(); + if (ctaBackground != null && callToActionView != null) { + callToActionView.setBackground(ctaBackground); + } + + Drawable primaryBackground = styles.getPrimaryTextBackgroundColor(); + if (primaryBackground != null && primaryView != null) { + primaryView.setBackground(primaryBackground); + } + + Drawable secondaryBackground = styles.getSecondaryTextBackgroundColor(); + if (secondaryBackground != null && secondaryView != null) { + secondaryView.setBackground(secondaryBackground); + } + + Drawable tertiaryBackground = styles.getTertiaryTextBackgroundColor(); + if (tertiaryBackground != null && tertiaryView != null) { + tertiaryView.setBackground(tertiaryBackground); + } + + invalidate(); + requestLayout(); + } + + private boolean areAllViewsInitialized() { + return nativeAdView != null + && callToActionView != null + && primaryView != null + && secondaryView != null + && iconView != null; + } + + private boolean adHasOnlyStore(NativeAd nativeAd) { + String store = nativeAd.getStore(); + String advertiser = nativeAd.getAdvertiser(); + return !TextUtils.isEmpty(store) && TextUtils.isEmpty(advertiser); + } + + public void setNativeAd(NativeAd nativeAd) { + this.nativeAd = nativeAd; + + if (!areAllViewsInitialized()) { + return; + } + + String store = nativeAd.getStore(); + String advertiser = nativeAd.getAdvertiser(); + String headline = nativeAd.getHeadline(); + String body = nativeAd.getBody(); + String cta = nativeAd.getCallToAction(); + Double starRating = nativeAd.getStarRating(); + com.google.android.libraries.ads.mobile.sdk.common.Image icon = nativeAd.getIcon(); + + nativeAdView.setCallToActionView(callToActionView); + nativeAdView.setHeadlineView(primaryView); + + secondaryView.setVisibility(VISIBLE); + + String secondaryText = ""; + if (adHasOnlyStore(nativeAd)) { + nativeAdView.setStoreView(secondaryView); + secondaryText = store; + } else if (!TextUtils.isEmpty(advertiser)) { + nativeAdView.setAdvertiserView(secondaryView); + secondaryText = advertiser; + } + + primaryView.setText(headline); + callToActionView.setText(cta); + + if (starRating != null && starRating > 0) { + secondaryView.setVisibility(GONE); + ratingBar.setVisibility(VISIBLE); + ratingBar.setRating(starRating.floatValue()); + nativeAdView.setStarRatingView(ratingBar); + } else { + secondaryView.setText(secondaryText); + secondaryView.setVisibility(VISIBLE); + ratingBar.setVisibility(GONE); + } + + if (icon != null) { + iconView.setVisibility(VISIBLE); + iconView.setImageDrawable(icon.getDrawable()); + nativeAdView.setIconView(iconView); + } else { + iconView.setVisibility(GONE); + } + + if (tertiaryView != null) { + tertiaryView.setText(body); + nativeAdView.setBodyView(tertiaryView); + } + + // Decagon registers NativeAd and MediaView together instead of setMediaView / setNativeAd. + // We register at the end of setNativeAd, after all asset views are set, per Decagon best + // practices. + nativeAdView.registerNativeAd(nativeAd, mediaView); + } + + public void destroyNativeAd() { + if (nativeAd != null) { + nativeAd.destroy(); + } + } + + public String getTemplateTypeName() { + if (templateType == R.layout.nextgen_medium_template_view) { + return MEDIUM_TEMPLATE; + } else if (templateType == R.layout.nextgen_small_template_view) { + return SMALL_TEMPLATE; + } + return ""; + } + + @Override + public void onFinishInflate() { + super.onFinishInflate(); + nativeAdView = (NativeAdView) findViewById(R.id.native_ad_view); + primaryView = (TextView) findViewById(R.id.primary); + secondaryView = (TextView) findViewById(R.id.secondary); + tertiaryView = (TextView) findViewById(R.id.body); + ratingBar = (RatingBar) findViewById(R.id.rating_bar); + ratingBar.setEnabled(false); + callToActionView = (Button) findViewById(R.id.cta); + iconView = (ImageView) findViewById(R.id.ad_icon); + mediaView = (MediaView) findViewById(R.id.media_view); + background = (ConstraintLayout) findViewById(R.id.ad_background); + } +} diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityBannerAd.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityBannerAd.java index 3d9a962ed..e3bcabf84 100644 --- a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityBannerAd.java +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityBannerAd.java @@ -26,6 +26,7 @@ import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.libraries.ads.mobile.sdk.banner.BannerAd; import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdEventCallback; import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdRequest; @@ -54,9 +55,13 @@ protected static class Insets { /** The {@link BannerAd} to display to the user. */ private BannerAd bannerAd; + /** The {@link AdWrapper} used to load ads. */ + private final AdWrapper adWrapper; + /** The {@link View} that contains the banner ad. */ private View adView; + /** A code indicating where to place the ad. */ private int positionCode; @@ -91,8 +96,14 @@ protected static class Insets { private View.OnLayoutChangeListener layoutChangeListener; public UnityBannerAd(Activity activity, UnityBannerAdCallback callback) { + this(activity, callback, AdWrapper.forBanner()); + } + + @VisibleForTesting + UnityBannerAd(Activity activity, UnityBannerAdCallback callback, AdWrapper adWrapper) { unityPlayerActivity = activity; this.callback = callback; + this.adWrapper = adWrapper; } /** @@ -121,7 +132,7 @@ public void create(final int positionX, final int positionY) { } protected void load(final BannerAdRequest adRequest) { - BannerAd.load( + adWrapper.load( adRequest, new AdLoadCallback() { @Override @@ -368,6 +379,8 @@ public void destroy() { .getDecorView() .getRootView() .removeOnLayoutChangeListener(layoutChangeListener); + + layoutChangeListener = null; } /** @@ -375,6 +388,10 @@ public void destroy() { * reposition banner ads as required. */ protected void setLayoutChangeListener() { + if (layoutChangeListener != null) { + return; + } + layoutChangeListener = new View.OnLayoutChangeListener() { @Override diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityBannerAdPreloader.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityBannerAdPreloader.java new file mode 100644 index 000000000..28cf3a1aa --- /dev/null +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityBannerAdPreloader.java @@ -0,0 +1,163 @@ +package com.google.unity.ads.nextgen; + +import android.app.Activity; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAd; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdPreloader; +import com.google.android.libraries.ads.mobile.sdk.common.AdLoadCallback; +import com.google.android.libraries.ads.mobile.sdk.common.AdRequest; +import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; +import com.google.android.libraries.ads.mobile.sdk.common.PreloadCallback; +import com.google.android.libraries.ads.mobile.sdk.common.PreloadConfiguration; +import com.google.android.libraries.ads.mobile.sdk.common.ResponseInfo; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** Unity implementation of the {@link BannerAdPreloader}. */ +public class UnityBannerAdPreloader { + + /** The {@code Activity} on which the banner ad will display. */ + private final Activity activity; + + /** An executor used to run the callbacks. */ + private final ExecutorService service; + + /** + * A {@code UnityPreloadCallback} implemented in Unity via {@code AndroidJavaProxy} to receive ad + * events. + */ + private final UnityPreloadCallback preloadCallback; + + private final BannerAdPreloaderWrapper preloaderWrapper; + + public UnityBannerAdPreloader(Activity activity, UnityPreloadCallback preloadCallback) { + this( + activity, + preloadCallback, + new BannerAdPreloaderWrapper(), + Executors.newSingleThreadExecutor()); + } + + @VisibleForTesting + public UnityBannerAdPreloader( + Activity activity, + UnityPreloadCallback preloadCallback, + BannerAdPreloaderWrapper preloaderWrapper, + ExecutorService service) { + this.activity = activity; + this.preloadCallback = preloadCallback; + this.service = service; + this.preloaderWrapper = preloaderWrapper; + } + + public boolean start(String preloadId, PreloadConfiguration preloadConfiguration) { + return preloaderWrapper.start( + preloadId, + preloadConfiguration, + new PreloadCallback() { + @Override + public void onAdPreloaded(@NonNull String preloadId, ResponseInfo responseInfo) { + service.execute( + () -> { + if (preloadCallback != null) { + preloadCallback.onAdPreloaded(preloadId, responseInfo); + } + }); + } + + @Override + public void onAdsExhausted(@NonNull String preloadId) { + service.execute( + () -> { + if (preloadCallback != null) { + preloadCallback.onAdsExhausted(preloadId); + } + }); + } + + @Override + public void onAdFailedToPreload(@NonNull String preloadId, @NonNull LoadAdError adError) { + service.execute( + () -> { + if (preloadCallback != null) { + preloadCallback.onAdFailedToPreload(preloadId, adError); + } + }); + } + }); + } + + public boolean isAdAvailable(String preloadId) { + return preloaderWrapper.isAdAvailable(preloadId); + } + + public int getNumAdsAvailable(String preloadId) { + return preloaderWrapper.getNumAdsAvailable(preloadId); + } + + @SuppressWarnings("VisibleForTests") + @Nullable + public UnityBannerAd pollAd(String preloadId, UnityBannerAdCallback callback) { + final BannerAd pooledAd = preloaderWrapper.pollAd(preloadId); + if (pooledAd == null) { + return null; + } + AdWrapper preloadedWrapper = + new AdWrapper( + new AdWrapper.AdLoader() { + @Override + public void load(AdRequest adRequest, AdLoadCallback loadCallback) { + loadCallback.onAdLoaded(pooledAd); + } + }); + return new UnityBannerAd(activity, callback, preloadedWrapper); + } + + @Nullable + public PreloadConfiguration getConfiguration(String preloadId) { + return preloaderWrapper.getConfiguration(preloadId); + } + + public Map getConfigurations() { + return preloaderWrapper.getConfigurations(); + } + + public void destroy(String preloadId) { + boolean unused = preloaderWrapper.destroy(preloadId); + } + + /** Wrapper for BannerAdPreloader static methods to facilitate testing. */ + @VisibleForTesting + public static class BannerAdPreloaderWrapper { + public boolean start(String preloadId, PreloadConfiguration config, PreloadCallback callback) { + return BannerAdPreloader.start(preloadId, config, callback); + } + + public boolean isAdAvailable(String preloadId) { + return BannerAdPreloader.isAdAvailable(preloadId); + } + + public int getNumAdsAvailable(String preloadId) { + return BannerAdPreloader.getNumAdsAvailable(preloadId); + } + + public BannerAd pollAd(String preloadId) { + return BannerAdPreloader.pollAd(preloadId); + } + + public PreloadConfiguration getConfiguration(String preloadId) { + return BannerAdPreloader.getConfiguration(preloadId); + } + + public Map getConfigurations() { + return BannerAdPreloader.getConfigurations(); + } + + public boolean destroy(String preloadId) { + return BannerAdPreloader.destroy(preloadId); + } + } +} diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityNativeTemplateAdCallbackNextgen.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityNativeTemplateAdCallbackNextgen.java new file mode 100644 index 000000000..087d1caf0 --- /dev/null +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityNativeTemplateAdCallbackNextgen.java @@ -0,0 +1,15 @@ +package com.google.unity.ads.nextgen; + +import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; + +/** + * An interface form of {@link UnityNativeTemplateAdCallbackNextgen} that can be implemented via + * {@code AndroidJavaProxy} in Unity to receive ad events synchronously. + */ +public interface UnityNativeTemplateAdCallbackNextgen + extends UnityPaidEventListener, UnityFullScreenContentCallback { + + void onNativeAdLoaded(); + + void onNativeAdFailedToLoad(LoadAdError error); +} diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityNativeTemplateAdNextgen.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityNativeTemplateAdNextgen.java new file mode 100644 index 000000000..64a023fe3 --- /dev/null +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityNativeTemplateAdNextgen.java @@ -0,0 +1,726 @@ +/* + * Copyright (C) 2024 Google, Inc. + * + * 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 + * + * http://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 com.google.unity.ads.nextgen; + +import android.app.Activity; +import android.os.Build; +import android.util.Log; +import android.view.DisplayCutout; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.FrameLayout; +import androidx.annotation.NonNull; +import com.google.android.libraries.ads.mobile.sdk.banner.AdSize; +import com.google.android.libraries.ads.mobile.sdk.common.AdRequest; +import com.google.android.libraries.ads.mobile.sdk.common.AdValue; +import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; +import com.google.android.libraries.ads.mobile.sdk.common.ResponseInfo; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAd; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdEventCallback; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdLoader; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdLoaderCallback; +import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdRequest; +import com.google.unity.ads.PluginUtils; +import com.google.unity.ads.R; +import com.google.unity.ads.nativead.UnityNativeTemplateStyle; +import com.google.unity.ads.nativead.UnityNativeTemplateType; +import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +/** Native Template ad implementation for the Google Mobile Ads Unity plugin. */ +public class UnityNativeTemplateAdNextgen { + + /** Class to hold the insets of the cutout area. */ + protected static class Insets { + int top = 0; + int bottom = 0; + int left = 0; + int right = 0; + } + + /** The {@link NativeAd}. */ + private NativeAd nativeAd; + + /** The {@code Activity} on which the native template will display. */ + private Activity activity; + + /** The template view rendering the native ad. */ + private TemplateViewNextgen templateView; + + /** + * A {@code View.OnLayoutChangeListener} used to detect orientation changes and reposition native + * overlay ads as required. + */ + private View.OnLayoutChangeListener mLayoutChangeListener; + + /** A code indicating where to place the ad. */ + private int mPositionCode; + + /** AdSize to track the size of the Template view. */ + private AdSize mAdSize; + + /** A boolean indicating whether the ad has been hidden. */ + protected boolean hidden; + + /** + * Offset for the ad in the x-axis when a custom position is used. Value will be 0 for non-custom + * positions. + */ + private int mHorizontalOffset; + + /** + * Offset for the ad in the y-axis when a custom position is used. Value will be 0 for non-custom + * positions. + */ + private int mVerticalOffset; + + /** A listener implemented in Unity via {@code AndroidJavaProxy} to receive ad events. */ + private UnityNativeTemplateAdCallbackNextgen callback; + + public UnityNativeTemplateAdNextgen( + Activity activity, UnityNativeTemplateAdCallbackNextgen callback) { + this.activity = activity; + this.callback = callback; + hidden = false; + mHorizontalOffset = 0; + mVerticalOffset = 0; + } + + /** Short-circuit constructor for preloaded ads. */ + public UnityNativeTemplateAdNextgen( + Activity activity, UnityNativeTemplateAdCallbackNextgen callback, NativeAd hydratedAd) { + this(activity, callback); + this.nativeAd = hydratedAd; + wireAdEvents(this.nativeAd); + } + + /** + * Loads a native ad using the provided NativeAdOptions and AdRequest. + * + * @param adUnitId Your ad unit ID. + * @param options The NativeAdOptions used to customize the native ad request. + * @param request The AdRequest used to fetch the native ad. + */ + public void loadAd( + final String adUnitId, final NativeAdOptions options, final AdRequest request) { + activity.runOnUiThread( + new Runnable() { + @Override + public void run() { + NativeAdRequest.Builder builder = + new NativeAdRequest.Builder(adUnitId, Arrays.asList(NativeAd.NativeAdType.NATIVE)); + + if (options != null) { + // Convert MediaAspectRatio + NativeAd.NativeMediaAspectRatio mediaEnum = NativeAd.NativeMediaAspectRatio.UNKNOWN; + switch (options.getMediaAspectRatio()) { + case 1: + mediaEnum = NativeAd.NativeMediaAspectRatio.ANY; + break; + case 2: + mediaEnum = NativeAd.NativeMediaAspectRatio.LANDSCAPE; + break; + case 3: + mediaEnum = NativeAd.NativeMediaAspectRatio.PORTRAIT; + break; + case 4: + mediaEnum = NativeAd.NativeMediaAspectRatio.SQUARE; + break; + default: + mediaEnum = NativeAd.NativeMediaAspectRatio.UNKNOWN; + break; + } + builder.setMediaAspectRatio(mediaEnum); + + // Convert AdChoicesPlacement + com.google.android.libraries.ads.mobile.sdk.common.AdChoicesPlacement placementEnum = + com.google.android.libraries.ads.mobile.sdk.common.AdChoicesPlacement.TOP_RIGHT; + switch (options.getAdChoicesPlacement()) { + case 0: + placementEnum = + com.google.android.libraries.ads.mobile.sdk.common.AdChoicesPlacement + .TOP_LEFT; + break; + case 1: + placementEnum = + com.google.android.libraries.ads.mobile.sdk.common.AdChoicesPlacement + .TOP_RIGHT; + break; + case 2: + placementEnum = + com.google.android.libraries.ads.mobile.sdk.common.AdChoicesPlacement + .BOTTOM_RIGHT; + break; + case 3: + placementEnum = + com.google.android.libraries.ads.mobile.sdk.common.AdChoicesPlacement + .BOTTOM_LEFT; + break; + } + builder.setAdChoicesPlacement(placementEnum); + + // Apply VideoOptions + if (options.getVideoOptions() != null) { + builder.setVideoOptions(options.getVideoOptions()); + } + } + + NativeAdRequest nativeAdRequest = builder.build(); + + NativeAdLoader.load( + nativeAdRequest, + new NativeAdLoaderCallback() { + @Override + public void onNativeAdLoaded(@NonNull NativeAd ad) { + nativeAd = ad; + if (callback != null) { + callback.onNativeAdLoaded(); + } + wireAdEvents(nativeAd); + } + + @Override + public void onAdFailedToLoad(@NonNull LoadAdError adError) { + if (callback != null) { + callback.onNativeAdFailedToLoad(adError); + } + } + }); + } + }); + } + + private void wireAdEvents(NativeAd ad) { + if (ad == null) { + return; + } + ad.setAdEventCallback( + new NativeAdEventCallback() { + @Override + public void onAdImpression() { + if (callback != null) { + callback.onAdImpression(); + } + } + + @Override + public void onAdClicked() { + if (callback != null) { + callback.onAdClicked(); + } + } + + @Override + public void onAdShowedFullScreenContent() { + if (callback != null) { + callback.onAdShowedFullScreenContent(); + } + } + + @Override + public void onAdDismissedFullScreenContent() { + if (callback != null) { + callback.onAdDismissedFullScreenContent(); + } + } + + @Override + public void onAdPaid(@NonNull AdValue adValue) { + new Thread( + new Runnable() { + @Override + public void run() { + if (callback != null) { + callback.onPaidEvent( + Util.getAdValuePrecisionType(adValue.getPrecisionType()), + adValue.getValueMicros(), + adValue.getCurrencyCode()); + } + } + }) + .start(); + } + }); + } + + /** Gets the placement ID of the {@link NativeAd}. */ + public long getPlacementId() { + if (nativeAd == null) { + return 0; + } + return nativeAd.getPlacementId(); + } + + /** + * Sets the placement ID of the {@link NativeAd}. + * + *

To ensure this placement ID is included in reporting, call this method before showing the + * ad. + * + * @param placementId A long integer provided by the AdMob UI for the configured placement. + */ + public void setPlacementId(long placementId) { + if (nativeAd == null) { + return; + } + nativeAd.setPlacementId(placementId); + } + + /** + * Updates the Native Template position. + * + * @param positionCode A code indicating where to place the ad. + */ + public void setPositionCode(final int positionCode) { + activity.runOnUiThread( + new Runnable() { + @Override + public void run() { + mPositionCode = positionCode; + updatePosition(); + } + }); + } + + /** + * Updates the Native Template position. + * + * @param positionX Position of template ad on the x axis. + * @param positionY Position of template ad on the y axis. + */ + public void setPosition(final int positionX, final int positionY) { + activity.runOnUiThread( + new Runnable() { + @Override + public void run() { + mPositionCode = PluginUtils.POSITION_CUSTOM; + mHorizontalOffset = positionX; + mVerticalOffset = positionY; + updatePosition(); + } + }); + } + + /** + * Renders the Native Template position at the x,y coordinates using default sizing. + * + * @param templateStyle Template Style. + * @param positionX Position of template ad on the x axis. + * @param positionY Position of template ad on the y axis. + */ + public void renderDefaultSizeAtPosition( + final UnityNativeTemplateStyle templateStyle, final int positionX, final int positionY) { + removeTemplateView(); + + mPositionCode = PluginUtils.POSITION_CUSTOM; + mHorizontalOffset = positionX; + mVerticalOffset = positionY; + mAdSize = null; + + activity.runOnUiThread( + new Runnable() { + @Override + public void run() { + int layoutId = R.layout.nextgen_medium_template_view_layout; + if (templateStyle.getTemplateType() == UnityNativeTemplateType.SMALL) { + layoutId = R.layout.nextgen_small_template_view_layout; + } + + LayoutInflater layoutInflater = LayoutInflater.from(activity); + templateView = (TemplateViewNextgen) layoutInflater.inflate(layoutId, null); + templateView.setStyles(templateStyle.asNativeTemplateStyle()); + templateView.setNativeAd(nativeAd); + + FrameLayout layout = new FrameLayout(activity); + layout.addView(templateView, getLayoutParams()); + activity.addContentView( + layout, + new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); + setLayoutChangeListener(); + } + }); + } + + /** + * Renders the Native Template position at the positionCode using default sizing. + * + * @param templateStyle Template Style. + * @param positionCode A code indicating where to place the template ad. + */ + public void renderDefaultSizeAtPositionCode( + final UnityNativeTemplateStyle templateStyle, final int positionCode) { + removeTemplateView(); + + mPositionCode = positionCode; + mAdSize = null; + + activity.runOnUiThread( + new Runnable() { + @Override + public void run() { + int layoutId = R.layout.nextgen_medium_template_view_layout; + if (templateStyle.getTemplateType() == UnityNativeTemplateType.SMALL) { + layoutId = R.layout.nextgen_small_template_view_layout; + } + + LayoutInflater layoutInflater = LayoutInflater.from(activity); + templateView = (TemplateViewNextgen) layoutInflater.inflate(layoutId, null); + templateView.setStyles(templateStyle.asNativeTemplateStyle()); + templateView.setNativeAd(nativeAd); + + FrameLayout layout = new FrameLayout(activity); + layout.addView(templateView, getLayoutParams()); + activity.addContentView( + layout, + new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); + setLayoutChangeListener(); + } + }); + } + + /** + * Renders the Native Template position with the provided adsize at the x,y coordinates. + * + * @param templateStyle Template Style. + * @param adSize AdSize of the template to be displayed. + * @param positionX Position of template ad on the x axis. + * @param positionY Position of template ad on the y axis. + */ + public void renderCustomSizeAtPosition( + final UnityNativeTemplateStyle templateStyle, + final AdSize adSize, + final int positionX, + final int positionY) { + removeTemplateView(); + + mPositionCode = PluginUtils.POSITION_CUSTOM; + mHorizontalOffset = positionX; + mVerticalOffset = positionY; + mAdSize = adSize; + + activity.runOnUiThread( + new Runnable() { + @Override + public void run() { + int layoutId = R.layout.nextgen_medium_template_view_layout; + if (templateStyle.getTemplateType() == UnityNativeTemplateType.SMALL) { + layoutId = R.layout.nextgen_small_template_view_layout; + } + + LayoutInflater layoutInflater = LayoutInflater.from(activity); + templateView = (TemplateViewNextgen) layoutInflater.inflate(layoutId, null); + templateView.setStyles(templateStyle.asNativeTemplateStyle()); + templateView.setNativeAd(nativeAd); + + FrameLayout layout = new FrameLayout(activity); + FrameLayout.LayoutParams layoutParams = getLayoutParams(); + layoutParams.height = adSize.getHeight(); + layoutParams.width = adSize.getWidth(); + layout.addView(templateView, layoutParams); + activity.addContentView( + layout, + new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); + setLayoutChangeListener(); + } + }); + } + + /** + * Renders the Native Template ad with the provided adsize at the positionCode. + * + * @param templateStyle Template Style. + * @param adSize AdSize of the template to be displayed. + * @param positionCode A code indicating where to place the template ad. + */ + public void renderCustomSizeAtPositionCode( + final UnityNativeTemplateStyle templateStyle, final AdSize adSize, final int positionCode) { + removeTemplateView(); + + mPositionCode = positionCode; + mAdSize = adSize; + + activity.runOnUiThread( + new Runnable() { + @Override + public void run() { + int layoutId = R.layout.nextgen_medium_template_view_layout; + if (templateStyle.getTemplateType() == UnityNativeTemplateType.SMALL) { + layoutId = R.layout.nextgen_small_template_view_layout; + } + + LayoutInflater layoutInflater = LayoutInflater.from(activity); + templateView = (TemplateViewNextgen) layoutInflater.inflate(layoutId, null); + templateView.setStyles(templateStyle.asNativeTemplateStyle()); + templateView.setNativeAd(nativeAd); + + FrameLayout layout = new FrameLayout(activity); + FrameLayout.LayoutParams layoutParams = getLayoutParams(); + layoutParams.height = adSize.getHeight(); + layoutParams.width = adSize.getWidth(); + layout.addView(templateView, layoutParams); + activity.addContentView( + layout, + new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); + setLayoutChangeListener(); + } + }); + } + + /** Returns the request response info. */ + public ResponseInfo getResponseInfo() { + FutureTask task = + new FutureTask<>( + new Callable() { + @Override + public ResponseInfo call() { + return nativeAd.getResponseInfo(); + } + }); + activity.runOnUiThread(task); + + ResponseInfo result = null; + try { + result = task.get(); + } catch (InterruptedException exception) { + Log.e( + PluginUtils.LOGTAG, + String.format( + "Unable to check native response info: %s", exception.getLocalizedMessage())); + } catch (ExecutionException exception) { + Log.e( + PluginUtils.LOGTAG, + String.format( + "Unable to check native response info: %s", exception.getLocalizedMessage())); + } + return result; + } + + /** Sets the Native Template View to be visible. */ + public void show() { + activity.runOnUiThread( + new Runnable() { + @Override + public void run() { + if (templateView == null) { + return; + } + hidden = false; + templateView.setVisibility(View.VISIBLE); + updatePosition(); + } + }); + } + + /** Sets the Native Template View to be gone. */ + public void hide() { + activity.runOnUiThread( + new Runnable() { + @Override + public void run() { + if (templateView == null) { + return; + } + hidden = true; + templateView.setVisibility(View.GONE); + } + }); + } + + /** + * Get Native Template View height. + * + * @return the height of the Native Template View. + */ + public float getHeightInPixels() { + if (templateView == null) { + return 0; + } + return templateView.getHeight(); + } + + /** + * Get Native Template View width. + * + * @return the width of the Native Template View. + */ + public float getWidthInPixels() { + if (templateView == null) { + return 0; + } + return templateView.getWidth(); + } + + /** Destroys the Native Template View. */ + public void destroy() { + activity.runOnUiThread( + new Runnable() { + @Override + public void run() { + if (templateView != null) { + templateView.destroyNativeAd(); + ViewParent parentView = templateView.getParent(); + if (parentView instanceof ViewGroup) { + ((ViewGroup) parentView).removeView(templateView); + } + } + } + }); + + activity + .getWindow() + .getDecorView() + .getRootView() + .removeOnLayoutChangeListener(mLayoutChangeListener); + + mLayoutChangeListener = null; + } + + /** Sets a listener to update the position of the Native Overlay in case of layout changes */ + protected void setLayoutChangeListener() { + if (mLayoutChangeListener != null) { + return; + } + + mLayoutChangeListener = + new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange( + View v, + int left, + int top, + int right, + int bottom, + int oldLeft, + int oldTop, + int oldRight, + int oldBottom) { + boolean isViewBoundsSame = + left == oldLeft && right == oldRight && bottom == oldBottom && top == oldTop; + if (isViewBoundsSame) { + return; + } + + if (!hidden) { + updatePosition(); + } + } + }; + + activity + .getWindow() + .getDecorView() + .getRootView() + .addOnLayoutChangeListener(mLayoutChangeListener); + } + + /** Update the Native Overlay View position based on current parameters. */ + private void updatePosition() { + if (templateView == null) { + return; + } + activity.runOnUiThread( + new Runnable() { + @Override + public void run() { + FrameLayout.LayoutParams layoutParams = getLayoutParams(); + if (mAdSize != null) { + layoutParams.height = mAdSize.getHeight(); + layoutParams.width = mAdSize.getWidth(); + } + templateView.setLayoutParams(layoutParams); + } + }); + } + + /** Removes the currently shown Native Overlay View. */ + private void removeTemplateView() { + if (templateView != null) { + activity.runOnUiThread( + new Runnable() { + @Override + public void run() { + ViewGroup vg = (ViewGroup) (templateView.getParent()); + vg.removeView(templateView); + } + }); + } + } + + /** + * Create layout params for the ad view with relevant positioning details. + * + * @return configured {@link FrameLayout.LayoutParams}. + */ + protected FrameLayout.LayoutParams getLayoutParams() { + final FrameLayout.LayoutParams adParams = + new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT); + adParams.gravity = PluginUtils.getLayoutGravityForPositionCode(mPositionCode); + + Insets insets = getInsets(); + int insetLeft = insets.left; + int insetTop = insets.top; + adParams.bottomMargin = insets.bottom; + adParams.rightMargin = insets.right; + if (mPositionCode == PluginUtils.POSITION_CUSTOM) { + int leftOffset = (int) PluginUtils.convertDpToPixel(mHorizontalOffset); + if (leftOffset < insetLeft) { + leftOffset = insetLeft; + } + int topOffset = (int) PluginUtils.convertDpToPixel(mVerticalOffset); + if (topOffset < insetTop) { + topOffset = insetTop; + } + adParams.leftMargin = leftOffset; + adParams.topMargin = topOffset; + } else { + adParams.leftMargin = insetLeft; + if (mPositionCode == PluginUtils.POSITION_TOP + || mPositionCode == PluginUtils.POSITION_TOP_LEFT + || mPositionCode == PluginUtils.POSITION_TOP_RIGHT) { + adParams.topMargin = insetTop; + } + } + return adParams; + } + + private Insets getInsets() { + Insets insets = new Insets(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P + || activity.getWindow() == null + || activity.getWindow().getDecorView().getRootWindowInsets() == null + || activity.getWindow().getDecorView().getRootWindowInsets().getDisplayCutout() == null) { + return insets; + } + + DisplayCutout displayCutout = + activity.getWindow().getDecorView().getRootWindowInsets().getDisplayCutout(); + insets.top = displayCutout.getSafeInsetTop(); + insets.left = displayCutout.getSafeInsetLeft(); + insets.bottom = displayCutout.getSafeInsetBottom(); + insets.right = displayCutout.getSafeInsetRight(); + return insets; + } +} diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityNativeTemplateTypeNextgen.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityNativeTemplateTypeNextgen.java new file mode 100644 index 000000000..dc9a7f048 --- /dev/null +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityNativeTemplateTypeNextgen.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 Google, Inc. + * + * 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 + * + * http://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 com.google.unity.ads.nextgen; + +import android.util.Log; +import com.google.unity.ads.PluginUtils; +import com.google.unity.ads.R; + +/** Enums to specify Type of Native Template to be used and its associated layout file */ +public enum UnityNativeTemplateTypeNextgen { + SMALL(R.layout.nextgen_small_template_view_layout), + MEDIUM(R.layout.nextgen_medium_template_view_layout); + + private final int resourceId; + + UnityNativeTemplateTypeNextgen(int resourceId) { + this.resourceId = resourceId; + } + + public int resourceId() { + return resourceId; + } + + public static UnityNativeTemplateTypeNextgen fromIntValue(int value) { + if (value >= 0 && value < UnityNativeTemplateTypeNextgen.values().length) { + return UnityNativeTemplateTypeNextgen.values()[value]; + } + Log.w(PluginUtils.LOGTAG, "Invalid template type index: " + value); + return MEDIUM; + } +} diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/res/layout/nextgen_medium_template_view.xml b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/res/layout/nextgen_medium_template_view.xml new file mode 100644 index 000000000..98f087538 --- /dev/null +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/res/layout/nextgen_medium_template_view.xml @@ -0,0 +1,283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/res/layout/nextgen_medium_template_view_layout.xml b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/res/layout/nextgen_medium_template_view_layout.xml new file mode 100644 index 000000000..587ad9682 --- /dev/null +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/res/layout/nextgen_medium_template_view_layout.xml @@ -0,0 +1,8 @@ + + diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/res/layout/nextgen_small_template_view.xml b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/res/layout/nextgen_small_template_view.xml new file mode 100644 index 000000000..37abcb8aa --- /dev/null +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/res/layout/nextgen_small_template_view.xml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/res/layout/nextgen_small_template_view_layout.xml b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/res/layout/nextgen_small_template_view_layout.xml new file mode 100644 index 000000000..1c3180e8a --- /dev/null +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/res/layout/nextgen_small_template_view_layout.xml @@ -0,0 +1,8 @@ + + diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/test/java/com/google/unity/ads/nextgen/UnityBannerAdPreloaderTest.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/test/java/com/google/unity/ads/nextgen/UnityBannerAdPreloaderTest.java new file mode 100644 index 000000000..7fb7b7913 --- /dev/null +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/test/java/com/google/unity/ads/nextgen/UnityBannerAdPreloaderTest.java @@ -0,0 +1,166 @@ +package com.google.unity.ads.nextgen; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; +import com.google.android.libraries.ads.mobile.sdk.banner.AdSize; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAd; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdRequest; +import com.google.android.libraries.ads.mobile.sdk.common.AdRequest; +import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; +import com.google.android.libraries.ads.mobile.sdk.common.PreloadCallback; +import com.google.android.libraries.ads.mobile.sdk.common.PreloadConfiguration; +import com.google.android.libraries.ads.mobile.sdk.common.ResponseInfo; +import com.google.unity.ads.nextgen.UnityBannerAdPreloader.BannerAdPreloaderWrapper; +import java.util.ArrayList; +import java.util.Map; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; + +/** Unit tests for {@link UnityBannerAdPreloader}. */ +@RunWith(RobolectricTestRunner.class) +public final class UnityBannerAdPreloaderTest { + + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + + private Activity activity; + private UnityBannerAdPreloader unityBannerAdPreloader; + private PreloadConfiguration preloadConfiguration; + + private static final String PRELOAD_ID = "preloadId"; + + @Mock private UnityPreloadCallback mockPreloadCallback; + @Mock private AdRequest mockAdRequest; + @Mock private BannerAd mockBannerAd; + @Mock private UnityBannerAdCallback mockBannerAdCallback; + @Mock private BannerAdPreloaderWrapper mockWrapper; + @Captor private ArgumentCaptor preloadCallbackCaptor; + + @Before + public void setUp() { + activity = Robolectric.buildActivity(Activity.class).create().get(); + unityBannerAdPreloader = + new UnityBannerAdPreloader( + activity, mockPreloadCallback, mockWrapper, newDirectExecutorService()); + preloadConfiguration = new PreloadConfiguration(mockAdRequest, 1); + } + + @Test + public void testStart() { + boolean unused = unityBannerAdPreloader.start(PRELOAD_ID, preloadConfiguration); + + verify(mockWrapper) + .start(eq(PRELOAD_ID), eq(preloadConfiguration), preloadCallbackCaptor.capture()); + + PreloadCallback callback = preloadCallbackCaptor.getValue(); + ResponseInfo responseInfo = + new ResponseInfo("AdapterName", "responseId", new Bundle(), null, new ArrayList<>()); + + callback.onAdPreloaded(PRELOAD_ID, responseInfo); + verify(mockPreloadCallback).onAdPreloaded(PRELOAD_ID, responseInfo); + + LoadAdError loadAdError = + new LoadAdError(LoadAdError.ErrorCode.INTERNAL_ERROR, "error message", null); + callback.onAdFailedToPreload(PRELOAD_ID, loadAdError); + verify(mockPreloadCallback).onAdFailedToPreload(PRELOAD_ID, loadAdError); + + callback.onAdsExhausted(PRELOAD_ID); + verify(mockPreloadCallback).onAdsExhausted(PRELOAD_ID); + } + + @Test + public void testStart_nullCallback() { + UnityBannerAdPreloader preloader = + new UnityBannerAdPreloader(activity, null, mockWrapper, newDirectExecutorService()); + + boolean unused = preloader.start(PRELOAD_ID, preloadConfiguration); + + verify(mockWrapper) + .start(eq(PRELOAD_ID), eq(preloadConfiguration), preloadCallbackCaptor.capture()); + + PreloadCallback callback = preloadCallbackCaptor.getValue(); + ResponseInfo responseInfo = + new ResponseInfo("AdapterName", "responseId", new Bundle(), null, new ArrayList<>()); + + callback.onAdPreloaded(PRELOAD_ID, responseInfo); + LoadAdError loadAdError = + new LoadAdError(LoadAdError.ErrorCode.INTERNAL_ERROR, "error message", null); + callback.onAdFailedToPreload(PRELOAD_ID, loadAdError); + callback.onAdsExhausted(PRELOAD_ID); + } + + @Test + public void testIsAdAvailable() { + boolean unused = unityBannerAdPreloader.isAdAvailable(PRELOAD_ID); + verify(mockWrapper).isAdAvailable(PRELOAD_ID); + } + + @Test + public void testGetNumAdsAvailable() { + int unused = unityBannerAdPreloader.getNumAdsAvailable(PRELOAD_ID); + verify(mockWrapper).getNumAdsAvailable(PRELOAD_ID); + } + + @Test + public void testPollAd_adAvailable() { + when(mockWrapper.pollAd(PRELOAD_ID)).thenReturn(mockBannerAd); + + UnityBannerAd unityBannerAd = unityBannerAdPreloader.pollAd(PRELOAD_ID, mockBannerAdCallback); + + assertThat(unityBannerAd).isNotNull(); + when(mockBannerAd.getView(activity)).thenReturn(new View(activity)); + + BannerAdRequest adRequest = new BannerAdRequest.Builder("adUnit", AdSize.BANNER).build(); + unityBannerAd.load(adRequest); + verify(mockBannerAdCallback, timeout(1000)).onAdLoaded(); + } + + @Test + public void testPollAd_adNotAvailable() { + when(mockWrapper.pollAd(PRELOAD_ID)).thenReturn(null); + + UnityBannerAd unityBannerAd = unityBannerAdPreloader.pollAd(PRELOAD_ID, mockBannerAdCallback); + + assertThat(unityBannerAd).isNull(); + } + + @Test + public void testGetConfiguration() { + PreloadConfiguration unused = unityBannerAdPreloader.getConfiguration(PRELOAD_ID); + verify(mockWrapper).getConfiguration(PRELOAD_ID); + } + + @Test + public void testGetConfigurations() { + Map unused = unityBannerAdPreloader.getConfigurations(); + verify(mockWrapper).getConfigurations(); + } + + @Test + public void testDestroy() { + unityBannerAdPreloader.destroy(PRELOAD_ID); + verify(mockWrapper).destroy(PRELOAD_ID); + } + + @Test + public void testPublicConstructor() { + UnityBannerAdPreloader preloader = new UnityBannerAdPreloader(activity, mockPreloadCallback); + assertThat(preloader).isNotNull(); + } +} diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/test/java/com/google/unity/ads/nextgen/UnityBannerAdTest.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/test/java/com/google/unity/ads/nextgen/UnityBannerAdTest.java new file mode 100644 index 000000000..35f7de965 --- /dev/null +++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/test/java/com/google/unity/ads/nextgen/UnityBannerAdTest.java @@ -0,0 +1,469 @@ +package com.google.unity.ads.nextgen; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; + +import android.app.Activity; +import android.os.Bundle; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; +import com.google.android.libraries.ads.mobile.sdk.banner.AdSize; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAd; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdEventCallback; +import com.google.android.libraries.ads.mobile.sdk.banner.BannerAdRequest; +import com.google.android.libraries.ads.mobile.sdk.common.AdLoadCallback; +import com.google.android.libraries.ads.mobile.sdk.common.AdValue; +import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError; +import com.google.android.libraries.ads.mobile.sdk.common.PrecisionType; +import com.google.android.libraries.ads.mobile.sdk.common.ResponseInfo; +import com.google.unity.ads.PluginUtils; +import java.util.ArrayList; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLooper; + +/** Unit tests for {@link UnityBannerAd}. */ +@RunWith(RobolectricTestRunner.class) +public final class UnityBannerAdTest { + + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + + private Activity activity; + @Mock private UnityBannerAdCallback mockCallback; + @Mock private BannerAd mockBannerAd; + @Mock private AdWrapper mockAdWrapper; + + @Captor private ArgumentCaptor> adLoadCallbackCaptor; + @Captor private ArgumentCaptor adEventCallbackCaptor; + + private UnityBannerAd unityBannerAd; + private BannerAdRequest bannerAdRequest; + + @Before + public void setUp() { + activity = Robolectric.buildActivity(Activity.class).create().start().resume().visible().get(); + unityBannerAd = new UnityBannerAd(activity, mockCallback, mockAdWrapper); + bannerAdRequest = new BannerAdRequest.Builder("test-ad-unit", AdSize.BANNER).build(); + } + + private View simulateAdLoadSuccess() { + return simulateAdLoadSuccess(AdSize.BANNER); + } + + private View simulateAdLoadSuccess(AdSize adSize) { + View fakeView = new View(activity); + when(mockBannerAd.getView(activity)).thenReturn(fakeView); + when(mockBannerAd.getAdSize()).thenReturn(adSize); + + unityBannerAd.load(bannerAdRequest); + verify(mockAdWrapper).load(eq(bannerAdRequest), adLoadCallbackCaptor.capture()); + adLoadCallbackCaptor.getValue().onAdLoaded(mockBannerAd); + return fakeView; + } + + @Test + public void testLoad_onAdLoaded() { + View fakeView = simulateAdLoadSuccess(); + + verify(mockCallback, timeout(1000)).onAdLoaded(); + + ShadowLooper.idleMainLooper(); + verify(mockBannerAd).getView(activity); + assertThat(fakeView.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void testLoad_onAdFailedToLoad() { + unityBannerAd.load(bannerAdRequest); + + verify(mockAdWrapper).load(eq(bannerAdRequest), adLoadCallbackCaptor.capture()); + LoadAdError loadAdError = + new LoadAdError(LoadAdError.ErrorCode.INTERNAL_ERROR, "domain", /* responseInfo= */ null); + adLoadCallbackCaptor.getValue().onAdFailedToLoad(loadAdError); + + verify(mockCallback, timeout(1000)).onAdFailedToLoad(loadAdError); + } + + @Test + public void testShow_whenAdLoaded_setsVisibilityAndPosition() { + View fakeView = simulateAdLoadSuccess(); + + unityBannerAd.show(); + ShadowLooper.idleMainLooper(); + + verify(mockBannerAd, atLeastOnce()).getView(activity); + + assertThat(fakeView.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void testHide_removesViewFromParent() { + View unusedView = simulateAdLoadSuccess(); + + ShadowLooper.idleMainLooper(); + assertThat(unityBannerAd.hidden).isFalse(); + + unityBannerAd.hide(); + ShadowLooper.idleMainLooper(); + + assertThat(unityBannerAd.hidden).isTrue(); + } + + @Test + public void testAdEvents_invokeCallback() { + View unusedView = simulateAdLoadSuccess(); + + verify(mockBannerAd).setAdEventCallback(adEventCallbackCaptor.capture()); + BannerAdEventCallback eventCallback = adEventCallbackCaptor.getValue(); + + eventCallback.onAdImpression(); + verify(mockCallback).onAdImpression(); + + eventCallback.onAdClicked(); + verify(mockCallback).onAdClicked(); + + eventCallback.onAdShowedFullScreenContent(); + verify(mockCallback).onAdOpened(); + + eventCallback.onAdDismissedFullScreenContent(); + verify(mockCallback).onAdClosed(); + + PrecisionType precisionType = PrecisionType.PRECISE; + long valueMicros = 1000000L; + String currencyCode = "USD"; + AdValue adValue = new AdValue(precisionType, valueMicros, currencyCode); + eventCallback.onAdPaid(adValue); + verify(mockCallback) + .onPaidEvent(Util.getAdValuePrecisionType(precisionType), valueMicros, currencyCode); + } + + @Test + public void testGetResponseInfo_whenAdNotLoaded_returnsNull() { + assertThat(unityBannerAd.getResponseInfo()).isNull(); + } + + @Test + public void testGetResponseInfo_whenAdLoaded_returnsResponseInfo() { + ResponseInfo responseInfo = + new ResponseInfo( + "AdapterName", + "responseId", + new Bundle(), + /* loadedAdSourceResponseInfo= */ null, + new ArrayList<>()); + when(mockBannerAd.getResponseInfo()).thenReturn(responseInfo); + + View unusedView = simulateAdLoadSuccess(); + + ResponseInfo actualResponseInfo = unityBannerAd.getResponseInfo(); + verify(mockBannerAd).getResponseInfo(); + assertThat(actualResponseInfo).isEqualTo(responseInfo); + } + + @Test + public void testGetHeightInPixels_whenAdNotLoaded_returnsNegativeOne() { + assertThat(unityBannerAd.getHeightInPixels()).isEqualTo(-1.0f); + } + + @Test + public void testGetHeightInPixels_whenAdLoaded_returnsHeight() { + AdSize customAdSize = new AdSize(300, 250); + View unusedView = simulateAdLoadSuccess(customAdSize); + + int expectedHeight = customAdSize.getHeightInPixels(activity); + assertThat(unityBannerAd.getHeightInPixels()).isEqualTo(expectedHeight); + } + + @Test + public void testGetWidthInPixels_whenAdNotLoaded_returnsNegativeOne() { + assertThat(unityBannerAd.getWidthInPixels()).isEqualTo(-1.0f); + } + + @Test + public void testGetWidthInPixels_whenAdLoaded_returnsWidth() { + AdSize customAdSize = new AdSize(300, 250); + View unusedView = simulateAdLoadSuccess(customAdSize); + + int expectedWidth = customAdSize.getWidthInPixels(activity); + assertThat(unityBannerAd.getWidthInPixels()).isEqualTo(expectedWidth); + } + + @Test + public void testIsCollapsible_whenAdNotLoaded_returnsFalse() { + assertThat(unityBannerAd.isCollapsible()).isFalse(); + } + + @Test + public void testIsCollapsible_whenAdLoaded_returnsCollapsible() { + when(mockBannerAd.isCollapsible()).thenReturn(true); + View unusedView = simulateAdLoadSuccess(); + + assertThat(unityBannerAd.isCollapsible()).isTrue(); + } + + @Test + public void testDestroy_callsBannerAdDestroy() { + View unusedView = simulateAdLoadSuccess(); + + unityBannerAd.destroy(); + ShadowLooper.idleMainLooper(); + + verify(mockBannerAd).destroy(); + } + + @Test + public void testDestroy_whenBannerAdIsNull_doesNothing() { + unityBannerAd.load(bannerAdRequest); + + unityBannerAd.destroy(); + ShadowLooper.idleMainLooper(); + + verify(mockBannerAd, never()).destroy(); + } + + @Test + public void testShow_beforeAdLoaded_doesNothing() { + unityBannerAd.show(); + ShadowLooper.idleMainLooper(); + + verify(mockBannerAd, never()).getView(activity); + } + + @Test + public void testLoad_whenHidden_doesNotShowAutomatically() { + unityBannerAd.hidden = true; + + View unusedView = simulateAdLoadSuccess(); + + ShadowLooper.idleMainLooper(); + + verify(mockBannerAd, never()).getView(activity); + } + + @Test + public void testCreate_withPositionCode_setsLayoutParamsGravity() { + unityBannerAd.create(PluginUtils.POSITION_BOTTOM); + + FrameLayout.LayoutParams params = unityBannerAd.getLayoutParams(); + + assertThat(params.gravity).isEqualTo(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); + } + + @Test + public void testCreate_withPositionTop_setsLayoutParamsGravity() { + unityBannerAd.create(PluginUtils.POSITION_TOP); + + FrameLayout.LayoutParams params = unityBannerAd.getLayoutParams(); + + assertThat(params.gravity).isEqualTo(Gravity.TOP | Gravity.CENTER_HORIZONTAL); + } + + @Test + public void testCreate_withCustomOffset_setsCustomPositionAndOffsets() { + int customX = 100; + int customY = 200; + unityBannerAd.create(customX, customY); + + FrameLayout.LayoutParams params = unityBannerAd.getLayoutParams(); + + assertThat(params.gravity).isEqualTo(Gravity.TOP | Gravity.LEFT); + float density = activity.getResources().getDisplayMetrics().density; + assertThat(params.leftMargin).isEqualTo((int) (customX * density)); + assertThat(params.topMargin).isEqualTo((int) (customY * density)); + } + + @Test + public void testSetPosition_withPositionCode_updatesLayoutParams() { + unityBannerAd.create(PluginUtils.POSITION_TOP); + + unityBannerAd.setPosition(PluginUtils.POSITION_BOTTOM); + ShadowLooper.idleMainLooper(); + + FrameLayout.LayoutParams params = unityBannerAd.getLayoutParams(); + assertThat(params.gravity).isEqualTo(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL); + } + + @Test + public void testSetPosition_withCustomOffset_updatesLayoutParams() { + unityBannerAd.create(PluginUtils.POSITION_TOP); + + int customX = 50; + int customY = 60; + unityBannerAd.setPosition(customX, customY); + ShadowLooper.idleMainLooper(); + + FrameLayout.LayoutParams params = unityBannerAd.getLayoutParams(); + assertThat(params.gravity).isEqualTo(Gravity.TOP | Gravity.LEFT); + + float density = activity.getResources().getDisplayMetrics().density; + assertThat(params.leftMargin).isEqualTo((int) (customX * density)); + assertThat(params.topMargin).isEqualTo((int) (customY * density)); + } + + @Test + public void testLayoutChangeListener_onLayoutChange_updatesPositionIfBoundsChanged() { + View fakeView = simulateAdLoadSuccess(); + unityBannerAd.show(); + ShadowLooper.idleMainLooper(); + + FrameLayout.LayoutParams initialParams = (FrameLayout.LayoutParams) fakeView.getLayoutParams(); + assertThat(initialParams).isNotNull(); + int initialGravity = initialParams.gravity; + + unityBannerAd.setPosition(PluginUtils.POSITION_BOTTOM); + + View decorView = activity.getWindow().getDecorView(); + decorView.layout(0, 0, 500, 500); + ShadowLooper.idleMainLooper(); + + FrameLayout.LayoutParams updatedParams = (FrameLayout.LayoutParams) fakeView.getLayoutParams(); + assertThat(updatedParams).isNotNull(); + assertThat(updatedParams.gravity).isNotEqualTo(initialGravity); + } + + @Test + @Config(sdk = 27) + public void testGetSafeInsets_sdkLessThanP_returnsEmptyInsets() { + View fakeView = new View(activity); + when(mockBannerAd.getView(activity)).thenReturn(fakeView); + unityBannerAd.load(bannerAdRequest); + verify(mockAdWrapper).load(eq(bannerAdRequest), adLoadCallbackCaptor.capture()); + adLoadCallbackCaptor.getValue().onAdLoaded(mockBannerAd); + + unityBannerAd.create(PluginUtils.POSITION_TOP); + ShadowLooper.idleMainLooper(); + + FrameLayout.LayoutParams params = unityBannerAd.getLayoutParams(); + assertThat(params.leftMargin).isEqualTo(0); + assertThat(params.topMargin).isEqualTo(0); + assertThat(params.rightMargin).isEqualTo(0); + assertThat(params.bottomMargin).isEqualTo(0); + + // Verify that setLayoutParams was called on the fakeView. + assertThat(fakeView.getLayoutParams()).isInstanceOf(FrameLayout.LayoutParams.class); + FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) fakeView.getLayoutParams(); + assertThat(layoutParams.leftMargin).isEqualTo(0); + assertThat(layoutParams.topMargin).isEqualTo(0); + assertThat(layoutParams.rightMargin).isEqualTo(0); + assertThat(layoutParams.bottomMargin).isEqualTo(0); + } + + @Test + public void testGetHeightInPixels_whenTaskThrowsException_returnsNegativeOne() { + View fakeView = new View(activity); + when(mockBannerAd.getView(activity)).thenReturn(fakeView); + + when(mockBannerAd.getAdSize()).thenThrow(new RuntimeException("Test Exception")); + + unityBannerAd.load(bannerAdRequest); + verify(mockAdWrapper).load(eq(bannerAdRequest), adLoadCallbackCaptor.capture()); + adLoadCallbackCaptor.getValue().onAdLoaded(mockBannerAd); + + assertThat(unityBannerAd.getHeightInPixels()).isEqualTo(-1.0f); + } + + @Test + public void testGetWidthInPixels_whenTaskThrowsException_returnsNegativeOne() { + View fakeView = new View(activity); + when(mockBannerAd.getView(activity)).thenReturn(fakeView); + + when(mockBannerAd.getAdSize()).thenThrow(new RuntimeException("Test Exception")); + + unityBannerAd.load(bannerAdRequest); + verify(mockAdWrapper).load(eq(bannerAdRequest), adLoadCallbackCaptor.capture()); + adLoadCallbackCaptor.getValue().onAdLoaded(mockBannerAd); + + assertThat(unityBannerAd.getWidthInPixels()).isEqualTo(-1.0f); + } + + @Test + public void testDestroy_whenHiddenAndNotShown_doesNotThrowAndCallsDestroy() { + unityBannerAd.hidden = true; + + View unusedView = simulateAdLoadSuccess(); + + ShadowLooper.idleMainLooper(); + unityBannerAd.destroy(); + ShadowLooper.idleMainLooper(); + + verify(mockBannerAd).destroy(); + } + + @Test + @Config(sdk = 28) + public void testGetSafeInsets_whenWindowIsNull_returnsZeroInsets() { + shadowOf(activity).setWindow(null); + UnityBannerAd ad = new UnityBannerAd(activity, mockCallback, mockAdWrapper); + ad.create(PluginUtils.POSITION_TOP); + + FrameLayout.LayoutParams params = ad.getLayoutParams(); + + assertThat(params.leftMargin).isEqualTo(0); + assertThat(params.topMargin).isEqualTo(0); + assertThat(params.rightMargin).isEqualTo(0); + assertThat(params.bottomMargin).isEqualTo(0); + } + + @Test + public void testPublicConstructor_createsInstanceWithoutCrash() { + UnityBannerAd ad = new UnityBannerAd(activity, mockCallback); + assertThat(ad).isNotNull(); + } + + @Test + public void testLoad_multipleTimes_onlyOneLayoutChangeListener() throws Exception { + java.lang.reflect.Field field = UnityBannerAd.class.getDeclaredField("layoutChangeListener"); + field.setAccessible(true); + + View.OnLayoutChangeListener listenerBefore = + (View.OnLayoutChangeListener) field.get(unityBannerAd); + assertThat(listenerBefore).isNull(); + + unityBannerAd.load(bannerAdRequest); + + View.OnLayoutChangeListener firstRegistration = + (View.OnLayoutChangeListener) field.get(unityBannerAd); + assertThat(firstRegistration).isNotNull(); + + unityBannerAd.load(bannerAdRequest); + + View.OnLayoutChangeListener secondRegistration = + (View.OnLayoutChangeListener) field.get(unityBannerAd); + + assertThat(firstRegistration).isSameInstanceAs(secondRegistration); + } + + @Test + public void testDestroy_nullifiesLayoutChangeListener() throws Exception { + + unityBannerAd.load(bannerAdRequest); + + java.lang.reflect.Field field = UnityBannerAd.class.getDeclaredField("layoutChangeListener"); + field.setAccessible(true); + View.OnLayoutChangeListener listener = (View.OnLayoutChangeListener) field.get(unityBannerAd); + assertThat(listener).isNotNull(); + + unityBannerAd.destroy(); + + View.OnLayoutChangeListener listenerAfter = + (View.OnLayoutChangeListener) field.get(unityBannerAd); + assertThat(listenerAfter).isNull(); + } +}