66
77import android .app .Activity ;
88import android .graphics .Bitmap ;
9+ import android .view .View ;
910import io .sentry .Attachment ;
1011import io .sentry .EventProcessor ;
1112import io .sentry .Hint ;
1415import io .sentry .android .core .internal .util .AndroidCurrentDateProvider ;
1516import io .sentry .android .core .internal .util .Debouncer ;
1617import io .sentry .android .core .internal .util .ScreenshotUtils ;
18+ import io .sentry .android .replay .util .MaskRenderer ;
19+ import io .sentry .android .replay .util .ViewsKt ;
20+ import io .sentry .android .replay .viewhierarchy .ViewHierarchyNode ;
1721import io .sentry .protocol .SentryTransaction ;
1822import io .sentry .util .HintUtils ;
1923import io .sentry .util .Objects ;
24+ import java .io .Closeable ;
25+ import java .io .IOException ;
2026import org .jetbrains .annotations .ApiStatus ;
2127import org .jetbrains .annotations .NotNull ;
2228import org .jetbrains .annotations .Nullable ;
2632 * captured.
2733 */
2834@ ApiStatus .Internal
29- public final class ScreenshotEventProcessor implements EventProcessor {
35+ public final class ScreenshotEventProcessor implements EventProcessor , Closeable {
3036
3137 private final @ NotNull SentryAndroidOptions options ;
3238 private final @ NotNull BuildInfoProvider buildInfoProvider ;
@@ -35,9 +41,12 @@ public final class ScreenshotEventProcessor implements EventProcessor {
3541 private static final long DEBOUNCE_WAIT_TIME_MS = 2000 ;
3642 private static final int DEBOUNCE_MAX_EXECUTIONS = 3 ;
3743
44+ private @ Nullable MaskRenderer maskRenderer = null ;
45+
3846 public ScreenshotEventProcessor (
3947 final @ NotNull SentryAndroidOptions options ,
40- final @ NotNull BuildInfoProvider buildInfoProvider ) {
48+ final @ NotNull BuildInfoProvider buildInfoProvider ,
49+ final boolean isReplayAvailable ) {
4150 this .options = Objects .requireNonNull (options , "SentryAndroidOptions is required" );
4251 this .buildInfoProvider =
4352 Objects .requireNonNull (buildInfoProvider , "BuildInfoProvider is required" );
@@ -47,11 +56,25 @@ public ScreenshotEventProcessor(
4756 DEBOUNCE_WAIT_TIME_MS ,
4857 DEBOUNCE_MAX_EXECUTIONS );
4958
59+ if (isReplayAvailable ) {
60+ maskRenderer = new MaskRenderer ();
61+ }
62+
5063 if (options .isAttachScreenshot ()) {
5164 addIntegrationToSdkVersion ("Screenshot" );
5265 }
5366 }
5467
68+ private boolean isMaskingEnabled () {
69+ if (maskRenderer == null ) {
70+ options
71+ .getLogger ()
72+ .log (SentryLevel .WARNING , "Screenshot masking requires sentry-android-replay module" );
73+ return false ;
74+ }
75+ return !options .getScreenshotOptions ().getMaskViewClasses ().isEmpty ();
76+ }
77+
5578 @ Override
5679 public @ NotNull SentryTransaction process (
5780 @ NotNull SentryTransaction transaction , @ NotNull Hint hint ) {
@@ -89,25 +112,81 @@ public ScreenshotEventProcessor(
89112 return event ;
90113 }
91114
92- final Bitmap screenshot =
115+ Bitmap screenshot =
93116 captureScreenshot (
94117 activity , options .getThreadChecker (), options .getLogger (), buildInfoProvider );
95118 if (screenshot == null ) {
96119 return event ;
97120 }
98121
122+ // Apply masking if enabled and replay module is available
123+ if (isMaskingEnabled ()) {
124+ final @ Nullable View rootView =
125+ activity .getWindow () != null
126+ && activity .getWindow ().getDecorView () != null
127+ && activity .getWindow ().getDecorView ().getRootView () != null
128+ ? activity .getWindow ().getDecorView ().getRootView ()
129+ : null ;
130+ if (rootView != null ) {
131+ screenshot = applyMasking (screenshot , rootView );
132+ }
133+ }
134+
135+ final Bitmap finalScreenshot = screenshot ;
99136 hint .setScreenshot (
100137 Attachment .fromByteProvider (
101- () -> ScreenshotUtils .compressBitmapToPng (screenshot , options .getLogger ()),
138+ () -> ScreenshotUtils .compressBitmapToPng (finalScreenshot , options .getLogger ()),
102139 "screenshot.png" ,
103140 "image/png" ,
104141 false ));
105142 hint .set (ANDROID_ACTIVITY , activity );
106143 return event ;
107144 }
108145
146+ private @ NotNull Bitmap applyMasking (
147+ final @ NotNull Bitmap screenshot , final @ NotNull View rootView ) {
148+ try {
149+ // Make bitmap mutable if needed
150+ Bitmap mutableBitmap = screenshot ;
151+ if (!screenshot .isMutable ()) {
152+ mutableBitmap = screenshot .copy (Bitmap .Config .ARGB_8888 , true );
153+ if (mutableBitmap == null ) {
154+ return screenshot ;
155+ }
156+ }
157+
158+ // we can access it here, since it's "internal" only for Kotlin
159+
160+ // Build view hierarchy and apply masks
161+ final ViewHierarchyNode rootNode =
162+ ViewHierarchyNode .Companion .fromView (rootView , null , 0 , options .getScreenshotOptions ());
163+ ViewsKt .traverse (rootView , rootNode , options .getScreenshotOptions (), options .getLogger ());
164+
165+ if (maskRenderer != null ) {
166+ maskRenderer .renderMasks (mutableBitmap , rootNode , null );
167+ }
168+
169+ // Recycle original if we created a copy
170+ if (mutableBitmap != screenshot && !screenshot .isRecycled ()) {
171+ screenshot .recycle ();
172+ }
173+
174+ return mutableBitmap ;
175+ } catch (Throwable e ) {
176+ options .getLogger ().log (SentryLevel .ERROR , "Failed to mask screenshot" , e );
177+ return screenshot ;
178+ }
179+ }
180+
109181 @ Override
110182 public @ Nullable Long getOrder () {
111183 return 10000L ;
112184 }
185+
186+ @ Override
187+ public void close () throws IOException {
188+ if (maskRenderer != null ) {
189+ maskRenderer .close ();
190+ }
191+ }
113192}
0 commit comments