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..ef3f77e89 100644
--- a/source/plugin/Assets/GoogleMobileAds/Platforms/Unity/GoogleMobileAdsClientFactory.cs
+++ b/source/plugin/Assets/GoogleMobileAds/Platforms/Unity/GoogleMobileAdsClientFactory.cs
@@ -68,6 +68,10 @@ public INativeOverlayAdClient BuildNativeOverlayAdClient()
return new GoogleMobileAds.Unity.NativeOverlayAdClient();
}
+ public IPictureInPictureAdClient BuildPictureInPictureAdClient(){
+ return null;
+ }
+
public IApplicationPreferencesClient ApplicationPreferencesInstance()
{
return new GoogleMobileAds.Unity.ApplicationPreferencesClient();
diff --git a/source/plugin/Assets/GoogleMobileAds/Platforms/iOS/GoogleMobileAdsClientFactory.cs b/source/plugin/Assets/GoogleMobileAds/Platforms/iOS/GoogleMobileAdsClientFactory.cs
index 449c4b60b..4747a1412 100644
--- a/source/plugin/Assets/GoogleMobileAds/Platforms/iOS/GoogleMobileAdsClientFactory.cs
+++ b/source/plugin/Assets/GoogleMobileAds/Platforms/iOS/GoogleMobileAdsClientFactory.cs
@@ -111,6 +111,10 @@ public INativeOverlayAdClient BuildNativeOverlayAdClient() {
" on non-iOS runtime");
}
+ public IPictureInPictureAdClient BuildPictureInPictureAdClient(){
+ return null;
+ }
+
public IApplicationPreferencesClient ApplicationPreferencesInstance() {
if (Application.platform == RuntimePlatform.IPhonePlayer)
{
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 7808afdc1..b997a8b30 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
@@ -6,6 +6,8 @@
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;
+import com.google.android.libraries.ads.mobile.sdk.pip.PictureInPictureAd;
+import com.google.android.libraries.ads.mobile.sdk.pip.PictureInPictureAdRequest;
import com.google.android.libraries.ads.mobile.sdk.rewarded.RewardedAd;
import com.google.android.libraries.ads.mobile.sdk.rewardedinterstitial.RewardedInterstitialAd;
@@ -67,4 +69,20 @@ public static AdWrapper forRewarded() {
public static AdWrapper forRewardedInterstitial() {
return new AdWrapper<>(RewardedInterstitialAd::load);
}
+
+ /** Creates a new AdWrapper for loading PictureInPictureAds. */
+ public static AdWrapper forPictureInPicture() {
+ return new AdWrapper<>(
+ new AdLoader() {
+ @Override
+ public void load(AdRequest adRequest, AdLoadCallback callback) {
+ if (adRequest instanceof PictureInPictureAdRequest) {
+ PictureInPictureAd.load((PictureInPictureAdRequest) adRequest, callback);
+ } else {
+ throw new IllegalArgumentException(
+ "AdRequest must be of type PictureInPictureAdRequest for PiP Ads");
+ }
+ }
+ });
+ }
}
diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityPictureInPictureAd.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityPictureInPictureAd.java
new file mode 100644
index 000000000..e62028c3d
--- /dev/null
+++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityPictureInPictureAd.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2026 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.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+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.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.pip.PictureInPictureAd;
+import com.google.android.libraries.ads.mobile.sdk.pip.PictureInPictureAdEventCallback;
+import com.google.android.libraries.ads.mobile.sdk.pip.PictureInPictureAdOptions;
+import com.google.android.libraries.ads.mobile.sdk.pip.PictureInPictureAdPosition;
+import com.google.android.libraries.ads.mobile.sdk.pip.PictureInPictureAdRequest;
+
+/** Native Java implementation for Picture-in-Picture Ads in the Google Mobile Ads Unity plugin. */
+public class UnityPictureInPictureAd {
+
+ private static final String LOG_TAG = "GoogleMobileAds";
+
+ private final Activity activity;
+ private final UnityPictureInPictureAdCallback callback;
+ private final AdWrapper adWrapper;
+ @Nullable private PictureInPictureAd pipAd;
+
+ public UnityPictureInPictureAd(Activity activity, UnityPictureInPictureAdCallback callback) {
+ this(activity, callback, AdWrapper.forPictureInPicture());
+ }
+
+ @VisibleForTesting
+ UnityPictureInPictureAd(
+ Activity activity,
+ UnityPictureInPictureAdCallback callback,
+ AdWrapper adWrapper) {
+ this.activity = activity;
+ this.callback = callback;
+ this.adWrapper = adWrapper;
+ }
+
+ /**
+ * Loads a Picture-in-Picture ad on the UI thread.
+ *
+ * @param adUnitId The ad unit ID to load.
+ * @param adRequest The native AdRequest object.
+ */
+ public void load(String adUnitId, AdRequest adRequest) {
+ activity.runOnUiThread(
+ () -> {
+ try {
+ PictureInPictureAdRequest pipRequest =
+ new PictureInPictureAdRequest.Builder(adUnitId).build();
+
+ adWrapper.load(
+ pipRequest,
+ new AdLoadCallback() {
+ @Override
+ public void onAdLoaded(@NonNull PictureInPictureAd ad) {
+ pipAd = ad;
+ pipAd.setAdEventCallback(
+ new PictureInPictureAdEventCallback() {
+ @Override
+ public void onAdShown() {
+ if (callback != null) {
+ callback.onAdShown();
+ }
+ }
+
+ @Override
+ public void onAdHidden() {
+ if (callback != null) {
+ callback.onAdHidden();
+ }
+ }
+
+ @Override
+ public void onAdClicked() {
+ if (callback != null) {
+ callback.onAdClicked();
+ }
+ }
+
+ @Override
+ public void onAdImpression() {
+ if (callback != null) {
+ callback.onAdImpression();
+ }
+ }
+
+ @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) {
+ if (callback != null) {
+ callback.onPaidEvent(
+ Util.getAdValuePrecisionType(adValue.getPrecisionType()),
+ adValue.getValueMicros(),
+ adValue.getCurrencyCode());
+ }
+ }
+ });
+
+ if (callback != null) {
+ callback.onAdLoaded();
+ }
+ }
+
+ @Override
+ public void onAdFailedToLoad(@NonNull LoadAdError loadAdError) {
+ if (callback != null) {
+ callback.onAdFailedToLoad(loadAdError);
+ }
+ }
+ });
+ } catch (Throwable t) {
+ Log.e(LOG_TAG, "Failed to load PictureInPictureAd: " + t, t);
+ if (callback != null) {
+ callback.onAdFailedToLoad(
+ new LoadAdError(
+ LoadAdError.ErrorCode.INTERNAL_ERROR,
+ t.getMessage() != null ? t.getMessage() : t.toString(),
+ null));
+ }
+ }
+ });
+ }
+
+ /**
+ * Shows the Picture-in-Picture ad at the given corner position.
+ *
+ * @param positionOrdinal 0=BOTTOM_RIGHT, 1=BOTTOM_LEFT, 2=TOP_LEFT, 3=TOP_RIGHT.
+ */
+ public void show(int positionOrdinal) {
+ activity.runOnUiThread(
+ () -> {
+ if (pipAd == null) {
+ Log.e(LOG_TAG, "Failed to show PictureInPictureAd: ad is null.");
+ return;
+ }
+ PictureInPictureAdPosition position;
+ switch (positionOrdinal) {
+ case 1:
+ position = PictureInPictureAdPosition.BOTTOM_RIGHT;
+ break;
+ case 2:
+ position = PictureInPictureAdPosition.BOTTOM_LEFT;
+ break;
+ case 3:
+ position = PictureInPictureAdPosition.TOP_LEFT;
+ break;
+ case 4:
+ position = PictureInPictureAdPosition.TOP_RIGHT;
+ break;
+ case 0:
+ default:
+ position = PictureInPictureAdPosition.DEFAULT;
+ break;
+ }
+ PictureInPictureAdOptions options =
+ new PictureInPictureAdOptions.Builder().setPosition(position).build();
+ pipAd.show(activity, options);
+ });
+ }
+
+ /** Hides the Picture-in-Picture ad. */
+ public void hide() {
+ activity.runOnUiThread(
+ () -> {
+ if (pipAd != null) {
+ pipAd.hide();
+ }
+ });
+ }
+
+ /** Destroys the Picture-in-Picture ad and cleans up references. */
+ public void destroy() {
+ activity.runOnUiThread(
+ () -> {
+ if (pipAd != null) {
+ pipAd.destroy();
+ pipAd = null;
+ }
+ });
+ }
+
+ /** Returns the current or last known position ordinal (0=DEFAULT, 1=BOTTOM_RIGHT, etc.). */
+ public int getAdPosition() {
+ if (pipAd == null) {
+ return 0;
+ }
+ PictureInPictureAdPosition position = pipAd.getPosition();
+ if (position == null) {
+ return 0;
+ }
+ switch (position) {
+ case DEFAULT:
+ return 0;
+ case BOTTOM_RIGHT:
+ return 1;
+ case BOTTOM_LEFT:
+ return 2;
+ case TOP_LEFT:
+ return 3;
+ case TOP_RIGHT:
+ return 4;
+ }
+ return 0;
+ }
+
+ /** Returns ResponseInfo for the loaded ad, if available. */
+ @Nullable
+ public ResponseInfo getResponseInfo() {
+ return pipAd != null ? pipAd.getResponseInfo() : null;
+ }
+}
diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityPictureInPictureAdCallback.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityPictureInPictureAdCallback.java
new file mode 100644
index 000000000..1b7816a56
--- /dev/null
+++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/main/java/com/google/unity/ads/nextgen/UnityPictureInPictureAdCallback.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2026 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 com.google.android.libraries.ads.mobile.sdk.common.LoadAdError;
+
+/**
+ * An interface form of {@link UnityPictureInPictureAdCallback} that can be implemented via {@code
+ * AndroidJavaProxy} in Unity to receive ad events synchronously.
+ */
+public interface UnityPictureInPictureAdCallback extends UnityPaidEventListener {
+
+ /** Called when the picture in picture ad is loaded. */
+ void onAdLoaded();
+
+ /** Called when the picture in picture ad fails to load. */
+ void onAdFailedToLoad(LoadAdError error);
+
+ /** Called when the picture in picture ad is displayed on the screen. */
+ void onAdShown();
+
+ /** Called when the picture in picture ad is hidden from the screen. */
+ void onAdHidden();
+
+ /** Called when the picture in picture ad records an impression. */
+ void onAdImpression();
+
+ /** Called when the picture in picture ad records a click. */
+ void onAdClicked();
+
+ /** Called when the picture in picture ad opens an overlay that covers the screen. */
+ void onAdShowedFullScreenContent();
+
+ /** Called when the picture in picture ad dismisses an overlay that it previously showed. */
+ void onAdDismissedFullScreenContent();
+}
diff --git a/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/test/java/com/google/unity/ads/nextgen/UnityPictureInPictureAdTest.java b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/test/java/com/google/unity/ads/nextgen/UnityPictureInPictureAdTest.java
new file mode 100644
index 000000000..a72c5c694
--- /dev/null
+++ b/source/plugin/Assets/Plugins/Android/GoogleMobileAdsPlugin.androidlib/src/test/java/com/google/unity/ads/nextgen/UnityPictureInPictureAdTest.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2026 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 static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.os.Bundle;
+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.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.android.libraries.ads.mobile.sdk.pip.PictureInPictureAd;
+import com.google.android.libraries.ads.mobile.sdk.pip.PictureInPictureAdEventCallback;
+import com.google.android.libraries.ads.mobile.sdk.pip.PictureInPictureAdOptions;
+import com.google.android.libraries.ads.mobile.sdk.pip.PictureInPictureAdPosition;
+import com.google.android.libraries.ads.mobile.sdk.pip.PictureInPictureAdRequest;
+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.Mockito;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+
+/** Unit tests for {@link UnityPictureInPictureAd} and {@link UnityPictureInPictureAdCallback}. */
+@RunWith(RobolectricTestRunner.class)
+public final class UnityPictureInPictureAdTest {
+
+ private static final String AD_UNIT_ID = "ca-app-pub-3940256099942544/6300978111";
+
+ @Rule public final MockitoRule mocks = MockitoJUnit.rule();
+
+ private Activity activity;
+ @Mock private UnityPictureInPictureAdCallback mockCallback;
+ @Mock private PictureInPictureAd mockPipAd;
+ @Mock private AdWrapper mockAdWrapper;
+ @Mock private AdRequest mockAdRequest;
+
+ @Captor private ArgumentCaptor adRequestCaptor;
+ @Captor private ArgumentCaptor> adLoadCallbackCaptor;
+ @Captor private ArgumentCaptor adEventCallbackCaptor;
+ @Captor private ArgumentCaptor adOptionsCaptor;
+
+ private UnityPictureInPictureAd unityPictureInPictureAd;
+
+ @Before
+ public void setUp() {
+ activity = Robolectric.buildActivity(Activity.class).create().get();
+ unityPictureInPictureAd = new UnityPictureInPictureAd(activity, mockCallback, mockAdWrapper);
+ }
+
+ @Test
+ public void testPublicConstructor_createsInstance() {
+ UnityPictureInPictureAd ad = new UnityPictureInPictureAd(activity, mockCallback);
+ assertThat(ad).isNotNull();
+ }
+
+ @Test
+ public void testLoad_onAdLoaded_invokesCallback() {
+ unityPictureInPictureAd.load(AD_UNIT_ID, mockAdRequest);
+
+ verify(mockAdWrapper).load(adRequestCaptor.capture(), adLoadCallbackCaptor.capture());
+ assertThat(adRequestCaptor.getValue().getAdUnitId()).isEqualTo(AD_UNIT_ID);
+
+ adLoadCallbackCaptor.getValue().onAdLoaded(mockPipAd);
+ verify(mockCallback).onAdLoaded();
+ verify(mockPipAd).setAdEventCallback(any());
+ }
+
+ @Test
+ public void testLoad_onAdFailedToLoad_invokesCallback() {
+ unityPictureInPictureAd.load(AD_UNIT_ID, mockAdRequest);
+
+ verify(mockAdWrapper).load(adRequestCaptor.capture(), adLoadCallbackCaptor.capture());
+ LoadAdError loadAdError =
+ new LoadAdError(LoadAdError.ErrorCode.INTERNAL_ERROR, "error message", null);
+ adLoadCallbackCaptor.getValue().onAdFailedToLoad(loadAdError);
+
+ verify(mockCallback).onAdFailedToLoad(loadAdError);
+ verify(mockCallback, never()).onAdLoaded();
+ }
+
+ @Test
+ public void testAdEventCallbacks_allEventsTriggered() {
+ // Load the ad first
+ unityPictureInPictureAd.load(AD_UNIT_ID, mockAdRequest);
+ verify(mockAdWrapper).load(any(), adLoadCallbackCaptor.capture());
+ adLoadCallbackCaptor.getValue().onAdLoaded(mockPipAd);
+
+ verify(mockPipAd).setAdEventCallback(adEventCallbackCaptor.capture());
+ PictureInPictureAdEventCallback eventCallback = adEventCallbackCaptor.getValue();
+
+ // Verify onAdShown
+ eventCallback.onAdShown();
+ verify(mockCallback).onAdShown();
+
+ // Verify onAdHidden
+ eventCallback.onAdHidden();
+ verify(mockCallback).onAdHidden();
+
+ // Verify onAdClicked
+ eventCallback.onAdClicked();
+ verify(mockCallback).onAdClicked();
+
+ // Verify onAdImpression
+ eventCallback.onAdImpression();
+ verify(mockCallback).onAdImpression();
+
+ // Verify onAdShowedFullScreenContent
+ eventCallback.onAdShowedFullScreenContent();
+ verify(mockCallback).onAdShowedFullScreenContent();
+
+ // Verify onAdDismissedFullScreenContent
+ eventCallback.onAdDismissedFullScreenContent();
+ verify(mockCallback).onAdDismissedFullScreenContent();
+
+ // Verify onAdPaid
+ PrecisionType precisionType = PrecisionType.PRECISE;
+ long valueMicros = 1500000L;
+ String currencyCode = "USD";
+ eventCallback.onAdPaid(new AdValue(precisionType, valueMicros, currencyCode));
+ verify(mockCallback)
+ .onPaidEvent(Util.getAdValuePrecisionType(precisionType), valueMicros, currencyCode);
+ }
+
+ @Test
+ public void testShow_whenAdNotLoaded_doesNotThrow() {
+ unityPictureInPictureAd.show(0);
+ verify(mockPipAd, never()).show(any(), any());
+ }
+
+ @Test
+ public void testShow_defaultPosition_passesDefaultOptions() {
+ // Load the ad
+ unityPictureInPictureAd.load(AD_UNIT_ID, mockAdRequest);
+ verify(mockAdWrapper).load(any(), adLoadCallbackCaptor.capture());
+ adLoadCallbackCaptor.getValue().onAdLoaded(mockPipAd);
+
+ // Call show(0) for Default
+ unityPictureInPictureAd.show(0);
+
+ verify(mockPipAd).show(eq(activity), adOptionsCaptor.capture());
+ assertThat(adOptionsCaptor.getValue().getPosition())
+ .isEqualTo(PictureInPictureAdPosition.DEFAULT);
+ }
+
+ @Test
+ public void testShow_explicitPositions_passesCorrectOptions() {
+ // Load the ad
+ unityPictureInPictureAd.load(AD_UNIT_ID, mockAdRequest);
+ verify(mockAdWrapper).load(any(), adLoadCallbackCaptor.capture());
+ adLoadCallbackCaptor.getValue().onAdLoaded(mockPipAd);
+
+ // Test position 1 -> BOTTOM_RIGHT
+ unityPictureInPictureAd.show(1);
+ verify(mockPipAd).show(eq(activity), adOptionsCaptor.capture());
+ assertThat(adOptionsCaptor.getValue().getPosition())
+ .isEqualTo(PictureInPictureAdPosition.BOTTOM_RIGHT);
+
+ // Test position 2 -> BOTTOM_LEFT
+ unityPictureInPictureAd.show(2);
+ verify(mockPipAd, Mockito.times(2)).show(eq(activity), adOptionsCaptor.capture());
+ assertThat(adOptionsCaptor.getValue().getPosition())
+ .isEqualTo(PictureInPictureAdPosition.BOTTOM_LEFT);
+
+ // Test position 3 -> TOP_LEFT
+ unityPictureInPictureAd.show(3);
+ verify(mockPipAd, Mockito.times(3)).show(eq(activity), adOptionsCaptor.capture());
+ assertThat(adOptionsCaptor.getValue().getPosition())
+ .isEqualTo(PictureInPictureAdPosition.TOP_LEFT);
+
+ // Test position 4 -> TOP_RIGHT
+ unityPictureInPictureAd.show(4);
+ verify(mockPipAd, Mockito.times(4)).show(eq(activity), adOptionsCaptor.capture());
+ assertThat(adOptionsCaptor.getValue().getPosition())
+ .isEqualTo(PictureInPictureAdPosition.TOP_RIGHT);
+ }
+
+ @Test
+ public void testHide_whenAdLoaded_callsPipAdHide() {
+ unityPictureInPictureAd.load(AD_UNIT_ID, mockAdRequest);
+ verify(mockAdWrapper).load(any(), adLoadCallbackCaptor.capture());
+ adLoadCallbackCaptor.getValue().onAdLoaded(mockPipAd);
+
+ unityPictureInPictureAd.hide();
+ verify(mockPipAd).hide();
+ }
+
+ @Test
+ public void testHide_whenAdNotLoaded_doesNotThrow() {
+ unityPictureInPictureAd.hide();
+ verify(mockPipAd, never()).hide();
+ }
+
+ @Test
+ public void testDestroy_whenAdLoaded_destroysAndCleansUp() {
+ unityPictureInPictureAd.load(AD_UNIT_ID, mockAdRequest);
+ verify(mockAdWrapper).load(any(), adLoadCallbackCaptor.capture());
+ adLoadCallbackCaptor.getValue().onAdLoaded(mockPipAd);
+
+ unityPictureInPictureAd.destroy();
+ verify(mockPipAd).destroy();
+
+ // Verify subsequent show or hide does not invoke pipAd
+ unityPictureInPictureAd.show(0);
+ verify(mockPipAd, never()).show(any(), any());
+ }
+
+ @Test
+ public void testGetAdPosition_whenAdNotLoaded_returnsDefaultZero() {
+ assertThat(unityPictureInPictureAd.getAdPosition()).isEqualTo(0);
+ }
+
+ @Test
+ public void testGetAdPosition_whenAdLoaded_mapsPositionsCorrectly() {
+ unityPictureInPictureAd.load(AD_UNIT_ID, mockAdRequest);
+ verify(mockAdWrapper).load(any(), adLoadCallbackCaptor.capture());
+ adLoadCallbackCaptor.getValue().onAdLoaded(mockPipAd);
+
+ // DEFAULT -> 0
+ when(mockPipAd.getPosition()).thenReturn(PictureInPictureAdPosition.DEFAULT);
+ assertThat(unityPictureInPictureAd.getAdPosition()).isEqualTo(0);
+
+ // BOTTOM_RIGHT -> 1
+ when(mockPipAd.getPosition()).thenReturn(PictureInPictureAdPosition.BOTTOM_RIGHT);
+ assertThat(unityPictureInPictureAd.getAdPosition()).isEqualTo(1);
+
+ // BOTTOM_LEFT -> 2
+ when(mockPipAd.getPosition()).thenReturn(PictureInPictureAdPosition.BOTTOM_LEFT);
+ assertThat(unityPictureInPictureAd.getAdPosition()).isEqualTo(2);
+
+ // TOP_LEFT -> 3
+ when(mockPipAd.getPosition()).thenReturn(PictureInPictureAdPosition.TOP_LEFT);
+ assertThat(unityPictureInPictureAd.getAdPosition()).isEqualTo(3);
+
+ // TOP_RIGHT -> 4
+ when(mockPipAd.getPosition()).thenReturn(PictureInPictureAdPosition.TOP_RIGHT);
+ assertThat(unityPictureInPictureAd.getAdPosition()).isEqualTo(4);
+ }
+
+ @Test
+ public void testGetResponseInfo_whenAdNotLoaded_returnsNull() {
+ assertThat(unityPictureInPictureAd.getResponseInfo()).isNull();
+ }
+
+ @Test
+ public void testGetResponseInfo_whenAdLoaded_returnsResponseInfo() {
+ ResponseInfo responseInfo =
+ new ResponseInfo("AdapterName", "responseId", new Bundle(), null, new ArrayList<>());
+ when(mockPipAd.getResponseInfo()).thenReturn(responseInfo);
+
+ unityPictureInPictureAd.load(AD_UNIT_ID, mockAdRequest);
+ verify(mockAdWrapper).load(any(), adLoadCallbackCaptor.capture());
+ adLoadCallbackCaptor.getValue().onAdLoaded(mockPipAd);
+
+ ResponseInfo actualResponseInfo = unityPictureInPictureAd.getResponseInfo();
+ verify(mockPipAd).getResponseInfo();
+ assertThat(actualResponseInfo).isEqualTo(responseInfo);
+ }
+}