diff --git a/.travis.yml b/.travis.yml index cbfaa7dd0..e68e58428 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: android +dist: trusty jdk: - oraclejdk8 env: diff --git a/PrebidMobile/API1.0/src/main/java/org/prebid/mobile/Util.java b/PrebidMobile/API1.0/src/main/java/org/prebid/mobile/Util.java index d038c7602..651e0dda0 100644 --- a/PrebidMobile/API1.0/src/main/java/org/prebid/mobile/Util.java +++ b/PrebidMobile/API1.0/src/main/java/org/prebid/mobile/Util.java @@ -16,8 +16,17 @@ package org.prebid.mobile; +import android.annotation.TargetApi; +import android.os.Build; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.Size; import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.ValueCallback; +import android.webkit.WebView; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -26,14 +35,17 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; +import java.util.List; import java.util.Random; +import java.util.regex.Matcher; +import java.util.regex.Pattern; -class Util { +public class Util { - private static final Random RANDOM = new Random(); static final String MOPUB_BANNER_VIEW_CLASS = "com.mopub.mobileads.MoPubView"; static final String MOPUB_INTERSTITIAL_CLASS = "com.mopub.mobileads.MoPubInterstitial"; static final String DFP_AD_REQUEST_CLASS = "com.google.android.gms.ads.doubleclick.PublisherAdRequest"; + private static final Random RANDOM = new Random(); private static final HashSet reservedKeys; private static final int MoPubQueryStringLimit = 4000; @@ -45,6 +57,202 @@ private Util() { } + public static void findPrebidCreativeSize(View adView, CreativeSizeCompletionHandler completionHandler) { + + List webViewList = new ArrayList<>(2); + recursivelyFindWebView(adView, webViewList); + if (webViewList.size() == 0) { + LogUtil.w("adView doesn't include WebView"); + return; + } + + findSizeInWebViewListAsync(webViewList, completionHandler); + } + + @Nullable + static void recursivelyFindWebView(View view, List webViewList) { + if (view instanceof ViewGroup) { + //ViewGroup + ViewGroup viewGroup = (ViewGroup) view; + + if (!(viewGroup instanceof WebView)) { + for (int i = 0; i < viewGroup.getChildCount(); i++) { + recursivelyFindWebView(viewGroup.getChildAt(i), webViewList); + } + } else { + //WebView + final WebView webView = (WebView) viewGroup; + webViewList.add(webView); + } + + } + } + + static void findSizeInWebViewListAsync(@Size(min = 1) final List webViewList, final CreativeSizeCompletionHandler completionHandler) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + LogUtil.d("webViewList size:" + webViewList.size()); + + iterateWebViewListAsync(webViewList, webViewList.size() - 1, new WebViewPrebidCallback() { + @Override + public void success(final WebView webView, @NonNull CreativeSize adSize) { + + completionHandler.onSize(adSize); + webView.post(new Runnable() { + @Override + public void run() { + webView.getSettings().setLoadWithOverviewMode(true); + } + }); + + } + + @Override + public void failure() { + completionHandler.onSize(null); + } + }); + + + } else { + LogUtil.w("AndroidSDK < KITKAT"); + completionHandler.onSize(null); + } + + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + static void iterateWebViewListAsync(@Size(min = 1) final List webViewList, final int index, final WebViewPrebidCallback webViewPrebidCallback) { + + final WebView webView = webViewList.get(index); + + webView.evaluateJavascript("document.body.innerHTML", new ValueCallback() { + + private void repeatOrFail() { + int nextIndex = index - 1; + + if (nextIndex >= 0) { + iterateWebViewListAsync(webViewList, nextIndex, webViewPrebidCallback); + } else { + webViewPrebidCallback.failure(); + } + } + + @Override + public void onReceiveValue(@Nullable String html) { + + if (html == null) { + LogUtil.w("webView jsCode is null"); + + repeatOrFail(); + } else { + + @Nullable + CreativeSize adSize = findSizeInJavaScript(html); + + if (adSize == null) { + LogUtil.w("adSize is null"); + repeatOrFail(); + } else { + webViewPrebidCallback.success(webView, adSize); + } + + } + + } + }); + } + + @Nullable + static CreativeSize findSizeInJavaScript(@Nullable String jsCode) { + + if (TextUtils.isEmpty(jsCode)) { + LogUtil.w("jsCode is empty"); + return null; + } + + String hbSizeKeyValue = findHbSizeKeyValue(jsCode); + if (hbSizeKeyValue == null) { + LogUtil.w("HbSizeKeyValue is null"); + return null; + } + + String hbSizeValue = findHbSizeValue(hbSizeKeyValue); + if (hbSizeValue == null) { + LogUtil.w("HbSizeValue is null"); + return null; + } + + return stringToSize(hbSizeValue); + } + + @Nullable + static String findHbSizeKeyValue(String text) { + return matchAndCheck("hb_size\\W+[0-9]+x[0-9]+", text); + } + + @Nullable + static String findHbSizeValue(String text) { + return matchAndCheck("[0-9]+x[0-9]+", text); + } + + @NonNull + static String[] matches(String regex, String text) { + + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(text); + + List allMatches = new ArrayList<>(); + while (matcher.find()) { + allMatches.add(matcher.group()); + } + + return allMatches.toArray(new String[0]); + } + + @Nullable + static String matchAndCheck(String regex, String text) { + + String[] matched = matches(regex, text); + + if (matched.length == 0) { + return null; + } + + String firstResult = matched[0]; + return firstResult; + } + + @Nullable + static CreativeSize stringToSize(String size) { + String[] sizeArr = size.split("x"); + + if (sizeArr.length != 2) { + LogUtil.w(size + "has a wrong format"); + return null; + } + + String widthString = sizeArr[0]; + String heightString = sizeArr[1]; + + int width; + int height; + try { + width = Integer.parseInt(widthString); + } catch (NumberFormatException e) { + LogUtil.w(size + "can not be converted to Size"); + return null; + } + + try { + height = Integer.parseInt(heightString); + } catch (NumberFormatException e) { + LogUtil.w(size + "can not be converted to Size"); + return null; + } + + return new CreativeSize(width, height); + } + static Class getClassFromString(String className) { try { return Class.forName(className); @@ -281,4 +489,68 @@ private static void removeUsedCustomTargetingForDFP(Object adRequestObj) { } } } + + public interface CreativeSizeCompletionHandler { + void onSize(@Nullable CreativeSize size); + } + + private interface WebViewPrebidCallback { + void success(WebView webView, CreativeSize adSize); + + void failure(); + } + + /** + * Utility Size class + */ + public static class CreativeSize { + private int width; + private int height; + + /** + * Creates an ad size object with width and height as specified + * + * @param width width of the ad container + * @param height height of the ad container + */ + public CreativeSize(int width, int height) { + this.width = width; + this.height = height; + } + + /** + * Returns the width of the ad container + * + * @return width + */ + public int getWidth() { + return width; + } + + /** + * Returns the height of the ad container + * + * @return height + */ + public int getHeight() { + return height; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CreativeSize adSize = (CreativeSize) o; + + if (width != adSize.width) return false; + return height == adSize.height; + } + + @Override + public int hashCode() { + String size = width + "x" + height; + return size.hashCode(); + } + } } diff --git a/PrebidMobile/API1.0/src/test/java/org/prebid/mobile/UtilTest.java b/PrebidMobile/API1.0/src/test/java/org/prebid/mobile/UtilTest.java index 27c55b076..936935156 100644 --- a/PrebidMobile/API1.0/src/test/java/org/prebid/mobile/UtilTest.java +++ b/PrebidMobile/API1.0/src/test/java/org/prebid/mobile/UtilTest.java @@ -30,7 +30,9 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertNull; @RunWith(RobolectricTestRunner.class) @@ -111,4 +113,87 @@ public void testSupportedAdObject() throws Exception { Object object = new Object(); assertFalse(Util.supportedAdObject(object)); } + + @Test + public void testRegexMatches() { + String[] result = Util.matches("^a", "aaa aaa"); + assertEquals(1, result.length); + assertEquals("a", result[0]); + + result = Util.matches("^b", "aaa aaa"); + assertEquals(0, result.length); + + result = Util.matches("aaa aaa", "^a"); + assertEquals(0, result.length); + + result = Util.matches("[0-9]+x[0-9]+", "{ \n adManagerResponse:\"hb_size\":[\"728x90\"],\"hb_size_rubicon\":[\"1x1\"],moPubResponse:\"hb_size:300x250\" \n }"); + assertEquals(3, result.length); + assertEquals("728x90", result[0]); + assertEquals("1x1", result[1]); + assertEquals("300x250", result[2]); + + result = Util.matches("hb_size\\W+[0-9]+x[0-9]+", "{ \n adManagerResponse:\"hb_size\":[\"728x90\"],\"hb_size_rubicon\":[\"1x1\"],moPubResponse:\"hb_size:300x250\" \n }"); + assertEquals(2, result.length); + assertEquals("hb_size\":[\"728x90", result[0]); + assertEquals("hb_size:300x250", result[1]); + } + + @Test + public void testRegexMatchAndCheck() { + String result = Util.matchAndCheck("^a", "aaa aaa"); + + assertNotNull(result); + assertEquals("a", result); + + result = Util.matchAndCheck("^b", "aaa aaa"); + assertNull(result); + } + + + @Test + public void testFindHbSizeValue() { + String result = Util.findHbSizeValue("{ \n adManagerResponse:\"hb_size\":[\"728x90\"],\"hb_size_rubicon\":[\"728x90\"],moPubResponse:\"hb_size:300x250\" \n }"); + assertNotNull(result); + assertEquals("728x90", result); + } + + @Test + public void testFindHbSizeKeyValue() { + String result = Util.findHbSizeKeyValue("{ \n adManagerResponse:\"hb_size\":[\"728x90\"],\"hb_size_rubicon\":[\"728x90\"],moPubResponse:\"hb_size:300x250\" \n }"); + assertNotNull(result); + assertEquals("hb_size\":[\"728x90", result); + } + + @Test + public void testStringToCGSize() { + Util.CreativeSize result = Util.stringToSize("300x250"); + assertNotNull(result); + assertEquals(new Util.CreativeSize(300, 250), result); + + result = Util.stringToSize("300x250x1"); + assertNull(result); + + result = Util.stringToSize("ERROR"); + assertNull(result); + + result = Util.stringToSize("300x250ERROR"); + assertNull(result); + } + + @Test + public void testFindSizeInJavaScript() { + Util.CreativeSize result = Util.findSizeInJavaScript(null); + assertNull(result); + + result = Util.findSizeInJavaScript(""); + assertNull(result); + + result = Util.findSizeInJavaScript(""); + assertNull(result); + + result = Util.findSizeInJavaScript(""); + assertNotNull(result); + assertEquals(new Util.CreativeSize(728, 90), result); + } + } diff --git a/PrebidMobile/API1.0Demo/src/androidTest/java/org/prebid/mobile/app/DFPBannerComplexTest.java b/PrebidMobile/API1.0Demo/src/androidTest/java/org/prebid/mobile/app/DFPBannerComplexTest.java new file mode 100644 index 000000000..060e7046c --- /dev/null +++ b/PrebidMobile/API1.0Demo/src/androidTest/java/org/prebid/mobile/app/DFPBannerComplexTest.java @@ -0,0 +1,249 @@ +/* + * Copyright 2018-2019 Prebid.org, 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 org.prebid.mobile.app; + +import android.graphics.Color; +import android.view.View; +import android.widget.FrameLayout; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.rule.ActivityTestRule; + +import com.google.android.gms.ads.AdListener; +import com.google.android.gms.ads.AdSize; +import com.google.android.gms.ads.doubleclick.PublisherAdRequest; +import com.google.android.gms.ads.doubleclick.PublisherAdView; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.prebid.mobile.BannerAdUnit; +import org.prebid.mobile.Host; +import org.prebid.mobile.LogUtil; +import org.prebid.mobile.OnCompleteListener; +import org.prebid.mobile.PrebidMobile; +import org.prebid.mobile.ResultCode; +import org.prebid.mobile.Util; + +import java.util.concurrent.CountDownLatch; + +import static org.junit.Assert.assertEquals; + +@RunWith(AndroidJUnit4.class) +public class DFPBannerComplexTest { + @Rule + public ActivityTestRule m = new ActivityTestRule<>(DemoActivity.class); + + //30x250 -> 728x90 + @Test + public void testRubiconDFPBannerResizeSanityAppCheckTest() throws Exception { + + final CountDownLatch lock = new CountDownLatch(1); + + PrebidMobile.setPrebidServerHost(Host.RUBICON); + PrebidMobile.setPrebidServerAccountId(Constants.PBS_ACCOUNT_ID_RUBICON); + + DemoActivity demoActivity = m.getActivity(); + + final IntegerWrapper firstTransactionCount = new IntegerWrapper(); + final IntegerWrapper secondTransactionCount = new IntegerWrapper(); + + final int transactionFailRepeatCount = 5; + final int screenshotDelayMillis = 3_000; + final int transactionFailDelayMillis = 3_000; + + final FrameLayout adFrame = demoActivity.findViewById(R.id.adFrame); + demoActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + adFrame.removeAllViews(); + } + }); + + final PublisherAdView dfpAdView = new PublisherAdView(demoActivity); + //Programmatic fix + dfpAdView.setAdUnitId("/5300653/test_adunit_pavliuchyk_300x250_puc_ucTagData_prebid-server.rubiconproject.com"); + dfpAdView.setAdSizes(new AdSize(300, 250)); + + //Targeting creative + /* + dfpAdView.setAdUnitId("/5300653/Banner_PUC_b397711"); + dfpAdView.setAdSizes(new AdSize(300, 250), new AdSize(728, 90)); + */ + + final BannerAdUnit bannerAdUnit = new BannerAdUnit("1001-1", 300, 250); + + final PublisherAdRequest request = new PublisherAdRequest.Builder().build(); + final OnCompleteListener completeListener = new OnCompleteListener() { + @Override + public void onComplete(ResultCode resultCode) { + dfpAdView.loadAd(request); + } + }; + + dfpAdView.setAdListener(new AdListener() { + + private void notifyResult() { + lock.countDown(); + } + + private void update(boolean isSuccess) { + if (isSuccess) { + + if (firstTransactionCount.getValue() != -1) { + firstTransactionCount.value = -1; + //TODO make a call + + bannerAdUnit.addAdditionalSize(728, 90); + bannerAdUnit.fetchDemand(request, completeListener); + } else if (secondTransactionCount.getValue() != -1) { + secondTransactionCount.value = -1; + + notifyResult(); + } + + } else { + try { + Thread.sleep(transactionFailDelayMillis); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + if (firstTransactionCount.getValue() != -1) { + if (firstTransactionCount.getValue() > transactionFailRepeatCount -2) { + Assert.fail("first Transaction Count == " + transactionFailRepeatCount); + } else { + //repeat + firstTransactionCount.value++; + bannerAdUnit.fetchDemand(request, completeListener); + } + } else if (secondTransactionCount.getValue() != -1) { + if (secondTransactionCount.getValue() > transactionFailRepeatCount -2) { + Assert.fail("second Transaction Count == " + transactionFailRepeatCount); + } else { + //repeat + secondTransactionCount.value++; + bannerAdUnit.fetchDemand(request, completeListener); + } + } else { + Assert.fail("Unexpected"); + } + + } + + } + + @Override + public void onAdLoaded() { + super.onAdLoaded(); + + //Programmatic fix + Util.findPrebidCreativeSize(dfpAdView, new Util.CreativeSizeCompletionHandler() { + @Override + public void onSize(final Util.CreativeSize size) { + if (size != null) { + + dfpAdView.setAdSizes(new AdSize(size.getWidth(), size.getHeight())); + + final View child = dfpAdView.getChildAt(0); + child.setBackgroundColor(Color.RED); + + dfpAdView.post(new Runnable() { + @Override + public void run() { + + float density = dfpAdView.getResources().getDisplayMetrics().density; + double dpW = Math.ceil(child.getMinimumWidth() / density); + double dpH = Math.ceil(child.getMinimumHeight() / density); + + try { + Thread.sleep(screenshotDelayMillis); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + assertEquals((int)dpW, size.getWidth()); + assertEquals((int)dpH, size.getHeight()); + + update(true); + + } + }); + + } else { + LogUtil.w("size is null"); + update(false); + } + } + }); + + //Targeting creative + /* + try { + Thread.sleep(screenshotDelayMillis); + } catch (InterruptedException e) { + e.printStackTrace(); + } + update(true); + */ + } + + @Override + public void onAdFailedToLoad(int i) { + super.onAdFailedToLoad(i); + LogUtil.w("onAdFailedToLoad:" + i); + + update(false); + } + }); + + demoActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + adFrame.addView(dfpAdView); + } + }); + + bannerAdUnit.fetchDemand(request, completeListener); + + //TravisCI fix + Thread.sleep(2 * transactionFailRepeatCount * transactionFailDelayMillis + 2 * screenshotDelayMillis); + //local test +// lock.await(); + + try { + Thread.sleep(screenshotDelayMillis); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + assertEquals(-1, firstTransactionCount.getValue()); + assertEquals(-1, secondTransactionCount.getValue()); + + + } + + private static class IntegerWrapper { + int value = 0; + + public int getValue() { + return value; + } + } + +} diff --git a/PrebidMobile/API1.0Demo/src/main/java/org/prebid/mobile/app/DemoActivity.java b/PrebidMobile/API1.0Demo/src/main/java/org/prebid/mobile/app/DemoActivity.java index 9f34b2a1c..f6c194fb7 100644 --- a/PrebidMobile/API1.0Demo/src/main/java/org/prebid/mobile/app/DemoActivity.java +++ b/PrebidMobile/API1.0Demo/src/main/java/org/prebid/mobile/app/DemoActivity.java @@ -39,6 +39,7 @@ import org.prebid.mobile.InterstitialAdUnit; import org.prebid.mobile.OnCompleteListener; import org.prebid.mobile.ResultCode; +import org.prebid.mobile.Util; import static org.prebid.mobile.app.Constants.MOPUB_BANNER_ADUNIT_ID_300x250; import static org.prebid.mobile.app.Constants.MOPUB_BANNER_ADUNIT_ID_320x50; @@ -88,7 +89,14 @@ void createDFPBanner(String size) { public void onAdLoaded() { super.onAdLoaded(); - dfpAdView.setAdSizes(dfpAdView.getAdSize()); + Util.findPrebidCreativeSize(dfpAdView, new Util.CreativeSizeCompletionHandler() { + @Override + public void onSize(final Util.CreativeSize size) { + if (size != null) { + dfpAdView.setAdSizes(new AdSize(size.getWidth(), size.getHeight())); + } + } + }); } });