Skip to content

Commit e3623e5

Browse files
authored
[SR] Redaction fixes part 1
2 parents 11437d4 + 7b01de3 commit e3623e5

File tree

4 files changed

+403
-133
lines changed

4 files changed

+403
-133
lines changed

sentry-android-replay/api/sentry-android-replay.api

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -148,34 +148,48 @@ public final class io/sentry/android/replay/video/SimpleMp4FrameMuxer : io/sentr
148148
public fun start (Landroid/media/MediaFormat;)V
149149
}
150150

151-
public final class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode {
151+
public abstract class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode {
152152
public static final field Companion Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode$Companion;
153-
public fun <init> (FFIIZLjava/lang/Integer;Landroid/graphics/Rect;)V
154-
public synthetic fun <init> (FFIIZLjava/lang/Integer;Landroid/graphics/Rect;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
155-
public final fun component1 ()F
156-
public final fun component2 ()F
157-
public final fun component3 ()I
158-
public final fun component4 ()I
159-
public final fun component5 ()Z
160-
public final fun component6 ()Ljava/lang/Integer;
161-
public final fun component7 ()Landroid/graphics/Rect;
162-
public final fun copy (FFIIZLjava/lang/Integer;Landroid/graphics/Rect;)Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;
163-
public static synthetic fun copy$default (Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;FFIIZLjava/lang/Integer;Landroid/graphics/Rect;ILjava/lang/Object;)Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;
164-
public fun equals (Ljava/lang/Object;)Z
153+
public synthetic fun <init> (FFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
154+
public synthetic fun <init> (FFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
165155
public final fun getChildren ()Ljava/util/List;
166-
public final fun getDominantColor ()Ljava/lang/Integer;
156+
public final fun getDistance ()I
157+
public final fun getElevation ()F
167158
public final fun getHeight ()I
159+
public final fun getParent ()Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;
168160
public final fun getShouldRedact ()Z
169161
public final fun getVisibleRect ()Landroid/graphics/Rect;
170162
public final fun getWidth ()I
171163
public final fun getX ()F
172164
public final fun getY ()F
173-
public fun hashCode ()I
165+
public final fun isImportantForContentCapture ()Z
166+
public final fun isObscured (Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;)Z
167+
public final fun isVisible ()Z
174168
public final fun setChildren (Ljava/util/List;)V
175-
public fun toString ()Ljava/lang/String;
169+
public final fun setImportantForContentCapture (Z)V
170+
public final fun traverse (Lkotlin/jvm/functions/Function1;)V
176171
}
177172

178173
public final class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode$Companion {
179-
public final fun fromView (Landroid/view/View;Lio/sentry/SentryOptions;)Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;
174+
public final fun fromView (Landroid/view/View;Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ILio/sentry/SentryOptions;)Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;
175+
}
176+
177+
public final class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode$GenericViewHierarchyNode : io/sentry/android/replay/viewhierarchy/ViewHierarchyNode {
178+
public fun <init> (FFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;)V
179+
public synthetic fun <init> (FFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
180+
}
181+
182+
public final class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode$ImageViewHierarchyNode : io/sentry/android/replay/viewhierarchy/ViewHierarchyNode {
183+
public fun <init> (FFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;)V
184+
public synthetic fun <init> (FFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
185+
}
186+
187+
public final class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode$TextViewHierarchyNode : io/sentry/android/replay/viewhierarchy/ViewHierarchyNode {
188+
public fun <init> (Landroid/text/Layout;Ljava/lang/Integer;IIFFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;)V
189+
public synthetic fun <init> (Landroid/text/Layout;Ljava/lang/Integer;IIFFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
190+
public final fun getDominantColor ()Ljava/lang/Integer;
191+
public final fun getLayout ()Landroid/text/Layout;
192+
public final fun getPaddingLeft ()I
193+
public final fun getPaddingTop ()I
180194
}
181195

sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.content.Context
55
import android.graphics.Bitmap
66
import android.graphics.Bitmap.Config.ARGB_8888
77
import android.graphics.Canvas
8+
import android.graphics.Color
89
import android.graphics.Matrix
910
import android.graphics.Paint
1011
import android.graphics.Point
@@ -25,7 +26,10 @@ import io.sentry.SentryLevel.INFO
2526
import io.sentry.SentryLevel.WARNING
2627
import io.sentry.SentryOptions
2728
import io.sentry.SentryReplayOptions
29+
import io.sentry.android.replay.util.getVisibleRects
2830
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode
31+
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode
32+
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode
2933
import java.io.File
3034
import java.lang.ref.WeakReference
3135
import java.util.concurrent.atomic.AtomicBoolean
@@ -122,21 +126,42 @@ internal class ScreenshotRecorder(
122126
)
123127
val canvas = Canvas(scaledBitmap)
124128
canvas.setMatrix(prescaledMatrix)
125-
viewHierarchy.traverse {
126-
if (it.shouldRedact && (it.width > 0 && it.height > 0)) {
127-
it.visibleRect ?: return@traverse
128-
129-
// TODO: check for view type rather than rely on absence of dominantColor here
130-
val color = if (it.dominantColor == null) {
131-
singlePixelBitmapCanvas.drawBitmap(bitmap, it.visibleRect, Rect(0, 0, 1, 1), null)
132-
singlePixelBitmap.getPixel(0, 0)
133-
} else {
134-
it.dominantColor
129+
viewHierarchy.traverse { node ->
130+
if (node.shouldRedact && (node.width > 0 && node.height > 0)) {
131+
node.visibleRect ?: return@traverse false
132+
133+
if (viewHierarchy.isObscured(node)) {
134+
return@traverse true
135+
}
136+
137+
val (visibleRects, color) = when (node) {
138+
is ImageViewHierarchyNode -> {
139+
singlePixelBitmapCanvas.drawBitmap(
140+
bitmap,
141+
node.visibleRect,
142+
Rect(0, 0, 1, 1),
143+
null
144+
)
145+
listOf(node.visibleRect) to singlePixelBitmap.getPixel(0, 0)
146+
}
147+
is TextViewHierarchyNode -> {
148+
node.layout.getVisibleRects(
149+
node.visibleRect,
150+
node.paddingLeft,
151+
node.paddingTop
152+
) to (node.dominantColor ?: Color.BLACK)
153+
}
154+
else -> {
155+
listOf(node.visibleRect) to Color.BLACK
156+
}
135157
}
136158

137159
maskingPaint.setColor(color)
138-
canvas.drawRoundRect(RectF(it.visibleRect), 10f, 10f, maskingPaint)
160+
visibleRects.forEach { rect ->
161+
canvas.drawRoundRect(RectF(rect), 10f, 10f, maskingPaint)
162+
}
139163
}
164+
return@traverse true
140165
}
141166
}
142167

@@ -165,7 +190,7 @@ internal class ScreenshotRecorder(
165190
return
166191
}
167192

168-
val rootNode = ViewHierarchyNode.fromView(root, options)
193+
val rootNode = ViewHierarchyNode.fromView(root, null, 0, options)
169194
root.traverse(rootNode)
170195
pendingViewHierarchy.set(rootNode)
171196

@@ -206,15 +231,6 @@ internal class ScreenshotRecorder(
206231
thread.quitSafely()
207232
}
208233

209-
private fun ViewHierarchyNode.traverse(callback: (ViewHierarchyNode) -> Unit) {
210-
callback(this)
211-
if (this.children != null) {
212-
this.children!!.forEach {
213-
it.traverse(callback)
214-
}
215-
}
216-
}
217-
218234
private fun View.traverse(parentNode: ViewHierarchyNode) {
219235
if (this !is ViewGroup) {
220236
return
@@ -228,7 +244,7 @@ internal class ScreenshotRecorder(
228244
for (i in 0 until childCount) {
229245
val child = getChildAt(i)
230246
if (child != null) {
231-
val childNode = ViewHierarchyNode.fromView(child, options)
247+
val childNode = ViewHierarchyNode.fromView(child, parentNode, indexOfChild(child), options)
232248
childNodes.add(childNode)
233249
child.traverse(childNode)
234250
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package io.sentry.android.replay.util
2+
3+
import android.annotation.SuppressLint
4+
import android.annotation.TargetApi
5+
import android.graphics.Point
6+
import android.graphics.Rect
7+
import android.graphics.drawable.BitmapDrawable
8+
import android.graphics.drawable.ColorDrawable
9+
import android.graphics.drawable.Drawable
10+
import android.graphics.drawable.GradientDrawable
11+
import android.graphics.drawable.InsetDrawable
12+
import android.graphics.drawable.VectorDrawable
13+
import android.os.Build.VERSION
14+
import android.os.Build.VERSION_CODES
15+
import android.text.Layout
16+
import android.view.View
17+
18+
/**
19+
* Adapted copy of AccessibilityNodeInfo from https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/View.java;l=10718
20+
*/
21+
internal fun View.isVisibleToUser(): Pair<Boolean, Rect?> {
22+
if (isAttachedToWindow) {
23+
// Attached to invisible window means this view is not visible.
24+
if (windowVisibility != View.VISIBLE) {
25+
return false to null
26+
}
27+
// An invisible predecessor or one with alpha zero means
28+
// that this view is not visible to the user.
29+
var current: Any = this
30+
while (current is View) {
31+
val view = current
32+
val transitionAlpha = if (VERSION.SDK_INT >= VERSION_CODES.Q) view.transitionAlpha else 1f
33+
// We have attach info so this view is attached and there is no
34+
// need to check whether we reach to ViewRootImpl on the way up.
35+
if (view.alpha <= 0 || transitionAlpha <= 0 || view.visibility != View.VISIBLE) {
36+
return false to null
37+
}
38+
current = view.parent
39+
}
40+
// Check if the view is entirely covered by its predecessors.
41+
val rect = Rect()
42+
val offset = Point()
43+
val isVisible = getGlobalVisibleRect(rect, offset)
44+
return isVisible to rect
45+
}
46+
return false to null
47+
}
48+
49+
@SuppressLint("ObsoleteSdkInt")
50+
@TargetApi(21)
51+
internal fun Drawable?.isRedactable(): Boolean {
52+
// TODO: maybe find a way how to check if the drawable is coming from the apk or loaded from network
53+
// TODO: otherwise maybe check for the bitmap size and don't redact those that take a lot of height (e.g. a background of a whatsapp chat)
54+
return when (this) {
55+
is InsetDrawable, is ColorDrawable, is VectorDrawable, is GradientDrawable -> false
56+
is BitmapDrawable -> !bitmap.isRecycled && bitmap.height > 10 && bitmap.width > 10
57+
else -> true
58+
}
59+
}
60+
61+
internal fun Layout?.getVisibleRects(globalRect: Rect, paddingLeft: Int, paddingTop: Int): List<Rect> {
62+
if (this == null) {
63+
return listOf(globalRect)
64+
}
65+
66+
val rects = mutableListOf<Rect>()
67+
for (i in 0 until lineCount) {
68+
val lineStart = getPrimaryHorizontal(getLineStart(i)).toInt()
69+
val ellipsisCount = getEllipsisCount(i)
70+
val lineEnd = getPrimaryHorizontal(getLineVisibleEnd(i) - ellipsisCount + if (ellipsisCount > 0) 1 else 0).toInt()
71+
val lineTop = getLineTop(i)
72+
val lineBottom = getLineBottom(i)
73+
val rect = Rect()
74+
rect.left = globalRect.left + paddingLeft + lineStart
75+
rect.right = rect.left + (lineEnd - lineStart)
76+
rect.top = globalRect.top + paddingTop + lineTop
77+
rect.bottom = rect.top + (lineBottom - lineTop)
78+
79+
rects += rect
80+
}
81+
return rects
82+
}

0 commit comments

Comments
 (0)