diff --git a/app/src/main/java/com/bugsnag/android/AppDataCollector.kt b/app/src/main/java/com/bugsnag/android/AppDataCollector.kt index 07fe06cbd9..696935490b 100644 --- a/app/src/main/java/com/bugsnag/android/AppDataCollector.kt +++ b/app/src/main/java/com/bugsnag/android/AppDataCollector.kt @@ -104,7 +104,7 @@ internal class AppDataCollector( return null } - val nowMs = System.currentTimeMillis() + val nowMs = SystemClock.elapsedRealtime() var durationMs: Long = 0 val sessionStartTimeMs: Long = sessionTracker.lastEnteredForegroundMs diff --git a/app/src/main/java/com/bugsnag/android/Breadcrumb.java b/app/src/main/java/com/bugsnag/android/Breadcrumb.java index 5d7ba6a78c..79e2ef1538 100644 --- a/app/src/main/java/com/bugsnag/android/Breadcrumb.java +++ b/app/src/main/java/com/bugsnag/android/Breadcrumb.java @@ -1,5 +1,7 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.DateUtils; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; diff --git a/app/src/main/java/com/bugsnag/android/BreadcrumbState.kt b/app/src/main/java/com/bugsnag/android/BreadcrumbState.kt index 331ca721a5..20192ebea9 100644 --- a/app/src/main/java/com/bugsnag/android/BreadcrumbState.kt +++ b/app/src/main/java/com/bugsnag/android/BreadcrumbState.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.DateUtils import java.io.IOException import java.util.concurrent.atomic.AtomicInteger diff --git a/app/src/main/java/com/bugsnag/android/Client.java b/app/src/main/java/com/bugsnag/android/Client.java index 205ecdfc44..e31df68f9f 100644 --- a/app/src/main/java/com/bugsnag/android/Client.java +++ b/app/src/main/java/com/bugsnag/android/Client.java @@ -79,7 +79,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { final ClientObservable clientObservable; PluginClient pluginClient; - final Notifier notifier = new Notifier(); + final Notifier notifier; @Nullable final LastRunInfo lastRunInfo; @@ -117,6 +117,8 @@ public class Client implements MetadataAware, CallbackAware, UserAware { ContextModule contextModule = new ContextModule(androidContext); appContext = contextModule.getCtx(); + notifier = configuration.getNotifier(); + connectivity = new ConnectivityCompat(appContext, new Function2() { @Override public Unit invoke(Boolean hasConnection, String networkState) { @@ -233,7 +235,8 @@ public class Client implements MetadataAware, CallbackAware, UserAware { DeliveryDelegate deliveryDelegate, LastRunInfoStore lastRunInfoStore, LaunchCrashTracker launchCrashTracker, - ExceptionHandler exceptionHandler + ExceptionHandler exceptionHandler, + Notifier notifier ) { this.immutableConfig = immutableConfig; this.metadataState = metadataState; @@ -255,6 +258,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { this.launchCrashTracker = launchCrashTracker; this.lastRunInfo = null; this.exceptionHandler = exceptionHandler; + this.notifier = notifier; } void registerLifecycleCallbacks() { diff --git a/app/src/main/java/com/bugsnag/android/ClientObservable.kt b/app/src/main/java/com/bugsnag/android/ClientObservable.kt index d96724c100..cd6ab1fe5d 100644 --- a/app/src/main/java/com/bugsnag/android/ClientObservable.kt +++ b/app/src/main/java/com/bugsnag/android/ClientObservable.kt @@ -21,7 +21,8 @@ internal class ClientObservable : BaseObservable() { conf.buildUuid, conf.releaseStage, lastRunInfoPath, - consecutiveLaunchCrashes + consecutiveLaunchCrashes, + conf.sendThreads ) } } diff --git a/app/src/main/java/com/bugsnag/android/ConfigInternal.kt b/app/src/main/java/com/bugsnag/android/ConfigInternal.kt index e02e0ffdff..15e4526236 100644 --- a/app/src/main/java/com/bugsnag/android/ConfigInternal.kt +++ b/app/src/main/java/com/bugsnag/android/ConfigInternal.kt @@ -49,6 +49,8 @@ internal class ConfigInternal(var apiKey: String) : CallbackAware, MetadataAware var projectPackages: Set = emptySet() var persistenceDirectory: File? = null + val notifier: Notifier = Notifier() + protected val plugins = HashSet() override fun addOnError(onError: OnErrorCallback) = callbackState.addOnError(onError) @@ -79,7 +81,7 @@ internal class ConfigInternal(var apiKey: String) : CallbackAware, MetadataAware } companion object { - private const val DEFAULT_MAX_BREADCRUMBS = 25 + private const val DEFAULT_MAX_BREADCRUMBS = 50 private const val DEFAULT_MAX_PERSISTED_SESSIONS = 128 private const val DEFAULT_MAX_PERSISTED_EVENTS = 32 private const val DEFAULT_LAUNCH_CRASH_THRESHOLD_MS: Long = 5000 diff --git a/app/src/main/java/com/bugsnag/android/Configuration.java b/app/src/main/java/com/bugsnag/android/Configuration.java index 033a4d96e1..13c2daef93 100644 --- a/app/src/main/java/com/bugsnag/android/Configuration.java +++ b/app/src/main/java/com/bugsnag/android/Configuration.java @@ -513,7 +513,7 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware { * Sets the maximum number of breadcrumbs which will be stored. Once the threshold is reached, * the oldest breadcrumbs will be deleted. * - * By default, 25 breadcrumbs are stored: this can be amended up to a maximum of 100. + * By default, 50 breadcrumbs are stored: this can be amended up to a maximum of 100. */ public int getMaxBreadcrumbs() { return impl.getMaxBreadcrumbs(); @@ -523,7 +523,7 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware { * Sets the maximum number of breadcrumbs which will be stored. Once the threshold is reached, * the oldest breadcrumbs will be deleted. * - * By default, 25 breadcrumbs are stored: this can be amended up to a maximum of 100. + * By default, 50 breadcrumbs are stored: this can be amended up to a maximum of 100. */ public void setMaxBreadcrumbs(int maxBreadcrumbs) { if (maxBreadcrumbs >= MIN_BREADCRUMBS && maxBreadcrumbs <= MAX_BREADCRUMBS) { @@ -981,4 +981,8 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware { Set getPlugins() { return impl.getPlugins(); } + + Notifier getNotifier() { + return impl.getNotifier(); + } } diff --git a/app/src/main/java/com/bugsnag/android/DateUtils.java b/app/src/main/java/com/bugsnag/android/DateUtils.java deleted file mode 100644 index 406e16d19b..0000000000 --- a/app/src/main/java/com/bugsnag/android/DateUtils.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.bugsnag.android; - -import androidx.annotation.NonNull; - -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; - -class DateUtils { - // SimpleDateFormat isn't thread safe, cache one instance per thread as needed. - private static final ThreadLocal iso8601Holder = new ThreadLocal() { - @NonNull - @Override - protected DateFormat initialValue() { - TimeZone tz = TimeZone.getTimeZone("UTC"); - DateFormat iso8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); - iso8601.setTimeZone(tz); - return iso8601; - } - }; - - @NonNull - static String toIso8601(@NonNull Date date) { - DateFormat dateFormat = iso8601Holder.get(); - if (dateFormat == null) { - throw new IllegalStateException("Unable to find valid dateformatter"); - } - return dateFormat.format(date); - } - - @NonNull - static Date fromIso8601(@NonNull String date) { - try { - return iso8601Holder.get().parse(date); - } catch (ParseException exc) { - throw new IllegalArgumentException("Failed to parse timestamp", exc); - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/DeliveryHeaders.kt b/app/src/main/java/com/bugsnag/android/DeliveryHeaders.kt index cebc83bfc7..89df18055d 100644 --- a/app/src/main/java/com/bugsnag/android/DeliveryHeaders.kt +++ b/app/src/main/java/com/bugsnag/android/DeliveryHeaders.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.DateUtils import java.io.OutputStream import java.security.DigestOutputStream import java.security.MessageDigest diff --git a/app/src/main/java/com/bugsnag/android/NativeInterface.java b/app/src/main/java/com/bugsnag/android/NativeInterface.java index 45802b55a3..d6d2528542 100644 --- a/app/src/main/java/com/bugsnag/android/NativeInterface.java +++ b/app/src/main/java/com/bugsnag/android/NativeInterface.java @@ -430,10 +430,36 @@ public class NativeInterface { getClient().setAutoDetectAnrs(autoDetectAnrs); } + public static void startSession() { + getClient().startSession(); + } + + public static void pauseSession() { + getClient().pauseSession(); + } + + public static boolean resumeSession() { + return getClient().resumeSession(); + } + + @Nullable + public static Session getCurrentSession() { + return getClient().sessionTracker.getCurrentSession(); + } + /** * Marks the launch period as complete */ public static void markLaunchCompleted() { getClient().markLaunchCompleted(); } + + /** + * Get the last run info object + */ + @Nullable + public static LastRunInfo getLastRunInfo() { + return getClient().getLastRunInfo(); + } + } diff --git a/app/src/main/java/com/bugsnag/android/Notifier.kt b/app/src/main/java/com/bugsnag/android/Notifier.kt index 578ec134d9..150a2bbee4 100644 --- a/app/src/main/java/com/bugsnag/android/Notifier.kt +++ b/app/src/main/java/com/bugsnag/android/Notifier.kt @@ -7,7 +7,7 @@ import java.io.IOException */ class Notifier @JvmOverloads constructor( var name: String = "Android Bugsnag Notifier", - var version: String = "5.12.0", + var version: String = "5.14.0", var url: String = "https://bugsnag.com" ) : JsonStream.Streamable { diff --git a/app/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt b/app/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt index 146aba9b65..1e817b22ec 100644 --- a/app/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt +++ b/app/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.DateUtils import java.io.IOException import java.lang.reflect.Array import java.util.Date diff --git a/app/src/main/java/com/bugsnag/android/SessionTracker.java b/app/src/main/java/com/bugsnag/android/SessionTracker.java index 93ec02155a..345d635b0a 100644 --- a/app/src/main/java/com/bugsnag/android/SessionTracker.java +++ b/app/src/main/java/com/bugsnag/android/SessionTracker.java @@ -1,7 +1,10 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.DateUtils; import com.bugsnag.android.internal.ImmutableConfig; +import android.os.SystemClock; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -319,11 +322,11 @@ class SessionTracker extends BaseObservable { } void onActivityStarted(String activityName) { - updateForegroundTracker(activityName, true, System.currentTimeMillis()); + updateForegroundTracker(activityName, true, SystemClock.elapsedRealtime()); } void onActivityStopped(String activityName) { - updateForegroundTracker(activityName, false, System.currentTimeMillis()); + updateForegroundTracker(activityName, false, SystemClock.elapsedRealtime()); } /** @@ -349,7 +352,7 @@ class SessionTracker extends BaseObservable { if (noActivityRunningForMs >= timeoutMs && configuration.getAutoTrackSessions()) { - startNewSession(new Date(nowMs), client.getUser(), true); + startNewSession(new Date(), client.getUser(), true); } } foregroundActivities.add(activityName); diff --git a/app/src/main/java/com/bugsnag/android/StateEvent.kt b/app/src/main/java/com/bugsnag/android/StateEvent.kt index de6bb3963b..83ba82108e 100644 --- a/app/src/main/java/com/bugsnag/android/StateEvent.kt +++ b/app/src/main/java/com/bugsnag/android/StateEvent.kt @@ -9,7 +9,8 @@ sealed class StateEvent { // JvmField allows direct field access optimizations @JvmField val buildUuid: String?, @JvmField val releaseStage: String?, @JvmField val lastRunInfoPath: String, - @JvmField val consecutiveLaunchCrashes: Int + @JvmField val consecutiveLaunchCrashes: Int, + @JvmField val sendThreads: ThreadSendPolicy ) : StateEvent() object DeliverPending : StateEvent() diff --git a/app/src/main/java/com/bugsnag/android/Thread.java b/app/src/main/java/com/bugsnag/android/Thread.java index 90daa50e49..d6d5ee73fe 100644 --- a/app/src/main/java/com/bugsnag/android/Thread.java +++ b/app/src/main/java/com/bugsnag/android/Thread.java @@ -1,6 +1,7 @@ package com.bugsnag.android; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.io.IOException; import java.util.List; @@ -19,9 +20,10 @@ public class Thread implements JsonStream.Streamable { @NonNull String name, @NonNull ThreadType type, boolean errorReportingThread, + @NonNull Thread.State state, @NonNull Stacktrace stacktrace, @NonNull Logger logger) { - this.impl = new ThreadInternal(id, name, type, errorReportingThread, stacktrace); + this.impl = new ThreadInternal(id, name, type, errorReportingThread, state, stacktrace); this.logger = logger; } @@ -81,6 +83,25 @@ public class Thread implements JsonStream.Streamable { return impl.getType(); } + /** + * Sets the state of thread (from {@link java.lang.Thread}) + */ + public void setState(@NonNull Thread.State threadState) { + if (threadState != null) { + impl.setState(threadState); + } else { + logNull("state"); + } + } + + /** + * Gets the state of the thread (from {@link java.lang.Thread}) + */ + @NonNull + public Thread.State getState() { + return impl.getState(); + } + /** * Gets whether the thread was the thread that caused the event */ @@ -111,4 +132,79 @@ public class Thread implements JsonStream.Streamable { public void toStream(@NonNull JsonStream stream) throws IOException { impl.toStream(stream); } + + /** + * The state of a reported {@link Thread}. These states correspond directly to + * {@link java.lang.Thread.State}, except for {@code UNKNOWN} which indicates that + * a state could not be captured or mapped. + */ + public enum State { + NEW("NEW"), + BLOCKED("BLOCKED"), + RUNNABLE("RUNNABLE"), + TERMINATED("TERMINATED"), + TIMED_WAITING("TIMED_WAITING"), + WAITING("WAITING"), + UNKNOWN("UNKNOWN"); + + private final String descriptor; + + State(String descriptor) { + this.descriptor = descriptor; + } + + @NonNull + public String getDescriptor() { + return descriptor; + } + + @NonNull + public static State forThread(@NonNull java.lang.Thread thread) { + java.lang.Thread.State state = thread.getState(); + return getState(state); + } + + /** + * Lookup the {@code State} for a given {@link #getDescriptor() descriptor} code. Unlike + * {@link #valueOf(String) valueOf}, this method will return {@link #UNKNOWN} is no + * matching {@code State} constant can be found. + * + * @param descriptor a consistent descriptor of the state constant to lookup + * @return the requested {@link State} or {@link #UNKNOWN} + */ + @NonNull + public static State byDescriptor(@Nullable String descriptor) { + if (descriptor == null) { + return UNKNOWN; + } + + for (State state : values()) { + if (state.getDescriptor().equals(descriptor)) { + return state; + } + } + + return UNKNOWN; + } + + @NonNull + private static State getState(java.lang.Thread.State state) { + switch (state) { + case NEW: + return NEW; + case BLOCKED: + return BLOCKED; + case RUNNABLE: + return RUNNABLE; + case TERMINATED: + return TERMINATED; + case TIMED_WAITING: + return TIMED_WAITING; + case WAITING: + return WAITING; + default: + return UNKNOWN; + } + } + } } diff --git a/app/src/main/java/com/bugsnag/android/ThreadInternal.kt b/app/src/main/java/com/bugsnag/android/ThreadInternal.kt index 459c17f87e..efacfb5644 100644 --- a/app/src/main/java/com/bugsnag/android/ThreadInternal.kt +++ b/app/src/main/java/com/bugsnag/android/ThreadInternal.kt @@ -7,6 +7,7 @@ class ThreadInternal internal constructor( var name: String, var type: ThreadType, val isErrorReportingThread: Boolean, + var state: Thread.State, stacktrace: Stacktrace ) : JsonStream.Streamable { @@ -18,6 +19,7 @@ class ThreadInternal internal constructor( writer.name("id").value(id) writer.name("name").value(name) writer.name("type").value(type.desc) + writer.name("state").value(state.descriptor) writer.name("stacktrace") writer.beginArray() diff --git a/app/src/main/java/com/bugsnag/android/ThreadState.kt b/app/src/main/java/com/bugsnag/android/ThreadState.kt index ea27eb92a8..06e00268ac 100644 --- a/app/src/main/java/com/bugsnag/android/ThreadState.kt +++ b/app/src/main/java/com/bugsnag/android/ThreadState.kt @@ -67,7 +67,7 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc if (trace != null) { val stacktrace = Stacktrace(trace, projectPackages, logger) val errorThread = thread.id == currentThreadId - Thread(thread.id, thread.name, ThreadType.ANDROID, errorThread, stacktrace, logger) + Thread(thread.id, thread.name, ThreadType.ANDROID, errorThread, Thread.State.forThread(thread), stacktrace, logger) } else { null } diff --git a/app/src/main/java/com/bugsnag/android/internal/DateUtils.kt b/app/src/main/java/com/bugsnag/android/internal/DateUtils.kt new file mode 100644 index 0000000000..fbcbdd95b0 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/internal/DateUtils.kt @@ -0,0 +1,36 @@ +package com.bugsnag.android.internal + +import java.text.DateFormat +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +object DateUtils { + // SimpleDateFormat isn't thread safe, cache one instance per thread as needed. + private val iso8601Holder = object : ThreadLocal() { + override fun initialValue(): DateFormat { + return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") + } + } + } + + private val iso8601Format: DateFormat + get() = requireNotNull(iso8601Holder.get()) { "Unable to find valid dateformatter" } + + @JvmStatic + fun toIso8601(date: Date): String { + return iso8601Format.format(date) + } + + @JvmStatic + fun fromIso8601(date: String): Date { + return try { + iso8601Format.parse(date) ?: throw ParseException("DateFormat.parse returned null", 0) + } catch (exc: ParseException) { + throw IllegalArgumentException("Failed to parse timestamp", exc) + } + } +} diff --git a/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt b/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt index 3a5b42c59d..9ace0f6686 100644 --- a/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt +++ b/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt @@ -16,7 +16,7 @@ import com.bugsnag.android.EndpointConfiguration import com.bugsnag.android.ErrorTypes import com.bugsnag.android.EventPayload import com.bugsnag.android.Logger -import com.bugsnag.android.ManifestConfigLoader +import com.bugsnag.android.ManifestConfigLoader.Companion.BUILD_UUID import com.bugsnag.android.NoopLogger import com.bugsnag.android.ThreadSendPolicy import com.bugsnag.android.errorApiHeaders @@ -52,7 +52,8 @@ data class ImmutableConfig( // results cached here to avoid unnecessary lookups in Client. val packageInfo: PackageInfo?, - val appInfo: ApplicationInfo? + val appInfo: ApplicationInfo?, + val redactedKeys: Collection ) { @JvmName("getErrorApiDeliveryParams") @@ -162,7 +163,8 @@ internal fun convertToImmutableConfig( persistenceDirectory = persistenceDir, sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously, packageInfo = packageInfo, - appInfo = appInfo + appInfo = appInfo, + redactedKeys = config.redactedKeys.toSet() ) } @@ -209,7 +211,7 @@ internal fun sanitiseConfiguration( } // populate buildUUID from manifest - val buildUuid = appInfo?.metaData?.getString(ManifestConfigLoader.BUILD_UUID) + val buildUuid = populateBuildUuid(appInfo) @Suppress("SENSELESS_COMPARISON") if (configuration.delivery == null) { @@ -224,5 +226,15 @@ internal fun sanitiseConfiguration( ) } +private fun populateBuildUuid(appInfo: ApplicationInfo?): String? { + val bundle = appInfo?.metaData + return when { + bundle?.containsKey(BUILD_UUID) == true -> { + bundle.getString(BUILD_UUID) ?: bundle.getInt(BUILD_UUID).toString() + } + else -> null + } +} + internal const val RELEASE_STAGE_DEVELOPMENT = "development" internal const val RELEASE_STAGE_PRODUCTION = "production"