Skip to content

Commit 74e9aa1

Browse files
authored
Merge 499c540 into c3f503e
2 parents c3f503e + 499c540 commit 74e9aa1

File tree

4 files changed

+199
-18
lines changed

4 files changed

+199
-18
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Features
66

77
- Add thread information to spans ([#2998](https://github.com/getsentry/sentry-java/pull/2998))
8+
- Use PixelCopy API for capturing screenshots on API level 24+ ([#3008](https://github.com/getsentry/sentry-java/pull/3008))
89

910
### Fixes
1011

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

+77-18
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@
55
import android.graphics.Bitmap;
66
import android.graphics.Canvas;
77
import android.os.Build;
8+
import android.os.Handler;
9+
import android.os.HandlerThread;
10+
import android.view.PixelCopy;
811
import android.view.View;
12+
import android.view.Window;
913
import androidx.annotation.Nullable;
14+
import androidx.annotation.RequiresApi;
1015
import io.sentry.ILogger;
1116
import io.sentry.SentryLevel;
1217
import io.sentry.android.core.BuildInfoProvider;
1318
import io.sentry.util.thread.IMainThreadChecker;
1419
import java.io.ByteArrayOutputStream;
1520
import java.util.concurrent.CountDownLatch;
1621
import java.util.concurrent.TimeUnit;
22+
import java.util.concurrent.atomic.AtomicBoolean;
1723
import org.jetbrains.annotations.ApiStatus;
1824
import org.jetbrains.annotations.NotNull;
1925

@@ -30,21 +36,36 @@ public class ScreenshotUtils {
3036
activity, AndroidMainThreadChecker.getInstance(), logger, buildInfoProvider);
3137
}
3238

39+
@RequiresApi(api = Build.VERSION_CODES.O)
3340
public static @Nullable byte[] takeScreenshot(
3441
final @NotNull Activity activity,
3542
final @NotNull IMainThreadChecker mainThreadChecker,
3643
final @NotNull ILogger logger,
3744
final @NotNull BuildInfoProvider buildInfoProvider) {
3845

39-
if (!isActivityValid(activity, buildInfoProvider)
40-
|| activity.getWindow() == null
41-
|| activity.getWindow().getDecorView() == null
42-
|| activity.getWindow().getDecorView().getRootView() == null) {
46+
if (!isActivityValid(activity, buildInfoProvider)) {
4347
logger.log(SentryLevel.DEBUG, "Activity isn't valid, not taking screenshot.");
4448
return null;
4549
}
4650

47-
final View view = activity.getWindow().getDecorView().getRootView();
51+
final @Nullable Window window = activity.getWindow();
52+
if (window == null) {
53+
logger.log(SentryLevel.DEBUG, "Activity window is null, not taking screenshot.");
54+
return null;
55+
}
56+
57+
final @Nullable View decorView = window.peekDecorView();
58+
if (decorView == null) {
59+
logger.log(SentryLevel.DEBUG, "DecorView is null, not taking screenshot.");
60+
return null;
61+
}
62+
63+
final @Nullable View view = decorView.getRootView();
64+
if (view == null) {
65+
logger.log(SentryLevel.DEBUG, "Root view is null, not taking screenshot.");
66+
return null;
67+
}
68+
4869
if (view.getWidth() <= 0 || view.getHeight() <= 0) {
4970
logger.log(SentryLevel.DEBUG, "View's width and height is zeroed, not taking screenshot.");
5071
return null;
@@ -55,20 +76,58 @@ public class ScreenshotUtils {
5576
final Bitmap bitmap =
5677
Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
5778

58-
final Canvas canvas = new Canvas(bitmap);
59-
if (mainThreadChecker.isMainThread()) {
60-
view.draw(canvas);
61-
} else {
62-
final @NotNull CountDownLatch latch = new CountDownLatch(1);
63-
activity.runOnUiThread(
64-
() -> {
65-
try {
66-
view.draw(canvas);
79+
final @NotNull CountDownLatch latch = new CountDownLatch(1);
80+
81+
// Use Pixel Copy API on new devices, fallback to canvas rendering on older ones
82+
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.O) {
83+
84+
final HandlerThread thread = new HandlerThread("SentryScreenshot");
85+
thread.start();
86+
87+
boolean success = false;
88+
try {
89+
final Handler handler = new Handler(thread.getLooper());
90+
final AtomicBoolean copyResultSuccess = new AtomicBoolean(false);
91+
92+
PixelCopy.request(
93+
window,
94+
bitmap,
95+
copyResult -> {
96+
copyResultSuccess.set(copyResult == PixelCopy.SUCCESS);
6797
latch.countDown();
68-
} catch (Throwable e) {
69-
logger.log(SentryLevel.ERROR, "Taking screenshot failed (view.draw).", e);
70-
}
71-
});
98+
},
99+
handler);
100+
101+
success =
102+
latch.await(CAPTURE_TIMEOUT_MS, TimeUnit.MILLISECONDS) && copyResultSuccess.get();
103+
} catch (Throwable e) {
104+
// ignored
105+
logger.log(SentryLevel.ERROR, "Taking screenshot using PixelCopy failed.", e);
106+
} finally {
107+
thread.quit();
108+
}
109+
110+
if (!success) {
111+
return null;
112+
}
113+
} else {
114+
final Canvas canvas = new Canvas(bitmap);
115+
if (mainThreadChecker.isMainThread()) {
116+
view.draw(canvas);
117+
latch.countDown();
118+
} else {
119+
activity.runOnUiThread(
120+
() -> {
121+
try {
122+
view.draw(canvas);
123+
} catch (Throwable e) {
124+
logger.log(SentryLevel.ERROR, "Taking screenshot failed (view.draw).", e);
125+
} finally {
126+
latch.countDown();
127+
}
128+
});
129+
}
130+
72131
if (!latch.await(CAPTURE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
73132
return null;
74133
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class ScreenshotEventProcessorTest {
4646
whenever(rootView.height).thenReturn(1)
4747
whenever(view.rootView).thenReturn(rootView)
4848
whenever(window.decorView).thenReturn(view)
49+
whenever(window.peekDecorView()).thenReturn(view)
4950
whenever(activity.window).thenReturn(window)
5051
whenever(activity.runOnUiThread(any())).then {
5152
it.getArgument<Runnable>(0).run()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package io.sentry.android.core.internal.util
2+
3+
import android.app.Activity
4+
import android.os.Build
5+
import android.os.Bundle
6+
import android.view.View
7+
import android.view.Window
8+
import androidx.test.ext.junit.runners.AndroidJUnit4
9+
import io.sentry.ILogger
10+
import io.sentry.android.core.BuildInfoProvider
11+
import junit.framework.TestCase.assertNull
12+
import org.junit.runner.RunWith
13+
import org.mockito.kotlin.mock
14+
import org.mockito.kotlin.whenever
15+
import org.robolectric.Robolectric.buildActivity
16+
import org.robolectric.annotation.Config
17+
import org.robolectric.shadows.ShadowPixelCopy
18+
import kotlin.test.Test
19+
import kotlin.test.assertNotNull
20+
21+
@Config(
22+
shadows = [ShadowPixelCopy::class],
23+
sdk = [26, 33]
24+
)
25+
@RunWith(AndroidJUnit4::class)
26+
class ScreenshotUtilTest {
27+
28+
@Test
29+
fun `when window is null, null is returned`() {
30+
val activity = mock<Activity>()
31+
whenever(activity.isFinishing).thenReturn(false)
32+
whenever(activity.isDestroyed).thenReturn(false)
33+
34+
val data =
35+
ScreenshotUtils.takeScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
36+
assertNull(data)
37+
}
38+
39+
@Test
40+
fun `when decorView is null, null is returned`() {
41+
val activity = mock<Activity>()
42+
whenever(activity.isFinishing).thenReturn(false)
43+
whenever(activity.isDestroyed).thenReturn(false)
44+
whenever(activity.window).thenReturn(mock<Window>())
45+
46+
val data =
47+
ScreenshotUtils.takeScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
48+
assertNull(data)
49+
}
50+
51+
@Test
52+
fun `when root view is null, null is returned`() {
53+
val activity = mock<Activity>()
54+
val window = mock<Window>()
55+
val decorView = mock<View>()
56+
whenever(activity.isFinishing).thenReturn(false)
57+
whenever(activity.isDestroyed).thenReturn(false)
58+
whenever(activity.window).thenReturn(window)
59+
60+
whenever(window.peekDecorView()).thenReturn(decorView)
61+
62+
val data =
63+
ScreenshotUtils.takeScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
64+
assertNull(data)
65+
}
66+
67+
@Test
68+
fun `when root view has no size, null is returned`() {
69+
val activity = mock<Activity>()
70+
val window = mock<Window>()
71+
val decorView = mock<View>()
72+
val rootView = mock<View>()
73+
whenever(activity.isFinishing).thenReturn(false)
74+
whenever(activity.isDestroyed).thenReturn(false)
75+
whenever(activity.window).thenReturn(window)
76+
77+
whenever(window.peekDecorView()).thenReturn(decorView)
78+
whenever(decorView.rootView).thenReturn(rootView)
79+
80+
whenever(rootView.width).thenReturn(0)
81+
whenever(rootView.height).thenReturn(0)
82+
83+
val data =
84+
ScreenshotUtils.takeScreenshot(activity, mock<ILogger>(), mock<BuildInfoProvider>())
85+
assertNull(data)
86+
}
87+
88+
@Test
89+
fun `capturing screenshots works for Android O using PixelCopy API`() {
90+
val controller = buildActivity(ExampleActivity::class.java, null).setup()
91+
controller.create().start().resume()
92+
93+
val logger = mock<ILogger>()
94+
val buildInfoProvider = mock<BuildInfoProvider>()
95+
whenever(buildInfoProvider.sdkInfoVersion).thenReturn(Build.VERSION_CODES.O)
96+
97+
val data = ScreenshotUtils.takeScreenshot(controller.get(), logger, buildInfoProvider)
98+
assertNotNull(data)
99+
}
100+
101+
@Test
102+
fun `capturing screenshots works pre Android O using Canvas API`() {
103+
val controller = buildActivity(ExampleActivity::class.java, null).setup()
104+
controller.create().start().resume()
105+
106+
val logger = mock<ILogger>()
107+
val buildInfoProvider = mock<BuildInfoProvider>()
108+
whenever(buildInfoProvider.sdkInfoVersion).thenReturn(Build.VERSION_CODES.N)
109+
110+
val data = ScreenshotUtils.takeScreenshot(controller.get(), logger, buildInfoProvider)
111+
assertNotNull(data)
112+
}
113+
}
114+
115+
class ExampleActivity : Activity() {
116+
override fun onCreate(savedInstanceState: Bundle?) {
117+
super.onCreate(savedInstanceState)
118+
setContentView(View(this))
119+
}
120+
}

0 commit comments

Comments
 (0)