Skip to content

Commit 4fedad4

Browse files
committed
Address PR feedback
1 parent 9caa949 commit 4fedad4

File tree

4 files changed

+113
-16
lines changed

4 files changed

+113
-16
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
### Features
66

77
- Add experimental Sentry Android Distribution module for integrating with Sentry Build Distribution to check for and install updates ([#4804](https://github.com/getsentry/sentry-java/pull/4804))
8-
- Session Replay: Add new experimental Canvas Capture Strategy ([#4777](https://github.com/getsentry/sentry-java/pull/4777))
8+
- Session Replay: Add new _experimental_ Canvas Capture Strategy ([#4777](https://github.com/getsentry/sentry-java/pull/4777))
99
- A new screenshot capture strategy that uses Android's Canvas API for more accurate text masking
10-
- Any `.drawText()` calls are replaced with rectangles to ensure no text is not captured
10+
- Any `.drawText()` or `.drawBitmap()` calls are replaced by rectangles, ensuring no text or images are present in the resulting output
1111
```kotlin
1212
SentryAndroid.init(context) { options ->
1313
options.sessionReplay.screenshotStrategy = ScreenshotStrategyType.CANVAS

sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.sentry.ILogger;
77
import io.sentry.InitPriority;
88
import io.sentry.ProfileLifecycle;
9+
import io.sentry.ScreenshotStrategyType;
910
import io.sentry.SentryFeedbackOptions;
1011
import io.sentry.SentryIntegrationPackageStorage;
1112
import io.sentry.SentryLevel;
@@ -111,6 +112,7 @@ final class ManifestMetadataReader {
111112
static final String REPLAYS_MASK_ALL_IMAGES = "io.sentry.session-replay.mask-all-images";
112113

113114
static final String REPLAYS_DEBUG = "io.sentry.session-replay.debug";
115+
static final String REPLAYS_SCREENSHOT_STRATEGY = "io.sentry.session-replay.screenshot-strategy";
114116

115117
static final String FORCE_INIT = "io.sentry.force-init";
116118

@@ -476,6 +478,16 @@ static void applyMetadata(
476478

477479
options.getSessionReplay().setDebug(readBool(metadata, logger, REPLAYS_DEBUG, false));
478480

481+
final @Nullable String screenshotStrategyRaw =
482+
readString(metadata, logger, REPLAYS_SCREENSHOT_STRATEGY, null);
483+
if (screenshotStrategyRaw != null) {
484+
if ("canvas".equals(screenshotStrategyRaw)) {
485+
options.getSessionReplay().setScreenshotStrategy(ScreenshotStrategyType.CANVAS);
486+
} else {
487+
// always default to PIXEL_COPYq
488+
options.getSessionReplay().setScreenshotStrategy(ScreenshotStrategyType.PIXEL_COPY);
489+
}
490+
}
479491
options.setIgnoredErrors(readList(metadata, logger, IGNORED_ERRORS));
480492

481493
final @Nullable List<String> includes = readList(metadata, logger, IN_APP_INCLUDES);

sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1835,4 +1835,51 @@ class ManifestMetadataReaderTest {
18351835
// Assert
18361836
assertFalse(fixture.options.feedbackOptions.isShowBranding)
18371837
}
1838+
1839+
@Test
1840+
fun `applyMetadata reads screenshot strategy canvas to options`() {
1841+
// Arrange
1842+
val bundle = bundleOf(ManifestMetadataReader.REPLAYS_SCREENSHOT_STRATEGY to "canvas")
1843+
val context = fixture.getContext(metaData = bundle)
1844+
1845+
// Act
1846+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1847+
1848+
// Assert
1849+
assertEquals(
1850+
io.sentry.ScreenshotStrategyType.CANVAS,
1851+
fixture.options.sessionReplay.screenshotStrategy,
1852+
)
1853+
}
1854+
1855+
@Test
1856+
fun `applyMetadata reads screenshot strategy and defaults to PIXEL_COPY for unknown value`() {
1857+
// Arrange
1858+
val bundle = bundleOf(ManifestMetadataReader.REPLAYS_SCREENSHOT_STRATEGY to "unknown")
1859+
val context = fixture.getContext(metaData = bundle)
1860+
1861+
// Act
1862+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1863+
1864+
// Assert
1865+
assertEquals(
1866+
io.sentry.ScreenshotStrategyType.PIXEL_COPY,
1867+
fixture.options.sessionReplay.screenshotStrategy,
1868+
)
1869+
}
1870+
1871+
@Test
1872+
fun `applyMetadata reads screenshot strategy and keeps default if not found`() {
1873+
// Arrange
1874+
val context = fixture.getContext()
1875+
1876+
// Act
1877+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1878+
1879+
// Assert
1880+
assertEquals(
1881+
io.sentry.ScreenshotStrategyType.PIXEL_COPY,
1882+
fixture.options.sessionReplay.screenshotStrategy,
1883+
)
1884+
}
18381885
}

sentry-android-replay/src/main/java/io/sentry/android/replay/screenshot/CanvasStrategy.kt

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,16 @@ import android.view.View
2929
import androidx.annotation.RequiresApi
3030
import io.sentry.SentryLevel
3131
import io.sentry.SentryOptions
32+
import io.sentry.SentryReplayOptions
33+
import io.sentry.SentryReplayOptions.IMAGE_VIEW_CLASS_NAME
34+
import io.sentry.SentryReplayOptions.TEXT_VIEW_CLASS_NAME
3235
import io.sentry.android.replay.ExecutorProvider
3336
import io.sentry.android.replay.ScreenshotRecorderCallback
3437
import io.sentry.android.replay.ScreenshotRecorderConfig
3538
import io.sentry.android.replay.util.submitSafely
3639
import io.sentry.util.AutoClosableReentrantLock
3740
import io.sentry.util.IntegrationUtils
41+
import java.io.Closeable
3842
import java.util.WeakHashMap
3943
import java.util.concurrent.atomic.AtomicBoolean
4044
import java.util.concurrent.atomic.AtomicReference
@@ -55,13 +59,14 @@ internal class CanvasStrategy(
5559
private val prescaledMatrix by
5660
lazy(NONE) { Matrix().apply { preScale(config.scaleFactorX, config.scaleFactorY) } }
5761
private val lastCaptureSuccessful = AtomicBoolean(false)
58-
private val textIgnoringCanvas = TextIgnoringDelegateCanvas()
62+
private val textIgnoringCanvas = TextIgnoringDelegateCanvas(options.sessionReplay)
5963

6064
private val isClosed = AtomicBoolean(false)
6165

6266
private val onImageAvailableListener: (holder: PictureReaderHolder) -> Unit = { holder ->
6367
if (isClosed.get()) {
6468
options.logger.log(SentryLevel.ERROR, "CanvasStrategy already closed, skipping image")
69+
holder.close()
6570
} else {
6671
try {
6772
val image = holder.reader.acquireLatestImage()
@@ -86,12 +91,20 @@ internal class CanvasStrategy(
8691
}
8792
}
8893
} finally {
89-
image.close()
94+
try {
95+
image.close()
96+
} catch (_: Throwable) {
97+
// ignored
98+
}
9099
}
91100
} catch (e: Throwable) {
92101
options.logger.log(SentryLevel.ERROR, "CanvasStrategy: image processing failed", e)
93102
} finally {
94-
freePictureRef.set(holder)
103+
if (isClosed.get()) {
104+
holder.close()
105+
} else {
106+
freePictureRef.set(holder)
107+
}
95108
}
96109
}
97110
}
@@ -116,10 +129,7 @@ internal class CanvasStrategy(
116129
)
117130
return@Runnable
118131
}
119-
val holder = unprocessedPictureRef.getAndSet(null)
120-
if (holder == null) {
121-
return@Runnable
122-
}
132+
val holder = unprocessedPictureRef.getAndSet(null) ?: return@Runnable
123133

124134
try {
125135
if (!holder.setup.getAndSet(true)) {
@@ -135,14 +145,20 @@ internal class CanvasStrategy(
135145
surface.unlockCanvasAndPost(canvas)
136146
}
137147
} catch (t: Throwable) {
138-
freePictureRef.set(holder)
148+
if (isClosed.get()) {
149+
holder.close()
150+
} else {
151+
freePictureRef.set(holder)
152+
}
139153
options.logger.log(SentryLevel.ERROR, "Canvas Strategy: picture render failed", t)
140154
}
141155
}
142156

143157
@SuppressLint("UnclosedTrace")
144158
override fun capture(root: View) {
145-
159+
if (isClosed.get()) {
160+
return
161+
}
146162
val holder = freePictureRef.getAndSet(null)
147163
if (holder == null) {
148164
options.logger.log(SentryLevel.DEBUG, "No free Picture available, skipping capture")
@@ -156,9 +172,12 @@ internal class CanvasStrategy(
156172
root.draw(textIgnoringCanvas)
157173
holder.picture.endRecording()
158174

159-
unprocessedPictureRef.set(holder)
160-
161-
executor.getExecutor().submitSafely(options, "screenshot_recorder.canvas", pictureRenderTask)
175+
if (isClosed.get()) {
176+
holder.close()
177+
} else {
178+
unprocessedPictureRef.set(holder)
179+
executor.getExecutor().submitSafely(options, "screenshot_recorder.canvas", pictureRenderTask)
180+
}
162181
}
163182

164183
override fun onContentChanged() {
@@ -173,7 +192,11 @@ internal class CanvasStrategy(
173192
recycle()
174193
}
175194
}
195+
screenshot = null
176196
}
197+
// the image can be free, unprocessed or in transit
198+
freePictureRef.getAndSet(null)?.reader?.close()
199+
unprocessedPictureRef.getAndSet(null)?.reader?.close()
177200
}
178201

179202
override fun lastCaptureSuccessful(): Boolean {
@@ -191,7 +214,7 @@ internal class CanvasStrategy(
191214
}
192215

193216
@SuppressLint("UseKtx")
194-
private class TextIgnoringDelegateCanvas : Canvas() {
217+
private class TextIgnoringDelegateCanvas(sessionReplay: SentryReplayOptions) : Canvas() {
195218

196219
lateinit var delegate: Canvas
197220
private val solidPaint = Paint()
@@ -205,6 +228,13 @@ private class TextIgnoringDelegateCanvas : Canvas() {
205228

206229
private val bitmapColorCache = WeakHashMap<Bitmap, Pair<Int, Int>>()
207230

231+
private val maskAllText =
232+
sessionReplay.maskViewClasses.contains(TEXT_VIEW_CLASS_NAME) ||
233+
sessionReplay.maskViewClasses.size > 1
234+
private val maskAllImages =
235+
sessionReplay.maskViewClasses.contains(IMAGE_VIEW_CLASS_NAME) ||
236+
sessionReplay.maskViewClasses.size > 1
237+
208238
override fun isHardwareAccelerated(): Boolean {
209239
return false
210240
}
@@ -992,7 +1022,7 @@ private class PictureReaderHolder(
9921022
val width: Int,
9931023
val height: Int,
9941024
val listener: (holder: PictureReaderHolder) -> Unit,
995-
) : ImageReader.OnImageAvailableListener {
1025+
) : ImageReader.OnImageAvailableListener, Closeable {
9961026
val picture = Picture()
9971027

9981028
@SuppressLint("InlinedApi")
@@ -1005,4 +1035,12 @@ private class PictureReaderHolder(
10051035
listener(this)
10061036
}
10071037
}
1038+
1039+
override fun close() {
1040+
try {
1041+
reader.close()
1042+
} catch (_: Throwable) {
1043+
// ignored
1044+
}
1045+
}
10081046
}

0 commit comments

Comments
 (0)