Skip to content

Commit

Permalink
[SR] Detect dominant color for TextViews with Spans (#3682)
Browse files Browse the repository at this point in the history
  • Loading branch information
romtsn authored Sep 11, 2024
1 parent 8586d1f commit 731ae5a
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Fixes

- Avoid stopping appStartProfiler after application creation ([#3630](https://github.com/getsentry/sentry-java/pull/3630))
- Session Replay: Correctly detect dominant color for `TextView`s with Spans ([#3682](https://github.com/getsentry/sentry-java/pull/3682))

*Breaking changes*:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import io.sentry.SentryLevel.WARNING
import io.sentry.SentryOptions
import io.sentry.SentryReplayOptions
import io.sentry.android.replay.util.MainLooperHandler
import io.sentry.android.replay.util.dominantTextColor
import io.sentry.android.replay.util.getVisibleRects
import io.sentry.android.replay.util.gracefullyShutdown
import io.sentry.android.replay.util.submitSafely
Expand Down Expand Up @@ -142,13 +143,14 @@ internal class ScreenshotRecorder(
}

is TextViewHierarchyNode -> {
// TODO: find a way to get the correct text color for RN
// TODO: now it always returns black
val textColor = node.layout.dominantTextColor
?: node.dominantColor
?: Color.BLACK
node.layout.getVisibleRects(
node.visibleRect,
node.paddingLeft,
node.paddingTop
) to (node.dominantColor ?: Color.BLACK)
) to textColor
}

else -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import android.graphics.drawable.VectorDrawable
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.text.Layout
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.view.View
import android.widget.TextView
import java.lang.NullPointerException
Expand Down Expand Up @@ -101,3 +103,34 @@ internal val TextView.totalPaddingTopSafe: Int
} catch (e: NullPointerException) {
extendedPaddingTop
}

/**
* Returns the dominant text color of the layout by looking at the [ForegroundColorSpan] spans if
* this text is a [Spanned] text. If the text is not a [Spanned] text or there are no spans, it
* returns null.
*/
internal val Layout?.dominantTextColor: Int? get() {
this ?: return null

if (text !is Spanned) return null

val spans = (text as Spanned).getSpans(0, text.length, ForegroundColorSpan::class.java)

// determine the dominant color by the span with the longest range
var longestSpan = Int.MIN_VALUE
var dominantColor: Int? = null
for (span in spans) {
val spanStart = (text as Spanned).getSpanStart(span)
val spanEnd = (text as Spanned).getSpanEnd(span)
if (spanStart == -1 || spanEnd == -1) {
// the span is not attached
continue
}
val spanLength = spanEnd - spanStart
if (spanLength > longestSpan) {
longestSpan = spanLength
dominantColor = span.foregroundColor
}
}
return dominantColor
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package io.sentry.android.replay.util

import android.app.Activity
import android.graphics.Color
import android.os.Bundle
import android.os.Looper
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.widget.LinearLayout
import android.widget.LinearLayout.LayoutParams
import android.widget.TextView
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.sentry.SentryOptions
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode
import org.junit.runner.RunWith
import org.robolectric.Robolectric.buildActivity
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue

@RunWith(AndroidJUnit4::class)
@Config(sdk = [30])
class TextViewDominantColorTest {

@Test
fun `when no spans, returns currentTextColor`() {
val controller = buildActivity(TextViewActivity::class.java, null).setup()
controller.create().start().resume()

TextViewActivity.textView?.setTextColor(Color.WHITE)

val node = ViewHierarchyNode.fromView(TextViewActivity.textView!!, null, 0, SentryOptions())
assertTrue(node is TextViewHierarchyNode)
assertNull(node.layout.dominantTextColor)
}

@Test
fun `when has a foreground color span, returns its color`() {
val controller = buildActivity(TextViewActivity::class.java, null).setup()
controller.create().start().resume()

val text = "Hello, World!"
TextViewActivity.textView?.text = SpannableString(text).apply {
setSpan(ForegroundColorSpan(Color.RED), 0, text.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
}
TextViewActivity.textView?.setTextColor(Color.WHITE)
TextViewActivity.textView?.requestLayout()

shadowOf(Looper.getMainLooper()).idle()

val node = ViewHierarchyNode.fromView(TextViewActivity.textView!!, null, 0, SentryOptions())
assertTrue(node is TextViewHierarchyNode)
assertEquals(Color.RED, node.layout.dominantTextColor)
}

@Test
fun `when has multiple foreground color spans, returns color of the longest span`() {
val controller = buildActivity(TextViewActivity::class.java, null).setup()
controller.create().start().resume()

val text = "Hello, World!"
TextViewActivity.textView?.text = SpannableString(text).apply {
setSpan(ForegroundColorSpan(Color.RED), 0, 5, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
setSpan(ForegroundColorSpan(Color.BLACK), 6, text.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
}
TextViewActivity.textView?.setTextColor(Color.WHITE)
TextViewActivity.textView?.requestLayout()

shadowOf(Looper.getMainLooper()).idle()

val node = ViewHierarchyNode.fromView(TextViewActivity.textView!!, null, 0, SentryOptions())
assertTrue(node is TextViewHierarchyNode)
assertEquals(Color.BLACK, node.layout.dominantTextColor)
}
}

private class TextViewActivity : Activity() {

companion object {
var textView: TextView? = null
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val linearLayout = LinearLayout(this).apply {
setBackgroundColor(android.R.color.white)
orientation = LinearLayout.VERTICAL
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
}

textView = TextView(this).apply {
text = "Hello, World!"
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
}
linearLayout.addView(textView)

setContentView(linearLayout)
}
}

0 comments on commit 731ae5a

Please sign in to comment.