diff --git a/ChangeLog.txt b/ChangeLog.txt index 209abbd01..918fb38d5 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -1,5 +1,11 @@ Google Mobile Ads Unity Plugin Change Log +************** +Version Next +************** + +- Added Picture-in-Picture ad support. + ************** Version 11.4.0 ************** diff --git a/source/plugin/Assets/GoogleMobileAds/Api/Core/PictureInPictureAdPosition.cs b/source/plugin/Assets/GoogleMobileAds/Api/Core/PictureInPictureAdPosition.cs new file mode 100644 index 000000000..9f9b5fb16 --- /dev/null +++ b/source/plugin/Assets/GoogleMobileAds/Api/Core/PictureInPictureAdPosition.cs @@ -0,0 +1,25 @@ +// Copyright 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. + +namespace GoogleMobileAds.Api +{ + public enum PictureInPictureAdPosition + { + Default = 0, + BottomRight = 1, + BottomLeft = 2, + TopLeft = 3, + TopRight = 4 + } +} diff --git a/source/plugin/Assets/GoogleMobileAds/Api/PictureInPictureAd.cs b/source/plugin/Assets/GoogleMobileAds/Api/PictureInPictureAd.cs new file mode 100644 index 000000000..d12ea96fb --- /dev/null +++ b/source/plugin/Assets/GoogleMobileAds/Api/PictureInPictureAd.cs @@ -0,0 +1,241 @@ +// Copyright 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.Common; + +namespace GoogleMobileAds.Api +{ + /// + /// A Picture-in-Picture (PiP) ad that displays in a floating window across screens. + /// + public class PictureInPictureAd + { + /// + /// Raised when the ad is estimated to have earned money. + /// + public event Action OnAdPaid; + + /// + /// Raised when an ad is clicked. + /// + public event Action OnAdClicked; + + /// + /// Raised when an impression is recorded for an ad. + /// + public event Action OnAdImpressionRecorded; + + /// + /// Raised when the PiP ad is displayed on the screen. + /// + public event Action OnAdShown; + + /// + /// Raised when the PiP ad is hidden from the screen. + /// + public event Action OnAdHidden; + + /// + /// Raised when an ad opened full-screen content. + /// + public event Action OnAdFullScreenContentOpened; + + /// + /// Raised when the ad closed full-screen content. + /// + public event Action OnAdFullScreenContentClosed; + + protected internal IPictureInPictureAdClient _client; + + protected internal PictureInPictureAd() {} + + internal PictureInPictureAd(IPictureInPictureAdClient client) + { + _client = client; + RegisterAdEvents(); + } + + /// + /// Loads a Picture-in-Picture ad. + /// + public static void Load(string adUnitId, + AdRequest request, + Action adLoadCallback) + { + if (adLoadCallback == null) + { + Debug.LogError("adLoadCallback is null. No ad was loaded."); + return; + } + + var client = MobileAds.GetClientFactory().BuildPictureInPictureAdClient(); + client.CreatePictureInPictureAd(); + client.OnAdLoaded += (sender, args) => + { + var pipAd = new PictureInPictureAd(client); + MobileAds.RaiseAction(() => + { + adLoadCallback(pipAd, null); + }); + }; + client.OnAdFailedToLoad += (sender, error) => + { + var loadAdError = (error != null && error.LoadAdErrorClient != null) + ? new LoadAdError(error.LoadAdErrorClient) + : null; + MobileAds.RaiseAction(() => + { + adLoadCallback(null, loadAdError); + }); + }; + client.LoadAd(adUnitId, request); + } + + /// + /// Shows the Picture-in-Picture ad on the screen at the specified corner position. + /// + public void Show(PictureInPictureAdPosition position = PictureInPictureAdPosition.Default) + { + if (_client != null) + { + _client.Show(position); + } + } + + /// + /// Hides the Picture-in-Picture ad from the screen. + /// + public void Hide() + { + if (_client != null) + { + _client.Hide(); + } + } + + /// + /// Destroys the Picture-in-Picture ad and cleans up native resources. + /// + public void Destroy() + { + if (_client != null) + { + _client.Destroy(); + } + } + + /// + /// Returns the ResponseInfo for the loaded ad, or null if unavailable. + /// + public ResponseInfo GetResponseInfo() + { + return _client != null ? new ResponseInfo(_client.GetResponseInfoClient()) : null; + } + + /// + /// Returns the current or last known position of the Picture-in-Picture ad. + /// + public PictureInPictureAdPosition GetAdPosition() + { + return _client != null ? _client.GetAdPosition() : PictureInPictureAdPosition.Default; + } + + private void RegisterAdEvents() + { + if (_client == null) + { + return; + } + + _client.OnAdShown += (sender, args) => + { + MobileAds.RaiseAction(() => + { + if (OnAdShown != null) + { + OnAdShown(); + } + }); + }; + + _client.OnAdHidden += (sender, args) => + { + MobileAds.RaiseAction(() => + { + if (OnAdHidden != null) + { + OnAdHidden(); + } + }); + }; + + _client.OnAdClicked += () => + { + MobileAds.RaiseAction(() => + { + if (OnAdClicked != null) + { + OnAdClicked(); + } + }); + }; + + _client.OnAdDidRecordImpression += (sender, args) => + { + MobileAds.RaiseAction(() => + { + if (OnAdImpressionRecorded != null) + { + OnAdImpressionRecorded(); + } + }); + }; + + _client.OnAdDidPresentFullScreenContent += (sender, args) => + { + MobileAds.RaiseAction(() => + { + if (OnAdFullScreenContentOpened != null) + { + OnAdFullScreenContentOpened(); + } + }); + }; + + _client.OnAdDidDismissFullScreenContent += (sender, args) => + { + MobileAds.RaiseAction(() => + { + if (OnAdFullScreenContentClosed != null) + { + OnAdFullScreenContentClosed(); + } + }); + }; + + _client.OnPaidEvent += (adValue) => + { + MobileAds.RaiseAction(() => + { + if (OnAdPaid != null) + { + OnAdPaid(adValue); + } + }); + }; + } + } +} diff --git a/source/plugin/Assets/GoogleMobileAds/Common/IClientFactory.cs b/source/plugin/Assets/GoogleMobileAds/Common/IClientFactory.cs index 37c2e0f7d..df2a08b86 100644 --- a/source/plugin/Assets/GoogleMobileAds/Common/IClientFactory.cs +++ b/source/plugin/Assets/GoogleMobileAds/Common/IClientFactory.cs @@ -38,6 +38,8 @@ public interface IClientFactory INativeOverlayAdClient BuildNativeOverlayAdClient(); + IPictureInPictureAdClient BuildPictureInPictureAdClient(); + IApplicationPreferencesClient ApplicationPreferencesInstance(); IMobileAdsClient MobileAdsInstance(); diff --git a/source/plugin/Assets/GoogleMobileAds/Common/IPictureInPictureAdClient.cs b/source/plugin/Assets/GoogleMobileAds/Common/IPictureInPictureAdClient.cs new file mode 100644 index 000000000..b6a8fdb22 --- /dev/null +++ b/source/plugin/Assets/GoogleMobileAds/Common/IPictureInPictureAdClient.cs @@ -0,0 +1,62 @@ +// Copyright 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 GoogleMobileAds.Api; + +namespace GoogleMobileAds.Common +{ + public interface IPictureInPictureAdClient + { + // Ad event fired when the picture in picture ad has loaded. + event EventHandler OnAdLoaded; + // Ad event fired when the picture in picture ad has failed to load. + event EventHandler OnAdFailedToLoad; + // Ad event fired when the picture in picture ad is displayed on screen. + event EventHandler OnAdShown; + // Ad event fired when the picture in picture ad is hidden from screen. + event EventHandler OnAdHidden; + // Ad event fired when an ad impression has been recorded. + event EventHandler OnAdDidRecordImpression; + // Ad event fired when an ad has been clicked. + event Action OnAdClicked; + // Ad event fired when the ad opens an overlay covering the screen. + event EventHandler OnAdDidPresentFullScreenContent; + // Ad event fired when the ad overlay is dismissed. + event EventHandler OnAdDidDismissFullScreenContent; + // Ad event fired when the picture in picture ad is estimated to have earned money. + event Action OnPaidEvent; + + // Creates a picture in picture ad wrapper. + void CreatePictureInPictureAd(); + + // Loads a picture in picture ad. + void LoadAd(string adUnitId, AdRequest request); + + // Shows the picture in picture ad on screen. + void Show(PictureInPictureAdPosition position); + + // Hides the picture in picture ad. + void Hide(); + + // Destroys the picture in picture ad. + void Destroy(); + + // Returns the current or last known position of the picture in picture ad. + PictureInPictureAdPosition GetAdPosition(); + + // Returns the response info for the loaded ad. + IResponseInfoClient GetResponseInfoClient(); + } +} diff --git a/source/plugin/Assets/GoogleMobileAds/Platforms/Android/GoogleMobileAdsClientFactory.cs b/source/plugin/Assets/GoogleMobileAds/Platforms/Android/GoogleMobileAdsClientFactory.cs index b51a39967..f0d243169 100644 --- a/source/plugin/Assets/GoogleMobileAds/Platforms/Android/GoogleMobileAdsClientFactory.cs +++ b/source/plugin/Assets/GoogleMobileAds/Platforms/Android/GoogleMobileAdsClientFactory.cs @@ -146,6 +146,16 @@ public INativeOverlayAdClient BuildNativeOverlayAdClient() " on non-Android runtime"); } + public IPictureInPictureAdClient BuildPictureInPictureAdClient() + { + if (Application.platform == RuntimePlatform.Android) + { + return new GoogleMobileAds.Android.PictureInPictureAdClient(); + } + throw new InvalidOperationException(@"Called " + MethodBase.GetCurrentMethod().Name + + " on non-Android runtime"); + } + public IApplicationPreferencesClient ApplicationPreferencesInstance() { if (Application.platform == RuntimePlatform.Android) { return new GoogleMobileAds.Android.ApplicationPreferencesClient(); @@ -180,7 +190,7 @@ private bool IsNextGenEnabled() new AndroidJavaClass(GoogleMobileAds.Android.NextGenUtils.MobileAdsClassName); _nextGenEnabled = true; } - catch (AndroidJavaException) + catch (Exception) { _nextGenEnabled = false; } diff --git a/source/plugin/Assets/GoogleMobileAds/Platforms/Android/NextGenUtils.cs b/source/plugin/Assets/GoogleMobileAds/Platforms/Android/NextGenUtils.cs index 42305e04f..3f062a486 100644 --- a/source/plugin/Assets/GoogleMobileAds/Platforms/Android/NextGenUtils.cs +++ b/source/plugin/Assets/GoogleMobileAds/Platforms/Android/NextGenUtils.cs @@ -120,6 +120,11 @@ internal class NextGenUtils { public const string UnityBannerAdCallbackClassName = "com.google.unity.ads.nextgen.UnityBannerAdCallback"; + public const string UnityPictureInPictureAdClassName = + "com.google.unity.ads.nextgen.UnityPictureInPictureAd"; + public const string UnityPictureInPictureAdCallbackClassName = + "com.google.unity.ads.nextgen.UnityPictureInPictureAdCallback"; + public const string UnityInterstitialAdClassName = "com.google.unity.ads.nextgen.UnityInterstitialAd"; public const string UnityInterstitialAdCallbackClassName = diff --git a/source/plugin/Assets/GoogleMobileAds/Platforms/Android/PictureInPictureAdClient.cs b/source/plugin/Assets/GoogleMobileAds/Platforms/Android/PictureInPictureAdClient.cs new file mode 100644 index 000000000..03398fcdf --- /dev/null +++ b/source/plugin/Assets/GoogleMobileAds/Platforms/Android/PictureInPictureAdClient.cs @@ -0,0 +1,166 @@ +// Copyright 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 GoogleMobileAds.Api; +using GoogleMobileAds.Common; +using UnityEngine; + +namespace GoogleMobileAds.Android +{ + public class PictureInPictureAdClient : AndroidJavaProxy, IPictureInPictureAdClient + { + internal AndroidJavaObject androidPictureInPictureAd; + + public PictureInPictureAdClient() : base(NextGenUtils.UnityPictureInPictureAdCallbackClassName) + { + AndroidJavaClass playerClass = new AndroidJavaClass(Utils.UnityActivityClassName); + AndroidJavaObject activity = playerClass.GetStatic("currentActivity"); + this.androidPictureInPictureAd = new AndroidJavaObject( + NextGenUtils.UnityPictureInPictureAdClassName, activity, this); + } + + public event EventHandler OnAdLoaded; + public event EventHandler OnAdFailedToLoad; + public event EventHandler OnAdShown; + public event EventHandler OnAdHidden; + public event EventHandler OnAdDidRecordImpression; + public event Action OnAdClicked; + public event EventHandler OnAdDidPresentFullScreenContent; + public event EventHandler OnAdDidDismissFullScreenContent; + public event Action OnPaidEvent; + + public void CreatePictureInPictureAd() + { + // No-op for Next-Gen. + } + + public void LoadAd(string adUnitId, AdRequest request) + { + this.androidPictureInPictureAd.Call("load", adUnitId, NextGenUtils.GetAdRequestJavaObject(request, adUnitId)); + } + + public void Show(PictureInPictureAdPosition position) + { + this.androidPictureInPictureAd.Call("show", (int)position); + } + + public void Hide() + { + this.androidPictureInPictureAd.Call("hide"); + } + + public void Destroy() + { + this.androidPictureInPictureAd.Call("destroy"); + } + + public PictureInPictureAdPosition GetAdPosition() + { + int ordinal = this.androidPictureInPictureAd.Call("getAdPosition"); + return (PictureInPictureAdPosition)ordinal; + } + + public IResponseInfoClient GetResponseInfoClient() + { + return new ResponseInfoClient(ResponseInfoClientType.AdLoaded, this.androidPictureInPictureAd); + } + + #region Callback methods called from Java via AndroidJavaProxy + + public void onAdLoaded() + { + if (this.OnAdLoaded != null) + { + this.OnAdLoaded(this, EventArgs.Empty); + } + } + + public void onAdFailedToLoad(AndroidJavaObject error) + { + if (this.OnAdFailedToLoad != null) + { + LoadAdErrorClientEventArgs args = new LoadAdErrorClientEventArgs + { + LoadAdErrorClient = new NextGenLoadAdErrorClient(error) + }; + this.OnAdFailedToLoad(this, args); + } + } + + public void onAdShown() + { + if (this.OnAdShown != null) + { + this.OnAdShown(this, EventArgs.Empty); + } + } + + public void onAdHidden() + { + if (this.OnAdHidden != null) + { + this.OnAdHidden(this, EventArgs.Empty); + } + } + + public void onAdClicked() + { + if (this.OnAdClicked != null) + { + this.OnAdClicked(); + } + } + + public void onAdImpression() + { + if (this.OnAdDidRecordImpression != null) + { + this.OnAdDidRecordImpression(this, EventArgs.Empty); + } + } + + public void onAdShowedFullScreenContent() + { + if (this.OnAdDidPresentFullScreenContent != null) + { + this.OnAdDidPresentFullScreenContent(this, EventArgs.Empty); + } + } + + public void onAdDismissedFullScreenContent() + { + if (this.OnAdDidDismissFullScreenContent != null) + { + this.OnAdDidDismissFullScreenContent(this, EventArgs.Empty); + } + } + + public void onPaidEvent(int precision, long valueMicros, string currencyCode) + { + if (this.OnPaidEvent != null) + { + AdValue adValue = new AdValue + { + Precision = (AdValue.PrecisionType)precision, + Value = valueMicros, + CurrencyCode = currencyCode + }; + this.OnPaidEvent(adValue); + } + } + + #endregion + } +} diff --git a/source/plugin/Assets/GoogleMobileAds/Platforms/Unity/GoogleMobileAdsClientFactory.cs b/source/plugin/Assets/GoogleMobileAds/Platforms/Unity/GoogleMobileAdsClientFactory.cs index aab163f16..c0379d60c 100644 --- a/source/plugin/Assets/GoogleMobileAds/Platforms/Unity/GoogleMobileAdsClientFactory.cs +++ b/source/plugin/Assets/GoogleMobileAds/Platforms/Unity/GoogleMobileAdsClientFactory.cs @@ -78,6 +78,11 @@ public IMobileAdsClient MobileAdsInstance() return new GoogleMobileAds.Unity.MobileAdsClient(); } + public IPictureInPictureAdClient BuildPictureInPictureAdClient() + { + return new GoogleMobileAds.Unity.PictureInPictureAdClient(); + } + #if GMA_PREVIEW_FEATURES public IAppOpenAdPreloaderClient BuildAppOpenAdPreloaderClient() { diff --git a/source/plugin/Assets/GoogleMobileAds/Platforms/Unity/PictureInPictureAdClient.cs b/source/plugin/Assets/GoogleMobileAds/Platforms/Unity/PictureInPictureAdClient.cs new file mode 100644 index 000000000..5194aab01 --- /dev/null +++ b/source/plugin/Assets/GoogleMobileAds/Platforms/Unity/PictureInPictureAdClient.cs @@ -0,0 +1,779 @@ +// Copyright 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 System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; +using GoogleMobileAds.Api; +using GoogleMobileAds.Common; + +namespace GoogleMobileAds.Unity +{ + public class PictureInPictureAdClient : IPictureInPictureAdClient + { + public event EventHandler OnAdLoaded; + public event EventHandler OnAdFailedToLoad; + public event EventHandler OnAdShown; + public event EventHandler OnAdHidden; + public event EventHandler OnAdDidRecordImpression; + public event Action OnAdClicked; + public event EventHandler OnAdDidPresentFullScreenContent; + public event EventHandler OnAdDidDismissFullScreenContent; + public event Action OnPaidEvent; + + private GameObject _gameObject; + private RectTransform _cardRect; + private PipSimulatorComponent _simulatorComponent; + private string _adUnitId; + private PictureInPictureAdPosition _currentPosition = PictureInPictureAdPosition.Default; + private bool _isLoaded = false; + private bool _didImpress = false; + + public void CreatePictureInPictureAd() + { + // No-op for Unity Editor simulator. + } + + public void LoadAd(string adUnitId, AdRequest request) + { + _adUnitId = adUnitId; + if (string.IsNullOrEmpty(adUnitId) || request == null) + { + if (OnAdFailedToLoad != null) + { + OnAdFailedToLoad.Invoke(this, new LoadAdErrorClientEventArgs + { + LoadAdErrorClient = new LoadAdErrorClient() + }); + } + return; + } + + GameObject prefab = Resources.Load("PlaceholderAds/Banners/MEDIUM_RECTANGLE") as GameObject; + if (prefab == null) + { + prefab = Resources.Load("PlaceholderAds/Banners/BANNER") as GameObject; + } + if (prefab == null) + { + Debug.LogError("No PiP placeholder prefab found in Resources."); + if (OnAdFailedToLoad != null) + { + OnAdFailedToLoad.Invoke(this, new LoadAdErrorClientEventArgs + { + LoadAdErrorClient = new LoadAdErrorClient() + }); + } + return; + } + + _gameObject = GameObject.Instantiate(prefab); + Canvas canvas = _gameObject.GetComponent(); + if (canvas != null) + { + // Set to maximum sorting order to render on top of scene UI. + canvas.sortingOrder = 32767; + } + + // Remove existing Button component on prefab so dragging does not trigger button click + Button existingButton = _gameObject.GetComponentInChildren