Skip to content

Commit

Permalink
Merge 2cce844 into 507f924
Browse files Browse the repository at this point in the history
  • Loading branch information
krystofwoldrich authored Nov 16, 2022
2 parents 507f924 + 2cce844 commit c768b19
Show file tree
Hide file tree
Showing 12 changed files with 203 additions and 86 deletions.
18 changes: 17 additions & 1 deletion sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ 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 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 +80,14 @@ public final class io/sentry/android/core/BuildInfoProvider {
public fun isEmulator ()Ljava/lang/Boolean;
}

public class io/sentry/android/core/CurrentActivityHolder {
public fun <init> ()V
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 @@ -105,7 +121,7 @@ public final class io/sentry/android/core/PhoneStateBreadcrumbsIntegration : io/
}

public final class io/sentry/android/core/ScreenshotEventProcessor : android/app/Application$ActivityLifecycleCallbacks, io/sentry/EventProcessor, java/io/Closeable {
public fun <init> (Landroid/app/Application;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/BuildInfoProvider;)V
public fun <init> (Landroid/app/Application;Lio/sentry/android/core/SentryAndroidOptions;)V
public fun close ()V
public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onActivityDestroyed (Landroid/app/Activity;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
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";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,7 @@ private static void installDefaultIntegrations(
if (isFragmentAvailable) {
options.addIntegration(new FragmentLifecycleIntegration((Application) context, true, true));
}
options.addEventProcessor(
new ScreenshotEventProcessor((Application) context, options, buildInfoProvider));
options.addEventProcessor(new ScreenshotEventProcessor((Application) context, options));
} else {
options
.getLogger()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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;

@ApiStatus.Internal
public class CurrentActivityHolder {

private static @Nullable CurrentActivityHolder instance;

private @Nullable WeakReference<Activity> currentActivity;

public static CurrentActivityHolder getInstance() {
if (instance != null) {
return instance;
}
instance = new CurrentActivityHolder();
return instance;
}

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

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

currentActivity = new WeakReference<>(activity);
}

public void clearActivity() {
currentActivity = null;
}
}
Original file line number Diff line number Diff line change
@@ -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,26 +30,22 @@ 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 final @NotNull CurrentActivityHolder currentActivityHolder;
private boolean lifecycleCallbackInstalled = true;

public ScreenshotEventProcessor(
final @NotNull Application application,
final @NotNull SentryAndroidOptions options,
final @NotNull BuildInfoProvider buildInfoProvider) {
final @NotNull Application application, final @NotNull SentryAndroidOptions options) {
this.application = Objects.requireNonNull(application, "Application is required");
this.options = Objects.requireNonNull(options, "SentryAndroidOptions is required");
this.buildInfoProvider =
Objects.requireNonNull(buildInfoProvider, "BuildInfoProvider is required");
this.currentActivityHolder = CurrentActivityHolder.getInstance();

application.registerActivityLifecycleCallbacks(this);
}

@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 +60,24 @@ public ScreenshotEventProcessor(

return event;
}
if (currentActivityHolder.getActivity() == 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(currentActivityHolder.getActivity(), options.getLogger());
if (screenshot == null) {
return event;
}

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

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

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

private void cleanCurrentActivity(@NonNull Activity activity) {
if (currentActivity != null && currentActivity.get() == activity) {
currentActivity = null;
if (currentActivityHolder.getActivity() == activity) {
currentActivityHolder.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.setActivity(activity);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.sentry.android.core.internal.util;

import android.app.Activity;
import android.os.Build;
import androidx.annotation.Nullable;
import org.jetbrains.annotations.ApiStatus;

@ApiStatus.Internal
public class ActivityUtils {
public static boolean isActivityValid(@Nullable Activity activity) {
if (activity == null) {
return false;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
return !activity.isFinishing() && !activity.isDestroyed();
} else {
return !activity.isFinishing();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package io.sentry.android.core.internal.util;

import static io.sentry.android.core.internal.util.ActivityUtils.isActivityValid;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.view.View;
import androidx.annotation.Nullable;
import io.sentry.ILogger;
import io.sentry.SentryLevel;
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 @Nullable Activity activity, final @NotNull ILogger logger) {
if (activity == null) {
return null;
}

if (!isActivityValid(activity)
|| 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 {
// 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) {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ class ScreenshotEventProcessorTest {

private class Fixture {
val application = mock<Application>()
val buildInfo = mock<BuildInfoProvider>()
val activity = mock<Activity>()
val window = mock<Window>()
val view = mock<View>()
Expand All @@ -49,7 +48,7 @@ class ScreenshotEventProcessorTest {
fun getSut(attachScreenshot: Boolean = false): ScreenshotEventProcessor {
options.isAttachScreenshot = attachScreenshot

return ScreenshotEventProcessor(application, options, buildInfo)
return ScreenshotEventProcessor(application, options)
}
}

Expand Down
Loading

0 comments on commit c768b19

Please sign in to comment.