Skip to content

Commit

Permalink
chore(screenshots): Enable screenshots for Hybrid SDKs (#2360)
Browse files Browse the repository at this point in the history
  • Loading branch information
krystofwoldrich committed Nov 21, 2022
2 parents d49f98e + 6a469aa commit 16371c5
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 85 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,7 @@

- Use `canonicalName` in Fragment Integration for better de-obfuscation ([#2379](https://github.com/getsentry/sentry-java/pull/2379))
- Fix Timber and Fragment integrations auto-installation for obfuscated builds ([#2379](https://github.com/getsentry/sentry-java/pull/2379))
- Don't attach screenshots to events from Hybrid SDKs ([#2360](https://github.com/getsentry/sentry-java/pull/2360))

## 6.8.0

Expand Down
16 changes: 16 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Expand Up @@ -22,6 +22,15 @@ public final class io/sentry/android/core/ActivityLifecycleIntegration : android
public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V
}

public final class io/sentry/android/core/AndroidLogger : io/sentry/ILogger {
public fun <init> ()V
public fun <init> (Ljava/lang/String;)V
public fun isEnabled (Lio/sentry/SentryLevel;)Z
public fun log (Lio/sentry/SentryLevel;Ljava/lang/String;Ljava/lang/Throwable;)V
public fun log (Lio/sentry/SentryLevel;Ljava/lang/String;[Ljava/lang/Object;)V
public fun log (Lio/sentry/SentryLevel;Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
}

public final class io/sentry/android/core/AnrIntegration : io/sentry/Integration, java/io/Closeable {
public fun <init> (Landroid/content/Context;)V
public fun close ()V
Expand Down Expand Up @@ -72,6 +81,13 @@ public final class io/sentry/android/core/BuildInfoProvider {
public fun isEmulator ()Ljava/lang/Boolean;
}

public class io/sentry/android/core/CurrentActivityHolder {
public fun clearActivity ()V
public fun getActivity ()Landroid/app/Activity;
public static fun getInstance ()Lio/sentry/android/core/CurrentActivityHolder;
public fun setActivity (Landroid/app/Activity;)V
}

public abstract class io/sentry/android/core/EnvelopeFileObserverIntegration : io/sentry/Integration, java/io/Closeable {
public fun <init> ()V
public fun close ()V
Expand Down
Expand Up @@ -3,12 +3,22 @@
import android.util.Log;
import io.sentry.ILogger;
import io.sentry.SentryLevel;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

final class AndroidLogger implements ILogger {
@ApiStatus.Internal
public final class AndroidLogger implements ILogger {

private static final String tag = "Sentry";
private final @NotNull String tag;

public AndroidLogger() {
this("Sentry");
}

public AndroidLogger(final @NotNull String tag) {
this.tag = tag;
}

@SuppressWarnings("AnnotateFormatMethod")
@Override
Expand Down
@@ -0,0 +1,41 @@
package io.sentry.android.core;

import android.app.Activity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

@ApiStatus.Internal
public class CurrentActivityHolder {

private static final @NotNull CurrentActivityHolder instance = new CurrentActivityHolder();

private CurrentActivityHolder() {}

private @Nullable WeakReference<Activity> currentActivity;

public static @NonNull CurrentActivityHolder getInstance() {
return instance;
}

public @Nullable Activity getActivity() {
if (currentActivity != null) {
return currentActivity.get();
}
return null;
}

public void setActivity(final @NonNull Activity activity) {
if (currentActivity != null && currentActivity.get() == activity) {
return;
}

currentActivity = new WeakReference<>(activity);
}

public void clearActivity() {
currentActivity = null;
}
}
@@ -1,27 +1,22 @@
package io.sentry.android.core;

import static io.sentry.TypeCheckHint.ANDROID_ACTIVITY;
import static io.sentry.android.core.internal.util.ScreenshotUtils.takeScreenshot;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Application;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.sentry.Attachment;
import io.sentry.EventProcessor;
import io.sentry.Hint;
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
import io.sentry.util.HintUtils;
import io.sentry.util.Objects;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.lang.ref.WeakReference;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

Expand All @@ -35,7 +30,6 @@ public final class ScreenshotEventProcessor

private final @NotNull Application application;
private final @NotNull SentryAndroidOptions options;
private @Nullable WeakReference<Activity> currentActivity;
private final @NotNull BuildInfoProvider buildInfoProvider;
private boolean lifecycleCallbackInstalled = true;

Expand All @@ -54,7 +48,7 @@ public ScreenshotEventProcessor(
@SuppressWarnings("NullAway")
@Override
public @NotNull SentryEvent process(final @NotNull SentryEvent event, @NotNull Hint hint) {
if (!lifecycleCallbackInstalled) {
if (!lifecycleCallbackInstalled || !event.isErrored()) {
return event;
}
if (!options.isAttachScreenshot()) {
Expand All @@ -69,60 +63,24 @@ public ScreenshotEventProcessor(

return event;
}
final Activity activity = CurrentActivityHolder.getInstance().getActivity();
if (activity == null || HintUtils.isFromHybridSdk(hint)) {
return event;
}

if (event.isErrored() && currentActivity != null) {
final Activity activity = currentActivity.get();
if (isActivityValid(activity)
&& activity.getWindow() != null
&& activity.getWindow().getDecorView() != null
&& activity.getWindow().getDecorView().getRootView() != null) {
final View view = activity.getWindow().getDecorView().getRootView();

if (view.getWidth() > 0 && view.getHeight() > 0) {
try {
// ARGB_8888 -> This configuration is very flexible and offers the best quality
final Bitmap bitmap =
Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);

final Canvas canvas = new Canvas(bitmap);
view.draw(canvas);

final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

// 0 meaning compress for small size, 100 meaning compress for max quality.
// Some formats, like PNG which is lossless, will ignore the quality setting.
bitmap.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream);

if (byteArrayOutputStream.size() > 0) {
// screenshot png is around ~100-150 kb
hint.setScreenshot(Attachment.fromScreenshot(byteArrayOutputStream.toByteArray()));
hint.set(ANDROID_ACTIVITY, activity);
} else {
this.options
.getLogger()
.log(SentryLevel.DEBUG, "Screenshot is 0 bytes, not attaching the image.");
}
} catch (Throwable e) {
this.options.getLogger().log(SentryLevel.ERROR, "Taking screenshot failed.", e);
}
} else {
this.options
.getLogger()
.log(SentryLevel.DEBUG, "View's width and height is zeroed, not taking screenshot.");
}
} else {
this.options
.getLogger()
.log(SentryLevel.DEBUG, "Activity isn't valid, not taking screenshot.");
}
final byte[] screenshot = takeScreenshot(activity, options.getLogger(), buildInfoProvider);
if (screenshot == null) {
return event;
}

hint.setScreenshot(Attachment.fromScreenshot(screenshot));
hint.set(ANDROID_ACTIVITY, activity);
return event;
}

@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
setCurrentActivity(activity);
CurrentActivityHolder.getInstance().setActivity(activity);
}

@Override
Expand Down Expand Up @@ -157,32 +115,17 @@ public void onActivityDestroyed(@NonNull Activity activity) {
public void close() throws IOException {
if (options.isAttachScreenshot()) {
application.unregisterActivityLifecycleCallbacks(this);
currentActivity = null;
CurrentActivityHolder.getInstance().clearActivity();
}
}

private void cleanCurrentActivity(@NonNull Activity activity) {
if (currentActivity != null && currentActivity.get() == activity) {
currentActivity = null;
if (CurrentActivityHolder.getInstance().getActivity() == activity) {
CurrentActivityHolder.getInstance().clearActivity();
}
}

private void setCurrentActivity(@NonNull Activity activity) {
if (currentActivity != null && currentActivity.get() == activity) {
return;
}
currentActivity = new WeakReference<>(activity);
}

@SuppressLint("NewApi")
private boolean isActivityValid(@Nullable Activity activity) {
if (activity == null) {
return false;
}
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
return !activity.isFinishing() && !activity.isDestroyed();
} else {
return !activity.isFinishing();
}
CurrentActivityHolder.getInstance().setActivity(activity);
}
}
@@ -0,0 +1,71 @@
package io.sentry.android.core.internal.util;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.os.Build;
import android.view.View;
import androidx.annotation.Nullable;
import io.sentry.ILogger;
import io.sentry.SentryLevel;
import io.sentry.android.core.BuildInfoProvider;
import java.io.ByteArrayOutputStream;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

@ApiStatus.Internal
public class ScreenshotUtils {
public static @Nullable byte[] takeScreenshot(
final @NotNull Activity activity,
final @NotNull ILogger logger,
final @NotNull BuildInfoProvider buildInfoProvider) {
if (!isActivityValid(activity, buildInfoProvider)
|| activity.getWindow() == null
|| activity.getWindow().getDecorView() == null
|| activity.getWindow().getDecorView().getRootView() == null) {
logger.log(SentryLevel.DEBUG, "Activity isn't valid, not taking screenshot.");
return null;
}

final View view = activity.getWindow().getDecorView().getRootView();
if (view.getWidth() <= 0 || view.getHeight() <= 0) {
logger.log(SentryLevel.DEBUG, "View's width and height is zeroed, not taking screenshot.");
return null;
}

try (final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
// ARGB_8888 -> This configuration is very flexible and offers the best quality
final Bitmap bitmap =
Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);

final Canvas canvas = new Canvas(bitmap);
view.draw(canvas);

// 0 meaning compress for small size, 100 meaning compress for max quality.
// Some formats, like PNG which is lossless, will ignore the quality setting.
bitmap.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream);

if (byteArrayOutputStream.size() <= 0) {
logger.log(SentryLevel.DEBUG, "Screenshot is 0 bytes, not attaching the image.");
return null;
}

// screenshot png is around ~100-150 kb
return byteArrayOutputStream.toByteArray();
} catch (Throwable e) {
logger.log(SentryLevel.ERROR, "Taking screenshot failed.", e);
}
return null;
}

@SuppressLint("NewApi")
private static boolean isActivityValid(
final @NotNull Activity activity, final @NotNull BuildInfoProvider buildInfoProvider) {
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
return !activity.isFinishing() && !activity.isDestroyed();
} else {
return !activity.isFinishing();
}
}
}
6 changes: 6 additions & 0 deletions sentry/api/sentry.api
Expand Up @@ -1901,6 +1901,10 @@ public final class io/sentry/TypeCheckHint {
public static final field OKHTTP_RESPONSE Ljava/lang/String;
public static final field OPEN_FEIGN_REQUEST Ljava/lang/String;
public static final field OPEN_FEIGN_RESPONSE Ljava/lang/String;
public static final field SENTRY_DART_SDK_NAME Ljava/lang/String;
public static final field SENTRY_DOTNET_SDK_NAME Ljava/lang/String;
public static final field SENTRY_IS_FROM_HYBRID_SDK Ljava/lang/String;
public static final field SENTRY_JAVASCRIPT_SDK_NAME Ljava/lang/String;
public static final field SENTRY_SYNTHETIC_EXCEPTION Ljava/lang/String;
public static final field SENTRY_TYPE_CHECK_HINT Ljava/lang/String;
public static final field SERVLET_REQUEST Ljava/lang/String;
Expand Down Expand Up @@ -3362,10 +3366,12 @@ public final class io/sentry/util/HintUtils {
public static fun createWithTypeCheckHint (Ljava/lang/Object;)Lio/sentry/Hint;
public static fun getSentrySdkHint (Lio/sentry/Hint;)Ljava/lang/Object;
public static fun hasType (Lio/sentry/Hint;Ljava/lang/Class;)Z
public static fun isFromHybridSdk (Lio/sentry/Hint;)Z
public static fun runIfDoesNotHaveType (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/util/HintUtils$SentryNullableConsumer;)V
public static fun runIfHasType (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/util/HintUtils$SentryConsumer;)V
public static fun runIfHasType (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/util/HintUtils$SentryConsumer;Lio/sentry/util/HintUtils$SentryHintFallback;)V
public static fun runIfHasTypeLogIfNot (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/ILogger;Lio/sentry/util/HintUtils$SentryConsumer;)V
public static fun setIsFromHybridSdk (Lio/sentry/Hint;Ljava/lang/String;)V
public static fun setTypeCheckHint (Lio/sentry/Hint;Ljava/lang/Object;)V
public static fun shouldApplyScopeData (Lio/sentry/Hint;)Z
}
Expand Down
3 changes: 3 additions & 0 deletions sentry/src/main/java/io/sentry/OutboxSender.java
Expand Up @@ -133,6 +133,9 @@ private void processEnvelope(final @NotNull SentryEnvelope envelope, final @NotN
if (event == null) {
logEnvelopeItemNull(item, currentItem);
} else {
if (event.getSdk() != null) {
HintUtils.setIsFromHybridSdk(hint, event.getSdk().getName());
}
if (envelope.getHeader().getEventId() != null
&& !envelope.getHeader().getEventId().equals(event.getEventId())) {
logUnexpectedEventId(envelope, event.getEventId(), currentItem);
Expand Down
9 changes: 9 additions & 0 deletions sentry/src/main/java/io/sentry/TypeCheckHint.java
Expand Up @@ -7,6 +7,15 @@ public final class TypeCheckHint {

@ApiStatus.Internal public static final String SENTRY_TYPE_CHECK_HINT = "sentry:typeCheckHint";

@ApiStatus.Internal
public static final String SENTRY_IS_FROM_HYBRID_SDK = "sentry:isFromHybridSdk";

@ApiStatus.Internal public static final String SENTRY_JAVASCRIPT_SDK_NAME = "sentry.javascript";

@ApiStatus.Internal public static final String SENTRY_DOTNET_SDK_NAME = "sentry.dotnet";

@ApiStatus.Internal public static final String SENTRY_DART_SDK_NAME = "sentry.dart";

/** Used for Synthetic exceptions. */
public static final String SENTRY_SYNTHETIC_EXCEPTION = "syntheticException";

Expand Down

0 comments on commit 16371c5

Please sign in to comment.