@@ -2,8 +2,10 @@ package io.sentry.android.replay.viewhierarchy
2
2
3
3
import android.app.Activity
4
4
import android.content.Context
5
+ import android.graphics.Bitmap
5
6
import android.graphics.Canvas
6
7
import android.graphics.Color
8
+ import android.graphics.drawable.BitmapDrawable
7
9
import android.graphics.drawable.Drawable
8
10
import android.os.Bundle
9
11
import android.os.Looper
@@ -14,18 +16,22 @@ import android.widget.LinearLayout.LayoutParams
14
16
import android.widget.RadioButton
15
17
import android.widget.TextView
16
18
import androidx.test.ext.junit.runners.AndroidJUnit4
19
+ import androidx.test.core.app.ApplicationProvider
17
20
import io.sentry.SentryOptions
21
+ import io.sentry.android.replay.R
18
22
import io.sentry.android.replay.maskAllImages
19
23
import io.sentry.android.replay.maskAllText
20
24
import io.sentry.android.replay.sentryReplayMask
21
25
import io.sentry.android.replay.sentryReplayUnmask
26
+ import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.GenericViewHierarchyNode
22
27
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode
23
28
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode
24
29
import kotlin.test.BeforeTest
25
30
import kotlin.test.Test
26
31
import kotlin.test.assertFalse
27
32
import kotlin.test.assertTrue
28
33
import org.junit.runner.RunWith
34
+ import org.robolectric.Robolectric
29
35
import org.robolectric.Robolectric.buildActivity
30
36
import org.robolectric.Shadows.shadowOf
31
37
import org.robolectric.annotation.Config
@@ -225,6 +231,142 @@ class MaskingOptionsTest {
225
231
assertTrue(textNode.shouldMask)
226
232
assertTrue(imageNode.shouldMask)
227
233
}
234
+
235
+ @Test
236
+ fun `views are masked when class name matches mask package pattern` () {
237
+ val textView = TextView (ApplicationProvider .getApplicationContext()).apply {
238
+ text = " Test text"
239
+ visibility = View .VISIBLE
240
+ measure(View .MeasureSpec .makeMeasureSpec(100 , View .MeasureSpec .EXACTLY ),
241
+ View .MeasureSpec .makeMeasureSpec(50 , View .MeasureSpec .EXACTLY ))
242
+ layout(0 , 0 , 100 , 50 )
243
+ }
244
+ val imageView = ImageView (ApplicationProvider .getApplicationContext()).apply {
245
+ visibility = View .VISIBLE
246
+ measure(View .MeasureSpec .makeMeasureSpec(100 , View .MeasureSpec .EXACTLY ),
247
+ View .MeasureSpec .makeMeasureSpec(50 , View .MeasureSpec .EXACTLY ))
248
+ layout(0 , 0 , 100 , 50 )
249
+ }
250
+
251
+ // Create a bitmap drawable that should be considered maskable
252
+ val bitmap = Bitmap .createBitmap(50 , 50 , Bitmap .Config .ARGB_8888 )
253
+ val context = ApplicationProvider .getApplicationContext<Context >()
254
+ val drawable = BitmapDrawable (context.resources, bitmap)
255
+ imageView.setImageDrawable(drawable)
256
+
257
+ val options =
258
+ SentryOptions ().apply {
259
+ sessionReplay.addMaskPackage(" android.widget.*" )
260
+ }
261
+
262
+ val textNode = ViewHierarchyNode .fromView(textView, null , 0 , options)
263
+ val imageNode = ViewHierarchyNode .fromView(imageView, null , 0 , options)
264
+
265
+ // Both views from android.widget.* should be masked
266
+ assertTrue(textNode.shouldMask)
267
+ assertTrue(imageNode.shouldMask)
268
+ }
269
+
270
+ @Test
271
+ fun `views are unmasked when class name matches unmask package pattern` () {
272
+ val textView = TextView (ApplicationProvider .getApplicationContext())
273
+ val imageView = ImageView (ApplicationProvider .getApplicationContext())
274
+
275
+ // Create a bitmap drawable that should be considered maskable
276
+ val bitmap = Bitmap .createBitmap(50 , 50 , Bitmap .Config .ARGB_8888 )
277
+ val context = ApplicationProvider .getApplicationContext<Context >()
278
+ val drawable = BitmapDrawable (context.resources, bitmap)
279
+ imageView.setImageDrawable(drawable)
280
+
281
+ val options =
282
+ SentryOptions ().apply {
283
+ sessionReplay.addMaskPackage(" android.*" )
284
+ sessionReplay.addUnmaskPackage(" android.widget.*" )
285
+ }
286
+
287
+ val textNode = ViewHierarchyNode .fromView(textView, null , 0 , options)
288
+ val imageNode = ViewHierarchyNode .fromView(imageView, null , 0 , options)
289
+
290
+ // Both views should be unmasked due to more specific unmask pattern
291
+ assertFalse(textNode.shouldMask)
292
+ assertFalse(imageNode.shouldMask)
293
+ }
294
+
295
+ @Test
296
+ fun `views are masked with specific package patterns` () {
297
+ val textView = TextView (ApplicationProvider .getApplicationContext()).apply {
298
+ text = " Test text"
299
+ visibility = View .VISIBLE
300
+ measure(View .MeasureSpec .makeMeasureSpec(100 , View .MeasureSpec .EXACTLY ),
301
+ View .MeasureSpec .makeMeasureSpec(50 , View .MeasureSpec .EXACTLY ))
302
+ layout(0 , 0 , 100 , 50 )
303
+ }
304
+ val linearLayout = LinearLayout (ApplicationProvider .getApplicationContext()).apply {
305
+ visibility = View .VISIBLE
306
+ measure(View .MeasureSpec .makeMeasureSpec(100 , View .MeasureSpec .EXACTLY ),
307
+ View .MeasureSpec .makeMeasureSpec(50 , View .MeasureSpec .EXACTLY ))
308
+ layout(0 , 0 , 100 , 50 )
309
+ }
310
+
311
+ val options =
312
+ SentryOptions ().apply {
313
+ sessionReplay.addMaskPackage(" android.widget.TextView" )
314
+ }
315
+
316
+ val textNode = ViewHierarchyNode .fromView(textView, null , 0 , options)
317
+ val layoutNode = ViewHierarchyNode .fromView(linearLayout, null , 0 , options)
318
+
319
+ // TextView should be masked by exact match
320
+ assertTrue(textNode.shouldMask)
321
+ // LinearLayout should not be masked
322
+ assertFalse(layoutNode.shouldMask)
323
+ }
324
+
325
+ @Test
326
+ fun `package patterns work with multiple patterns` () {
327
+ val textView = TextView (ApplicationProvider .getApplicationContext()).apply {
328
+ text = " Test text"
329
+ visibility = View .VISIBLE
330
+ measure(View .MeasureSpec .makeMeasureSpec(100 , View .MeasureSpec .EXACTLY ),
331
+ View .MeasureSpec .makeMeasureSpec(50 , View .MeasureSpec .EXACTLY ))
332
+ layout(0 , 0 , 100 , 50 )
333
+ }
334
+ val imageView = ImageView (ApplicationProvider .getApplicationContext()).apply {
335
+ visibility = View .VISIBLE
336
+ measure(View .MeasureSpec .makeMeasureSpec(100 , View .MeasureSpec .EXACTLY ),
337
+ View .MeasureSpec .makeMeasureSpec(50 , View .MeasureSpec .EXACTLY ))
338
+ layout(0 , 0 , 100 , 50 )
339
+ }
340
+ val linearLayout = LinearLayout (ApplicationProvider .getApplicationContext()).apply {
341
+ visibility = View .VISIBLE
342
+ measure(View .MeasureSpec .makeMeasureSpec(100 , View .MeasureSpec .EXACTLY ),
343
+ View .MeasureSpec .makeMeasureSpec(50 , View .MeasureSpec .EXACTLY ))
344
+ layout(0 , 0 , 100 , 50 )
345
+ }
346
+
347
+ // Create a bitmap drawable that should be considered maskable
348
+ val bitmap = Bitmap .createBitmap(50 , 50 , Bitmap .Config .ARGB_8888 )
349
+ val context = ApplicationProvider .getApplicationContext<Context >()
350
+ val drawable = BitmapDrawable (context.resources, bitmap)
351
+ imageView.setImageDrawable(drawable)
352
+
353
+ val options =
354
+ SentryOptions ().apply {
355
+ sessionReplay.addMaskPackage(" android.widget.TextView" )
356
+ sessionReplay.addMaskPackage(" android.widget.ImageView" )
357
+ sessionReplay.addUnmaskPackage(" android.widget.LinearLayout" )
358
+ }
359
+
360
+ val textNode = ViewHierarchyNode .fromView(textView, null , 0 , options)
361
+ val imageNode = ViewHierarchyNode .fromView(imageView, null , 0 , options)
362
+ val layoutNode = ViewHierarchyNode .fromView(linearLayout, null , 0 , options)
363
+
364
+ // TextView and ImageView should be masked by exact matches
365
+ assertTrue(textNode.shouldMask)
366
+ assertTrue(imageNode.shouldMask)
367
+ // LinearLayout should not be masked (not in any mask patterns)
368
+ assertFalse(layoutNode.shouldMask)
369
+ }
228
370
}
229
371
230
372
private class CustomView (context : Context ) : View(context) {
0 commit comments