Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
WIP
  • Loading branch information
romtsn committed Sep 25, 2024
commit d1b1ee9025d9b98aa92763220650c426f0bc3b97
10 changes: 9 additions & 1 deletion sentry-android-replay/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import io.gitlab.arturbosch.detekt.Detekt
import org.jetbrains.kotlin.config.KotlinCompilerVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask

plugins {
id("com.android.library")
Expand Down Expand Up @@ -27,7 +28,9 @@ android {

buildTypes {
getByName("debug")
getByName("release")
getByName("release") {
consumerProguardFiles("proguard-rules.pro")
}
}

kotlinOptions {
Expand Down Expand Up @@ -65,6 +68,7 @@ kotlin {
dependencies {
api(projects.sentry)

compileOnly("androidx.compose.ui:ui:1.4.0")
implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION))

// tests
Expand All @@ -83,3 +87,7 @@ tasks.withType<Detekt> {
// Target version of the generated JVM bytecode. It is used for type resolution.
jvmTarget = JavaVersion.VERSION_1_8.toString()
}

tasks.withType<KotlinCompilationTask<*>>().configureEach {
compilerOptions.freeCompilerArgs.add("-opt-in=androidx.compose.ui.ExperimentalComposeUiApi")
}
14 changes: 14 additions & 0 deletions sentry-android-replay/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# Uncomment this to preserve the line number information for
# debugging stack traces.
-keepattributes SourceFile,LineNumberTable

-dontwarn androidx.compose.ui.draw.PainterElement
-dontwarn androidx.compose.ui.draw.PainterModifierNodeElement
-dontwarn androidx.compose.ui.platform.AndroidComposeView
-dontwarn androidx.compose.ui.graphics.painter.Painter
#-dontwarn coil.compose.ContentPainterModifier
#-dontwarn coil3.compose.ContentPainterModifier
-keepclasseswithmembernames class * {
androidx.compose.ui.graphics.painter.Painter painter;
}
-keepnames class * extends androidx.compose.ui.graphics.painter.Painter
-keepnames class androidx.compose.ui.draw.PainterModifierNodeElement
-keepnames class androidx.compose.ui.draw.PainterElement
-keepnames class androidx.compose.ui.platform.AndroidComposeView
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import android.graphics.Rect
import android.graphics.RectF
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.util.Log
import android.view.PixelCopy
import android.view.View
import android.view.ViewGroup
Expand All @@ -24,10 +25,10 @@ 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
import io.sentry.android.replay.viewhierarchy.ComposeViewHierarchyNode
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode
Expand All @@ -38,6 +39,7 @@ import java.util.concurrent.ThreadFactory
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import kotlin.math.roundToInt
import kotlin.system.measureNanoTime

@TargetApi(26)
internal class ScreenshotRecorder(
Expand Down Expand Up @@ -115,6 +117,7 @@ internal class ScreenshotRecorder(
return@request
}

// TODO: handle animations with heuristics (e.g. if we fall under this condition 2 times in a row, we should capture)
if (contentChanged.get()) {
options.logger.log(INFO, "Failed to determine view hierarchy, not capturing")
bitmap.recycle()
Expand All @@ -132,9 +135,9 @@ internal class ScreenshotRecorder(
node.visibleRect ?: return@traverse false

// TODO: investigate why it returns true on RN when it shouldn't
// if (viewHierarchy.isObscured(node)) {
// return@traverse true
// }
if (viewHierarchy.isObscured(node)) {
return@traverse true
}

val (visibleRects, color) = when (node) {
is ImageViewHierarchyNode -> {
Expand All @@ -143,7 +146,7 @@ internal class ScreenshotRecorder(
}

is TextViewHierarchyNode -> {
val textColor = node.layout.dominantTextColor
val textColor = node.layout?.dominantTextColor
?: node.dominantColor
?: Color.BLACK
node.layout.getVisibleRects(
Expand Down Expand Up @@ -256,6 +259,17 @@ internal class ScreenshotRecorder(
return
}

var isCompose: Boolean
val time = measureNanoTime {
isCompose = ComposeViewHierarchyNode.fromView(this, parentNode, options)
}
if (isCompose) {
Log.e("TIME", String.format("%.2f", time / 1_000_000.0) + "ms")
// if it's a compose view, we can skip the children as they are already traversed in
// the ComposeViewHierarchyNode.fromView method
return
}

if (this.childCount == 0) {
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.view.View
import android.widget.TextView
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.text.TextLayoutResult
import io.sentry.SentryOptions
import io.sentry.android.replay.R
import java.lang.NullPointerException
import kotlin.math.roundToInt

/**
* Adapted copy of AccessibilityNodeInfo from https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/View.java;l=10718
Expand Down Expand Up @@ -65,7 +70,7 @@ internal fun Drawable?.isRedactable(): Boolean {
}
}

internal fun Layout?.getVisibleRects(globalRect: Rect, paddingLeft: Int, paddingTop: Int): List<Rect> {
internal fun TextLayout?.getVisibleRects(globalRect: Rect, paddingLeft: Int, paddingTop: Int): List<Rect> {
if (this == null) {
return listOf(globalRect)
}
Expand Down Expand Up @@ -105,32 +110,66 @@ internal val TextView.totalPaddingTopSafe: Int
}

/**
* 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.
* Converts an [Int] ARGB color to an opaque color by setting the alpha channel to 255.
*/
internal val Layout?.dominantTextColor: Int? get() {
this ?: return null
internal fun Int.toOpaque() = this or 0xFF000000.toInt()

if (text !is Spanned) return null
interface TextLayout {
val lineCount: Int
/**
* 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.
*/
val dominantTextColor: Int?
fun getPrimaryHorizontal(offset: Int): Float
fun getEllipsisCount(line: Int): Int
fun getLineVisibleEnd(line: Int): Int
fun getLineTop(line: Int): Int
fun getLineBottom(line: Int): Int
fun getLineStart(line: Int): Int
}

val spans = (text as Spanned).getSpans(0, text.length, ForegroundColorSpan::class.java)
class AndroidTextLayout(private val layout: Layout) : TextLayout {
override val lineCount: Int get() = layout.lineCount
override val dominantTextColor: Int? get() {
if (layout.text !is Spanned) return null

// 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
val spans = (layout.text as Spanned).getSpans(0, layout.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 = (layout.text as Spanned).getSpanStart(span)
val spanEnd = (layout.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?.toOpaque()
}
return dominantColor
override fun getPrimaryHorizontal(offset: Int): Float = layout.getPrimaryHorizontal(offset)
override fun getEllipsisCount(line: Int): Int = layout.getEllipsisCount(line)
override fun getLineVisibleEnd(line: Int): Int = layout.getLineVisibleEnd(line)
override fun getLineTop(line: Int): Int = layout.getLineTop(line)
override fun getLineBottom(line: Int): Int = layout.getLineBottom(line)
override fun getLineStart(line: Int): Int = layout.getLineStart(line)
}

class ComposeTextLayout(internal val layout: TextLayoutResult) : TextLayout {
override val lineCount: Int get() = layout.lineCount
override val dominantTextColor: Int get() = layout.layoutInput.style.color.toArgb().toOpaque()
override fun getPrimaryHorizontal(offset: Int): Float = layout.getHorizontalPosition(offset, usePrimaryDirection = true)
override fun getEllipsisCount(line: Int): Int = if (layout.isLineEllipsized(line)) 1 else 0
override fun getLineVisibleEnd(line: Int): Int = layout.getLineEnd(line, visibleEnd = true)
override fun getLineTop(line: Int): Int = layout.getLineTop(line).roundToInt()
override fun getLineBottom(line: Int): Int = layout.getLineBottom(line).roundToInt()
override fun getLineStart(line: Int): Int = layout.getLineStart(line)
}
Loading