Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
* Added `NativeOutOfMemoryPlugin` as a new way to report `OutOfMemoryError`s that uses pre-allocated memory in the NDK module instead of allocating an `Event` object. When used `OutOfMemoryError`s will be more reliably reported, but will not be passed to `OnErrorCallback`s (`OnSendCallback` works as expected).
[#2384](https://github.com/bugsnag/bugsnag-android/pull/2384)
* Added `appHangCooldownMillis` to the AppHangPlugin to control the number of AppHang errors produced when the app is performance constrained
[]()
[#2389](https://github.com/bugsnag/bugsnag-android/pull/2389)
* Moved root/jailbreak detection onto a background thread so that it no longer blocks startup (this should improve startup performance in most common cases)
[#2391](https://github.com/bugsnag/bugsnag-android/pull/2391)

## 6.24.0 (2026-02-11)

Expand Down
7 changes: 7 additions & 0 deletions bugsnag-android-core/api/bugsnag-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,10 @@ public final class com/bugsnag/android/StateEvent$StartSession : com/bugsnag/and
public final fun getUnhandledCount ()I
}

public final class com/bugsnag/android/StateEvent$SynchronizeState : com/bugsnag/android/StateEvent {
public static final field INSTANCE Lcom/bugsnag/android/StateEvent$SynchronizeState;
}

public final class com/bugsnag/android/StateEvent$UpdateContext : com/bugsnag/android/StateEvent {
public final field context Ljava/lang/String;
public fun <init> (Ljava/lang/String;)V
Expand Down Expand Up @@ -1179,13 +1183,15 @@ public final class com/bugsnag/android/internal/TaskType : java/lang/Enum {
public abstract interface class com/bugsnag/android/internal/dag/Provider {
public abstract fun get ()Ljava/lang/Object;
public abstract fun getOrNull ()Ljava/lang/Object;
public abstract fun isComplete ()Z
}

public abstract class com/bugsnag/android/internal/dag/RunnableProvider : com/bugsnag/android/internal/dag/Provider, java/lang/Runnable {
public fun <init> ()V
public fun get ()Ljava/lang/Object;
public fun getOrNull ()Ljava/lang/Object;
public abstract fun invoke ()Ljava/lang/Object;
public fun isComplete ()Z
public final fun run ()V
}

Expand All @@ -1197,6 +1203,7 @@ public final class com/bugsnag/android/internal/dag/ValueProvider : com/bugsnag/
public fun get ()Ljava/lang/Object;
public fun getOrNull ()Ljava/lang/Object;
public fun hashCode ()I
public fun isComplete ()Z
public fun toString ()Ljava/lang/String;
}

3 changes: 1 addition & 2 deletions bugsnag-android-core/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<ID>LongParameterList:AppWithState.kt$AppWithState$( binaryArch: String?, id: String?, releaseStage: String?, version: String?, codeBundleId: String?, buildUuid: Provider&lt;String?>?, type: String?, versionCode: Number?, /** * The number of milliseconds the application was running before the event occurred */ var duration: Number?, /** * The number of milliseconds the application was running in the foreground before the * event occurred */ var durationInForeground: Number?, /** * Whether the application was in the foreground when the event occurred */ var inForeground: Boolean?, /** * Whether the application was launching when the event occurred */ var isLaunching: Boolean? )</ID>
<ID>LongParameterList:AppWithState.kt$AppWithState$( binaryArch: String?, id: String?, releaseStage: String?, version: String?, codeBundleId: String?, buildUuid: String?, type: String?, versionCode: Number?, /** * The number of milliseconds the application was running before the event occurred */ duration: Number?, /** * The number of milliseconds the application was running in the foreground before the * event occurred */ durationInForeground: Number?, /** * Whether the application was in the foreground when the event occurred */ inForeground: Boolean?, /** * Whether the application was launching when the event occurred */ isLaunching: Boolean? )</ID>
<ID>LongParameterList:AppWithState.kt$AppWithState$( config: ImmutableConfig, binaryArch: String?, id: String?, releaseStage: String?, version: String?, codeBundleId: String?, duration: Number?, durationInForeground: Number?, inForeground: Boolean?, isLaunching: Boolean? )</ID>
<ID>LongParameterList:DataCollectionModule.kt$DataCollectionModule$( contextModule: ContextModule, configModule: ConfigModule, systemServiceModule: SystemServiceModule, trackerModule: TrackerModule, bgTaskService: BackgroundTaskService, connectivity: Connectivity, deviceIdStore: Provider&lt;DeviceIdStore>, memoryTrimState: MemoryTrimState )</ID>
<ID>LongParameterList:DataCollectionModule.kt$DataCollectionModule$( contextModule: ContextModule, configModule: ConfigModule, systemServiceModule: SystemServiceModule, trackerModule: TrackerModule, bgTaskService: BackgroundTaskService, connectivity: Connectivity, deviceId: Provider&lt;DeviceIdStore.DeviceIds?>, memoryTrimState: MemoryTrimState, clientObservable: ClientObservable )</ID>
<ID>LongParameterList:Device.kt$Device$( buildInfo: DeviceBuildInfo, /** * The Application Binary Interface used */ var cpuAbi: Array&lt;String>?, /** * Whether the device has been jailbroken */ var jailbroken: Boolean?, /** * A UUID generated by Bugsnag and used for the individual application on a device */ var id: String?, /** * The IETF language tag of the locale used */ var locale: String?, /** * The total number of bytes of memory on the device */ var totalMemory: Long?, /** * A collection of names and their versions of the primary languages, frameworks or * runtimes that the application is running on */ runtimeVersions: MutableMap&lt;String, Any>? )</ID>
<ID>LongParameterList:DeviceBuildInfo.kt$DeviceBuildInfo$( val manufacturer: String?, val model: String?, val osVersion: String?, val apiLevel: Int?, val osBuild: String?, val fingerprint: String?, val tags: String?, val brand: String?, val cpuAbis: Array&lt;String>? )</ID>
<ID>LongParameterList:DeviceDataCollector.kt$DeviceDataCollector$( private val connectivity: Connectivity, private val appContext: Context, resources: Resources, private val deviceIdStore: Provider&lt;DeviceIdStore.DeviceIds?>, private val buildInfo: DeviceBuildInfo, private val dataDirectory: File, private val rootedFuture: Provider&lt;Boolean>?, private val bgTaskService: BackgroundTaskService, private val logger: Logger )</ID>
Expand Down Expand Up @@ -84,7 +84,6 @@
<ID>SwallowedException:ConnectivityCompat.kt$ConnectivityLegacy$e: NullPointerException</ID>
<ID>SwallowedException:ContextExtensions.kt$exc: RuntimeException</ID>
<ID>SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$e: Throwable</ID>
<ID>SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$exc: Exception</ID>
<ID>SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$exception: Exception</ID>
<ID>SwallowedException:DeviceIdFilePersistence.kt$DeviceIdFilePersistence$exc: OverlappingFileLockException</ID>
<ID>SwallowedException:EventStore.kt$EventStore$exception: RejectedExecutionException</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public Client(@NonNull Context androidContext, @NonNull String apiKey) {
* @param configuration a configuration for the Client
*/
public Client(@NonNull Context androidContext, @NonNull final Configuration configuration) {
ContextModule contextModule = new ContextModule(androidContext, bgTaskService);
ContextModule contextModule = new ContextModule(androidContext);
appContext = contextModule.getCtx();

notifier = configuration.getNotifier();
Expand Down Expand Up @@ -186,16 +186,16 @@ public Unit invoke(Boolean hasConnection, String networkState) {

// lookup system services
final SystemServiceModule systemServiceModule =
new SystemServiceModule(contextModule, bgTaskService);
new SystemServiceModule(contextModule);

// setup further state trackers and data collection
TrackerModule trackerModule = new TrackerModule(configModule,
storageModule, this, bgTaskService, callbackState);

DataCollectionModule dataCollectionModule = new DataCollectionModule(contextModule,
configModule, systemServiceModule, trackerModule,
bgTaskService, connectivity, storageModule.getDeviceIdStore(),
memoryTrimState);
bgTaskService, connectivity, storageModule.getDeviceId(),
memoryTrimState, clientObservable);

// load the device + user information
userState = storageModule.loadUser(configuration.getUser());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@ internal class ClientObservable : BaseObservable() {
fun postNdkDeliverPending() {
updateState { StateEvent.DeliverPending }
}

fun postSynchronizeState() {
updateState { StateEvent.SynchronizeState }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.bugsnag.android

import android.os.Environment
import com.bugsnag.android.internal.BackgroundTaskService
import com.bugsnag.android.internal.RootDetectionProvider
import com.bugsnag.android.internal.dag.BackgroundDependencyModule
import com.bugsnag.android.internal.dag.ConfigModule
import com.bugsnag.android.internal.dag.ContextModule
Expand All @@ -19,8 +20,9 @@ internal class DataCollectionModule(
trackerModule: TrackerModule,
bgTaskService: BackgroundTaskService,
connectivity: Connectivity,
deviceIdStore: Provider<DeviceIdStore>,
memoryTrimState: MemoryTrimState
deviceId: Provider<DeviceIdStore.DeviceIds?>,
memoryTrimState: MemoryTrimState,
clientObservable: ClientObservable
) : BackgroundDependencyModule(bgTaskService) {

private val ctx = contextModule.ctx
Expand All @@ -41,17 +43,15 @@ internal class DataCollectionModule(
)
}

private val rootDetection = provider {
val rootDetector = RootDetector(logger = logger, deviceBuildInfo = deviceBuildInfo)
rootDetector.isRooted()
}
private val rootDetection = RootDetectionProvider(deviceBuildInfo, clientObservable, logger)
.apply { start() }

val deviceDataCollector = provider {
DeviceDataCollector(
connectivity,
ctx,
ctx.resources,
deviceIdStore.map { it.load() },
deviceId,
deviceBuildInfo,
dataDir,
rootDetection,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,10 @@ internal class DeviceDataCollector(
}

private fun checkIsRooted(): Boolean {
val rooted = rootedFuture ?: return false
return try {
rootedFuture != null && rootedFuture.get()
} catch (exc: Exception) {
rooted.isComplete && rooted.get()
} catch (_: Exception) {
false
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ internal class DeviceIdStore @JvmOverloads @Suppress("LongParameterList") constr
return internalPersistence.loadDeviceId(true)
}

@Synchronized
fun load(): DeviceIds? {
if (deviceIds != null) {
return deviceIds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,6 @@ sealed class StateEvent { // JvmField allows direct field access optimizations
) : StateEvent()

object ClearFeatureFlags : StateEvent()

object SynchronizeState : StateEvent()
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,15 @@ internal class StorageModule(
)
}

val deviceId = deviceIdStore.map {
it.load()
}

val userStore = provider {
UserStore(
immutableConfig.persistUser,
bugsnagDir,
deviceIdStore.map { it.load() },
deviceId,
sharedPrefMigrator = sharedPrefMigrator,
logger = immutableConfig.logger
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.bugsnag.android.internal

import com.bugsnag.android.ClientObservable
import com.bugsnag.android.DeviceBuildInfo
import com.bugsnag.android.Logger
import com.bugsnag.android.RootDetector
import com.bugsnag.android.internal.dag.RunnableProvider

internal class RootDetectionProvider(
private val deviceBuildInfo: DeviceBuildInfo,
private val clientObservable: ClientObservable,
private val logger: Logger,
) : RunnableProvider<Boolean>() {
var isRooted: Boolean = false
private set

fun start() {
// root detection can take 100+ms so we always have a dedicated background thread for it
// we fire an event once we're finished to let any downstream notifiers know the result
val worker = Thread(this, "Bugsnag Worker")
worker.priority = Thread.MIN_PRIORITY
worker.isDaemon = true
worker.start()
}

override fun invoke(): Boolean {
val rootDetector = RootDetector(logger = logger, deviceBuildInfo = deviceBuildInfo)
isRooted = rootDetector.isRooted()
clientObservable.postSynchronizeState()
return isRooted
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ internal class ConfigModule(
configuration: Configuration,
connectivity: Connectivity,
bgTaskExecutor: BackgroundTaskService
) : BackgroundDependencyModule(bgTaskExecutor) {
) : DependencyModule {
val config = sanitiseConfiguration(contextModule.ctx, configuration, connectivity, bgTaskExecutor)
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
package com.bugsnag.android.internal.dag

import android.content.Context
import com.bugsnag.android.internal.BackgroundTaskService

/**
* A dependency module which accesses the application context object, falling back to the supplied
* context if it is the base context.
*/
internal class ContextModule(
appContext: Context,
bgTaskService: BackgroundTaskService
) : BackgroundDependencyModule(bgTaskService) {

internal class ContextModule(appContext: Context) : DependencyModule {
val ctx: Context = when (appContext.applicationContext) {
null -> appContext
else -> appContext.applicationContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ interface Provider<E> {
* to calculate the value.
*/
fun get(): E

val isComplete: Boolean
}

/**
Expand All @@ -40,6 +42,12 @@ abstract class RunnableProvider<E> : Provider<E>, Runnable {
@Volatile
private var value: Any? = null

override val isComplete
get() = when (state.get()) {
TASK_STATE_PENDING, TASK_STATE_RUNNING -> false
else -> true
}

/**
* Calculate the value of this [Provider]. This function will be called at-most once by [run].
* Do not call this function directly, instead use [get] and [getOrNull] which implement the
Expand Down Expand Up @@ -91,18 +99,13 @@ abstract class RunnableProvider<E> : Provider<E>, Runnable {
*/
private fun awaitResult() {
synchronized(this) {
while (!isComplete()) {
while (!isComplete) {
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
(this as Object).wait()
}
}
}

private fun isComplete() = when (state.get()) {
TASK_STATE_PENDING, TASK_STATE_RUNNING -> false
else -> true
}

/**
* The main entry point for a provider, typically called by a worker thread from
* [BackgroundTaskService]. If [run] has already been called this will be a no-op (including
Expand Down Expand Up @@ -178,4 +181,5 @@ abstract class RunnableProvider<E> : Provider<E>, Runnable {
data class ValueProvider<T>(private val value: T) : Provider<T> {
override fun getOrNull(): T? = get()
override fun get(): T = value
override val isComplete: Boolean get() = true
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,11 @@ package com.bugsnag.android.internal.dag

import com.bugsnag.android.getActivityManager
import com.bugsnag.android.getStorageManager
import com.bugsnag.android.internal.BackgroundTaskService

/**
* A dependency module which provides a reference to Android system services.
*/
internal class SystemServiceModule(
contextModule: ContextModule,
bgTaskService: BackgroundTaskService
) : BackgroundDependencyModule(bgTaskService) {

internal class SystemServiceModule(contextModule: ContextModule) : DependencyModule {
val storageManager = contextModule.ctx.getStorageManager()
val activityManager = contextModule.ctx.getActivityManager()
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class NativeBridge(private val bgTaskService: BackgroundTaskService) : StateObse
}

private external fun addBreadcrumb(name: String, type: Int, timestamp: String, metadata: Any)
private external fun synchronizeState()
external fun reportOutOfMemory(oom: OutOfMemoryError)
external fun addMetadataString(tab: String, key: String, value: String)
external fun addMetadataDouble(tab: String, key: String, value: Double)
Expand Down Expand Up @@ -171,6 +172,8 @@ class NativeBridge(private val bgTaskService: BackgroundTaskService) : StateObse
is StateEvent.ClearFeatureFlag -> clearFeatureFlag(event.name)
is StateEvent.ClearFeatureFlags -> clearFeatureFlags()

is StateEvent.SynchronizeState -> synchronizeState()

else -> {
// ignore
}
Expand Down
13 changes: 13 additions & 0 deletions bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,19 @@ static void JNI_NativeBridge_setInternalMetricsEnabled(JNIEnv *env,
bsg_set_internal_metrics_enabled(enabled);
}

static void JNI_NativeBridge_synchronizeState(JNIEnv *env, jobject thiz) {
bsg_environment *bsg_env = request_env_write_lock();

if (bsg_env == NULL) {
goto end;
}

bsg_populate_event(env, &bsg_env->next_event);

end:
release_env_write_lock();
}

static void JNI_NativeBridge_reportOutOfMemory(JNIEnv *env, jobject thiz,
jobject oom) {

Expand Down
2 changes: 1 addition & 1 deletion bugsnag-plugin-android-ndk/src/main/jni/event.c
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ char *bugsnag_event_get_context(void *event_ptr) {
void bugsnag_event_set_context(void *event_ptr, const char *value) {
bugsnag_event *event = (bugsnag_event *)event_ptr;
bsg_notify_api_called(event, BSG_API_EVENT_SET_CONTEXT);
bsg_strncpy(event->context, value, sizeof(event->context));
bsg_strncpy(event->context, value, sizeof(event->context) - 1);
}

char *bugsnag_event_get_grouping_discriminator(void *event_ptr) {
Expand Down
2 changes: 1 addition & 1 deletion bugsnag-plugin-android-ndk/src/main/jni/metadata.c
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ static void populate_app_data(JNIEnv *env, bugsnag_event *event) {

copy_map_value_string(env, data, "id", event->app.id, sizeof(event->app.id));
event->app.in_foreground = get_map_value_bool(env, data, "inForeground");
event->app.is_launching = true;
event->app.is_launching = get_map_value_bool(env, data, "isLaunching");

char name[64];
copy_map_value_string(env, data, "name", name, sizeof(name));
Expand Down
Loading
Loading