Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
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
1 change: 1 addition & 0 deletions sentry-android-replay/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ android {

composeOptions {
kotlinCompilerExtensionVersion = Config.androidComposeCompilerVersion
useLiveLiterals = false
}

buildTypes {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ 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.ViewTreeObserver
Expand Down Expand Up @@ -101,7 +100,6 @@ internal class ScreenshotRecorder(
Bitmap.Config.ARGB_8888
)

val timeStart = System.nanoTime()
// postAtFrontOfQueue to ensure the view hierarchy and bitmap are ase close in-sync as possible
mainLooperHandler.post {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
package io.sentry.android.replay.util

import android.graphics.Rect
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorProducer
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.text.TextLayoutResult
import kotlin.math.roundToInt
Expand Down Expand Up @@ -78,13 +80,6 @@ internal fun Painter.isMaskable(): Boolean {
!className.contains("Brush")
}

/**
* Converts from [androidx.compose.ui.geometry.Rect] to [android.graphics.Rect].
*/
internal fun androidx.compose.ui.geometry.Rect.toRect(): Rect {
return Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())
}

internal data class TextAttributes(val color: Color?, val hasFillModifier: Boolean)

/**
Expand Down Expand Up @@ -125,3 +120,87 @@ internal fun LayoutNode.findTextAttributes(): TextAttributes {
}
return TextAttributes(color, hasFillModifier)
}

/**
* Returns the smaller of the given values. If any value is NaN, returns NaN. Preferred over
* `kotlin.comparisons.minOf()` for 4 arguments as it avoids allocating an array because of the
* varargs.
*/
private inline fun fastMinOf(a: Float, b: Float, c: Float, d: Float): Float {
return minOf(a, minOf(b, minOf(c, d)))
}

/**
* Returns the largest of the given values. If any value is NaN, returns NaN. Preferred over
* `kotlin.comparisons.maxOf()` for 4 arguments as it avoids allocating an array because of the
* varargs.
*/
private inline fun fastMaxOf(a: Float, b: Float, c: Float, d: Float): Float {
return maxOf(a, maxOf(b, maxOf(c, d)))
}

/**
* Returns this float value clamped in the inclusive range defined by [minimumValue] and
* [maximumValue]. Unlike [Float.coerceIn], the range is not validated: the caller must ensure that
* [minimumValue] is less than [maximumValue].
*/
private inline fun Float.fastCoerceIn(minimumValue: Float, maximumValue: Float) =
this.fastCoerceAtLeast(minimumValue).fastCoerceAtMost(maximumValue)

/** Ensures that this value is not less than the specified [minimumValue]. */
private inline fun Float.fastCoerceAtLeast(minimumValue: Float): Float {
return if (this < minimumValue) minimumValue else this
}

/** Ensures that this value is not greater than the specified [maximumValue]. */
private inline fun Float.fastCoerceAtMost(maximumValue: Float): Float {
return if (this > maximumValue) maximumValue else this
}

/**
* A faster copy of https://github.com/androidx/androidx/blob/fc7df0dd68466ac3bb16b1c79b7a73dd0bfdd4c1/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt#L187
*
* Since we traverse the tree from the root, we don't need to find it again from the leaf node and
* just pass it as an argument.
*
* @return boundaries of this layout relative to the window's origin.
*/
internal fun LayoutCoordinates.boundsInWindow(root: LayoutCoordinates?): Rect {
root ?: return Rect()

val rootWidth = root.size.width.toFloat()
val rootHeight = root.size.height.toFloat()

val bounds = root.localBoundingBoxOf(this)
val boundsLeft = bounds.left.fastCoerceIn(0f, rootWidth)
val boundsTop = bounds.top.fastCoerceIn(0f, rootHeight)
val boundsRight = bounds.right.fastCoerceIn(0f, rootWidth)
val boundsBottom = bounds.bottom.fastCoerceIn(0f, rootHeight)

if (boundsLeft == boundsRight || boundsTop == boundsBottom) {
return Rect()
}

val topLeft = root.localToWindow(Offset(boundsLeft, boundsTop))
val topRight = root.localToWindow(Offset(boundsRight, boundsTop))
val bottomRight = root.localToWindow(Offset(boundsRight, boundsBottom))
val bottomLeft = root.localToWindow(Offset(boundsLeft, boundsBottom))

val topLeftX = topLeft.x
val topRightX = topRight.x
val bottomLeftX = bottomLeft.x
val bottomRightX = bottomRight.x

val left = fastMinOf(topLeftX, topRightX, bottomLeftX, bottomRightX)
val right = fastMaxOf(topLeftX, topRightX, bottomLeftX, bottomRightX)

val topLeftY = topLeft.y
val topRightY = topRight.y
val bottomLeftY = bottomLeft.y
val bottomRightY = bottomRight.y

val top = fastMinOf(topLeftY, topRightY, bottomLeftY, bottomRightY)
val bottom = fastMaxOf(topLeftY, topRightY, bottomLeftY, bottomRightY)

return Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import android.annotation.TargetApi
import android.view.View
import androidx.compose.ui.graphics.isUnspecified
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.findRootCoordinates
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.node.Owner
Expand All @@ -19,11 +20,11 @@ import io.sentry.SentryOptions
import io.sentry.SentryReplayOptions
import io.sentry.android.replay.SentryReplayModifiers
import io.sentry.android.replay.util.ComposeTextLayout
import io.sentry.android.replay.util.boundsInWindow
import io.sentry.android.replay.util.findPainter
import io.sentry.android.replay.util.findTextAttributes
import io.sentry.android.replay.util.isMaskable
import io.sentry.android.replay.util.toOpaque
import io.sentry.android.replay.util.toRect
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.GenericViewHierarchyNode
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode
Expand Down Expand Up @@ -62,19 +63,26 @@ internal object ComposeViewHierarchyNode {
return options.experimental.sessionReplay.maskViewClasses.contains(className)
}

private var _rootCoordinates: LayoutCoordinates? = null

private fun fromComposeNode(
node: LayoutNode,
parent: ViewHierarchyNode?,
distance: Int,
isComposeRoot: Boolean,
options: SentryOptions
): ViewHierarchyNode? {
val isInTree = node.isPlaced && node.isAttached
if (!isInTree) {
return null
}

if (isComposeRoot) {
_rootCoordinates = node.coordinates.findRootCoordinates()
}

val semantics = node.collapsedSemantics
val visibleRect = node.coordinates.boundsInWindow().toRect()
val visibleRect = node.coordinates.boundsInWindow(_rootCoordinates)
val isVisible = !node.outerCoordinator.isTransparent() &&
(semantics == null || !semantics.contains(SemanticsProperties.InvisibleToUser)) &&
visibleRect.height() > 0 && visibleRect.width() > 0
Expand Down Expand Up @@ -167,7 +175,7 @@ internal object ComposeViewHierarchyNode {

try {
val rootNode = (view as? Owner)?.root ?: return false
rootNode.traverse(parent, options)
rootNode.traverse(parent, isComposeRoot = true, options)
} catch (e: Throwable) {
options.logger.log(
SentryLevel.ERROR,
Expand All @@ -185,7 +193,7 @@ internal object ComposeViewHierarchyNode {
return true
}

private fun LayoutNode.traverse(parentNode: ViewHierarchyNode, options: SentryOptions) {
private fun LayoutNode.traverse(parentNode: ViewHierarchyNode, isComposeRoot: Boolean, options: SentryOptions) {
val children = this.children
if (children.isEmpty()) {
return
Expand All @@ -194,10 +202,10 @@ internal object ComposeViewHierarchyNode {
val childNodes = ArrayList<ViewHierarchyNode>(children.size)
for (index in children.indices) {
val child = children[index]
val childNode = fromComposeNode(child, parentNode, index, options)
val childNode = fromComposeNode(child, parentNode, index, isComposeRoot, options)
if (childNode != null) {
childNodes.add(childNode)
child.traverse(childNode, options)
child.traverse(childNode, isComposeRoot = false, options)
}
}
parentNode.children = childNodes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ fun Github(
.fillMaxSize()
) {
Image(
painter = painterResource(IR.drawable.logo_pocket_casts),
painter = painterResource(IR.drawable.sentry_glyph),
contentDescription = "LOGO",
colorFilter = ColorFilter.tint(Color.Black),
modifier = Modifier.padding(vertical = 16.dp)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="29dp"
android:viewportWidth="50"
android:viewportHeight="44">
<path
android:pathData="M29,2.26a4.67,4.67 0,0 0,-8 0L14.42,13.53A32.21,32.21 0,0 1,32.17 40.19H27.55A27.68,27.68 0,0 0,12.09 17.47L6,28a15.92,15.92 0,0 1,9.23 12.17H4.62A0.76,0.76 0,0 1,4 39.06l2.94,-5a10.74,10.74 0,0 0,-3.36 -1.9l-2.91,5a4.54,4.54 0,0 0,1.69 6.24A4.66,4.66 0,0 0,4.62 44H19.15a19.4,19.4 0,0 0,-8 -17.31l2.31,-4A23.87,23.87 0,0 1,23.76 44H36.07a35.88,35.88 0,0 0,-16.41 -31.8l4.67,-8a0.77,0.77 0,0 1,1.05 -0.27c0.53,0.29 20.29,34.77 20.66,35.17a0.76,0.76 0,0 1,-0.68 1.13H40.6q0.09,1.91 0,3.81h4.78A4.59,4.59 0,0 0,50 39.43a4.49,4.49 0,0 0,-0.62 -2.28Z"
android:fillColor="#362d59"/>
</vector>
2 changes: 1 addition & 1 deletion sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -2718,8 +2718,8 @@ public final class io/sentry/SentryReplayOptions {
public static final field TEXT_VIEW_CLASS_NAME Ljava/lang/String;
public static final field VIDEO_VIEW_CLASS_NAME Ljava/lang/String;
public static final field WEB_VIEW_CLASS_NAME Ljava/lang/String;
public fun <init> ()V
public fun <init> (Ljava/lang/Double;Ljava/lang/Double;)V
public fun <init> (Z)V
public fun addMaskViewClass (Ljava/lang/String;)V
public fun addUnmaskViewClass (Ljava/lang/String;)V
public fun getErrorReplayDuration ()J
Expand Down
18 changes: 10 additions & 8 deletions sentry/src/main/java/io/sentry/SentryReplayOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,16 @@ public enum SentryReplayQuality {
/** The maximum duration of a full session replay, defaults to 1h. */
private long sessionDuration = 60 * 60 * 1000L;

public SentryReplayOptions() {
setMaskAllText(true);
setMaskAllImages(true);
maskViewClasses.add(WEB_VIEW_CLASS_NAME);
maskViewClasses.add(VIDEO_VIEW_CLASS_NAME);
maskViewClasses.add(ANDROIDX_MEDIA_VIEW_CLASS_NAME);
maskViewClasses.add(EXOPLAYER_CLASS_NAME);
maskViewClasses.add(EXOPLAYER_STYLED_CLASS_NAME);
public SentryReplayOptions(final boolean empty) {
if (!empty) {
setMaskAllText(true);
setMaskAllImages(true);
maskViewClasses.add(WEB_VIEW_CLASS_NAME);
maskViewClasses.add(VIDEO_VIEW_CLASS_NAME);
maskViewClasses.add(ANDROIDX_MEDIA_VIEW_CLASS_NAME);
maskViewClasses.add(EXOPLAYER_CLASS_NAME);
maskViewClasses.add(EXOPLAYER_STYLED_CLASS_NAME);
}
}

public SentryReplayOptions(
Expand Down