From e8bd0f0f43f43c3f21c807d328178800f51270d1 Mon Sep 17 00:00:00 2001 From: Viktor Rasevych Date: Mon, 30 Oct 2023 11:42:28 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=90=20Implement=20resize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration/MraidBannerFunctionalTest.kt | 163 +++++- .../CriteoInterstitialMraidControllerTest.kt | 19 + .../publisher/util/ViewPositionTrackerTest.kt | 8 +- .../com/criteo/publisher/BannerLogMessage.kt | 7 + .../publisher/CriteoBannerAdWebViewFactory.kt | 4 +- .../publisher/CriteoBannerMraidController.kt | 477 +++++++++++++++++- .../criteo/publisher/DependencyProvider.java | 2 +- .../advancednative/AdChoiceOverlay.java | 12 +- .../com/criteo/publisher/adview/AdWebView.kt | 1 + .../publisher/adview/CriteoMraidController.kt | 56 +- .../publisher/adview/DummyMraidController.kt | 16 + .../publisher/adview/MraidController.kt | 24 + .../publisher/adview/MraidInteractor.kt | 4 + .../publisher/adview/MraidMessageHandler.kt | 20 + .../adview/MraidMessageHandlerListener.kt | 10 + .../adview/MraidResizeCustomClosePosition.kt | 32 ++ .../com/criteo/publisher/adview/MraidState.kt | 1 + .../CriteoInterstitialMraidController.kt | 21 + .../criteo/publisher/util/AndroidUtil.java | 10 - .../com/criteo/publisher/util/DeviceUtil.kt | 58 ++- .../publisher/util/ViewPositionTracker.kt | 20 +- publisher-sdk/src/main/res/values/ids.xml | 2 + .../adview/CriteoMraidControllerTest.kt | 55 +- .../publisher/adview/MraidInteractorTest.kt | 11 + .../adview/MraidMessageHandlerTest.kt | 23 + .../MraidResizeCustomClosePositionTest.kt | 63 +++ .../publisher/util/AndroidUtilUnitTest.kt | 66 --- .../criteo/publisher/util/DeviceUtilTest.kt | 28 +- test-utils/build.gradle.kts | 4 +- .../com/criteo/publisher/MraidPosition.kt | 22 + 30 files changed, 1097 insertions(+), 142 deletions(-) create mode 100644 publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidResizeCustomClosePosition.kt create mode 100644 publisher-sdk/src/test/java/com/criteo/publisher/adview/MraidResizeCustomClosePositionTest.kt delete mode 100644 publisher-sdk/src/test/java/com/criteo/publisher/util/AndroidUtilUnitTest.kt create mode 100644 test-utils/src/main/java/com/criteo/publisher/MraidPosition.kt diff --git a/publisher-sdk/src/androidTest/java/com/criteo/publisher/integration/MraidBannerFunctionalTest.kt b/publisher-sdk/src/androidTest/java/com/criteo/publisher/integration/MraidBannerFunctionalTest.kt index 43a0f68fb..2999d3d10 100644 --- a/publisher-sdk/src/androidTest/java/com/criteo/publisher/integration/MraidBannerFunctionalTest.kt +++ b/publisher-sdk/src/androidTest/java/com/criteo/publisher/integration/MraidBannerFunctionalTest.kt @@ -23,9 +23,12 @@ import androidx.test.filters.FlakyTest import androidx.test.rule.ActivityTestRule import com.criteo.publisher.CriteoBannerView import com.criteo.publisher.CriteoUtil +import com.criteo.publisher.DependencyProvider import com.criteo.publisher.MraidData +import com.criteo.publisher.MraidPosition import com.criteo.publisher.R import com.criteo.publisher.TestAdUnits +import com.criteo.publisher.adview.MraidResizeCustomClosePosition import com.criteo.publisher.adview.MraidState import com.criteo.publisher.adview.Redirection import com.criteo.publisher.callMraidObjectBlocking @@ -83,6 +86,7 @@ class MraidBannerFunctionalTest { private lateinit var onExpanded: CountDownLatch private lateinit var onHidden: CountDownLatch private lateinit var onDefault: CountDownLatch + private lateinit var onResized: CountDownLatch @Before fun setUp() { @@ -92,6 +96,7 @@ class MraidBannerFunctionalTest { onExpanded = CountDownLatch(1) onHidden = CountDownLatch(1) onDefault = CountDownLatch(1) + resetResizeCounter() givenInitializedSdk(validBannerAdUnit) bannerView = whenLoadingABanner(validBannerAdUnit)!! @@ -105,12 +110,7 @@ class MraidBannerFunctionalTest { onExpanded.await() mockedDependenciesRule.waitForIdleState() - assertThat(getCurrentState()).isEqualTo(MraidState.EXPANDED) - assertThat(getWebView().parent).isNotNull - assertThat(getWebView().parent).isNotEqualTo(bannerView) - assertThat(bannerView.childCount).isEqualTo(1) - assertThat(bannerView.getChildAt(0).id).isEqualTo(R.id.adWebViewPlaceholder) - assertThat((getWebView().parent as ViewGroup).id).isEqualTo(R.id.adWebViewDialogContainer) + assertExpandedCorrectly() } @Test @@ -138,18 +138,110 @@ class MraidBannerFunctionalTest { onDefault.await() mockedDependenciesRule.waitForIdleState() + assertClosedCorrectly(originalLayoutParams) + } + + private fun assertClosedCorrectly(originalLayoutParams: ViewGroup.LayoutParams?) { assertThat(getCurrentState()).isEqualTo(MraidState.DEFAULT) assertThat(getWebView().parent).isEqualTo(bannerView) assertThat(getWebView().layoutParams).isEqualTo(originalLayoutParams) } + @Test + @FlakyTest(detail = "Flakiness comes from UI and concurrency") + fun whenExpandFromResizedState_ShouldMoveWebViewToDialogAndUpdateState() { + setResizeProperties(100, 100, 0, 0, MraidResizeCustomClosePosition.Center, true) + resize() + + onResized.await() + mockedDependenciesRule.waitForIdleState() + + expand() + + onExpanded.await() + mockedDependenciesRule.waitForIdleState() + + assertExpandedCorrectly() + } + @Test @FlakyTest(detail = "Flakiness comes from UI and concurrency") fun whenOpen_ShouldDelegateToRedirection() { open() mockedDependenciesRule.waitForIdleState() - verify(redirection).redirect(eq("https://www.criteo.com"), eq(activityRule.activity.componentName), any()) + verify(redirection).redirect( + eq("https://www.criteo.com"), + eq(activityRule.activity.componentName), + any() + ) + } + + @Test + @FlakyTest(detail = "Flakiness comes from UI and concurrency") + fun whenResize_shouldMoveViewAboveAllViewsWithProperParamsAndUpdateState() { + val originalPosition = getCurrentPosition() + + setResizeProperties(100, 100, 20, 20, MraidResizeCustomClosePosition.Center, true) + resize() + + onResized.await() + mockedDependenciesRule.waitForIdleState() + + assertThat(getCurrentState()).isEqualTo(MraidState.RESIZED) + val currentPosition = getCurrentPosition() + assertThat(currentPosition.width).isEqualTo(100) + assertThat(currentPosition.height).isEqualTo(100) + assertThat(currentPosition.x).isEqualTo(originalPosition.x + 20) + assertThat(currentPosition.y).isEqualTo(originalPosition.y + 20) + assertThat(getWebView().parent).isNotEqualTo(bannerView) + assertThat(bannerView.getChildAt(0).id).isEqualTo(R.id.adWebViewPlaceholder) + } + + @Test + @FlakyTest(detail = "Flakiness comes from UI and concurrency") + fun whenResizeAndThenResizeWithDifferentParameter_shouldMoveViewAboveAllViewsWithProperParamsAndUpdateState() { + val originalPosition = getCurrentPosition() + + setResizeProperties(100, 100, 15, 15, MraidResizeCustomClosePosition.Center, true) + resize() + + onResized.await() + mockedDependenciesRule.waitForIdleState() + + resetResizeCounter() + setResizeProperties(150, 150, 10, 10, MraidResizeCustomClosePosition.TopCenter, false) + resize() + + onResized.await() + mockedDependenciesRule.waitForIdleState() + + assertThat(getCurrentState()).isEqualTo(MraidState.RESIZED) + val currentPosition = getCurrentPosition() + assertThat(currentPosition.width).isEqualTo(150) + assertThat(currentPosition.height).isEqualTo(150) + assertThat(currentPosition.x).isEqualTo(originalPosition.x + 15 + 10) + assertThat(currentPosition.y).isEqualTo(originalPosition.y + 15 + 10) + + assertThat(getWebView().parent).isNotEqualTo(bannerView) + assertThat(bannerView.getChildAt(0).id).isEqualTo(R.id.adWebViewPlaceholder) + } + + @Test + @FlakyTest(detail = "Flakiness comes from UI and concurrency") + fun whenResizeAndThenClose_ShouldMoveBackToOriginalContainer() { + val originalLayoutParams = getWebView().layoutParams + setResizeProperties(100, 100, 0, 0, MraidResizeCustomClosePosition.Center, true) + resize() + + onResized.await() + mockedDependenciesRule.waitForIdleState() + + close() + onDefault.await() + mockedDependenciesRule.waitForIdleState() + + assertClosedCorrectly(originalLayoutParams) } private fun givenInitializedSdk(vararg preloadedAdUnits: AdUnit) { @@ -189,6 +281,15 @@ class MraidBannerFunctionalTest { waitForBids() } + private fun assertExpandedCorrectly() { + assertThat(getCurrentState()).isEqualTo(MraidState.EXPANDED) + assertThat(getWebView().parent).isNotNull + assertThat(getWebView().parent).isNotEqualTo(bannerView) + assertThat(bannerView.childCount).isEqualTo(1) + assertThat(bannerView.getChildAt(0).id).isEqualTo(R.id.adWebViewPlaceholder) + assertThat((getWebView().parent as ViewGroup).id).isEqualTo(R.id.adWebViewDialogContainer) + } + private fun expand() { getWebView().callMraidObjectBlocking("expand()") } @@ -197,6 +298,41 @@ class MraidBannerFunctionalTest { getWebView().callMraidObjectBlocking("close()") } + private fun setResizeProperties( + width: Int, + height: Int, + offsetX: Int, + offsetY: Int, + customClosePosition: MraidResizeCustomClosePosition, + allowOffscreen: Boolean + ) { + getWebView().callMraidObjectBlocking(buildString { + append("setResizeProperties") + append("(") + append("{") + append("width:") + append(width) + append(", height:") + append(height) + append(", offsetX:") + append(offsetX) + append(", offsetY:") + append(offsetY) + append(", customClosePosition:") + append("\"") + append(customClosePosition.value) + append("\"") + append(", allowOffscreen:") + append(allowOffscreen) + append("}") + append(")") + }) + } + + private fun resize() { + getWebView().callMraidObjectBlocking("resize()") + } + private fun open() { getWebView().callMraidObjectBlocking("open(\"https://www.criteo.com\")") } @@ -204,6 +340,9 @@ class MraidBannerFunctionalTest { private fun getCurrentState() = getWebView().getJavascriptResultBlocking("window.mraid.getState()") .toMraidState() + private fun getCurrentPosition() = getWebView().getJavascriptResultBlocking("window.mraid.getCurrentPosition()") + .toMraidPosition() + private fun getWebView(): WebView { return bannerView.adWebView } @@ -217,6 +356,10 @@ class MraidBannerFunctionalTest { onReady.countDown() } + fun resetResizeCounter() { + onResized = CountDownLatch(1) + } + @JavascriptInterface fun onStateChange(newState: String) { when (newState.toMraidState()) { @@ -224,6 +367,7 @@ class MraidBannerFunctionalTest { MraidState.DEFAULT -> onDefault.countDown() MraidState.EXPANDED -> onExpanded.countDown() MraidState.HIDDEN -> onHidden.countDown() + MraidState.RESIZED -> onResized.countDown() } } @@ -231,4 +375,9 @@ class MraidBannerFunctionalTest { val unquotedState = this.replace("\"", "") return MraidState.values().first { it.stringValue == unquotedState } } + + private fun String.toMraidPosition(): MraidPosition = DependencyProvider.getInstance() + .provideMoshi() + .adapter(MraidPosition::class.java) + .fromJson(this)!! } diff --git a/publisher-sdk/src/androidTest/java/com/criteo/publisher/interstitial/CriteoInterstitialMraidControllerTest.kt b/publisher-sdk/src/androidTest/java/com/criteo/publisher/interstitial/CriteoInterstitialMraidControllerTest.kt index f720c8428..48d7452d7 100644 --- a/publisher-sdk/src/androidTest/java/com/criteo/publisher/interstitial/CriteoInterstitialMraidControllerTest.kt +++ b/publisher-sdk/src/androidTest/java/com/criteo/publisher/interstitial/CriteoInterstitialMraidControllerTest.kt @@ -19,6 +19,8 @@ package com.criteo.publisher.interstitial import androidx.test.rule.ActivityTestRule import com.criteo.publisher.adview.MraidActionResult import com.criteo.publisher.adview.MraidPlacementType +import com.criteo.publisher.adview.MraidResizeActionResult +import com.criteo.publisher.adview.MraidResizeCustomClosePosition import com.criteo.publisher.adview.MraidState import com.criteo.publisher.concurrent.RunOnUiThreadExecutor import com.criteo.publisher.mock.MockedDependenciesRule @@ -91,6 +93,23 @@ class CriteoInterstitialMraidControllerTest { verify(callbackMock).invoke(argThat { this is MraidActionResult.Error }) } + @Test + fun doResize_ShouldCallbackError() { + val callbackMock = mock<(result: MraidResizeActionResult) -> Unit>() + + criteoInterstitialMraidController.doResize( + 100.0, + 100.0, + 0.0, + 0.0, + MraidResizeCustomClosePosition.Center, + true, + callbackMock + ) + + verify(callbackMock).invoke(argThat { this is MraidResizeActionResult.Error }) + } + @Test fun doClose_givenLoadingState_ShouldCallbackError() { val callbackMock = mock<(result: MraidActionResult) -> Unit>() diff --git a/publisher-sdk/src/androidTest/java/com/criteo/publisher/util/ViewPositionTrackerTest.kt b/publisher-sdk/src/androidTest/java/com/criteo/publisher/util/ViewPositionTrackerTest.kt index f85d68e26..678a6bfd7 100644 --- a/publisher-sdk/src/androidTest/java/com/criteo/publisher/util/ViewPositionTrackerTest.kt +++ b/publisher-sdk/src/androidTest/java/com/criteo/publisher/util/ViewPositionTrackerTest.kt @@ -75,10 +75,10 @@ class ViewPositionTrackerTest { view.getLocationInWindow(outWindowLocation) verify(listener, atLeastOnce()).onPositionChange( - eq(deviceUtil.pxToDp(outWindowLocation[0])), - eq(deviceUtil.pxToDp(outWindowLocation[1])), - eq(deviceUtil.pxToDp(view.width)), - eq(deviceUtil.pxToDp(view.height)) + eq(deviceUtil.pixelToDp(outWindowLocation[0])), + eq(deviceUtil.pixelToDp(outWindowLocation[1])), + eq(deviceUtil.pixelToDp(view.width)), + eq(deviceUtil.pixelToDp(view.height)) ) } diff --git a/publisher-sdk/src/main/java/com/criteo/publisher/BannerLogMessage.kt b/publisher-sdk/src/main/java/com/criteo/publisher/BannerLogMessage.kt index b73a322c5..2c4548a19 100644 --- a/publisher-sdk/src/main/java/com/criteo/publisher/BannerLogMessage.kt +++ b/publisher-sdk/src/main/java/com/criteo/publisher/BannerLogMessage.kt @@ -66,4 +66,11 @@ internal object BannerLogMessage { level = Log.ERROR, throwable = throwable ) + + @JvmStatic + fun onBannerFailedToResize(bannerView: CriteoBannerView?, throwable: Throwable) = LogMessage( + message = "BannerView(${bannerView?.bannerAdUnit}) failed to resize", + level = Log.ERROR, + throwable = throwable + ) } diff --git a/publisher-sdk/src/main/java/com/criteo/publisher/CriteoBannerAdWebViewFactory.kt b/publisher-sdk/src/main/java/com/criteo/publisher/CriteoBannerAdWebViewFactory.kt index ee32c95b7..d22549317 100644 --- a/publisher-sdk/src/main/java/com/criteo/publisher/CriteoBannerAdWebViewFactory.kt +++ b/publisher-sdk/src/main/java/com/criteo/publisher/CriteoBannerAdWebViewFactory.kt @@ -31,6 +31,8 @@ class CriteoBannerAdWebViewFactory { criteo: Criteo?, parentContainer: CriteoBannerView ): CriteoBannerAdWebView { - return CriteoBannerAdWebView(context, attrs, bannerAdUnit, criteo, parentContainer) + return CriteoBannerAdWebView(context, attrs, bannerAdUnit, criteo, parentContainer).also { + it.id = R.id.bannerAdWebView + } } } diff --git a/publisher-sdk/src/main/java/com/criteo/publisher/CriteoBannerMraidController.kt b/publisher-sdk/src/main/java/com/criteo/publisher/CriteoBannerMraidController.kt index d0de43a80..dc53e38d0 100644 --- a/publisher-sdk/src/main/java/com/criteo/publisher/CriteoBannerMraidController.kt +++ b/publisher-sdk/src/main/java/com/criteo/publisher/CriteoBannerMraidController.kt @@ -16,24 +16,32 @@ package com.criteo.publisher +import android.annotation.SuppressLint import android.app.Dialog import android.content.Context +import android.content.Context.WINDOW_SERVICE +import android.graphics.PixelFormat import android.os.Bundle +import android.view.Gravity import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams import android.view.Window import android.view.WindowManager +import android.widget.FrameLayout import android.widget.RelativeLayout import androidx.annotation.MainThread import com.criteo.publisher.BannerLogMessage.onBannerFailedToClose import com.criteo.publisher.BannerLogMessage.onBannerFailedToExpand +import com.criteo.publisher.BannerLogMessage.onBannerFailedToResize import com.criteo.publisher.advancednative.VisibilityTracker import com.criteo.publisher.adview.CriteoMraidController import com.criteo.publisher.adview.MraidActionResult import com.criteo.publisher.adview.MraidInteractor import com.criteo.publisher.adview.MraidMessageHandler import com.criteo.publisher.adview.MraidPlacementType +import com.criteo.publisher.adview.MraidResizeActionResult +import com.criteo.publisher.adview.MraidResizeCustomClosePosition import com.criteo.publisher.adview.MraidState import com.criteo.publisher.annotation.OpenForTesting import com.criteo.publisher.concurrent.RunOnUiThreadExecutor @@ -41,6 +49,8 @@ import com.criteo.publisher.util.DeviceUtil import com.criteo.publisher.util.ExternalVideoPlayer import com.criteo.publisher.util.ViewPositionTracker import com.criteo.publisher.util.doOnNextLayout +import kotlin.math.abs +import kotlin.math.roundToInt @OpenForTesting @Suppress("TooManyFunctions", "LongParameterList") @@ -50,7 +60,7 @@ internal class CriteoBannerMraidController( visibilityTracker: VisibilityTracker, mraidInteractor: MraidInteractor, mraidMessageHandler: MraidMessageHandler, - deviceUtil: DeviceUtil, + private val deviceUtil: DeviceUtil, viewPositionTracker: ViewPositionTracker, externalVideoPlayer: ExternalVideoPlayer ) : CriteoMraidController( @@ -70,6 +80,9 @@ internal class CriteoBannerMraidController( id = R.id.adWebViewPlaceholder } } + private var resizedRoot: FrameLayout? = null + private var resizedAdContainer: RelativeLayout? = null + private var resizedCloseRegion: View? = null override fun getPlacementType(): MraidPlacementType = MraidPlacementType.INLINE @@ -86,7 +99,11 @@ internal class CriteoBannerMraidController( EXPAND_ACTION ) ) - MraidState.DEFAULT -> expandFromDefaultState(width, height, onResult) + MraidState.DEFAULT, MraidState.RESIZED -> expandFromDefaultOrResizedState( + width, + height, + onResult + ) MraidState.EXPANDED -> onResult(MraidActionResult.Error("Ad already expanded", "expand")) MraidState.HIDDEN -> onResult( MraidActionResult.Error( @@ -108,7 +125,7 @@ internal class CriteoBannerMraidController( ) ) MraidState.DEFAULT -> closeFromDefaultState(onResult) - MraidState.EXPANDED -> closeFromExpandedState(onResult) + MraidState.EXPANDED, MraidState.RESIZED -> closeFromExpandedOrResizedState(onResult) MraidState.HIDDEN -> onResult( MraidActionResult.Error( "Can't close in hidden state", @@ -119,23 +136,80 @@ internal class CriteoBannerMraidController( } } + override fun doResize( + width: Double, + height: Double, + offsetX: Double, + offsetY: Double, + customClosePosition: MraidResizeCustomClosePosition, + allowOffscreen: Boolean, + onResult: (result: MraidResizeActionResult) -> Unit + ) { + runOnUiThreadExecutor.execute { + when (currentState) { + MraidState.LOADING -> onResult( + MraidResizeActionResult.Error( + "Can't resize in loading state", + RESIZE_ACTION + ) + ) + MraidState.DEFAULT, MraidState.RESIZED -> resizeFromDefaultOrResizedState( + width, + height, + offsetX, + offsetY, + customClosePosition, + allowOffscreen, + onResult + ) + MraidState.EXPANDED -> MraidActionResult.Error( + "Can't resize in expanded state", + RESIZE_ACTION + ) + MraidState.HIDDEN -> onResult( + MraidResizeActionResult.Error( + "Can't resize in hidden state", + RESIZE_ACTION + ) + ) + } + } + } + @Suppress("TooGenericExceptionCaught") - private fun expandFromDefaultState( + override fun resetToDefault() { + try { + if (currentState == MraidState.RESIZED) { + removeBannerFromParentAndCleanupResize() + } else { + removeBannerFromParent() + } + reattachBannerToOriginalContainer() + } catch (t: Throwable) { + logger.log(onBannerFailedToClose(bannerView.parentContainer, t)) + } + } + + @Suppress("TooGenericExceptionCaught") + private fun expandFromDefaultOrResizedState( width: Double, height: Double, onResult: (result: MraidActionResult) -> Unit ) { try { if (!bannerView.isAttachedToWindow) { - onResult(MraidActionResult.Error("View is detached from window", EXPAND_ACTION)) + onResult(MraidActionResult.Error(DETACHED_FROM_WINDOW_MESSAGE, EXPAND_ACTION)) return } val bannerContainer = bannerView.parentContainer val context = (bannerContainer.parent as View).context - bannerContainer.addView(placeholderView, LayoutParams(bannerView.width, bannerView.height)) - bannerContainer.removeView(bannerView) + if (currentState == MraidState.RESIZED) { + removeBannerFromParentAndCleanupResize() + } else { + replaceBannerWithPlaceholder(bannerContainer) + } val expandedLayout = RelativeLayout(context) expandedLayout.id = R.id.adWebViewDialogContainer @@ -164,26 +238,22 @@ internal class CriteoBannerMraidController( } @Suppress("TooGenericExceptionCaught") - private fun closeFromExpandedState(onResult: (result: MraidActionResult) -> Unit) { + private fun closeFromExpandedOrResizedState(onResult: (result: MraidActionResult) -> Unit) { try { if (!bannerView.isAttachedToWindow) { - onResult(MraidActionResult.Error("View is detached from window", CLOSE_ACTION)) + onResult(MraidActionResult.Error(DETACHED_FROM_WINDOW_MESSAGE, CLOSE_ACTION)) return } - removeBannerFromParent() - - val bannerContainer = bannerView.parentContainer - bannerContainer.addView( - bannerView, - LayoutParams(placeholderView.width, placeholderView.height) - ) - bannerContainer.removeView(placeholderView) - bannerView.doOnNextLayout { - bannerView.layoutParams = defaultBannerViewLayoutParams + if (currentState == MraidState.EXPANDED) { + dialog?.dismiss() + removeBannerFromParent() + } else { + removeBannerFromParentAndCleanupResize() } - dialog?.dismiss() + reattachBannerToOriginalContainer() + onResult(MraidActionResult.Success) } catch (t: Throwable) { logger.log(onBannerFailedToClose(bannerView.parentContainer, t)) @@ -191,6 +261,23 @@ internal class CriteoBannerMraidController( } } + private fun reattachBannerToOriginalContainer() { + val bannerContainer = bannerView.parentContainer + bannerContainer.addView( + bannerView, + LayoutParams(placeholderView.width, placeholderView.height) + ) + bannerContainer.removeView(placeholderView) + bannerView.doOnNextLayout { + bannerView.layoutParams = defaultBannerViewLayoutParams + } + } + + private fun replaceBannerWithPlaceholder(bannerContainer: CriteoBannerView) { + bannerContainer.addView(placeholderView, LayoutParams(bannerView.width, bannerView.height)) + bannerContainer.removeView(bannerView) + } + private fun removeBannerFromParent() { val expandedParent = bannerView.parent as ViewGroup expandedParent.removeView(bannerView) @@ -231,10 +318,357 @@ internal class CriteoBannerMraidController( return closeButton } + @Suppress("TooGenericExceptionCaught") + private fun resizeFromDefaultOrResizedState( + width: Double, + height: Double, + offsetX: Double, + offsetY: Double, + customClosePosition: MraidResizeCustomClosePosition, + allowOffscreen: Boolean, + onResult: (result: MraidResizeActionResult) -> Unit + ) { + try { + if (!bannerView.isAttachedToWindow) { + onResult(MraidResizeActionResult.Error(DETACHED_FROM_WINDOW_MESSAGE, RESIZE_ACTION)) + return + } + + var newXInPx = (currentPosition?.first + ?: 0).let { deviceUtil.dpToPixel(it) } + deviceUtil.dpToPixel(offsetX.roundToInt()) + var newYInPx = (currentPosition?.second + ?: 0).let { deviceUtil.dpToPixel(it) } + deviceUtil.dpToPixel(offsetY.roundToInt()) + + val widthInPx = deviceUtil.dpToPixel(width.roundToInt()) + val heightInPx = deviceUtil.dpToPixel(height.roundToInt()) + + // adjust view position to fit on screen + if (!allowOffscreen) { + newXInPx = findClosestPositionOnScreen(newXInPx, maxWidthInPx(), widthInPx) + newYInPx = findClosestPositionOnScreen(newYInPx, maxHeightInPx(), heightInPx) + } + + if (!isCloseRegionOnScreen( + newXInPx, + newYInPx, + widthInPx, + heightInPx, + customClosePosition + )) { + onResult(MraidResizeActionResult.Error("Close button will be offscreen", RESIZE_ACTION)) + return + } + + if (resizedRoot != null) { + updateResizedPopup( + widthInPx, + heightInPx, + customClosePosition, + newXInPx, + newYInPx, + allowOffscreen + ) + } else { + showResizedPopup( + widthInPx, + heightInPx, + customClosePosition, + newXInPx, + newYInPx, + allowOffscreen + ) + } + onResult( + MraidResizeActionResult.Success( + deviceUtil.pixelToDp(newXInPx), + deviceUtil.pixelToDp(newYInPx), + width.roundToInt(), + height.roundToInt() + ) + ) + } catch (t: Throwable) { + logger.log(onBannerFailedToResize(bannerView.parentContainer, t)) + onResult(MraidResizeActionResult.Error("Banner failed to resize", RESIZE_ACTION)) + } + } + + private fun updateResizedPopup( + widthInPx: Int, + heightInPx: Int, + customClosePosition: MraidResizeCustomClosePosition, + newXInPx: Int, + newYInPx: Int, + allowOffscreen: Boolean + ) { + resizedRoot?.let { root -> + resizedCloseRegion?.layoutParams = getCloseRegionLayoutParams(customClosePosition) + resizedAdContainer?.layoutParams = getResizedBannerViewLayoutParams( + newXInPx, + newYInPx, + widthInPx, + heightInPx, + allowOffscreen + ) + val layoutParams = (root.layoutParams as WindowManager.LayoutParams).also { + it.y = calculateWindowY(newYInPx) + it.x = newXInPx + it.width = getOnScreenWidth(widthInPx, newXInPx, allowOffscreen) + it.height = getOnScreenHeight(heightInPx, newYInPx, allowOffscreen) + } + val windowManager = root.context.getSystemService(WINDOW_SERVICE) as WindowManager + windowManager.updateViewLayout(resizedRoot, layoutParams) + } + } + + @SuppressLint("ClickableViewAccessibility") + private fun showResizedPopup( + widthInPx: Int, + heightInPx: Int, + customClosePosition: MraidResizeCustomClosePosition, + newXInPx: Int, + newYInPx: Int, + allowOffscreen: Boolean + ) { + val bannerContainer = bannerView.parentContainer + val context = (bannerContainer.parent as View).context + + replaceBannerWithPlaceholder(bannerContainer) + + val resizedRoot = FrameLayout(context) + resizedRoot.clipChildren = false + val resizedAdContainer = RelativeLayout(context) + resizedAdContainer.addView( + bannerView, + RelativeLayout.LayoutParams(widthInPx, heightInPx) + ) + resizedRoot.addView( + resizedAdContainer, + getResizedBannerViewLayoutParams(newXInPx, newYInPx, widthInPx, heightInPx, allowOffscreen) + ) + addCloseRegion(resizedAdContainer, customClosePosition) + + val params = WindowManager.LayoutParams( + getOnScreenWidth(widthInPx, newXInPx, allowOffscreen), + getOnScreenHeight(heightInPx, newYInPx, allowOffscreen), + WindowManager.LayoutParams.LAST_SUB_WINDOW, + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, + PixelFormat.TRANSLUCENT + ).also { + it.y = calculateWindowY(newYInPx) + it.x = newXInPx + it.gravity = Gravity.TOP or Gravity.LEFT + } + getWindowManager().addView(resizedRoot, params) + this.resizedRoot = resizedRoot + this.resizedAdContainer = resizedAdContainer + } + + // window coordinates include status bar even if app does not draws behind it + // we are always drawing past status bar(event if it is transparent) to not overlap it's gestures + private fun calculateWindowY(newYInPx: Int) = newYInPx + getTopBarHeight() + + private fun getOnScreenHeight( + heightInPx: Int, + newYInPx: Int, + allowOffscreen: Boolean + ) = heightInPx - abs( + when { + !allowOffscreen -> 0 + newYInPx < 0 -> { + newYInPx + } + newYInPx + heightInPx > maxHeightInPx() -> { + newYInPx + heightInPx - maxHeightInPx() + } + else -> { + 0 + } + } + ) + + private fun getOnScreenWidth( + widthInPx: Int, + newXInPx: Int, + allowOffscreen: Boolean + ) = widthInPx - abs( + when { + !allowOffscreen -> 0 + newXInPx < 0 -> newXInPx + newXInPx + widthInPx > maxWidthInPx() -> { + newXInPx + widthInPx - maxWidthInPx() + } + else -> 0 + } + ) + + private fun findClosestPositionOnScreen(position: Int, maxSize: Int, dimensionSize: Int): Int { + val minPosition = 0 + val maxPosition = maxSize - dimensionSize + + val validPosition = when { + position < minPosition -> minPosition + position > maxPosition -> maxPosition + else -> position + } + + return validPosition + } + + private fun getResizedBannerViewLayoutParams( + x: Int, + y: Int, + widthInPx: Int, + heightInPx: Int, + allowOffscreen: Boolean + ): FrameLayout.LayoutParams { + return FrameLayout.LayoutParams( + widthInPx, + heightInPx + ).also { + it.gravity = Gravity.CENTER + val leftMargin = when { + !allowOffscreen -> 0 + x < 0 -> { + x + } + x + widthInPx > maxWidthInPx() -> { + x + widthInPx - maxWidthInPx() + } + else -> { + 0 + } + } + + val topMargin = when { + !allowOffscreen -> 0 + y < 0 -> { + y + } + y + heightInPx > maxHeightInPx() -> { + y + heightInPx - maxHeightInPx() + } + else -> { + 0 + } + } + + it.setMargins(leftMargin / 2, topMargin / 2, 0, 0) + } + } + + private fun addCloseRegion( + resizedLayout: RelativeLayout, + customClosePosition: MraidResizeCustomClosePosition + ) { + val closeRegion = View(resizedLayout.context) + closeRegion.id = R.id.adWebViewCloseRegion + closeRegion.setOnClickListener { + onClose() + } + + resizedLayout.addView( + closeRegion, + getCloseRegionLayoutParams(customClosePosition) + ) + resizedCloseRegion = closeRegion + } + + private fun getCloseRegionLayoutParams(customClosePosition: MraidResizeCustomClosePosition): + RelativeLayout.LayoutParams { + val closeRegionInPx = deviceUtil.dpToPixel(CLOSE_REGION_SIZE) + + return RelativeLayout.LayoutParams(closeRegionInPx, closeRegionInPx).also { + if (customClosePosition == MraidResizeCustomClosePosition.Center) { + it.addRule(RelativeLayout.CENTER_IN_PARENT) + } else { + if (customClosePosition.value.startsWith("top")) { + it.addRule(RelativeLayout.ALIGN_TOP, bannerView.id) + } + if (customClosePosition.value.startsWith("bottom")) { + it.addRule(RelativeLayout.ALIGN_BOTTOM, bannerView.id) + } + if (customClosePosition.value.endsWith("left")) { + it.addRule(RelativeLayout.ALIGN_LEFT, bannerView.id) + } + if (customClosePosition.value.endsWith("right")) { + it.addRule(RelativeLayout.ALIGN_RIGHT, bannerView.id) + } + if (customClosePosition.value.endsWith("center")) { + it.addRule(RelativeLayout.CENTER_HORIZONTAL, bannerView.id) + } + } + } + } + + private fun isCloseRegionOnScreen( + x: Int, + y: Int, + width: Int, + height: Int, + customClosePosition: MraidResizeCustomClosePosition + ): Boolean { + var closeX = 0 + var closeY = 0 + val closeRegionSize = deviceUtil.dpToPixel(CLOSE_REGION_SIZE) + val halfCloseRegionSize = closeRegionSize / 2 + + when (customClosePosition) { + MraidResizeCustomClosePosition.TopCenter -> { + closeX = x + (width / 2 - halfCloseRegionSize) + closeY = y + } + MraidResizeCustomClosePosition.TopRight -> { + closeX = x + width - closeRegionSize + closeY = y + } + MraidResizeCustomClosePosition.TopLeft -> { + closeX = x + closeY = y + } + MraidResizeCustomClosePosition.Center -> { + closeX = x + (width / 2 - halfCloseRegionSize) + closeY = y + (height / 2 - halfCloseRegionSize) + } + MraidResizeCustomClosePosition.BottomCenter -> { + closeX = x + (width / 2 - halfCloseRegionSize) + closeY = y + height - closeRegionSize + } + MraidResizeCustomClosePosition.BottomRight -> { + closeX = x + width - closeRegionSize + closeY = y + height - closeRegionSize + } + MraidResizeCustomClosePosition.BottomLeft -> { + closeX = x + closeY = y + height - closeRegionSize + } + } + + return closeX >= 0 && + closeX <= maxWidthInPx() - closeRegionSize && + closeY >= 0 && + closeY <= maxHeightInPx() - closeRegionSize + } + + private fun removeBannerFromParentAndCleanupResize() { + resizedAdContainer?.removeView(bannerView) + getWindowManager().removeView(resizedRoot) + resizedAdContainer = null + resizedRoot = null + resizedCloseRegion = null + } + private fun getAvailableWidthInPixels() = bannerView.resources.configuration.screenWidthDp * getDensity() private fun getAvailableHeightInPixels() = bannerView.resources.configuration.screenHeightDp * getDensity() private fun getDensity() = bannerView.resources.displayMetrics.density + private fun maxWidthInPx() = maxSize?.first?.let { deviceUtil.dpToPixel(it) } ?: 0 + + private fun maxHeightInPx() = maxSize?.second?.let { deviceUtil.dpToPixel(it) } ?: 0 + + private fun getWindowManager() = bannerView.context.getSystemService(WINDOW_SERVICE) as WindowManager + + private fun getTopBarHeight() = deviceUtil.getTopSystemBarHeight(bannerView.parentContainer) + private class ExpandedDialog(context: Context, val onBackPressedCallback: () -> Unit) : Dialog( context, android.R.style.Theme_Translucent @@ -259,6 +693,9 @@ internal class CriteoBannerMraidController( private companion object { private const val EXPAND_ACTION = "expand" private const val CLOSE_ACTION = "close" + private const val RESIZE_ACTION = "resize" + private const val DETACHED_FROM_WINDOW_MESSAGE = "View is detached from window" private const val DIM_AMOUNT = 0.8f + private const val CLOSE_REGION_SIZE = 50 } } diff --git a/publisher-sdk/src/main/java/com/criteo/publisher/DependencyProvider.java b/publisher-sdk/src/main/java/com/criteo/publisher/DependencyProvider.java index e3681b458..b6e1037ee 100644 --- a/publisher-sdk/src/main/java/com/criteo/publisher/DependencyProvider.java +++ b/publisher-sdk/src/main/java/com/criteo/publisher/DependencyProvider.java @@ -465,7 +465,7 @@ public Redirection provideRedirection() { public AdChoiceOverlay provideAdChoiceOverlay() { return getOrCreate(AdChoiceOverlay.class, () -> new AdChoiceOverlay( provideBuildConfigWrapper(), - provideAndroidUtil() + provideDeviceUtil() )); } diff --git a/publisher-sdk/src/main/java/com/criteo/publisher/advancednative/AdChoiceOverlay.java b/publisher-sdk/src/main/java/com/criteo/publisher/advancednative/AdChoiceOverlay.java index 26c3b9ff1..bace88527 100644 --- a/publisher-sdk/src/main/java/com/criteo/publisher/advancednative/AdChoiceOverlay.java +++ b/publisher-sdk/src/main/java/com/criteo/publisher/advancednative/AdChoiceOverlay.java @@ -28,8 +28,8 @@ import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.criteo.publisher.util.AndroidUtil; import com.criteo.publisher.util.BuildConfigWrapper; +import com.criteo.publisher.util.DeviceUtil; import java.lang.ref.WeakReference; import java.util.Map; import java.util.WeakHashMap; @@ -45,14 +45,14 @@ public class AdChoiceOverlay { private final BuildConfigWrapper buildConfigWrapper; @NonNull - private final AndroidUtil androidUtil; + private final DeviceUtil deviceUtil; public AdChoiceOverlay( @NonNull BuildConfigWrapper buildConfigWrapper, - @NonNull AndroidUtil androidUtil + @NonNull DeviceUtil deviceUtil ) { this.buildConfigWrapper = buildConfigWrapper; - this.androidUtil = androidUtil; + this.deviceUtil = deviceUtil; } /** @@ -92,8 +92,8 @@ ViewGroup addOverlay(@NonNull View view) { // Put the AdChoice at the top right corner FrameLayout.LayoutParams adChoiceLayoutParams = (FrameLayout.LayoutParams) adChoiceImageView.getLayoutParams(); adChoiceLayoutParams.gravity = Gravity.RIGHT; - adChoiceLayoutParams.width = androidUtil.dpToPixel(buildConfigWrapper.getAdChoiceIconWidthInDp()); - adChoiceLayoutParams.height = androidUtil.dpToPixel(buildConfigWrapper.getAdChoiceIconHeightInDp()); + adChoiceLayoutParams.width = deviceUtil.dpToPixel(buildConfigWrapper.getAdChoiceIconWidthInDp()); + adChoiceLayoutParams.height = deviceUtil.dpToPixel(buildConfigWrapper.getAdChoiceIconHeightInDp()); adChoiceImageView.setMinimumWidth(adChoiceLayoutParams.width); adChoiceImageView.setMinimumHeight(adChoiceLayoutParams.height); diff --git a/publisher-sdk/src/main/java/com/criteo/publisher/adview/AdWebView.kt b/publisher-sdk/src/main/java/com/criteo/publisher/adview/AdWebView.kt index 40a191ddb..6d5d5d419 100644 --- a/publisher-sdk/src/main/java/com/criteo/publisher/adview/AdWebView.kt +++ b/publisher-sdk/src/main/java/com/criteo/publisher/adview/AdWebView.kt @@ -33,6 +33,7 @@ abstract class AdWebView @JvmOverloads constructor( abstract fun provideMraidController(): MraidController override fun setWebViewClient(client: WebViewClient) { + mraidController.resetToDefault() // create new mraid controller since new ad is loaded mraidController = provideMraidController() mraidController.onWebViewClientSet(client) diff --git a/publisher-sdk/src/main/java/com/criteo/publisher/adview/CriteoMraidController.kt b/publisher-sdk/src/main/java/com/criteo/publisher/adview/CriteoMraidController.kt index 7d0073726..03b097bd5 100644 --- a/publisher-sdk/src/main/java/com/criteo/publisher/adview/CriteoMraidController.kt +++ b/publisher-sdk/src/main/java/com/criteo/publisher/adview/CriteoMraidController.kt @@ -45,11 +45,14 @@ internal abstract class CriteoMraidController( private var adWebViewClient: AdWebViewClient? = null private var mraidState: MraidState = MraidState.LOADING private var isMraidAd = false + private var ignoreOnPositionChange = false override val currentState: MraidState get() = mraidState protected val logger = LoggerFactory.getLogger(javaClass) + protected var currentPosition: Pair? = null // x, y in dp + protected var maxSize: Pair? = null // width, height in dp init { setupMessageHandler() @@ -60,7 +63,9 @@ internal abstract class CriteoMraidController( } override fun onPositionChange(x: Int, y: Int, width: Int, height: Int) { - mraidInteractor.setCurrentPosition(x, y, width, height) + if (!ignoreOnPositionChange) { + updateCurrentPosition(x, y, width, height) + } } override fun onGone() { @@ -98,6 +103,35 @@ internal abstract class CriteoMraidController( }) } + override fun onResize( + width: Double, + height: Double, + offsetX: Double, + offsetY: Double, + customClosePosition: MraidResizeCustomClosePosition, + allowOffscreen: Boolean + ) { + ignoreOnPositionChange = true + doResize(width, height, offsetX, offsetY, customClosePosition, allowOffscreen) { + when (it) { + is MraidResizeActionResult.Error -> { + mraidInteractor.notifyError(it.message, it.action) + ignoreOnPositionChange = false + } + is MraidResizeActionResult.Success -> { + mraidInteractor.notifyResized() + updateCurrentPosition( + it.x, + it.y, + it.width, + it.height + ) + mraidState = MraidState.RESIZED + } + } + } + } + override fun onPageFinished() { invokeIfMraidAd { onMraidLoaded() @@ -178,16 +212,27 @@ internal abstract class CriteoMraidController( } } + private fun updateCurrentPosition( + x: Int, + y: Int, + width: Int, + height: Int + ) { + mraidInteractor.setCurrentPosition(x, y, width, height) + currentPosition = x to y + } + private fun setMaxSize(configuration: Configuration) { mraidInteractor.setMaxSize( configuration.screenWidthDp, configuration.screenHeightDp, adWebView.resources.displayMetrics.density.toDouble() ) + maxSize = configuration.screenWidthDp to configuration.screenHeightDp } private fun setScreenSize() { - val screenSize = deviceUtil.getRealSceeenSize() + val screenSize = deviceUtil.getRealScreenSize() mraidInteractor.setScreenSize(screenSize.width, screenSize.height) } @@ -197,15 +242,18 @@ internal abstract class CriteoMraidController( private fun updateCurrentStateOnClose() { mraidState = when (currentState) { - MraidState.EXPANDED -> MraidState.DEFAULT + MraidState.EXPANDED, MraidState.RESIZED -> MraidState.DEFAULT MraidState.DEFAULT -> MraidState.HIDDEN else -> currentState } } private fun setAsClosed() { - if (currentState == MraidState.DEFAULT || currentState == MraidState.EXPANDED) { + if (currentState == MraidState.DEFAULT || + currentState == MraidState.EXPANDED || + currentState == MraidState.RESIZED) { mraidInteractor.notifyClosed() + ignoreOnPositionChange = false } updateCurrentStateOnClose() } diff --git a/publisher-sdk/src/main/java/com/criteo/publisher/adview/DummyMraidController.kt b/publisher-sdk/src/main/java/com/criteo/publisher/adview/DummyMraidController.kt index 4023d0fee..897d12a71 100644 --- a/publisher-sdk/src/main/java/com/criteo/publisher/adview/DummyMraidController.kt +++ b/publisher-sdk/src/main/java/com/criteo/publisher/adview/DummyMraidController.kt @@ -38,6 +38,18 @@ internal class DummyMraidController : MraidController { // no-op } + override fun doResize( + width: Double, + height: Double, + offsetX: Double, + offsetY: Double, + customClosePosition: MraidResizeCustomClosePosition, + allowOffscreen: Boolean, + onResult: (result: MraidResizeActionResult) -> Unit + ) { + // no-op + } + override fun onWebViewClientSet(client: WebViewClient) { // no-op } @@ -49,4 +61,8 @@ internal class DummyMraidController : MraidController { override fun onClosed() { // no-op } + + override fun resetToDefault() { + // no-op + } } diff --git a/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidController.kt b/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidController.kt index 7b70e5004..f2b6b87b0 100644 --- a/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidController.kt +++ b/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidController.kt @@ -34,6 +34,17 @@ interface MraidController { fun doClose(@MainThread onResult: (result: MraidActionResult) -> Unit) + @Suppress("LongParameterList") + fun doResize( + width: Double, + height: Double, + offsetX: Double, + offsetY: Double, + customClosePosition: MraidResizeCustomClosePosition, + allowOffscreen: Boolean, + @MainThread onResult: (result: MraidResizeActionResult) -> Unit + ) + fun onWebViewClientSet(client: WebViewClient) fun onConfigurationChange(newConfig: Configuration?) @@ -42,9 +53,22 @@ interface MraidController { * Notify when ad was closed by non-MRAID call (eg. by clicking on SDK provided button) */ fun onClosed() + + /** + * Brings back [AdWebViewClient] to default container if + * it absent + */ + fun resetToDefault() } sealed class MraidActionResult { object Success : MraidActionResult() data class Error(val message: String, val action: String) : MraidActionResult() } + +sealed class MraidResizeActionResult { + data class Success(val x: Int, val y: Int, val width: Int, val height: Int) : + MraidResizeActionResult() + + data class Error(val message: String, val action: String) : MraidResizeActionResult() +} diff --git a/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidInteractor.kt b/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidInteractor.kt index add853ced..d680a514d 100644 --- a/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidInteractor.kt +++ b/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidInteractor.kt @@ -42,6 +42,10 @@ internal class MraidInteractor(private val webView: WebView) { "notifyExpanded"() } + fun notifyResized() { + "notifyResized"() + } + fun notifyClosed() { "notifyClosed"() } diff --git a/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidMessageHandler.kt b/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidMessageHandler.kt index 70702fc25..94f00e4a0 100644 --- a/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidMessageHandler.kt +++ b/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidMessageHandler.kt @@ -61,4 +61,24 @@ class MraidMessageHandler { fun playVideo(url: String) { listener?.onPlayVideo(url) } + + @Suppress("LongParameterList") + @JavascriptInterface + fun resize( + width: Double, + height: Double, + offsetX: Double, + offsetY: Double, + customClosePosition: String, + allowOffscreen: Boolean + ) { + listener?.onResize( + width, + height, + offsetX, + offsetY, + customClosePosition.asCustomClosePosition(), + allowOffscreen + ) + } } diff --git a/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidMessageHandlerListener.kt b/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidMessageHandlerListener.kt index 26a16de2f..2af12f3aa 100644 --- a/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidMessageHandlerListener.kt +++ b/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidMessageHandlerListener.kt @@ -21,4 +21,14 @@ interface MraidMessageHandlerListener { fun onExpand(width: Double, height: Double) fun onClose() fun onPlayVideo(url: String) + + @Suppress("LongParameterList") + fun onResize( + width: Double, + height: Double, + offsetX: Double, + offsetY: Double, + customClosePosition: MraidResizeCustomClosePosition, + allowOffscreen: Boolean + ) } diff --git a/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidResizeCustomClosePosition.kt b/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidResizeCustomClosePosition.kt new file mode 100644 index 000000000..afc92460f --- /dev/null +++ b/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidResizeCustomClosePosition.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2020 Criteo + * + * 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.criteo.publisher.adview + +enum class MraidResizeCustomClosePosition(val value: String) { + TopLeft("top-left"), + TopRight("top-right"), + Center("center"), + BottomLeft("bottom-left"), + BottomRight("bottom-right"), + TopCenter("top-center"), + BottomCenter("bottom-center") +} + +internal fun String.asCustomClosePosition(): MraidResizeCustomClosePosition { + return MraidResizeCustomClosePosition.values().firstOrNull { it.value == this } + ?: MraidResizeCustomClosePosition.TopRight +} diff --git a/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidState.kt b/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidState.kt index 6bb00f231..73231174e 100644 --- a/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidState.kt +++ b/publisher-sdk/src/main/java/com/criteo/publisher/adview/MraidState.kt @@ -20,5 +20,6 @@ enum class MraidState(val stringValue: String) { LOADING("loading"), DEFAULT("default"), EXPANDED("expanded"), + RESIZED("resized"), HIDDEN("hidden"), } diff --git a/publisher-sdk/src/main/java/com/criteo/publisher/interstitial/CriteoInterstitialMraidController.kt b/publisher-sdk/src/main/java/com/criteo/publisher/interstitial/CriteoInterstitialMraidController.kt index 0d5e7c633..e2cd8d708 100644 --- a/publisher-sdk/src/main/java/com/criteo/publisher/interstitial/CriteoInterstitialMraidController.kt +++ b/publisher-sdk/src/main/java/com/criteo/publisher/interstitial/CriteoInterstitialMraidController.kt @@ -23,6 +23,8 @@ import com.criteo.publisher.adview.MraidActionResult import com.criteo.publisher.adview.MraidInteractor import com.criteo.publisher.adview.MraidMessageHandler import com.criteo.publisher.adview.MraidPlacementType +import com.criteo.publisher.adview.MraidResizeActionResult +import com.criteo.publisher.adview.MraidResizeCustomClosePosition import com.criteo.publisher.adview.MraidState import com.criteo.publisher.annotation.OpenForTesting import com.criteo.publisher.concurrent.RunOnUiThreadExecutor @@ -81,6 +83,25 @@ internal class CriteoInterstitialMraidController( } } + override fun doResize( + width: Double, + height: Double, + offsetX: Double, + offsetY: Double, + customClosePosition: MraidResizeCustomClosePosition, + allowOffscreen: Boolean, + onResult: (result: MraidResizeActionResult) -> Unit + ) { + runOnUiThreadExecutor.execute { + onResult(MraidResizeActionResult.Error("Interstitial ad can't be resized", "resize")) + } + } + + override fun resetToDefault() { + // nothing to do here + // interstitial AdWebView is never removed + } + private fun close(onResult: (result: MraidActionResult) -> Unit) { interstitialAdWebView.requestClose() onResult(MraidActionResult.Success) diff --git a/publisher-sdk/src/main/java/com/criteo/publisher/util/AndroidUtil.java b/publisher-sdk/src/main/java/com/criteo/publisher/util/AndroidUtil.java index 8f06abc2f..700634b91 100644 --- a/publisher-sdk/src/main/java/com/criteo/publisher/util/AndroidUtil.java +++ b/publisher-sdk/src/main/java/com/criteo/publisher/util/AndroidUtil.java @@ -60,14 +60,4 @@ public int getOrientation() { : Configuration.ORIENTATION_LANDSCAPE; } - /** - * Transform given distance in DP (density-independent pixel) into pixels. - * - * @param dp distance in DP - * @return equivalent in pixels - */ - public int dpToPixel(int dp) { - return (int) Math.ceil(dp * context.getResources().getDisplayMetrics().density); - } - } diff --git a/publisher-sdk/src/main/java/com/criteo/publisher/util/DeviceUtil.kt b/publisher-sdk/src/main/java/com/criteo/publisher/util/DeviceUtil.kt index 47982b9a1..0738e59bd 100644 --- a/publisher-sdk/src/main/java/com/criteo/publisher/util/DeviceUtil.kt +++ b/publisher-sdk/src/main/java/com/criteo/publisher/util/DeviceUtil.kt @@ -15,6 +15,7 @@ */ package com.criteo.publisher.util +import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.content.pm.PackageManager @@ -22,11 +23,14 @@ import android.graphics.Point import android.net.Uri import android.os.Build import android.util.DisplayMetrics +import android.view.View +import android.view.WindowInsets import android.view.WindowManager import com.criteo.publisher.annotation.OpenForTesting import com.criteo.publisher.model.AdSize import kotlin.math.min +@Suppress("TooManyFunctions") @OpenForTesting class DeviceUtil(private val context: Context) { @@ -51,8 +55,8 @@ class DeviceUtil(private val context: Context) { fun getCurrentScreenSize(): AdSize { val metrics = displayMetrics - val widthInDp = pxToDp(metrics.widthPixels) - val heightInDp = pxToDp(metrics.heightPixels) + val widthInDp = pixelToDp(metrics.widthPixels) + val heightInDp = pixelToDp(metrics.heightPixels) return AdSize(widthInDp, heightInDp) } @@ -60,7 +64,7 @@ class DeviceUtil(private val context: Context) { * * @return device screenSize including status and navigation bar */ - fun getRealSceeenSize(): AdSize { + fun getRealScreenSize(): AdSize { val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager val widthPx: Int val heightPx: Int @@ -74,7 +78,7 @@ class DeviceUtil(private val context: Context) { widthPx = point.x heightPx = point.y } - return AdSize(pxToDp(widthPx), pxToDp(heightPx)) + return AdSize(pixelToDp(widthPx), pixelToDp(heightPx)) } fun canSendSms(): Boolean { @@ -95,8 +99,24 @@ class DeviceUtil(private val context: Context) { return true } - fun pxToDp(pxValue: Int): Int { - return Math.round(pxValue / displayMetrics.density) + /** + * Transform given distance in pixels into DP (density-independent pixel). + * + * @param pixelValue distance in pixels + * @return equivalent in DP + */ + fun pixelToDp(pixelValue: Int): Int { + return Math.round(pixelValue / displayMetrics.density) + } + + /** + * Transform given distance in DP (density-independent pixel) into pixels. + * + * @param dp distance in DP + * @return equivalent in pixels + */ + fun dpToPixel(dp: Int): Int { + return Math.ceil((dp * context.resources.displayMetrics.density).toDouble()).toInt() } fun canHandleIntent(intent: Intent): Boolean { @@ -111,6 +131,32 @@ class DeviceUtil(private val context: Context) { return activities.isNotEmpty() } + /** + * @return top system bar height in pixels + */ + fun getTopSystemBarHeight(view: View): Int { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + view.rootWindowInsets?.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())?.top ?: 0 + } else { + view.rootWindowInsets?.systemWindowInsetTop ?: 0 + } + } else { + getStatusBarHeight(view.context) + } + } + + @SuppressLint("DiscouragedApi", "InternalInsetResource") + private fun getStatusBarHeight(context: Context): Int { + val resources = context.resources + var result = 0 + val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") + if (resourceId > 0) { + result = resources.getDimensionPixelSize(resourceId) + } + return result + } + private val displayMetrics: DisplayMetrics get() = context.resources.displayMetrics } diff --git a/publisher-sdk/src/main/java/com/criteo/publisher/util/ViewPositionTracker.kt b/publisher-sdk/src/main/java/com/criteo/publisher/util/ViewPositionTracker.kt index 6ad9af54a..5ec3154ec 100644 --- a/publisher-sdk/src/main/java/com/criteo/publisher/util/ViewPositionTracker.kt +++ b/publisher-sdk/src/main/java/com/criteo/publisher/util/ViewPositionTracker.kt @@ -117,13 +117,15 @@ internal class ViewPositionTracker( this.getLocationInWindow(outWindowLocation) val currentPosition = previousPosition - val newX = deviceUtil.pxToDp(outWindowLocation[0]) - val newY = deviceUtil.pxToDp(outWindowLocation[1]) - val newWidth = deviceUtil.pxToDp(width) - val newHeight = deviceUtil.pxToDp(height) + // we subtract top bar height and only work in coordinates between status and navigation bar + val newYInPixel = outWindowLocation[1] - deviceUtil.getTopSystemBarHeight(this) + val newXInDp = deviceUtil.pixelToDp(outWindowLocation[0]) + val newYInDp = deviceUtil.pixelToDp(newYInPixel) + val newWidthInDp = deviceUtil.pixelToDp(width) + val newHeightInDp = deviceUtil.pixelToDp(height) fun onPositionChange() { - val newPosition = Position(newX, newY, newWidth, newHeight) + val newPosition = Position(newXInDp, newYInDp, newWidthInDp, newHeightInDp) notifyPositionChange(newPosition) previousPosition = newPosition } @@ -132,10 +134,10 @@ internal class ViewPositionTracker( currentPosition == null -> { onPositionChange() } - newX != currentPosition.x || - newY != currentPosition.y || - newWidth != currentPosition.width || - newHeight != currentPosition.height -> { + newXInDp != currentPosition.x || + newYInDp != currentPosition.y || + newWidthInDp != currentPosition.width || + newHeightInDp != currentPosition.height -> { onPositionChange() } } diff --git a/publisher-sdk/src/main/res/values/ids.xml b/publisher-sdk/src/main/res/values/ids.xml index 3b2e9e953..e5bacfe03 100644 --- a/publisher-sdk/src/main/res/values/ids.xml +++ b/publisher-sdk/src/main/res/values/ids.xml @@ -2,4 +2,6 @@ + + \ No newline at end of file diff --git a/publisher-sdk/src/test/java/com/criteo/publisher/adview/CriteoMraidControllerTest.kt b/publisher-sdk/src/test/java/com/criteo/publisher/adview/CriteoMraidControllerTest.kt index 598ebe506..6054d02ee 100644 --- a/publisher-sdk/src/test/java/com/criteo/publisher/adview/CriteoMraidControllerTest.kt +++ b/publisher-sdk/src/test/java/com/criteo/publisher/adview/CriteoMraidControllerTest.kt @@ -94,6 +94,7 @@ class CriteoMraidControllerTest { private var placementType: MraidPlacementType = MraidPlacementType.INLINE private var actionResult: MraidActionResult = MraidActionResult.Success + private lateinit var resizeActionResult: MraidResizeActionResult private lateinit var criteoMraidController: CriteoMraidController @@ -123,12 +124,28 @@ class CriteoMraidControllerTest { override fun doClose(onResult: (result: MraidActionResult) -> Unit) { onResult(actionResult) } + + override fun doResize( + width: Double, + height: Double, + offsetX: Double, + offsetY: Double, + customClosePosition: MraidResizeCustomClosePosition, + allowOffscreen: Boolean, + onResult: (result: MraidResizeActionResult) -> Unit + ) { + onResult(resizeActionResult) + } + + override fun resetToDefault() { + // no-op + } } whenever(adWebView.resources).thenReturn(resources) whenever(resources.configuration).thenReturn(configuration) whenever(resources.displayMetrics).thenReturn(displayMetrics) - whenever(deviceUtil.getRealSceeenSize()).thenReturn(AdSize(100, 100)) + whenever(deviceUtil.getRealScreenSize()).thenReturn(AdSize(100, 100)) } @Test @@ -238,6 +255,42 @@ class CriteoMraidControllerTest { verify(mraidInteractor, never()).notifyClosed() } + @Test + fun onResizeWithSuccess_ShouldNotifyMraidInteractorAboutResizeAndChangeCurrentState() { + resizeActionResult = MraidResizeActionResult.Success(0, 0, 100, 100) + + criteoMraidController.onResize( + 100.0, + 100.0, + 0.0, + 0.0, + MraidResizeCustomClosePosition.Center, + true + ) + + verify(mraidInteractor).notifyResized() + verify(mraidInteractor).setCurrentPosition(0, 0, 100, 100) + assertThat(criteoMraidController.currentState).isEqualTo(MraidState.RESIZED) + } + + @Test + fun onResizeWithError_ShouldNotifyMraidInteractorAboutError() { + val errorResult = MraidResizeActionResult.Error("message", "action") + resizeActionResult = errorResult + + criteoMraidController.onResize( + 100.0, + 100.0, + 0.0, + 0.0, + MraidResizeCustomClosePosition.Center, + true + ) + + verify(mraidInteractor).notifyError(errorResult.message, errorResult.action) + verify(mraidInteractor, never()).notifyResized() + } + @Test fun onPageFinishedGivenMraidAd_ShouldInitializeDefaultValues() { whenever(deviceUtil.canSendSms()).thenReturn(true) diff --git a/publisher-sdk/src/test/java/com/criteo/publisher/adview/MraidInteractorTest.kt b/publisher-sdk/src/test/java/com/criteo/publisher/adview/MraidInteractorTest.kt index 08be8da4e..04550cd7a 100644 --- a/publisher-sdk/src/test/java/com/criteo/publisher/adview/MraidInteractorTest.kt +++ b/publisher-sdk/src/test/java/com/criteo/publisher/adview/MraidInteractorTest.kt @@ -123,6 +123,17 @@ class MraidInteractorTest { verifyNoMoreInteractions(webView) } + @Test + fun whenNotifyResized_ShouldEvaluateNotifyResizedOnMraidObject() { + mraidInteractor.notifyResized() + + verify(webView).evaluateJavascript( + "window.mraid.notifyResized()", + null + ) + verifyNoMoreInteractions(webView) + } + @Test fun whenNotifyClosed_ShouldEvaluateNotifyClosedOnMraidObject() { mraidInteractor.notifyClosed() diff --git a/publisher-sdk/src/test/java/com/criteo/publisher/adview/MraidMessageHandlerTest.kt b/publisher-sdk/src/test/java/com/criteo/publisher/adview/MraidMessageHandlerTest.kt index f73b479ae..584f1b0f2 100644 --- a/publisher-sdk/src/test/java/com/criteo/publisher/adview/MraidMessageHandlerTest.kt +++ b/publisher-sdk/src/test/java/com/criteo/publisher/adview/MraidMessageHandlerTest.kt @@ -134,4 +134,27 @@ class MraidMessageHandlerTest { fun whenPlayVideo_givenListenerIsNull_shouldNotThrow() { assertThatCode { mraidMessageHandler.playVideo("https://criteo.com/cat_video.mp4") }.doesNotThrowAnyException() } + + @Test + fun whenResize_givenListener_shouldCallOnResizeOnListener() { + mraidMessageHandler.setListener(listener) + + mraidMessageHandler.resize(100.0, 100.0, 0.0, 0.0, "center", true) + + verify(listener).onResize(100.0, 100.0, 0.0, 0.0, MraidResizeCustomClosePosition.Center, true) + } + + @Test + fun whenResize_givenListenerIsNull_shouldNotThrow() { + assertThatCode { + mraidMessageHandler.resize( + 100.0, + 100.0, + 0.0, + 0.0, + "center", + true + ) + }.doesNotThrowAnyException() + } } diff --git a/publisher-sdk/src/test/java/com/criteo/publisher/adview/MraidResizeCustomClosePositionTest.kt b/publisher-sdk/src/test/java/com/criteo/publisher/adview/MraidResizeCustomClosePositionTest.kt new file mode 100644 index 000000000..dfe4fbb27 --- /dev/null +++ b/publisher-sdk/src/test/java/com/criteo/publisher/adview/MraidResizeCustomClosePositionTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2020 Criteo + * + * 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.criteo.publisher.adview + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class MraidResizeCustomClosePositionTest { + + @Test + fun asCustomClosePosition_givenTopLeftString_shouldReturnTopLeftEnumValue() { + assertThat("top-left".asCustomClosePosition()).isEqualTo(MraidResizeCustomClosePosition.TopLeft) + } + + @Test + fun asCustomClosePosition_givenTopRightString_shouldReturnTopRightEnumValue() { + assertThat("top-right".asCustomClosePosition()).isEqualTo(MraidResizeCustomClosePosition.TopRight) + } + + @Test + fun asCustomClosePosition_givenCenterString_shouldReturnCenterEnumValue() { + assertThat("center".asCustomClosePosition()).isEqualTo(MraidResizeCustomClosePosition.Center) + } + + @Test + fun asCustomClosePosition_givenBottomLeftString_shouldReturnBottomLeftEnumValue() { + assertThat("bottom-left".asCustomClosePosition()).isEqualTo(MraidResizeCustomClosePosition.BottomLeft) + } + + @Test + fun asCustomClosePosition_givenBottomRightString_shouldReturnBottomRightEnumValue() { + assertThat("bottom-right".asCustomClosePosition()).isEqualTo(MraidResizeCustomClosePosition.BottomRight) + } + + @Test + fun asCustomClosePosition_givenTopCenterString_shouldReturnTopCenterEnumValue() { + assertThat("top-center".asCustomClosePosition()).isEqualTo(MraidResizeCustomClosePosition.TopCenter) + } + + @Test + fun asCustomClosePosition_givenBottomCenterString_shouldReturnBottomCenterEnumValue() { + assertThat("bottom-center".asCustomClosePosition()).isEqualTo(MraidResizeCustomClosePosition.BottomCenter) + } + + @Test + fun asCustomClosePosition_givenRandomString_shouldReturnTopRightEnumValue() { + assertThat("random".asCustomClosePosition()).isEqualTo(MraidResizeCustomClosePosition.TopRight) + } +} diff --git a/publisher-sdk/src/test/java/com/criteo/publisher/util/AndroidUtilUnitTest.kt b/publisher-sdk/src/test/java/com/criteo/publisher/util/AndroidUtilUnitTest.kt deleted file mode 100644 index 12b1d061e..000000000 --- a/publisher-sdk/src/test/java/com/criteo/publisher/util/AndroidUtilUnitTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2020 Criteo - * - * 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.criteo.publisher.util - -import android.content.Context -import android.util.DisplayMetrics -import org.assertj.core.api.Assertions.assertThat -import org.junit.Rule -import org.junit.Test -import org.mockito.Answers -import org.mockito.InjectMocks -import org.mockito.Mock -import org.mockito.junit.MockitoJUnit -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.stub - -class AndroidUtilUnitTest { - - @Rule - @JvmField - val mockitoRule = MockitoJUnit.rule() - - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private lateinit var context: Context - - @InjectMocks - private lateinit var androidUtil: AndroidUtil - - @Test - fun dpToPixel_GivenIntegerDensity_ReturnsScaledValue() { - givenDensity(2.0f) - - assertThat(androidUtil.dpToPixel(1)).isEqualTo(2) - assertThat(androidUtil.dpToPixel(42)).isEqualTo(84) - } - - @Test - fun dpToPixel_GivenFloatDensity_ReturnsScaledCeilValue() { - givenDensity(31.82f) - - assertThat(androidUtil.dpToPixel(1)).isEqualTo(32) - assertThat(androidUtil.dpToPixel(42)).isEqualTo(1337) - } - - private fun givenDensity(density: Float) { - val metrics = DisplayMetrics() - metrics.density = density - context.resources.stub { - on { displayMetrics } doReturn metrics - } - } -} diff --git a/publisher-sdk/src/test/java/com/criteo/publisher/util/DeviceUtilTest.kt b/publisher-sdk/src/test/java/com/criteo/publisher/util/DeviceUtilTest.kt index a4e00deae..98401852e 100644 --- a/publisher-sdk/src/test/java/com/criteo/publisher/util/DeviceUtilTest.kt +++ b/publisher-sdk/src/test/java/com/criteo/publisher/util/DeviceUtilTest.kt @@ -128,7 +128,7 @@ class DeviceUtilTest { null }.whenever(display).getRealSize(ArgumentMatchers.any()) - val (width, height) = deviceUtil.getRealSceeenSize() + val (width, height) = deviceUtil.getRealScreenSize() assertThat(width).isEqualTo(50) assertThat(height).isEqualTo(50) @@ -144,7 +144,7 @@ class DeviceUtilTest { whenever(bounds.width()).thenReturn(100) whenever(windowManager.maximumWindowMetrics).thenReturn(windowMetrics) - val (width, height) = deviceUtil.getRealSceeenSize() + val (width, height) = deviceUtil.getRealScreenSize() assertThat(width).isEqualTo(50) assertThat(height).isEqualTo(50) @@ -187,23 +187,39 @@ class DeviceUtilTest { } @Test - fun pxToDp_GivenNonZeroValue_ShouldReturnProperlyConvertedValue() { + fun pixelToDp_GivenNonZeroValue_ShouldReturnProperlyConvertedValue() { metrics.density = 2f - val result = deviceUtil.pxToDp(100) + val result = deviceUtil.pixelToDp(100) assertThat(result).isEqualTo(50) } @Test - fun pxToDp_GivenZero_ShouldReturnZero() { + fun pixelToDp_GivenZero_ShouldReturnZero() { metrics.density = 2f - val result = deviceUtil.pxToDp(0) + val result = deviceUtil.pixelToDp(0) assertThat(result).isEqualTo(0) } + @Test + fun dpToPixel_GivenIntegerDensity_ReturnsScaledValue() { + metrics.density = 2f + + assertThat(deviceUtil.dpToPixel(1)).isEqualTo(2) + assertThat(deviceUtil.dpToPixel(42)).isEqualTo(84) + } + + @Test + fun dpToPixel_GivenFloatDensity_ReturnsScaledCeilValue() { + metrics.density = 31.82f + + assertThat(deviceUtil.dpToPixel(1)).isEqualTo(32) + assertThat(deviceUtil.dpToPixel(42)).isEqualTo(1337) + } + @Test fun canHandleIntent_GivenAppToHandleIntentExistsAndApiLevel33_ShouldReturnTrue() { setSdkIntVersion(33) diff --git a/test-utils/build.gradle.kts b/test-utils/build.gradle.kts index 7ad1c7f12..01cfa76da 100644 --- a/test-utils/build.gradle.kts +++ b/test-utils/build.gradle.kts @@ -20,6 +20,7 @@ plugins { signing jacoco kotlin("android") + kotlin("kapt") id("kotlin-allopen") id("com.vanniktech.android.javadoc") version "0.3.0" id("io.gitlab.arturbosch.detekt") @@ -45,7 +46,8 @@ dependencies { implementation(Deps.JUnit.JUnit) implementation(Deps.Square.OkHttp.MockWebServer) implementation(Deps.Square.OkHttp.OkHttpTls) - compileOnly(Deps.Square.Moshi.Adapter) + implementation(Deps.Square.Moshi.Adapter) + kapt(Deps.Square.Moshi.Kapt) compileOnly(Deps.Mockito.Core) { because("Brings injected mock mechanism. Caller should provide its own Mockito deps.") diff --git a/test-utils/src/main/java/com/criteo/publisher/MraidPosition.kt b/test-utils/src/main/java/com/criteo/publisher/MraidPosition.kt new file mode 100644 index 000000000..c4acd95f1 --- /dev/null +++ b/test-utils/src/main/java/com/criteo/publisher/MraidPosition.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Criteo + * + * 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.criteo.publisher + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class MraidPosition(val x: Int, val y: Int, val width: Int, val height: Int)