From 4a4ffbaee9aa522c0745b6b334f7b5c045de4feb Mon Sep 17 00:00:00 2001 From: maureenorea-clores <93700127+maureenorea-clores@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:53:35 +0900 Subject: [PATCH] fix: image glitch and add url validations (RMCCX-7331, RMCCX-7345) --- .../data/customjson/ApplyClickableImage.kt | 14 ++++++- .../runtime/view/InAppMessageBaseView.kt | 1 - .../runtime/view/InAppMessageViewListener.kt | 4 +- .../customjson/ApplyClickableImageSpec.kt | 30 ++++++++++----- .../data/customjson/MessageMapperSpec.kt | 4 +- .../view/InAppMessageViewListenerSpec.kt | 38 +++++++++---------- 6 files changed, 57 insertions(+), 34 deletions(-) diff --git a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/ApplyClickableImage.kt b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/ApplyClickableImage.kt index 47126da6..a45b974d 100644 --- a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/ApplyClickableImage.kt +++ b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/ApplyClickableImage.kt @@ -14,9 +14,21 @@ internal fun UiMessage.applyCustomClickableImage(clickableImage: ClickableImage? ) } + fun String?.isValidUrlOrDeeplink(): Boolean { + if (this.isNullOrBlank() || this != this.trim()) { + return false + } + + return if (this.startsWith("http")) { + Regex("https://.*").matches(this) + } else { + Regex(".*://.*").matches(this) + } + } + @SuppressWarnings("ComplexCondition") if (clickableImage == null || - clickableImage.url.isNullOrEmpty() || + !clickableImage.url.isValidUrlOrDeeplink() || imageUrl.isNullOrEmpty() || !type.campaignTypeCanBeClickable() || isPushPrimer diff --git a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageBaseView.kt b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageBaseView.kt index 1337349f..29a595c0 100644 --- a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageBaseView.kt +++ b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageBaseView.kt @@ -170,7 +170,6 @@ internal open class InAppMessageBaseView(context: Context, attrs: AttributeSet?) // load the image then display the view this.visibility = GONE findViewById(R.id.message_image_view)?.let { imgView -> - imgView.setOnTouchListener(this.listener) if (!this.imageClickBehavior?.uri.isNullOrEmpty()) { imgView.setOnClickListener(this.listener) } diff --git a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageViewListener.kt b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageViewListener.kt index 09f10798..7b5ea1a1 100644 --- a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageViewListener.kt +++ b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageViewListener.kt @@ -43,7 +43,7 @@ internal class InAppMessageViewListener( /** * Callback When touch event occurred. Which will trigger to magnify message view content. */ - @SuppressLint("NewApi", "ClickableViewAccessibility") + @SuppressLint("NewApi") override fun onTouch(view: View, event: MotionEvent): Boolean { if (buildChecker.isAndroidQAndAbove()) { when (event.actionMasked) { @@ -52,7 +52,7 @@ internal class InAppMessageViewListener( MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> this.magnifier?.dismiss() else -> this.magnifier?.dismiss() } - // No need to performClick as it will be handled by system through setOnClickListener if it is clickable + return view.performClick() } return false } diff --git a/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/ApplyClickableImageSpec.kt b/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/ApplyClickableImageSpec.kt index deb1413f..a3ee88c3 100644 --- a/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/ApplyClickableImageSpec.kt +++ b/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/ApplyClickableImageSpec.kt @@ -21,29 +21,41 @@ class ApplyClickableImageSpec { } @Test - fun `should do nothing if url attribute does not exist or empty`() { + fun `should do nothing if url attribute is invalid`() { var uiMessage = message.applyCustomClickableImage(ClickableImage(), false) uiMessage shouldBeEqualTo message uiMessage = message.applyCustomClickableImage(ClickableImage(""), false) uiMessage shouldBeEqualTo message + + uiMessage = message.applyCustomClickableImage(ClickableImage("ogle.124dsefsd"), false) + uiMessage shouldBeEqualTo message + + uiMessage = message.applyCustomClickableImage(ClickableImage("http://test.com"), false) + uiMessage shouldBeEqualTo message + + uiMessage = message.applyCustomClickableImage(ClickableImage(" myapp://open"), false) + uiMessage shouldBeEqualTo message + + uiMessage = message.applyCustomClickableImage(ClickableImage("https://test.com "), false) + uiMessage shouldBeEqualTo message } @Test fun `should do nothing if imageUrl does not exist or empty`() { var uiMessage = message.copy(imageUrl = null) - .applyCustomClickableImage(ClickableImage("http://test.com"), false) + .applyCustomClickableImage(ClickableImage("https://test.com"), false) uiMessage.content?.onClick?.uri shouldBeEqualTo null uiMessage = message.copy(imageUrl = "") - .applyCustomClickableImage(ClickableImage("http://test.com"), false) + .applyCustomClickableImage(ClickableImage("https://test.com"), false) uiMessage.content?.onClick?.uri shouldBeEqualTo null } @Test fun `should do nothing if campaign layout isn't clickable`() { val uiMessage = message.copy(type = InAppMessageType.SLIDE.typeId) - .applyCustomClickableImage(ClickableImage("http://test.com"), false) + .applyCustomClickableImage(ClickableImage("https://test.com"), false) uiMessage.content?.onClick?.uri shouldBeEqualTo null } @@ -51,7 +63,7 @@ class ApplyClickableImageSpec { @Test fun `should do nothing if campaign is a PushPrimer`() { val uiMessage = message.copy(type = InAppMessageType.MODAL.typeId) - .applyCustomClickableImage(ClickableImage("http://test.com"), true) + .applyCustomClickableImage(ClickableImage("https://test.com"), true) uiMessage.content?.onClick?.uri shouldBeEqualTo null } @@ -59,9 +71,9 @@ class ApplyClickableImageSpec { @Test fun `should update content url to clickableImage url for null content data`() { val uiMessage = message.copy(type = InAppMessageType.MODAL.typeId) - .applyCustomClickableImage(ClickableImage("http://test.com"), false) + .applyCustomClickableImage(ClickableImage("https://test.com"), false) - uiMessage.content?.onClick?.uri shouldBeEqualTo "http://test.com" + uiMessage.content?.onClick?.uri shouldBeEqualTo "https://test.com" } @Test @@ -70,8 +82,8 @@ class ApplyClickableImageSpec { type = InAppMessageType.MODAL.typeId, content = Content(onClick = OnClickBehavior(3)), ) - .applyCustomClickableImage(ClickableImage("http://test.com"), false) + .applyCustomClickableImage(ClickableImage("myapp://open?param=value"), false) - uiMessage.content?.onClick?.uri shouldBeEqualTo "http://test.com" + uiMessage.content?.onClick?.uri shouldBeEqualTo "myapp://open?param=value" } } diff --git a/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/MessageMapperSpec.kt b/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/MessageMapperSpec.kt index 31244417..412aaa50 100644 --- a/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/MessageMapperSpec.kt +++ b/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/MessageMapperSpec.kt @@ -74,7 +74,7 @@ class MessageMapperSpec { val uiMessage = MessageMapper.mapFrom( TestDataHelper.createDummyMessage( messagePayload = payload, - customJson = JsonParser.parseString("""{"clickableImage": { "url": "http://test.com" }}""") + customJson = JsonParser.parseString("""{"clickableImage": { "url": "https://test.com" }}""") .asJsonObject, ), ) @@ -82,6 +82,6 @@ class MessageMapperSpec { uiMessage.content shouldNotBeEqualTo null uiMessage.content?.onClick shouldNotBeEqualTo null uiMessage.content?.onClick?.action shouldBeEqualTo ButtonActionType.REDIRECT.typeId - uiMessage.content?.onClick?.uri shouldBeEqualTo "http://test.com" + uiMessage.content?.onClick?.uri shouldBeEqualTo "https://test.com" } } diff --git a/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageViewListenerSpec.kt b/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageViewListenerSpec.kt index 56a11dc2..4adb58d6 100644 --- a/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageViewListenerSpec.kt +++ b/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageViewListenerSpec.kt @@ -138,78 +138,78 @@ class InAppMessageViewListenerOnTouchSpec : InAppMessageViewListenerSpec() { } @Test - fun `should return false on touch with action down and onClick listener`() { + fun `should return true on touch with action down and onClick listener`() { val listener = setupListener(MotionEvent.ACTION_DOWN) - listener.onTouch(mockView, mockMotionEvent).shouldBeFalse() + listener.onTouch(mockView, mockMotionEvent).shouldBeTrue() } @Test - fun `should return false on touch with action move and onClick listener`() { + fun `should return true on touch with action move and onClick listener`() { val listener = setupListener(MotionEvent.ACTION_MOVE) listener.magnifier = Mockito.mock(Magnifier::class.java) - listener.onTouch(mockView, mockMotionEvent).shouldBeFalse() + listener.onTouch(mockView, mockMotionEvent).shouldBeTrue() } @Test - fun `should return false on touch with action cancel and onClick listener`() { + fun `should return true on touch with action cancel and onClick listener`() { val listener = setupListener(MotionEvent.ACTION_CANCEL) listener.magnifier = Mockito.mock(Magnifier::class.java) - listener.onTouch(mockView, mockMotionEvent).shouldBeFalse() + listener.onTouch(mockView, mockMotionEvent).shouldBeTrue() } @Test - fun `should return false on touch with action up and onClick listener`() { + fun `should return true on touch with action up and onClick listener`() { val listener = setupListener(MotionEvent.ACTION_UP) listener.magnifier = Mockito.mock(Magnifier::class.java) - listener.onTouch(mockView, mockMotionEvent).shouldBeFalse() + listener.onTouch(mockView, mockMotionEvent).shouldBeTrue() } @Test - fun `should return false on touch with other action and onClick listener`() { + fun `should return true on touch with other action and onClick listener`() { val listener = setupListener(MotionEvent.ACTION_OUTSIDE) listener.magnifier = Mockito.mock(Magnifier::class.java) - listener.onTouch(mockView, mockMotionEvent).shouldBeFalse() + listener.onTouch(mockView, mockMotionEvent).shouldBeTrue() } @Test - fun `should return false on touch with action move, null magnifier, and onClick listener`() { + fun `should return true on touch with action move, null magnifier, and onClick listener`() { val listener = setupListener(MotionEvent.ACTION_MOVE) - listener.onTouch(mockView, mockMotionEvent).shouldBeFalse() + listener.onTouch(mockView, mockMotionEvent).shouldBeTrue() listener.magnifier.shouldBeNull() } @Test - fun `should return false on touch with action cancel, null magnifier, and onClick listener`() { + fun `should return true on touch with action cancel, null magnifier, and onClick listener`() { val listener = setupListener(MotionEvent.ACTION_CANCEL) - listener.onTouch(mockView, mockMotionEvent).shouldBeFalse() + listener.onTouch(mockView, mockMotionEvent).shouldBeTrue() listener.magnifier.shouldBeNull() } @Test - fun `should return false on touch with action up, null magnifier, and onClick listener`() { + fun `should return true on touch with action up, null magnifier, and onClick listener`() { val listener = setupListener(MotionEvent.ACTION_UP) - listener.onTouch(mockView, mockMotionEvent).shouldBeFalse() + listener.onTouch(mockView, mockMotionEvent).shouldBeTrue() listener.magnifier.shouldBeNull() } @Test - fun `should return false on touch with other action, null magnifier, and onClick listener`() { + fun `should return true on touch with other action, null magnifier, and onClick listener`() { val listener = setupListener(MotionEvent.ACTION_OUTSIDE) - listener.onTouch(mockView, mockMotionEvent).shouldBeFalse() + listener.onTouch(mockView, mockMotionEvent).shouldBeTrue() listener.magnifier.shouldBeNull() } @Test - fun `should return false on touch with action down`() { + fun `should return true on touch with action down`() { val listener = InAppMessageViewListener( MessageMapper.mapFrom( TestDataHelper.createDummyMessage(campaignId = "1", isTest = true),