-
-
Notifications
You must be signed in to change notification settings - Fork 365
fix(android): apply screenshot masking in captureScreenshot() #6565
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4269fb0
6d0778a
1f29c11
bfb341e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,5 @@ | ||
| package io.sentry.react; | ||
|
|
||
| import static io.sentry.android.core.internal.util.ScreenshotUtils.takeScreenshot; | ||
| import static io.sentry.vendor.Base64.NO_PADDING; | ||
| import static io.sentry.vendor.Base64.NO_WRAP; | ||
| import static java.util.concurrent.TimeUnit.SECONDS; | ||
|
|
@@ -10,8 +9,10 @@ | |
| import android.content.pm.PackageInfo; | ||
| import android.content.pm.PackageManager; | ||
| import android.content.res.AssetManager; | ||
| import android.graphics.Bitmap; | ||
| import android.net.Uri; | ||
| import android.util.SparseIntArray; | ||
| import android.view.View; | ||
| import androidx.annotation.VisibleForTesting; | ||
| import androidx.core.app.FrameMetricsAggregator; | ||
| import androidx.fragment.app.FragmentActivity; | ||
|
|
@@ -47,10 +48,15 @@ | |
| import io.sentry.android.core.SentryAndroidDateProvider; | ||
| import io.sentry.android.core.SentryAndroidOptions; | ||
| import io.sentry.android.core.SentryFramesDelayResult; | ||
| import io.sentry.android.core.SentryScreenshotOptions; | ||
| import io.sentry.android.core.SentryShakeDetector; | ||
| import io.sentry.android.core.ViewHierarchyEventProcessor; | ||
| import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader; | ||
| import io.sentry.android.core.internal.util.ScreenshotUtils; | ||
| import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; | ||
| import io.sentry.android.replay.util.MaskRenderer; | ||
| import io.sentry.android.replay.util.ViewsKt; | ||
| import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode; | ||
| import io.sentry.android.core.performance.AppStartMetrics; | ||
| import io.sentry.profilemeasurements.ProfileMeasurement; | ||
| import io.sentry.profilemeasurements.ProfileMeasurementValue; | ||
|
|
@@ -546,7 +552,7 @@ private static byte[] takeScreenshotOnUiThread(Activity activity) { | |
| final byte[][] bytesWrapper = {{}}; // wrapper to be able to set the value in the runnable | ||
| final Runnable runTakeScreenshot = | ||
| () -> { | ||
| bytesWrapper[0] = takeScreenshot(activity, logger, buildInfo); | ||
| bytesWrapper[0] = takeMaskedScreenshot(activity); | ||
| doneSignal.countDown(); | ||
| }; | ||
|
|
||
|
|
@@ -566,6 +572,84 @@ private static byte[] takeScreenshotOnUiThread(Activity activity) { | |
| return bytesWrapper[0]; | ||
| } | ||
|
|
||
| private static @Nullable byte[] takeMaskedScreenshot(final @NotNull Activity activity) { | ||
| final @Nullable Bitmap screenshot = | ||
| ScreenshotUtils.captureScreenshot(activity, logger, buildInfo); | ||
| if (screenshot == null) { | ||
| return null; | ||
| } | ||
|
|
||
| final @Nullable SentryScreenshotOptions maskingOptions = screenshotMaskingOptions(); | ||
| if (maskingOptions == null) { | ||
| return ScreenshotUtils.compressBitmapToPng(screenshot, logger); | ||
| } | ||
|
Comment on lines
+583
to
+585
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The Suggested FixWrap the Prompt for AI AgentAlso affects:
|
||
|
|
||
| final @Nullable Bitmap masked = maskScreenshot(activity, screenshot, maskingOptions); | ||
| if (masked == null) { | ||
| return null; | ||
| } | ||
|
|
||
| return ScreenshotUtils.compressBitmapToPng(masked, logger); | ||
| } | ||
|
|
||
| private static @Nullable SentryScreenshotOptions screenshotMaskingOptions() { | ||
| final @NotNull SentryOptions options = ScopesAdapter.getInstance().getOptions(); | ||
| if (!(options instanceof SentryAndroidOptions)) { | ||
| return null; | ||
| } | ||
|
|
||
| return ((SentryAndroidOptions) options).getScreenshot(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Disabled masking now requires ReplayMedium Severity
Triggered by project rule: PR Review Guidelines for Cursor Bot Reviewed by Cursor Bugbot for commit bfb341e. Configure here. |
||
| } | ||
|
|
||
| private static @Nullable Bitmap maskScreenshot( | ||
| final @NotNull Activity activity, | ||
| final @NotNull Bitmap screenshot, | ||
| final @NotNull SentryScreenshotOptions maskingOptions) { | ||
| Bitmap mutableScreenshot = screenshot; | ||
| boolean createdCopy = false; | ||
| try { | ||
| final @Nullable View rootView = | ||
| activity.getWindow() != null && activity.getWindow().peekDecorView() != null | ||
| ? activity.getWindow().peekDecorView().getRootView() | ||
| : null; | ||
| if (rootView == null) { | ||
| screenshot.recycle(); | ||
| return null; | ||
| } | ||
|
|
||
| final @NotNull ViewHierarchyNode rootNode = | ||
| ViewHierarchyNode.Companion.fromView(rootView, null, 0, maskingOptions); | ||
| ViewsKt.traverse(rootView, rootNode, maskingOptions, logger, null); | ||
|
|
||
| if (!screenshot.isMutable()) { | ||
| mutableScreenshot = screenshot.copy(Bitmap.Config.ARGB_8888, true); | ||
| if (mutableScreenshot == null) { | ||
| screenshot.recycle(); | ||
| return null; | ||
| } | ||
| createdCopy = true; | ||
| } | ||
|
|
||
| try (final MaskRenderer maskRenderer = new MaskRenderer()) { | ||
| maskRenderer.renderMasks(mutableScreenshot, rootNode, null); | ||
| } | ||
|
|
||
| if (createdCopy && !screenshot.isRecycled()) { | ||
| screenshot.recycle(); | ||
| } | ||
| return mutableScreenshot; | ||
| } catch (Throwable e) { // NOPMD - masking must never crash the screenshot flow | ||
| logger.log(SentryLevel.ERROR, "Failed to mask screenshot.", e); | ||
| if (createdCopy && !mutableScreenshot.isRecycled()) { | ||
| mutableScreenshot.recycle(); | ||
| } | ||
| if (!screenshot.isRecycled()) { | ||
| screenshot.recycle(); | ||
| } | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| public void fetchViewHierarchy(Promise promise) { | ||
| final @Nullable Activity activity = getCurrentActivity(); | ||
| final @Nullable ViewHierarchy viewHierarchy = | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mask rendering blocks Android UI thread
Medium Severity
takeMaskedScreenshot()performs hierarchy traversal and full-bitmap mask rendering inside the UI-thread runnable. Complex screens can freeze rendering or trigger an ANR, while background callers may time out and receivenulleven though masking continues.Triggered by project rule: PR Review Guidelines for Cursor Bot
Reviewed by Cursor Bugbot for commit 1f29c11. Configure here.