Skip to content

Commit ee4247a

Browse files
authored
Merge 388809f into 087248f
2 parents 087248f + 388809f commit ee4247a

File tree

8 files changed

+174
-14
lines changed

8 files changed

+174
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
- The `MANIFEST.MF` of `sentry-opentelemetry-agent` now has `Implementation-Version` set to the raw version ([#4291](https://github.com/getsentry/sentry-java/pull/4291))
2828
- An example value would be `8.6.0`
2929
- The value of the `Sentry-Version-Name` attribute looks like `sentry-8.5.0-otel-2.10.0`
30+
- Compress Screenshots on a background thread ([#4295](https://github.com/getsentry/sentry-java/pull/4295))
3031

3132
### Internal
3233

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
package io.sentry.android.core;
22

33
import static io.sentry.TypeCheckHint.ANDROID_ACTIVITY;
4-
import static io.sentry.android.core.internal.util.ScreenshotUtils.takeScreenshot;
4+
import static io.sentry.android.core.internal.util.ScreenshotUtils.captureScreenshot;
55
import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion;
66

77
import android.app.Activity;
8+
import android.graphics.Bitmap;
89
import io.sentry.Attachment;
910
import io.sentry.EventProcessor;
1011
import io.sentry.Hint;
1112
import io.sentry.SentryEvent;
1213
import io.sentry.SentryLevel;
1314
import io.sentry.android.core.internal.util.AndroidCurrentDateProvider;
1415
import io.sentry.android.core.internal.util.Debouncer;
16+
import io.sentry.android.core.internal.util.ScreenshotUtils;
1517
import io.sentry.protocol.SentryTransaction;
1618
import io.sentry.util.HintUtils;
1719
import io.sentry.util.Objects;
@@ -87,14 +89,19 @@ public ScreenshotEventProcessor(
8789
return event;
8890
}
8991

90-
final byte[] screenshot =
91-
takeScreenshot(
92+
final Bitmap screenshot =
93+
captureScreenshot(
9294
activity, options.getThreadChecker(), options.getLogger(), buildInfoProvider);
9395
if (screenshot == null) {
9496
return event;
9597
}
9698

97-
hint.setScreenshot(Attachment.fromScreenshot(screenshot));
99+
hint.setScreenshot(
100+
Attachment.fromByteProvider(
101+
() -> ScreenshotUtils.compressBitmapToPng(screenshot, options.getLogger()),
102+
"screenshot.png",
103+
"image/png",
104+
false));
98105
hint.set(ANDROID_ACTIVITY, activity);
99106
return event;
100107
}

sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,44 @@ public class ScreenshotUtils {
2727

2828
private static final long CAPTURE_TIMEOUT_MS = 1000;
2929

30+
// Used by Hybrid SDKs
31+
/**
32+
* @noinspection unused
33+
*/
3034
public static @Nullable byte[] takeScreenshot(
3135
final @NotNull Activity activity,
3236
final @NotNull ILogger logger,
3337
final @NotNull BuildInfoProvider buildInfoProvider) {
3438
return takeScreenshot(activity, AndroidThreadChecker.getInstance(), logger, buildInfoProvider);
3539
}
3640

41+
// Used by Hybrid SDKs
3742
@SuppressLint("NewApi")
3843
public static @Nullable byte[] takeScreenshot(
3944
final @NotNull Activity activity,
4045
final @NotNull IThreadChecker threadChecker,
4146
final @NotNull ILogger logger,
4247
final @NotNull BuildInfoProvider buildInfoProvider) {
48+
49+
final @Nullable Bitmap screenshot =
50+
captureScreenshot(activity, threadChecker, logger, buildInfoProvider);
51+
return compressBitmapToPng(screenshot, logger);
52+
}
53+
54+
public static @Nullable Bitmap captureScreenshot(
55+
final @NotNull Activity activity,
56+
final @NotNull ILogger logger,
57+
final @NotNull BuildInfoProvider buildInfoProvider) {
58+
return captureScreenshot(
59+
activity, AndroidThreadChecker.getInstance(), logger, buildInfoProvider);
60+
}
61+
62+
@SuppressLint("NewApi")
63+
public static @Nullable Bitmap captureScreenshot(
64+
final @NotNull Activity activity,
65+
final @NotNull IThreadChecker threadChecker,
66+
final @NotNull ILogger logger,
67+
final @NotNull BuildInfoProvider buildInfoProvider) {
4368
// We are keeping BuildInfoProvider param for compatibility, as it's being used by
4469
// cross-platform SDKs
4570

@@ -71,7 +96,7 @@ public class ScreenshotUtils {
7196
return null;
7297
}
7398

74-
try (final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
99+
try {
75100
// ARGB_8888 -> This configuration is very flexible and offers the best quality
76101
final Bitmap bitmap =
77102
Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
@@ -132,7 +157,19 @@ public class ScreenshotUtils {
132157
return null;
133158
}
134159
}
160+
return bitmap;
161+
} catch (Throwable e) {
162+
logger.log(SentryLevel.ERROR, "Taking screenshot failed.", e);
163+
}
164+
return null;
165+
}
135166

167+
public static @Nullable byte[] compressBitmapToPng(
168+
final @Nullable Bitmap bitmap, final @NotNull ILogger logger) {
169+
if (bitmap == null || bitmap.isRecycled()) {
170+
return null;
171+
}
172+
try (final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
136173
// 0 meaning compress for small size, 100 meaning compress for max quality.
137174
// Some formats, like PNG which is lossless, will ignore the quality setting.
138175
bitmap.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream);
@@ -145,7 +182,7 @@ public class ScreenshotUtils {
145182
// screenshot png is around ~100-150 kb
146183
return byteArrayOutputStream.toByteArray();
147184
} catch (Throwable e) {
148-
logger.log(SentryLevel.ERROR, "Taking screenshot failed.", e);
185+
logger.log(SentryLevel.ERROR, "Compressing bitmap failed.", e);
149186
}
150187
return null;
151188
}

sentry-android-core/src/test/java/io/sentry/android/core/internal/util/ScreenshotUtilTest.kt

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package io.sentry.android.core.internal.util
22

33
import android.app.Activity
4+
import android.graphics.Bitmap
45
import android.os.Build
56
import android.os.Bundle
67
import android.view.View
78
import android.view.Window
89
import androidx.test.ext.junit.runners.AndroidJUnit4
910
import io.sentry.ILogger
11+
import io.sentry.NoOpLogger
1012
import io.sentry.android.core.BuildInfoProvider
1113
import junit.framework.TestCase.assertNull
1214
import org.junit.runner.RunWith
@@ -17,6 +19,7 @@ import org.robolectric.annotation.Config
1719
import org.robolectric.shadows.ShadowPixelCopy
1820
import kotlin.test.Test
1921
import kotlin.test.assertNotNull
22+
import kotlin.test.assertTrue
2023

2124
@Config(
2225
shadows = [ShadowPixelCopy::class],
@@ -32,7 +35,7 @@ class ScreenshotUtilTest {
3235
whenever(activity.isDestroyed).thenReturn(false)
3336

3437
val data =
35-
ScreenshotUtils.takeScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
38+
ScreenshotUtils.captureScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
3639
assertNull(data)
3740
}
3841

@@ -44,7 +47,7 @@ class ScreenshotUtilTest {
4447
whenever(activity.window).thenReturn(mock<Window>())
4548

4649
val data =
47-
ScreenshotUtils.takeScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
50+
ScreenshotUtils.captureScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
4851
assertNull(data)
4952
}
5053

@@ -60,7 +63,7 @@ class ScreenshotUtilTest {
6063
whenever(window.peekDecorView()).thenReturn(decorView)
6164

6265
val data =
63-
ScreenshotUtils.takeScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
66+
ScreenshotUtils.captureScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
6467
assertNull(data)
6568
}
6669

@@ -81,7 +84,7 @@ class ScreenshotUtilTest {
8184
whenever(rootView.height).thenReturn(0)
8285

8386
val data =
84-
ScreenshotUtils.takeScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
87+
ScreenshotUtils.captureScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
8588
assertNull(data)
8689
}
8790

@@ -94,7 +97,7 @@ class ScreenshotUtilTest {
9497
val buildInfoProvider = mock<BuildInfoProvider>()
9598
whenever(buildInfoProvider.sdkInfoVersion).thenReturn(Build.VERSION_CODES.O)
9699

97-
val data = ScreenshotUtils.takeScreenshot(controller.get(), logger, buildInfoProvider)
100+
val data = ScreenshotUtils.captureScreenshot(controller.get(), logger, buildInfoProvider)
98101
assertNotNull(data)
99102
}
100103

@@ -107,9 +110,32 @@ class ScreenshotUtilTest {
107110
val buildInfoProvider = mock<BuildInfoProvider>()
108111
whenever(buildInfoProvider.sdkInfoVersion).thenReturn(Build.VERSION_CODES.N)
109112

110-
val data = ScreenshotUtils.takeScreenshot(controller.get(), logger, buildInfoProvider)
113+
val data = ScreenshotUtils.captureScreenshot(controller.get(), logger, buildInfoProvider)
111114
assertNotNull(data)
112115
}
116+
117+
@Test
118+
fun `a null bitmap compresses into null`() {
119+
val bytes = ScreenshotUtils.compressBitmapToPng(null, NoOpLogger.getInstance())
120+
assertNull(bytes)
121+
}
122+
123+
@Test
124+
fun `a recycled bitmap compresses into null`() {
125+
val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
126+
bitmap.recycle()
127+
128+
val bytes = ScreenshotUtils.compressBitmapToPng(bitmap, NoOpLogger.getInstance())
129+
assertNull(bytes)
130+
}
131+
132+
@Test
133+
fun `a valid bitmap compresses into a valid bytearray`() {
134+
val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
135+
val bytes = ScreenshotUtils.compressBitmapToPng(bitmap, NoOpLogger.getInstance())
136+
assertNotNull(bytes)
137+
assertTrue(bytes.isNotEmpty())
138+
}
113139
}
114140

115141
class ExampleActivity : Activity() {

sentry/api/sentry.api

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@ public final class io/sentry/Attachment {
1111
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V
1212
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V
1313
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;)V
14+
public fun <init> (Ljava/util/concurrent/Callable;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V
1415
public fun <init> ([BLjava/lang/String;)V
1516
public fun <init> ([BLjava/lang/String;Ljava/lang/String;)V
1617
public fun <init> ([BLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V
1718
public fun <init> ([BLjava/lang/String;Ljava/lang/String;Z)V
19+
public static fun fromByteProvider (Ljava/util/concurrent/Callable;Ljava/lang/String;Ljava/lang/String;Z)Lio/sentry/Attachment;
1820
public static fun fromScreenshot ([B)Lio/sentry/Attachment;
1921
public static fun fromThreadDump ([B)Lio/sentry/Attachment;
2022
public static fun fromViewHierarchy (Lio/sentry/protocol/ViewHierarchy;)Lio/sentry/Attachment;
2123
public fun getAttachmentType ()Ljava/lang/String;
24+
public fun getByteProvider ()Ljava/util/concurrent/Callable;
2225
public fun getBytes ()[B
2326
public fun getContentType ()Ljava/lang/String;
2427
public fun getFilename ()Ljava/lang/String;

sentry/src/main/java/io/sentry/Attachment.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.sentry.protocol.ViewHierarchy;
44
import java.io.File;
5+
import java.util.concurrent.Callable;
56
import org.jetbrains.annotations.NotNull;
67
import org.jetbrains.annotations.Nullable;
78

@@ -10,6 +11,7 @@ public final class Attachment {
1011

1112
private @Nullable byte[] bytes;
1213
private final @Nullable JsonSerializable serializable;
14+
private final @Nullable Callable<byte[]> byteProvider;
1315
private @Nullable String pathname;
1416
private final @NotNull String filename;
1517
private final @Nullable String contentType;
@@ -84,6 +86,7 @@ public Attachment(
8486
final boolean addToTransactions) {
8587
this.bytes = bytes;
8688
this.serializable = null;
89+
this.byteProvider = null;
8790
this.filename = filename;
8891
this.contentType = contentType;
8992
this.attachmentType = attachmentType;
@@ -109,6 +112,33 @@ public Attachment(
109112
final boolean addToTransactions) {
110113
this.bytes = null;
111114
this.serializable = serializable;
115+
this.byteProvider = null;
116+
this.filename = filename;
117+
this.contentType = contentType;
118+
this.attachmentType = attachmentType;
119+
this.addToTransactions = addToTransactions;
120+
}
121+
122+
/**
123+
* Initializes an Attachment with bytes factory, a filename, a content type, and
124+
* addToTransactions.
125+
*
126+
* @param byteProvider A provider holding the attachment payload
127+
* @param filename The name of the attachment to display in Sentry.
128+
* @param contentType The content type of the attachment.
129+
* @param attachmentType the attachment type.
130+
* @param addToTransactions <code>true</code> if the SDK should add this attachment to every
131+
* {@link ITransaction} or set to <code>false</code> if it shouldn't.
132+
*/
133+
public Attachment(
134+
final @NotNull Callable<byte[]> byteProvider,
135+
final @NotNull String filename,
136+
final @Nullable String contentType,
137+
final @Nullable String attachmentType,
138+
final boolean addToTransactions) {
139+
this.bytes = null;
140+
this.serializable = null;
141+
this.byteProvider = byteProvider;
112142
this.filename = filename;
113143
this.contentType = contentType;
114144
this.attachmentType = attachmentType;
@@ -186,6 +216,7 @@ public Attachment(
186216
this.pathname = pathname;
187217
this.filename = filename;
188218
this.serializable = null;
219+
this.byteProvider = null;
189220
this.contentType = contentType;
190221
this.attachmentType = attachmentType;
191222
this.addToTransactions = addToTransactions;
@@ -212,6 +243,7 @@ public Attachment(
212243
this.pathname = pathname;
213244
this.filename = filename;
214245
this.serializable = null;
246+
this.byteProvider = null;
215247
this.contentType = contentType;
216248
this.addToTransactions = addToTransactions;
217249
}
@@ -240,6 +272,7 @@ public Attachment(
240272
this.pathname = pathname;
241273
this.filename = filename;
242274
this.serializable = null;
275+
this.byteProvider = null;
243276
this.contentType = contentType;
244277
this.addToTransactions = addToTransactions;
245278
this.attachmentType = attachmentType;
@@ -310,16 +343,35 @@ boolean isAddToTransactions() {
310343
return attachmentType;
311344
}
312345

346+
public @Nullable Callable<byte[]> getByteProvider() {
347+
return byteProvider;
348+
}
349+
313350
/**
314351
* Creates a new Screenshot Attachment
315352
*
316-
* @param screenshotBytes the array bytes
353+
* @param screenshotBytes the array bytes of the PNG screenshot
317354
* @return the Attachment
318355
*/
319356
public static @NotNull Attachment fromScreenshot(final byte[] screenshotBytes) {
320357
return new Attachment(screenshotBytes, "screenshot.png", "image/png", false);
321358
}
322359

360+
/**
361+
* Creates a new Screenshot Attachment
362+
*
363+
* @param provider the mechanism providing the screenshot payload
364+
* @return the Attachment
365+
*/
366+
public static @NotNull Attachment fromByteProvider(
367+
final @NotNull Callable<byte[]> provider,
368+
final @NotNull String filename,
369+
final @Nullable String contentType,
370+
final boolean addToTransactions) {
371+
return new Attachment(
372+
provider, filename, contentType, DEFAULT_ATTACHMENT_TYPE, addToTransactions);
373+
}
374+
323375
/**
324376
* Creates a new View Hierarchy Attachment
325377
*

0 commit comments

Comments
 (0)