processesInErrorState = null;
- try {
- // It can throw RuntimeException or OutOfMemoryError
- processesInErrorState = am.getProcessesInErrorState();
- } catch (Throwable e) {
- logger.log(SentryLevel.ERROR, "Error getting ActivityManager#getProcessesInErrorState.", e);
- }
- // if list is null, there's no process in ANR state.
- if (processesInErrorState != null) {
- for (ActivityManager.ProcessErrorStateInfo item : processesInErrorState) {
- if (item.condition == NOT_RESPONDING) {
- return true;
- }
- }
- }
- // when list is empty, or there's no element NOT_RESPONDING, we can assume the app is not
- // blocked
- return false;
- }
- return true;
- }
-
- public interface ANRListener {
- /**
- * Called when an ANR is detected.
- *
- * @param error The error describing the ANR.
- */
- void onAppNotResponding(@NotNull ApplicationNotResponding error);
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityBreadcrumbsIntegration.java
deleted file mode 100644
index 3cd7709c4be..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityBreadcrumbsIntegration.java
+++ /dev/null
@@ -1,142 +0,0 @@
-package io.sentry.android.core;
-
-import static io.sentry.TypeCheckHint.ANDROID_ACTIVITY;
-import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion;
-
-import android.app.Activity;
-import android.app.Application;
-import android.os.Bundle;
-import io.sentry.Breadcrumb;
-import io.sentry.Hint;
-import io.sentry.IScopes;
-import io.sentry.ISentryLifecycleToken;
-import io.sentry.Integration;
-import io.sentry.SentryLevel;
-import io.sentry.SentryOptions;
-import io.sentry.util.AutoClosableReentrantLock;
-import io.sentry.util.Objects;
-import java.io.Closeable;
-import java.io.IOException;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/** Automatically adds breadcrumbs for Activity Lifecycle Events. */
-public final class ActivityBreadcrumbsIntegration
- implements Integration, Closeable, Application.ActivityLifecycleCallbacks {
-
- private final @NotNull Application application;
- private @Nullable IScopes scopes;
- private boolean enabled;
- private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
-
- // TODO check if locking is even required at all for lifecycle methods
- public ActivityBreadcrumbsIntegration(final @NotNull Application application) {
- this.application = Objects.requireNonNull(application, "Application is required");
- }
-
- @Override
- public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) {
- final SentryAndroidOptions androidOptions =
- Objects.requireNonNull(
- (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null,
- "SentryAndroidOptions is required");
-
- this.scopes = Objects.requireNonNull(scopes, "Scopes are required");
- this.enabled = androidOptions.isEnableActivityLifecycleBreadcrumbs();
- options
- .getLogger()
- .log(SentryLevel.DEBUG, "ActivityBreadcrumbsIntegration enabled: %s", enabled);
-
- if (enabled) {
- application.registerActivityLifecycleCallbacks(this);
- options.getLogger().log(SentryLevel.DEBUG, "ActivityBreadcrumbIntegration installed.");
- addIntegrationToSdkVersion("ActivityBreadcrumbs");
- }
- }
-
- @Override
- public void close() throws IOException {
- if (enabled) {
- application.unregisterActivityLifecycleCallbacks(this);
- if (scopes != null) {
- scopes
- .getOptions()
- .getLogger()
- .log(SentryLevel.DEBUG, "ActivityBreadcrumbsIntegration removed.");
- }
- }
- }
-
- @Override
- public void onActivityCreated(
- final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- addBreadcrumb(activity, "created");
- }
- }
-
- @Override
- public void onActivityStarted(final @NotNull Activity activity) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- addBreadcrumb(activity, "started");
- }
- }
-
- @Override
- public void onActivityResumed(final @NotNull Activity activity) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- addBreadcrumb(activity, "resumed");
- }
- }
-
- @Override
- public void onActivityPaused(final @NotNull Activity activity) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- addBreadcrumb(activity, "paused");
- }
- }
-
- @Override
- public void onActivityStopped(final @NotNull Activity activity) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- addBreadcrumb(activity, "stopped");
- }
- }
-
- @Override
- public void onActivitySaveInstanceState(
- final @NotNull Activity activity, final @NotNull Bundle outState) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- addBreadcrumb(activity, "saveInstanceState");
- }
- }
-
- @Override
- public void onActivityDestroyed(final @NotNull Activity activity) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- addBreadcrumb(activity, "destroyed");
- }
- }
-
- private void addBreadcrumb(final @NotNull Activity activity, final @NotNull String state) {
- if (scopes == null) {
- return;
- }
-
- final Breadcrumb breadcrumb = new Breadcrumb();
- breadcrumb.setType("navigation");
- breadcrumb.setData("state", state);
- breadcrumb.setData("screen", getActivityName(activity));
- breadcrumb.setCategory("ui.lifecycle");
- breadcrumb.setLevel(SentryLevel.INFO);
-
- final Hint hint = new Hint();
- hint.set(ANDROID_ACTIVITY, activity);
-
- scopes.addBreadcrumb(breadcrumb, hint);
- }
-
- private @NotNull String getActivityName(final @NotNull Activity activity) {
- return activity.getClass().getSimpleName();
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java
deleted file mode 100644
index 3895819fc94..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java
+++ /dev/null
@@ -1,263 +0,0 @@
-package io.sentry.android.core;
-
-import android.app.Activity;
-import android.util.SparseIntArray;
-import androidx.core.app.FrameMetricsAggregator;
-import io.sentry.ISentryLifecycleToken;
-import io.sentry.MeasurementUnit;
-import io.sentry.SentryLevel;
-import io.sentry.android.core.internal.util.AndroidThreadChecker;
-import io.sentry.protocol.MeasurementValue;
-import io.sentry.protocol.SentryId;
-import io.sentry.util.AutoClosableReentrantLock;
-import io.sentry.util.LazyEvaluator;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.WeakHashMap;
-import java.util.concurrent.ConcurrentHashMap;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.jetbrains.annotations.TestOnly;
-import org.jetbrains.annotations.VisibleForTesting;
-
-/**
- * A class that tracks slow and frozen frames using the FrameMetricsAggregator class from
- * androidx.core package. It also checks if the FrameMetricsAggregator class is available at
- * runtime.
- *
- * If performance-v2 is enabled, frame metrics are recorded using {@link
- * io.sentry.android.core.internal.util.SentryFrameMetricsCollector} via {@link
- * SpanFrameMetricsCollector} instead and this implementation will no-op.
- */
-public final class ActivityFramesTracker {
-
- private @NotNull LazyEvaluator frameMetricsAggregator;
- private @NotNull final SentryAndroidOptions options;
-
- private final @NotNull Map>
- activityMeasurements = new ConcurrentHashMap<>();
- private final @NotNull Map frameCountAtStartSnapshots =
- new WeakHashMap<>();
-
- private final @NotNull MainLooperHandler handler;
- protected @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
-
- private final @NotNull LazyEvaluator androidXAvailable;
-
- public ActivityFramesTracker(
- final @NotNull io.sentry.util.LoadClass loadClass,
- final @NotNull SentryAndroidOptions options,
- final @NotNull MainLooperHandler handler) {
-
- androidXAvailable =
- loadClass.isClassAvailableLazy(
- "androidx.core.app.FrameMetricsAggregator", options.getLogger());
- frameMetricsAggregator = new LazyEvaluator<>(() -> new FrameMetricsAggregator());
-
- this.options = options;
- this.handler = handler;
- }
-
- public ActivityFramesTracker(
- final @NotNull io.sentry.util.LoadClass loadClass,
- final @NotNull SentryAndroidOptions options) {
- this(loadClass, options, new MainLooperHandler());
- }
-
- @TestOnly
- ActivityFramesTracker(
- final @NotNull io.sentry.util.LoadClass loadClass,
- final @NotNull SentryAndroidOptions options,
- final @NotNull MainLooperHandler handler,
- final @NotNull FrameMetricsAggregator frameMetricsAggregator) {
-
- this(loadClass, options, handler);
- this.frameMetricsAggregator = new LazyEvaluator<>(() -> frameMetricsAggregator);
- }
-
- @VisibleForTesting
- public boolean isFrameMetricsAggregatorAvailable() {
- return androidXAvailable.getValue()
- && options.isEnableFramesTracking()
- && !options.isEnablePerformanceV2();
- }
-
- @SuppressWarnings("NullAway")
- public void addActivity(final @NotNull Activity activity) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- if (!isFrameMetricsAggregatorAvailable()) {
- return;
- }
-
- runSafelyOnUiThread(
- () -> frameMetricsAggregator.getValue().add(activity), "FrameMetricsAggregator.add");
- snapshotFrameCountsAtStart(activity);
- }
- }
-
- private void snapshotFrameCountsAtStart(final @NotNull Activity activity) {
- FrameCounts frameCounts = calculateCurrentFrameCounts();
- if (frameCounts != null) {
- frameCountAtStartSnapshots.put(activity, frameCounts);
- }
- }
-
- private @Nullable FrameCounts calculateCurrentFrameCounts() {
- if (!isFrameMetricsAggregatorAvailable()) {
- return null;
- }
-
- if (!androidXAvailable.getValue()) {
- return null;
- }
-
- final @Nullable SparseIntArray[] framesRates = frameMetricsAggregator.getValue().getMetrics();
-
- int totalFrames = 0;
- int slowFrames = 0;
- int frozenFrames = 0;
-
- if (framesRates != null && framesRates.length > 0) {
- final SparseIntArray totalIndexArray = framesRates[FrameMetricsAggregator.TOTAL_INDEX];
- if (totalIndexArray != null) {
- for (int i = 0; i < totalIndexArray.size(); i++) {
- int frameTime = totalIndexArray.keyAt(i);
- int numFrames = totalIndexArray.valueAt(i);
- totalFrames += numFrames;
- // hard coded values, its also in the official android docs and frame metrics API
- if (frameTime > 700) {
- // frozen frames, threshold is 700ms
- frozenFrames += numFrames;
- } else if (frameTime > 16) {
- // slow frames, above 16ms, 60 frames/second
- slowFrames += numFrames;
- }
- }
- }
- }
-
- return new FrameCounts(totalFrames, slowFrames, frozenFrames);
- }
-
- @SuppressWarnings("NullAway")
- public void setMetrics(final @NotNull Activity activity, final @NotNull SentryId transactionId) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- if (!isFrameMetricsAggregatorAvailable()) {
- return;
- }
-
- // NOTE: removing an activity does not reset the frame counts, only reset() does
- // throws IllegalArgumentException when attempting to remove
- // OnFrameMetricsAvailableListener
- // that was never added.
- // there's no contains method.
- // throws NullPointerException when attempting to remove
- // OnFrameMetricsAvailableListener and
- // there was no
- // Observers, See
- // https://android.googlesource.com/platform/frameworks/base/+/140ff5ea8e2d99edc3fbe63a43239e459334c76b
- runSafelyOnUiThread(() -> frameMetricsAggregator.getValue().remove(activity), null);
-
- final @Nullable FrameCounts frameCounts = diffFrameCountsAtEnd(activity);
-
- if (frameCounts == null
- || (frameCounts.totalFrames == 0
- && frameCounts.slowFrames == 0
- && frameCounts.frozenFrames == 0)) {
- return;
- }
-
- final MeasurementValue tfValues =
- new MeasurementValue(frameCounts.totalFrames, MeasurementUnit.NONE);
- final MeasurementValue sfValues =
- new MeasurementValue(frameCounts.slowFrames, MeasurementUnit.NONE);
- final MeasurementValue ffValues =
- new MeasurementValue(frameCounts.frozenFrames, MeasurementUnit.NONE);
- final Map measurements = new HashMap<>();
- measurements.put(MeasurementValue.KEY_FRAMES_TOTAL, tfValues);
- measurements.put(MeasurementValue.KEY_FRAMES_SLOW, sfValues);
- measurements.put(MeasurementValue.KEY_FRAMES_FROZEN, ffValues);
-
- activityMeasurements.put(transactionId, measurements);
- }
- }
-
- private @Nullable FrameCounts diffFrameCountsAtEnd(final @NotNull Activity activity) {
- @Nullable final FrameCounts frameCountsAtStart = frameCountAtStartSnapshots.remove(activity);
- if (frameCountsAtStart == null) {
- return null;
- }
-
- @Nullable final FrameCounts frameCountsAtEnd = calculateCurrentFrameCounts();
- if (frameCountsAtEnd == null) {
- return null;
- }
-
- final int diffTotalFrames = frameCountsAtEnd.totalFrames - frameCountsAtStart.totalFrames;
- final int diffSlowFrames = frameCountsAtEnd.slowFrames - frameCountsAtStart.slowFrames;
- final int diffFrozenFrames = frameCountsAtEnd.frozenFrames - frameCountsAtStart.frozenFrames;
-
- return new FrameCounts(diffTotalFrames, diffSlowFrames, diffFrozenFrames);
- }
-
- @Nullable
- public Map takeMetrics(final @NotNull SentryId transactionId) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- if (!isFrameMetricsAggregatorAvailable()) {
- return null;
- }
-
- final Map stringMeasurementValueMap =
- activityMeasurements.get(transactionId);
- activityMeasurements.remove(transactionId);
- return stringMeasurementValueMap;
- }
- }
-
- @SuppressWarnings("NullAway")
- public void stop() {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- if (isFrameMetricsAggregatorAvailable()) {
- runSafelyOnUiThread(
- () -> frameMetricsAggregator.getValue().stop(), "FrameMetricsAggregator.stop");
- frameMetricsAggregator.getValue().reset();
- }
- activityMeasurements.clear();
- }
- }
-
- private void runSafelyOnUiThread(final Runnable runnable, final String tag) {
- try {
- if (AndroidThreadChecker.getInstance().isMainThread()) {
- runnable.run();
- } else {
- handler.post(
- () -> {
- try {
- runnable.run();
- } catch (Throwable ignored) {
- if (tag != null) {
- options.getLogger().log(SentryLevel.WARNING, "Failed to execute " + tag);
- }
- }
- });
- }
- } catch (Throwable ignored) {
- if (tag != null) {
- options.getLogger().log(SentryLevel.WARNING, "Failed to execute " + tag);
- }
- }
- }
-
- private static final class FrameCounts {
- private final int totalFrames;
- private final int slowFrames;
- private final int frozenFrames;
-
- private FrameCounts(final int totalFrames, final int slowFrames, final int frozenFrames) {
- this.totalFrames = totalFrames;
- this.slowFrames = slowFrames;
- this.frozenFrames = frozenFrames;
- }
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java
deleted file mode 100644
index 9d748e5a27a..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java
+++ /dev/null
@@ -1,799 +0,0 @@
-package io.sentry.android.core;
-
-import static io.sentry.MeasurementUnit.Duration.MILLISECOND;
-import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion;
-
-import android.app.Activity;
-import android.app.Application;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import io.sentry.FullyDisplayedReporter;
-import io.sentry.IScope;
-import io.sentry.IScopes;
-import io.sentry.ISentryLifecycleToken;
-import io.sentry.ISpan;
-import io.sentry.ITransaction;
-import io.sentry.Instrumenter;
-import io.sentry.Integration;
-import io.sentry.NoOpTransaction;
-import io.sentry.SentryDate;
-import io.sentry.SentryLevel;
-import io.sentry.SentryNanotimeDate;
-import io.sentry.SentryOptions;
-import io.sentry.SpanOptions;
-import io.sentry.SpanStatus;
-import io.sentry.TracesSamplingDecision;
-import io.sentry.TransactionContext;
-import io.sentry.TransactionOptions;
-import io.sentry.android.core.internal.util.ClassUtil;
-import io.sentry.android.core.internal.util.FirstDrawDoneListener;
-import io.sentry.android.core.performance.ActivityLifecycleSpanHelper;
-import io.sentry.android.core.performance.AppStartMetrics;
-import io.sentry.android.core.performance.TimeSpan;
-import io.sentry.protocol.MeasurementValue;
-import io.sentry.protocol.TransactionNameSource;
-import io.sentry.util.AutoClosableReentrantLock;
-import io.sentry.util.Objects;
-import io.sentry.util.TracingUtils;
-import java.io.Closeable;
-import java.io.IOException;
-import java.lang.ref.WeakReference;
-import java.util.Date;
-import java.util.Map;
-import java.util.WeakHashMap;
-import java.util.concurrent.Future;
-import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.TimeUnit;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.jetbrains.annotations.TestOnly;
-import org.jetbrains.annotations.VisibleForTesting;
-
-public final class ActivityLifecycleIntegration
- implements Integration, Closeable, Application.ActivityLifecycleCallbacks {
-
- static final String UI_LOAD_OP = "ui.load";
- static final String APP_START_WARM = "app.start.warm";
- static final String APP_START_COLD = "app.start.cold";
- static final String TTID_OP = "ui.load.initial_display";
- static final String TTFD_OP = "ui.load.full_display";
- static final long TTFD_TIMEOUT_MILLIS = 25000;
- private static final String TRACE_ORIGIN = "auto.ui.activity";
-
- private final @NotNull Application application;
- private final @NotNull BuildInfoProvider buildInfoProvider;
- private @Nullable IScopes scopes;
- private @Nullable SentryAndroidOptions options;
-
- private boolean performanceEnabled = false;
-
- private boolean timeToFullDisplaySpanEnabled = false;
-
- private boolean isAllActivityCallbacksAvailable;
-
- private boolean firstActivityCreated = false;
-
- private @Nullable FullyDisplayedReporter fullyDisplayedReporter = null;
- private @Nullable ISpan appStartSpan;
- private final @NotNull WeakHashMap ttidSpanMap = new WeakHashMap<>();
- private final @NotNull WeakHashMap ttfdSpanMap = new WeakHashMap<>();
- private final @NotNull WeakHashMap activitySpanHelpers =
- new WeakHashMap<>();
- private @NotNull SentryDate lastPausedTime = new SentryNanotimeDate(new Date(0), 0);
- private @Nullable Future> ttfdAutoCloseFuture = null;
-
- // WeakHashMap isn't thread safe but ActivityLifecycleCallbacks is only called from the
- // main-thread
- private final @NotNull WeakHashMap activitiesWithOngoingTransactions =
- new WeakHashMap<>();
-
- private final @NotNull ActivityFramesTracker activityFramesTracker;
- private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
- private boolean fullyDisplayedCalled = false;
- private final @NotNull AutoClosableReentrantLock fullyDisplayedLock =
- new AutoClosableReentrantLock();
-
- public ActivityLifecycleIntegration(
- final @NotNull Application application,
- final @NotNull BuildInfoProvider buildInfoProvider,
- final @NotNull ActivityFramesTracker activityFramesTracker) {
- this.application = Objects.requireNonNull(application, "Application is required");
- this.buildInfoProvider =
- Objects.requireNonNull(buildInfoProvider, "BuildInfoProvider is required");
- this.activityFramesTracker =
- Objects.requireNonNull(activityFramesTracker, "ActivityFramesTracker is required");
-
- if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.Q) {
- isAllActivityCallbacksAvailable = true;
- }
- }
-
- @Override
- public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) {
- this.options =
- Objects.requireNonNull(
- (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null,
- "SentryAndroidOptions is required");
-
- this.scopes = Objects.requireNonNull(scopes, "Scopes are required");
-
- performanceEnabled = isPerformanceEnabled(this.options);
- fullyDisplayedReporter = this.options.getFullyDisplayedReporter();
- timeToFullDisplaySpanEnabled = this.options.isEnableTimeToFullDisplayTracing();
-
- application.registerActivityLifecycleCallbacks(this);
- this.options.getLogger().log(SentryLevel.DEBUG, "ActivityLifecycleIntegration installed.");
- addIntegrationToSdkVersion("ActivityLifecycle");
- }
-
- private boolean isPerformanceEnabled(final @NotNull SentryAndroidOptions options) {
- return options.isTracingEnabled() && options.isEnableAutoActivityLifecycleTracing();
- }
-
- @Override
- public void close() throws IOException {
- application.unregisterActivityLifecycleCallbacks(this);
-
- if (options != null) {
- options.getLogger().log(SentryLevel.DEBUG, "ActivityLifecycleIntegration removed.");
- }
-
- activityFramesTracker.stop();
- }
-
- private @NotNull String getActivityName(final @NotNull Activity activity) {
- return activity.getClass().getSimpleName();
- }
-
- private void stopPreviousTransactions() {
- for (final Map.Entry entry :
- activitiesWithOngoingTransactions.entrySet()) {
- final ITransaction transaction = entry.getValue();
- final ISpan ttidSpan = ttidSpanMap.get(entry.getKey());
- final ISpan ttfdSpan = ttfdSpanMap.get(entry.getKey());
- finishTransaction(transaction, ttidSpan, ttfdSpan);
- }
- }
-
- private void startTracing(final @NotNull Activity activity) {
- WeakReference weakActivity = new WeakReference<>(activity);
- if (scopes != null && !isRunningTransactionOrTrace(activity)) {
- if (!performanceEnabled) {
- activitiesWithOngoingTransactions.put(activity, NoOpTransaction.getInstance());
- if (options.isEnableAutoTraceIdGeneration()) {
- TracingUtils.startNewTrace(scopes);
- }
- } else {
- // as we allow a single transaction running on the bound Scope, we finish the previous ones
- stopPreviousTransactions();
-
- final String activityName = getActivityName(activity);
-
- final @Nullable SentryDate appStartTime;
- final @Nullable Boolean coldStart;
- final @NotNull TimeSpan appStartTimeSpan =
- AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options);
-
- // we only track app start for processes that will show an Activity (full launch).
- // Here we check the process importance which will tell us that.
- final boolean foregroundImportance = ContextUtils.isForegroundImportance();
- if (foregroundImportance && appStartTimeSpan.hasStarted()) {
- appStartTime = appStartTimeSpan.getStartTimestamp();
- coldStart =
- AppStartMetrics.getInstance().getAppStartType() == AppStartMetrics.AppStartType.COLD;
- } else {
- appStartTime = null;
- coldStart = null;
- }
-
- final TransactionOptions transactionOptions = new TransactionOptions();
-
- // Set deadline timeout based on configured option
- final long deadlineTimeoutMillis = options.getDeadlineTimeout();
- // No deadline when zero or negative value is set
- transactionOptions.setDeadlineTimeout(
- deadlineTimeoutMillis <= 0 ? null : deadlineTimeoutMillis);
-
- if (options.isEnableActivityLifecycleTracingAutoFinish()) {
- transactionOptions.setIdleTimeout(options.getIdleTimeout());
- transactionOptions.setTrimEnd(true);
- }
- transactionOptions.setWaitForChildren(true);
- transactionOptions.setTransactionFinishedCallback(
- (finishingTransaction) -> {
- @Nullable Activity unwrappedActivity = weakActivity.get();
- if (unwrappedActivity != null) {
- activityFramesTracker.setMetrics(
- unwrappedActivity, finishingTransaction.getEventId());
- } else {
- if (options != null) {
- options
- .getLogger()
- .log(
- SentryLevel.WARNING,
- "Unable to track activity frames as the Activity %s has been destroyed.",
- activityName);
- }
- }
- });
-
- // This will be the start timestamp of the transaction, as well as the ttid/ttfd spans
- final @NotNull SentryDate ttidStartTime;
- final @Nullable TracesSamplingDecision appStartSamplingDecision;
-
- if (!(firstActivityCreated || appStartTime == null || coldStart == null)) {
- // The first activity ttid/ttfd spans should start at the app start time
- ttidStartTime = appStartTime;
- // The app start transaction inherits the sampling decision from the app start profiling,
- // then clears it
- appStartSamplingDecision = AppStartMetrics.getInstance().getAppStartSamplingDecision();
- AppStartMetrics.getInstance().setAppStartSamplingDecision(null);
- } else {
- // The ttid/ttfd spans should start when the previous activity called its onPause method
- ttidStartTime = lastPausedTime;
- appStartSamplingDecision = null;
- }
- transactionOptions.setStartTimestamp(ttidStartTime);
- transactionOptions.setAppStartTransaction(appStartSamplingDecision != null);
- setSpanOrigin(transactionOptions);
-
- // we can only bind to the scope if there's no running transaction
- ITransaction transaction =
- scopes.startTransaction(
- new TransactionContext(
- activityName,
- TransactionNameSource.COMPONENT,
- UI_LOAD_OP,
- appStartSamplingDecision),
- transactionOptions);
-
- final SpanOptions spanOptions = new SpanOptions();
- setSpanOrigin(spanOptions);
-
- // in case appStartTime isn't available, we don't create a span for it.
- if (!(firstActivityCreated || appStartTime == null || coldStart == null)) {
- // start specific span for app start
- appStartSpan =
- transaction.startChild(
- getAppStartOp(coldStart),
- getAppStartDesc(coldStart),
- appStartTime,
- Instrumenter.SENTRY,
- spanOptions);
-
- // in case there's already an end time (e.g. due to deferred SDK init)
- // we can finish the app-start span
- finishAppStartSpan();
- }
- final @NotNull ISpan ttidSpan =
- transaction.startChild(
- TTID_OP,
- getTtidDesc(activityName),
- ttidStartTime,
- Instrumenter.SENTRY,
- spanOptions);
- ttidSpanMap.put(activity, ttidSpan);
-
- if (timeToFullDisplaySpanEnabled && fullyDisplayedReporter != null && options != null) {
- final @NotNull ISpan ttfdSpan =
- transaction.startChild(
- TTFD_OP,
- getTtfdDesc(activityName),
- ttidStartTime,
- Instrumenter.SENTRY,
- spanOptions);
- try {
- ttfdSpanMap.put(activity, ttfdSpan);
- ttfdAutoCloseFuture =
- options
- .getExecutorService()
- .schedule(
- () -> finishExceededTtfdSpan(ttfdSpan, ttidSpan), TTFD_TIMEOUT_MILLIS);
- } catch (RejectedExecutionException e) {
- options
- .getLogger()
- .log(
- SentryLevel.ERROR,
- "Failed to call the executor. Time to full display span will not be finished automatically. Did you call Sentry.close()?",
- e);
- }
- }
-
- // lets bind to the scope so other integrations can pick it up
- scopes.configureScope(
- scope -> {
- applyScope(scope, transaction);
- });
-
- activitiesWithOngoingTransactions.put(activity, transaction);
- }
- }
- }
-
- private void setSpanOrigin(final @NotNull SpanOptions spanOptions) {
- spanOptions.setOrigin(TRACE_ORIGIN);
- }
-
- @VisibleForTesting
- void applyScope(final @NotNull IScope scope, final @NotNull ITransaction transaction) {
- scope.withTransaction(
- scopeTransaction -> {
- // we'd not like to overwrite existent transactions bound to the Scope
- // manually.
- if (scopeTransaction == null) {
- scope.setTransaction(transaction);
- } else if (options != null) {
- options
- .getLogger()
- .log(
- SentryLevel.DEBUG,
- "Transaction '%s' won't be bound to the Scope since there's one already in there.",
- transaction.getName());
- }
- });
- }
-
- @VisibleForTesting
- void clearScope(final @NotNull IScope scope, final @NotNull ITransaction transaction) {
- scope.withTransaction(
- scopeTransaction -> {
- if (scopeTransaction == transaction) {
- scope.clearTransaction();
- }
- });
- }
-
- private boolean isRunningTransactionOrTrace(final @NotNull Activity activity) {
- return activitiesWithOngoingTransactions.containsKey(activity);
- }
-
- private void stopTracing(final @NotNull Activity activity, final boolean shouldFinishTracing) {
- if (performanceEnabled && shouldFinishTracing) {
- final ITransaction transaction = activitiesWithOngoingTransactions.get(activity);
- finishTransaction(transaction, null, null);
- }
- }
-
- private void finishTransaction(
- final @Nullable ITransaction transaction,
- final @Nullable ISpan ttidSpan,
- final @Nullable ISpan ttfdSpan) {
- if (transaction != null) {
- // if io.sentry.traces.activity.auto-finish.enable is disabled, transaction may be already
- // finished manually when this method is called.
- if (transaction.isFinished()) {
- return;
- }
-
- // in case the ttidSpan isn't completed yet, we finish it as cancelled to avoid memory leak
- finishSpan(ttidSpan, SpanStatus.DEADLINE_EXCEEDED);
- finishExceededTtfdSpan(ttfdSpan, ttidSpan);
- cancelTtfdAutoClose();
-
- SpanStatus status = transaction.getStatus();
- // status might be set by other integrations, let's not overwrite it
- if (status == null) {
- status = SpanStatus.OK;
- }
- transaction.finish(status);
- if (scopes != null) {
- // make sure to remove the transaction from scope, as it may contain running children,
- // therefore `finish` method will not remove it from scope
- scopes.configureScope(
- scope -> {
- clearScope(scope, transaction);
- });
- }
- }
- }
-
- @Override
- public void onActivityPreCreated(
- final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {
- final ActivityLifecycleSpanHelper helper =
- new ActivityLifecycleSpanHelper(activity.getClass().getName());
- activitySpanHelpers.put(activity, helper);
- // The very first activity start timestamp cannot be set to the class instantiation time, as it
- // may happen before an activity is started (service, broadcast receiver, etc). So we set it
- // here.
- if (firstActivityCreated) {
- return;
- }
- lastPausedTime =
- scopes != null
- ? scopes.getOptions().getDateProvider().now()
- : AndroidDateUtils.getCurrentSentryDateTime();
- helper.setOnCreateStartTimestamp(lastPausedTime);
- }
-
- @Override
- public void onActivityCreated(
- final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {
- if (!isAllActivityCallbacksAvailable) {
- onActivityPreCreated(activity, savedInstanceState);
- }
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- if (scopes != null && options != null && options.isEnableScreenTracking()) {
- final @Nullable String activityClassName = ClassUtil.getClassName(activity);
- scopes.configureScope(scope -> scope.setScreen(activityClassName));
- }
- startTracing(activity);
- final @Nullable ISpan ttidSpan = ttidSpanMap.get(activity);
- final @Nullable ISpan ttfdSpan = ttfdSpanMap.get(activity);
-
- firstActivityCreated = true;
-
- if (performanceEnabled
- && ttidSpan != null
- && ttfdSpan != null
- && fullyDisplayedReporter != null) {
- fullyDisplayedReporter.registerFullyDrawnListener(
- () -> onFullFrameDrawn(ttidSpan, ttfdSpan));
- }
- }
- }
-
- @Override
- public void onActivityPostCreated(
- final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {
- final ActivityLifecycleSpanHelper helper = activitySpanHelpers.get(activity);
- if (helper != null) {
- helper.createAndStopOnCreateSpan(
- appStartSpan != null ? appStartSpan : activitiesWithOngoingTransactions.get(activity));
- }
- }
-
- @Override
- public void onActivityPreStarted(final @NotNull Activity activity) {
- final ActivityLifecycleSpanHelper helper = activitySpanHelpers.get(activity);
- if (helper != null) {
- helper.setOnStartStartTimestamp(
- options != null
- ? options.getDateProvider().now()
- : AndroidDateUtils.getCurrentSentryDateTime());
- }
- }
-
- @Override
- public void onActivityStarted(final @NotNull Activity activity) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- if (!isAllActivityCallbacksAvailable) {
- onActivityPostCreated(activity, null);
- onActivityPreStarted(activity);
- }
- if (performanceEnabled) {
- // The docs on the screen rendering performance tracing
- // (https://firebase.google.com/docs/perf-mon/screen-traces?platform=android#definition),
- // state that the tracing starts for every Activity class when the app calls
- // .onActivityStarted.
- // Adding an Activity in onActivityCreated leads to Window.FEATURE_NO_TITLE not
- // working. Moving this to onActivityStarted fixes the problem.
- activityFramesTracker.addActivity(activity);
- }
- }
- }
-
- @Override
- public void onActivityPostStarted(final @NotNull Activity activity) {
- final ActivityLifecycleSpanHelper helper = activitySpanHelpers.get(activity);
- if (helper != null) {
- helper.createAndStopOnStartSpan(
- appStartSpan != null ? appStartSpan : activitiesWithOngoingTransactions.get(activity));
- // Needed to handle hybrid SDKs
- helper.saveSpanToAppStartMetrics();
- }
- }
-
- @Override
- public void onActivityResumed(final @NotNull Activity activity) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- if (!isAllActivityCallbacksAvailable) {
- onActivityPostStarted(activity);
- }
- if (performanceEnabled) {
-
- final @Nullable ISpan ttidSpan = ttidSpanMap.get(activity);
- final @Nullable ISpan ttfdSpan = ttfdSpanMap.get(activity);
- if (activity.getWindow() != null) {
- FirstDrawDoneListener.registerForNextDraw(
- activity, () -> onFirstFrameDrawn(ttfdSpan, ttidSpan), buildInfoProvider);
- } else {
- // Posting a task to the main thread's handler will make it executed after it finished
- // its current job. That is, right after the activity draws the layout.
- new Handler(Looper.getMainLooper()).post(() -> onFirstFrameDrawn(ttfdSpan, ttidSpan));
- }
- }
- }
- }
-
- @Override
- public void onActivityPostResumed(@NotNull Activity activity) {
- // empty override, required to avoid a api-level breaking super.onActivityPostResumed() calls
- }
-
- @Override
- public void onActivityPrePaused(@NotNull Activity activity) {
- // only executed if API >= 29 otherwise it happens on onActivityPaused
- // as the SDK may gets (re-)initialized mid activity lifecycle, ensure we set the flag here as
- // well
- // this ensures any newly launched activity will not use the app start timestamp as txn start
- firstActivityCreated = true;
- lastPausedTime =
- scopes != null
- ? scopes.getOptions().getDateProvider().now()
- : AndroidDateUtils.getCurrentSentryDateTime();
- }
-
- @Override
- public void onActivityPaused(final @NotNull Activity activity) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- // only executed if API < 29 otherwise it happens on onActivityPrePaused
- if (!isAllActivityCallbacksAvailable) {
- onActivityPrePaused(activity);
- }
- }
- }
-
- @Override
- public void onActivityStopped(final @NotNull Activity activity) {
- // no-op (acquire lock if this no longer is no-op)
- }
-
- @Override
- public void onActivitySaveInstanceState(
- final @NotNull Activity activity, final @NotNull Bundle outState) {
- // no-op (acquire lock if this no longer is no-op)
- }
-
- @Override
- public void onActivityDestroyed(final @NotNull Activity activity) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- final ActivityLifecycleSpanHelper helper = activitySpanHelpers.remove(activity);
- if (helper != null) {
- helper.clear();
- }
- if (performanceEnabled) {
-
- // in case the appStartSpan isn't completed yet, we finish it as cancelled to avoid
- // memory leak
- finishSpan(appStartSpan, SpanStatus.CANCELLED);
-
- // we finish the ttidSpan as cancelled in case it isn't completed yet
- final ISpan ttidSpan = ttidSpanMap.get(activity);
- final ISpan ttfdSpan = ttfdSpanMap.get(activity);
- finishSpan(ttidSpan, SpanStatus.DEADLINE_EXCEEDED);
-
- // we finish the ttfdSpan as deadline_exceeded in case it isn't completed yet
- finishExceededTtfdSpan(ttfdSpan, ttidSpan);
- cancelTtfdAutoClose();
-
- // in case people opt-out enableActivityLifecycleTracingAutoFinish and forgot to finish it,
- // we make sure to finish it when the activity gets destroyed.
- stopTracing(activity, true);
-
- // set it to null in case its been just finished as cancelled
- appStartSpan = null;
- ttidSpanMap.remove(activity);
- ttfdSpanMap.remove(activity);
- }
-
- // clear it up, so we don't start again for the same activity if the activity is in the
- // activity stack still.
- // if the activity is opened again and not in memory, transactions will be created normally.
- activitiesWithOngoingTransactions.remove(activity);
-
- if (activitiesWithOngoingTransactions.isEmpty() && !activity.isChangingConfigurations()) {
- clear();
- }
- }
- }
-
- private void clear() {
- firstActivityCreated = false;
- lastPausedTime = new SentryNanotimeDate(new Date(0), 0);
- activitySpanHelpers.clear();
- }
-
- private void finishSpan(final @Nullable ISpan span) {
- if (span != null && !span.isFinished()) {
- span.finish();
- }
- }
-
- private void finishSpan(final @Nullable ISpan span, final @NotNull SentryDate endTimestamp) {
- finishSpan(span, endTimestamp, null);
- }
-
- private void finishSpan(
- final @Nullable ISpan span,
- final @NotNull SentryDate endTimestamp,
- final @Nullable SpanStatus spanStatus) {
- if (span != null && !span.isFinished()) {
- final @NotNull SpanStatus status =
- spanStatus != null
- ? spanStatus
- : span.getStatus() != null ? span.getStatus() : SpanStatus.OK;
- span.finish(status, endTimestamp);
- }
- }
-
- private void finishSpan(final @Nullable ISpan span, final @NotNull SpanStatus status) {
- if (span != null && !span.isFinished()) {
- span.finish(status);
- }
- }
-
- private void cancelTtfdAutoClose() {
- if (ttfdAutoCloseFuture != null) {
- ttfdAutoCloseFuture.cancel(false);
- ttfdAutoCloseFuture = null;
- }
- }
-
- private void onFirstFrameDrawn(final @Nullable ISpan ttfdSpan, final @Nullable ISpan ttidSpan) {
- // app start span
- final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance();
- final @NotNull TimeSpan appStartTimeSpan = appStartMetrics.getAppStartTimeSpan();
- final @NotNull TimeSpan sdkInitTimeSpan = appStartMetrics.getSdkInitTimeSpan();
-
- // and we need to set the end time of the app start here, after the first frame is drawn.
- if (appStartTimeSpan.hasStarted() && appStartTimeSpan.hasNotStopped()) {
- appStartTimeSpan.stop();
- }
- if (sdkInitTimeSpan.hasStarted() && sdkInitTimeSpan.hasNotStopped()) {
- sdkInitTimeSpan.stop();
- }
- finishAppStartSpan();
-
- // Sentry.reportFullyDisplayed can be run in any thread, so we have to ensure synchronization
- // with first frame drawn
- try (final @NotNull ISentryLifecycleToken ignored = fullyDisplayedLock.acquire()) {
- if (options != null && ttidSpan != null) {
- final SentryDate endDate = options.getDateProvider().now();
- final long durationNanos = endDate.diff(ttidSpan.getStartDate());
- final long durationMillis = TimeUnit.NANOSECONDS.toMillis(durationNanos);
- ttidSpan.setMeasurement(
- MeasurementValue.KEY_TIME_TO_INITIAL_DISPLAY, durationMillis, MILLISECOND);
- // If Sentry.reportFullyDisplayed was called before the first frame is drawn, we finish
- // the ttfd now
- if (ttfdSpan != null && fullyDisplayedCalled) {
- fullyDisplayedCalled = false;
- ttidSpan.setMeasurement(
- MeasurementValue.KEY_TIME_TO_FULL_DISPLAY, durationMillis, MILLISECOND);
- ttfdSpan.setMeasurement(
- MeasurementValue.KEY_TIME_TO_FULL_DISPLAY, durationMillis, MILLISECOND);
- finishSpan(ttfdSpan, endDate);
- }
-
- finishSpan(ttidSpan, endDate);
- } else {
- finishSpan(ttidSpan);
- if (fullyDisplayedCalled) {
- finishSpan(ttfdSpan);
- }
- }
- }
- }
-
- private void onFullFrameDrawn(final @NotNull ISpan ttidSpan, final @NotNull ISpan ttfdSpan) {
- cancelTtfdAutoClose();
- // Sentry.reportFullyDisplayed can be run in any thread, so we have to ensure synchronization
- // with first frame drawn
- try (final @NotNull ISentryLifecycleToken ignored = fullyDisplayedLock.acquire()) {
- // If the TTID span didn't finish, it means the first frame was not drawn yet, which means
- // Sentry.reportFullyDisplayed was called too early. We set a flag, so that whenever the TTID
- // will finish, we will finish the TTFD span as well.
- if (!ttidSpan.isFinished()) {
- fullyDisplayedCalled = true;
- return;
- }
- if (options != null) {
- final SentryDate endDate = options.getDateProvider().now();
- final long durationNanos = endDate.diff(ttfdSpan.getStartDate());
- final long durationMillis = TimeUnit.NANOSECONDS.toMillis(durationNanos);
- ttfdSpan.setMeasurement(
- MeasurementValue.KEY_TIME_TO_FULL_DISPLAY, durationMillis, MILLISECOND);
- finishSpan(ttfdSpan, endDate);
- } else {
- finishSpan(ttfdSpan);
- }
- }
- }
-
- private void finishExceededTtfdSpan(
- final @Nullable ISpan ttfdSpan, final @Nullable ISpan ttidSpan) {
- if (ttfdSpan == null || ttfdSpan.isFinished()) {
- return;
- }
- ttfdSpan.setDescription(getExceededTtfdDesc(ttfdSpan));
- // We set the end timestamp of the ttfd span to be equal to the ttid span.
- final @Nullable SentryDate ttidEndDate = ttidSpan != null ? ttidSpan.getFinishDate() : null;
- final @NotNull SentryDate ttfdEndDate =
- ttidEndDate != null ? ttidEndDate : ttfdSpan.getStartDate();
- finishSpan(ttfdSpan, ttfdEndDate, SpanStatus.DEADLINE_EXCEEDED);
- }
-
- @TestOnly
- @NotNull
- WeakHashMap getActivitiesWithOngoingTransactions() {
- return activitiesWithOngoingTransactions;
- }
-
- @TestOnly
- @NotNull
- WeakHashMap getActivitySpanHelpers() {
- return activitySpanHelpers;
- }
-
- @TestOnly
- void setFirstActivityCreated(boolean firstActivityCreated) {
- this.firstActivityCreated = firstActivityCreated;
- }
-
- @TestOnly
- @NotNull
- ActivityFramesTracker getActivityFramesTracker() {
- return activityFramesTracker;
- }
-
- @TestOnly
- @Nullable
- ISpan getAppStartSpan() {
- return appStartSpan;
- }
-
- @TestOnly
- @NotNull
- WeakHashMap getTtidSpanMap() {
- return ttidSpanMap;
- }
-
- @TestOnly
- @NotNull
- WeakHashMap getTtfdSpanMap() {
- return ttfdSpanMap;
- }
-
- private @NotNull String getTtidDesc(final @NotNull String activityName) {
- return activityName + " initial display";
- }
-
- private @NotNull String getTtfdDesc(final @NotNull String activityName) {
- return activityName + " full display";
- }
-
- private @NotNull String getExceededTtfdDesc(final @NotNull ISpan ttfdSpan) {
- final @Nullable String ttfdCurrentDescription = ttfdSpan.getDescription();
- if (ttfdCurrentDescription != null && ttfdCurrentDescription.endsWith(" - Deadline Exceeded"))
- return ttfdCurrentDescription;
- return ttfdSpan.getDescription() + " - Deadline Exceeded";
- }
-
- private @NotNull String getAppStartDesc(final boolean coldStart) {
- if (coldStart) {
- return "Cold Start";
- } else {
- return "Warm Start";
- }
- }
-
- private @NotNull String getAppStartOp(final boolean coldStart) {
- if (coldStart) {
- return APP_START_COLD;
- } else {
- return APP_START_WARM;
- }
- }
-
- private void finishAppStartSpan() {
- final @Nullable SentryDate appStartEndTime =
- AppStartMetrics.getInstance()
- .getAppStartTimeSpanWithFallback(options)
- .getProjectedStopTimestamp();
- if (performanceEnabled && appStartEndTime != null) {
- finishSpan(appStartSpan, appStartEndTime);
- }
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java
deleted file mode 100644
index 41362c9d93e..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java
+++ /dev/null
@@ -1,409 +0,0 @@
-package io.sentry.android.core;
-
-import static io.sentry.DataCategory.All;
-import static io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED;
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import android.os.Build;
-import io.sentry.CompositePerformanceCollector;
-import io.sentry.DataCategory;
-import io.sentry.IContinuousProfiler;
-import io.sentry.ILogger;
-import io.sentry.IScopes;
-import io.sentry.ISentryExecutorService;
-import io.sentry.ISentryLifecycleToken;
-import io.sentry.NoOpScopes;
-import io.sentry.PerformanceCollectionData;
-import io.sentry.ProfileChunk;
-import io.sentry.ProfileLifecycle;
-import io.sentry.Sentry;
-import io.sentry.SentryDate;
-import io.sentry.SentryLevel;
-import io.sentry.SentryNanotimeDate;
-import io.sentry.SentryOptions;
-import io.sentry.TracesSampler;
-import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
-import io.sentry.protocol.SentryId;
-import io.sentry.transport.RateLimiter;
-import io.sentry.util.AutoClosableReentrantLock;
-import io.sentry.util.LazyEvaluator;
-import io.sentry.util.SentryRandom;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.Future;
-import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.atomic.AtomicBoolean;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.jetbrains.annotations.VisibleForTesting;
-
-@ApiStatus.Internal
-public class AndroidContinuousProfiler
- implements IContinuousProfiler, RateLimiter.IRateLimitObserver {
- private static final long MAX_CHUNK_DURATION_MILLIS = 60000;
-
- private final @NotNull ILogger logger;
- private final @Nullable String profilingTracesDirPath;
- private final int profilingTracesHz;
- private final @NotNull LazyEvaluator.Evaluator executorServiceSupplier;
- private final @NotNull BuildInfoProvider buildInfoProvider;
- private boolean isInitialized = false;
- private final @NotNull SentryFrameMetricsCollector frameMetricsCollector;
- private @Nullable AndroidProfiler profiler = null;
- private boolean isRunning = false;
- private @Nullable IScopes scopes;
- private @Nullable Future> stopFuture;
- private @Nullable CompositePerformanceCollector performanceCollector;
- private final @NotNull List payloadBuilders = new ArrayList<>();
- private @NotNull SentryId profilerId = SentryId.EMPTY_ID;
- private @NotNull SentryId chunkId = SentryId.EMPTY_ID;
- private final @NotNull AtomicBoolean isClosed = new AtomicBoolean(false);
- private @NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate();
- private volatile boolean shouldSample = true;
- private boolean shouldStop = false;
- private boolean isSampled = false;
- private int rootSpanCounter = 0;
-
- private final AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
- private final AutoClosableReentrantLock payloadLock = new AutoClosableReentrantLock();
-
- public AndroidContinuousProfiler(
- final @NotNull BuildInfoProvider buildInfoProvider,
- final @NotNull SentryFrameMetricsCollector frameMetricsCollector,
- final @NotNull ILogger logger,
- final @Nullable String profilingTracesDirPath,
- final int profilingTracesHz,
- final @NotNull LazyEvaluator.Evaluator executorServiceSupplier) {
- this.logger = logger;
- this.frameMetricsCollector = frameMetricsCollector;
- this.buildInfoProvider = buildInfoProvider;
- this.profilingTracesDirPath = profilingTracesDirPath;
- this.profilingTracesHz = profilingTracesHz;
- this.executorServiceSupplier = executorServiceSupplier;
- }
-
- private void init() {
- // We initialize it only once
- if (isInitialized) {
- return;
- }
- isInitialized = true;
- if (profilingTracesDirPath == null) {
- logger.log(
- SentryLevel.WARNING,
- "Disabling profiling because no profiling traces dir path is defined in options.");
- return;
- }
- if (profilingTracesHz <= 0) {
- logger.log(
- SentryLevel.WARNING,
- "Disabling profiling because trace rate is set to %d",
- profilingTracesHz);
- return;
- }
-
- profiler =
- new AndroidProfiler(
- profilingTracesDirPath,
- (int) SECONDS.toMicros(1) / profilingTracesHz,
- frameMetricsCollector,
- null,
- logger);
- }
-
- @Override
- public void startProfiler(
- final @NotNull ProfileLifecycle profileLifecycle,
- final @NotNull TracesSampler tracesSampler) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- if (shouldSample) {
- isSampled = tracesSampler.sampleSessionProfile(SentryRandom.current().nextDouble());
- shouldSample = false;
- }
- if (!isSampled) {
- logger.log(SentryLevel.DEBUG, "Profiler was not started due to sampling decision.");
- return;
- }
- switch (profileLifecycle) {
- case TRACE:
- // rootSpanCounter should never be negative, unless the user changed profile lifecycle
- // while
- // the profiler is running or close() is called. This is just a safety check.
- if (rootSpanCounter < 0) {
- rootSpanCounter = 0;
- }
- rootSpanCounter++;
- break;
- case MANUAL:
- // We check if the profiler is already running and log a message only in manual mode,
- // since
- // in trace mode we can have multiple concurrent traces
- if (isRunning()) {
- logger.log(SentryLevel.DEBUG, "Profiler is already running.");
- return;
- }
- break;
- }
- if (!isRunning()) {
- logger.log(SentryLevel.DEBUG, "Started Profiler.");
- start();
- }
- }
- }
-
- private void initScopes() {
- if ((scopes == null || scopes == NoOpScopes.getInstance())
- && Sentry.getCurrentScopes() != NoOpScopes.getInstance()) {
- this.scopes = Sentry.getCurrentScopes();
- this.performanceCollector =
- Sentry.getCurrentScopes().getOptions().getCompositePerformanceCollector();
- final @Nullable RateLimiter rateLimiter = scopes.getRateLimiter();
- if (rateLimiter != null) {
- rateLimiter.addRateLimitObserver(this);
- }
- }
- }
-
- private void start() {
- initScopes();
-
- // Debug.startMethodTracingSampling() is only available since Lollipop, but Android Profiler
- // causes crashes on api 21 -> https://github.com/getsentry/sentry-java/issues/3392
- if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return;
-
- // Let's initialize trace folder and profiling interval
- init();
- // init() didn't create profiler, should never happen
- if (profiler == null) {
- return;
- }
-
- if (scopes != null) {
- final @Nullable RateLimiter rateLimiter = scopes.getRateLimiter();
- if (rateLimiter != null
- && (rateLimiter.isActiveForCategory(All)
- || rateLimiter.isActiveForCategory(DataCategory.ProfileChunkUi))) {
- logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler.");
- // Let's stop and reset profiler id, as the profile is now broken anyway
- stop(false);
- return;
- }
-
- // If device is offline, we don't start the profiler, to avoid flooding the cache
- // TODO .getConnectionStatus() may be blocking, investigate if this can be done async
- if (scopes.getOptions().getConnectionStatusProvider().getConnectionStatus() == DISCONNECTED) {
- logger.log(SentryLevel.WARNING, "Device is offline. Stopping profiler.");
- // Let's stop and reset profiler id, as the profile is now broken anyway
- stop(false);
- return;
- }
- startProfileChunkTimestamp = scopes.getOptions().getDateProvider().now();
- } else {
- startProfileChunkTimestamp = new SentryNanotimeDate();
- }
- final AndroidProfiler.ProfileStartData startData = profiler.start();
- // check if profiling started
- if (startData == null) {
- return;
- }
-
- isRunning = true;
-
- if (profilerId.equals(SentryId.EMPTY_ID)) {
- profilerId = new SentryId();
- }
-
- if (chunkId.equals(SentryId.EMPTY_ID)) {
- chunkId = new SentryId();
- }
-
- if (performanceCollector != null) {
- performanceCollector.start(chunkId.toString());
- }
-
- try {
- stopFuture =
- executorServiceSupplier.evaluate().schedule(() -> stop(true), MAX_CHUNK_DURATION_MILLIS);
- } catch (RejectedExecutionException e) {
- logger.log(
- SentryLevel.ERROR,
- "Failed to schedule profiling chunk finish. Did you call Sentry.close()?",
- e);
- shouldStop = true;
- }
- }
-
- @Override
- public void stopProfiler(final @NotNull ProfileLifecycle profileLifecycle) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- switch (profileLifecycle) {
- case TRACE:
- rootSpanCounter--;
- // If there are active spans, and profile lifecycle is trace, we don't stop the profiler
- if (rootSpanCounter > 0) {
- return;
- }
- // rootSpanCounter should never be negative, unless the user changed profile lifecycle
- // while the profiler is running or close() is called. This is just a safety check.
- if (rootSpanCounter < 0) {
- rootSpanCounter = 0;
- }
- shouldStop = true;
- break;
- case MANUAL:
- shouldStop = true;
- break;
- }
- }
- }
-
- private void stop(final boolean restartProfiler) {
- initScopes();
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- if (stopFuture != null) {
- stopFuture.cancel(true);
- }
- // check if profiler was created and it's running
- if (profiler == null || !isRunning) {
- // When the profiler is stopped due to an error (e.g. offline or rate limited), reset the
- // ids
- profilerId = SentryId.EMPTY_ID;
- chunkId = SentryId.EMPTY_ID;
- return;
- }
-
- // onTransactionStart() is only available since Lollipop_MR1
- // and SystemClock.elapsedRealtimeNanos() since Jelly Bean
- if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) {
- return;
- }
-
- List performanceCollectionData = null;
- if (performanceCollector != null) {
- performanceCollectionData = performanceCollector.stop(chunkId.toString());
- }
-
- final AndroidProfiler.ProfileEndData endData =
- profiler.endAndCollect(false, performanceCollectionData);
-
- // check if profiler end successfully
- if (endData == null) {
- logger.log(
- SentryLevel.ERROR,
- "An error occurred while collecting a profile chunk, and it won't be sent.");
- } else {
- // The scopes can be null if the profiler is started before the SDK is initialized (app
- // start profiling), meaning there's no scopes to send the chunks. In that case, we store
- // the data in a list and send it when the next chunk is finished.
- try (final @NotNull ISentryLifecycleToken ignored2 = payloadLock.acquire()) {
- payloadBuilders.add(
- new ProfileChunk.Builder(
- profilerId,
- chunkId,
- endData.measurementsMap,
- endData.traceFile,
- startProfileChunkTimestamp,
- ProfileChunk.PLATFORM_ANDROID));
- }
- }
-
- isRunning = false;
- // A chunk is finished. Next chunk will have a different id.
- chunkId = SentryId.EMPTY_ID;
-
- if (scopes != null) {
- sendChunks(scopes, scopes.getOptions());
- }
-
- if (restartProfiler && !shouldStop) {
- logger.log(SentryLevel.DEBUG, "Profile chunk finished. Starting a new one.");
- start();
- } else {
- // When the profiler is stopped manually, we have to reset its id
- profilerId = SentryId.EMPTY_ID;
- logger.log(SentryLevel.DEBUG, "Profile chunk finished.");
- }
- }
- }
-
- public void reevaluateSampling() {
- shouldSample = true;
- }
-
- @Override
- public void close(final boolean isTerminating) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- rootSpanCounter = 0;
- shouldStop = true;
- if (isTerminating) {
- stop(false);
- isClosed.set(true);
- }
- }
- }
-
- @Override
- public @NotNull SentryId getProfilerId() {
- return profilerId;
- }
-
- @Override
- public @NotNull SentryId getChunkId() {
- return chunkId;
- }
-
- private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOptions options) {
- try {
- options
- .getExecutorService()
- .submit(
- () -> {
- // SDK is closed, we don't send the chunks
- if (isClosed.get()) {
- return;
- }
- final ArrayList payloads = new ArrayList<>(payloadBuilders.size());
- try (final @NotNull ISentryLifecycleToken ignored = payloadLock.acquire()) {
- for (ProfileChunk.Builder builder : payloadBuilders) {
- payloads.add(builder.build(options));
- }
- payloadBuilders.clear();
- }
- for (ProfileChunk payload : payloads) {
- scopes.captureProfileChunk(payload);
- }
- });
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.DEBUG, "Failed to send profile chunks.", e);
- }
- }
-
- @Override
- public boolean isRunning() {
- return isRunning;
- }
-
- @VisibleForTesting
- @Nullable
- Future> getStopFuture() {
- return stopFuture;
- }
-
- @VisibleForTesting
- public int getRootSpanCounter() {
- return rootSpanCounter;
- }
-
- @Override
- public void onRateLimitChanged(@NotNull RateLimiter rateLimiter) {
- // We stop the profiler as soon as we are rate limited, to avoid the performance overhead
- if (rateLimiter.isActiveForCategory(All)
- || rateLimiter.isActiveForCategory(DataCategory.ProfileChunkUi)) {
- logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler.");
- stop(false);
- }
- // If we are not rate limited anymore, we don't do anything: the profile is broken, so it's
- // useless to restart it automatically
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java
deleted file mode 100644
index ea7a20deab1..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java
+++ /dev/null
@@ -1,109 +0,0 @@
-package io.sentry.android.core;
-
-import android.os.SystemClock;
-import android.system.Os;
-import android.system.OsConstants;
-import io.sentry.ILogger;
-import io.sentry.IPerformanceSnapshotCollector;
-import io.sentry.PerformanceCollectionData;
-import io.sentry.SentryLevel;
-import io.sentry.util.FileUtils;
-import io.sentry.util.Objects;
-import java.io.File;
-import java.io.IOException;
-import java.util.regex.Pattern;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-
-// The approach to get the cpu usage info was taken from
-// https://eng.lyft.com/monitoring-cpu-performance-of-lyfts-android-applications-4e36fafffe12
-// The content of the /proc/self/stat file is specified in
-// https://man7.org/linux/man-pages/man5/proc.5.html
-@ApiStatus.Internal
-public final class AndroidCpuCollector implements IPerformanceSnapshotCollector {
-
- private long lastRealtimeNanos = 0;
- private long lastCpuNanos = 0;
-
- /** Number of clock ticks per second. */
- private long clockSpeedHz = 1;
-
- private long numCores = 1;
- private final long NANOSECOND_PER_SECOND = 1_000_000_000;
-
- /** Number of nanoseconds per clock tick. */
- private double nanosecondsPerClockTick = NANOSECOND_PER_SECOND / (double) clockSpeedHz;
-
- /** File containing stats about this process. */
- private final @NotNull File selfStat = new File("/proc/self/stat");
-
- private final @NotNull ILogger logger;
- private boolean isEnabled = false;
- private final @NotNull Pattern newLinePattern = Pattern.compile("[\n\t\r ]");
-
- public AndroidCpuCollector(final @NotNull ILogger logger) {
- this.logger = Objects.requireNonNull(logger, "Logger is required.");
- }
-
- @Override
- public void setup() {
- isEnabled = true;
- clockSpeedHz = Os.sysconf(OsConstants._SC_CLK_TCK);
- numCores = Os.sysconf(OsConstants._SC_NPROCESSORS_CONF);
- nanosecondsPerClockTick = NANOSECOND_PER_SECOND / (double) clockSpeedHz;
- lastCpuNanos = readTotalCpuNanos();
- }
-
- @Override
- public void collect(final @NotNull PerformanceCollectionData performanceCollectionData) {
- if (!isEnabled) {
- return;
- }
- final long nowNanos = SystemClock.elapsedRealtimeNanos();
- final long realTimeNanosDiff = nowNanos - lastRealtimeNanos;
- lastRealtimeNanos = nowNanos;
- final long cpuNanos = readTotalCpuNanos();
- final long cpuNanosDiff = cpuNanos - lastCpuNanos;
- lastCpuNanos = cpuNanos;
- // Later we need to divide the percentage by the number of cores, otherwise we could
- // get a percentage value higher than 1. We also want to send the percentage as a
- // number from 0 to 100, so we are going to multiply it by 100
- final double cpuUsagePercentage = cpuNanosDiff / (double) realTimeNanosDiff;
-
- performanceCollectionData.setCpuUsagePercentage(
- (cpuUsagePercentage / (double) numCores) * 100.0);
- }
-
- /** Read the /proc/self/stat file and parses the result. */
- private long readTotalCpuNanos() {
- String stat = null;
- try {
- stat = FileUtils.readText(selfStat);
- } catch (IOException e) {
- // If an error occurs when reading the file, we avoid reading it again until the setup method
- // is called again
- isEnabled = false;
- logger.log(
- SentryLevel.WARNING, "Unable to read /proc/self/stat file. Disabling cpu collection.", e);
- }
- if (stat != null) {
- stat = stat.trim();
- String[] stats = newLinePattern.split(stat);
- try {
- // Amount of clock ticks this process has been scheduled in user mode
- long uTime = Long.parseLong(stats[13]);
- // Amount of clock ticks this process has been scheduled in kernel mode
- long sTime = Long.parseLong(stats[14]);
- // Amount of clock ticks this process' waited-for children has been scheduled in user mode
- long cuTime = Long.parseLong(stats[15]);
- // Amount of clock ticks this process' waited-for children has been scheduled in kernel mode
- long csTime = Long.parseLong(stats[16]);
- return (long) ((uTime + sTime + cuTime + csTime) * nanosecondsPerClockTick);
- } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
- logger.log(SentryLevel.ERROR, "Error parsing /proc/self/stat file.", e);
- return 0;
- }
- }
- return 0;
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidDateUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidDateUtils.java
deleted file mode 100644
index 55c1e2c7cec..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidDateUtils.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package io.sentry.android.core;
-
-import io.sentry.SentryDate;
-import io.sentry.SentryDateProvider;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-
-@ApiStatus.Internal
-public final class AndroidDateUtils {
-
- private static final SentryDateProvider dateProvider = new SentryAndroidDateProvider();
-
- /**
- * Get the current SentryDate (UTC).
- *
- * NOTE: options.getDateProvider() should be preferred. This is only a fallback for static
- * invocations.
- *
- * @return the UTC SentryDate
- */
- public static @NotNull SentryDate getCurrentSentryDateTime() {
- return dateProvider.now();
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidFatalLogger.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidFatalLogger.java
deleted file mode 100644
index 76d6ba99784..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidFatalLogger.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package io.sentry.android.core;
-
-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;
-
-@ApiStatus.Internal
-public final class AndroidFatalLogger implements ILogger {
-
- private final @NotNull String tag;
-
- public AndroidFatalLogger() {
- this("Sentry");
- }
-
- public AndroidFatalLogger(final @NotNull String tag) {
- this.tag = tag;
- }
-
- @SuppressWarnings("AnnotateFormatMethod")
- @Override
- public void log(
- final @NotNull SentryLevel level,
- final @NotNull String message,
- final @Nullable Object... args) {
- if (args == null || args.length == 0) {
- Log.println(toLogcatLevel(level), tag, message);
- } else {
- Log.println(toLogcatLevel(level), tag, String.format(message, args));
- }
- }
-
- @SuppressWarnings("AnnotateFormatMethod")
- @Override
- public void log(
- final @NotNull SentryLevel level,
- final @Nullable Throwable throwable,
- final @NotNull String message,
- final @Nullable Object... args) {
- if (args == null || args.length == 0) {
- log(level, message, throwable);
- } else {
- log(level, String.format(message, args), throwable);
- }
- }
-
- @Override
- public void log(
- final @NotNull SentryLevel level,
- final @NotNull String message,
- final @Nullable Throwable throwable) {
- Log.wtf(tag, message, throwable);
- }
-
- @Override
- public boolean isEnabled(@Nullable SentryLevel level) {
- return true;
- }
-
- private int toLogcatLevel(final @NotNull SentryLevel sentryLevel) {
- return Log.ASSERT;
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLogger.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLogger.java
deleted file mode 100644
index ef943c16968..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLogger.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package io.sentry.android.core;
-
-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;
-
-@ApiStatus.Internal
-public final class AndroidLogger implements ILogger {
-
- private final @NotNull String tag;
-
- public AndroidLogger() {
- this("Sentry");
- }
-
- public AndroidLogger(final @NotNull String tag) {
- this.tag = tag;
- }
-
- @SuppressWarnings("AnnotateFormatMethod")
- @Override
- public void log(
- final @NotNull SentryLevel level,
- final @NotNull String message,
- final @Nullable Object... args) {
- if (args == null || args.length == 0) {
- Log.println(toLogcatLevel(level), tag, message);
- } else {
- Log.println(toLogcatLevel(level), tag, String.format(message, args));
- }
- }
-
- @SuppressWarnings("AnnotateFormatMethod")
- @Override
- public void log(
- final @NotNull SentryLevel level,
- final @Nullable Throwable throwable,
- final @NotNull String message,
- final @Nullable Object... args) {
- if (args == null || args.length == 0) {
- log(level, message, throwable);
- } else {
- log(level, String.format(message, args), throwable);
- }
- }
-
- @Override
- public void log(
- final @NotNull SentryLevel level,
- final @NotNull String message,
- final @Nullable Throwable throwable) {
-
- switch (level) {
- case INFO:
- Log.i(tag, message, throwable);
- break;
- case WARNING:
- Log.w(tag, message, throwable);
- break;
- case ERROR:
- Log.e(tag, message, throwable);
- break;
- case FATAL:
- Log.wtf(tag, message, throwable);
- break;
- case DEBUG:
- default:
- Log.d(tag, message, throwable);
- break;
- }
- }
-
- @Override
- public boolean isEnabled(@Nullable SentryLevel level) {
- return true;
- }
-
- private int toLogcatLevel(final @NotNull SentryLevel sentryLevel) {
- switch (sentryLevel) {
- case INFO:
- return Log.INFO;
- case WARNING:
- return Log.WARN;
- case FATAL:
- return Log.ASSERT;
- case DEBUG:
- default:
- return Log.DEBUG;
- }
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLoggerBatchProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLoggerBatchProcessor.java
deleted file mode 100644
index 13b12dc702a..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLoggerBatchProcessor.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package io.sentry.android.core;
-
-import io.sentry.ISentryClient;
-import io.sentry.SentryLevel;
-import io.sentry.SentryOptions;
-import io.sentry.logger.LoggerBatchProcessor;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-
-@ApiStatus.Internal
-public final class AndroidLoggerBatchProcessor extends LoggerBatchProcessor
- implements AppState.AppStateListener {
-
- public AndroidLoggerBatchProcessor(
- @NotNull SentryOptions options, @NotNull ISentryClient client) {
- super(options, client);
- AppState.getInstance().addAppStateListener(this);
- }
-
- @Override
- public void onForeground() {
- // no-op
- }
-
- @Override
- public void onBackground() {
- try {
- options
- .getExecutorService()
- .submit(
- new Runnable() {
- @Override
- public void run() {
- flush(LoggerBatchProcessor.FLUSH_AFTER_MS);
- }
- });
- } catch (Throwable t) {
- options.getLogger().log(SentryLevel.ERROR, t, "Failed to submit log flush in onBackground()");
- }
- }
-
- @Override
- public void close(boolean isRestarting) {
- AppState.getInstance().removeAppStateListener(this);
- super.close(isRestarting);
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLoggerBatchProcessorFactory.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLoggerBatchProcessorFactory.java
deleted file mode 100644
index 694f94c7f7b..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLoggerBatchProcessorFactory.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package io.sentry.android.core;
-
-import io.sentry.SentryClient;
-import io.sentry.SentryOptions;
-import io.sentry.logger.ILoggerBatchProcessor;
-import io.sentry.logger.ILoggerBatchProcessorFactory;
-import org.jetbrains.annotations.NotNull;
-
-public final class AndroidLoggerBatchProcessorFactory implements ILoggerBatchProcessorFactory {
- @Override
- public @NotNull ILoggerBatchProcessor create(
- @NotNull SentryOptions options, @NotNull SentryClient client) {
- return new AndroidLoggerBatchProcessor(options, client);
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java
deleted file mode 100644
index 6775d818b4e..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package io.sentry.android.core;
-
-import android.os.Debug;
-import io.sentry.IPerformanceSnapshotCollector;
-import io.sentry.PerformanceCollectionData;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-
-@ApiStatus.Internal
-public class AndroidMemoryCollector implements IPerformanceSnapshotCollector {
-
- @Override
- public void setup() {}
-
- @Override
- public void collect(final @NotNull PerformanceCollectionData performanceCollectionData) {
- long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
- long usedNativeMemory = Debug.getNativeHeapSize() - Debug.getNativeHeapFreeSize();
- performanceCollectionData.setUsedHeapMemory(usedMemory);
- performanceCollectionData.setUsedNativeMemory(usedNativeMemory);
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessor.java
deleted file mode 100644
index 290f2a9d4ed..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessor.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package io.sentry.android.core;
-
-import io.sentry.ISentryClient;
-import io.sentry.SentryLevel;
-import io.sentry.SentryOptions;
-import io.sentry.metrics.MetricsBatchProcessor;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-
-@ApiStatus.Internal
-public final class AndroidMetricsBatchProcessor extends MetricsBatchProcessor
- implements AppState.AppStateListener {
-
- public AndroidMetricsBatchProcessor(
- final @NotNull SentryOptions options, final @NotNull ISentryClient client) {
- super(options, client);
- AppState.getInstance().addAppStateListener(this);
- }
-
- @Override
- public void onForeground() {
- // no-op
- }
-
- @Override
- public void onBackground() {
- try {
- options
- .getExecutorService()
- .submit(
- new Runnable() {
- @Override
- public void run() {
- flush(MetricsBatchProcessor.FLUSH_AFTER_MS);
- }
- });
- } catch (Throwable t) {
- options
- .getLogger()
- .log(SentryLevel.ERROR, t, "Failed to submit metrics flush in onBackground()");
- }
- }
-
- @Override
- public void close(boolean isRestarting) {
- AppState.getInstance().removeAppStateListener(this);
- super.close(isRestarting);
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessorFactory.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessorFactory.java
deleted file mode 100644
index 319440c27a9..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessorFactory.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package io.sentry.android.core;
-
-import io.sentry.SentryClient;
-import io.sentry.SentryOptions;
-import io.sentry.metrics.IMetricsBatchProcessor;
-import io.sentry.metrics.IMetricsBatchProcessorFactory;
-import org.jetbrains.annotations.NotNull;
-
-public final class AndroidMetricsBatchProcessorFactory implements IMetricsBatchProcessorFactory {
- @Override
- public @NotNull IMetricsBatchProcessor create(
- final @NotNull SentryOptions options, final @NotNull SentryClient client) {
- return new AndroidMetricsBatchProcessor(options, client);
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java
deleted file mode 100644
index 81ac3b35d6a..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java
+++ /dev/null
@@ -1,501 +0,0 @@
-package io.sentry.android.core;
-
-import static io.sentry.android.core.NdkIntegration.SENTRY_NDK_CLASS_NAME;
-
-import android.app.Application;
-import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.os.Build;
-import io.sentry.CompositePerformanceCollector;
-import io.sentry.DeduplicateMultithreadedEventProcessor;
-import io.sentry.DefaultCompositePerformanceCollector;
-import io.sentry.DefaultVersionDetector;
-import io.sentry.IContinuousProfiler;
-import io.sentry.ILogger;
-import io.sentry.ISentryLifecycleToken;
-import io.sentry.ITransactionProfiler;
-import io.sentry.NoOpCompositePerformanceCollector;
-import io.sentry.NoOpConnectionStatusProvider;
-import io.sentry.NoOpContinuousProfiler;
-import io.sentry.NoOpReplayBreadcrumbConverter;
-import io.sentry.NoOpSocketTagger;
-import io.sentry.NoOpTransactionProfiler;
-import io.sentry.NoopVersionDetector;
-import io.sentry.ScopeType;
-import io.sentry.SendFireAndForgetEnvelopeSender;
-import io.sentry.SendFireAndForgetOutboxSender;
-import io.sentry.SentryLevel;
-import io.sentry.SentryOpenTelemetryMode;
-import io.sentry.android.core.cache.AndroidEnvelopeCache;
-import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader;
-import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator;
-import io.sentry.android.core.internal.modules.AssetsModulesLoader;
-import io.sentry.android.core.internal.util.AndroidConnectionStatusProvider;
-import io.sentry.android.core.internal.util.AndroidCurrentDateProvider;
-import io.sentry.android.core.internal.util.AndroidRuntimeManager;
-import io.sentry.android.core.internal.util.AndroidThreadChecker;
-import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
-import io.sentry.android.core.performance.AppStartMetrics;
-import io.sentry.android.distribution.DistributionIntegration;
-import io.sentry.android.fragment.FragmentLifecycleIntegration;
-import io.sentry.android.replay.DefaultReplayBreadcrumbConverter;
-import io.sentry.android.replay.ReplayIntegration;
-import io.sentry.android.timber.SentryTimberIntegration;
-import io.sentry.cache.PersistingOptionsObserver;
-import io.sentry.cache.PersistingScopeObserver;
-import io.sentry.compose.gestures.ComposeGestureTargetLocator;
-import io.sentry.compose.viewhierarchy.ComposeViewHierarchyExporter;
-import io.sentry.internal.debugmeta.NoOpDebugMetaLoader;
-import io.sentry.internal.gestures.GestureTargetLocator;
-import io.sentry.internal.modules.NoOpModulesLoader;
-import io.sentry.internal.viewhierarchy.ViewHierarchyExporter;
-import io.sentry.protocol.SentryId;
-import io.sentry.transport.CurrentDateProvider;
-import io.sentry.transport.NoOpEnvelopeCache;
-import io.sentry.transport.NoOpTransportGate;
-import io.sentry.util.LazyEvaluator;
-import io.sentry.util.Objects;
-import io.sentry.util.thread.NoOpThreadChecker;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.jetbrains.annotations.TestOnly;
-
-/**
- * Android Options initializer, it reads configurations from AndroidManifest and sets to the
- * SentryAndroidOptions. It also adds default values for some fields.
- */
-@SuppressWarnings("Convert2MethodRef") // older AGP versions do not support method references
-final class AndroidOptionsInitializer {
-
- static final long DEFAULT_FLUSH_TIMEOUT_MS = 4000;
-
- static final String SENTRY_COMPOSE_GESTURE_INTEGRATION_CLASS_NAME =
- "io.sentry.compose.gestures.ComposeGestureTargetLocator";
-
- static final String SENTRY_COMPOSE_VIEW_HIERARCHY_INTEGRATION_CLASS_NAME =
- "io.sentry.compose.viewhierarchy.ComposeViewHierarchyExporter";
-
- static final String COMPOSE_CLASS_NAME = "androidx.compose.ui.node.Owner";
-
- /** private ctor */
- private AndroidOptionsInitializer() {}
-
- /**
- * Init method of the Android Options initializer
- *
- * @param options the SentryAndroidOptions
- * @param context the Application context
- */
- @TestOnly
- static void loadDefaultAndMetadataOptions(
- final @NotNull SentryAndroidOptions options, final @NotNull Context context) {
- final ILogger logger = new AndroidLogger();
- loadDefaultAndMetadataOptions(options, context, logger, new BuildInfoProvider(logger));
- }
-
- /**
- * Init method of the Android Options initializer
- *
- * @param options the SentryAndroidOptions
- * @param context the Application context
- * @param logger the ILogger interface
- * @param buildInfoProvider the BuildInfoProvider interface
- */
- static void loadDefaultAndMetadataOptions(
- final @NotNull SentryAndroidOptions options,
- @NotNull Context context,
- final @NotNull ILogger logger,
- final @NotNull BuildInfoProvider buildInfoProvider) {
- Objects.requireNonNull(context, "The context is required.");
-
- @NotNull final Context finalContext = ContextUtils.getApplicationContext(context);
-
- Objects.requireNonNull(options, "The options object is required.");
- Objects.requireNonNull(logger, "The ILogger object is required.");
-
- // Firstly set the logger, if `debug=true` configured, logging can start asap.
- options.setLogger(logger);
- options.setFatalLogger(new AndroidFatalLogger());
-
- options.setDefaultScopeType(ScopeType.CURRENT);
- options.setOpenTelemetryMode(SentryOpenTelemetryMode.OFF);
- options.setDateProvider(new SentryAndroidDateProvider());
- options.setRuntimeManager(new AndroidRuntimeManager());
- options.getLogs().setLoggerBatchProcessorFactory(new AndroidLoggerBatchProcessorFactory());
- options.getMetrics().setMetricsBatchProcessorFactory(new AndroidMetricsBatchProcessorFactory());
-
- // set a lower flush timeout on Android to avoid ANRs
- options.setFlushTimeoutMillis(DEFAULT_FLUSH_TIMEOUT_MS);
-
- options.setFrameMetricsCollector(
- new SentryFrameMetricsCollector(finalContext, logger, buildInfoProvider));
-
- ManifestMetadataReader.applyMetadata(finalContext, options, buildInfoProvider);
-
- options.setCacheDirPath(
- options
- .getRuntimeManager()
- .runWithRelaxedPolicy(() -> getCacheDir(finalContext).getAbsolutePath()));
-
- readDefaultOptionValues(options, finalContext, buildInfoProvider);
- AppState.getInstance().registerLifecycleObserver(options);
- }
-
- @TestOnly
- static void initializeIntegrationsAndProcessors(
- final @NotNull SentryAndroidOptions options,
- final @NotNull Context context,
- final @NotNull io.sentry.util.LoadClass loadClass,
- final @NotNull ActivityFramesTracker activityFramesTracker,
- final boolean isReplayAvailable) {
- initializeIntegrationsAndProcessors(
- options,
- context,
- new BuildInfoProvider(new AndroidLogger()),
- loadClass,
- activityFramesTracker,
- isReplayAvailable);
- }
-
- static void initializeIntegrationsAndProcessors(
- final @NotNull SentryAndroidOptions options,
- final @NotNull Context context,
- final @NotNull BuildInfoProvider buildInfoProvider,
- final @NotNull io.sentry.util.LoadClass loadClass,
- final @NotNull ActivityFramesTracker activityFramesTracker,
- final boolean isReplayAvailable) {
-
- if (options.getCacheDirPath() != null
- && options.getEnvelopeDiskCache() instanceof NoOpEnvelopeCache) {
- options.setEnvelopeDiskCache(new AndroidEnvelopeCache(options));
- }
-
- if (options.getConnectionStatusProvider() instanceof NoOpConnectionStatusProvider) {
- options.setConnectionStatusProvider(
- new AndroidConnectionStatusProvider(
- context, options, buildInfoProvider, AndroidCurrentDateProvider.getInstance()));
- }
-
- if (options.getCacheDirPath() != null) {
- options.addScopeObserver(new PersistingScopeObserver(options));
- options.addOptionsObserver(new PersistingOptionsObserver(options));
- }
-
- options.addEventProcessor(new DeduplicateMultithreadedEventProcessor(options));
- options.addEventProcessor(
- new DefaultAndroidEventProcessor(context, buildInfoProvider, options));
- options.addEventProcessor(new PerformanceAndroidEventProcessor(options, activityFramesTracker));
- options.addEventProcessor(new ScreenshotEventProcessor(options, buildInfoProvider));
- options.addEventProcessor(new ViewHierarchyEventProcessor(options));
- options.addEventProcessor(
- new ApplicationExitInfoEventProcessor(context, options, buildInfoProvider));
- if (options.getTransportGate() instanceof NoOpTransportGate) {
- options.setTransportGate(new AndroidTransportGate(options));
- }
-
- final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance();
-
- if (options.getModulesLoader() instanceof NoOpModulesLoader) {
- options.setModulesLoader(new AssetsModulesLoader(context, options.getLogger()));
- }
- if (options.getDebugMetaLoader() instanceof NoOpDebugMetaLoader) {
- options.setDebugMetaLoader(new AssetsDebugMetaLoader(context, options.getLogger()));
- }
- if (options.getVersionDetector() instanceof NoopVersionDetector) {
- options.setVersionDetector(new DefaultVersionDetector(options));
- }
-
- final @NotNull LazyEvaluator isAndroidXScrollViewAvailable =
- loadClass.isClassAvailableLazy("androidx.core.view.ScrollingView", options);
- final boolean isComposeUpstreamAvailable =
- loadClass.isClassAvailable(COMPOSE_CLASS_NAME, options);
-
- if (options.getGestureTargetLocators().isEmpty()) {
- final List gestureTargetLocators = new ArrayList<>(2);
- gestureTargetLocators.add(new AndroidViewGestureTargetLocator(isAndroidXScrollViewAvailable));
-
- final boolean isComposeAvailable =
- (isComposeUpstreamAvailable
- && loadClass.isClassAvailable(
- SENTRY_COMPOSE_GESTURE_INTEGRATION_CLASS_NAME, options));
-
- if (isComposeAvailable) {
- gestureTargetLocators.add(new ComposeGestureTargetLocator(options.getLogger()));
- }
- options.setGestureTargetLocators(gestureTargetLocators);
- }
-
- if (options.getViewHierarchyExporters().isEmpty()
- && isComposeUpstreamAvailable
- && loadClass.isClassAvailable(
- SENTRY_COMPOSE_VIEW_HIERARCHY_INTEGRATION_CLASS_NAME, options)) {
-
- final List viewHierarchyExporters = new ArrayList<>(1);
- viewHierarchyExporters.add(new ComposeViewHierarchyExporter(options.getLogger()));
- options.setViewHierarchyExporters(viewHierarchyExporters);
- }
-
- if (options.getThreadChecker() instanceof NoOpThreadChecker) {
- options.setThreadChecker(AndroidThreadChecker.getInstance());
- }
- if (options.getSocketTagger() instanceof NoOpSocketTagger) {
- options.setSocketTagger(AndroidSocketTagger.getInstance());
- }
-
- if (options.getPerformanceCollectors().isEmpty()) {
- options.addPerformanceCollector(new AndroidMemoryCollector());
- options.addPerformanceCollector(new AndroidCpuCollector(options.getLogger()));
-
- if (options.isEnablePerformanceV2()) {
- options.addPerformanceCollector(
- new SpanFrameMetricsCollector(
- options,
- Objects.requireNonNull(
- options.getFrameMetricsCollector(),
- "options.getFrameMetricsCollector is required")));
- }
- }
- if (options.getCompositePerformanceCollector() instanceof NoOpCompositePerformanceCollector) {
- options.setCompositePerformanceCollector(new DefaultCompositePerformanceCollector(options));
- }
-
- if (isReplayAvailable
- && options.getReplayController().getBreadcrumbConverter()
- instanceof NoOpReplayBreadcrumbConverter) {
- options
- .getReplayController()
- .setBreadcrumbConverter(new DefaultReplayBreadcrumbConverter(options));
- }
-
- // Check if the profiler was already instantiated in the app start.
- // We use the Android profiler, that uses a global start/stop api, so we need to preserve the
- // state of the profiler, and it's only possible retaining the instance.
- final @Nullable ITransactionProfiler appStartTransactionProfiler;
- final @Nullable IContinuousProfiler appStartContinuousProfiler;
- try (final @NotNull ISentryLifecycleToken ignored = AppStartMetrics.staticLock.acquire()) {
- appStartTransactionProfiler = appStartMetrics.getAppStartProfiler();
- appStartContinuousProfiler = appStartMetrics.getAppStartContinuousProfiler();
- appStartMetrics.setAppStartProfiler(null);
- appStartMetrics.setAppStartContinuousProfiler(null);
- }
-
- setupProfiler(
- options,
- context,
- buildInfoProvider,
- appStartTransactionProfiler,
- appStartContinuousProfiler,
- options.getCompositePerformanceCollector());
- }
-
- /** Setup the correct profiler (transaction or continuous) based on the options. */
- private static void setupProfiler(
- final @NotNull SentryAndroidOptions options,
- final @NotNull Context context,
- final @NotNull BuildInfoProvider buildInfoProvider,
- final @Nullable ITransactionProfiler appStartTransactionProfiler,
- final @Nullable IContinuousProfiler appStartContinuousProfiler,
- final @NotNull CompositePerformanceCollector performanceCollector) {
- if (options.isProfilingEnabled() || options.getProfilesSampleRate() != null) {
- options.setContinuousProfiler(NoOpContinuousProfiler.getInstance());
- // This is a safeguard, but it should never happen, as the app start profiler should be the
- // continuous one.
- if (appStartContinuousProfiler != null) {
- appStartContinuousProfiler.close(true);
- }
- if (appStartTransactionProfiler != null) {
- options.setTransactionProfiler(appStartTransactionProfiler);
- } else {
- options.setTransactionProfiler(
- new AndroidTransactionProfiler(
- context,
- options,
- buildInfoProvider,
- Objects.requireNonNull(
- options.getFrameMetricsCollector(),
- "options.getFrameMetricsCollector is required")));
- }
- } else {
- options.setTransactionProfiler(NoOpTransactionProfiler.getInstance());
- // This is a safeguard, but it should never happen, as the app start profiler should be the
- // transaction one.
- if (appStartTransactionProfiler != null) {
- appStartTransactionProfiler.close();
- }
- if (appStartContinuousProfiler != null) {
- options.setContinuousProfiler(appStartContinuousProfiler);
- // If the profiler is running, we start the performance collector too, otherwise we'd miss
- // measurements in app launch profiles
- final @NotNull SentryId chunkId = appStartContinuousProfiler.getChunkId();
- if (appStartContinuousProfiler.isRunning() && !chunkId.equals(SentryId.EMPTY_ID)) {
- performanceCollector.start(chunkId.toString());
- }
- } else {
- options.setContinuousProfiler(
- new AndroidContinuousProfiler(
- buildInfoProvider,
- Objects.requireNonNull(
- options.getFrameMetricsCollector(),
- "options.getFrameMetricsCollector is required"),
- options.getLogger(),
- options.getProfilingTracesDirPath(),
- options.getProfilingTracesHz(),
- () -> options.getExecutorService()));
- }
- }
- }
-
- static void installDefaultIntegrations(
- final @NotNull Context context,
- final @NotNull SentryAndroidOptions options,
- final @NotNull BuildInfoProvider buildInfoProvider,
- final @NotNull io.sentry.util.LoadClass loadClass,
- final @NotNull ActivityFramesTracker activityFramesTracker,
- final boolean isFragmentAvailable,
- final boolean isTimberAvailable,
- final boolean isReplayAvailable,
- final boolean isDistributionAvailable) {
-
- // Integration MUST NOT cache option values in ctor, as they will be configured later by the
- // user
-
- // read the startup crash marker here to avoid doing double-IO for the SendCachedEnvelope
- // integrations below
- LazyEvaluator startupCrashMarkerEvaluator =
- new LazyEvaluator<>(() -> AndroidEnvelopeCache.hasStartupCrashMarker(options));
-
- options.addIntegration(
- new SendCachedEnvelopeIntegration(
- new SendFireAndForgetEnvelopeSender(() -> options.getCacheDirPath()),
- startupCrashMarkerEvaluator));
-
- // Integrations are registered in the same order. NDK before adding Watch outbox,
- // because sentry-native move files around and we don't want to watch that.
- final Class> sentryNdkClass = loadClass.loadClass(SENTRY_NDK_CLASS_NAME, options.getLogger());
- options.addIntegration(new NdkIntegration(sentryNdkClass));
-
- if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.S) {
- options.addIntegration(new TombstoneIntegration(context));
- }
-
- // this integration uses android.os.FileObserver, we can't move to sentry
- // before creating a pure java impl.
- options.addIntegration(EnvelopeFileObserverIntegration.getOutboxFileObserver());
-
- // Send cached envelopes from outbox path
- // this should be executed after NdkIntegration because sentry-native move files on init.
- // and we'd like to send them right away
- options.addIntegration(
- new SendCachedEnvelopeIntegration(
- new SendFireAndForgetOutboxSender(() -> options.getOutboxPath()),
- startupCrashMarkerEvaluator));
-
- // AppLifecycleIntegration has to be installed before AnrIntegration, because AnrIntegration
- // relies on AppState set by it
- options.addIntegration(new AppLifecycleIntegration());
- // AnrIntegration must be installed before ReplayIntegration, as ReplayIntegration relies on
- // it to set the replayId in case of an ANR
- options.addIntegration(AnrIntegrationFactory.create(context, buildInfoProvider));
-
- // registerActivityLifecycleCallbacks is only available if Context is an AppContext
- if (context instanceof Application) {
- options.addIntegration(
- new ActivityLifecycleIntegration(
- (Application) context, buildInfoProvider, activityFramesTracker));
- options.addIntegration(new ActivityBreadcrumbsIntegration((Application) context));
- options.addIntegration(new UserInteractionIntegration((Application) context, loadClass));
- if (isFragmentAvailable) {
- options.addIntegration(new FragmentLifecycleIntegration((Application) context, true, true));
- }
- } else {
- options
- .getLogger()
- .log(
- SentryLevel.WARNING,
- "ActivityLifecycle, FragmentLifecycle and UserInteraction Integrations need an Application class to be installed.");
- }
-
- if (isTimberAvailable) {
- options.addIntegration(new SentryTimberIntegration());
- }
- options.addIntegration(new AppComponentsBreadcrumbsIntegration(context));
- options.addIntegration(new SystemEventsBreadcrumbsIntegration(context));
- options.addIntegration(new NetworkBreadcrumbsIntegration(context, buildInfoProvider));
- if (isReplayAvailable) {
- final ReplayIntegration replay =
- new ReplayIntegration(context, CurrentDateProvider.getInstance());
- options.addIntegration(replay);
- options.setReplayController(replay);
- }
- if (isDistributionAvailable) {
- final DistributionIntegration distribution = new DistributionIntegration((context));
- options.setDistributionController(distribution);
- options.addIntegration(distribution);
- }
- options
- .getFeedbackOptions()
- .setDialogHandler(new SentryAndroidOptions.AndroidUserFeedbackIDialogHandler());
- }
-
- /**
- * Reads and sets default option values that are Android specific like release and inApp
- *
- * @param options the SentryAndroidOptions
- * @param context the Android context methods
- */
- private static void readDefaultOptionValues(
- final @NotNull SentryAndroidOptions options,
- final @NotNull Context context,
- final @NotNull BuildInfoProvider buildInfoProvider) {
- final @Nullable PackageInfo packageInfo =
- ContextUtils.getPackageInfo(context, buildInfoProvider);
- if (packageInfo != null) {
- // Sets App's release if not set by Manifest
- if (options.getRelease() == null) {
- options.setRelease(
- getSentryReleaseVersion(
- packageInfo, ContextUtils.getVersionCode(packageInfo, buildInfoProvider)));
- }
-
- // Sets the App's package name as InApp
- final String packageName = packageInfo.packageName;
- if (packageName != null && !packageName.startsWith("android.")) {
- options.addInAppInclude(packageName);
- }
- }
-
- if (options.getDistinctId() == null) {
- try {
- options.setDistinctId(
- options.getRuntimeManager().runWithRelaxedPolicy(() -> Installation.id(context)));
- } catch (RuntimeException e) {
- options.getLogger().log(SentryLevel.ERROR, "Could not generate distinct Id.", e);
- }
- }
- }
-
- /**
- * Returns the sentry release version (eg io.sentry.sample@1.0.0+10000) -
- * packageName@versionName+buildVersion
- *
- * @param packageInfo the PackageInfo
- * @param versionCode the versionCode
- * @return the sentry release version as a String
- */
- private static @NotNull String getSentryReleaseVersion(
- final @NotNull PackageInfo packageInfo, final @NotNull String versionCode) {
- return packageInfo.packageName + "@" + packageInfo.versionName + "+" + versionCode;
- }
-
- /**
- * Retrieve the Sentry cache dir.
- *
- * @param context the Application context
- */
- static @NotNull File getCacheDir(final @NotNull Context context) {
- return new File(context.getCacheDir(), "sentry");
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java
deleted file mode 100644
index f4357b010f7..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java
+++ /dev/null
@@ -1,365 +0,0 @@
-package io.sentry.android.core;
-
-import android.annotation.SuppressLint;
-import android.os.Debug;
-import android.os.Process;
-import android.os.SystemClock;
-import io.sentry.DateUtils;
-import io.sentry.ILogger;
-import io.sentry.ISentryExecutorService;
-import io.sentry.ISentryLifecycleToken;
-import io.sentry.PerformanceCollectionData;
-import io.sentry.SentryLevel;
-import io.sentry.SentryNanotimeDate;
-import io.sentry.SentryUUID;
-import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
-import io.sentry.profilemeasurements.ProfileMeasurement;
-import io.sentry.profilemeasurements.ProfileMeasurementValue;
-import io.sentry.util.AutoClosableReentrantLock;
-import io.sentry.util.LazyEvaluator;
-import io.sentry.util.Objects;
-import java.io.File;
-import java.util.ArrayDeque;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.Future;
-import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.TimeUnit;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-@ApiStatus.Internal
-public class AndroidProfiler {
- public static class ProfileStartData {
- public final long startNanos;
- public final long startCpuMillis;
- public final @NotNull Date startTimestamp;
-
- public ProfileStartData(
- final long startNanos, final long startCpuMillis, final @NotNull Date startTimestamp) {
- this.startNanos = startNanos;
- this.startCpuMillis = startCpuMillis;
- this.startTimestamp = startTimestamp;
- }
- }
-
- public static class ProfileEndData {
- public final long endNanos;
- public final long endCpuMillis;
- public final @NotNull File traceFile;
- public final @NotNull Map measurementsMap;
- public final boolean didTimeout;
-
- public ProfileEndData(
- final long endNanos,
- final long endCpuMillis,
- final boolean didTimeout,
- final @NotNull File traceFile,
- final @NotNull Map measurementsMap) {
- this.endNanos = endNanos;
- this.traceFile = traceFile;
- this.endCpuMillis = endCpuMillis;
- this.measurementsMap = measurementsMap;
- this.didTimeout = didTimeout;
- }
- }
-
- /**
- * This appears to correspond to the buffer size of the data part of the file, excluding the key
- * part. Once the buffer is full, new records are ignored, but the resulting trace file will be
- * valid.
- *
- * 30 second traces can require a buffer of a few MB. 8MB is the default buffer size for
- * [Debug.startMethodTracingSampling], but 3 should be enough for most cases. We can adjust this
- * in the future if we notice that traces are being truncated in some applications.
- */
- private static final int BUFFER_SIZE_BYTES = 3_000_000;
-
- private static final int PROFILING_TIMEOUT_MILLIS = 30_000;
- private long profileStartNanos = 0;
- private final @NotNull File traceFilesDir;
- private final int intervalUs;
- private @Nullable Future> scheduledFinish = null;
- private @Nullable File traceFile = null;
- private @Nullable String frameMetricsCollectorId;
- private final @NotNull SentryFrameMetricsCollector frameMetricsCollector;
- private final @NotNull ArrayDeque screenFrameRateMeasurements =
- new ArrayDeque<>();
- private final @NotNull ArrayDeque slowFrameRenderMeasurements =
- new ArrayDeque<>();
- private final @NotNull ArrayDeque frozenFrameRenderMeasurements =
- new ArrayDeque<>();
- private final @NotNull Map measurementsMap = new HashMap<>();
- private final @Nullable LazyEvaluator.Evaluator
- timeoutExecutorServiceSupplier;
- private final @NotNull ILogger logger;
- private volatile boolean isRunning = false;
- protected final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
-
- public AndroidProfiler(
- final @NotNull String tracesFilesDirPath,
- final int intervalUs,
- final @NotNull SentryFrameMetricsCollector frameMetricsCollector,
- final @Nullable LazyEvaluator.Evaluator
- timeoutExecutorServiceSupplier,
- final @NotNull ILogger logger) {
- this.traceFilesDir =
- new File(Objects.requireNonNull(tracesFilesDirPath, "TracesFilesDirPath is required"));
- this.intervalUs = intervalUs;
- this.logger = Objects.requireNonNull(logger, "Logger is required");
- // Timeout executor is nullable, as timeouts will not be there for continuous profiling
- this.timeoutExecutorServiceSupplier = timeoutExecutorServiceSupplier;
- this.frameMetricsCollector =
- Objects.requireNonNull(frameMetricsCollector, "SentryFrameMetricsCollector is required");
- }
-
- @SuppressLint("NewApi")
- public @Nullable ProfileStartData start() {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- // intervalUs is 0 only if there was a problem in the init
- if (intervalUs == 0) {
- logger.log(
- SentryLevel.WARNING, "Disabling profiling because intervaUs is set to %d", intervalUs);
- return null;
- }
-
- if (isRunning) {
- logger.log(SentryLevel.WARNING, "Profiling has already started...");
- return null;
- }
-
- // We create a file with a uuid name, so no need to check if it already exists
- traceFile = new File(traceFilesDir, SentryUUID.generateSentryId() + ".trace");
-
- measurementsMap.clear();
- screenFrameRateMeasurements.clear();
- slowFrameRenderMeasurements.clear();
- frozenFrameRenderMeasurements.clear();
-
- frameMetricsCollectorId =
- frameMetricsCollector.startCollection(
- new SentryFrameMetricsCollector.FrameMetricsCollectorListener() {
- float lastRefreshRate = 0;
-
- @Override
- public void onFrameMetricCollected(
- final long frameStartNanos,
- final long frameEndNanos,
- final long durationNanos,
- final long delayNanos,
- final boolean isSlow,
- final boolean isFrozen,
- final float refreshRate) {
- // profileStartNanos is calculated through SystemClock.elapsedRealtimeNanos(),
- // but frameEndNanos uses System.nanotime(), so we convert it to get the timestamp
- // relative to profileStartNanos
- final long timestampNanos = new SentryNanotimeDate().nanoTimestamp();
- final long frameTimestampRelativeNanos =
- frameEndNanos
- - System.nanoTime()
- + SystemClock.elapsedRealtimeNanos()
- - profileStartNanos;
-
- // We don't allow negative relative timestamps.
- // So we add a check, even if this should never happen.
- if (frameTimestampRelativeNanos < 0) {
- return;
- }
- if (isFrozen) {
- frozenFrameRenderMeasurements.addLast(
- new ProfileMeasurementValue(
- frameTimestampRelativeNanos, durationNanos, timestampNanos));
- } else if (isSlow) {
- slowFrameRenderMeasurements.addLast(
- new ProfileMeasurementValue(
- frameTimestampRelativeNanos, durationNanos, timestampNanos));
- }
- if (refreshRate != lastRefreshRate) {
- lastRefreshRate = refreshRate;
- screenFrameRateMeasurements.addLast(
- new ProfileMeasurementValue(
- frameTimestampRelativeNanos, refreshRate, timestampNanos));
- }
- }
- });
-
- // We stop profiling after a timeout to avoid huge profiles to be sent
- try {
- if (timeoutExecutorServiceSupplier != null) {
- scheduledFinish =
- timeoutExecutorServiceSupplier
- .evaluate()
- .schedule(() -> endAndCollect(true, null), PROFILING_TIMEOUT_MILLIS);
- }
- } catch (RejectedExecutionException e) {
- logger.log(
- SentryLevel.ERROR,
- "Failed to call the executor. Profiling will not be automatically finished. Did you call Sentry.close()?",
- e);
- }
-
- profileStartNanos = SystemClock.elapsedRealtimeNanos();
- final @NotNull Date profileStartTimestamp = DateUtils.getCurrentDateTime();
- long profileStartCpuMillis = Process.getElapsedCpuTime();
-
- // We don't make any check on the file existence or writeable state, because we don't want to
- // make file IO in the main thread.
- // We cannot offload the work to the executorService, as if that's very busy, profiles could
- // start/stop with a lot of delay and even cause ANRs.
- try {
- // If there is any problem with the file this method will throw (but it will not throw in
- // tests)
- Debug.startMethodTracingSampling(traceFile.getPath(), BUFFER_SIZE_BYTES, intervalUs);
- isRunning = true;
- return new ProfileStartData(
- profileStartNanos, profileStartCpuMillis, profileStartTimestamp);
- } catch (Throwable e) {
- endAndCollect(false, null);
- logger.log(SentryLevel.ERROR, "Unable to start a profile: ", e);
- isRunning = false;
- return null;
- }
- }
- }
-
- @SuppressLint("NewApi")
- public @Nullable ProfileEndData endAndCollect(
- final boolean isTimeout,
- final @Nullable List performanceCollectionData) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- if (!isRunning) {
- logger.log(SentryLevel.WARNING, "Profiler not running");
- return null;
- }
-
- try {
- // If there is any problem with the file this method could throw, but the start is also
- // wrapped, so this should never happen (except for tests, where this is the only method
- // that throws)
- Debug.stopMethodTracing();
- } catch (Throwable e) {
- logger.log(SentryLevel.ERROR, "Error while stopping profiling: ", e);
- } finally {
- isRunning = false;
- }
- frameMetricsCollector.stopCollection(frameMetricsCollectorId);
-
- long transactionEndNanos = SystemClock.elapsedRealtimeNanos();
- long transactionEndCpuMillis = Process.getElapsedCpuTime();
-
- if (traceFile == null) {
- logger.log(SentryLevel.ERROR, "Trace file does not exists");
- return null;
- }
-
- if (!slowFrameRenderMeasurements.isEmpty()) {
- measurementsMap.put(
- ProfileMeasurement.ID_SLOW_FRAME_RENDERS,
- new ProfileMeasurement(
- ProfileMeasurement.UNIT_NANOSECONDS, slowFrameRenderMeasurements));
- }
- if (!frozenFrameRenderMeasurements.isEmpty()) {
- measurementsMap.put(
- ProfileMeasurement.ID_FROZEN_FRAME_RENDERS,
- new ProfileMeasurement(
- ProfileMeasurement.UNIT_NANOSECONDS, frozenFrameRenderMeasurements));
- }
- if (!screenFrameRateMeasurements.isEmpty()) {
- measurementsMap.put(
- ProfileMeasurement.ID_SCREEN_FRAME_RATES,
- new ProfileMeasurement(ProfileMeasurement.UNIT_HZ, screenFrameRateMeasurements));
- }
- putPerformanceCollectionDataInMeasurements(performanceCollectionData);
-
- if (scheduledFinish != null) {
- scheduledFinish.cancel(true);
- scheduledFinish = null;
- }
-
- return new ProfileEndData(
- transactionEndNanos, transactionEndCpuMillis, isTimeout, traceFile, measurementsMap);
- }
- }
-
- public void close() {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- // we cancel any scheduled work
- if (scheduledFinish != null) {
- scheduledFinish.cancel(true);
- scheduledFinish = null;
- }
-
- // stop profiling if running
- if (isRunning) {
- endAndCollect(true, null);
- }
- }
- }
-
- @SuppressLint("NewApi")
- private void putPerformanceCollectionDataInMeasurements(
- final @Nullable List performanceCollectionData) {
-
- // This difference is required, since the PerformanceCollectionData timestamps are expressed in
- // terms of System.currentTimeMillis() and measurements timestamps require the nanoseconds since
- // the beginning, expressed with SystemClock.elapsedRealtimeNanos()
- long timestampDiff =
- SystemClock.elapsedRealtimeNanos()
- - profileStartNanos
- - TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis());
- if (performanceCollectionData != null) {
- final @NotNull ArrayDeque memoryUsageMeasurements =
- new ArrayDeque<>(performanceCollectionData.size());
- final @NotNull ArrayDeque nativeMemoryUsageMeasurements =
- new ArrayDeque<>(performanceCollectionData.size());
- final @NotNull ArrayDeque cpuUsageMeasurements =
- new ArrayDeque<>(performanceCollectionData.size());
-
- synchronized (performanceCollectionData) {
- for (final @NotNull PerformanceCollectionData data : performanceCollectionData) {
- final long nanoTimestamp = data.getNanoTimestamp();
- final long relativeStartNs = nanoTimestamp + timestampDiff;
- final @Nullable Double cpuUsagePercentage = data.getCpuUsagePercentage();
- final @Nullable Long usedHeapMemory = data.getUsedHeapMemory();
- final @Nullable Long usedNativeMemory = data.getUsedNativeMemory();
-
- if (cpuUsagePercentage != null) {
- cpuUsageMeasurements.add(
- new ProfileMeasurementValue(relativeStartNs, cpuUsagePercentage, nanoTimestamp));
- }
- if (usedHeapMemory != null) {
- memoryUsageMeasurements.add(
- new ProfileMeasurementValue(relativeStartNs, usedHeapMemory, nanoTimestamp));
- }
- if (usedNativeMemory != null) {
- nativeMemoryUsageMeasurements.add(
- new ProfileMeasurementValue(relativeStartNs, usedNativeMemory, nanoTimestamp));
- }
- }
- }
-
- if (!cpuUsageMeasurements.isEmpty()) {
- measurementsMap.put(
- ProfileMeasurement.ID_CPU_USAGE,
- new ProfileMeasurement(ProfileMeasurement.UNIT_PERCENT, cpuUsageMeasurements));
- }
- if (!memoryUsageMeasurements.isEmpty()) {
- measurementsMap.put(
- ProfileMeasurement.ID_MEMORY_FOOTPRINT,
- new ProfileMeasurement(ProfileMeasurement.UNIT_BYTES, memoryUsageMeasurements));
- }
- if (!nativeMemoryUsageMeasurements.isEmpty()) {
- measurementsMap.put(
- ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT,
- new ProfileMeasurement(ProfileMeasurement.UNIT_BYTES, nativeMemoryUsageMeasurements));
- }
- }
- }
-
- boolean isRunning() {
- return isRunning;
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidSocketTagger.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidSocketTagger.java
deleted file mode 100644
index 7c4afd309f2..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidSocketTagger.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package io.sentry.android.core;
-
-import android.net.TrafficStats;
-import io.sentry.ISocketTagger;
-import org.jetbrains.annotations.ApiStatus;
-
-@ApiStatus.Internal
-public final class AndroidSocketTagger implements ISocketTagger {
-
- // just a random number to tag outgoing traffic from the Sentry SDK
- private static final int SENTRY_TAG = 0xF001;
-
- private static final AndroidSocketTagger instance = new AndroidSocketTagger();
-
- private AndroidSocketTagger() {}
-
- public static AndroidSocketTagger getInstance() {
- return instance;
- }
-
- @Override
- public void tagSockets() {
- TrafficStats.setThreadStatsTag(SENTRY_TAG);
- }
-
- @Override
- public void untagSockets() {
- TrafficStats.clearThreadStatsTag();
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java
deleted file mode 100644
index e44ed746a08..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java
+++ /dev/null
@@ -1,347 +0,0 @@
-package io.sentry.android.core;
-
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.os.Build;
-import io.sentry.DateUtils;
-import io.sentry.ILogger;
-import io.sentry.ISentryExecutorService;
-import io.sentry.ISentryLifecycleToken;
-import io.sentry.ITransaction;
-import io.sentry.ITransactionProfiler;
-import io.sentry.PerformanceCollectionData;
-import io.sentry.ProfilingTraceData;
-import io.sentry.ProfilingTransactionData;
-import io.sentry.ScopesAdapter;
-import io.sentry.SentryLevel;
-import io.sentry.SentryOptions;
-import io.sentry.android.core.internal.util.CpuInfoUtils;
-import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
-import io.sentry.util.AutoClosableReentrantLock;
-import io.sentry.util.LazyEvaluator;
-import io.sentry.util.Objects;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-final class AndroidTransactionProfiler implements ITransactionProfiler {
- private final @NotNull Context context;
- private final @NotNull ILogger logger;
- private final @Nullable String profilingTracesDirPath;
- private final boolean isProfilingEnabled;
- private final int profilingTracesHz;
- private final @NotNull LazyEvaluator.Evaluator executorServiceSupplier;
- private final @NotNull BuildInfoProvider buildInfoProvider;
- private boolean isInitialized = false;
- private final @NotNull AtomicBoolean isRunning = new AtomicBoolean(false);
- private final @NotNull SentryFrameMetricsCollector frameMetricsCollector;
- private volatile @Nullable ProfilingTransactionData currentProfilingTransactionData;
-
- /**
- * The underlying profiler instance. It is thread safe to call it after checking if it's not null,
- * because we never nullify it after instantiation.
- */
- private volatile @Nullable AndroidProfiler profiler = null;
-
- private long profileStartNanos;
- private long profileStartCpuMillis;
- private @NotNull Date profileStartTimestamp;
- private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
-
- public AndroidTransactionProfiler(
- final @NotNull Context context,
- final @NotNull SentryAndroidOptions sentryAndroidOptions,
- final @NotNull BuildInfoProvider buildInfoProvider,
- final @NotNull SentryFrameMetricsCollector frameMetricsCollector) {
- this(
- context,
- buildInfoProvider,
- frameMetricsCollector,
- sentryAndroidOptions.getLogger(),
- sentryAndroidOptions.getProfilingTracesDirPath(),
- sentryAndroidOptions.isProfilingEnabled(),
- sentryAndroidOptions.getProfilingTracesHz(),
- () -> sentryAndroidOptions.getExecutorService());
- }
-
- public AndroidTransactionProfiler(
- final @NotNull Context context,
- final @NotNull BuildInfoProvider buildInfoProvider,
- final @NotNull SentryFrameMetricsCollector frameMetricsCollector,
- final @NotNull ILogger logger,
- final @Nullable String profilingTracesDirPath,
- final boolean isProfilingEnabled,
- final int profilingTracesHz,
- final @NotNull ISentryExecutorService executorService) {
- this(
- context,
- buildInfoProvider,
- frameMetricsCollector,
- logger,
- profilingTracesDirPath,
- isProfilingEnabled,
- profilingTracesHz,
- () -> executorService);
- }
-
- public AndroidTransactionProfiler(
- final @NotNull Context context,
- final @NotNull BuildInfoProvider buildInfoProvider,
- final @NotNull SentryFrameMetricsCollector frameMetricsCollector,
- final @NotNull ILogger logger,
- final @Nullable String profilingTracesDirPath,
- final boolean isProfilingEnabled,
- final int profilingTracesHz,
- final @NotNull LazyEvaluator.Evaluator executorServiceSupplier) {
- this.context =
- Objects.requireNonNull(
- ContextUtils.getApplicationContext(context), "The application context is required");
- this.logger = Objects.requireNonNull(logger, "ILogger is required");
- this.frameMetricsCollector =
- Objects.requireNonNull(frameMetricsCollector, "SentryFrameMetricsCollector is required");
- this.buildInfoProvider =
- Objects.requireNonNull(buildInfoProvider, "The BuildInfoProvider is required.");
- this.profilingTracesDirPath = profilingTracesDirPath;
- this.isProfilingEnabled = isProfilingEnabled;
- this.profilingTracesHz = profilingTracesHz;
- this.executorServiceSupplier =
- Objects.requireNonNull(
- executorServiceSupplier, "A supplier for ISentryExecutorService is required.");
- this.profileStartTimestamp = DateUtils.getCurrentDateTime();
- }
-
- private void init() {
- // We initialize it only once
- if (isInitialized) {
- return;
- }
- isInitialized = true;
-
- if (!isProfilingEnabled) {
- logger.log(SentryLevel.INFO, "Profiling is disabled in options.");
- return;
- }
- if (profilingTracesDirPath == null) {
- logger.log(
- SentryLevel.WARNING,
- "Disabling profiling because no profiling traces dir path is defined in options.");
- return;
- }
- if (profilingTracesHz <= 0) {
- logger.log(
- SentryLevel.WARNING,
- "Disabling profiling because trace rate is set to %d",
- profilingTracesHz);
- return;
- }
-
- profiler =
- new AndroidProfiler(
- profilingTracesDirPath,
- (int) SECONDS.toMicros(1) / profilingTracesHz,
- frameMetricsCollector,
- executorServiceSupplier,
- logger);
- }
-
- @Override
- public void start() {
- // Debug.startMethodTracingSampling() is only available since Lollipop, but Android Profiler
- // causes crashes on api 21 -> https://github.com/getsentry/sentry-java/issues/3392
- if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return;
-
- // When the first transaction is starting, we can start profiling
- if (!isRunning.getAndSet(true)) {
- // Let's initialize trace folder and profiling interval
- init();
-
- if (onFirstStart()) {
- logger.log(SentryLevel.DEBUG, "Profiler started.");
- } else {
- // If profiler is not null and is running, it means that a profile is already running
- if (profiler != null && profiler.isRunning()) {
- logger.log(
- SentryLevel.WARNING, "A profile is already running. This profile will be ignored.");
- } else {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- // Ensure we unbind any transaction data, just in case of concurrent starts
- currentProfilingTransactionData = null;
- }
- // Otherwise we update the flag, because it means the profiler is not running
- isRunning.set(false);
- }
- }
- }
- }
-
- @SuppressLint("NewApi")
- private boolean onFirstStart() {
- // init() didn't create profiler, should never happen
- if (profiler == null) {
- return false;
- }
-
- final AndroidProfiler.ProfileStartData startData = profiler.start();
- // check if profiling started
- if (startData == null) {
- return false;
- }
- profileStartNanos = startData.startNanos;
- profileStartCpuMillis = startData.startCpuMillis;
- profileStartTimestamp = startData.startTimestamp;
- return true;
- }
-
- @Override
- public void bindTransaction(final @NotNull ITransaction transaction) {
- // If the profiler is running, but no profilingTransactionData is set, we bind it here
- if (isRunning.get() && currentProfilingTransactionData == null) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- // If the profiler is running, but no profilingTransactionData is set, we bind it here
- if (isRunning.get() && currentProfilingTransactionData == null) {
- currentProfilingTransactionData =
- new ProfilingTransactionData(transaction, profileStartNanos, profileStartCpuMillis);
- }
- }
- }
- }
-
- @Override
- public @Nullable ProfilingTraceData onTransactionFinish(
- final @NotNull ITransaction transaction,
- final @Nullable List performanceCollectionData,
- final @NotNull SentryOptions options) {
- return onTransactionFinish(
- transaction.getName(),
- transaction.getEventId().toString(),
- transaction.getSpanContext().getTraceId().toString(),
- false,
- performanceCollectionData,
- options);
- }
-
- @SuppressLint("NewApi")
- private @Nullable ProfilingTraceData onTransactionFinish(
- final @NotNull String transactionName,
- final @NotNull String transactionId,
- final @NotNull String traceId,
- final boolean isTimeout,
- final @Nullable List performanceCollectionData,
- final @NotNull SentryOptions options) {
-
- // onTransactionStart() is only available since Lollipop_MR1
- // and SystemClock.elapsedRealtimeNanos() since Jelly Bean
- // and SUPPORTED_ABIS since KITKAT
- if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return null;
-
- // check if profiler was created
- if (profiler == null) {
- return null;
- }
-
- final ProfilingTransactionData txData;
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- txData = currentProfilingTransactionData;
-
- // Transaction finished, but it's not in the current profile
- if (txData == null || !txData.getId().equals(transactionId)) {
- // A transaction is finishing, but it's not profiled. We can skip it
- logger.log(
- SentryLevel.INFO,
- "Transaction %s (%s) finished, but was not currently being profiled. Skipping",
- transactionName,
- traceId);
- return null;
- }
- currentProfilingTransactionData = null;
- }
-
- logger.log(SentryLevel.DEBUG, "Transaction %s (%s) finished.", transactionName, traceId);
-
- final AndroidProfiler.ProfileEndData endData =
- profiler.endAndCollect(false, performanceCollectionData);
-
- isRunning.set(false);
-
- // check if profiler end successfully
- if (endData == null) {
- return null;
- }
-
- long transactionDurationNanos = endData.endNanos - profileStartNanos;
-
- final @NotNull List transactionList = new ArrayList<>(1);
- transactionList.add(txData);
- txData.notifyFinish(
- endData.endNanos, profileStartNanos, endData.endCpuMillis, profileStartCpuMillis);
-
- String totalMem = "0";
- final @Nullable Long memory =
- (options instanceof SentryAndroidOptions)
- ? DeviceInfoUtil.getInstance(context, (SentryAndroidOptions) options).getTotalMemory()
- : null;
- if (memory != null) {
- totalMem = Long.toString(memory);
- }
- final String[] abis = Build.SUPPORTED_ABIS;
-
- // cpu max frequencies are read with a lambda because reading files is involved, so it will be
- // done in the background when the trace file is read
- return new ProfilingTraceData(
- endData.traceFile,
- profileStartTimestamp,
- transactionList,
- transactionName,
- transactionId,
- traceId,
- Long.toString(transactionDurationNanos),
- buildInfoProvider.getSdkInfoVersion(),
- abis != null && abis.length > 0 ? abis[0] : "",
- () -> CpuInfoUtils.getInstance().readMaxFrequencies(),
- buildInfoProvider.getManufacturer(),
- buildInfoProvider.getModel(),
- buildInfoProvider.getVersionRelease(),
- buildInfoProvider.isEmulator(),
- totalMem,
- options.getProguardUuid(),
- options.getRelease(),
- options.getEnvironment(),
- (endData.didTimeout || isTimeout)
- ? ProfilingTraceData.TRUNCATION_REASON_TIMEOUT
- : ProfilingTraceData.TRUNCATION_REASON_NORMAL,
- endData.measurementsMap);
- }
-
- @Override
- public boolean isRunning() {
- return isRunning.get();
- }
-
- @Override
- public void close() {
- final @Nullable ProfilingTransactionData txData = currentProfilingTransactionData;
- // we stop profiling
- if (txData != null) {
- onTransactionFinish(
- txData.getName(),
- txData.getId(),
- txData.getTraceId(),
- true,
- null,
- ScopesAdapter.getInstance().getOptions());
- }
- // in case the app start profiling is running, and it's not bound to a transaction, we still
- // stop profiling, but we also have to manually update the flag.
- isRunning.set(false);
-
- // we have to first stop profiling otherwise we would lost the last profile
- if (profiler != null) {
- profiler.close();
- }
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransportGate.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransportGate.java
deleted file mode 100644
index fa138a802f2..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransportGate.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package io.sentry.android.core;
-
-import io.sentry.IConnectionStatusProvider;
-import io.sentry.SentryOptions;
-import io.sentry.transport.ITransportGate;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.TestOnly;
-
-final class AndroidTransportGate implements ITransportGate {
-
- private final @NotNull SentryOptions options;
-
- AndroidTransportGate(final @NotNull SentryOptions options) {
- this.options = options;
- }
-
- @Override
- public boolean isConnected() {
- return isConnected(options.getConnectionStatusProvider().getConnectionStatus());
- }
-
- @TestOnly
- boolean isConnected(final @NotNull IConnectionStatusProvider.ConnectionStatus status) {
- // let's assume its connected if there's no permission or something as we can't really know
- // whether is online or not.
- switch (status) {
- case CONNECTED:
- case UNKNOWN:
- case NO_PERMISSION:
- return true;
- default:
- return false;
- }
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java
deleted file mode 100644
index 8243493a50b..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java
+++ /dev/null
@@ -1,201 +0,0 @@
-package io.sentry.android.core;
-
-import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import io.sentry.Hint;
-import io.sentry.IScopes;
-import io.sentry.ISentryLifecycleToken;
-import io.sentry.Integration;
-import io.sentry.SentryEvent;
-import io.sentry.SentryLevel;
-import io.sentry.SentryOptions;
-import io.sentry.exception.ExceptionMechanismException;
-import io.sentry.hints.AbnormalExit;
-import io.sentry.hints.TransactionEnd;
-import io.sentry.protocol.Mechanism;
-import io.sentry.util.AutoClosableReentrantLock;
-import io.sentry.util.HintUtils;
-import io.sentry.util.Objects;
-import java.io.Closeable;
-import java.io.IOException;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.jetbrains.annotations.TestOnly;
-
-/**
- * When the UI thread of an Android app is blocked for too long, an "Application Not Responding"
- * (ANR) error is triggered. Sends an event if an ANR happens
- */
-public final class AnrIntegration implements Integration, Closeable {
-
- private final @NotNull Context context;
- private boolean isClosed = false;
- private final @NotNull AutoClosableReentrantLock startLock = new AutoClosableReentrantLock();
-
- public AnrIntegration(final @NotNull Context context) {
- this.context = ContextUtils.getApplicationContext(context);
- }
-
- /**
- * Responsible for watching UI thread. being static to avoid multiple instances running at the
- * same time.
- */
- @SuppressLint("StaticFieldLeak") // we have watchDogLock to avoid those leaks
- private static @Nullable ANRWatchDog anrWatchDog;
-
- private @Nullable SentryOptions options;
-
- protected static final @NotNull AutoClosableReentrantLock watchDogLock =
- new AutoClosableReentrantLock();
-
- @Override
- public final void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) {
- this.options = Objects.requireNonNull(options, "SentryOptions is required");
- register(scopes, (SentryAndroidOptions) options);
- }
-
- private void register(
- final @NotNull IScopes scopes, final @NotNull SentryAndroidOptions options) {
- options
- .getLogger()
- .log(SentryLevel.DEBUG, "AnrIntegration enabled: %s", options.isAnrEnabled());
-
- if (options.isAnrEnabled()) {
- addIntegrationToSdkVersion("Anr");
- try {
- options
- .getExecutorService()
- .submit(
- () -> {
- try (final @NotNull ISentryLifecycleToken ignored = startLock.acquire()) {
- if (!isClosed) {
- startAnrWatchdog(scopes, options);
- }
- }
- });
- } catch (Throwable e) {
- options
- .getLogger()
- .log(SentryLevel.DEBUG, "Failed to start AnrIntegration on executor thread.", e);
- }
- }
- }
-
- private void startAnrWatchdog(
- final @NotNull IScopes scopes, final @NotNull SentryAndroidOptions options) {
- try (final @NotNull ISentryLifecycleToken ignored = watchDogLock.acquire()) {
- if (anrWatchDog == null) {
- options
- .getLogger()
- .log(
- SentryLevel.DEBUG,
- "ANR timeout in milliseconds: %d",
- options.getAnrTimeoutIntervalMillis());
-
- anrWatchDog =
- new ANRWatchDog(
- options.getAnrTimeoutIntervalMillis(),
- options.isAnrReportInDebug(),
- error -> reportANR(scopes, options, error),
- options.getLogger(),
- context);
- anrWatchDog.start();
-
- options.getLogger().log(SentryLevel.DEBUG, "AnrIntegration installed.");
- }
- }
- }
-
- @TestOnly
- void reportANR(
- final @NotNull IScopes scopes,
- final @NotNull SentryAndroidOptions options,
- final @NotNull ApplicationNotResponding error) {
- options.getLogger().log(SentryLevel.INFO, "ANR triggered with message: %s", error.getMessage());
-
- // if LifecycleWatcher isn't available, we always assume the ANR is foreground
- final boolean isAppInBackground = Boolean.TRUE.equals(AppState.getInstance().isInBackground());
-
- @SuppressWarnings("ThrowableNotThrown")
- final Throwable anrThrowable = buildAnrThrowable(isAppInBackground, options, error);
-
- final SentryEvent event = new SentryEvent(anrThrowable);
- event.setLevel(SentryLevel.ERROR);
-
- final AnrHint anrHint = new AnrHint(isAppInBackground);
- final Hint hint = HintUtils.createWithTypeCheckHint(anrHint);
- scopes.captureEvent(event, hint);
- }
-
- private @NotNull Throwable buildAnrThrowable(
- final boolean isAppInBackground,
- final @NotNull SentryAndroidOptions options,
- final @NotNull ApplicationNotResponding anr) {
-
- String message = "ANR for at least " + options.getAnrTimeoutIntervalMillis() + " ms.";
- if (isAppInBackground) {
- message = "Background " + message;
- }
-
- final ApplicationNotResponding error = new ApplicationNotResponding(message, anr.getThread());
- final Mechanism mechanism = new Mechanism();
- mechanism.setType("ANR");
-
- return new ExceptionMechanismException(mechanism, error, error.getThread(), true);
- }
-
- @TestOnly
- @Nullable
- ANRWatchDog getANRWatchDog() {
- return anrWatchDog;
- }
-
- @Override
- public void close() throws IOException {
- try (final @NotNull ISentryLifecycleToken ignored = startLock.acquire()) {
- isClosed = true;
- }
- try (final @NotNull ISentryLifecycleToken ignored = watchDogLock.acquire()) {
- if (anrWatchDog != null) {
- anrWatchDog.interrupt();
- anrWatchDog = null;
- if (options != null) {
- options.getLogger().log(SentryLevel.DEBUG, "AnrIntegration removed.");
- }
- }
- }
- }
-
- /**
- * ANR is an abnormal session exit, according to Develop Docs
- * because we don't know whether the app has recovered after it or not.
- */
- static final class AnrHint implements AbnormalExit, TransactionEnd {
-
- private final boolean isBackgroundAnr;
-
- AnrHint(final boolean isBackgroundAnr) {
- this.isBackgroundAnr = isBackgroundAnr;
- }
-
- @Override
- public String mechanism() {
- return isBackgroundAnr ? "anr_background" : "anr_foreground";
- }
-
- // We don't want the current thread (watchdog) to be marked as crashed, otherwise the Sentry
- // Console prioritizes it over the main thread in the thread's list.
- @Override
- public boolean ignoreCurrentThread() {
- return true;
- }
-
- @Override
- public @Nullable Long timestamp() {
- return null;
- }
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegrationFactory.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegrationFactory.java
deleted file mode 100644
index cff8f72cd36..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegrationFactory.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package io.sentry.android.core;
-
-import android.content.Context;
-import android.os.Build;
-import io.sentry.Integration;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-
-@ApiStatus.Internal
-public final class AnrIntegrationFactory {
-
- @NotNull
- public static Integration create(
- final @NotNull Context context, final @NotNull BuildInfoProvider buildInfoProvider) {
- if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.R) {
- return new AnrV2Integration(context);
- } else {
- return new AnrIntegration(context);
- }
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java
deleted file mode 100644
index af3a942c8cc..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java
+++ /dev/null
@@ -1,332 +0,0 @@
-package io.sentry.android.core;
-
-import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion;
-
-import android.annotation.SuppressLint;
-import android.app.ActivityManager;
-import android.app.ApplicationExitInfo;
-import android.content.Context;
-import io.sentry.Attachment;
-import io.sentry.DateUtils;
-import io.sentry.Hint;
-import io.sentry.ILogger;
-import io.sentry.IScopes;
-import io.sentry.Integration;
-import io.sentry.SentryEvent;
-import io.sentry.SentryLevel;
-import io.sentry.SentryOptions;
-import io.sentry.android.core.cache.AndroidEnvelopeCache;
-import io.sentry.android.core.internal.threaddump.Lines;
-import io.sentry.android.core.internal.threaddump.ThreadDumpParser;
-import io.sentry.hints.AbnormalExit;
-import io.sentry.hints.Backfillable;
-import io.sentry.hints.BlockingFlushHint;
-import io.sentry.protocol.DebugImage;
-import io.sentry.protocol.DebugMeta;
-import io.sentry.protocol.Message;
-import io.sentry.protocol.SentryId;
-import io.sentry.protocol.SentryThread;
-import io.sentry.transport.CurrentDateProvider;
-import io.sentry.transport.ICurrentDateProvider;
-import io.sentry.util.HintUtils;
-import io.sentry.util.Objects;
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.Closeable;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.util.List;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-@SuppressLint("NewApi") // we check this in AnrIntegrationFactory
-public class AnrV2Integration implements Integration, Closeable {
-
- private final @NotNull Context context;
- private final @NotNull ICurrentDateProvider dateProvider;
- private @Nullable SentryAndroidOptions options;
-
- public AnrV2Integration(final @NotNull Context context) {
- // using CurrentDateProvider instead of AndroidCurrentDateProvider as AppExitInfo uses
- // System.currentTimeMillis
- this(context, CurrentDateProvider.getInstance());
- }
-
- AnrV2Integration(
- final @NotNull Context context, final @NotNull ICurrentDateProvider dateProvider) {
- this.context = ContextUtils.getApplicationContext(context);
- this.dateProvider = dateProvider;
- }
-
- @SuppressLint("NewApi") // we do the check in the AnrIntegrationFactory
- @Override
- public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) {
- this.options =
- Objects.requireNonNull(
- (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null,
- "SentryAndroidOptions is required");
-
- this.options
- .getLogger()
- .log(SentryLevel.DEBUG, "AnrIntegration enabled: %s", this.options.isAnrEnabled());
-
- if (this.options.getCacheDirPath() == null) {
- this.options
- .getLogger()
- .log(SentryLevel.INFO, "Cache dir is not set, unable to process ANRs");
- return;
- }
-
- if (this.options.isAnrEnabled()) {
- try {
- options
- .getExecutorService()
- .submit(
- new ApplicationExitInfoHistoryDispatcher(
- context, scopes, this.options, dateProvider, new AnrV2Policy(this.options)));
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.DEBUG, "Failed to start ANR processor.", e);
- }
- options.getLogger().log(SentryLevel.DEBUG, "AnrV2Integration installed.");
- addIntegrationToSdkVersion("AnrV2");
- }
- }
-
- @Override
- public void close() throws IOException {
- if (options != null) {
- options.getLogger().log(SentryLevel.DEBUG, "AnrV2Integration removed.");
- }
- }
-
- private static final class AnrV2Policy
- implements ApplicationExitInfoHistoryDispatcher.ApplicationExitInfoPolicy {
-
- private final @NotNull SentryAndroidOptions options;
-
- AnrV2Policy(final @NotNull SentryAndroidOptions options) {
- this.options = options;
- }
-
- @Override
- public @NotNull String getLabel() {
- return "ANR";
- }
-
- @Override
- public int getTargetReason() {
- return ApplicationExitInfo.REASON_ANR;
- }
-
- @Override
- public boolean shouldReportHistorical() {
- return options.isReportHistoricalAnrs();
- }
-
- @Override
- public @Nullable Long getLastReportedTimestamp() {
- return AndroidEnvelopeCache.lastReportedAnr(options);
- }
-
- @Override
- public @Nullable ApplicationExitInfoHistoryDispatcher.Report buildReport(
- final @NotNull ApplicationExitInfo exitInfo, final boolean shouldEnrich) {
- final long anrTimestamp = exitInfo.getTimestamp();
- final boolean isBackground =
- exitInfo.getImportance() != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
-
- final ParseResult result = parseThreadDump(exitInfo, isBackground);
- if (result.type == ParseResult.Type.NO_DUMP) {
- options
- .getLogger()
- .log(
- SentryLevel.WARNING,
- "Not reporting ANR event as there was no thread dump for the ANR %s",
- exitInfo.toString());
- return null;
- }
- final AnrV2Hint anrHint =
- new AnrV2Hint(
- options.getFlushTimeoutMillis(),
- options.getLogger(),
- anrTimestamp,
- shouldEnrich,
- isBackground);
-
- final Hint hint = HintUtils.createWithTypeCheckHint(anrHint);
-
- final SentryEvent event = new SentryEvent();
- if (result.type == ParseResult.Type.ERROR) {
- final Message sentryMessage = new Message();
- sentryMessage.setFormatted(
- "Sentry Android SDK failed to parse system thread dump for "
- + "this ANR. We recommend enabling [SentryOptions.isAttachAnrThreadDump] option "
- + "to attach the thread dump as plain text and report this issue on GitHub.");
- event.setMessage(sentryMessage);
- } else if (result.type == ParseResult.Type.DUMP) {
- event.setThreads(result.threads);
- if (result.debugImages != null) {
- final DebugMeta debugMeta = new DebugMeta();
- debugMeta.setImages(result.debugImages);
- event.setDebugMeta(debugMeta);
- }
- }
- event.setLevel(SentryLevel.FATAL);
- event.setTimestamp(DateUtils.getDateTime(anrTimestamp));
-
- if (options.isAttachAnrThreadDump()) {
- if (result.dump != null) {
- hint.setThreadDump(Attachment.fromThreadDump(result.dump));
- }
- }
-
- return new ApplicationExitInfoHistoryDispatcher.Report(event, hint, anrHint);
- }
-
- private @NotNull ParseResult parseThreadDump(
- final @NotNull ApplicationExitInfo exitInfo, final boolean isBackground) {
- final byte[] dump;
-
- try (final InputStream trace = exitInfo.getTraceInputStream()) {
- if (trace == null) {
- return new ParseResult(ParseResult.Type.NO_DUMP);
- }
- dump = getDumpBytes(trace);
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.WARNING, "Failed to read ANR thread dump", e);
- return new ParseResult(ParseResult.Type.NO_DUMP);
- }
-
- try (final BufferedReader reader =
- new BufferedReader(new InputStreamReader(new ByteArrayInputStream(dump)))) {
- final Lines lines = Lines.readLines(reader);
-
- final ThreadDumpParser threadDumpParser = new ThreadDumpParser(options, isBackground);
- threadDumpParser.parse(lines);
-
- final @NotNull List threads = threadDumpParser.getThreads();
- final @NotNull List debugImages = threadDumpParser.getDebugImages();
-
- if (threads.isEmpty()) {
- // if the list is empty this means the system failed to capture a proper thread dump of
- // the android threads, and only contains kernel-level threads and statuses, those ANRs
- // are not actionable and neither they are reported by Google Play Console, so we just
- // fall back to not reporting them
- return new ParseResult(ParseResult.Type.NO_DUMP);
- }
- return new ParseResult(ParseResult.Type.DUMP, dump, threads, debugImages);
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.WARNING, "Failed to parse ANR thread dump", e);
- return new ParseResult(ParseResult.Type.ERROR, dump);
- }
- }
-
- private byte[] getDumpBytes(final @NotNull InputStream trace) throws IOException {
- try (final ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
-
- int nRead;
- final byte[] data = new byte[1024];
-
- while ((nRead = trace.read(data, 0, data.length)) != -1) {
- buffer.write(data, 0, nRead);
- }
-
- return buffer.toByteArray();
- }
- }
- }
-
- @ApiStatus.Internal
- public static final class AnrV2Hint extends BlockingFlushHint
- implements Backfillable, AbnormalExit {
-
- private final long timestamp;
-
- private final boolean shouldEnrich;
-
- private final boolean isBackgroundAnr;
-
- public AnrV2Hint(
- final long flushTimeoutMillis,
- final @NotNull ILogger logger,
- final long timestamp,
- final boolean shouldEnrich,
- final boolean isBackgroundAnr) {
- super(flushTimeoutMillis, logger);
- this.timestamp = timestamp;
- this.shouldEnrich = shouldEnrich;
- this.isBackgroundAnr = isBackgroundAnr;
- }
-
- @Override
- public boolean ignoreCurrentThread() {
- return false;
- }
-
- @NotNull
- @Override
- public Long timestamp() {
- return timestamp;
- }
-
- @Override
- public boolean shouldEnrich() {
- return shouldEnrich;
- }
-
- @Override
- public String mechanism() {
- return isBackgroundAnr ? "anr_background" : "anr_foreground";
- }
-
- @Override
- public boolean isFlushable(@Nullable SentryId eventId) {
- return true;
- }
-
- @Override
- public void setFlushable(@NotNull SentryId eventId) {}
- }
-
- static final class ParseResult {
-
- enum Type {
- DUMP,
- NO_DUMP,
- ERROR
- }
-
- final Type type;
- final byte[] dump;
- final @Nullable List threads;
- final @Nullable List debugImages;
-
- ParseResult(final @NotNull Type type) {
- this.type = type;
- this.dump = null;
- this.threads = null;
- this.debugImages = null;
- }
-
- ParseResult(final @NotNull Type type, final byte[] dump) {
- this.type = type;
- this.dump = dump;
- this.threads = null;
- this.debugImages = null;
- }
-
- ParseResult(
- final @NotNull Type type,
- final byte[] dump,
- final @Nullable List threads,
- final @Nullable List debugImages) {
- this.type = type;
- this.dump = dump;
- this.threads = threads;
- this.debugImages = debugImages;
- }
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java
deleted file mode 100644
index 43ed3422cd9..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java
+++ /dev/null
@@ -1,181 +0,0 @@
-package io.sentry.android.core;
-
-import static io.sentry.TypeCheckHint.ANDROID_CONFIGURATION;
-import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion;
-
-import android.content.ComponentCallbacks2;
-import android.content.Context;
-import android.content.res.Configuration;
-import io.sentry.Breadcrumb;
-import io.sentry.Hint;
-import io.sentry.IScopes;
-import io.sentry.Integration;
-import io.sentry.SentryLevel;
-import io.sentry.SentryOptions;
-import io.sentry.android.core.internal.util.AndroidCurrentDateProvider;
-import io.sentry.android.core.internal.util.Debouncer;
-import io.sentry.android.core.internal.util.DeviceOrientations;
-import io.sentry.protocol.Device;
-import io.sentry.util.Objects;
-import java.io.Closeable;
-import java.io.IOException;
-import java.util.Locale;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-public final class AppComponentsBreadcrumbsIntegration
- implements Integration, Closeable, ComponentCallbacks2 {
-
- private static final long DEBOUNCE_WAIT_TIME_MS = 60 * 1000;
- // pre-allocate hint to avoid creating it every time for the low memory case
- private static final @NotNull Hint EMPTY_HINT = new Hint();
-
- private final @NotNull Context context;
- private @Nullable IScopes scopes;
- private @Nullable SentryAndroidOptions options;
-
- private final @NotNull Debouncer trimMemoryDebouncer =
- new Debouncer(AndroidCurrentDateProvider.getInstance(), DEBOUNCE_WAIT_TIME_MS, 0);
-
- public AppComponentsBreadcrumbsIntegration(final @NotNull Context context) {
- this.context =
- Objects.requireNonNull(ContextUtils.getApplicationContext(context), "Context is required");
- }
-
- @Override
- public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) {
- this.scopes = Objects.requireNonNull(scopes, "Scopes are required");
- this.options =
- Objects.requireNonNull(
- (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null,
- "SentryAndroidOptions is required");
-
- this.options
- .getLogger()
- .log(
- SentryLevel.DEBUG,
- "AppComponentsBreadcrumbsIntegration enabled: %s",
- this.options.isEnableAppComponentBreadcrumbs());
-
- if (this.options.isEnableAppComponentBreadcrumbs()) {
- try {
- // if its a ContextImpl, registerComponentCallbacks can't be used
- context.registerComponentCallbacks(this);
- options
- .getLogger()
- .log(SentryLevel.DEBUG, "AppComponentsBreadcrumbsIntegration installed.");
- addIntegrationToSdkVersion("AppComponentsBreadcrumbs");
- } catch (Throwable e) {
- this.options.setEnableAppComponentBreadcrumbs(false);
- options.getLogger().log(SentryLevel.INFO, e, "ComponentCallbacks2 is not available.");
- }
- }
- }
-
- @Override
- public void close() throws IOException {
- try {
- // if its a ContextImpl, unregisterComponentCallbacks can't be used
- context.unregisterComponentCallbacks(this);
- } catch (Throwable ignored) {
- // fine, might throw on older versions
- if (options != null) {
- options
- .getLogger()
- .log(SentryLevel.DEBUG, ignored, "It was not possible to unregisterComponentCallbacks");
- }
- }
-
- if (options != null) {
- options.getLogger().log(SentryLevel.DEBUG, "AppComponentsBreadcrumbsIntegration removed.");
- }
- }
-
- @SuppressWarnings("deprecation")
- @Override
- public void onConfigurationChanged(@NotNull Configuration newConfig) {
- final long now = System.currentTimeMillis();
- executeInBackground(() -> captureConfigurationChangedBreadcrumb(now, newConfig));
- }
-
- @SuppressWarnings("deprecation")
- @Override
- public void onLowMemory() {
- // we do this in onTrimMemory below already, this is legacy API (14 or below)
- }
-
- @Override
- public void onTrimMemory(final int level) {
- if (level < TRIM_MEMORY_BACKGROUND) {
- // only add breadcrumb if TRIM_MEMORY_BACKGROUND, TRIM_MEMORY_MODERATE or
- // TRIM_MEMORY_COMPLETE.
- // Release as much memory as the process can.
-
- // TRIM_MEMORY_UI_HIDDEN, TRIM_MEMORY_RUNNING_MODERATE, TRIM_MEMORY_RUNNING_LOW and
- // TRIM_MEMORY_RUNNING_CRITICAL.
- // Release any memory that your app doesn't need to run.
- // So they are still not so critical at the point of killing the process.
- // https://developer.android.com/topic/performance/memory
- return;
- }
-
- if (trimMemoryDebouncer.checkForDebounce()) {
- // if we received trim_memory within 1 minute time, ignore this call
- return;
- }
-
- final long now = System.currentTimeMillis();
- executeInBackground(() -> captureLowMemoryBreadcrumb(now, level));
- }
-
- private void captureLowMemoryBreadcrumb(final long timeMs, final int level) {
- if (scopes != null) {
- final Breadcrumb breadcrumb = new Breadcrumb(timeMs);
- breadcrumb.setType("system");
- breadcrumb.setCategory("device.event");
- breadcrumb.setMessage("Low memory");
- breadcrumb.setData("action", "LOW_MEMORY");
- breadcrumb.setData("level", level);
- breadcrumb.setLevel(SentryLevel.WARNING);
- scopes.addBreadcrumb(breadcrumb, EMPTY_HINT);
- }
- }
-
- private void captureConfigurationChangedBreadcrumb(
- final long timeMs, final @NotNull Configuration newConfig) {
- if (scopes != null) {
- final Device.DeviceOrientation deviceOrientation =
- DeviceOrientations.getOrientation(context.getResources().getConfiguration().orientation);
-
- String orientation;
- if (deviceOrientation != null) {
- orientation = deviceOrientation.name().toLowerCase(Locale.ROOT);
- } else {
- orientation = "undefined";
- }
-
- final Breadcrumb breadcrumb = new Breadcrumb(timeMs);
- breadcrumb.setType("navigation");
- breadcrumb.setCategory("device.orientation");
- breadcrumb.setData("position", orientation);
- breadcrumb.setLevel(SentryLevel.INFO);
-
- final Hint hint = new Hint();
- hint.set(ANDROID_CONFIGURATION, newConfig);
-
- scopes.addBreadcrumb(breadcrumb, hint);
- }
- }
-
- private void executeInBackground(final @NotNull Runnable runnable) {
- if (options != null) {
- try {
- options.getExecutorService().submit(runnable);
- } catch (Throwable t) {
- options
- .getLogger()
- .log(SentryLevel.ERROR, t, "Failed to submit app components breadcrumb task");
- }
- }
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java
deleted file mode 100644
index 9fd90b23099..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package io.sentry.android.core;
-
-import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion;
-
-import io.sentry.IScopes;
-import io.sentry.ISentryLifecycleToken;
-import io.sentry.Integration;
-import io.sentry.SentryLevel;
-import io.sentry.SentryOptions;
-import io.sentry.util.AutoClosableReentrantLock;
-import io.sentry.util.Objects;
-import java.io.Closeable;
-import java.io.IOException;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.jetbrains.annotations.TestOnly;
-
-public final class AppLifecycleIntegration implements Integration, Closeable {
-
- private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
- @TestOnly @Nullable volatile LifecycleWatcher watcher;
-
- private @Nullable SentryAndroidOptions options;
-
- @Override
- public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) {
- Objects.requireNonNull(scopes, "Scopes are required");
- this.options =
- Objects.requireNonNull(
- (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null,
- "SentryAndroidOptions is required");
-
- this.options
- .getLogger()
- .log(
- SentryLevel.DEBUG,
- "enableSessionTracking enabled: %s",
- this.options.isEnableAutoSessionTracking());
-
- this.options
- .getLogger()
- .log(
- SentryLevel.DEBUG,
- "enableAppLifecycleBreadcrumbs enabled: %s",
- this.options.isEnableAppLifecycleBreadcrumbs());
-
- if (this.options.isEnableAutoSessionTracking()
- || this.options.isEnableAppLifecycleBreadcrumbs()) {
- try (final ISentryLifecycleToken ignored = lock.acquire()) {
- if (watcher != null) {
- return;
- }
-
- watcher =
- new LifecycleWatcher(
- scopes,
- this.options.getSessionTrackingIntervalMillis(),
- this.options.isEnableAutoSessionTracking(),
- this.options.isEnableAppLifecycleBreadcrumbs());
-
- AppState.getInstance().addAppStateListener(watcher);
- }
-
- options.getLogger().log(SentryLevel.DEBUG, "AppLifecycleIntegration installed.");
- addIntegrationToSdkVersion("AppLifecycle");
- }
- }
-
- private void removeObserver() {
- final @Nullable LifecycleWatcher watcherRef;
- try (final ISentryLifecycleToken ignored = lock.acquire()) {
- watcherRef = watcher;
- watcher = null;
- }
-
- if (watcherRef != null) {
- AppState.getInstance().removeAppStateListener(watcherRef);
- if (options != null) {
- options.getLogger().log(SentryLevel.DEBUG, "AppLifecycleIntegration removed.");
- }
- }
- }
-
- @Override
- public void close() throws IOException {
- removeObserver();
- // TODO: probably should move it to Scopes.close(), but that'd require a new interface and
- // different implementations for Java and Android. This is probably fine like this too, because
- // integrations are closed in the same place
- AppState.getInstance().unregisterLifecycleObserver();
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java
deleted file mode 100644
index 74522f7aaac..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java
+++ /dev/null
@@ -1,221 +0,0 @@
-package io.sentry.android.core;
-
-import androidx.annotation.NonNull;
-import androidx.lifecycle.DefaultLifecycleObserver;
-import androidx.lifecycle.LifecycleOwner;
-import androidx.lifecycle.ProcessLifecycleOwner;
-import io.sentry.ILogger;
-import io.sentry.ISentryLifecycleToken;
-import io.sentry.NoOpLogger;
-import io.sentry.SentryLevel;
-import io.sentry.SentryOptions;
-import io.sentry.android.core.internal.util.AndroidThreadChecker;
-import io.sentry.util.AutoClosableReentrantLock;
-import java.io.Closeable;
-import java.io.IOException;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.jetbrains.annotations.TestOnly;
-
-/** AppState holds the state of the App, e.g. whether the app is in background/foreground, etc. */
-@ApiStatus.Internal
-public final class AppState implements Closeable {
- private static @NotNull AppState instance = new AppState();
- private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
- private volatile LifecycleObserver lifecycleObserver;
- private MainLooperHandler handler = new MainLooperHandler();
-
- private AppState() {}
-
- public static @NotNull AppState getInstance() {
- return instance;
- }
-
- private volatile @Nullable Boolean inBackground = null;
-
- @TestOnly
- void setHandler(final @NotNull MainLooperHandler handler) {
- this.handler = handler;
- }
-
- @ApiStatus.Internal
- @TestOnly
- public void resetInstance() {
- instance = new AppState();
- }
-
- @ApiStatus.Internal
- @TestOnly
- public LifecycleObserver getLifecycleObserver() {
- return lifecycleObserver;
- }
-
- public @Nullable Boolean isInBackground() {
- return inBackground;
- }
-
- void setInBackground(final boolean inBackground) {
- this.inBackground = inBackground;
- }
-
- public void addAppStateListener(final @NotNull AppStateListener listener) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- ensureLifecycleObserver(NoOpLogger.getInstance());
-
- if (lifecycleObserver != null) {
- lifecycleObserver.listeners.add(listener);
- }
- }
- }
-
- public void removeAppStateListener(final @NotNull AppStateListener listener) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- if (lifecycleObserver != null) {
- lifecycleObserver.listeners.remove(listener);
- }
- }
- }
-
- @ApiStatus.Internal
- public void registerLifecycleObserver(final @Nullable SentryOptions options) {
- if (lifecycleObserver != null) {
- return;
- }
-
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- ensureLifecycleObserver(options != null ? options.getLogger() : NoOpLogger.getInstance());
- }
- }
-
- private void ensureLifecycleObserver(final @NotNull ILogger logger) {
- if (lifecycleObserver != null) {
- return;
- }
- try {
- Class.forName("androidx.lifecycle.ProcessLifecycleOwner");
- // create it right away, so it's available in addAppStateListener in case it's posted to main
- // thread
- lifecycleObserver = new LifecycleObserver();
-
- if (AndroidThreadChecker.getInstance().isMainThread()) {
- addObserverInternal(logger);
- } else {
- // some versions of the androidx lifecycle-process require this to be executed on the main
- // thread.
- handler.post(() -> addObserverInternal(logger));
- }
- } catch (ClassNotFoundException e) {
- logger.log(
- SentryLevel.WARNING,
- "androidx.lifecycle is not available, some features might not be properly working,"
- + "e.g. Session Tracking, Network and System Events breadcrumbs, etc.");
- } catch (Throwable e) {
- logger.log(SentryLevel.ERROR, "AppState could not register lifecycle observer", e);
- }
- }
-
- private void addObserverInternal(final @NotNull ILogger logger) {
- final @Nullable LifecycleObserver observerRef = lifecycleObserver;
- try {
- // might already be unregistered/removed so we have to check for nullability
- if (observerRef != null) {
- ProcessLifecycleOwner.get().getLifecycle().addObserver(observerRef);
- }
- } catch (Throwable e) {
- // This is to handle a potential 'AbstractMethodError' gracefully. The error is triggered in
- // connection with conflicting dependencies of the androidx.lifecycle.
- // //See the issue here: https://github.com/getsentry/sentry-java/pull/2228
- lifecycleObserver = null;
- logger.log(
- SentryLevel.ERROR,
- "AppState failed to get Lifecycle and could not install lifecycle observer.",
- e);
- }
- }
-
- @ApiStatus.Internal
- public void unregisterLifecycleObserver() {
- if (lifecycleObserver == null) {
- return;
- }
-
- final @Nullable LifecycleObserver ref;
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- ref = lifecycleObserver;
- lifecycleObserver.listeners.clear();
- lifecycleObserver = null;
- }
-
- if (AndroidThreadChecker.getInstance().isMainThread()) {
- removeObserverInternal(ref);
- } else {
- // some versions of the androidx lifecycle-process require this to be executed on the main
- // thread.
- // avoid method refs on Android due to some issues with older AGP setups
- // noinspection Convert2MethodRef
- handler.post(() -> removeObserverInternal(ref));
- }
- }
-
- private void removeObserverInternal(final @Nullable LifecycleObserver ref) {
- if (ref != null) {
- ProcessLifecycleOwner.get().getLifecycle().removeObserver(ref);
- }
- }
-
- @Override
- public void close() throws IOException {
- unregisterLifecycleObserver();
- }
-
- @ApiStatus.Internal
- public final class LifecycleObserver implements DefaultLifecycleObserver {
- final List listeners =
- new CopyOnWriteArrayList() {
- @Override
- public boolean add(AppStateListener appStateListener) {
- final boolean addResult = super.add(appStateListener);
- // notify the listeners immediately to let them "catch up" with the current state
- // (mimics the behavior of androidx.lifecycle)
- if (Boolean.FALSE.equals(inBackground)) {
- appStateListener.onForeground();
- } else if (Boolean.TRUE.equals(inBackground)) {
- appStateListener.onBackground();
- }
- return addResult;
- }
- };
-
- @ApiStatus.Internal
- @TestOnly
- public List getListeners() {
- return listeners;
- }
-
- @Override
- public void onStart(@NonNull LifecycleOwner owner) {
- setInBackground(false);
- for (AppStateListener listener : listeners) {
- listener.onForeground();
- }
- }
-
- @Override
- public void onStop(@NonNull LifecycleOwner owner) {
- setInBackground(true);
- for (AppStateListener listener : listeners) {
- listener.onBackground();
- }
- }
- }
-
- // If necessary, we can adjust this and add other callbacks in the future
- public interface AppStateListener {
- void onForeground();
-
- void onBackground();
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java
deleted file mode 100644
index ec60bd8f128..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java
+++ /dev/null
@@ -1,781 +0,0 @@
-package io.sentry.android.core;
-
-import static io.sentry.cache.PersistingOptionsObserver.DIST_FILENAME;
-import static io.sentry.cache.PersistingOptionsObserver.ENVIRONMENT_FILENAME;
-import static io.sentry.cache.PersistingOptionsObserver.PROGUARD_UUID_FILENAME;
-import static io.sentry.cache.PersistingOptionsObserver.RELEASE_FILENAME;
-import static io.sentry.cache.PersistingOptionsObserver.REPLAY_ERROR_SAMPLE_RATE_FILENAME;
-import static io.sentry.cache.PersistingOptionsObserver.SDK_VERSION_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.BREADCRUMBS_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.CONTEXTS_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.EXTRAS_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.FINGERPRINT_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.LEVEL_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.REPLAY_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.REQUEST_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.TRACE_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.TRANSACTION_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.USER_FILENAME;
-import static io.sentry.protocol.Contexts.REPLAY_ID;
-
-import android.annotation.SuppressLint;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.os.Build;
-import android.util.DisplayMetrics;
-import androidx.annotation.WorkerThread;
-import io.sentry.BackfillingEventProcessor;
-import io.sentry.Breadcrumb;
-import io.sentry.Hint;
-import io.sentry.IpAddressUtils;
-import io.sentry.SentryBaseEvent;
-import io.sentry.SentryEvent;
-import io.sentry.SentryExceptionFactory;
-import io.sentry.SentryLevel;
-import io.sentry.SentryOptions;
-import io.sentry.SentryStackTraceFactory;
-import io.sentry.SpanContext;
-import io.sentry.android.core.internal.util.CpuInfoUtils;
-import io.sentry.cache.PersistingOptionsObserver;
-import io.sentry.cache.PersistingScopeObserver;
-import io.sentry.hints.AbnormalExit;
-import io.sentry.hints.Backfillable;
-import io.sentry.protocol.App;
-import io.sentry.protocol.Contexts;
-import io.sentry.protocol.DebugImage;
-import io.sentry.protocol.DebugMeta;
-import io.sentry.protocol.Device;
-import io.sentry.protocol.Mechanism;
-import io.sentry.protocol.OperatingSystem;
-import io.sentry.protocol.Request;
-import io.sentry.protocol.SdkVersion;
-import io.sentry.protocol.SentryStackTrace;
-import io.sentry.protocol.SentryThread;
-import io.sentry.protocol.SentryTransaction;
-import io.sentry.protocol.User;
-import io.sentry.util.HintUtils;
-import io.sentry.util.SentryRandom;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * Processes cached ApplicationExitInfo events (ANRs, tombstones) on a background thread, so we can
- * safely read data from disk synchronously.
- */
-@ApiStatus.Internal
-@WorkerThread
-public final class ApplicationExitInfoEventProcessor implements BackfillingEventProcessor {
-
- private final @NotNull Context context;
-
- private final @NotNull SentryAndroidOptions options;
-
- private final @NotNull BuildInfoProvider buildInfoProvider;
-
- private final @NotNull SentryExceptionFactory sentryExceptionFactory;
-
- private final @Nullable PersistingScopeObserver persistingScopeObserver;
-
- // Only ANRv2 events are currently enriched with hint-specific content.
- // This can be extended to other hints like NativeCrashExit.
- private final @NotNull List hintEnrichers =
- Collections.singletonList(new AnrHintEnricher());
-
- public ApplicationExitInfoEventProcessor(
- final @NotNull Context context,
- final @NotNull SentryAndroidOptions options,
- final @NotNull BuildInfoProvider buildInfoProvider) {
- this.context = ContextUtils.getApplicationContext(context);
- this.options = options;
- this.buildInfoProvider = buildInfoProvider;
- this.persistingScopeObserver = options.findPersistingScopeObserver();
-
- final SentryStackTraceFactory sentryStackTraceFactory =
- new SentryStackTraceFactory(this.options);
-
- sentryExceptionFactory = new SentryExceptionFactory(sentryStackTraceFactory);
- }
-
- private @Nullable HintEnricher findEnricher(final @NotNull Object hint) {
- for (HintEnricher enricher : hintEnrichers) {
- if (enricher.supports(hint)) {
- return enricher;
- }
- }
- return null;
- }
-
- @Override
- public @NotNull SentryTransaction process(
- @NotNull SentryTransaction transaction, @NotNull Hint hint) {
- // that's only necessary because on newer versions of Unity, if not overriding this method, it's
- // throwing 'java.lang.AbstractMethodError: abstract method' and the reason is probably
- // compilation mismatch
- return transaction;
- }
-
- @Override
- public @Nullable SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) {
- final Object unwrappedHint = HintUtils.getSentrySdkHint(hint);
- if (!(unwrappedHint instanceof Backfillable)) {
- options
- .getLogger()
- .log(
- SentryLevel.WARNING,
- "The event is not Backfillable, but has been passed to BackfillingEventProcessor, skipping.");
- return event;
- }
- final @NotNull Backfillable backfillable = (Backfillable) unwrappedHint;
- final @Nullable HintEnricher hintEnricher = findEnricher(unwrappedHint);
-
- if (hintEnricher != null) {
- hintEnricher.applyPreEnrichment(event, backfillable, unwrappedHint);
- }
-
- // We always set os and device even if the ApplicationExitInfo event is not enrich-able.
- // The OS context may change in the meantime (OS update); we consider this an edge-case.
- mergeOS(event);
- setDevice(event);
-
- if (!backfillable.shouldEnrich()) {
- options
- .getLogger()
- .log(
- SentryLevel.DEBUG,
- "The event is Backfillable, but should not be enriched, skipping.");
- return event;
- }
-
- backfillScope(event);
-
- backfillOptions(event);
-
- setStaticValues(event);
-
- if (hintEnricher != null) {
- hintEnricher.applyPostEnrichment(event, backfillable, unwrappedHint);
- }
-
- return event;
- }
-
- // region scope persisted values
- private void backfillScope(final @NotNull SentryEvent event) {
- setRequest(event);
- setUser(event);
- setScopeTags(event);
- setBreadcrumbs(event);
- setExtras(event);
- setContexts(event);
- setTransaction(event);
- setFingerprints(event);
- setLevel(event);
- setTrace(event);
- setReplayId(event);
- }
-
- private boolean sampleReplay(final @NotNull SentryEvent event) {
- final @Nullable String replayErrorSampleRate =
- PersistingOptionsObserver.read(options, REPLAY_ERROR_SAMPLE_RATE_FILENAME, String.class);
-
- if (replayErrorSampleRate == null) {
- return false;
- }
-
- try {
- // we have to sample here with the old sample rate, because it may change between app launches
- final double replayErrorSampleRateDouble = Double.parseDouble(replayErrorSampleRate);
- if (replayErrorSampleRateDouble < SentryRandom.current().nextDouble()) {
- options
- .getLogger()
- .log(
- SentryLevel.DEBUG,
- "Not capturing replay for ANR %s due to not being sampled.",
- event.getEventId());
- return false;
- }
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Error parsing replay sample rate.", e);
- return false;
- }
-
- return true;
- }
-
- private void setReplayId(final @NotNull SentryEvent event) {
- @Nullable String persistedReplayId = readFromDisk(options, REPLAY_FILENAME, String.class);
- @Nullable String cacheDirPath = options.getCacheDirPath();
- if (cacheDirPath == null) {
- return;
- }
- final @NotNull File replayFolder = new File(cacheDirPath, "replay_" + persistedReplayId);
- if (!replayFolder.exists()) {
- if (!sampleReplay(event)) {
- return;
- }
- // if the replay folder does not exist (e.g. running in buffer mode), we need to find the
- // latest replay folder that was modified before the ANR event.
- persistedReplayId = null;
- long lastModified = Long.MIN_VALUE;
- final File[] dirs = new File(cacheDirPath).listFiles();
- if (dirs != null) {
- for (File dir : dirs) {
- if (dir.isDirectory() && dir.getName().startsWith("replay_")) {
- if (dir.lastModified() > lastModified
- && dir.lastModified() <= event.getTimestamp().getTime()) {
- lastModified = dir.lastModified();
- persistedReplayId = dir.getName().substring("replay_".length());
- }
- }
- }
- }
- }
-
- if (persistedReplayId == null) {
- return;
- }
-
- // store the relevant replayId so ReplayIntegration can pick it up and finalize that replay
- PersistingScopeObserver.store(options, persistedReplayId, REPLAY_FILENAME);
- event.getContexts().put(REPLAY_ID, persistedReplayId);
- }
-
- private void setTrace(final @NotNull SentryEvent event) {
- final SpanContext spanContext = readFromDisk(options, TRACE_FILENAME, SpanContext.class);
- if (event.getContexts().getTrace() == null) {
- if (spanContext != null) {
- event.getContexts().setTrace(spanContext);
- }
- }
- }
-
- private void setLevel(final @NotNull SentryEvent event) {
- final SentryLevel level = readFromDisk(options, LEVEL_FILENAME, SentryLevel.class);
- if (event.getLevel() == null) {
- event.setLevel(level);
- }
- }
-
- @SuppressWarnings("unchecked")
- private void setFingerprints(final @NotNull SentryEvent event) {
- final List fingerprint =
- (List) readFromDisk(options, FINGERPRINT_FILENAME, List.class);
- if (event.getFingerprints() == null) {
- event.setFingerprints(fingerprint);
- }
- }
-
- private void setTransaction(final @NotNull SentryEvent event) {
- final String transaction = readFromDisk(options, TRANSACTION_FILENAME, String.class);
- if (event.getTransaction() == null) {
- event.setTransaction(transaction);
- }
- }
-
- private void setContexts(final @NotNull SentryBaseEvent event) {
- final Contexts persistedContexts = readFromDisk(options, CONTEXTS_FILENAME, Contexts.class);
- if (persistedContexts == null) {
- return;
- }
- final Contexts eventContexts = event.getContexts();
- for (Map.Entry entry : new Contexts(persistedContexts).entrySet()) {
- final Object value = entry.getValue();
- if (SpanContext.TYPE.equals(entry.getKey()) && value instanceof SpanContext) {
- // we fill it in setTrace later on
- continue;
- }
- if (!eventContexts.containsKey(entry.getKey())) {
- eventContexts.put(entry.getKey(), value);
- }
- }
- }
-
- @SuppressWarnings("unchecked")
- private void setExtras(final @NotNull SentryBaseEvent event) {
- final Map extras =
- (Map) readFromDisk(options, EXTRAS_FILENAME, Map.class);
- if (extras == null) {
- return;
- }
- if (event.getExtras() == null) {
- event.setExtras(new HashMap<>(extras));
- } else {
- for (Map.Entry item : extras.entrySet()) {
- if (!event.getExtras().containsKey(item.getKey())) {
- event.getExtras().put(item.getKey(), item.getValue());
- }
- }
- }
- }
-
- @SuppressWarnings("unchecked")
- private void setBreadcrumbs(final @NotNull SentryBaseEvent event) {
- final List breadcrumbs =
- (List) readFromDisk(options, BREADCRUMBS_FILENAME, List.class);
- if (breadcrumbs == null) {
- return;
- }
- if (event.getBreadcrumbs() == null) {
- event.setBreadcrumbs(breadcrumbs);
- } else {
- event.getBreadcrumbs().addAll(breadcrumbs);
- }
- }
-
- @SuppressWarnings("unchecked")
- private void setScopeTags(final @NotNull SentryBaseEvent event) {
- final Map tags =
- (Map)
- readFromDisk(options, PersistingScopeObserver.TAGS_FILENAME, Map.class);
- if (tags == null) {
- return;
- }
- if (event.getTags() == null) {
- event.setTags(new HashMap<>(tags));
- } else {
- for (Map.Entry item : tags.entrySet()) {
- if (!event.getTags().containsKey(item.getKey())) {
- event.setTag(item.getKey(), item.getValue());
- }
- }
- }
- }
-
- private void setUser(final @NotNull SentryBaseEvent event) {
- if (event.getUser() == null) {
- final User user = readFromDisk(options, USER_FILENAME, User.class);
- event.setUser(user);
- }
- }
-
- private void setRequest(final @NotNull SentryBaseEvent event) {
- if (event.getRequest() == null) {
- final Request request = readFromDisk(options, REQUEST_FILENAME, Request.class);
- event.setRequest(request);
- }
- }
-
- private @Nullable T readFromDisk(
- final @NotNull SentryOptions options,
- final @NotNull String fileName,
- final @NotNull Class clazz) {
- if (persistingScopeObserver == null) {
- return null;
- }
-
- return persistingScopeObserver.read(options, fileName, clazz);
- }
-
- // endregion
-
- // region options persisted values
- private void backfillOptions(final @NotNull SentryEvent event) {
- setRelease(event);
- setEnvironment(event);
- setDist(event);
- setDebugMeta(event);
- setSdk(event);
- setApp(event);
- setOptionsTags(event);
- }
-
- private void setApp(final @NotNull SentryBaseEvent event) {
- App app = event.getContexts().getApp();
- if (app == null) {
- app = new App();
- }
- app.setAppName(ContextUtils.getApplicationName(context));
-
- final PackageInfo packageInfo = ContextUtils.getPackageInfo(context, buildInfoProvider);
- if (packageInfo != null) {
- app.setAppIdentifier(packageInfo.packageName);
- }
-
- // backfill versionName and versionCode from the persisted release string
- final String release =
- event.getRelease() != null
- ? event.getRelease()
- : PersistingOptionsObserver.read(options, RELEASE_FILENAME, String.class);
- if (release != null) {
- try {
- final String versionName =
- release.substring(release.indexOf('@') + 1, release.indexOf('+'));
- final String versionCode = release.substring(release.indexOf('+') + 1);
- app.setAppVersion(versionName);
- app.setAppBuild(versionCode);
- } catch (Throwable e) {
- options
- .getLogger()
- .log(SentryLevel.WARNING, "Failed to parse release from scope cache: %s", release);
- }
- }
-
- try {
- final ContextUtils.SplitApksInfo splitApksInfo =
- DeviceInfoUtil.getInstance(context, options).getSplitApksInfo();
- if (splitApksInfo != null) {
- app.setSplitApks(splitApksInfo.isSplitApks());
- if (splitApksInfo.getSplitNames() != null) {
- app.setSplitNames(Arrays.asList(splitApksInfo.getSplitNames()));
- }
- }
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Error getting split apks info.", e);
- }
-
- event.getContexts().setApp(app);
- }
-
- private void setRelease(final @NotNull SentryBaseEvent event) {
- if (event.getRelease() == null) {
- final String release =
- PersistingOptionsObserver.read(options, RELEASE_FILENAME, String.class);
- event.setRelease(release);
- }
- }
-
- private void setEnvironment(final @NotNull SentryBaseEvent event) {
- if (event.getEnvironment() == null) {
- final String environment =
- PersistingOptionsObserver.read(options, ENVIRONMENT_FILENAME, String.class);
- event.setEnvironment(environment != null ? environment : options.getEnvironment());
- }
- }
-
- private void setDebugMeta(final @NotNull SentryBaseEvent event) {
- DebugMeta debugMeta = event.getDebugMeta();
-
- if (debugMeta == null) {
- debugMeta = new DebugMeta();
- }
- if (debugMeta.getImages() == null) {
- debugMeta.setImages(new ArrayList<>());
- }
- List images = debugMeta.getImages();
- if (images != null) {
- final String proguardUuid =
- PersistingOptionsObserver.read(options, PROGUARD_UUID_FILENAME, String.class);
-
- if (proguardUuid != null) {
- final DebugImage debugImage = new DebugImage();
- debugImage.setType(DebugImage.PROGUARD);
- debugImage.setUuid(proguardUuid);
- images.add(debugImage);
- }
- event.setDebugMeta(debugMeta);
- }
- }
-
- private void setDist(final @NotNull SentryBaseEvent event) {
- if (event.getDist() == null) {
- final String dist = PersistingOptionsObserver.read(options, DIST_FILENAME, String.class);
- event.setDist(dist);
- }
- // if there's no user-set dist, fall back to versionCode from the persisted release string
- if (event.getDist() == null) {
- final String release =
- PersistingOptionsObserver.read(options, RELEASE_FILENAME, String.class);
- if (release != null) {
- try {
- final String versionCode = release.substring(release.indexOf('+') + 1);
- event.setDist(versionCode);
- } catch (Throwable e) {
- options
- .getLogger()
- .log(SentryLevel.WARNING, "Failed to parse release from scope cache: %s", release);
- }
- }
- }
- }
-
- private void setSdk(final @NotNull SentryBaseEvent event) {
- if (event.getSdk() == null) {
- final SdkVersion sdkVersion =
- PersistingOptionsObserver.read(options, SDK_VERSION_FILENAME, SdkVersion.class);
- event.setSdk(sdkVersion);
- }
- }
-
- @SuppressWarnings("unchecked")
- private void setOptionsTags(final @NotNull SentryBaseEvent event) {
- final Map tags =
- (Map)
- PersistingOptionsObserver.read(
- options, PersistingOptionsObserver.TAGS_FILENAME, Map.class);
- if (tags == null) {
- return;
- }
- if (event.getTags() == null) {
- event.setTags(new HashMap<>(tags));
- } else {
- for (Map.Entry item : tags.entrySet()) {
- if (!event.getTags().containsKey(item.getKey())) {
- event.setTag(item.getKey(), item.getValue());
- }
- }
- }
- }
-
- // endregion
-
- @Override
- public @Nullable Long getOrder() {
- return 12000L;
- }
-
- // region static values
- private void setStaticValues(final @NotNull SentryEvent event) {
- mergeUser(event);
- setSideLoadedInfo(event);
- }
-
- private void setDefaultPlatform(final @NotNull SentryBaseEvent event) {
- if (event.getPlatform() == null) {
- // this actually means JVM related.
- event.setPlatform(SentryBaseEvent.DEFAULT_PLATFORM);
- }
- }
-
- private void mergeUser(final @NotNull SentryBaseEvent event) {
- @Nullable User user = event.getUser();
- if (user == null) {
- user = new User();
- event.setUser(user);
- }
-
- // userId should be set even if event is Cached as the userId is static and won't change anyway.
- if (user.getId() == null) {
- user.setId(getDeviceId());
- }
- if (user.getIpAddress() == null && options.isSendDefaultPii()) {
- user.setIpAddress(IpAddressUtils.DEFAULT_IP_ADDRESS);
- }
- }
-
- private @Nullable String getDeviceId() {
- try {
- return options.getRuntimeManager().runWithRelaxedPolicy(() -> Installation.id(context));
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Error getting installationId.", e);
- }
- return null;
- }
-
- private void setSideLoadedInfo(final @NotNull SentryBaseEvent event) {
- try {
- final ContextUtils.SideLoadedInfo sideLoadedInfo =
- DeviceInfoUtil.getInstance(context, options).getSideLoadedInfo();
- if (sideLoadedInfo != null) {
- final @NotNull Map tags = sideLoadedInfo.asTags();
- for (Map.Entry entry : tags.entrySet()) {
- event.setTag(entry.getKey(), entry.getValue());
- }
- }
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Error getting side loaded info.", e);
- }
- }
-
- private void setDevice(final @NotNull SentryBaseEvent event) {
- if (event.getContexts().getDevice() == null) {
- event.getContexts().setDevice(getDevice());
- }
- }
-
- // only use static data that does not change between app launches (e.g. timezone, boottime,
- // battery level will change)
- @SuppressLint("NewApi")
- private @NotNull Device getDevice() {
- Device device = new Device();
- device.setManufacturer(Build.MANUFACTURER);
- device.setBrand(Build.BRAND);
- device.setFamily(ContextUtils.getFamily(options.getLogger()));
- device.setModel(Build.MODEL);
- device.setModelId(Build.ID);
- device.setArchs(ContextUtils.getArchitectures());
-
- final ActivityManager.MemoryInfo memInfo =
- ContextUtils.getMemInfo(context, options.getLogger());
- if (memInfo != null) {
- // in bytes
- device.setMemorySize(getMemorySize(memInfo));
- }
-
- device.setSimulator(buildInfoProvider.isEmulator());
-
- DisplayMetrics displayMetrics = ContextUtils.getDisplayMetrics(context, options.getLogger());
- if (displayMetrics != null) {
- device.setScreenWidthPixels(displayMetrics.widthPixels);
- device.setScreenHeightPixels(displayMetrics.heightPixels);
- device.setScreenDensity(displayMetrics.density);
- device.setScreenDpi(displayMetrics.densityDpi);
- }
-
- if (device.getId() == null) {
- device.setId(getDeviceId());
- }
-
- final @NotNull List cpuFrequencies = CpuInfoUtils.getInstance().readMaxFrequencies();
- if (!cpuFrequencies.isEmpty()) {
- device.setProcessorFrequency(Collections.max(cpuFrequencies).doubleValue());
- device.setProcessorCount(cpuFrequencies.size());
- }
-
- return device;
- }
-
- private @NotNull Long getMemorySize(final @NotNull ActivityManager.MemoryInfo memInfo) {
- return memInfo.totalMem;
- }
-
- private void mergeOS(final @NotNull SentryBaseEvent event) {
- final OperatingSystem currentOS = event.getContexts().getOperatingSystem();
- final OperatingSystem androidOS =
- DeviceInfoUtil.getInstance(context, options).getOperatingSystem();
-
- // make Android OS the main OS using the 'os' key
- event.getContexts().setOperatingSystem(androidOS);
-
- if (currentOS != null) {
- // add additional OS which was already part of the SentryEvent (eg Linux read from NDK)
- String osNameKey = currentOS.getName();
- if (osNameKey != null && !osNameKey.isEmpty()) {
- osNameKey = "os_" + osNameKey.trim().toLowerCase(Locale.ROOT);
- } else {
- osNameKey = "os_1";
- }
- event.getContexts().put(osNameKey, currentOS);
- }
- // endregion
- }
-
- private interface HintEnricher {
- boolean supports(@NotNull Object hint);
-
- void applyPreEnrichment(
- @NotNull SentryEvent event, @NotNull Backfillable hint, @NotNull Object rawHint);
-
- void applyPostEnrichment(
- @NotNull SentryEvent event, @NotNull Backfillable hint, @NotNull Object rawHint);
- }
-
- private final class AnrHintEnricher implements HintEnricher {
-
- @Override
- public boolean supports(@NotNull Object hint) {
- // While this is specifically an ANR enricher we discriminate enrichment application
- // on the broader AbnormalExit hints for now.
- return hint instanceof AbnormalExit;
- }
-
- // by default we assume that the ANR is foreground, unless abnormalMechanism is "anr_background"
- private boolean isBackgroundAnr(final @NotNull Object hint) {
- if (hint instanceof AbnormalExit) {
- final String abnormalMechanism = ((AbnormalExit) hint).mechanism();
- return "anr_background".equals(abnormalMechanism);
- }
- return false;
- }
-
- @Override
- public void applyPreEnrichment(
- @NotNull SentryEvent event, @NotNull Backfillable hint, @NotNull Object rawHint) {
- final boolean isBackgroundAnr = isBackgroundAnr(rawHint);
- // we always set exception values and default platform even if the ANR is not enrich-able
- setDefaultPlatform(event);
- setAnrExceptions(event, hint, isBackgroundAnr);
- }
-
- @Override
- public void applyPostEnrichment(
- @NotNull SentryEvent event, @NotNull Backfillable hint, @NotNull Object rawHint) {
- final boolean isBackgroundAnr = isBackgroundAnr(rawHint);
- setAppForeground(event, !isBackgroundAnr);
- setDefaultAnrFingerprint(event, isBackgroundAnr);
- }
-
- private void setDefaultAnrFingerprint(
- final @NotNull SentryEvent event, final boolean isBackgroundAnr) {
- // sentry does not yet have a capability to provide default server-side fingerprint rules,
- // so we're doing this on the SDK side to group background and foreground ANRs separately
- // even if they have similar stacktraces.
- if (event.getFingerprints() == null) {
- event.setFingerprints(
- Arrays.asList("{{ default }}", isBackgroundAnr ? "background-anr" : "foreground-anr"));
- }
- }
-
- private void setAppForeground(
- final @NotNull SentryBaseEvent event, final boolean inForeground) {
- App app = event.getContexts().getApp();
- if (app == null) {
- app = new App();
- event.getContexts().setApp(app);
- }
- // TODO: not entirely correct, because we define background ANRs as not the ones of
- // IMPORTANCE_FOREGROUND, but this doesn't mean the app was in foreground when an ANR
- // happened but it's our best effort for now. We could serialize AppState in theory.
- if (app.getInForeground() == null) {
- app.setInForeground(inForeground);
- }
- }
-
- @Nullable
- private SentryThread findMainThread(final @Nullable List threads) {
- if (threads != null) {
- for (SentryThread thread : threads) {
- final String name = thread.getName();
- if (name != null && name.equals("main")) {
- return thread;
- }
- }
- }
- return null;
- }
-
- private void setAnrExceptions(
- final @NotNull SentryEvent event,
- final @NotNull Backfillable hint,
- final boolean isBackgroundAnr) {
- if (event.getExceptions() != null) {
- return;
- }
- // AnrV2 threads contain a thread dump from the OS, so we just search for the main thread dump
- // and make an exception out of its stacktrace
- final Mechanism mechanism = new Mechanism();
- if (!hint.shouldEnrich()) {
- // we only enrich the latest ANR in the list, so this is historical
- mechanism.setType("HistoricalAppExitInfo");
- } else {
- mechanism.setType("AppExitInfo");
- }
-
- String message = "ANR";
- if (isBackgroundAnr) {
- message = "Background " + message;
- }
- final ApplicationNotResponding anr =
- new ApplicationNotResponding(message, Thread.currentThread());
-
- SentryThread mainThread = findMainThread(event.getThreads());
- if (mainThread == null) {
- // if there's no main thread in the event threads, we just create a dummy thread so the
- // exception is properly created as well, but without stacktrace
- mainThread = new SentryThread();
- mainThread.setStacktrace(new SentryStackTrace());
- }
- event.setExceptions(
- sentryExceptionFactory.getSentryExceptionsFromThread(mainThread, mechanism, anr));
- }
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoHistoryDispatcher.java b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoHistoryDispatcher.java
deleted file mode 100644
index 79c3f19c6e5..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoHistoryDispatcher.java
+++ /dev/null
@@ -1,247 +0,0 @@
-package io.sentry.android.core;
-
-import android.app.ActivityManager;
-import android.app.ApplicationExitInfo;
-import android.content.Context;
-import android.os.Build;
-import androidx.annotation.RequiresApi;
-import io.sentry.Hint;
-import io.sentry.IScopes;
-import io.sentry.SentryEvent;
-import io.sentry.SentryLevel;
-import io.sentry.cache.EnvelopeCache;
-import io.sentry.cache.IEnvelopeCache;
-import io.sentry.hints.BlockingFlushHint;
-import io.sentry.protocol.SentryId;
-import io.sentry.transport.ICurrentDateProvider;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-@ApiStatus.Internal
-final class ApplicationExitInfoHistoryDispatcher implements Runnable {
-
- // using 91 to avoid timezone change hassle, 90 days is how long Sentry keeps the events
- static final long NINETY_DAYS_THRESHOLD = TimeUnit.DAYS.toMillis(91);
-
- private final @NotNull Context context;
- private final @NotNull IScopes scopes;
- private final @NotNull SentryAndroidOptions options;
- private final @NotNull ApplicationExitInfoPolicy policy;
- private final long threshold;
-
- ApplicationExitInfoHistoryDispatcher(
- final @NotNull Context context,
- final @NotNull IScopes scopes,
- final @NotNull SentryAndroidOptions options,
- final @NotNull ICurrentDateProvider dateProvider,
- final @NotNull ApplicationExitInfoPolicy policy) {
- this.context = ContextUtils.getApplicationContext(context);
- this.scopes = scopes;
- this.options = options;
- this.policy = policy;
- this.threshold = dateProvider.getCurrentTimeMillis() - NINETY_DAYS_THRESHOLD;
- }
-
- @RequiresApi(api = Build.VERSION_CODES.R)
- @Override
- public void run() {
- final ActivityManager activityManager =
- (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
-
- if (activityManager == null) {
- options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve ActivityManager.");
- return;
- }
-
- final List applicationExitInfoList =
- activityManager.getHistoricalProcessExitReasons(null, 0, 0);
-
- if (applicationExitInfoList.isEmpty()) {
- options.getLogger().log(SentryLevel.DEBUG, "No records in historical exit reasons.");
- return;
- }
-
- waitPreviousSessionFlush();
-
- final List exitInfos = new ArrayList<>(applicationExitInfoList);
- final @Nullable Long lastReportedTimestamp = policy.getLastReportedTimestamp();
-
- final ApplicationExitInfo latest = removeLatest(exitInfos);
- if (latest == null) {
- options
- .getLogger()
- .log(
- SentryLevel.DEBUG,
- "No %ss have been found in the historical exit reasons list.",
- policy.getLabel());
- return;
- }
-
- if (latest.getTimestamp() < threshold) {
- options
- .getLogger()
- .log(
- SentryLevel.DEBUG,
- "Latest %s happened too long ago, returning early.",
- policy.getLabel());
- return;
- }
-
- if (lastReportedTimestamp != null && latest.getTimestamp() <= lastReportedTimestamp) {
- options
- .getLogger()
- .log(
- SentryLevel.DEBUG,
- "Latest %s has already been reported, returning early.",
- policy.getLabel());
- return;
- }
-
- if (policy.shouldReportHistorical()) {
- reportHistorical(exitInfos, lastReportedTimestamp);
- }
-
- report(latest, true);
- }
-
- private void waitPreviousSessionFlush() {
- final IEnvelopeCache cache = options.getEnvelopeDiskCache();
- if (cache instanceof EnvelopeCache) {
- if (options.isEnableAutoSessionTracking()
- && !((EnvelopeCache) cache).waitPreviousSessionFlush()) {
- options
- .getLogger()
- .log(
- SentryLevel.WARNING,
- "Timed out waiting to flush previous session to its own file.");
-
- // if we timed out waiting here, we can already flush the latch, because the timeout is
- // big enough to wait for it only once and we don't have to wait again in
- // PreviousSessionFinalizer
- ((EnvelopeCache) cache).flushPreviousSession();
- }
- }
- }
-
- @RequiresApi(api = Build.VERSION_CODES.R)
- private @Nullable ApplicationExitInfo removeLatest(
- final @NotNull List exitInfos) {
- for (Iterator it = exitInfos.iterator(); it.hasNext(); ) {
- ApplicationExitInfo applicationExitInfo = it.next();
- if (applicationExitInfo.getReason() == policy.getTargetReason()) {
- it.remove();
- return applicationExitInfo;
- }
- }
- return null;
- }
-
- @RequiresApi(api = Build.VERSION_CODES.R)
- private void reportHistorical(
- final @NotNull List exitInfos,
- final @Nullable Long lastReportedTimestamp) {
- Collections.reverse(exitInfos);
- for (ApplicationExitInfo applicationExitInfo : exitInfos) {
- if (applicationExitInfo.getReason() == policy.getTargetReason()) {
- if (applicationExitInfo.getTimestamp() < threshold) {
- options
- .getLogger()
- .log(
- SentryLevel.DEBUG,
- "%s happened too long ago %s.",
- policy.getLabel(),
- applicationExitInfo);
- continue;
- }
-
- if (lastReportedTimestamp != null
- && applicationExitInfo.getTimestamp() <= lastReportedTimestamp) {
- options
- .getLogger()
- .log(
- SentryLevel.DEBUG,
- "%s has already been reported %s.",
- policy.getLabel(),
- applicationExitInfo);
- continue;
- }
-
- report(applicationExitInfo, false); // do not enrich past events
- }
- }
- }
-
- private void report(final @NotNull ApplicationExitInfo exitInfo, final boolean enrich) {
- final @Nullable Report report = policy.buildReport(exitInfo, enrich);
-
- if (report == null) {
- return;
- }
-
- final @NotNull SentryId sentryId = scopes.captureEvent(report.getEvent(), report.getHint());
- final boolean isEventDropped = sentryId.equals(SentryId.EMPTY_ID);
- if (!isEventDropped) {
- final @Nullable BlockingFlushHint flushHint = report.getFlushHint();
- if (flushHint != null && !flushHint.waitFlush()) {
- options
- .getLogger()
- .log(
- SentryLevel.WARNING,
- "Timed out waiting to flush %s event to disk. Event: %s",
- policy.getLabel(),
- report.getEvent().getEventId());
- }
- }
- }
-
- interface ApplicationExitInfoPolicy {
- @NotNull
- String getLabel();
-
- int getTargetReason();
-
- boolean shouldReportHistorical();
-
- @Nullable
- Long getLastReportedTimestamp();
-
- @Nullable
- Report buildReport(@NotNull ApplicationExitInfo exitInfo, boolean enrich);
- }
-
- public static final class Report {
- private final @NotNull SentryEvent event;
- private final @NotNull Hint hint;
- private final @Nullable BlockingFlushHint flushHint;
-
- Report(
- final @NotNull SentryEvent event,
- final @NotNull Hint hint,
- final @Nullable BlockingFlushHint flushHint) {
- this.event = event;
- this.hint = hint;
- this.flushHint = flushHint;
- }
-
- @NotNull
- public SentryEvent getEvent() {
- return event;
- }
-
- @NotNull
- public Hint getHint() {
- return hint;
- }
-
- @Nullable
- public BlockingFlushHint getFlushHint() {
- return flushHint;
- }
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationNotResponding.java b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationNotResponding.java
deleted file mode 100644
index f4998240f81..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationNotResponding.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// https://github.com/SalomonBrys/ANR-WatchDog/blob/1969075f75f5980e9000eaffbaa13b0daf282dcb/anr-watchdog/src/main/java/com/github/anrwatchdog/ANRError.java
-// Based on the class above. The API unnecessary here was removed.
-package io.sentry.android.core;
-
-import io.sentry.util.Objects;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * Error thrown by ANRWatchDog when an ANR is detected. Contains the stack trace of the frozen UI
- * thread.
- *
- * It is important to notice that, in an ApplicationNotResponding, all the "Caused by" are not
- * really the cause of the exception. Each "Caused by" is the stack trace of a running thread. Note
- * that the main thread always comes first.
- */
-final class ApplicationNotResponding extends RuntimeException {
- private static final long serialVersionUID = 252541144579117016L;
-
- private final @NotNull Thread thread;
-
- ApplicationNotResponding(final @Nullable String message, final @NotNull Thread thread) {
- super(message);
- this.thread = Objects.requireNonNull(thread, "Thread must be provided.");
- setStackTrace(this.thread.getStackTrace());
- }
-
- public @NotNull Thread getThread() {
- return thread;
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/BuildInfoProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/BuildInfoProvider.java
deleted file mode 100644
index 5b0786ffb7f..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/BuildInfoProvider.java
+++ /dev/null
@@ -1,76 +0,0 @@
-package io.sentry.android.core;
-
-import android.os.Build;
-import io.sentry.ILogger;
-import io.sentry.SentryLevel;
-import io.sentry.util.Objects;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/** The Android Impl. of BuildInfoProvider which returns the Build class info. */
-@ApiStatus.Internal
-public final class BuildInfoProvider {
-
- final @NotNull ILogger logger;
-
- public BuildInfoProvider(final @NotNull ILogger logger) {
- this.logger = Objects.requireNonNull(logger, "The ILogger object is required.");
- }
-
- /**
- * Returns the Build.VERSION.SDK_INT
- *
- * @return the Build.VERSION.SDK_INT
- */
- public int getSdkInfoVersion() {
- return Build.VERSION.SDK_INT;
- }
-
- public @Nullable String getBuildTags() {
- return Build.TAGS;
- }
-
- public @Nullable String getManufacturer() {
- return Build.MANUFACTURER;
- }
-
- public @Nullable String getModel() {
- return Build.MODEL;
- }
-
- public @Nullable String getVersionRelease() {
- return Build.VERSION.RELEASE;
- }
-
- /**
- * Check whether the application is running in an emulator.
- * https://github.com/flutter/plugins/blob/master/packages/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java#L105
- *
- * @return true if the application is running in an emulator, false otherwise
- */
- public @Nullable Boolean isEmulator() {
- try {
- return (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
- || Build.FINGERPRINT.startsWith("generic")
- || Build.FINGERPRINT.startsWith("unknown")
- || Build.HARDWARE.contains("goldfish")
- || Build.HARDWARE.contains("ranchu")
- || Build.MODEL.contains("google_sdk")
- || Build.MODEL.contains("Emulator")
- || Build.MODEL.contains("Android SDK built for x86")
- || Build.MANUFACTURER.contains("Genymotion")
- || Build.PRODUCT.contains("sdk_google")
- || Build.PRODUCT.contains("google_sdk")
- || Build.PRODUCT.contains("sdk")
- || Build.PRODUCT.contains("sdk_x86")
- || Build.PRODUCT.contains("vbox86p")
- || Build.PRODUCT.contains("emulator")
- || Build.PRODUCT.contains("simulator");
- } catch (Throwable e) {
- logger.log(
- SentryLevel.ERROR, "Error checking whether application is running in an emulator.", e);
- return null;
- }
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java
deleted file mode 100644
index 60ae00f2ead..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java
+++ /dev/null
@@ -1,551 +0,0 @@
-package io.sentry.android.core;
-
-import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
-import static android.content.Context.ACTIVITY_SERVICE;
-import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
-
-import android.annotation.SuppressLint;
-import android.app.ActivityManager;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.os.Build;
-import android.os.Handler;
-import android.util.DisplayMetrics;
-import io.sentry.ILogger;
-import io.sentry.SentryLevel;
-import io.sentry.SentryOptions;
-import io.sentry.android.core.util.AndroidLazyEvaluator;
-import io.sentry.protocol.App;
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.jetbrains.annotations.TestOnly;
-
-@ApiStatus.Internal
-public final class ContextUtils {
-
- static class SideLoadedInfo {
- private final boolean isSideLoaded;
- private final @Nullable String installerStore;
-
- public SideLoadedInfo(final boolean isSideLoaded, final @Nullable String installerStore) {
- this.isSideLoaded = isSideLoaded;
- this.installerStore = installerStore;
- }
-
- public boolean isSideLoaded() {
- return isSideLoaded;
- }
-
- public @Nullable String getInstallerStore() {
- return installerStore;
- }
-
- public @NotNull Map asTags() {
- final Map data = new HashMap<>();
- data.put("isSideLoaded", String.valueOf(isSideLoaded));
- if (installerStore != null) {
- data.put("installerStore", installerStore);
- }
- return data;
- }
- }
-
- static class SplitApksInfo {
- // https://github.com/google/bundletool/blob/master/src/main/java/com/android/tools/build/bundletool/model/AndroidManifest.java#L257-L263
- static final String SPLITS_REQUIRED = "com.android.vending.splits.required";
-
- private final boolean isSplitApks;
- private final String[] splitNames;
-
- public SplitApksInfo(final boolean isSplitApks, final String[] splitNames) {
- this.isSplitApks = isSplitApks;
- this.splitNames = splitNames;
- }
-
- public boolean isSplitApks() {
- return isSplitApks;
- }
-
- public @Nullable String[] getSplitNames() {
- return splitNames;
- }
- }
-
- private ContextUtils() {}
-
- // to avoid doing a bunch of Binder calls we use LazyEvaluator to cache the values that are static
- // during the app process running
-
- /**
- * Since this packageInfo uses flags 0 we can assume it's static and cache it as the package name
- * or version code cannot change during runtime, only after app update (which will spin up a new
- * process).
- */
- @SuppressLint("NewApi")
- private static final @NotNull AndroidLazyEvaluator staticPackageInfo33 =
- new AndroidLazyEvaluator<>(
- context -> {
- try {
- return context
- .getPackageManager()
- .getPackageInfo(context.getPackageName(), PackageManager.PackageInfoFlags.of(0));
- } catch (Throwable e) {
- return null;
- }
- });
-
- private static final @NotNull AndroidLazyEvaluator staticPackageInfo =
- new AndroidLazyEvaluator<>(
- context -> {
- try {
- return context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
- } catch (Throwable e) {
- return null;
- }
- });
-
- private static final @NotNull AndroidLazyEvaluator applicationName =
- new AndroidLazyEvaluator<>(
- context -> {
- try {
- final ApplicationInfo applicationInfo = context.getApplicationInfo();
- final int stringId = applicationInfo.labelRes;
- if (stringId == 0) {
- if (applicationInfo.nonLocalizedLabel != null) {
- return applicationInfo.nonLocalizedLabel.toString();
- }
- return context.getPackageManager().getApplicationLabel(applicationInfo).toString();
- } else {
- return context.getString(stringId);
- }
- } catch (Throwable e) {
- return null;
- }
- });
-
- /**
- * Since this applicationInfo uses the same flag (METADATA) we can assume it's static and cache it
- * as the manifest metadata cannot change during runtime, only after app update (which will spin
- * up a new process).
- */
- @SuppressLint("NewApi")
- private static final @NotNull AndroidLazyEvaluator staticAppInfo33 =
- new AndroidLazyEvaluator<>(
- context -> {
- try {
- return context
- .getPackageManager()
- .getApplicationInfo(
- context.getPackageName(),
- PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA));
- } catch (Throwable e) {
- return null;
- }
- });
-
- private static final @NotNull AndroidLazyEvaluator staticAppInfo =
- new AndroidLazyEvaluator<>(
- context -> {
- try {
- return context
- .getPackageManager()
- .getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
- } catch (Throwable e) {
- return null;
- }
- });
-
- /**
- * Return the Application's PackageInfo if possible, or null.
- *
- * @return the Application's PackageInfo if possible, or null
- */
- @Nullable
- static PackageInfo getPackageInfo(
- final @NotNull Context context, final @NotNull BuildInfoProvider buildInfoProvider) {
- if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.TIRAMISU) {
- return staticPackageInfo33.getValue(context);
- } else {
- return staticPackageInfo.getValue(context);
- }
- }
-
- /**
- * Return the Application's PackageInfo with the specified flags if possible, or null.
- *
- * @return the Application's PackageInfo if possible, or null
- */
- @SuppressLint("NewApi")
- @Nullable
- @SuppressWarnings("deprecation")
- static PackageInfo getPackageInfo(
- final @NotNull Context context,
- final int flags,
- final @NotNull ILogger logger,
- final @NotNull BuildInfoProvider buildInfoProvider) {
- try {
- if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.TIRAMISU) {
- return context
- .getPackageManager()
- .getPackageInfo(context.getPackageName(), PackageManager.PackageInfoFlags.of(flags));
- } else {
- return context.getPackageManager().getPackageInfo(context.getPackageName(), flags);
- }
- } catch (Throwable e) {
- logger.log(SentryLevel.ERROR, "Error getting package info.", e);
- return null;
- }
- }
-
- /**
- * Return the Application's ApplicationInfo if possible. Throws @{@link
- * android.content.pm.PackageManager.NameNotFoundException} if the package is not found.
- *
- * @return the Application's ApplicationInfo if possible, or throws
- */
- @SuppressLint("NewApi")
- @Nullable
- @SuppressWarnings("deprecation")
- static ApplicationInfo getApplicationInfo(
- final @NotNull Context context, final @NotNull BuildInfoProvider buildInfoProvider) {
- if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.TIRAMISU) {
- return staticAppInfo33.getValue(context);
- } else {
- return staticAppInfo.getValue(context);
- }
- }
-
- /**
- * Returns the App's version code based on the PackageInfo
- *
- * @param packageInfo the PackageInfo class
- * @return the versionCode or LongVersionCode based on your API version
- */
- @SuppressLint("NewApi")
- @NotNull
- static String getVersionCode(
- final @NotNull PackageInfo packageInfo, final @NotNull BuildInfoProvider buildInfoProvider) {
- if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.P) {
- return Long.toString(packageInfo.getLongVersionCode());
- }
- return getVersionCodeDep(packageInfo);
- }
-
- /**
- * Returns the App's version name based on the PackageInfo
- *
- * @param packageInfo the PackageInfo class
- * @return the versionName
- */
- @Nullable
- static String getVersionName(final @NotNull PackageInfo packageInfo) {
- return packageInfo.versionName;
- }
-
- @SuppressWarnings("deprecation")
- private static @NotNull String getVersionCodeDep(final @NotNull PackageInfo packageInfo) {
- return Integer.toString(packageInfo.versionCode);
- }
-
- /**
- * Check if the Started process has IMPORTANCE_FOREGROUND importance which means that the process
- * will start an Activity.
- *
- * @return true if IMPORTANCE_FOREGROUND and false otherwise
- */
- @ApiStatus.Internal
- public static boolean isForegroundImportance() {
- try {
- final ActivityManager.RunningAppProcessInfo appProcessInfo =
- new ActivityManager.RunningAppProcessInfo();
- ActivityManager.getMyMemoryState(appProcessInfo);
- return appProcessInfo.importance == IMPORTANCE_FOREGROUND;
- } catch (Throwable ignored) {
- // should never happen
- }
- return false;
- }
-
- /**
- * Determines if the app is a packaged android library for running Compose Preview Mode
- *
- * @param context the context
- * @return true, if the app is actually a library running as an app for Compose Preview Mode
- */
- @ApiStatus.Internal
- public static boolean appIsLibraryForComposePreview(final @NotNull Context context) {
- // Jetpack Compose Preview (aka "Run Preview on Device")
- // uses the androidTest flavor for android library modules,
- // so let's fail-fast by checking this first
- if (context.getPackageName().endsWith(".test")) {
- try {
- final @NotNull ActivityManager activityManager =
- (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
- final @NotNull List appTasks = activityManager.getAppTasks();
- for (final ActivityManager.AppTask task : appTasks) {
- final @Nullable ComponentName component = task.getTaskInfo().baseIntent.getComponent();
- if (component != null
- && component.getClassName().equals("androidx.compose.ui.tooling.PreviewActivity")) {
- return true;
- }
- }
- } catch (Throwable t) {
- // ignored
- }
- }
- return false;
- }
-
- /**
- * Get the device's current kernel version, as a string. Attempts to read /proc/version, and falls
- * back to the 'os.version' System Property.
- *
- * @return the device's current kernel version, as a string
- */
- @SuppressWarnings("DefaultCharset")
- static @Nullable String getKernelVersion(final @NotNull ILogger logger) {
- // its possible to try to execute 'uname' and parse it or also another unix commands or even
- // looking for well known root installed apps
- final String errorMsg = "Exception while attempting to read kernel information";
- final String defaultVersion = System.getProperty("os.version");
-
- final File file = new File("/proc/version");
- if (!file.canRead()) {
- return defaultVersion;
- }
- try (BufferedReader br = new BufferedReader(new FileReader(file))) {
- return br.readLine();
- } catch (IOException e) {
- logger.log(SentryLevel.ERROR, errorMsg, e);
- }
-
- return defaultVersion;
- }
-
- @SuppressWarnings({"deprecation"})
- static @Nullable SideLoadedInfo retrieveSideLoadedInfo(
- final @NotNull Context context,
- final @NotNull ILogger logger,
- final @NotNull BuildInfoProvider buildInfoProvider) {
- String packageName = null;
- try {
- final PackageInfo packageInfo = getPackageInfo(context, buildInfoProvider);
- final PackageManager packageManager = context.getPackageManager();
-
- if (packageInfo != null && packageManager != null) {
- packageName = packageInfo.packageName;
-
- // getInstallSourceInfo requires INSTALL_PACKAGES permission which is only given to system
- // apps.
- // if it's installed via adb, system apps or untrusted sources
- // could be amazon, google play etc - or null in case of sideload
- final String installerPackageName = packageManager.getInstallerPackageName(packageName);
- return new SideLoadedInfo(installerPackageName == null, installerPackageName);
- }
- } catch (IllegalArgumentException e) {
- // it'll never be thrown as we are querying its own App's package.
- logger.log(SentryLevel.DEBUG, "%s package isn't installed.", packageName);
- }
-
- return null;
- }
-
- @SuppressWarnings({"deprecation"})
- static @Nullable SplitApksInfo retrieveSplitApksInfo(
- final @NotNull Context context, final @NotNull BuildInfoProvider buildInfoProvider) {
- String[] splitNames = null;
- final ApplicationInfo applicationInfo = getApplicationInfo(context, buildInfoProvider);
- final PackageInfo packageInfo = getPackageInfo(context, buildInfoProvider);
-
- if (packageInfo != null) {
- splitNames = packageInfo.splitNames;
- boolean isSplitApks = false;
- if (applicationInfo != null && applicationInfo.metaData != null) {
- isSplitApks = applicationInfo.metaData.getBoolean(SplitApksInfo.SPLITS_REQUIRED);
- }
-
- return new SplitApksInfo(isSplitApks, splitNames);
- }
-
- return null;
- }
-
- /**
- * Get the human-facing Application name.
- *
- * @return Application name
- */
- static @Nullable String getApplicationName(final @NotNull Context context) {
- return applicationName.getValue(context);
- }
-
- /**
- * Get the DisplayMetrics object for the current application.
- *
- * @return the DisplayMetrics object for the current application
- */
- static @Nullable DisplayMetrics getDisplayMetrics(
- final @NotNull Context context, final @NotNull ILogger logger) {
- try {
- return context.getResources().getDisplayMetrics();
- } catch (Throwable e) {
- logger.log(SentryLevel.ERROR, "Error getting DisplayMetrics.", e);
- return null;
- }
- }
-
- /**
- * Fake the device family by using the first word in the Build.MODEL. Works well in most cases...
- * "Nexus 6P" -> "Nexus", "Galaxy S7" -> "Galaxy".
- *
- * @return family name of the device, as best we can tell
- */
- static @Nullable String getFamily(final @NotNull ILogger logger) {
- try {
- return Build.MODEL.split(" ", -1)[0];
- } catch (Throwable e) {
- logger.log(SentryLevel.ERROR, "Error getting device family.", e);
- return null;
- }
- }
-
- static @NotNull String[] getArchitectures() {
- return Build.SUPPORTED_ABIS;
- }
-
- /**
- * Get MemoryInfo object representing the memory state of the application.
- *
- * @return MemoryInfo object representing the memory state of the application
- */
- static @Nullable ActivityManager.MemoryInfo getMemInfo(
- final @NotNull Context context, final @NotNull ILogger logger) {
- try {
- final ActivityManager actManager =
- (ActivityManager) context.getSystemService(ACTIVITY_SERVICE);
- final ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();
- if (actManager != null) {
- actManager.getMemoryInfo(memInfo);
- return memInfo;
- }
- logger.log(SentryLevel.INFO, "Error getting MemoryInfo.");
- return null;
- } catch (Throwable e) {
- logger.log(SentryLevel.ERROR, "Error getting MemoryInfo.", e);
- return null;
- }
- }
-
- /** Register an exported BroadcastReceiver, independently from platform version. */
- static @Nullable Intent registerReceiver(
- final @NotNull Context context,
- final @NotNull SentryOptions options,
- final @Nullable BroadcastReceiver receiver,
- final @NotNull IntentFilter filter,
- final @Nullable Handler handler) {
- return registerReceiver(
- context, new BuildInfoProvider(options.getLogger()), receiver, filter, handler);
- }
-
- /** Register an exported BroadcastReceiver, independently from platform version. */
- @SuppressLint({"NewApi", "UnspecifiedRegisterReceiverFlag"})
- static @Nullable Intent registerReceiver(
- final @NotNull Context context,
- final @NotNull BuildInfoProvider buildInfoProvider,
- final @Nullable BroadcastReceiver receiver,
- final @NotNull IntentFilter filter,
- final @Nullable Handler handler) {
- if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.TIRAMISU) {
- // From https://developer.android.com/guide/components/broadcasts#context-registered-receivers
- // If this receiver is listening for broadcasts sent from the system or from other apps, even
- // other apps that you own—use the RECEIVER_EXPORTED flag. If instead this receiver is
- // listening only for broadcasts sent by your app, use the RECEIVER_NOT_EXPORTED flag.
- return context.registerReceiver(
- receiver, filter, null, handler, Context.RECEIVER_NOT_EXPORTED);
- } else {
- return context.registerReceiver(receiver, filter, null, handler);
- }
- }
-
- static void setAppPackageInfo(
- final @NotNull PackageInfo packageInfo,
- final @NotNull BuildInfoProvider buildInfoProvider,
- final @Nullable DeviceInfoUtil deviceInfoUtil,
- final @NotNull App app) {
- app.setAppIdentifier(packageInfo.packageName);
- app.setAppVersion(packageInfo.versionName);
- app.setAppBuild(ContextUtils.getVersionCode(packageInfo, buildInfoProvider));
-
- final Map permissions = new HashMap<>();
- final String[] requestedPermissions = packageInfo.requestedPermissions;
- final int[] requestedPermissionsFlags = packageInfo.requestedPermissionsFlags;
-
- if (requestedPermissions != null
- && requestedPermissions.length > 0
- && requestedPermissionsFlags != null
- && requestedPermissionsFlags.length > 0) {
- for (int i = 0; i < requestedPermissions.length; i++) {
- String permission = requestedPermissions[i];
- permission = permission.substring(permission.lastIndexOf('.') + 1);
-
- final boolean granted =
- (requestedPermissionsFlags[i] & REQUESTED_PERMISSION_GRANTED)
- == REQUESTED_PERMISSION_GRANTED;
- permissions.put(permission, granted ? "granted" : "not_granted");
- }
- }
- app.setPermissions(permissions);
-
- if (deviceInfoUtil != null) {
- try {
- final ContextUtils.SplitApksInfo splitApksInfo = deviceInfoUtil.getSplitApksInfo();
- if (splitApksInfo != null) {
- app.setSplitApks(splitApksInfo.isSplitApks());
- if (splitApksInfo.getSplitNames() != null) {
- app.setSplitNames(Arrays.asList(splitApksInfo.getSplitNames()));
- }
- }
- } catch (Throwable e) {
- }
- }
- }
-
- /**
- * Get the app context
- *
- * @return the app context, or if not available, the provided context
- */
- @NotNull
- public static Context getApplicationContext(final @NotNull Context context) {
- // it returns null if ContextImpl, so let's check for nullability
- final @Nullable Context appContext = context.getApplicationContext();
- if (appContext != null) {
- return appContext;
- }
- return context;
- }
-
- @TestOnly
- static void resetInstance() {
- staticPackageInfo33.resetValue();
- staticPackageInfo.resetValue();
- applicationName.resetValue();
- staticAppInfo33.resetValue();
- staticAppInfo.resetValue();
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/CurrentActivityHolder.java b/sentry-android-core/src/main/java/io/sentry/android/core/CurrentActivityHolder.java
deleted file mode 100644
index a7733821c05..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/CurrentActivityHolder.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package io.sentry.android.core;
-
-import android.app.Activity;
-import java.lang.ref.WeakReference;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-@ApiStatus.Internal
-public class CurrentActivityHolder {
-
- private static final @NotNull CurrentActivityHolder instance = new CurrentActivityHolder();
-
- private CurrentActivityHolder() {}
-
- private @Nullable WeakReference currentActivity;
-
- public static @NotNull CurrentActivityHolder getInstance() {
- return instance;
- }
-
- public @Nullable Activity getActivity() {
- if (currentActivity != null) {
- return currentActivity.get();
- }
- return null;
- }
-
- public void setActivity(final @NotNull Activity activity) {
- if (currentActivity != null && currentActivity.get() == activity) {
- return;
- }
-
- currentActivity = new WeakReference<>(activity);
- }
-
- public void clearActivity() {
- currentActivity = null;
- }
-
- public void clearActivity(final @NotNull Activity activity) {
- if (currentActivity != null && currentActivity.get() != activity) {
- return;
- }
- currentActivity = null;
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java
deleted file mode 100644
index 14cafab224d..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java
+++ /dev/null
@@ -1,428 +0,0 @@
-package io.sentry.android.core;
-
-import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.os.Build;
-import io.sentry.*;
-import io.sentry.android.core.internal.util.AndroidThreadChecker;
-import io.sentry.android.core.performance.AppStartMetrics;
-import io.sentry.android.core.performance.TimeSpan;
-import io.sentry.protocol.App;
-import io.sentry.protocol.OperatingSystem;
-import io.sentry.protocol.SentryException;
-import io.sentry.protocol.SentryStackFrame;
-import io.sentry.protocol.SentryStackTrace;
-import io.sentry.protocol.SentryThread;
-import io.sentry.protocol.SentryTransaction;
-import io.sentry.protocol.User;
-import io.sentry.util.HintUtils;
-import io.sentry.util.LazyEvaluator;
-import io.sentry.util.Objects;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.RejectedExecutionException;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.jetbrains.annotations.TestOnly;
-
-final class DefaultAndroidEventProcessor implements EventProcessor {
-
- @TestOnly final Context context;
-
- private final @NotNull BuildInfoProvider buildInfoProvider;
- private final @NotNull SentryAndroidOptions options;
- @TestOnly final @Nullable Future deviceInfoUtil;
- private final @NotNull LazyEvaluator deviceFamily =
- new LazyEvaluator<>(() -> ContextUtils.getFamily(NoOpLogger.getInstance()));
-
- public DefaultAndroidEventProcessor(
- final @NotNull Context context,
- final @NotNull BuildInfoProvider buildInfoProvider,
- final @NotNull SentryAndroidOptions options) {
- this.context =
- Objects.requireNonNull(
- ContextUtils.getApplicationContext(context), "The application context is required.");
- this.buildInfoProvider =
- Objects.requireNonNull(buildInfoProvider, "The BuildInfoProvider is required.");
- this.options = Objects.requireNonNull(options, "The options object is required.");
-
- // don't ref. to method reference, theres a bug on it
- // noinspection Convert2MethodRef
- // some device info performs disk I/O, but it's result is cached, let's pre-cache it
- @Nullable Future deviceInfoUtil;
- final @NotNull ExecutorService executorService = Executors.newSingleThreadExecutor();
- try {
- deviceInfoUtil =
- executorService.submit(() -> DeviceInfoUtil.getInstance(this.context, options));
- } catch (RejectedExecutionException e) {
- deviceInfoUtil = null;
- options.getLogger().log(SentryLevel.WARNING, "Device info caching task rejected.", e);
- }
- this.deviceInfoUtil = deviceInfoUtil;
- executorService.shutdown();
- }
-
- @Override
- public @NotNull SentryEvent process(final @NotNull SentryEvent event, final @NotNull Hint hint) {
- final boolean applyScopeData = shouldApplyScopeData(event, hint);
- if (applyScopeData) {
- // we only set memory data if it's not a hard crash, when it's a hard crash the event is
- // enriched on restart, so non static data might be wrong, eg lowMemory or availMem will
- // be different if the App. crashes because of OOM.
- processNonCachedEvent(event, hint);
- setThreads(event, hint);
- }
-
- setCommons(event, true, applyScopeData);
-
- fixExceptionOrder(event);
-
- return event;
- }
-
- @Override
- public @Nullable SentryLogEvent process(@NotNull SentryLogEvent event) {
- setDevice(event);
- setOs(event);
- return event;
- }
-
- @Override
- public @Nullable SentryMetricsEvent process(
- final @NotNull SentryMetricsEvent event, final @NotNull Hint hint) {
- setDevice(event);
- setOs(event);
- return event;
- }
-
- /**
- * The last exception is usually used for picking the issue title, but the convention is to send
- * inner exceptions first, e.g. [inner, outer] This doesn't work very well on Android, as some
- * hooks like Application.onCreate is wrapped by Android framework with a RuntimeException. Thus,
- * if the last exception is a RuntimeInit$MethodAndArgsCaller, reverse the order to get a better
- * issue title. This is a quick fix, for more details see: #64074 #59679 #64088
- *
- * @param event the event to process
- */
- private static void fixExceptionOrder(final @NotNull SentryEvent event) {
- boolean reverseExceptions = false;
-
- final @Nullable List exceptions = event.getExceptions();
- if (exceptions != null && exceptions.size() > 1) {
- final @NotNull SentryException lastException = exceptions.get(exceptions.size() - 1);
- if ("java.lang".equals(lastException.getModule())) {
- final @Nullable SentryStackTrace stacktrace = lastException.getStacktrace();
- if (stacktrace != null) {
- final @Nullable List frames = stacktrace.getFrames();
- if (frames != null) {
- for (final @NotNull SentryStackFrame frame : frames) {
- if ("com.android.internal.os.RuntimeInit$MethodAndArgsCaller"
- .equals(frame.getModule())) {
- reverseExceptions = true;
- break;
- }
- }
- }
- }
- }
- }
-
- if (reverseExceptions) {
- Collections.reverse(exceptions);
- }
- }
-
- private void setCommons(
- final @NotNull SentryBaseEvent event,
- final boolean errorEvent,
- final boolean applyScopeData) {
- mergeUser(event);
- setDevice(event, errorEvent, applyScopeData);
- setSideLoadedInfo(event);
- }
-
- private boolean shouldApplyScopeData(
- final @NotNull SentryBaseEvent event, final @NotNull Hint hint) {
- if (HintUtils.shouldApplyScopeData(hint)) {
- return true;
- } else {
- options
- .getLogger()
- .log(
- SentryLevel.DEBUG,
- "Event was cached so not applying data relevant to the current app execution/version: %s",
- event.getEventId());
- return false;
- }
- }
-
- private void mergeUser(final @NotNull SentryBaseEvent event) {
- @Nullable User user = event.getUser();
- if (user == null) {
- user = new User();
- event.setUser(user);
- }
-
- // userId should be set even if event is Cached as the userId is static and won't change anyway.
- if (user.getId() == null) {
- user.setId(options.getRuntimeManager().runWithRelaxedPolicy(() -> Installation.id(context)));
- }
- if (user.getIpAddress() == null && options.isSendDefaultPii()) {
- user.setIpAddress(IpAddressUtils.DEFAULT_IP_ADDRESS);
- }
- }
-
- private void setDevice(
- final @NotNull SentryBaseEvent event,
- final boolean errorEvent,
- final boolean applyScopeData) {
- if (event.getContexts().getDevice() == null) {
- if (deviceInfoUtil != null) {
- try {
- event
- .getContexts()
- .setDevice(deviceInfoUtil.get().collectDeviceInformation(errorEvent, applyScopeData));
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info", e);
- }
- } else {
- options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info");
- }
- mergeOS(event);
- }
- }
-
- private void mergeOS(final @NotNull SentryBaseEvent event) {
- final OperatingSystem currentOS = event.getContexts().getOperatingSystem();
-
- if (deviceInfoUtil != null) {
- try {
- final OperatingSystem androidOS = deviceInfoUtil.get().getOperatingSystem();
- // make Android OS the main OS using the 'os' key
- event.getContexts().setOperatingSystem(androidOS);
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve os system", e);
- }
- } else {
- options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info");
- }
-
- if (currentOS != null) {
- // add additional OS which was already part of the SentryEvent (eg Linux read from NDK)
- String osNameKey = currentOS.getName();
- if (osNameKey != null && !osNameKey.isEmpty()) {
- osNameKey = "os_" + osNameKey.trim().toLowerCase(Locale.ROOT);
- } else {
- osNameKey = "os_1";
- }
- event.getContexts().put(osNameKey, currentOS);
- }
- }
-
- private void setDevice(final @NotNull SentryLogEvent event) {
- try {
- event.setAttribute(
- "device.brand",
- new SentryLogEventAttributeValue(SentryAttributeType.STRING, Build.BRAND));
- event.setAttribute(
- "device.model",
- new SentryLogEventAttributeValue(SentryAttributeType.STRING, Build.MODEL));
- event.setAttribute(
- "device.family",
- new SentryLogEventAttributeValue(SentryAttributeType.STRING, deviceFamily.getValue()));
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info", e);
- }
- }
-
- private void setOs(final @NotNull SentryLogEvent event) {
- try {
- event.setAttribute(
- "os.name", new SentryLogEventAttributeValue(SentryAttributeType.STRING, "Android"));
- event.setAttribute(
- "os.version",
- new SentryLogEventAttributeValue(SentryAttributeType.STRING, Build.VERSION.RELEASE));
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve os system", e);
- }
- }
-
- private void setDevice(final @NotNull SentryMetricsEvent event) {
- try {
- event.setAttribute(
- "device.brand",
- new SentryLogEventAttributeValue(SentryAttributeType.STRING, Build.BRAND));
- event.setAttribute(
- "device.model",
- new SentryLogEventAttributeValue(SentryAttributeType.STRING, Build.MODEL));
- event.setAttribute(
- "device.family",
- new SentryLogEventAttributeValue(SentryAttributeType.STRING, deviceFamily.getValue()));
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info", e);
- }
- }
-
- private void setOs(final @NotNull SentryMetricsEvent event) {
- try {
- event.setAttribute(
- "os.name", new SentryLogEventAttributeValue(SentryAttributeType.STRING, "Android"));
- event.setAttribute(
- "os.version",
- new SentryLogEventAttributeValue(SentryAttributeType.STRING, Build.VERSION.RELEASE));
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve os system", e);
- }
- }
-
- // Data to be applied to events that was created in the running process
- private void processNonCachedEvent(
- final @NotNull SentryBaseEvent event, final @NotNull Hint hint) {
- App app = event.getContexts().getApp();
- if (app == null) {
- app = new App();
- }
- setAppExtras(app, hint);
- setPackageInfo(event, app);
- event.getContexts().setApp(app);
- }
-
- private void setThreads(final @NotNull SentryEvent event, final @NotNull Hint hint) {
- if (event.getThreads() != null) {
- final boolean isHybridSDK = HintUtils.isFromHybridSdk(hint);
-
- for (final SentryThread thread : event.getThreads()) {
- final boolean isMainThread = AndroidThreadChecker.getInstance().isMainThread(thread);
-
- // TODO: Fix https://github.com/getsentry/team-mobile/issues/47
- if (thread.isCurrent() == null) {
- thread.setCurrent(isMainThread);
- }
-
- // This should not be set by Hybrid SDKs since they have their own threading model
- if (!isHybridSDK && thread.isMain() == null) {
- thread.setMain(isMainThread);
- }
- }
- }
- }
-
- private void setPackageInfo(final @NotNull SentryBaseEvent event, final @NotNull App app) {
- final PackageInfo packageInfo =
- ContextUtils.getPackageInfo(
- context, PackageManager.GET_PERMISSIONS, options.getLogger(), buildInfoProvider);
- if (packageInfo != null) {
- String versionCode = ContextUtils.getVersionCode(packageInfo, buildInfoProvider);
-
- setDist(event, versionCode);
-
- @Nullable DeviceInfoUtil deviceInfoUtil = null;
- if (this.deviceInfoUtil != null) {
- try {
- deviceInfoUtil = this.deviceInfoUtil.get();
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info", e);
- }
- } else {
- options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info");
- }
-
- ContextUtils.setAppPackageInfo(packageInfo, buildInfoProvider, deviceInfoUtil, app);
- }
- }
-
- private void setDist(final @NotNull SentryBaseEvent event, final @NotNull String versionCode) {
- if (event.getDist() == null) {
- event.setDist(versionCode);
- }
- }
-
- private void setAppExtras(final @NotNull App app, final @NotNull Hint hint) {
- app.setAppName(ContextUtils.getApplicationName(context));
- final @NotNull TimeSpan appStartTimeSpan =
- AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options);
- if (appStartTimeSpan.hasStarted()) {
- app.setAppStartTime(DateUtils.toUtilDate(appStartTimeSpan.getStartTimestamp()));
- }
-
- // This should not be set by Hybrid SDKs since they have their own app's lifecycle
- if (!HintUtils.isFromHybridSdk(hint) && app.getInForeground() == null) {
- // This feature depends on the AppLifecycleIntegration being installed, so only if
- // enableAutoSessionTracking or enableAppLifecycleBreadcrumbs are enabled.
- final @Nullable Boolean isBackground = AppState.getInstance().isInBackground();
- if (isBackground != null) {
- app.setInForeground(!isBackground);
- }
- }
- }
-
- /**
- * Sets the default user which contains only the userId.
- *
- * @return the User object
- */
- public @NotNull User getDefaultUser(final @NotNull Context context) {
- final @NotNull User user = new User();
- user.setId(options.getRuntimeManager().runWithRelaxedPolicy(() -> Installation.id(context)));
- return user;
- }
-
- private void setSideLoadedInfo(final @NotNull SentryBaseEvent event) {
- if (deviceInfoUtil != null) {
- try {
- final ContextUtils.SideLoadedInfo sideLoadedInfo = deviceInfoUtil.get().getSideLoadedInfo();
- if (sideLoadedInfo != null) {
- final @NotNull Map tags = sideLoadedInfo.asTags();
- for (Map.Entry entry : tags.entrySet()) {
- event.setTag(entry.getKey(), entry.getValue());
- }
- }
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Error getting side loaded info.", e);
- }
- } else {
- options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info");
- }
- }
-
- @Override
- public @NotNull SentryTransaction process(
- final @NotNull SentryTransaction transaction, final @NotNull Hint hint) {
- final boolean applyScopeData = shouldApplyScopeData(transaction, hint);
-
- if (applyScopeData) {
- processNonCachedEvent(transaction, hint);
- }
-
- setCommons(transaction, false, applyScopeData);
-
- return transaction;
- }
-
- @Override
- public @NotNull SentryReplayEvent process(
- final @NotNull SentryReplayEvent event, final @NotNull Hint hint) {
- final boolean applyScopeData = shouldApplyScopeData(event, hint);
- if (applyScopeData) {
- processNonCachedEvent(event, hint);
- }
-
- setCommons(event, false, applyScopeData);
-
- return event;
- }
-
- @Override
- public @Nullable Long getOrder() {
- return 8000L;
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java b/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java
deleted file mode 100644
index 5c06a558103..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java
+++ /dev/null
@@ -1,502 +0,0 @@
-package io.sentry.android.core;
-
-import static android.os.BatteryManager.EXTRA_TEMPERATURE;
-
-import android.annotation.SuppressLint;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.BatteryManager;
-import android.os.Build;
-import android.os.Environment;
-import android.os.LocaleList;
-import android.os.StatFs;
-import android.os.SystemClock;
-import android.util.DisplayMetrics;
-import io.sentry.DateUtils;
-import io.sentry.ISentryLifecycleToken;
-import io.sentry.SentryLevel;
-import io.sentry.SentryOptions;
-import io.sentry.android.core.internal.util.CpuInfoUtils;
-import io.sentry.android.core.internal.util.DeviceOrientations;
-import io.sentry.android.core.internal.util.RootChecker;
-import io.sentry.protocol.Device;
-import io.sentry.protocol.OperatingSystem;
-import io.sentry.util.AutoClosableReentrantLock;
-import java.io.File;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
-import java.util.TimeZone;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.jetbrains.annotations.TestOnly;
-
-@ApiStatus.Internal
-public final class DeviceInfoUtil {
-
- @SuppressLint("StaticFieldLeak")
- private static volatile DeviceInfoUtil instance;
-
- private static final @NotNull AutoClosableReentrantLock staticLock =
- new AutoClosableReentrantLock();
-
- private final @NotNull Context context;
- private final @NotNull SentryAndroidOptions options;
- private final @NotNull BuildInfoProvider buildInfoProvider;
- private final @Nullable Boolean isEmulator;
- private final @Nullable ContextUtils.SideLoadedInfo sideLoadedInfo;
- private final @Nullable ContextUtils.SplitApksInfo splitApksInfo;
- private final @NotNull OperatingSystem os;
-
- private final @Nullable Long totalMem;
-
- public DeviceInfoUtil(
- final @NotNull Context context, final @NotNull SentryAndroidOptions options) {
- this.context = context;
- this.options = options;
- this.buildInfoProvider = new BuildInfoProvider(options.getLogger());
-
- // these are potentially expense IO operations
- CpuInfoUtils.getInstance().readMaxFrequencies();
- os = retrieveOperatingSystemInformation();
- isEmulator = buildInfoProvider.isEmulator();
- sideLoadedInfo =
- ContextUtils.retrieveSideLoadedInfo(context, options.getLogger(), buildInfoProvider);
- splitApksInfo = ContextUtils.retrieveSplitApksInfo(context, buildInfoProvider);
- final @Nullable ActivityManager.MemoryInfo memInfo =
- ContextUtils.getMemInfo(context, options.getLogger());
- if (memInfo != null) {
- totalMem = memInfo.totalMem;
- } else {
- totalMem = null;
- }
- }
-
- @NotNull
- public static DeviceInfoUtil getInstance(
- final @NotNull Context context, final @NotNull SentryAndroidOptions options) {
- if (instance == null) {
- try (final @NotNull ISentryLifecycleToken ignored = staticLock.acquire()) {
- if (instance == null) {
- instance = new DeviceInfoUtil(ContextUtils.getApplicationContext(context), options);
- }
- }
- }
- return instance;
- }
-
- @TestOnly
- public static void resetInstance() {
- instance = null;
- }
-
- // we can get some inspiration here
- // https://github.com/flutter/plugins/blob/master/packages/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java
- @SuppressLint("NewApi")
- @NotNull
- public Device collectDeviceInformation(
- final boolean collectDeviceIO, final boolean collectDynamicData) {
- // TODO: missing usable memory
- final @NotNull Device device = new Device();
- device.setManufacturer(Build.MANUFACTURER);
- device.setBrand(Build.BRAND);
- device.setFamily(ContextUtils.getFamily(options.getLogger()));
- device.setModel(Build.MODEL);
- device.setModelId(Build.ID);
- device.setArchs(ContextUtils.getArchitectures());
- if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.S) {
- device.setChipset(Build.SOC_MANUFACTURER + " " + Build.SOC_MODEL);
- }
-
- device.setOrientation(getOrientation());
- if (isEmulator != null) {
- device.setSimulator(isEmulator);
- }
-
- final @Nullable DisplayMetrics displayMetrics =
- ContextUtils.getDisplayMetrics(context, options.getLogger());
- if (displayMetrics != null) {
- device.setScreenWidthPixels(displayMetrics.widthPixels);
- device.setScreenHeightPixels(displayMetrics.heightPixels);
- device.setScreenDensity(displayMetrics.density);
- device.setScreenDpi(displayMetrics.densityDpi);
- }
-
- device.setBootTime(getBootTime());
- device.setTimezone(getTimeZone());
-
- if (device.getId() == null) {
- device.setId(getDeviceId());
- }
-
- final @NotNull Locale locale = Locale.getDefault();
- if (device.getLocale() == null) {
- device.setLocale(locale.toString()); // eg en_US
- }
-
- final @NotNull List cpuFrequencies = CpuInfoUtils.getInstance().readMaxFrequencies();
- if (!cpuFrequencies.isEmpty()) {
- device.setProcessorFrequency(Collections.max(cpuFrequencies).doubleValue());
- device.setProcessorCount(cpuFrequencies.size());
- }
-
- device.setMemorySize(totalMem);
-
- // setting such values require IO hence we don't run for transactions
- if (collectDeviceIO && options.isCollectAdditionalContext()) {
- setDeviceIO(device, collectDynamicData, options.isCollectExternalStorageContext());
- }
-
- return device;
- }
-
- @NotNull
- public OperatingSystem getOperatingSystem() {
- return os;
- }
-
- @Nullable
- public Long getTotalMemory() {
- return totalMem;
- }
-
- @NotNull
- private OperatingSystem retrieveOperatingSystemInformation() {
-
- final OperatingSystem os = new OperatingSystem();
- os.setName("Android");
- os.setVersion(Build.VERSION.RELEASE);
- os.setBuild(Build.DISPLAY);
-
- final @Nullable String kernelVersion = ContextUtils.getKernelVersion(options.getLogger());
- if (kernelVersion != null) {
- os.setKernelVersion(kernelVersion);
- }
-
- if (options.isEnableRootCheck()) {
- final boolean rooted =
- new RootChecker(context, buildInfoProvider, options.getLogger()).isDeviceRooted();
- os.setRooted(rooted);
- }
- return os;
- }
-
- @Nullable
- public ContextUtils.SideLoadedInfo getSideLoadedInfo() {
- return sideLoadedInfo;
- }
-
- @Nullable
- public ContextUtils.SplitApksInfo getSplitApksInfo() {
- return splitApksInfo;
- }
-
- private void setDeviceIO(
- final @NotNull Device device,
- final boolean includeDynamicData,
- final boolean includeExternalStorage) {
- final Intent batteryIntent = getBatteryIntent();
- if (batteryIntent != null) {
- device.setBatteryLevel(getBatteryLevel(batteryIntent, options));
- device.setCharging(isCharging(batteryIntent, options));
- device.setBatteryTemperature(getBatteryTemperature(batteryIntent));
- }
-
- // TODO .getConnectionStatus() may be blocking, investigate if this can be done async
- Boolean connected;
- switch (options.getConnectionStatusProvider().getConnectionStatus()) {
- case DISCONNECTED:
- connected = false;
- break;
- case CONNECTED:
- connected = true;
- break;
- default:
- connected = null;
- }
- device.setOnline(connected);
-
- final @Nullable ActivityManager.MemoryInfo memInfo =
- ContextUtils.getMemInfo(context, options.getLogger());
- if (memInfo != null && includeDynamicData) {
- // in bytes
- device.setFreeMemory(memInfo.availMem);
- device.setLowMemory(memInfo.lowMemory);
- }
-
- // this way of getting the size of storage might be problematic for storages bigger than 2GB
- // check the use of
- // https://developer.android.com/reference/java/io/File.html#getFreeSpace%28%29
- options
- .getRuntimeManager()
- .runWithRelaxedPolicy(
- () -> {
- final @Nullable File dataDir = Environment.getDataDirectory();
- if (dataDir != null) {
- StatFs internalStorageStat = new StatFs(dataDir.getPath());
- device.setStorageSize(getTotalInternalStorage(internalStorageStat));
- device.setFreeStorage(getUnusedInternalStorage(internalStorageStat));
- }
-
- if (includeExternalStorage) {
- final @Nullable File internalStorageFile = context.getExternalFilesDir(null);
- final @Nullable StatFs externalStorageStat =
- getExternalStorageStat(internalStorageFile);
- if (externalStorageStat != null) {
- device.setExternalStorageSize(getTotalExternalStorage(externalStorageStat));
- device.setExternalFreeStorage(getUnusedExternalStorage(externalStorageStat));
- }
- }
- });
-
- if (device.getConnectionType() == null) {
- // wifi, ethernet or cellular, null if none
- device.setConnectionType(options.getConnectionStatusProvider().getConnectionType());
- }
- }
-
- @SuppressWarnings("NewApi")
- @NotNull
- private TimeZone getTimeZone() {
- if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.N) {
- LocaleList locales = context.getResources().getConfiguration().getLocales();
- if (!locales.isEmpty()) {
- Locale locale = locales.get(0);
- return Calendar.getInstance(locale).getTimeZone();
- }
- }
- return Calendar.getInstance().getTimeZone();
- }
-
- @SuppressWarnings("JdkObsolete")
- @Nullable
- private Date getBootTime() {
- try {
- // if user changes the clock, will give a wrong answer, consider ACTION_TIME_CHANGED.
- // currentTimeMillis returns UTC already
- return DateUtils.getDateTime(System.currentTimeMillis() - SystemClock.elapsedRealtime());
- } catch (IllegalArgumentException e) {
- options.getLogger().log(SentryLevel.ERROR, e, "Error getting the device's boot time.");
- }
- return null;
- }
-
- @Nullable
- private Intent getBatteryIntent() {
- return ContextUtils.registerReceiver(
- context, buildInfoProvider, null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED), null);
- }
-
- /**
- * Get the device's current battery level (as a percentage of total).
- *
- * @return the device's current battery level (as a percentage of total), or null if unknown
- */
- @Nullable
- public static Float getBatteryLevel(
- final @NotNull Intent batteryIntent, final @NotNull SentryOptions options) {
- try {
- int level = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
- int scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
-
- if (level == -1 || scale == -1) {
- return null;
- }
-
- float percentMultiplier = 100.0f;
-
- return ((float) level / (float) scale) * percentMultiplier;
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Error getting device battery level.", e);
- return null;
- }
- }
-
- /**
- * Checks whether or not the device is currently plugged in and charging, or null if unknown.
- *
- * @return whether or not the device is currently plugged in and charging, or null if unknown
- */
- @Nullable
- public static Boolean isCharging(
- final @NotNull Intent batteryIntent, final @NotNull SentryOptions options) {
- try {
- int plugged = batteryIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
- return plugged == BatteryManager.BATTERY_PLUGGED_AC
- || plugged == BatteryManager.BATTERY_PLUGGED_USB;
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Error getting device charging state.", e);
- return null;
- }
- }
-
- @Nullable
- private Float getBatteryTemperature(final @NotNull Intent batteryIntent) {
- try {
- int temperature = batteryIntent.getIntExtra(EXTRA_TEMPERATURE, -1);
- if (temperature != -1) {
- return ((float) temperature) / 10; // celsius
- }
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Error getting battery temperature.", e);
- }
- return null;
- }
-
- /**
- * Get the device's current screen orientation.
- *
- * @return the device's current screen orientation, or null if unknown
- */
- @Nullable
- private Device.DeviceOrientation getOrientation() {
- Device.DeviceOrientation deviceOrientation = null;
- try {
- deviceOrientation =
- DeviceOrientations.getOrientation(context.getResources().getConfiguration().orientation);
- if (deviceOrientation == null) {
- options
- .getLogger()
- .log(
- SentryLevel.INFO,
- "No device orientation available (ORIENTATION_SQUARE|ORIENTATION_UNDEFINED)");
- return null;
- }
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Error getting device orientation.", e);
- }
- return deviceOrientation;
- }
-
- /**
- * Get the total amount of internal storage, in bytes.
- *
- * @return the total amount of internal storage, in bytes
- */
- @Nullable
- private Long getTotalInternalStorage(final @NotNull StatFs stat) {
- try {
- long blockSize = stat.getBlockSizeLong();
- long totalBlocks = stat.getBlockCountLong();
- return totalBlocks * blockSize;
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Error getting total internal storage amount.", e);
- return null;
- }
- }
-
- /**
- * Get the unused amount of internal storage, in bytes.
- *
- * @return the unused amount of internal storage, in bytes
- */
- @Nullable
- private Long getUnusedInternalStorage(final @NotNull StatFs stat) {
- try {
- long blockSize = stat.getBlockSizeLong();
- long availableBlocks = stat.getAvailableBlocksLong();
- return availableBlocks * blockSize;
- } catch (Throwable e) {
- options
- .getLogger()
- .log(SentryLevel.ERROR, "Error getting unused internal storage amount.", e);
- return null;
- }
- }
-
- @Nullable
- private StatFs getExternalStorageStat(final @Nullable File internalStorage) {
- try {
- File path = getExternalStorageDep(internalStorage);
- if (path != null) { // && path.canRead()) { canRead() will read return false
- return new StatFs(path.getPath());
- }
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.INFO, "Not possible to read external files directory");
- }
- return null;
- }
-
- @Nullable
- private File getExternalStorageDep(final @Nullable File internalStorage) {
- final @Nullable File[] externalFilesDirs = context.getExternalFilesDirs(null);
-
- if (externalFilesDirs != null) {
- // return the 1st file which is not the emulated internal storage
- String internalStoragePath =
- internalStorage != null ? internalStorage.getAbsolutePath() : null;
- for (File file : externalFilesDirs) {
- // externalFilesDirs may contain null values :(
- if (file == null) {
- continue;
- }
-
- // return the 1st file if you cannot compare with the internal one
- if (internalStoragePath == null || internalStoragePath.isEmpty()) {
- return file;
- }
- // if we are looking to the same directory, let's check the next one or no external storage
- if (file.getAbsolutePath().contains(internalStoragePath)) {
- continue;
- }
- return file;
- }
- } else {
- options.getLogger().log(SentryLevel.INFO, "Not possible to read getExternalFilesDirs");
- }
- return null;
- }
-
- /**
- * Get the total amount of external storage, in bytes, or null if no external storage is mounted.
- *
- * @return the total amount of external storage, in bytes, or null if no external storage is
- * mounted
- */
- @Nullable
- private Long getTotalExternalStorage(final @NotNull StatFs stat) {
- try {
- final long blockSize = stat.getBlockSizeLong();
- final long totalBlocks = stat.getBlockCountLong();
- return totalBlocks * blockSize;
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Error getting total external storage amount.", e);
- return null;
- }
- }
-
- /**
- * Get the unused amount of external storage, in bytes, or null if no external storage is mounted.
- *
- * @return the unused amount of external storage, in bytes, or null if no external storage is
- * mounted
- */
- @Nullable
- private Long getUnusedExternalStorage(final @NotNull StatFs stat) {
- try {
- final long blockSize = stat.getBlockSizeLong();
- final long availableBlocks = stat.getAvailableBlocksLong();
- return availableBlocks * blockSize;
- } catch (Throwable e) {
- options
- .getLogger()
- .log(SentryLevel.ERROR, "Error getting unused external storage amount.", e);
- return null;
- }
- }
-
- @Nullable
- private String getDeviceId() {
- try {
- return options.getRuntimeManager().runWithRelaxedPolicy(() -> Installation.id(context));
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Error getting installationId.", e);
- }
- return null;
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/EmptySecureContentProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/EmptySecureContentProvider.java
deleted file mode 100644
index ff8f2b4e980..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/EmptySecureContentProvider.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package io.sentry.android.core;
-
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.net.Uri;
-import io.sentry.android.core.internal.util.ContentProviderSecurityChecker;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * A ContentProvider that does NOT store or provide any data for read or write operations.
- *
- * This does not allow for overriding the abstract query, insert, update, and delete operations
- * of the {@link ContentProvider}. Additionally, those functions are secure.
- */
-@ApiStatus.Internal
-abstract class EmptySecureContentProvider extends ContentProvider {
-
- private final ContentProviderSecurityChecker securityChecker =
- new ContentProviderSecurityChecker();
-
- @Override
- public final @Nullable Cursor query(
- @NotNull Uri uri,
- @Nullable String[] strings,
- @Nullable String s,
- @Nullable String[] strings1,
- @Nullable String s1) {
- securityChecker.checkPrivilegeEscalation(this);
- return null;
- }
-
- @Override
- public final @Nullable Uri insert(@NotNull Uri uri, @Nullable ContentValues contentValues) {
- securityChecker.checkPrivilegeEscalation(this);
- return null;
- }
-
- @Override
- public final int delete(@NotNull Uri uri, @Nullable String s, @Nullable String[] strings) {
- securityChecker.checkPrivilegeEscalation(this);
- return 0;
- }
-
- @Override
- public final int update(
- @NotNull Uri uri,
- @Nullable ContentValues contentValues,
- @Nullable String s,
- @Nullable String[] strings) {
- securityChecker.checkPrivilegeEscalation(this);
- return 0;
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserver.java b/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserver.java
deleted file mode 100644
index 4a3dc7a1786..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserver.java
+++ /dev/null
@@ -1,121 +0,0 @@
-package io.sentry.android.core;
-
-import static io.sentry.SentryLevel.ERROR;
-
-import android.os.FileObserver;
-import io.sentry.Hint;
-import io.sentry.IEnvelopeSender;
-import io.sentry.ILogger;
-import io.sentry.SentryLevel;
-import io.sentry.hints.ApplyScopeData;
-import io.sentry.hints.Cached;
-import io.sentry.hints.Flushable;
-import io.sentry.hints.Resettable;
-import io.sentry.hints.Retryable;
-import io.sentry.hints.SubmissionResult;
-import io.sentry.util.HintUtils;
-import io.sentry.util.Objects;
-import java.io.File;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-final class EnvelopeFileObserver extends FileObserver {
-
- private final String rootPath;
- private final IEnvelopeSender envelopeSender;
- private @NotNull final ILogger logger;
- private final long flushTimeoutMillis;
-
- // The preferred overload (Taking File instead of String) is only available from API 29
- @SuppressWarnings("deprecation")
- EnvelopeFileObserver(
- String path,
- IEnvelopeSender envelopeSender,
- @NotNull ILogger logger,
- final long flushTimeoutMillis) {
- super(path);
- this.rootPath = path;
- this.envelopeSender = Objects.requireNonNull(envelopeSender, "Envelope sender is required.");
- this.logger = Objects.requireNonNull(logger, "Logger is required.");
- this.flushTimeoutMillis = flushTimeoutMillis;
- }
-
- @Override
- public void onEvent(int eventType, @Nullable String relativePath) {
- if (relativePath == null || eventType != FileObserver.CLOSE_WRITE) {
- return;
- }
-
- logger.log(
- SentryLevel.DEBUG,
- "onEvent fired for EnvelopeFileObserver with event type %d on path: %s for file %s.",
- eventType,
- this.rootPath,
- relativePath);
-
- // TODO: Only some event types should be pass through?
-
- final CachedEnvelopeHint cachedHint = new CachedEnvelopeHint(flushTimeoutMillis, logger);
-
- final Hint hint = HintUtils.createWithTypeCheckHint(cachedHint);
-
- envelopeSender.processEnvelopeFile(this.rootPath + File.separator + relativePath, hint);
- }
-
- private static final class CachedEnvelopeHint
- implements Cached, Retryable, SubmissionResult, Flushable, ApplyScopeData, Resettable {
- boolean retry;
- boolean succeeded;
-
- private @NotNull CountDownLatch latch;
- private final long flushTimeoutMillis;
- private final @NotNull ILogger logger;
-
- public CachedEnvelopeHint(final long flushTimeoutMillis, final @NotNull ILogger logger) {
- reset();
- this.flushTimeoutMillis = flushTimeoutMillis;
- this.logger = Objects.requireNonNull(logger, "ILogger is required.");
- }
-
- @Override
- public boolean waitFlush() {
- try {
- return latch.await(flushTimeoutMillis, TimeUnit.MILLISECONDS);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- logger.log(ERROR, "Exception while awaiting on lock.", e);
- }
- return false;
- }
-
- @Override
- public boolean isRetry() {
- return retry;
- }
-
- @Override
- public void setRetry(boolean retry) {
- this.retry = retry;
- }
-
- @Override
- public void setResult(boolean succeeded) {
- this.succeeded = succeeded;
- latch.countDown();
- }
-
- @Override
- public boolean isSuccess() {
- return succeeded;
- }
-
- @Override
- public void reset() {
- latch = new CountDownLatch(1);
- retry = false;
- succeeded = false;
- }
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java
deleted file mode 100644
index 482d90c6e6c..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java
+++ /dev/null
@@ -1,119 +0,0 @@
-package io.sentry.android.core;
-
-import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion;
-
-import io.sentry.ILogger;
-import io.sentry.IScopes;
-import io.sentry.ISentryLifecycleToken;
-import io.sentry.Integration;
-import io.sentry.OutboxSender;
-import io.sentry.SentryLevel;
-import io.sentry.SentryOptions;
-import io.sentry.util.AutoClosableReentrantLock;
-import io.sentry.util.Objects;
-import java.io.Closeable;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.jetbrains.annotations.TestOnly;
-
-/** Watches the envelope dir. and send them (events) over. */
-public abstract class EnvelopeFileObserverIntegration implements Integration, Closeable {
- private @Nullable EnvelopeFileObserver observer;
- private @Nullable ILogger logger;
- private boolean isClosed = false;
- protected final @NotNull AutoClosableReentrantLock startLock = new AutoClosableReentrantLock();
-
- public static @NotNull EnvelopeFileObserverIntegration getOutboxFileObserver() {
- return new OutboxEnvelopeFileObserverIntegration();
- }
-
- @Override
- public final void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) {
- Objects.requireNonNull(scopes, "Scopes are required");
- Objects.requireNonNull(options, "SentryOptions is required");
-
- logger = options.getLogger();
-
- final String path = getPath(options);
- if (path == null) {
- logger.log(
- SentryLevel.WARNING,
- "Null given as a path to EnvelopeFileObserverIntegration. Nothing will be registered.");
- } else {
- logger.log(
- SentryLevel.DEBUG, "Registering EnvelopeFileObserverIntegration for path: %s", path);
-
- try {
- options
- .getExecutorService()
- .submit(
- () -> {
- try (final @NotNull ISentryLifecycleToken ignored = startLock.acquire()) {
- if (!isClosed) {
- startOutboxSender(scopes, options, path);
- }
- }
- });
- } catch (Throwable e) {
- logger.log(
- SentryLevel.DEBUG,
- "Failed to start EnvelopeFileObserverIntegration on executor thread.",
- e);
- }
- }
- }
-
- private void startOutboxSender(
- final @NotNull IScopes scopes,
- final @NotNull SentryOptions options,
- final @NotNull String path) {
- final OutboxSender outboxSender =
- new OutboxSender(
- scopes,
- options.getEnvelopeReader(),
- options.getSerializer(),
- options.getLogger(),
- options.getFlushTimeoutMillis(),
- options.getMaxQueueSize());
-
- observer =
- new EnvelopeFileObserver(
- path, outboxSender, options.getLogger(), options.getFlushTimeoutMillis());
- try {
- observer.startWatching();
- options.getLogger().log(SentryLevel.DEBUG, "EnvelopeFileObserverIntegration installed.");
- addIntegrationToSdkVersion("EnvelopeFileObserver");
- } catch (Throwable e) {
- // it could throw eg NoSuchFileException or NullPointerException
- options
- .getLogger()
- .log(SentryLevel.ERROR, "Failed to initialize EnvelopeFileObserverIntegration.", e);
- }
- }
-
- @Override
- public void close() {
- try (final @NotNull ISentryLifecycleToken ignored = startLock.acquire()) {
- isClosed = true;
- }
- if (observer != null) {
- observer.stopWatching();
-
- if (logger != null) {
- logger.log(SentryLevel.DEBUG, "EnvelopeFileObserverIntegration removed.");
- }
- }
- }
-
- @TestOnly
- abstract @Nullable String getPath(final @NotNull SentryOptions options);
-
- private static final class OutboxEnvelopeFileObserverIntegration
- extends EnvelopeFileObserverIntegration {
-
- @Override
- protected @Nullable String getPath(final @NotNull SentryOptions options) {
- return options.getOutboxPath();
- }
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/IDebugImagesLoader.java b/sentry-android-core/src/main/java/io/sentry/android/core/IDebugImagesLoader.java
deleted file mode 100644
index 7b98147aab8..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/IDebugImagesLoader.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package io.sentry.android.core;
-
-import io.sentry.protocol.DebugImage;
-import java.util.List;
-import java.util.Set;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.Nullable;
-
-/** Used for loading the list of debug images from sentry-native. */
-@ApiStatus.Internal
-public interface IDebugImagesLoader {
- @Nullable
- List loadDebugImages();
-
- @Nullable
- Set loadDebugImagesForAddresses(Set addresses);
-
- void clearDebugImages();
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/Installation.java b/sentry-android-core/src/main/java/io/sentry/android/core/Installation.java
deleted file mode 100644
index ba08e71342a..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/Installation.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package io.sentry.android.core;
-
-import android.content.Context;
-import io.sentry.ISentryLifecycleToken;
-import io.sentry.SentryUUID;
-import io.sentry.util.AutoClosableReentrantLock;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.RandomAccessFile;
-import java.nio.charset.Charset;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.jetbrains.annotations.TestOnly;
-
-final class Installation {
- @TestOnly static @Nullable String deviceId = null;
-
- @TestOnly static final String INSTALLATION = "INSTALLATION";
-
- private static final Charset UTF_8 = Charset.forName("UTF-8");
-
- protected static final @NotNull AutoClosableReentrantLock staticLock =
- new AutoClosableReentrantLock();
-
- private Installation() {}
-
- /**
- * Generates a random UUID and writes to a file to be used as an unique installationId. Reads the
- * installationId if already exists.
- *
- * @param context the Context
- * @return the generated installationId
- * @throws RuntimeException if not possible to read nor to write to the file.
- */
- public static String id(final @NotNull Context context) throws RuntimeException {
- try (final @NotNull ISentryLifecycleToken ignored = staticLock.acquire()) {
- if (deviceId == null) {
- final File installation = new File(context.getFilesDir(), INSTALLATION);
- try {
- if (!installation.exists()) {
- deviceId = writeInstallationFile(installation);
- return deviceId;
- }
- deviceId = readInstallationFile(installation);
- } catch (Throwable e) {
- throw new RuntimeException(e);
- }
- }
- return deviceId;
- }
- }
-
- @TestOnly
- static @NotNull String readInstallationFile(final @NotNull File installation) throws IOException {
- try (final RandomAccessFile f = new RandomAccessFile(installation, "r")) {
- final byte[] bytes = new byte[(int) f.length()];
- f.readFully(bytes);
- return new String(bytes, UTF_8);
- }
- }
-
- @TestOnly
- static @NotNull String writeInstallationFile(final @NotNull File installation)
- throws IOException {
- try (final OutputStream out = new FileOutputStream(installation)) {
- final String id = SentryUUID.generateSentryId();
- out.write(id.getBytes(UTF_8));
- out.flush();
- return id;
- }
- }
-}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java
deleted file mode 100644
index 7d0a7f77e58..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java
+++ /dev/null
@@ -1,354 +0,0 @@
-package io.sentry.android.core;
-
-import static io.sentry.Sentry.getCurrentScopes;
-import static io.sentry.SentryLevel.DEBUG;
-import static io.sentry.SentryLevel.INFO;
-import static io.sentry.SentryLevel.WARNING;
-
-import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import io.sentry.DateUtils;
-import io.sentry.ILogger;
-import io.sentry.IScope;
-import io.sentry.IScopes;
-import io.sentry.ISerializer;
-import io.sentry.ObjectWriter;
-import io.sentry.PropagationContext;
-import io.sentry.ScopeType;
-import io.sentry.ScopesAdapter;
-import io.sentry.SentryEnvelope;
-import io.sentry.SentryEnvelopeItem;
-import io.sentry.SentryEvent;
-import io.sentry.SentryLevel;
-import io.sentry.SentryOptions;
-import io.sentry.Session;
-import io.sentry.android.core.performance.ActivityLifecycleTimeSpan;
-import io.sentry.android.core.performance.AppStartMetrics;
-import io.sentry.android.core.performance.TimeSpan;
-import io.sentry.cache.EnvelopeCache;
-import io.sentry.protocol.App;
-import io.sentry.protocol.Device;
-import io.sentry.protocol.SentryId;
-import io.sentry.protocol.User;
-import io.sentry.util.MapObjectWriter;
-import io.sentry.util.TracingUtils;
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicReference;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/** Sentry SDK internal API methods meant for being used by the Sentry Hybrid SDKs. */
-@ApiStatus.Internal
-public final class InternalSentrySdk {
-
- /**
- * @return a copy of the current scopes's topmost scope, or null in case the scopes is disabled
- */
- @Nullable
- public static IScope getCurrentScope() {
- final @NotNull AtomicReference scopeRef = new AtomicReference<>();
- ScopesAdapter.getInstance()
- .configureScope(
- ScopeType.COMBINED,
- scope -> {
- scopeRef.set(scope.clone());
- });
- return scopeRef.get();
- }
-
- /**
- * Serializes the provided scope. Specific data may be back-filled (e.g. device context) if the
- * scope itself does not provide it.
- *
- * @param context Android context
- * @param options Sentry Options
- * @param scope the scope
- * @return a map containing all relevant scope data (user, contexts, tags, extras, fingerprint,
- * level, breadcrumbs)
- */
- @NotNull
- public static Map serializeScope(
- final @NotNull Context context,
- final @NotNull SentryAndroidOptions options,
- final @Nullable IScope scope) {
-
- final @NotNull Map data = new HashMap<>();
- if (scope == null) {
- return data;
- }
- try {
- final @NotNull ILogger logger = options.getLogger();
- final @NotNull ObjectWriter writer = new MapObjectWriter(data);
-
- final @NotNull DeviceInfoUtil deviceInfoUtil = DeviceInfoUtil.getInstance(context, options);
- final @NotNull Device deviceInfo = deviceInfoUtil.collectDeviceInformation(true, true);
- scope.getContexts().setDevice(deviceInfo);
- scope.getContexts().setOperatingSystem(deviceInfoUtil.getOperatingSystem());
-
- // user
- @Nullable User user = scope.getUser();
- if (user == null) {
- user = new User();
- scope.setUser(user);
- }
- if (user.getId() == null) {
- try {
- user.setId(
- options.getRuntimeManager().runWithRelaxedPolicy(() -> Installation.id(context)));
- } catch (RuntimeException e) {
- logger.log(SentryLevel.ERROR, "Could not retrieve installation ID", e);
- }
- }
-
- // app context
- @Nullable App app = scope.getContexts().getApp();
- if (app == null) {
- app = new App();
- }
- app.setAppName(ContextUtils.getApplicationName(context));
-
- final @NotNull TimeSpan appStartTimeSpan =
- AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options);
- if (appStartTimeSpan.hasStarted()) {
- app.setAppStartTime(DateUtils.toUtilDate(appStartTimeSpan.getStartTimestamp()));
- }
-
- final @NotNull BuildInfoProvider buildInfoProvider =
- new BuildInfoProvider(options.getLogger());
- final @Nullable PackageInfo packageInfo =
- ContextUtils.getPackageInfo(
- context, PackageManager.GET_PERMISSIONS, options.getLogger(), buildInfoProvider);
- if (packageInfo != null) {
- ContextUtils.setAppPackageInfo(packageInfo, buildInfoProvider, deviceInfoUtil, app);
- }
- scope.getContexts().setApp(app);
-
- writer.name("user").value(logger, scope.getUser());
- writer.name("contexts").value(logger, scope.getContexts());
- writer.name("tags").value(logger, scope.getTags());
- writer.name("extras").value(logger, scope.getExtras());
- writer.name("fingerprint").value(logger, scope.getFingerprint());
- writer.name("level").value(logger, scope.getLevel());
- writer.name("breadcrumbs").value(logger, scope.getBreadcrumbs());
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Could not serialize scope.", e);
- return new HashMap<>();
- }
-
- return data;
- }
-
- /**
- * Captures the provided envelope. Compared to {@link IScopes#captureEvent(SentryEvent)} this
- * method
- * - will not enrich events with additional data (e.g. scope)
- * - will not execute beforeSend: it's up to the caller to take care of this
- * - will not perform any sampling: it's up to the caller to take care of this
- * - will enrich the envelope with a Session update if applicable
- *
- * @param envelopeData the serialized envelope data
- * @return The Id (SentryId object) of the event, or null in case the envelope could not be
- * captured
- */
- @Nullable
- public static SentryId captureEnvelope(
- final @NotNull byte[] envelopeData, final boolean maybeStartNewSession) {
- final @NotNull IScopes scopes = ScopesAdapter.getInstance();
- final @NotNull SentryOptions options = scopes.getOptions();
-
- try (final InputStream envelopeInputStream = new ByteArrayInputStream(envelopeData)) {
- final @NotNull ISerializer serializer = options.getSerializer();
- final @Nullable SentryEnvelope envelope =
- options.getEnvelopeReader().read(envelopeInputStream);
- if (envelope == null) {
- return null;
- }
-
- final @NotNull List envelopeItems = new ArrayList<>();
-
- // determine session state based on events inside envelope
- @Nullable Session.State status = null;
- boolean crashedOrErrored = false;
- for (SentryEnvelopeItem item : envelope.getItems()) {
- envelopeItems.add(item);
-
- final SentryEvent event = item.getEvent(serializer);
- if (event != null) {
- if (event.isCrashed()) {
- status = Session.State.Crashed;
- }
- if (event.isCrashed() || event.isErrored()) {
- crashedOrErrored = true;
- }
- }
- }
-
- // update session and add it to envelope if necessary
- final @Nullable Session session = updateSession(scopes, options, status, crashedOrErrored);
- if (session != null) {
- final SentryEnvelopeItem sessionItem = SentryEnvelopeItem.fromSession(serializer, session);
- envelopeItems.add(sessionItem);
- deleteCurrentSessionFile(
- options,
- // should be sync if going to crash or already not a main thread
- !maybeStartNewSession || !scopes.getOptions().getThreadChecker().isMainThread());
- if (maybeStartNewSession) {
- scopes.startSession();
- }
- }
-
- final SentryEnvelope repackagedEnvelope =
- new SentryEnvelope(envelope.getHeader(), envelopeItems);
- return scopes.captureEnvelope(repackagedEnvelope);
- } catch (Throwable t) {
- options.getLogger().log(SentryLevel.ERROR, "Failed to capture envelope", t);
- }
- return null;
- }
-
- public static Map getAppStartMeasurement() {
- final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance();
- final @NotNull List