Skip to content

Commit 4886911

Browse files
committed
Fix background ANR from connectivity broadcast receiver
Replace the deprecated CONNECTIVITY_ACTION BroadcastReceiver (ConnectionChangeReceiver) with a ConnectivityManager.NetworkCallback (NetworkConnectionMonitor) registered on a background thread. Connectivity checks and the EventBus dispatch no longer run on the main thread, and a NetworkCallback is not a broadcast so it cannot trip the background-broadcast ANR timeout. The monitor is registered once for the process lifetime, dropping the unreliable foreground/background register/unregister workaround. The ConnectionChangeEvent contract is preserved, so subscribers are unchanged apart from the renamed enclosing type. Sentry: JETPACK-ANDROID-PB9
1 parent 55a1f70 commit 4886911

10 files changed

Lines changed: 97 additions & 110 deletions

File tree

WordPress/src/main/java/org/wordpress/android/AppInitializer.kt

Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,9 @@ import android.app.NotificationManager
1111
import android.app.SyncNotedAppOp
1212
import android.content.ComponentCallbacks2
1313
import android.content.Context
14-
import android.content.ContextWrapper
15-
import android.content.IntentFilter
1614
import android.content.res.Configuration
1715
import android.database.SQLException
1816
import android.database.sqlite.SQLiteException
19-
import android.net.ConnectivityManager
2017
import android.net.http.HttpResponseCache
2118
import android.os.Build
2219
import android.os.Build.VERSION_CODES
@@ -67,7 +64,7 @@ import org.wordpress.android.fluxc.store.StatsStore
6764
import org.wordpress.android.fluxc.tools.FluxCImageLoader
6865
import org.wordpress.android.fluxc.utils.ErrorUtils.OnUnexpectedError
6966
import org.wordpress.android.modules.APPLICATION_SCOPE
70-
import org.wordpress.android.networking.ConnectionChangeReceiver
67+
import org.wordpress.android.networking.NetworkConnectionMonitor
7168
import org.wordpress.android.networking.OAuthAuthenticator
7269
import org.wordpress.android.networking.RestClientUtils
7370
import org.wordpress.android.push.GCMRegistrationScheduler
@@ -92,7 +89,6 @@ import org.wordpress.android.ui.uploads.UploadService
9289
import org.wordpress.android.ui.uploads.UploadStarter
9390
import org.wordpress.android.util.AppLog
9491
import org.wordpress.android.util.AppLog.T
95-
import org.wordpress.android.util.AppLog.T.MAIN
9692
import org.wordpress.android.util.AppThemeUtils
9793
import org.wordpress.android.util.BitmapLruCache
9894
import org.wordpress.android.util.BuildConfigWrapper
@@ -148,6 +144,9 @@ class AppInitializer @Inject constructor(
148144
@Inject
149145
lateinit var uploadStarter: UploadStarter
150146

147+
@Inject
148+
lateinit var networkConnectionMonitor: NetworkConnectionMonitor
149+
151150
@Inject
152151
lateinit var statsWidgetUpdaters: StatsWidgetUpdaters
153152

@@ -339,6 +338,11 @@ class AppInitializer @Inject constructor(
339338
// Make the UploadStarter observe the app process so it can auto-start uploads
340339
uploadStarter.activateAutoUploading(ProcessLifecycleOwner.get() as ProcessLifecycleOwner)
341340

341+
// Monitor default-network connectivity for the whole process lifetime. Uses a NetworkCallback on a
342+
// background thread instead of the deprecated CONNECTIVITY_ACTION broadcast, so it can't cause a
343+
// background ANR. start() is idempotent, so it's safe if init() runs more than once.
344+
networkConnectionMonitor.start(application)
345+
342346
initAnalytics(SystemClock.elapsedRealtime() - startDate)
343347

344348
updateNotificationSettings()
@@ -792,7 +796,6 @@ class AppInitializer @Inject constructor(
792796
inner class ApplicationLifecycleMonitor {
793797
private var lastPingDate: Date? = null
794798
private var applicationOpenedDate: Date? = null
795-
private var connectionReceiverRegistered = false
796799
var firstActivityResumed = true
797800

798801
private val isPushNotificationPingNeeded: Boolean
@@ -845,18 +848,6 @@ class AppInitializer @Inject constructor(
845848

846849
AnalyticsTracker.track(Stat.APPLICATION_CLOSED, properties)
847850
AnalyticsTracker.endSession(false)
848-
// Methods onAppComesFromBackground and onAppGoesToBackground are only workarounds to track when the app
849-
// goes to or comes from background. The workarounds are not 100% reliable, so avoid unregistering the
850-
// receiver twice.
851-
if (connectionReceiverRegistered) {
852-
connectionReceiverRegistered = false
853-
try {
854-
application.unregisterReceiver(ConnectionChangeReceiver.getInstance())
855-
AppLog.d(MAIN, "ConnectionChangeReceiver successfully unregistered")
856-
} catch (e: IllegalArgumentException) {
857-
AppLog.e(MAIN, "ConnectionChangeReceiver was already unregistered")
858-
}
859-
}
860851

861852
// Disable the widgets if needed
862853
disableWidgetReceiversIfNeeded()
@@ -876,25 +867,6 @@ class AppInitializer @Inject constructor(
876867
}
877868
WordPress.appIsInTheBackground = false
878869

879-
// https://developer.android.com/reference/android/net/ConnectivityManager.html
880-
// Apps targeting Android 7.0 (API level 24) and higher do not receive this broadcast if the broadcast
881-
// receiver is declared in their manifest. Apps will still receive broadcasts if BroadcastReceiver is
882-
// registered with Context.registerReceiver() and that context is still valid.
883-
if (!connectionReceiverRegistered) {
884-
connectionReceiverRegistered = true
885-
if (Build.VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE) {
886-
application.registerReceiver(
887-
ConnectionChangeReceiver.getInstance(),
888-
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION),
889-
ContextWrapper.RECEIVER_EXPORTED
890-
)
891-
} else {
892-
application.registerReceiver(
893-
ConnectionChangeReceiver.getInstance(),
894-
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
895-
)
896-
}
897-
}
898870
AnalyticsUtils.refreshMetadata(accountStore, siteStore)
899871
applicationOpenedDate = Date()
900872
// This stat is part of a funnel that provides critical information. Before making ANY modification to this

WordPress/src/main/java/org/wordpress/android/networking/ConnectionChangeReceiver.java

Lines changed: 0 additions & 64 deletions
This file was deleted.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package org.wordpress.android.networking
2+
3+
import android.content.Context
4+
import android.net.ConnectivityManager
5+
import android.net.Network
6+
import android.os.Handler
7+
import android.os.HandlerThread
8+
import org.greenrobot.eventbus.EventBus
9+
import org.wordpress.android.util.AppLog
10+
import org.wordpress.android.util.AppLog.T
11+
import javax.inject.Inject
12+
import javax.inject.Singleton
13+
14+
/**
15+
* Global monitor for changes to the device's default network connectivity.
16+
*
17+
* Uses [ConnectivityManager.NetworkCallback] registered on a background [HandlerThread] rather than the
18+
* deprecated CONNECTIVITY_ACTION broadcast. Because a NetworkCallback is not a broadcast it is not subject to
19+
* background-broadcast ANR timeouts, and all connectivity work runs off the main thread. A
20+
* [ConnectionChangeEvent] is posted on EventBus whenever the connected state changes; EventBus marshals the
21+
* `onEventMainThread` subscribers back onto the main thread.
22+
*/
23+
@Singleton
24+
class NetworkConnectionMonitor @Inject constructor() {
25+
private var started = false
26+
private var wasConnected = false
27+
private var isFirstCallback = true
28+
29+
private var connectivityManager: ConnectivityManager? = null
30+
private var handlerThread: HandlerThread? = null
31+
private var networkCallback: ConnectivityManager.NetworkCallback? = null
32+
33+
class ConnectionChangeEvent(val isConnected: Boolean)
34+
35+
@Synchronized
36+
fun start(context: Context) {
37+
if (started) return
38+
val manager = context.applicationContext
39+
.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return
40+
connectivityManager = manager
41+
42+
val thread = HandlerThread("NetworkConnectionMonitor").apply { start() }
43+
handlerThread = thread
44+
45+
val callback = object : ConnectivityManager.NetworkCallback() {
46+
override fun onAvailable(network: Network) = onConnectivityChanged(true)
47+
override fun onLost(network: Network) = onConnectivityChanged(false)
48+
}
49+
networkCallback = callback
50+
51+
manager.registerDefaultNetworkCallback(callback, Handler(thread.looper))
52+
started = true
53+
}
54+
55+
@Synchronized
56+
fun stop() {
57+
if (!started) return
58+
networkCallback?.let { connectivityManager?.unregisterNetworkCallback(it) }
59+
handlerThread?.quitSafely()
60+
networkCallback = null
61+
handlerThread = null
62+
connectivityManager = null
63+
started = false
64+
}
65+
66+
/**
67+
* Called on the monitor's background thread. Posts a [ConnectionChangeEvent] on the first callback and
68+
* whenever the connected state actually changes, so subscribers aren't spammed when a network's
69+
* capabilities change without a change in availability.
70+
*/
71+
private fun onConnectivityChanged(isConnected: Boolean) {
72+
if (isFirstCallback || isConnected != wasConnected) {
73+
isFirstCallback = false
74+
wasConnected = isConnected
75+
AppLog.i(T.UTILS, "Connection status changed, isConnected=$isConnected")
76+
EventBus.getDefault().post(ConnectionChangeEvent(isConnected))
77+
}
78+
}
79+
}

WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
import org.wordpress.android.inappupdate.IInAppUpdateManager;
6363
import org.wordpress.android.inappupdate.InAppUpdateListener;
6464
import org.wordpress.android.ui.accounts.login.LoginAnalyticsListener;
65-
import org.wordpress.android.networking.ConnectionChangeReceiver;
65+
import org.wordpress.android.networking.NetworkConnectionMonitor;
6666
import org.wordpress.android.push.GCMMessageHandler;
6767
import org.wordpress.android.push.GCMMessageService;
6868
import org.wordpress.android.push.GCMRegistrationScheduler;
@@ -1490,7 +1490,7 @@ public void onEventMainThread(NotificationEvents.NotificationsUnseenStatus event
14901490

14911491
@SuppressWarnings("unused")
14921492
@Subscribe(threadMode = ThreadMode.MAIN)
1493-
public void onEventMainThread(ConnectionChangeReceiver.ConnectionChangeEvent event) {
1493+
public void onEventMainThread(NetworkConnectionMonitor.ConnectionChangeEvent event) {
14941494
updateConnectionBar(event.isConnected());
14951495
}
14961496

WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ import org.wordpress.android.fluxc.store.bloggingprompts.BloggingPromptsStore
119119
import org.wordpress.android.fluxc.tools.FluxCImageLoader
120120
import org.wordpress.android.imageeditor.preview.PreviewImageFragment
121121
import org.wordpress.android.imageeditor.preview.PreviewImageFragment.Companion.EditImageData.InputData
122-
import org.wordpress.android.networking.ConnectionChangeReceiver.ConnectionChangeEvent
122+
import org.wordpress.android.networking.NetworkConnectionMonitor.ConnectionChangeEvent
123123
import org.wordpress.android.support.ZendeskHelper
124124
import org.wordpress.android.ui.ActivityId
125125
import org.wordpress.android.ui.ActivityLauncher

WordPress/src/main/java/org/wordpress/android/ui/prefs/BlogPreferencesActivity.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import org.wordpress.android.fluxc.store.SiteStore.OnSiteChanged;
2323
import org.wordpress.android.fluxc.store.SiteStore.OnSiteDeleted;
2424
import org.wordpress.android.fluxc.store.SiteStore.OnSiteRemoved;
25-
import org.wordpress.android.networking.ConnectionChangeReceiver;
25+
import org.wordpress.android.networking.NetworkConnectionMonitor;
2626
import org.wordpress.android.ui.main.BaseAppCompatActivity;
2727
import org.wordpress.android.util.SiteUtils;
2828
import org.wordpress.android.util.ToastUtils;
@@ -119,7 +119,7 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
119119

120120
@SuppressWarnings("unused")
121121
@Subscribe(threadMode = ThreadMode.MAIN)
122-
public void onEventMainThread(ConnectionChangeReceiver.ConnectionChangeEvent event) {
122+
public void onEventMainThread(NetworkConnectionMonitor.ConnectionChangeEvent event) {
123123
SiteSettingsFragment siteSettingsFragment = getSettingsFragment();
124124
if (siteSettingsFragment != null) {
125125
if (!event.isConnected()) {

WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ import org.wordpress.android.models.ReaderPost
6969
import org.wordpress.android.models.ReaderPostDiscoverData
7070
import org.wordpress.android.models.ReaderTag
7171
import org.wordpress.android.models.ReaderTagType
72-
import org.wordpress.android.networking.ConnectionChangeReceiver.ConnectionChangeEvent
72+
import org.wordpress.android.networking.NetworkConnectionMonitor.ConnectionChangeEvent
7373
import org.wordpress.android.ui.ActionableEmptyView
7474
import org.wordpress.android.ui.ActivityLauncher
7575
import org.wordpress.android.ui.EmptyViewMessageType

WordPress/src/main/java/org/wordpress/android/ui/suggestion/SuggestionActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import org.wordpress.android.R
1616
import org.wordpress.android.WordPress
1717
import org.wordpress.android.databinding.SuggestUsersActivityBinding
1818
import org.wordpress.android.fluxc.model.SiteModel
19-
import org.wordpress.android.networking.ConnectionChangeReceiver.ConnectionChangeEvent
19+
import org.wordpress.android.networking.NetworkConnectionMonitor.ConnectionChangeEvent
2020
import org.wordpress.android.ui.main.BaseAppCompatActivity
2121
import org.wordpress.android.ui.suggestion.FinishAttempt.NotExactlyOneAvailable
2222
import org.wordpress.android.ui.suggestion.FinishAttempt.OnlyOneAvailable

WordPress/src/main/java/org/wordpress/android/ui/suggestion/SuggestionViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel
66
import org.wordpress.android.R
77
import org.wordpress.android.analytics.AnalyticsTracker
88
import org.wordpress.android.fluxc.model.SiteModel
9-
import org.wordpress.android.networking.ConnectionChangeReceiver.ConnectionChangeEvent
9+
import org.wordpress.android.networking.NetworkConnectionMonitor.ConnectionChangeEvent
1010
import org.wordpress.android.ui.suggestion.FinishAttempt.NotExactlyOneAvailable
1111
import org.wordpress.android.ui.suggestion.FinishAttempt.OnlyOneAvailable
1212
import org.wordpress.android.ui.suggestion.SuggestionType.Users

WordPress/src/test/java/org/wordpress/android/ui/suggestion/SuggestionViewModelTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import org.mockito.kotlin.whenever
1818
import org.wordpress.android.R
1919
import org.wordpress.android.analytics.AnalyticsTracker
2020
import org.wordpress.android.fluxc.model.SiteModel
21-
import org.wordpress.android.networking.ConnectionChangeReceiver.ConnectionChangeEvent
21+
import org.wordpress.android.networking.NetworkConnectionMonitor.ConnectionChangeEvent
2222
import org.wordpress.android.ui.suggestion.FinishAttempt.NotExactlyOneAvailable
2323
import org.wordpress.android.ui.suggestion.FinishAttempt.OnlyOneAvailable
2424
import org.wordpress.android.ui.suggestion.SuggestionType.Users

0 commit comments

Comments
 (0)