Skip to content

Commit 889ecea

Browse files
authored
feat(integrations): Introduce ctor overloads for SystemEvents and AndroidConnectionStatusProvider that accept a Handler (#4808)
* feat(integrations): Introduce ctor overloads for and that accept a Handler to invoke their callbacks on * Changelog
1 parent b23af9e commit 889ecea

File tree

8 files changed

+104
-9
lines changed

8 files changed

+104
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Features
66

77
- Add experimental Sentry Android Distribution module for integrating with Sentry Build Distribution to check for and install updates ([#4804](https://github.com/getsentry/sentry-java/pull/4804))
8+
- Allow passing a different `Handler` to `SystemEventsBreadcrumbsIntegration` and `AndroidConnectionStatusProvider` so their callbacks are deliver to that handler ([#4808](https://github.com/getsentry/sentry-java/pull/4808))
89

910
### Fixes
1011

sentry-android-core/api/sentry-android-core.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ public class io/sentry/android/core/SpanFrameMetricsCollector : io/sentry/IPerfo
446446

447447
public final class io/sentry/android/core/SystemEventsBreadcrumbsIntegration : io/sentry/Integration, io/sentry/android/core/AppState$AppStateListener, java/io/Closeable {
448448
public fun <init> (Landroid/content/Context;)V
449+
public fun <init> (Landroid/content/Context;Landroid/os/Handler;)V
449450
public fun <init> (Landroid/content/Context;Ljava/util/List;)V
450451
public fun close ()V
451452
public static fun getDefaultActions ()Ljava/util/List;

sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ private static void deduplicateIntegrations(
232232

233233
final List<Integration> timberIntegrations = new ArrayList<>();
234234
final List<Integration> fragmentIntegrations = new ArrayList<>();
235+
final List<Integration> systemEventsIntegrations = new ArrayList<>();
235236

236237
for (final Integration integration : options.getIntegrations()) {
237238
if (isFragmentAvailable) {
@@ -244,6 +245,9 @@ private static void deduplicateIntegrations(
244245
timberIntegrations.add(integration);
245246
}
246247
}
248+
if (integration instanceof SystemEventsBreadcrumbsIntegration) {
249+
systemEventsIntegrations.add(integration);
250+
}
247251
}
248252

249253
if (fragmentIntegrations.size() > 1) {
@@ -259,5 +263,12 @@ private static void deduplicateIntegrations(
259263
options.getIntegrations().remove(integration);
260264
}
261265
}
266+
267+
if (systemEventsIntegrations.size() > 1) {
268+
for (int i = 0; i < systemEventsIntegrations.size() - 1; i++) {
269+
final Integration integration = systemEventsIntegrations.get(i);
270+
options.getIntegrations().remove(integration);
271+
}
272+
}
262273
}
263274
}

sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,24 @@ public final class SystemEventsBreadcrumbsIntegration
7272
private final @NotNull AutoClosableReentrantLock receiverLock = new AutoClosableReentrantLock();
7373
// Track previous battery state to avoid duplicate breadcrumbs when values haven't changed
7474
private @Nullable BatteryState previousBatteryState;
75+
@TestOnly @Nullable Handler customHandler = null;
7576

7677
public SystemEventsBreadcrumbsIntegration(final @NotNull Context context) {
77-
this(context, getDefaultActionsInternal());
78+
this(context, getDefaultActionsInternal(), null);
79+
}
80+
81+
public SystemEventsBreadcrumbsIntegration(
82+
final @NotNull Context context, final @NotNull Handler handler) {
83+
this(context, getDefaultActionsInternal(), handler);
7884
}
7985

8086
SystemEventsBreadcrumbsIntegration(
81-
final @NotNull Context context, final @NotNull String[] actions) {
87+
final @NotNull Context context,
88+
final @NotNull String[] actions,
89+
final @Nullable Handler handler) {
8290
this.context = ContextUtils.getApplicationContext(context);
8391
this.actions = actions;
92+
this.customHandler = handler;
8493
}
8594

8695
public SystemEventsBreadcrumbsIntegration(
@@ -143,7 +152,7 @@ private void registerReceiver(
143152
filter.addAction(item);
144153
}
145154
}
146-
if (handlerThread == null) {
155+
if (customHandler == null && handlerThread == null) {
147156
handlerThread =
148157
new HandlerThread(
149158
"SystemEventsReceiver", Process.THREAD_PRIORITY_BACKGROUND);
@@ -154,7 +163,12 @@ private void registerReceiver(
154163
// official docs
155164

156165
// onReceive will be called on this handler thread
157-
final @NotNull Handler handler = new Handler(handlerThread.getLooper());
166+
@NotNull Handler handler;
167+
if (customHandler != null) {
168+
handler = customHandler;
169+
} else {
170+
handler = new Handler(handlerThread.getLooper());
171+
}
158172
ContextUtils.registerReceiver(context, options, receiver, filter, handler);
159173
if (!isReceiverRegistered.getAndSet(true)) {
160174
options

sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import android.net.Network;
99
import android.net.NetworkCapabilities;
1010
import android.os.Build;
11+
import android.os.Handler;
1112
import androidx.annotation.NonNull;
1213
import androidx.annotation.RequiresApi;
1314
import io.sentry.IConnectionStatusProvider;
@@ -42,6 +43,7 @@ public final class AndroidConnectionStatusProvider
4243
private final @NotNull BuildInfoProvider buildInfoProvider;
4344
private final @NotNull ICurrentDateProvider timeProvider;
4445
private final @NotNull List<IConnectionStatusObserver> connectionStatusObservers;
46+
private final @Nullable Handler handler;
4547
private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
4648
private volatile @Nullable NetworkCallback networkCallback;
4749

@@ -68,16 +70,26 @@ public final class AndroidConnectionStatusProvider
6870
private static final long CACHE_TTL_MS = 2 * 60 * 1000L; // 2 minutes
6971
private final @NotNull AtomicBoolean isConnected = new AtomicBoolean(false);
7072

71-
@SuppressLint("InlinedApi")
7273
public AndroidConnectionStatusProvider(
7374
@NotNull Context context,
7475
@NotNull SentryOptions options,
7576
@NotNull BuildInfoProvider buildInfoProvider,
7677
@NotNull ICurrentDateProvider timeProvider) {
78+
this(context, options, buildInfoProvider, timeProvider, null);
79+
}
80+
81+
@SuppressLint("InlinedApi")
82+
public AndroidConnectionStatusProvider(
83+
@NotNull Context context,
84+
@NotNull SentryOptions options,
85+
@NotNull BuildInfoProvider buildInfoProvider,
86+
@NotNull ICurrentDateProvider timeProvider,
87+
@Nullable Handler handler) {
7788
this.context = ContextUtils.getApplicationContext(context);
7889
this.options = options;
7990
this.buildInfoProvider = buildInfoProvider;
8091
this.timeProvider = timeProvider;
92+
this.handler = handler;
8193
this.connectionStatusObservers = new ArrayList<>();
8294

8395
capabilities[0] = NetworkCapabilities.NET_CAPABILITY_INTERNET;
@@ -326,7 +338,8 @@ private boolean hasSignificantTransportChanges(
326338
}
327339
};
328340

329-
if (registerNetworkCallback(context, options.getLogger(), buildInfoProvider, callback)) {
341+
if (registerNetworkCallback(
342+
context, options.getLogger(), buildInfoProvider, handler, callback)) {
330343
networkCallback = callback;
331344
options.getLogger().log(SentryLevel.DEBUG, "Network callback registered successfully");
332345
} else {
@@ -744,6 +757,7 @@ static boolean registerNetworkCallback(
744757
final @NotNull Context context,
745758
final @NotNull ILogger logger,
746759
final @NotNull BuildInfoProvider buildInfoProvider,
760+
final @Nullable Handler handler,
747761
final @NotNull NetworkCallback networkCallback) {
748762
if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.N) {
749763
logger.log(SentryLevel.DEBUG, "NetworkCallbacks need Android N+.");
@@ -758,7 +772,11 @@ static boolean registerNetworkCallback(
758772
return false;
759773
}
760774
try {
761-
connectivityManager.registerDefaultNetworkCallback(networkCallback);
775+
if (handler != null) {
776+
connectivityManager.registerDefaultNetworkCallback(networkCallback, handler);
777+
} else {
778+
connectivityManager.registerDefaultNetworkCallback(networkCallback);
779+
}
762780
} catch (Throwable t) {
763781
logger.log(SentryLevel.WARNING, "registerDefaultNetworkCallback failed", t);
764782
return false;

sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import android.app.ApplicationExitInfo
66
import android.content.Context
77
import android.os.Build
88
import android.os.Bundle
9+
import android.os.Handler
910
import android.os.Looper
1011
import android.os.SystemClock
1112
import androidx.test.core.app.ApplicationProvider
@@ -237,13 +238,19 @@ class SentryAndroidTest {
237238
}
238239

239240
@Test
240-
fun `deduplicates fragment and timber integrations`() {
241+
fun `deduplicates fragment, timber and system events integrations`() {
241242
var refOptions: SentryAndroidOptions? = null
242-
243243
fixture.initSut(autoInit = true) {
244244
it.addIntegration(FragmentLifecycleIntegration(ApplicationProvider.getApplicationContext()))
245245

246246
it.addIntegration(SentryTimberIntegration(minEventLevel = FATAL, minBreadcrumbLevel = DEBUG))
247+
248+
it.addIntegration(
249+
SystemEventsBreadcrumbsIntegration(
250+
ApplicationProvider.getApplicationContext(),
251+
CustomHandler(Looper.getMainLooper()),
252+
)
253+
)
247254
refOptions = it
248255
}
249256

@@ -256,6 +263,11 @@ class SentryAndroidTest {
256263
// fragment integration is not auto-installed in the test, since the context is not Application
257264
// but we just verify here that the single integration is preserved
258265
assertEquals(refOptions!!.integrations.filterIsInstance<FragmentLifecycleIntegration>().size, 1)
266+
267+
val systemEventsIntegrations =
268+
refOptions!!.integrations.filterIsInstance<SystemEventsBreadcrumbsIntegration>()
269+
assertEquals(systemEventsIntegrations.size, 1)
270+
assertTrue(systemEventsIntegrations.first().customHandler is CustomHandler)
259271
}
260272

261273
@Test
@@ -580,3 +592,5 @@ fun initForTest(context: Context, logger: ILogger) {
580592
fun initForTest(context: Context) {
581593
SentryAndroid.init(context)
582594
}
595+
596+
class CustomHandler(looper: Looper) : Handler(looper)

sentry-android-core/src/test/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegrationTest.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import android.content.Context
66
import android.content.Intent
77
import android.os.BatteryManager
88
import android.os.Build
9+
import android.os.Handler
910
import android.os.Looper
1011
import androidx.test.core.app.ApplicationProvider
1112
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -27,6 +28,7 @@ import kotlin.test.assertTrue
2728
import org.junit.runner.RunWith
2829
import org.mockito.kotlin.any
2930
import org.mockito.kotlin.anyOrNull
31+
import org.mockito.kotlin.argThat
3032
import org.mockito.kotlin.check
3133
import org.mockito.kotlin.mock
3234
import org.mockito.kotlin.never
@@ -53,6 +55,7 @@ class SystemEventsBreadcrumbsIntegrationTest {
5355
enableSystemEventBreadcrumbs: Boolean = true,
5456
enableSystemEventBreadcrumbsExtras: Boolean = false,
5557
executorService: ISentryExecutorService = ImmediateExecutorService(),
58+
handler: Handler? = null,
5659
): SystemEventsBreadcrumbsIntegration {
5760
options =
5861
SentryAndroidOptions().apply {
@@ -63,6 +66,7 @@ class SystemEventsBreadcrumbsIntegrationTest {
6366
return SystemEventsBreadcrumbsIntegration(
6467
context,
6568
SystemEventsBreadcrumbsIntegration.getDefaultActions().toTypedArray(),
69+
handler,
6670
)
6771
}
6872
}
@@ -585,4 +589,16 @@ class SystemEventsBreadcrumbsIntegrationTest {
585589
anyOrNull(),
586590
)
587591
}
592+
593+
@Test
594+
fun `When a custom handler is provided, it is used upon registering the callback`() {
595+
val customHandler = object : Handler(Looper.getMainLooper()) {}
596+
val sut = fixture.getSut(handler = customHandler)
597+
598+
sut.register(fixture.scopes, fixture.options)
599+
600+
verify(fixture.context)
601+
.registerReceiver(any(), any(), anyOrNull(), argThat { this == customHandler }, any())
602+
assertNotNull(sut.receiver)
603+
}
588604
}

sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderTest.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
1515
import android.net.NetworkCapabilities.TRANSPORT_WIFI
1616
import android.net.NetworkInfo
1717
import android.os.Build
18+
import android.os.Handler
19+
import android.os.Looper
1820
import androidx.test.ext.junit.runners.AndroidJUnit4
1921
import io.sentry.IConnectionStatusProvider
2022
import io.sentry.ILogger
@@ -38,6 +40,7 @@ import org.mockito.MockedStatic
3840
import org.mockito.Mockito.mockStatic
3941
import org.mockito.kotlin.any
4042
import org.mockito.kotlin.anyOrNull
43+
import org.mockito.kotlin.argThat
4144
import org.mockito.kotlin.argumentCaptor
4245
import org.mockito.kotlin.clearInvocations
4346
import org.mockito.kotlin.eq
@@ -274,6 +277,7 @@ class AndroidConnectionStatusProviderTest {
274277
contextMock,
275278
logger,
276279
buildInfo,
280+
null,
277281
mock(),
278282
)
279283
)
@@ -841,4 +845,20 @@ class AndroidConnectionStatusProviderTest {
841845
// Verify no additional unregister calls
842846
verifyNoInteractions(connectivityManager)
843847
}
848+
849+
@Test
850+
fun `registerNetworkCallback with a custom handlers calls connectivityManager with it`() {
851+
val customHandler = object : Handler(Looper.getMainLooper()) {}
852+
whenever(contextMock.getSystemService(any())).thenReturn(connectivityManager)
853+
AndroidConnectionStatusProvider.registerNetworkCallback(
854+
contextMock,
855+
logger,
856+
buildInfo,
857+
customHandler,
858+
mock(),
859+
)
860+
861+
verify(connectivityManager)
862+
.registerDefaultNetworkCallback(any<NetworkCallback>(), argThat { this == customHandler })
863+
}
844864
}

0 commit comments

Comments
 (0)