diff --git a/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt b/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt index 300566324748..5f176e95dff1 100644 --- a/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt +++ b/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt @@ -11,12 +11,9 @@ import android.app.NotificationManager import android.app.SyncNotedAppOp import android.content.ComponentCallbacks2 import android.content.Context -import android.content.ContextWrapper -import android.content.IntentFilter import android.content.res.Configuration import android.database.SQLException import android.database.sqlite.SQLiteException -import android.net.ConnectivityManager import android.net.http.HttpResponseCache import android.os.Build import android.os.Build.VERSION_CODES @@ -67,7 +64,7 @@ import org.wordpress.android.fluxc.store.StatsStore import org.wordpress.android.fluxc.tools.FluxCImageLoader import org.wordpress.android.fluxc.utils.ErrorUtils.OnUnexpectedError import org.wordpress.android.modules.APPLICATION_SCOPE -import org.wordpress.android.networking.ConnectionChangeReceiver +import org.wordpress.android.networking.NetworkConnectionMonitor import org.wordpress.android.networking.OAuthAuthenticator import org.wordpress.android.networking.RestClientUtils import org.wordpress.android.push.GCMRegistrationScheduler @@ -92,7 +89,6 @@ import org.wordpress.android.ui.uploads.UploadService import org.wordpress.android.ui.uploads.UploadStarter import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T -import org.wordpress.android.util.AppLog.T.MAIN import org.wordpress.android.util.AppThemeUtils import org.wordpress.android.util.BitmapLruCache import org.wordpress.android.util.BuildConfigWrapper @@ -148,6 +144,9 @@ class AppInitializer @Inject constructor( @Inject lateinit var uploadStarter: UploadStarter + @Inject + lateinit var networkConnectionMonitor: NetworkConnectionMonitor + @Inject lateinit var statsWidgetUpdaters: StatsWidgetUpdaters @@ -339,6 +338,11 @@ class AppInitializer @Inject constructor( // Make the UploadStarter observe the app process so it can auto-start uploads uploadStarter.activateAutoUploading(ProcessLifecycleOwner.get() as ProcessLifecycleOwner) + // Monitor default-network connectivity for the whole process lifetime. Uses a NetworkCallback on a + // background thread instead of the deprecated CONNECTIVITY_ACTION broadcast, so it can't cause a + // background ANR. start() is idempotent, so it's safe if init() runs more than once. + networkConnectionMonitor.start(application) + initAnalytics(SystemClock.elapsedRealtime() - startDate) updateNotificationSettings() @@ -792,7 +796,6 @@ class AppInitializer @Inject constructor( inner class ApplicationLifecycleMonitor { private var lastPingDate: Date? = null private var applicationOpenedDate: Date? = null - private var connectionReceiverRegistered = false var firstActivityResumed = true private val isPushNotificationPingNeeded: Boolean @@ -845,18 +848,6 @@ class AppInitializer @Inject constructor( AnalyticsTracker.track(Stat.APPLICATION_CLOSED, properties) AnalyticsTracker.endSession(false) - // Methods onAppComesFromBackground and onAppGoesToBackground are only workarounds to track when the app - // goes to or comes from background. The workarounds are not 100% reliable, so avoid unregistering the - // receiver twice. - if (connectionReceiverRegistered) { - connectionReceiverRegistered = false - try { - application.unregisterReceiver(ConnectionChangeReceiver.getInstance()) - AppLog.d(MAIN, "ConnectionChangeReceiver successfully unregistered") - } catch (e: IllegalArgumentException) { - AppLog.e(MAIN, "ConnectionChangeReceiver was already unregistered") - } - } // Disable the widgets if needed disableWidgetReceiversIfNeeded() @@ -876,25 +867,6 @@ class AppInitializer @Inject constructor( } WordPress.appIsInTheBackground = false - // https://developer.android.com/reference/android/net/ConnectivityManager.html - // Apps targeting Android 7.0 (API level 24) and higher do not receive this broadcast if the broadcast - // receiver is declared in their manifest. Apps will still receive broadcasts if BroadcastReceiver is - // registered with Context.registerReceiver() and that context is still valid. - if (!connectionReceiverRegistered) { - connectionReceiverRegistered = true - if (Build.VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE) { - application.registerReceiver( - ConnectionChangeReceiver.getInstance(), - IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION), - ContextWrapper.RECEIVER_EXPORTED - ) - } else { - application.registerReceiver( - ConnectionChangeReceiver.getInstance(), - IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) - ) - } - } AnalyticsUtils.refreshMetadata(accountStore, siteStore) applicationOpenedDate = Date() // This stat is part of a funnel that provides critical information. Before making ANY modification to this diff --git a/WordPress/src/main/java/org/wordpress/android/networking/ConnectionChangeReceiver.java b/WordPress/src/main/java/org/wordpress/android/networking/ConnectionChangeReceiver.java deleted file mode 100644 index e70f019062b8..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/networking/ConnectionChangeReceiver.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.wordpress.android.networking; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -import org.greenrobot.eventbus.EventBus; -import org.wordpress.android.util.AppLog; -import org.wordpress.android.util.AppLog.T; -import org.wordpress.android.util.NetworkUtils; - -/* - * global network connection change receiver - declared in the manifest to monitor - * android.net.conn.CONNECTIVITY_CHANGE - */ -public class ConnectionChangeReceiver extends BroadcastReceiver { - private static boolean mIsFirstReceive = true; - private static boolean mWasConnected = true; - private static ConnectionChangeReceiver sInstance; - - public static class ConnectionChangeEvent { - private final boolean mIsConnected; - - public ConnectionChangeEvent(boolean isConnected) { - mIsConnected = isConnected; - } - - public boolean isConnected() { - return mIsConnected; - } - } - - /* - * note that onReceive occurs when anything about the connection has changed, not just - * when the connection has been lost or restated, so it can happen quite often when the - * user is on the move. for this reason we only fire the event the first time onReceive - * is called, and afterwards only when we know connection availability has changed - */ - @Override - public void onReceive(Context context, Intent intent) { - boolean isConnected = NetworkUtils.isNetworkAvailable(context); - if (mIsFirstReceive || isConnected != mWasConnected) { - postConnectionChangeEvent(isConnected); - } - } - - private static void postConnectionChangeEvent(boolean isConnected) { - AppLog.i(T.UTILS, "Connection status changed, isConnected=" + isConnected); - mWasConnected = isConnected; - mIsFirstReceive = false; - EventBus.getDefault().post(new ConnectionChangeEvent(isConnected)); - } - - public static ConnectionChangeReceiver getInstance() { - if (sInstance == null) { - sInstance = new ConnectionChangeReceiver(); - } - return sInstance; - } - - public static EventBus getEventBus() { - return EventBus.getDefault(); - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/networking/NetworkConnectionMonitor.kt b/WordPress/src/main/java/org/wordpress/android/networking/NetworkConnectionMonitor.kt new file mode 100644 index 000000000000..d32995851773 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/networking/NetworkConnectionMonitor.kt @@ -0,0 +1,84 @@ +package org.wordpress.android.networking + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.os.Handler +import android.os.HandlerThread +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.AppLog.T +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Global monitor for changes to the device's network connectivity. + * + * Uses [ConnectivityManager.NetworkCallback] registered on a background [HandlerThread] rather than the + * deprecated CONNECTIVITY_ACTION broadcast. Because a NetworkCallback is not a broadcast it is not subject to + * background-broadcast ANR timeouts, and all connectivity work runs off the main thread. The connected state + * is exposed as [isConnected]; observers are notified on the main thread via LiveData whenever the connected + * state changes. + * + * Connectivity is tracked as the set of currently-available internet-capable networks rather than a single + * network. During a handover (e.g. Wi-Fi -> cellular) where the replacement is already up, it is added before + * the old one is removed, so the set doesn't empty and the handover isn't misreported as a disconnection; a + * genuine disconnect empties the set and is reported reliably. + */ +@Singleton +class NetworkConnectionMonitor @Inject constructor() { + private var started = false + private var wasConnected = false + private var isFirstCallback = true + private val availableNetworks = mutableSetOf() + + private val _isConnected = MutableLiveData() + val isConnected: LiveData = _isConnected + + @Synchronized + fun start(context: Context) { + if (started) return + val manager = context.applicationContext + .getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return + + val thread = HandlerThread("NetworkConnectionMonitor").apply { start() } + val callback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) = onNetworkAvailable(network) + override fun onLost(network: Network) = onNetworkLost(network) + } + val request = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() + manager.registerNetworkCallback(request, callback, Handler(thread.looper)) + started = true + } + + @VisibleForTesting + internal fun onNetworkAvailable(network: Network) { + availableNetworks.add(network) + onConnectivityChanged(availableNetworks.isNotEmpty()) + } + + @VisibleForTesting + internal fun onNetworkLost(network: Network) { + availableNetworks.remove(network) + onConnectivityChanged(availableNetworks.isNotEmpty()) + } + + /** + * Called on the monitor's background thread. Updates [isConnected] on the first callback and whenever the + * connected state actually changes, so observers aren't spammed while connectivity churns. + */ + private fun onConnectivityChanged(isConnected: Boolean) { + if (isFirstCallback || isConnected != wasConnected) { + isFirstCallback = false + wasConnected = isConnected + AppLog.i(T.UTILS, "Connection status changed, isConnected=$isConnected") + _isConnected.postValue(isConnected) + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java index 543665906fee..388077b97b14 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java @@ -62,7 +62,7 @@ import org.wordpress.android.inappupdate.IInAppUpdateManager; import org.wordpress.android.inappupdate.InAppUpdateListener; import org.wordpress.android.ui.accounts.login.LoginAnalyticsListener; -import org.wordpress.android.networking.ConnectionChangeReceiver; +import org.wordpress.android.networking.NetworkConnectionMonitor; import org.wordpress.android.push.GCMMessageHandler; import org.wordpress.android.push.GCMMessageService; import org.wordpress.android.push.GCMRegistrationScheduler; @@ -279,6 +279,8 @@ public class WPMainActivity extends BaseAppCompatActivity implements @Inject WpAppNotifierHandler mWpAppNotifierHandler; + @Inject NetworkConnectionMonitor mNetworkConnectionMonitor; + /* * fragments implement this if their contents can be scrolled, called when user * requests to scroll to the top @@ -414,6 +416,8 @@ && getIntent().getExtras().getBoolean(ARG_CONTINUE_JETPACK_CONNECT, false)) { mDispatcher.register(this); EventBus.getDefault().register(this); + mNetworkConnectionMonitor.isConnected().observe(this, this::updateConnectionBar); + if (authTokenToSet != null) { // Save Token to the AccountStore. This will trigger a onAuthenticationChanged. UpdateTokenPayload payload = new UpdateTokenPayload(authTokenToSet); @@ -1488,12 +1492,6 @@ public void onEventMainThread(NotificationEvents.NotificationsUnseenStatus event if (mBottomNav != null) mBottomNav.showNoteBadge(event.hasUnseenNotes); } - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(ConnectionChangeReceiver.ConnectionChangeEvent event) { - updateConnectionBar(event.isConnected()); - } - private void checkConnection() { updateConnectionBar(NetworkUtils.isNetworkAvailable(this)); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt index 23bad72bf636..bc55d8463056 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt @@ -119,7 +119,7 @@ import org.wordpress.android.fluxc.store.bloggingprompts.BloggingPromptsStore import org.wordpress.android.fluxc.tools.FluxCImageLoader import org.wordpress.android.imageeditor.preview.PreviewImageFragment import org.wordpress.android.imageeditor.preview.PreviewImageFragment.Companion.EditImageData.InputData -import org.wordpress.android.networking.ConnectionChangeReceiver.ConnectionChangeEvent +import org.wordpress.android.networking.NetworkConnectionMonitor import org.wordpress.android.support.ZendeskHelper import org.wordpress.android.ui.ActivityId import org.wordpress.android.ui.ActivityLauncher @@ -412,6 +412,8 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor @Inject lateinit var storageUtilsViewModel: StorageUtilsViewModel @Inject lateinit var editorBloggingPromptsViewModel: EditorBloggingPromptsViewModel @Inject lateinit var editorJetpackSocialViewModel: EditorJetpackSocialViewModel + + @Inject lateinit var networkConnectionMonitor: NetworkConnectionMonitor private lateinit var editPostNavigationViewModel: EditPostNavigationViewModel private lateinit var editPostSettingsViewModel: EditPostSettingsViewModel private lateinit var prepublishingViewModel: PrepublishingViewModel @@ -558,6 +560,9 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor } editorMedia.start(siteModel, this) startObserving() + networkConnectionMonitor.isConnected.observe(this) { connected -> + (editorFragment as? GutenbergNetworkConnectionListener)?.onConnectionStatusChange(connected) + } editorFragment?.let { hasSetPostContent = true it.setImageLoader(imageLoader) @@ -3858,11 +3863,6 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor } } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: ConnectionChangeEvent) { - (editorFragment as? GutenbergNetworkConnectionListener)?.onConnectionStatusChange(event.isConnected) - } - private fun refreshEditorTheme() { val payload = FetchEditorThemePayload(siteModel, gssEnabled = true) dispatcher.dispatch(EditorThemeActionBuilder.newFetchEditorThemeAction(payload)) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/BlogPreferencesActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/BlogPreferencesActivity.java index 1573d8dd2960..d6ed50a4377d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/BlogPreferencesActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/BlogPreferencesActivity.java @@ -10,7 +10,6 @@ import androidx.appcompat.widget.Toolbar; import org.apache.commons.text.StringEscapeUtils; -import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.wordpress.android.R; @@ -22,7 +21,7 @@ import org.wordpress.android.fluxc.store.SiteStore.OnSiteChanged; import org.wordpress.android.fluxc.store.SiteStore.OnSiteDeleted; import org.wordpress.android.fluxc.store.SiteStore.OnSiteRemoved; -import org.wordpress.android.networking.ConnectionChangeReceiver; +import org.wordpress.android.networking.NetworkConnectionMonitor; import org.wordpress.android.ui.main.BaseAppCompatActivity; import org.wordpress.android.util.SiteUtils; import org.wordpress.android.util.ToastUtils; @@ -42,6 +41,9 @@ public class BlogPreferencesActivity extends BaseAppCompatActivity { @Inject AccountStore mAccountStore; @Inject SiteStore mSiteStore; @Inject Dispatcher mDispatcher; + @Inject NetworkConnectionMonitor mNetworkConnectionMonitor; + + private boolean mLastConnected; @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -84,19 +86,19 @@ public void onCreate(@Nullable Bundle savedInstanceState) { .replace(R.id.fragment_container, siteSettingsFragment, KEY_SETTINGS_FRAGMENT) .commit(); } + + mNetworkConnectionMonitor.isConnected().observe(this, this::onConnectionChanged); } @Override protected void onStart() { super.onStart(); - EventBus.getDefault().register(this); mDispatcher.register(this); } @Override protected void onStop() { mDispatcher.unregister(this); - EventBus.getDefault().unregister(this); super.onStop(); } @@ -117,21 +119,22 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) { return super.onOptionsItemSelected(item); } - @SuppressWarnings("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(ConnectionChangeReceiver.ConnectionChangeEvent event) { + private void onConnectionChanged(boolean connected) { SiteSettingsFragment siteSettingsFragment = getSettingsFragment(); if (siteSettingsFragment != null) { - if (!event.isConnected()) { + // Toast only on a genuine transition to disconnected, so LiveData replay on re-foregrounding + // doesn't spuriously toast. + if (!connected && mLastConnected) { ToastUtils.showToast(this, getString(R.string.site_settings_disconnected_toast), Duration.LONG); } - siteSettingsFragment.setEditingEnabled(event.isConnected()); + siteSettingsFragment.setEditingEnabled(connected); // TODO: add this back when delete blog is back // https://github.com/wordpress-mobile/WordPress-Android/commit/6a90e3fe46e24ee40abdc4a7f8f0db06f157900c // Checks for stats widgets that were synched with a blog that could be gone now. // StatsWidgetProvider.updateWidgetsOnLogout(this); } + mLastConnected = connected; } @SuppressWarnings("unused") diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.kt index 399f7a270220..534f9b35f295 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.kt @@ -69,7 +69,7 @@ import org.wordpress.android.models.ReaderPost import org.wordpress.android.models.ReaderPostDiscoverData import org.wordpress.android.models.ReaderTag import org.wordpress.android.models.ReaderTagType -import org.wordpress.android.networking.ConnectionChangeReceiver.ConnectionChangeEvent +import org.wordpress.android.networking.NetworkConnectionMonitor import org.wordpress.android.ui.ActionableEmptyView import org.wordpress.android.ui.ActivityLauncher import org.wordpress.android.ui.EmptyViewMessageType @@ -205,6 +205,13 @@ class ReaderPostListFragment : ViewPagerFragment(), OnPostSelectedListener, OnFo @Inject lateinit var displayUtilsWrapper: DisplayUtilsWrapper + @Inject + lateinit var networkConnectionMonitor: NetworkConnectionMonitor + + // Initialised to true so the first LiveData replay of "connected" (e.g. on a freshly restored fragment + // with an active search) doesn't re-fire the search; only a genuine offline->online transition should. + private var lastConnected = true + private var readerPostAdapter: ReaderPostAdapter? = null private var siteSearchAdapter: ReaderSiteSearchAdapter? = null private var suggestionAdapter: ReaderSearchSuggestionAdapter? = null @@ -572,6 +579,15 @@ class ReaderPostListFragment : ViewPagerFragment(), OnPostSelectedListener, OnFo } } } + + networkConnectionMonitor.isConnected.observe(viewLifecycleOwner) { connected -> + // Only resubmit the search when connectivity actually transitions to connected, so LiveData replay + // on re-foregrounding doesn't re-fire the search. + if (connected && !lastConnected) { + currentSearchQuery?.let { submitSearchQuery(it) } + } + lastConnected = connected + } } private fun toggleJetpackBannerIfEnabled(showIfEnabled: Boolean, animateOnScroll: Boolean) { @@ -1370,15 +1386,6 @@ class ReaderPostListFragment : ViewPagerFragment(), OnPostSelectedListener, OnFo dispatcher.dispatch(ReaderActionBuilder.newReaderSearchSitesAction(payload)) } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: ConnectionChangeEvent) { - if (event.isConnected) { - if (currentSearchQuery != null) { - submitSearchQuery(currentSearchQuery!!) - } - } - } - private fun submitSearchQuery(query: String) { if (!isAdded) { return diff --git a/WordPress/src/main/java/org/wordpress/android/ui/suggestion/SuggestionActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/SuggestionActivity.kt index 1a93d896e43f..5e019477c475 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/suggestion/SuggestionActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/SuggestionActivity.kt @@ -10,13 +10,11 @@ import android.text.TextWatcher import android.view.KeyEvent import android.view.inputmethod.EditorInfo import androidx.activity.addCallback -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.databinding.SuggestUsersActivityBinding import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.networking.ConnectionChangeReceiver.ConnectionChangeEvent +import org.wordpress.android.networking.NetworkConnectionMonitor import org.wordpress.android.ui.main.BaseAppCompatActivity import org.wordpress.android.ui.suggestion.FinishAttempt.NotExactlyOneAvailable import org.wordpress.android.ui.suggestion.FinishAttempt.OnlyOneAvailable @@ -35,8 +33,13 @@ class SuggestionActivity : BaseAppCompatActivity() { @Inject lateinit var viewModel: SuggestionViewModel + + @Inject + lateinit var networkConnectionMonitor: NetworkConnectionMonitor private lateinit var binding: SuggestUsersActivityBinding + private var lastConnected = true + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (application as WordPress).component().inject(this) @@ -177,6 +180,16 @@ class SuggestionActivity : BaseAppCompatActivity() { updateEmptyView() }) + + networkConnectionMonitor.isConnected.observe(this) { connected -> + // Only refresh on a genuine offline->online transition; the initial LiveData replay would + // otherwise trigger a redundant fetch on top of the one already started in viewModel.init(). + if (connected && !lastConnected) { + viewModel.onConnectionChanged(connected) + } + lastConnected = connected + updateEmptyView() + } } private fun exitIfOnlyOneMatchingUser() { @@ -249,23 +262,11 @@ class SuggestionActivity : BaseAppCompatActivity() { override fun onResume() { super.onResume() - EventBus.getDefault().register(this) if (binding.autocompleteText.isAttachedToWindow) { binding.autocompleteText.showDropDown() } } - override fun onPause() { - EventBus.getDefault().unregister(this) - super.onPause() - } - - @Subscribe - fun onEventMainThread(event: ConnectionChangeEvent) { - viewModel.onConnectionChanged(event) - updateEmptyView() - } - companion object { const val SELECTED_VALUE = "SELECTED_VALUE" diff --git a/WordPress/src/main/java/org/wordpress/android/ui/suggestion/SuggestionViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/SuggestionViewModel.kt index c59b4257ef43..0999f70c4a43 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/suggestion/SuggestionViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/SuggestionViewModel.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.ViewModel import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.networking.ConnectionChangeReceiver.ConnectionChangeEvent import org.wordpress.android.ui.suggestion.FinishAttempt.NotExactlyOneAvailable import org.wordpress.android.ui.suggestion.FinishAttempt.OnlyOneAvailable import org.wordpress.android.ui.suggestion.SuggestionType.Users @@ -59,8 +58,8 @@ class SuggestionViewModel @Inject constructor( private fun supportsSuggestions(site: SiteModel): Boolean = SiteUtils.isAccessedViaWPComRest(site) - fun onConnectionChanged(event: ConnectionChangeEvent) { - if (event.isConnected) { + fun onConnectionChanged(isConnected: Boolean) { + if (isConnected) { suggestionSource.refreshSuggestions() } } diff --git a/WordPress/src/test/java/org/wordpress/android/networking/NetworkConnectionMonitorTest.kt b/WordPress/src/test/java/org/wordpress/android/networking/NetworkConnectionMonitorTest.kt new file mode 100644 index 000000000000..a340a4524e3d --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/networking/NetworkConnectionMonitorTest.kt @@ -0,0 +1,73 @@ +package org.wordpress.android.networking + +import android.net.Network +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.mock +import org.wordpress.android.BaseUnitTest + +@ExperimentalCoroutinesApi +class NetworkConnectionMonitorTest : BaseUnitTest() { + private lateinit var monitor: NetworkConnectionMonitor + private val emissions = mutableListOf() + + @Before + fun setUp() { + monitor = NetworkConnectionMonitor() + monitor.isConnected.observeForever { emissions.add(it) } + } + + @Test + fun `emits connected when the first network becomes available`() { + monitor.onNetworkAvailable(mock()) + + assertThat(monitor.isConnected.value).isTrue + assertThat(emissions).containsExactly(true) + } + + @Test + fun `emits disconnected when the only network is lost`() { + val network = mock() + monitor.onNetworkAvailable(network) + monitor.onNetworkLost(network) + + assertThat(monitor.isConnected.value).isFalse + assertThat(emissions).containsExactly(true, false) + } + + @Test + fun `does not report a disconnection during a network handover`() { + val wifi = mock() + val cellular = mock() + + monitor.onNetworkAvailable(wifi) + // the replacement network arrives before the old one is torn down + monitor.onNetworkAvailable(cellular) + monitor.onNetworkLost(wifi) + + assertThat(monitor.isConnected.value).isTrue + // only the initial connected emission; the handover is not a change + assertThat(emissions).containsExactly(true) + } + + @Test + fun `does not emit again while the connected state is unchanged`() { + monitor.onNetworkAvailable(mock()) + monitor.onNetworkAvailable(mock()) + + assertThat(emissions).containsExactly(true) + } + + @Test + fun `emits again when reconnecting after a full disconnect`() { + val first = mock() + monitor.onNetworkAvailable(first) + monitor.onNetworkLost(first) + monitor.onNetworkAvailable(mock()) + + assertThat(monitor.isConnected.value).isTrue + assertThat(emissions).containsExactly(true, false, true) + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/suggestion/SuggestionViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/suggestion/SuggestionViewModelTest.kt index cc3eb2667444..fc4b6137233b 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/suggestion/SuggestionViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/suggestion/SuggestionViewModelTest.kt @@ -18,7 +18,6 @@ import org.mockito.kotlin.whenever import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.networking.ConnectionChangeReceiver.ConnectionChangeEvent import org.wordpress.android.ui.suggestion.FinishAttempt.NotExactlyOneAvailable import org.wordpress.android.ui.suggestion.FinishAttempt.OnlyOneAvailable import org.wordpress.android.ui.suggestion.SuggestionType.Users @@ -88,14 +87,14 @@ class SuggestionViewModelTest { @Test fun `onConnectionChanged not connected`() { initViewModel() - viewModel.onConnectionChanged(ConnectionChangeEvent(false)) + viewModel.onConnectionChanged(false) verify(mockSuggestionSource, never()).refreshSuggestions() } @Test fun `onConnectionChanged connected`() { initViewModel() - viewModel.onConnectionChanged(ConnectionChangeEvent(true)) + viewModel.onConnectionChanged(true) verify(mockSuggestionSource).refreshSuggestions() } diff --git a/gradle.properties b/gradle.properties index 3487d45aafa4..853c22d90f77 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,3 +6,6 @@ org.gradle.configuration-cache=true # Dependency Analysis Plugin dependency.analysis.android.ignored.variants=release,wordpressRelease,jetpackRelease dependency.analysis.test.analysis=false + +# Enabled parallel sync for Gradle 9.4+ +org.gradle.tooling.parallel=true