From c35787a08f343c9f665be51544882a3aed8898fd Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 30 Sep 2020 11:54:01 +0100 Subject: [PATCH] Add ImaUtil for IMA extension utilities PiperOrigin-RevId: 334567234 --- .../ext/ima/AdPlaybackStateFactory.java | 56 -------- .../exoplayer2/ext/ima/ImaAdsLoader.java | 77 ++--------- .../android/exoplayer2/ext/ima/ImaUtil.java | 128 ++++++++++++++++++ .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 28 ++-- 4 files changed, 155 insertions(+), 134 deletions(-) delete mode 100644 extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackStateFactory.java create mode 100644 extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackStateFactory.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackStateFactory.java deleted file mode 100644 index a97307a419..0000000000 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackStateFactory.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * 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.android.exoplayer2.ext.ima; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.source.ads.AdPlaybackState; -import java.util.Arrays; -import java.util.List; - -/** - * Static utility class for constructing {@link AdPlaybackState} instances from IMA-specific data. - */ -/* package */ final class AdPlaybackStateFactory { - private AdPlaybackStateFactory() {} - - /** - * Construct an {@link AdPlaybackState} from the provided {@code cuePoints}. - * - * @param cuePoints The cue points of the ads in seconds. - * @return The {@link AdPlaybackState}. - */ - public static AdPlaybackState fromCuePoints(List cuePoints) { - if (cuePoints.isEmpty()) { - // If no cue points are specified, there is a preroll ad. - return new AdPlaybackState(/* adGroupTimesUs...= */ 0); - } - - int count = cuePoints.size(); - long[] adGroupTimesUs = new long[count]; - int adGroupIndex = 0; - for (int i = 0; i < count; i++) { - double cuePoint = cuePoints.get(i); - if (cuePoint == -1.0) { - adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE; - } else { - adGroupTimesUs[adGroupIndex++] = Math.round(C.MICROS_PER_SECOND * cuePoint); - } - } - // Cue points may be out of order, so sort them. - Arrays.sort(adGroupTimesUs, 0, adGroupIndex); - return new AdPlaybackState(adGroupTimesUs); - } -} diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 157fab938c..592920bfc4 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -33,7 +33,6 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; import com.google.ads.interactivemedia.v3.api.AdError; -import com.google.ads.interactivemedia.v3.api.AdError.AdErrorCode; import com.google.ads.interactivemedia.v3.api.AdErrorEvent; import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; import com.google.ads.interactivemedia.v3.api.AdEvent; @@ -134,7 +133,7 @@ public final class ImaAdsLoader private int mediaBitrate; private boolean focusSkipButtonWhenAvailable; private boolean playAdBeforeStartPosition; - private ImaFactory imaFactory; + private ImaUtil.ImaFactory imaFactory; /** * Creates a new builder for {@link ImaAdsLoader}. @@ -303,7 +302,7 @@ public final class ImaAdsLoader } @VisibleForTesting - /* package */ Builder setImaFactory(ImaFactory imaFactory) { + /* package */ Builder setImaFactory(ImaUtil.ImaFactory imaFactory) { this.imaFactory = checkNotNull(imaFactory); return this; } @@ -397,7 +396,7 @@ public final class ImaAdsLoader @Nullable private final Collection companionAdSlots; @Nullable private final AdErrorListener adErrorListener; @Nullable private final AdEventListener adEventListener; - private final ImaFactory imaFactory; + private final ImaUtil.ImaFactory imaFactory; private final ImaSdkSettings imaSdkSettings; private final Timeline.Period period; private final Handler handler; @@ -677,7 +676,7 @@ public final class ImaAdsLoader adsManager.resume(); } } else if (adsManager != null) { - adPlaybackState = AdPlaybackStateFactory.fromCuePoints(adsManager.getAdCuePoints()); + adPlaybackState = ImaUtil.getInitialAdPlaybackStateForCuePoints(adsManager.getAdCuePoints()); updateAdPlaybackState(); } else { // Ads haven't loaded yet, so request them. @@ -688,7 +687,7 @@ public final class ImaAdsLoader adDisplayContainer.registerFriendlyObstruction( imaFactory.createFriendlyObstruction( overlayInfo.view, - getFriendlyObstructionPurpose(overlayInfo.purpose), + ImaUtil.getFriendlyObstructionPurpose(overlayInfo.purpose), overlayInfo.reasonDetail)); } } @@ -1481,21 +1480,6 @@ public final class ImaAdsLoader return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]"; } - private static FriendlyObstructionPurpose getFriendlyObstructionPurpose( - @OverlayInfo.Purpose int purpose) { - switch (purpose) { - case OverlayInfo.PURPOSE_CONTROLS: - return FriendlyObstructionPurpose.VIDEO_CONTROLS; - case OverlayInfo.PURPOSE_CLOSE_AD: - return FriendlyObstructionPurpose.CLOSE_AD; - case OverlayInfo.PURPOSE_NOT_VISIBLE: - return FriendlyObstructionPurpose.NOT_VISIBLE; - case OverlayInfo.PURPOSE_OTHER: - default: - return FriendlyObstructionPurpose.OTHER; - } - } - private static DataSpec getAdsDataSpec(@Nullable Uri adTagUri) { return new DataSpec(adTagUri != null ? adTagUri : Uri.EMPTY); } @@ -1509,13 +1493,6 @@ public final class ImaAdsLoader : timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs()); } - private static boolean isAdGroupLoadError(AdError adError) { - // TODO: Find out what other errors need to be handled (if any), and whether each one relates to - // a single ad, ad group or the whole timeline. - return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH - || adError.getErrorCode() == AdErrorCode.UNKNOWN_ERROR; - } - private static Looper getImaLooper() { // IMA SDK callbacks occur on the main thread. This method can be used to check that the player // is using the same looper, to ensure all interaction with this class is on the main thread. @@ -1549,38 +1526,6 @@ public final class ImaAdsLoader } } - /** Factory for objects provided by the IMA SDK. */ - @VisibleForTesting - /* package */ interface ImaFactory { - /** Creates {@link ImaSdkSettings} for configuring the IMA SDK. */ - ImaSdkSettings createImaSdkSettings(); - /** - * Creates {@link AdsRenderingSettings} for giving the {@link AdsManager} parameters that - * control rendering of ads. - */ - AdsRenderingSettings createAdsRenderingSettings(); - /** - * Creates an {@link AdDisplayContainer} to hold the player for video ads, a container for - * non-linear ads, and slots for companion ads. - */ - AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player); - /** Creates an {@link AdDisplayContainer} to hold the player for audio ads. */ - AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player); - /** - * Creates a {@link FriendlyObstruction} to describe an obstruction considered "friendly" for - * viewability measurement purposes. - */ - FriendlyObstruction createFriendlyObstruction( - View view, - FriendlyObstructionPurpose friendlyObstructionPurpose, - @Nullable String reasonDetail); - /** Creates an {@link AdsRequest} to contain the data used to request ads. */ - AdsRequest createAdsRequest(); - /** Creates an {@link AdsLoader} for requesting ads using the specified settings. */ - AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); - } - private final class ComponentListener implements AdsLoadedListener, ContentProgressProvider, @@ -1610,7 +1555,8 @@ public final class ImaAdsLoader if (player != null) { // If a player is attached already, start playback immediately. try { - adPlaybackState = AdPlaybackStateFactory.fromCuePoints(adsManager.getAdCuePoints()); + adPlaybackState = + ImaUtil.getInitialAdPlaybackStateForCuePoints(adsManager.getAdCuePoints()); hasAdPlaybackState = true; updateAdPlaybackState(); } catch (RuntimeException e) { @@ -1680,7 +1626,7 @@ public final class ImaAdsLoader adPlaybackState = AdPlaybackState.NONE; hasAdPlaybackState = true; updateAdPlaybackState(); - } else if (isAdGroupLoadError(error)) { + } else if (ImaUtil.isAdGroupLoadError(error)) { try { handleAdGroupLoadError(error); } catch (RuntimeException e) { @@ -1795,8 +1741,11 @@ public final class ImaAdsLoader } } - /** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */ - private static final class DefaultImaFactory implements ImaFactory { + /** + * Default {@link ImaUtil.ImaFactory} for non-test usage, which delegates to {@link + * ImaSdkFactory}. + */ + private static final class DefaultImaFactory implements ImaUtil.ImaFactory { @Override public ImaSdkSettings createImaSdkSettings() { return ImaSdkFactory.getInstance().createImaSdkSettings(); diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java new file mode 100644 index 0000000000..c4b2c3dca3 --- /dev/null +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * 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.android.exoplayer2.ext.ima; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.Nullable; +import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; +import com.google.ads.interactivemedia.v3.api.AdError; +import com.google.ads.interactivemedia.v3.api.AdsLoader; +import com.google.ads.interactivemedia.v3.api.AdsManager; +import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; +import com.google.ads.interactivemedia.v3.api.AdsRequest; +import com.google.ads.interactivemedia.v3.api.FriendlyObstruction; +import com.google.ads.interactivemedia.v3.api.FriendlyObstructionPurpose; +import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; +import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.ads.AdPlaybackState; +import com.google.android.exoplayer2.source.ads.AdsLoader.OverlayInfo; +import java.util.Arrays; +import java.util.List; + +/** Utilities for working with IMA SDK and IMA extension data types. */ +/* package */ final class ImaUtil { + + /** Factory for objects provided by the IMA SDK. */ + public interface ImaFactory { + /** Creates {@link ImaSdkSettings} for configuring the IMA SDK. */ + ImaSdkSettings createImaSdkSettings(); + /** + * Creates {@link AdsRenderingSettings} for giving the {@link AdsManager} parameters that + * control rendering of ads. + */ + AdsRenderingSettings createAdsRenderingSettings(); + /** + * Creates an {@link AdDisplayContainer} to hold the player for video ads, a container for + * non-linear ads, and slots for companion ads. + */ + AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player); + /** Creates an {@link AdDisplayContainer} to hold the player for audio ads. */ + AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player); + /** + * Creates a {@link FriendlyObstruction} to describe an obstruction considered "friendly" for + * viewability measurement purposes. + */ + FriendlyObstruction createFriendlyObstruction( + View view, + FriendlyObstructionPurpose friendlyObstructionPurpose, + @Nullable String reasonDetail); + /** Creates an {@link AdsRequest} to contain the data used to request ads. */ + AdsRequest createAdsRequest(); + /** Creates an {@link AdsLoader} for requesting ads using the specified settings. */ + AdsLoader createAdsLoader( + Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); + } + + /** + * Returns the IMA {@link FriendlyObstructionPurpose} corresponding to the given {@link + * OverlayInfo#purpose}. + */ + public static FriendlyObstructionPurpose getFriendlyObstructionPurpose( + @OverlayInfo.Purpose int purpose) { + switch (purpose) { + case OverlayInfo.PURPOSE_CONTROLS: + return FriendlyObstructionPurpose.VIDEO_CONTROLS; + case OverlayInfo.PURPOSE_CLOSE_AD: + return FriendlyObstructionPurpose.CLOSE_AD; + case OverlayInfo.PURPOSE_NOT_VISIBLE: + return FriendlyObstructionPurpose.NOT_VISIBLE; + case OverlayInfo.PURPOSE_OTHER: + default: + return FriendlyObstructionPurpose.OTHER; + } + } + + /** + * Returns an initial {@link AdPlaybackState} with ad groups at the provided {@code cuePoints}. + * + * @param cuePoints The cue points of the ads in seconds. + * @return The {@link AdPlaybackState}. + */ + public static AdPlaybackState getInitialAdPlaybackStateForCuePoints(List cuePoints) { + if (cuePoints.isEmpty()) { + // If no cue points are specified, there is a preroll ad. + return new AdPlaybackState(/* adGroupTimesUs...= */ 0); + } + + int count = cuePoints.size(); + long[] adGroupTimesUs = new long[count]; + int adGroupIndex = 0; + for (int i = 0; i < count; i++) { + double cuePoint = cuePoints.get(i); + if (cuePoint == -1.0) { + adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE; + } else { + adGroupTimesUs[adGroupIndex++] = Math.round(C.MICROS_PER_SECOND * cuePoint); + } + } + // Cue points may be out of order, so sort them. + Arrays.sort(adGroupTimesUs, 0, adGroupIndex); + return new AdPlaybackState(adGroupTimesUs); + } + + /** Returns whether the ad error indicates that an entire ad group failed to load. */ + public static boolean isAdGroupLoadError(AdError adError) { + // TODO: Find out what other errors need to be handled (if any), and whether each one relates to + // a single ad, ad group or the whole timeline. + return adError.getErrorCode() == AdError.AdErrorCode.VAST_LINEAR_ASSET_MISMATCH + || adError.getErrorCode() == AdError.AdErrorCode.UNKNOWN_ERROR; + } + + private ImaUtil() {} +} diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java index c2cc384888..9861065454 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -55,7 +55,7 @@ import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.ext.ima.ImaAdsLoader.ImaFactory; +import com.google.android.exoplayer2.ext.ima.ImaUtil.ImaFactory; import com.google.android.exoplayer2.source.MaskingMediaSource.PlaceholderTimeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdsLoader; @@ -378,7 +378,7 @@ public final class ImaAdsLoaderTest { assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); } @@ -402,7 +402,7 @@ public final class ImaAdsLoaderTest { assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}}) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) @@ -424,7 +424,7 @@ public final class ImaAdsLoaderTest { verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble()); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); } @@ -448,7 +448,7 @@ public final class ImaAdsLoaderTest { .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withSkippedAdGroup(/* adGroupIndex= */ 0)); } @@ -473,7 +473,7 @@ public final class ImaAdsLoaderTest { .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withSkippedAdGroup(/* adGroupIndex= */ 0)); } @@ -500,7 +500,7 @@ public final class ImaAdsLoaderTest { verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble()); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); } @@ -531,7 +531,7 @@ public final class ImaAdsLoaderTest { .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withSkippedAdGroup(/* adGroupIndex= */ 0)); } @@ -563,7 +563,7 @@ public final class ImaAdsLoaderTest { .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withSkippedAdGroup(/* adGroupIndex= */ 0) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); } @@ -595,7 +595,7 @@ public final class ImaAdsLoaderTest { .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withSkippedAdGroup(/* adGroupIndex= */ 0)); } @@ -622,7 +622,7 @@ public final class ImaAdsLoaderTest { verify(mockAdsManager).destroy(); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withSkippedAdGroup(/* adGroupIndex= */ 0) .withSkippedAdGroup(/* adGroupIndex= */ 1)); @@ -663,7 +663,7 @@ public final class ImaAdsLoaderTest { .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withSkippedAdGroup(/* adGroupIndex= */ 0) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); } @@ -702,7 +702,7 @@ public final class ImaAdsLoaderTest { .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withSkippedAdGroup(/* adGroupIndex= */ 0)); } @@ -761,7 +761,7 @@ public final class ImaAdsLoaderTest { assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI)