From 0ab3bb34d63db186be39273cff42b96b1a983fe4 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 9 Oct 2024 18:28:14 +0200 Subject: [PATCH] [SR] Change terminology from redact/ignore to mask/unmask (#3741) * WIP * Compose works * Custom redaction works for Compose * Formatting * Clean up * Test * Add tests * Changelog * Change terminology from redact/ignore to mask/unmask * Changelog * [SR] Mask web and video views (#3775) * Replace logo with sentry * Add missing proguard rules * formatting * Faster boundsInWindow for compose * api dump * Dont use liveliterals * Remove redundant test * Increase timeout in failing test --- CHANGELOG.md | 16 +- .../android/core/ManifestMetadataReader.java | 8 +- .../core/ManifestMetadataReaderTest.kt | 14 +- .../sentry/android/core/SentryAndroidTest.kt | 2 +- .../api/sentry-android-replay.api | 18 +- sentry-android-replay/proguard-rules.pro | 12 +- .../android/replay/ModifierExtensions.kt | 8 +- .../android/replay/ScreenshotRecorder.kt | 4 +- .../android/replay/SessionReplayOptions.kt | 18 +- .../sentry/android/replay/ViewExtensions.kt | 12 +- .../io/sentry/android/replay/util/Nodes.kt | 10 +- .../io/sentry/android/replay/util/Views.kt | 4 +- .../replay/video/SimpleVideoEncoder.kt | 2 +- .../viewhierarchy/ComposeViewHierarchyNode.kt | 24 +- .../replay/viewhierarchy/ViewHierarchyNode.kt | 42 +-- ...nsTest.kt => ComposeMaskingOptionsTest.kt} | 96 +++--- .../viewhierarchy/MaskingOptionsTest.kt | 278 ++++++++++++++++++ .../viewhierarchy/RedactionOptionsTest.kt | 278 ------------------ .../src/main/AndroidManifest.xml | 3 +- .../android/compose/ComposeActivity.kt | 4 +- sentry/api/sentry.api | 17 +- .../java/io/sentry/SentryReplayOptions.java | 75 +++-- 22 files changed, 485 insertions(+), 460 deletions(-) rename sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/{ComposeRedactionOptionsTest.kt => ComposeMaskingOptionsTest.kt} (67%) create mode 100644 sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/MaskingOptionsTest.kt delete mode 100644 sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/RedactionOptionsTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index fc4c2d1009..ce2bd2cf4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,15 +6,16 @@ - Add support for `feedback` envelope header item type ([#3687](https://github.com/getsentry/sentry-java/pull/3687)) - Add breadcrumb.origin field ([#3727](https://github.com/getsentry/sentry-java/pull/3727)) -- Session Replay: Add options to selectively redact/ignore views from being captured. The following options are available: ([#3689](https://github.com/getsentry/sentry-java/pull/3689)) - - `android:tag="sentry-redact|sentry-ignore"` in XML or `view.setTag("sentry-redact|sentry-ignore")` in code tags - - if you already have a tag set for a view, you can set a tag by id: `` in XML or `view.setTag(io.sentry.android.replay.R.id.sentry_privacy, "redact|ignore")` in code - - `view.sentryReplayRedact()` or `view.sentryReplayIgnore()` extension functions - - redact/ignore `View`s of a certain type by adding fully-qualified classname to one of the lists `options.experimental.sessionReplay.addRedactViewClass()` or `options.experimental.sessionReplay.addIgnoreViewClass()`. Note, that all of the view subclasses/subtypes will be redacted/ignored as well - - For example, (this is already a default behavior) to redact all `TextView`s and their subclasses (`RadioButton`, `EditText`, etc.): `options.experimental.sessionReplay.addRedactViewClass("android.widget.TextView")` +- Session Replay: Add options to selectively mask/unmask views captured in replay. The following options are available: ([#3689](https://github.com/getsentry/sentry-java/pull/3689)) + - `android:tag="sentry-mask|sentry-unmask"` in XML or `view.setTag("sentry-mask|sentry-unmask")` in code tags + - if you already have a tag set for a view, you can set a tag by id: `` in XML or `view.setTag(io.sentry.android.replay.R.id.sentry_privacy, "mask|unmask")` in code + - `view.sentryReplayMask()` or `view.sentryReplayUnmask()` extension functions + - mask/unmask `View`s of a certain type by adding fully-qualified classname to one of the lists `options.experimental.sessionReplay.addMaskViewClass()` or `options.experimental.sessionReplay.addUnmaskViewClass()`. Note, that all of the view subclasses/subtypes will be masked/unmasked as well + - For example, (this is already a default behavior) to mask all `TextView`s and their subclasses (`RadioButton`, `EditText`, etc.): `options.experimental.sessionReplay.addMaskViewClass("android.widget.TextView")` - If you're using code obfuscation, adjust your proguard-rules accordingly, so your custom view class name is not minified - Session Replay: Support Jetpack Compose masking ([#3739](https://github.com/getsentry/sentry-java/pull/3739)) - - To selectively mask/unmask @Composables, use `Modifier.sentryReplayRedact()` and `Modifier.sentryReplayIgnore()` modifiers + - To selectively mask/unmask @Composables, use `Modifier.sentryReplayMask()` and `Modifier.sentryReplayUnmask()` modifiers +- Session Replay: Mask `WebView`, `VideoView` and `androidx.media3.ui.PlayerView` by default ([#3775](https://github.com/getsentry/sentry-java/pull/3775)) ### Fixes @@ -29,6 +30,7 @@ - `options.experimental.sessionReplay.errorSampleRate` was renamed to `options.experimental.sessionReplay.onErrorSampleRate` ([#3637](https://github.com/getsentry/sentry-java/pull/3637)) - Manifest option `io.sentry.session-replay.error-sample-rate` was renamed to `io.sentry.session-replay.on-error-sample-rate` ([#3637](https://github.com/getsentry/sentry-java/pull/3637)) +- Change `redactAllText` and `redactAllImages` to `maskAllText` and `maskAllImages` ([#3741](https://github.com/getsentry/sentry-java/pull/3741)) ## 7.14.0 diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index fc66c9d6ee..96d54d98de 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -108,9 +108,9 @@ final class ManifestMetadataReader { static final String REPLAYS_ERROR_SAMPLE_RATE = "io.sentry.session-replay.on-error-sample-rate"; - static final String REPLAYS_REDACT_ALL_TEXT = "io.sentry.session-replay.redact-all-text"; + static final String REPLAYS_MASK_ALL_TEXT = "io.sentry.session-replay.mask-all-text"; - static final String REPLAYS_REDACT_ALL_IMAGES = "io.sentry.session-replay.redact-all-images"; + static final String REPLAYS_MASK_ALL_IMAGES = "io.sentry.session-replay.mask-all-images"; /** ManifestMetadataReader ctor */ private ManifestMetadataReader() {} @@ -409,12 +409,12 @@ static void applyMetadata( options .getExperimental() .getSessionReplay() - .setRedactAllText(readBool(metadata, logger, REPLAYS_REDACT_ALL_TEXT, true)); + .setMaskAllText(readBool(metadata, logger, REPLAYS_MASK_ALL_TEXT, true)); options .getExperimental() .getSessionReplay() - .setRedactAllImages(readBool(metadata, logger, REPLAYS_REDACT_ALL_IMAGES, true)); + .setMaskAllImages(readBool(metadata, logger, REPLAYS_MASK_ALL_IMAGES, true)); } options diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index 8a86fcb2c5..e068af7b1c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -1465,21 +1465,21 @@ class ManifestMetadataReaderTest { } @Test - fun `applyMetadata reads session replay redact flags to options`() { + fun `applyMetadata reads session replay mask flags to options`() { // Arrange - val bundle = bundleOf(ManifestMetadataReader.REPLAYS_REDACT_ALL_TEXT to false, ManifestMetadataReader.REPLAYS_REDACT_ALL_IMAGES to false) + val bundle = bundleOf(ManifestMetadataReader.REPLAYS_MASK_ALL_TEXT to false, ManifestMetadataReader.REPLAYS_MASK_ALL_IMAGES to false) val context = fixture.getContext(metaData = bundle) // Act ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertTrue(fixture.options.experimental.sessionReplay.ignoreViewClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME)) - assertTrue(fixture.options.experimental.sessionReplay.ignoreViewClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME)) + assertTrue(fixture.options.experimental.sessionReplay.unmaskViewClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME)) + assertTrue(fixture.options.experimental.sessionReplay.unmaskViewClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME)) } @Test - fun `applyMetadata reads session replay redact flags to options and keeps default if not found`() { + fun `applyMetadata reads session replay mask flags to options and keeps default if not found`() { // Arrange val context = fixture.getContext() @@ -1487,7 +1487,7 @@ class ManifestMetadataReaderTest { ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertTrue(fixture.options.experimental.sessionReplay.redactViewClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME)) - assertTrue(fixture.options.experimental.sessionReplay.redactViewClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME)) + assertTrue(fixture.options.experimental.sessionReplay.maskViewClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME)) + assertTrue(fixture.options.experimental.sessionReplay.maskViewClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME)) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index 6ba69ffdcb..d75e0f88a2 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -440,7 +440,7 @@ class SentryAndroidTest { .untilTrue(asserted) // assert that persisted values have changed - options.executorService.close(5000L) // finalizes all enqueued persisting tasks + options.executorService.close(10000L) // finalizes all enqueued persisting tasks assertEquals( "TestActivity", PersistingScopeObserver.read(options, TRANSACTION_FILENAME, String::class.java) diff --git a/sentry-android-replay/api/sentry-android-replay.api b/sentry-android-replay/api/sentry-android-replay.api index 4b4c59b9a2..a08fb1dd98 100644 --- a/sentry-android-replay/api/sentry-android-replay.api +++ b/sentry-android-replay/api/sentry-android-replay.api @@ -29,8 +29,8 @@ public final class io/sentry/android/replay/GeneratedVideo { } public final class io/sentry/android/replay/ModifierExtensionsKt { - public static final fun sentryReplayIgnore (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; - public static final fun sentryReplayRedact (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; + public static final fun sentryReplayMask (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; + public static final fun sentryReplayUnmask (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; } public abstract interface class io/sentry/android/replay/Recorder : java/io/Closeable { @@ -120,15 +120,15 @@ public final class io/sentry/android/replay/SentryReplayModifiers { } public final class io/sentry/android/replay/SessionReplayOptionsKt { - public static final fun getRedactAllImages (Lio/sentry/SentryReplayOptions;)Z - public static final fun getRedactAllText (Lio/sentry/SentryReplayOptions;)Z - public static final fun setRedactAllImages (Lio/sentry/SentryReplayOptions;Z)V - public static final fun setRedactAllText (Lio/sentry/SentryReplayOptions;Z)V + public static final fun getMaskAllImages (Lio/sentry/SentryReplayOptions;)Z + public static final fun getMaskAllText (Lio/sentry/SentryReplayOptions;)Z + public static final fun setMaskAllImages (Lio/sentry/SentryReplayOptions;Z)V + public static final fun setMaskAllText (Lio/sentry/SentryReplayOptions;Z)V } public final class io/sentry/android/replay/ViewExtensionsKt { - public static final fun sentryReplayIgnore (Landroid/view/View;)V - public static final fun sentryReplayRedact (Landroid/view/View;)V + public static final fun sentryReplayMask (Landroid/view/View;)V + public static final fun sentryReplayUnmask (Landroid/view/View;)V } public final class io/sentry/android/replay/gestures/GestureRecorder : io/sentry/android/replay/OnRootViewsChangedListener { @@ -230,7 +230,7 @@ public abstract class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode { public final fun getElevation ()F public final fun getHeight ()I public final fun getParent ()Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode; - public final fun getShouldRedact ()Z + public final fun getShouldMask ()Z public final fun getVisibleRect ()Landroid/graphics/Rect; public final fun getWidth ()I public final fun getX ()F diff --git a/sentry-android-replay/proguard-rules.pro b/sentry-android-replay/proguard-rules.pro index 445c89b526..378c0964f8 100644 --- a/sentry-android-replay/proguard-rules.pro +++ b/sentry-android-replay/proguard-rules.pro @@ -2,13 +2,13 @@ # debugging stack traces. -keepattributes SourceFile,LineNumberTable -# Rules to detect Images/Icons and redact them +# Rules to detect Images/Icons and mask them -dontwarn androidx.compose.ui.graphics.painter.Painter -keepnames class * extends androidx.compose.ui.graphics.painter.Painter -keepclasseswithmembernames class * { androidx.compose.ui.graphics.painter.Painter painter; } -# Rules to detect Text colors and if they have Modifier.fillMaxWidth to later redact them +# Rules to detect Text colors and if they have Modifier.fillMaxWidth to later mask them -dontwarn androidx.compose.ui.graphics.ColorProducer -dontwarn androidx.compose.foundation.layout.FillElement -keepnames class androidx.compose.foundation.layout.FillElement @@ -18,3 +18,11 @@ # Rules to detect a compose view to parse its hierarchy -dontwarn androidx.compose.ui.platform.AndroidComposeView -keepnames class androidx.compose.ui.platform.AndroidComposeView +# Rules to detect a media player view to later mask it +-dontwarn androidx.media3.ui.PlayerView +-keepnames class androidx.media3.ui.PlayerView +# Rules to detect a ExoPlayer view to later mask it +-dontwarn com.google.android.exoplayer2.ui.PlayerView +-keepnames class com.google.android.exoplayer2.ui.PlayerView +-dontwarn com.google.android.exoplayer2.ui.StyledPlayerView +-keepnames class com.google.android.exoplayer2.ui.StyledPlayerView diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ModifierExtensions.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ModifierExtensions.kt index b1b119a89c..b5d5222388 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ModifierExtensions.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ModifierExtensions.kt @@ -12,18 +12,18 @@ public object SentryReplayModifiers { ) } -public fun Modifier.sentryReplayRedact(): Modifier { +public fun Modifier.sentryReplayMask(): Modifier { return semantics( properties = { - this[SentryPrivacy] = "redact" + this[SentryPrivacy] = "mask" } ) } -public fun Modifier.sentryReplayIgnore(): Modifier { +public fun Modifier.sentryReplayUnmask(): Modifier { return semantics( properties = { - this[SentryPrivacy] = "ignore" + this[SentryPrivacy] = "unmask" } ) } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt index 5b779babe0..8f823fa17c 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt @@ -124,11 +124,11 @@ internal class ScreenshotRecorder( val viewHierarchy = ViewHierarchyNode.fromView(root, null, 0, options) root.traverse(viewHierarchy, options) - recorder.submitSafely(options, "screenshot_recorder.redact") { + recorder.submitSafely(options, "screenshot_recorder.mask") { val canvas = Canvas(bitmap) canvas.setMatrix(prescaledMatrix) viewHierarchy.traverse { node -> - if (node.shouldRedact && (node.width > 0 && node.height > 0)) { + if (node.shouldMask && (node.width > 0 && node.height > 0)) { node.visibleRect ?: return@traverse false // TODO: investigate why it returns true on RN when it shouldn't diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/SessionReplayOptions.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/SessionReplayOptions.kt index e3e6605a96..fb5105565b 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/SessionReplayOptions.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/SessionReplayOptions.kt @@ -2,30 +2,30 @@ package io.sentry.android.replay import io.sentry.SentryReplayOptions -// since we don't have getters for redactAllText and redactAllImages, they won't be accessible as +// since we don't have getters for maskAllText and maskAllimages, they won't be accessible as // properties in Kotlin, therefore we create these extensions where a getter is dummy, but a setter // delegates to the corresponding method in SentryReplayOptions /** - * Redact all text content. Draws a rectangle of text bounds with text color on top. By default - * only views extending TextView are redacted. + * Mask all text content. Draws a rectangle of text bounds with text color on top. By default + * only views extending TextView are masked. * *

Default is enabled. */ -var SentryReplayOptions.redactAllText: Boolean +var SentryReplayOptions.maskAllText: Boolean @Deprecated("Getter is unsupported.", level = DeprecationLevel.ERROR) get() = error("Getter not supported") - set(value) = setRedactAllText(value) + set(value) = setMaskAllText(value) /** - * Redact all image content. Draws a rectangle of image bounds with image's dominant color on top. + * Mask all image content. Draws a rectangle of image bounds with image's dominant color on top. * By default only views extending ImageView with BitmapDrawable or custom Drawable type are - * redacted. ColorDrawable, InsetDrawable, VectorDrawable are all considered non-PII, as they come + * masked. ColorDrawable, InsetDrawable, VectorDrawable are all considered non-PII, as they come * from the apk. * *

Default is enabled. */ -var SentryReplayOptions.redactAllImages: Boolean +var SentryReplayOptions.maskAllImages: Boolean @Deprecated("Getter is unsupported.", level = DeprecationLevel.ERROR) get() = error("Getter not supported") - set(value) = setRedactAllImages(value) + set(value) = setMaskAllImages(value) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ViewExtensions.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ViewExtensions.kt index 37061a5b77..2625399c99 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ViewExtensions.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ViewExtensions.kt @@ -3,16 +3,16 @@ package io.sentry.android.replay import android.view.View /** - * Marks this view to be redacted in session replay. + * Marks this view to be masked in session replay. */ -fun View.sentryReplayRedact() { - setTag(R.id.sentry_privacy, "redact") +fun View.sentryReplayMask() { + setTag(R.id.sentry_privacy, "mask") } /** - * Marks this view to be ignored from redaction in session. + * Marks this view to be unmasked in session replay. * All its content will be visible in the replay, use with caution. */ -fun View.sentryReplayIgnore() { - setTag(R.id.sentry_privacy, "ignore") +fun View.sentryReplayUnmask() { + setTag(R.id.sentry_privacy, "unmask") } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt index 12152f50cb..5608371722 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt @@ -37,7 +37,7 @@ internal class ComposeTextLayout(internal val layout: TextLayoutResult, private // TODO: probably most of the below we can do via bytecode instrumentation and speed up at runtime /** - * This method is necessary to redact images in Compose. + * This method is necessary to mask images in Compose. * * We heuristically look up for classes that have a [Painter] modifier, usually they all have a * `Painter` string in their name, e.g. PainterElement, PainterModifierNodeElement or @@ -71,9 +71,9 @@ internal fun LayoutNode.findPainter(): Painter? { * [androidx.compose.ui.graphics.painter.BrushPainter] * * In theory, [androidx.compose.ui.graphics.painter.BitmapPainter] can also come from local assets, - * but it can as well come from a network resource, so we preemptively redact it. + * but it can as well come from a network resource, so we preemptively mask it. */ -internal fun Painter.isRedactable(): Boolean { +internal fun Painter.isMaskable(): Boolean { val className = this::class.java.name return !className.contains("Vector") && !className.contains("Color") && @@ -83,11 +83,11 @@ internal fun Painter.isRedactable(): Boolean { internal data class TextAttributes(val color: Color?, val hasFillModifier: Boolean) /** - * This method is necessary to redact text in Compose. + * This method is necessary to mask text in Compose. * * We heuristically look up for classes that have a [Text] modifier, usually they all have a * `Text` string in their name, e.g. TextStringSimpleElement or TextAnnotatedStringElement. We then - * get the color from the modifier, to be able to redact it with the correct color. + * get the color from the modifier, to be able to mask it with the correct color. * * We also look up for classes that have a [Fill] modifier, usually they all have a `Fill` string in * their name, e.g. FillElement. This is necessary to workaround a Compose bug where single-line diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Views.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Views.kt index 1c6111c1b0..0a0656de52 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Views.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Views.kt @@ -88,9 +88,9 @@ internal fun View.isVisibleToUser(): Pair { @SuppressLint("ObsoleteSdkInt") @TargetApi(21) -internal fun Drawable?.isRedactable(): Boolean { +internal fun Drawable?.isMaskable(): Boolean { // TODO: maybe find a way how to check if the drawable is coming from the apk or loaded from network - // 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) + // TODO: otherwise maybe check for the bitmap size and don't mask those that take a lot of height (e.g. a background of a whatsapp chat) return when (this) { is InsetDrawable, is ColorDrawable, is VectorDrawable, is GradientDrawable -> false is BitmapDrawable -> { diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleVideoEncoder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleVideoEncoder.kt index baf521a2e6..211decc098 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleVideoEncoder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleVideoEncoder.kt @@ -136,7 +136,7 @@ internal class SimpleVideoEncoder( ) format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate) format.setFloat(MediaFormat.KEY_FRAME_RATE, muxerConfig.frameRate.toFloat()) - format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, -1) // use -1 to force always non-key frames, meaning only partial updates to save the video size + format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 6) // use 6 to force non-key frames, meaning only partial updates to save the video size. Every 6th second is a key frame, which is useful for buffer mode format } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt index c611b91b47..888528f769 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt @@ -23,7 +23,7 @@ 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.isRedactable +import io.sentry.android.replay.util.isMaskable import io.sentry.android.replay.util.toOpaque import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.GenericViewHierarchyNode import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode @@ -45,22 +45,22 @@ internal object ComposeViewHierarchyNode { } } - private fun LayoutNode.shouldRedact(isImage: Boolean, options: SentryOptions): Boolean { + private fun LayoutNode.shouldMask(isImage: Boolean, options: SentryOptions): Boolean { val sentryPrivacyModifier = collapsedSemantics?.getOrNull(SentryReplayModifiers.SentryPrivacy) - if (sentryPrivacyModifier == "ignore") { + if (sentryPrivacyModifier == "unmask") { return false } - if (sentryPrivacyModifier == "redact") { + if (sentryPrivacyModifier == "mask") { return true } val className = getProxyClassName(isImage) - if (options.experimental.sessionReplay.ignoreViewClasses.contains(className)) { + if (options.experimental.sessionReplay.unmaskViewClasses.contains(className)) { return false } - return options.experimental.sessionReplay.redactViewClasses.contains(className) + return options.experimental.sessionReplay.maskViewClasses.contains(className) } private var _rootCoordinates: LayoutCoordinates? = null @@ -90,7 +90,7 @@ internal object ComposeViewHierarchyNode { val positionInWindow = node.coordinates.positionInWindow() return when { semantics?.contains(SemanticsProperties.Text) == true || isEditable -> { - val shouldRedact = isVisible && node.shouldRedact(isImage = false, options) + val shouldMask = isVisible && node.shouldMask(isImage = false, options) parent?.setImportantForCaptureToAncestors(true) val textLayoutResults = mutableListOf() @@ -115,7 +115,7 @@ internal object ComposeViewHierarchyNode { elevation = (parent?.elevation ?: 0f), distance = distance, parent = parent, - shouldRedact = shouldRedact, + shouldMask = shouldMask, isImportantForContentCapture = true, isVisible = isVisible, visibleRect = visibleRect @@ -124,7 +124,7 @@ internal object ComposeViewHierarchyNode { else -> { val painter = node.findPainter() if (painter != null) { - val shouldRedact = isVisible && node.shouldRedact(isImage = true, options) + val shouldMask = isVisible && node.shouldMask(isImage = true, options) parent?.setImportantForCaptureToAncestors(true) ImageViewHierarchyNode( @@ -137,11 +137,11 @@ internal object ComposeViewHierarchyNode { parent = parent, isVisible = isVisible, isImportantForContentCapture = true, - shouldRedact = shouldRedact && painter.isRedactable(), + shouldMask = shouldMask && painter.isMaskable(), visibleRect = visibleRect ) } else { - val shouldRedact = isVisible && node.shouldRedact(isImage = false, options) + val shouldMask = isVisible && node.shouldMask(isImage = false, options) // TODO: this currently does not support embedded AndroidViews, we'd have to // TODO: traverse the ViewHierarchyNode here again. For now we can recommend @@ -154,7 +154,7 @@ internal object ComposeViewHierarchyNode { elevation = (parent?.elevation ?: 0f), distance = distance, parent = parent, - shouldRedact = shouldRedact, + shouldMask = shouldMask, isImportantForContentCapture = false, /* will be set by children */ isVisible = isVisible, visibleRect = visibleRect diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt index a231e4f3d2..ef05ecb029 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt @@ -9,7 +9,7 @@ import io.sentry.SentryOptions import io.sentry.android.replay.R import io.sentry.android.replay.util.AndroidTextLayout import io.sentry.android.replay.util.TextLayout -import io.sentry.android.replay.util.isRedactable +import io.sentry.android.replay.util.isMaskable import io.sentry.android.replay.util.isVisibleToUser import io.sentry.android.replay.util.toOpaque import io.sentry.android.replay.util.totalPaddingTopSafe @@ -25,7 +25,7 @@ sealed class ViewHierarchyNode( /* Distance to the parent (index) */ val distance: Int, val parent: ViewHierarchyNode? = null, - val shouldRedact: Boolean = false, + val shouldMask: Boolean = false, /* Whether the node is important for content capture (=non-empty container) */ var isImportantForContentCapture: Boolean = false, val isVisible: Boolean = false, @@ -41,11 +41,11 @@ sealed class ViewHierarchyNode( elevation: Float, distance: Int, parent: ViewHierarchyNode? = null, - shouldRedact: Boolean = false, + shouldMask: Boolean = false, isImportantForContentCapture: Boolean = false, isVisible: Boolean = false, visibleRect: Rect? = null - ) : ViewHierarchyNode(x, y, width, height, elevation, distance, parent, shouldRedact, isImportantForContentCapture, isVisible, visibleRect) + ) : ViewHierarchyNode(x, y, width, height, elevation, distance, parent, shouldMask, isImportantForContentCapture, isVisible, visibleRect) class TextViewHierarchyNode( val layout: TextLayout? = null, @@ -59,11 +59,11 @@ sealed class ViewHierarchyNode( elevation: Float, distance: Int, parent: ViewHierarchyNode? = null, - shouldRedact: Boolean = false, + shouldMask: Boolean = false, isImportantForContentCapture: Boolean = false, isVisible: Boolean = false, visibleRect: Rect? = null - ) : ViewHierarchyNode(x, y, width, height, elevation, distance, parent, shouldRedact, isImportantForContentCapture, isVisible, visibleRect) + ) : ViewHierarchyNode(x, y, width, height, elevation, distance, parent, shouldMask, isImportantForContentCapture, isVisible, visibleRect) class ImageViewHierarchyNode( x: Float, @@ -73,11 +73,11 @@ sealed class ViewHierarchyNode( elevation: Float, distance: Int, parent: ViewHierarchyNode? = null, - shouldRedact: Boolean = false, + shouldMask: Boolean = false, isImportantForContentCapture: Boolean = false, isVisible: Boolean = false, visibleRect: Rect? = null - ) : ViewHierarchyNode(x, y, width, height, elevation, distance, parent, shouldRedact, isImportantForContentCapture, isVisible, visibleRect) + ) : ViewHierarchyNode(x, y, width, height, elevation, distance, parent, shouldMask, isImportantForContentCapture, isVisible, visibleRect) /** * Basically replicating this: https://developer.android.com/reference/android/view/View#isImportantForContentCapture() @@ -233,8 +233,8 @@ sealed class ViewHierarchyNode( ) companion object { - private const val SENTRY_IGNORE_TAG = "sentry-ignore" - private const val SENTRY_REDACT_TAG = "sentry-redact" + private const val SENTRY_UNMASK_TAG = "sentry-unmask" + private const val SENTRY_MASK_TAG = "sentry-mask" private fun Class<*>.isAssignableFrom(set: Set): Boolean { var cls: Class<*>? = this @@ -248,29 +248,29 @@ sealed class ViewHierarchyNode( return false } - private fun View.shouldRedact(options: SentryOptions): Boolean { - if ((tag as? String)?.lowercase()?.contains(SENTRY_IGNORE_TAG) == true || - getTag(R.id.sentry_privacy) == "ignore" + private fun View.shouldMask(options: SentryOptions): Boolean { + if ((tag as? String)?.lowercase()?.contains(SENTRY_UNMASK_TAG) == true || + getTag(R.id.sentry_privacy) == "unmask" ) { return false } - if ((tag as? String)?.lowercase()?.contains(SENTRY_REDACT_TAG) == true || - getTag(R.id.sentry_privacy) == "redact" + if ((tag as? String)?.lowercase()?.contains(SENTRY_MASK_TAG) == true || + getTag(R.id.sentry_privacy) == "mask" ) { return true } - if (this.javaClass.isAssignableFrom(options.experimental.sessionReplay.ignoreViewClasses)) { + if (this.javaClass.isAssignableFrom(options.experimental.sessionReplay.unmaskViewClasses)) { return false } - return this.javaClass.isAssignableFrom(options.experimental.sessionReplay.redactViewClasses) + return this.javaClass.isAssignableFrom(options.experimental.sessionReplay.maskViewClasses) } fun fromView(view: View, parent: ViewHierarchyNode?, distance: Int, options: SentryOptions): ViewHierarchyNode { val (isVisible, visibleRect) = view.isVisibleToUser() - val shouldRedact = isVisible && view.shouldRedact(options) + val shouldMask = isVisible && view.shouldMask(options) when (view) { is TextView -> { parent?.setImportantForCaptureToAncestors(true) @@ -284,7 +284,7 @@ sealed class ViewHierarchyNode( width = view.width, height = view.height, elevation = (parent?.elevation ?: 0f) + view.elevation, - shouldRedact = shouldRedact, + shouldMask = shouldMask, distance = distance, parent = parent, isImportantForContentCapture = true, @@ -305,7 +305,7 @@ sealed class ViewHierarchyNode( parent = parent, isVisible = isVisible, isImportantForContentCapture = true, - shouldRedact = shouldRedact && view.drawable?.isRedactable() == true, + shouldMask = shouldMask && view.drawable?.isMaskable() == true, visibleRect = visibleRect ) } @@ -319,7 +319,7 @@ sealed class ViewHierarchyNode( (parent?.elevation ?: 0f) + view.elevation, distance = distance, parent = parent, - shouldRedact = shouldRedact, + shouldMask = shouldMask, isImportantForContentCapture = false, /* will be set by children */ isVisible = isVisible, visibleRect = visibleRect diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ComposeRedactionOptionsTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ComposeMaskingOptionsTest.kt similarity index 67% rename from sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ComposeRedactionOptionsTest.kt rename to sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ComposeMaskingOptionsTest.kt index 981e351408..e5330fa827 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ComposeRedactionOptionsTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ComposeMaskingOptionsTest.kt @@ -22,10 +22,10 @@ import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import coil.compose.AsyncImage import io.sentry.SentryOptions -import io.sentry.android.replay.redactAllImages -import io.sentry.android.replay.redactAllText -import io.sentry.android.replay.sentryReplayIgnore -import io.sentry.android.replay.sentryReplayRedact +import io.sentry.android.replay.maskAllImages +import io.sentry.android.replay.maskAllText +import io.sentry.android.replay.sentryReplayMask +import io.sentry.android.replay.sentryReplayUnmask import io.sentry.android.replay.util.ComposeTextLayout import io.sentry.android.replay.util.traverse import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.GenericViewHierarchyNode @@ -43,132 +43,132 @@ import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) @Config(sdk = [30]) -class ComposeRedactionOptionsTest { +class ComposeMaskingOptionsTest { @Before fun setup() { System.setProperty("robolectric.areWindowsMarkedVisible", "true") - ComposeRedactionOptionsActivity.textModifierApplier = null - ComposeRedactionOptionsActivity.containerModifierApplier = null + ComposeMaskingOptionsActivity.textModifierApplier = null + ComposeMaskingOptionsActivity.containerModifierApplier = null } @Test - fun `when redactAllText is set all Text nodes are redacted`() { - val activity = buildActivity(ComposeRedactionOptionsActivity::class.java).setup() + fun `when maskAllText is set all Text nodes are masked`() { + val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup() val options = SentryOptions().apply { - experimental.sessionReplay.redactAllText = true + experimental.sessionReplay.maskAllText = true } val textNodes = activity.get().collectNodesOfType(options) assertEquals(4, textNodes.size) // [TextField, Text, Button, Activity Title] - assertTrue(textNodes.all { it.shouldRedact }) + assertTrue(textNodes.all { it.shouldMask }) // just a sanity check for parsing the tree assertEquals("Random repo", (textNodes[1].layout as ComposeTextLayout).layout.layoutInput.text.text) } @Test - fun `when redactAllText is set to false all Text nodes are ignored`() { - val activity = buildActivity(ComposeRedactionOptionsActivity::class.java).setup() + fun `when maskAllText is set to false all Text nodes are unmasked`() { + val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup() val options = SentryOptions().apply { - experimental.sessionReplay.redactAllText = false + experimental.sessionReplay.maskAllText = false } val textNodes = activity.get().collectNodesOfType(options) assertEquals(4, textNodes.size) // [TextField, Text, Button, Activity Title] - assertTrue(textNodes.none { it.shouldRedact }) + assertTrue(textNodes.none { it.shouldMask }) } @Test - fun `when redactAllImages is set all Image nodes are redacted`() { - val activity = buildActivity(ComposeRedactionOptionsActivity::class.java).setup() + fun `when maskAllImages is set all Image nodes are masked`() { + val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup() val options = SentryOptions().apply { - experimental.sessionReplay.redactAllImages = true + experimental.sessionReplay.maskAllImages = true } val imageNodes = activity.get().collectNodesOfType(options) assertEquals(1, imageNodes.size) // [AsyncImage] - assertTrue(imageNodes.all { it.shouldRedact }) + assertTrue(imageNodes.all { it.shouldMask }) } @Test - fun `when redactAllImages is set to false all Image nodes are ignored`() { - val activity = buildActivity(ComposeRedactionOptionsActivity::class.java).setup() + fun `when maskAllImages is set to false all Image nodes are unmasked`() { + val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup() val options = SentryOptions().apply { - experimental.sessionReplay.redactAllImages = false + experimental.sessionReplay.maskAllImages = false } val imageNodes = activity.get().collectNodesOfType(options) assertEquals(1, imageNodes.size) // [AsyncImage] - assertTrue(imageNodes.none { it.shouldRedact }) + assertTrue(imageNodes.none { it.shouldMask }) } @Test - fun `when sentry-redact modifier is set redacts the node`() { - ComposeRedactionOptionsActivity.textModifierApplier = { Modifier.sentryReplayRedact() } - val activity = buildActivity(ComposeRedactionOptionsActivity::class.java).setup() + fun `when sentry-mask modifier is set masks the node`() { + ComposeMaskingOptionsActivity.textModifierApplier = { Modifier.sentryReplayMask() } + val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup() val options = SentryOptions().apply { - experimental.sessionReplay.redactAllText = false + experimental.sessionReplay.maskAllText = false } val textNodes = activity.get().collectNodesOfType(options) assertEquals(4, textNodes.size) // [TextField, Text, Button, Activity Title] textNodes.forEach { if ((it.layout as? ComposeTextLayout)?.layout?.layoutInput?.text?.text == "Make Request") { - assertTrue(it.shouldRedact) + assertTrue(it.shouldMask) } else { - assertFalse(it.shouldRedact) + assertFalse(it.shouldMask) } } } @Test - fun `when sentry-ignore modifier is set ignores the node`() { - ComposeRedactionOptionsActivity.textModifierApplier = { Modifier.sentryReplayIgnore() } - val activity = buildActivity(ComposeRedactionOptionsActivity::class.java).setup() + fun `when sentry-unmask modifier is set unmasks the node`() { + ComposeMaskingOptionsActivity.textModifierApplier = { Modifier.sentryReplayUnmask() } + val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup() val options = SentryOptions().apply { - experimental.sessionReplay.redactAllText = true + experimental.sessionReplay.maskAllText = true } val textNodes = activity.get().collectNodesOfType(options) assertEquals(4, textNodes.size) // [TextField, Text, Button, Activity Title] textNodes.forEach { if ((it.layout as? ComposeTextLayout)?.layout?.layoutInput?.text?.text == "Make Request") { - assertFalse(it.shouldRedact) + assertFalse(it.shouldMask) } else { - assertTrue(it.shouldRedact) + assertTrue(it.shouldMask) } } } @Test - fun `when view is not visible, does not redact the view`() { - ComposeRedactionOptionsActivity.textModifierApplier = { Modifier.semantics { invisibleToUser() } } - val activity = buildActivity(ComposeRedactionOptionsActivity::class.java).setup() + fun `when view is not visible, does not mask the view`() { + ComposeMaskingOptionsActivity.textModifierApplier = { Modifier.semantics { invisibleToUser() } } + val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup() val options = SentryOptions().apply { - experimental.sessionReplay.redactAllText = true + experimental.sessionReplay.maskAllText = true } val textNodes = activity.get().collectNodesOfType(options) textNodes.forEach { if ((it.layout as? ComposeTextLayout)?.layout?.layoutInput?.text?.text == "Make Request") { - assertFalse(it.shouldRedact) + assertFalse(it.shouldMask) } else { - assertTrue(it.shouldRedact) + assertTrue(it.shouldMask) } } } @Test - fun `when a container view is ignored its children are not ignored`() { - ComposeRedactionOptionsActivity.containerModifierApplier = { Modifier.sentryReplayIgnore() } - val activity = buildActivity(ComposeRedactionOptionsActivity::class.java).setup() + fun `when a container view is unmasked its children are not unmasked`() { + ComposeMaskingOptionsActivity.containerModifierApplier = { Modifier.sentryReplayUnmask() } + val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup() val options = SentryOptions() @@ -176,9 +176,9 @@ class ComposeRedactionOptionsTest { val imageNodes = allNodes.filterIsInstance() val textNodes = allNodes.filterIsInstance() val genericNodes = allNodes.filterIsInstance() - assertTrue(imageNodes.all { it.shouldRedact }) - assertTrue(textNodes.all { it.shouldRedact }) - assertTrue(genericNodes.none { it.shouldRedact }) + assertTrue(imageNodes.all { it.shouldMask }) + assertTrue(textNodes.all { it.shouldMask }) + assertTrue(genericNodes.none { it.shouldMask }) } private inline fun Activity.collectNodesOfType(options: SentryOptions): List { @@ -197,7 +197,7 @@ class ComposeRedactionOptionsTest { } } -private class ComposeRedactionOptionsActivity : ComponentActivity() { +private class ComposeMaskingOptionsActivity : ComponentActivity() { companion object { var textModifierApplier: (() -> Modifier)? = null diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/MaskingOptionsTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/MaskingOptionsTest.kt new file mode 100644 index 0000000000..4a40e0a915 --- /dev/null +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/MaskingOptionsTest.kt @@ -0,0 +1,278 @@ +package io.sentry.android.replay.viewhierarchy + +import android.app.Activity +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.LinearLayout.LayoutParams +import android.widget.RadioButton +import android.widget.TextView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.SentryOptions +import io.sentry.android.replay.maskAllImages +import io.sentry.android.replay.maskAllText +import io.sentry.android.replay.sentryReplayMask +import io.sentry.android.replay.sentryReplayUnmask +import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode +import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode +import org.junit.runner.RunWith +import org.robolectric.Robolectric.buildActivity +import org.robolectric.annotation.Config +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [30]) +class MaskingOptionsTest { + + @BeforeTest + fun setup() { + System.setProperty("robolectric.areWindowsMarkedVisible", "true") + } + + @Test + fun `when maskAllText is set all TextView nodes are masked`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.maskAllText = true + } + + val textNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.textView!!, null, 0, options) + val radioButtonNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.radioButton!!, null, 0, options) + + assertTrue(textNode is TextViewHierarchyNode) + assertTrue(textNode.shouldMask) + + assertTrue(radioButtonNode is TextViewHierarchyNode) + assertTrue(radioButtonNode.shouldMask) + } + + @Test + fun `when maskAllText is set to false all TextView nodes are unmasked`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.maskAllText = false + } + + val textNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.textView!!, null, 0, options) + val radioButtonNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.radioButton!!, null, 0, options) + + assertTrue(textNode is TextViewHierarchyNode) + assertFalse(textNode.shouldMask) + + assertTrue(radioButtonNode is TextViewHierarchyNode) + assertFalse(radioButtonNode.shouldMask) + } + + @Test + fun `when maskAllImages is set all ImageView nodes are masked`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.maskAllImages = true + } + + val imageNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.imageView!!, null, 0, options) + + assertTrue(imageNode is ImageViewHierarchyNode) + assertTrue(imageNode.shouldMask) + } + + @Test + fun `when maskAllImages is set to false all ImageView nodes are unmasked`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.maskAllImages = false + } + + val imageNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.imageView!!, null, 0, options) + + assertTrue(imageNode is ImageViewHierarchyNode) + assertFalse(imageNode.shouldMask) + } + + @Test + fun `when sentry-mask tag is set mask the view`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.maskAllText = false + } + + MaskingOptionsActivity.textView!!.tag = "sentry-mask" + val textNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.textView!!, null, 0, options) + + assertTrue(textNode.shouldMask) + } + + @Test + fun `when sentry-unmask tag is set unmasks the view`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.maskAllText = true + } + + MaskingOptionsActivity.textView!!.tag = "sentry-unmask" + val textNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.textView!!, null, 0, options) + + assertFalse(textNode.shouldMask) + } + + @Test + fun `when sentry-privacy tag is set to mask masks the view`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.maskAllText = false + } + + MaskingOptionsActivity.textView!!.sentryReplayMask() + val textNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.textView!!, null, 0, options) + + assertTrue(textNode.shouldMask) + } + + @Test + fun `when sentry-privacy tag is set to unmask unmasks the view`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.maskAllText = true + } + + MaskingOptionsActivity.textView!!.sentryReplayUnmask() + val textNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.textView!!, null, 0, options) + + assertFalse(textNode.shouldMask) + } + + @Test + fun `when view is not visible, does not mask the view`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.maskAllText = true + } + + MaskingOptionsActivity.textView!!.visibility = View.GONE + val textNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.textView!!, null, 0, options) + + assertFalse(textNode.shouldMask) + } + + @Test + fun `when added to mask list masks custom view`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.maskViewClasses.add(CustomView::class.java.canonicalName) + } + + val customViewNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.customView!!, null, 0, options) + + assertTrue(customViewNode.shouldMask) + } + + @Test + fun `when subclass is added to ignored classes ignores all instances of that class`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.maskAllText = true // all TextView subclasses + experimental.sessionReplay.unmaskViewClasses.add(RadioButton::class.java.canonicalName) + } + + val textNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.textView!!, null, 0, options) + val radioButtonNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.radioButton!!, null, 0, options) + + assertTrue(textNode.shouldMask) + assertFalse(radioButtonNode.shouldMask) + } + + @Test + fun `when a container view is ignored its children are not ignored`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.unmaskViewClasses.add(LinearLayout::class.java.canonicalName) + } + + val linearLayoutNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.textView!!.parent as LinearLayout, null, 0, options) + val textNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.textView!!, null, 0, options) + val imageNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.imageView!!, null, 0, options) + + assertFalse(linearLayoutNode.shouldMask) + assertTrue(textNode.shouldMask) + assertTrue(imageNode.shouldMask) + } +} + +private class CustomView(context: Context) : View(context) { + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + canvas.drawColor(Color.BLACK) + } +} + +private class MaskingOptionsActivity : Activity() { + + companion object { + var textView: TextView? = null + var radioButton: RadioButton? = null + var imageView: ImageView? = null + var customView: CustomView? = 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) + + val image = this::class.java.classLoader.getResource("Tongariro.jpg")!! + imageView = ImageView(this).apply { + setImageDrawable(Drawable.createFromPath(image.path)) + layoutParams = LayoutParams(50, 50).apply { + setMargins(0, 16, 0, 0) + } + } + linearLayout.addView(imageView) + + radioButton = RadioButton(this).apply { + text = "Radio Button" + layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply { + setMargins(0, 16, 0, 0) + } + } + linearLayout.addView(radioButton) + + customView = CustomView(this).apply { + layoutParams = LayoutParams(50, 50).apply { + setMargins(0, 16, 0, 0) + } + } + linearLayout.addView(customView) + + setContentView(linearLayout) + } +} diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/RedactionOptionsTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/RedactionOptionsTest.kt deleted file mode 100644 index c1a50f7a62..0000000000 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/RedactionOptionsTest.kt +++ /dev/null @@ -1,278 +0,0 @@ -package io.sentry.android.replay.viewhierarchy - -import android.app.Activity -import android.content.Context -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.drawable.Drawable -import android.os.Bundle -import android.view.View -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.LinearLayout.LayoutParams -import android.widget.RadioButton -import android.widget.TextView -import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.sentry.SentryOptions -import io.sentry.android.replay.redactAllImages -import io.sentry.android.replay.redactAllText -import io.sentry.android.replay.sentryReplayIgnore -import io.sentry.android.replay.sentryReplayRedact -import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode -import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode -import org.junit.runner.RunWith -import org.robolectric.Robolectric.buildActivity -import org.robolectric.annotation.Config -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -@RunWith(AndroidJUnit4::class) -@Config(sdk = [30]) -class RedactionOptionsTest { - - @BeforeTest - fun setup() { - System.setProperty("robolectric.areWindowsMarkedVisible", "true") - } - - @Test - fun `when redactAllText is set all TextView nodes are redacted`() { - buildActivity(RedactionOptionsActivity::class.java).setup() - - val options = SentryOptions().apply { - experimental.sessionReplay.redactAllText = true - } - - val textNode = ViewHierarchyNode.fromView(RedactionOptionsActivity.textView!!, null, 0, options) - val radioButtonNode = ViewHierarchyNode.fromView(RedactionOptionsActivity.radioButton!!, null, 0, options) - - assertTrue(textNode is TextViewHierarchyNode) - assertTrue(textNode.shouldRedact) - - assertTrue(radioButtonNode is TextViewHierarchyNode) - assertTrue(radioButtonNode.shouldRedact) - } - - @Test - fun `when redactAllText is set to false all TextView nodes are ignored`() { - buildActivity(RedactionOptionsActivity::class.java).setup() - - val options = SentryOptions().apply { - experimental.sessionReplay.redactAllText = false - } - - val textNode = ViewHierarchyNode.fromView(RedactionOptionsActivity.textView!!, null, 0, options) - val radioButtonNode = ViewHierarchyNode.fromView(RedactionOptionsActivity.radioButton!!, null, 0, options) - - assertTrue(textNode is TextViewHierarchyNode) - assertFalse(textNode.shouldRedact) - - assertTrue(radioButtonNode is TextViewHierarchyNode) - assertFalse(radioButtonNode.shouldRedact) - } - - @Test - fun `when redactAllImages is set all ImageView nodes are redacted`() { - buildActivity(RedactionOptionsActivity::class.java).setup() - - val options = SentryOptions().apply { - experimental.sessionReplay.redactAllImages = true - } - - val imageNode = ViewHierarchyNode.fromView(RedactionOptionsActivity.imageView!!, null, 0, options) - - assertTrue(imageNode is ImageViewHierarchyNode) - assertTrue(imageNode.shouldRedact) - } - - @Test - fun `when redactAllImages is set to false all ImageView nodes are ignored`() { - buildActivity(RedactionOptionsActivity::class.java).setup() - - val options = SentryOptions().apply { - experimental.sessionReplay.redactAllImages = false - } - - val imageNode = ViewHierarchyNode.fromView(RedactionOptionsActivity.imageView!!, null, 0, options) - - assertTrue(imageNode is ImageViewHierarchyNode) - assertFalse(imageNode.shouldRedact) - } - - @Test - fun `when sentry-redact tag is set redacts the view`() { - buildActivity(RedactionOptionsActivity::class.java).setup() - - val options = SentryOptions().apply { - experimental.sessionReplay.redactAllText = false - } - - RedactionOptionsActivity.textView!!.tag = "sentry-redact" - val textNode = ViewHierarchyNode.fromView(RedactionOptionsActivity.textView!!, null, 0, options) - - assertTrue(textNode.shouldRedact) - } - - @Test - fun `when sentry-ignore tag is set ignores the view`() { - buildActivity(RedactionOptionsActivity::class.java).setup() - - val options = SentryOptions().apply { - experimental.sessionReplay.redactAllText = true - } - - RedactionOptionsActivity.textView!!.tag = "sentry-ignore" - val textNode = ViewHierarchyNode.fromView(RedactionOptionsActivity.textView!!, null, 0, options) - - assertFalse(textNode.shouldRedact) - } - - @Test - fun `when sentry-privacy tag is set to redact redacts the view`() { - buildActivity(RedactionOptionsActivity::class.java).setup() - - val options = SentryOptions().apply { - experimental.sessionReplay.redactAllText = false - } - - RedactionOptionsActivity.textView!!.sentryReplayRedact() - val textNode = ViewHierarchyNode.fromView(RedactionOptionsActivity.textView!!, null, 0, options) - - assertTrue(textNode.shouldRedact) - } - - @Test - fun `when sentry-privacy tag is set to ignore ignores the view`() { - buildActivity(RedactionOptionsActivity::class.java).setup() - - val options = SentryOptions().apply { - experimental.sessionReplay.redactAllText = true - } - - RedactionOptionsActivity.textView!!.sentryReplayIgnore() - val textNode = ViewHierarchyNode.fromView(RedactionOptionsActivity.textView!!, null, 0, options) - - assertFalse(textNode.shouldRedact) - } - - @Test - fun `when view is not visible, does not redact the view`() { - buildActivity(RedactionOptionsActivity::class.java).setup() - - val options = SentryOptions().apply { - experimental.sessionReplay.redactAllText = true - } - - RedactionOptionsActivity.textView!!.visibility = View.GONE - val textNode = ViewHierarchyNode.fromView(RedactionOptionsActivity.textView!!, null, 0, options) - - assertFalse(textNode.shouldRedact) - } - - @Test - fun `when added to redact list redacts custom view`() { - buildActivity(RedactionOptionsActivity::class.java).setup() - - val options = SentryOptions().apply { - experimental.sessionReplay.redactViewClasses.add(CustomView::class.java.canonicalName) - } - - val customViewNode = ViewHierarchyNode.fromView(RedactionOptionsActivity.customView!!, null, 0, options) - - assertTrue(customViewNode.shouldRedact) - } - - @Test - fun `when subclass is added to ignored classes ignores all instances of that class`() { - buildActivity(RedactionOptionsActivity::class.java).setup() - - val options = SentryOptions().apply { - experimental.sessionReplay.redactAllText = true // all TextView subclasses - experimental.sessionReplay.ignoreViewClasses.add(RadioButton::class.java.canonicalName) - } - - val textNode = ViewHierarchyNode.fromView(RedactionOptionsActivity.textView!!, null, 0, options) - val radioButtonNode = ViewHierarchyNode.fromView(RedactionOptionsActivity.radioButton!!, null, 0, options) - - assertTrue(textNode.shouldRedact) - assertFalse(radioButtonNode.shouldRedact) - } - - @Test - fun `when a container view is ignored its children are not ignored`() { - buildActivity(RedactionOptionsActivity::class.java).setup() - - val options = SentryOptions().apply { - experimental.sessionReplay.ignoreViewClasses.add(LinearLayout::class.java.canonicalName) - } - - val linearLayoutNode = ViewHierarchyNode.fromView(RedactionOptionsActivity.textView!!.parent as LinearLayout, null, 0, options) - val textNode = ViewHierarchyNode.fromView(RedactionOptionsActivity.textView!!, null, 0, options) - val imageNode = ViewHierarchyNode.fromView(RedactionOptionsActivity.imageView!!, null, 0, options) - - assertFalse(linearLayoutNode.shouldRedact) - assertTrue(textNode.shouldRedact) - assertTrue(imageNode.shouldRedact) - } -} - -private class CustomView(context: Context) : View(context) { - - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - canvas.drawColor(Color.BLACK) - } -} - -private class RedactionOptionsActivity : Activity() { - - companion object { - var textView: TextView? = null - var radioButton: RadioButton? = null - var imageView: ImageView? = null - var customView: CustomView? = 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) - - val image = this::class.java.classLoader.getResource("Tongariro.jpg")!! - imageView = ImageView(this).apply { - setImageDrawable(Drawable.createFromPath(image.path)) - layoutParams = LayoutParams(50, 50).apply { - setMargins(0, 16, 0, 0) - } - } - linearLayout.addView(imageView) - - radioButton = RadioButton(this).apply { - text = "Radio Button" - layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply { - setMargins(0, 16, 0, 0) - } - } - linearLayout.addView(radioButton) - - customView = CustomView(this).apply { - layoutParams = LayoutParams(50, 50).apply { - setMargins(0, 16, 0, 0) - } - } - linearLayout.addView(customView) - - setContentView(linearLayout) - } -} diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index 703685d6f0..058ad3710c 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -166,7 +166,6 @@ - - + diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt index 03d9e8d049..3d2e670495 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt @@ -36,7 +36,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import coil.compose.AsyncImage -import io.sentry.android.replay.sentryReplayIgnore +import io.sentry.android.replay.sentryReplayUnmask import io.sentry.compose.SentryTraced import io.sentry.compose.withSentryObservableEffect import io.sentry.samples.android.GithubAPI @@ -145,7 +145,7 @@ fun Github( .testTag("button_list_repos_async") .padding(top = 32.dp) ) { - Text("Make Request", modifier = Modifier.sentryReplayIgnore()) + Text("Make Request", modifier = Modifier.sentryReplayUnmask()) } } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index f8dc3b52ee..ac1eb2bc8a 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2711,27 +2711,32 @@ public final class io/sentry/SentryReplayEvent$ReplayType$Deserializer : io/sent } public final class io/sentry/SentryReplayOptions { + public static final field ANDROIDX_MEDIA_VIEW_CLASS_NAME Ljava/lang/String; + public static final field EXOPLAYER_CLASS_NAME Ljava/lang/String; + public static final field EXOPLAYER_STYLED_CLASS_NAME Ljava/lang/String; public static final field IMAGE_VIEW_CLASS_NAME Ljava/lang/String; 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 (Ljava/lang/Double;Ljava/lang/Double;)V public fun (Z)V - public fun addIgnoreViewClass (Ljava/lang/String;)V - public fun addRedactViewClass (Ljava/lang/String;)V + public fun addMaskViewClass (Ljava/lang/String;)V + public fun addUnmaskViewClass (Ljava/lang/String;)V public fun getErrorReplayDuration ()J public fun getFrameRate ()I - public fun getIgnoreViewClasses ()Ljava/util/Set; + public fun getMaskViewClasses ()Ljava/util/Set; public fun getOnErrorSampleRate ()Ljava/lang/Double; public fun getQuality ()Lio/sentry/SentryReplayOptions$SentryReplayQuality; - public fun getRedactViewClasses ()Ljava/util/Set; public fun getSessionDuration ()J public fun getSessionSampleRate ()Ljava/lang/Double; public fun getSessionSegmentDuration ()J + public fun getUnmaskViewClasses ()Ljava/util/Set; public fun isSessionReplayEnabled ()Z public fun isSessionReplayForErrorsEnabled ()Z + public fun setMaskAllImages (Z)V + public fun setMaskAllText (Z)V public fun setOnErrorSampleRate (Ljava/lang/Double;)V public fun setQuality (Lio/sentry/SentryReplayOptions$SentryReplayQuality;)V - public fun setRedactAllImages (Z)V - public fun setRedactAllText (Z)V public fun setSessionSampleRate (Ljava/lang/Double;)V } diff --git a/sentry/src/main/java/io/sentry/SentryReplayOptions.java b/sentry/src/main/java/io/sentry/SentryReplayOptions.java index 097f72c921..0c99085726 100644 --- a/sentry/src/main/java/io/sentry/SentryReplayOptions.java +++ b/sentry/src/main/java/io/sentry/SentryReplayOptions.java @@ -11,6 +11,12 @@ public final class SentryReplayOptions { public static final String TEXT_VIEW_CLASS_NAME = "android.widget.TextView"; public static final String IMAGE_VIEW_CLASS_NAME = "android.widget.ImageView"; + public static final String WEB_VIEW_CLASS_NAME = "android.webkit.WebView"; + public static final String VIDEO_VIEW_CLASS_NAME = "android.widget.VideoView"; + public static final String ANDROIDX_MEDIA_VIEW_CLASS_NAME = "androidx.media3.ui.PlayerView"; + public static final String EXOPLAYER_CLASS_NAME = "com.google.android.exoplayer2.ui.PlayerView"; + public static final String EXOPLAYER_STYLED_CLASS_NAME = + "com.google.android.exoplayer2.ui.StyledPlayerView"; public enum SentryReplayQuality { /** Video Scale: 80% Bit Rate: 50.000 */ @@ -52,19 +58,19 @@ public enum SentryReplayQuality { private @Nullable Double onErrorSampleRate; /** - * Redact all views with the specified class names. The class name is the fully qualified class - * name of the view, e.g. android.widget.TextView. The subclasses of the specified classes will be - * redacted as well. + * Mask all views with the specified class names. The class name is the fully qualified class name + * of the view, e.g. android.widget.TextView. The subclasses of the specified classes will be + * masked as well. * *

If you're using an obfuscation tool, make sure to add the respective proguard rules to keep * the class names. * *

Default is empty. */ - private Set redactViewClasses = new CopyOnWriteArraySet<>(); + private Set maskViewClasses = new CopyOnWriteArraySet<>(); /** - * Ignore all views with the specified class names from redaction. The class name is the fully + * Ignore all views with the specified class names from masking. The class name is the fully * qualified class name of the view, e.g. android.widget.TextView. The subclasses of the specified * classes will be ignored as well. * @@ -73,7 +79,7 @@ public enum SentryReplayQuality { * *

Default is empty. */ - private Set ignoreViewClasses = new CopyOnWriteArraySet<>(); + private Set unmaskViewClasses = new CopyOnWriteArraySet<>(); /** * Defines the quality of the session replay. The higher the quality, the more accurate the replay @@ -98,8 +104,13 @@ public enum SentryReplayQuality { public SentryReplayOptions(final boolean empty) { if (!empty) { - setRedactAllText(true); - setRedactAllImages(true); + 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); } } @@ -149,55 +160,55 @@ public void setSessionSampleRate(final @Nullable Double sessionSampleRate) { } /** - * Redact all text content. Draws a rectangle of text bounds with text color on top. By default - * only views extending TextView are redacted. + * Mask all text content. Draws a rectangle of text bounds with text color on top. By default only + * views extending TextView are masked. * *

Default is enabled. */ - public void setRedactAllText(final boolean redactAllText) { - if (redactAllText) { - addRedactViewClass(TEXT_VIEW_CLASS_NAME); - ignoreViewClasses.remove(TEXT_VIEW_CLASS_NAME); + public void setMaskAllText(final boolean maskAllText) { + if (maskAllText) { + addMaskViewClass(TEXT_VIEW_CLASS_NAME); + unmaskViewClasses.remove(TEXT_VIEW_CLASS_NAME); } else { - addIgnoreViewClass(TEXT_VIEW_CLASS_NAME); - redactViewClasses.remove(TEXT_VIEW_CLASS_NAME); + addUnmaskViewClass(TEXT_VIEW_CLASS_NAME); + maskViewClasses.remove(TEXT_VIEW_CLASS_NAME); } } /** - * Redact all image content. Draws a rectangle of image bounds with image's dominant color on top. + * Mask all image content. Draws a rectangle of image bounds with image's dominant color on top. * By default only views extending ImageView with BitmapDrawable or custom Drawable type are - * redacted. ColorDrawable, InsetDrawable, VectorDrawable are all considered non-PII, as they come + * masked. ColorDrawable, InsetDrawable, VectorDrawable are all considered non-PII, as they come * from the apk. * *

Default is enabled. */ - public void setRedactAllImages(final boolean redactAllImages) { - if (redactAllImages) { - addRedactViewClass(IMAGE_VIEW_CLASS_NAME); - ignoreViewClasses.remove(IMAGE_VIEW_CLASS_NAME); + public void setMaskAllImages(final boolean maskAllImages) { + if (maskAllImages) { + addMaskViewClass(IMAGE_VIEW_CLASS_NAME); + unmaskViewClasses.remove(IMAGE_VIEW_CLASS_NAME); } else { - addIgnoreViewClass(IMAGE_VIEW_CLASS_NAME); - redactViewClasses.remove(IMAGE_VIEW_CLASS_NAME); + addUnmaskViewClass(IMAGE_VIEW_CLASS_NAME); + maskViewClasses.remove(IMAGE_VIEW_CLASS_NAME); } } @NotNull - public Set getRedactViewClasses() { - return this.redactViewClasses; + public Set getMaskViewClasses() { + return this.maskViewClasses; } - public void addRedactViewClass(final @NotNull String className) { - this.redactViewClasses.add(className); + public void addMaskViewClass(final @NotNull String className) { + this.maskViewClasses.add(className); } @NotNull - public Set getIgnoreViewClasses() { - return this.ignoreViewClasses; + public Set getUnmaskViewClasses() { + return this.unmaskViewClasses; } - public void addIgnoreViewClass(final @NotNull String className) { - this.ignoreViewClasses.add(className); + public void addUnmaskViewClass(final @NotNull String className) { + this.unmaskViewClasses.add(className); } @ApiStatus.Internal