diff --git a/app/build.gradle b/app/build.gradle index 35ed7284f4e9e71b6f3821778e90f7ab6fc92cd9..b37cd54b52508f2de040189343c696b1e4a8bbcc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -229,9 +229,6 @@ dependencies { // JSoup implementation 'org.jsoup:jsoup:1.13.1' - // Flow reactive network - implementation 'ru.beryukhov:flowreactivenetwork:1.0.4' - // elib implementation 'foundation.e:elib:0.0.1-alpha11' } diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index e8d578f918e2004ba57aa52ec7dafeb1457a2756..67efd606d0b8c73967cec33124aaf5f3b83729d8 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -104,6 +104,8 @@ class MainActivity : AppCompatActivity() { } } + viewModel.setupConnectivityManager(this) + viewModel.internetConnection.observe(this) { isInternetAvailable -> hasInternet = isInternetAvailable if (isInternetAvailable) { @@ -205,7 +207,7 @@ class MainActivity : AppCompatActivity() { } } - if (!CommonUtilsModule.isNetworkAvailable(this)) { + if (viewModel.internetConnection.value != true) { showNoInternet() } diff --git a/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt b/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt index 03ae34b07e1f16e4c986999cc24feea3d0bf09b4..901d86692e6323930589e03a99219c7a0bc4a94d 100644 --- a/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt @@ -21,6 +21,10 @@ package foundation.e.apps import android.content.Context import android.content.Intent import android.graphics.Bitmap +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest import android.os.Build import android.util.Base64 import android.widget.ImageView @@ -31,7 +35,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData -import androidx.lifecycle.liveData +import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.exceptions.ApiException @@ -49,15 +53,15 @@ import foundation.e.apps.utils.enums.Type import foundation.e.apps.utils.enums.User import foundation.e.apps.utils.enums.isInitialized import foundation.e.apps.utils.enums.isUnFiltered +import foundation.e.apps.utils.modules.CommonUtilsModule import foundation.e.apps.utils.modules.DataStoreModule import foundation.e.apps.utils.modules.PWAManagerModule -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import ru.beryukhov.reactivenetwork.ReactiveNetwork -import ru.beryukhov.reactivenetwork.internet.observing.InternetObservingSettings -import ru.beryukhov.reactivenetwork.internet.observing.strategy.SocketInternetObservingStrategy import java.io.ByteArrayOutputStream import javax.inject.Inject +import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.launch @HiltViewModel class MainActivityViewModel @Inject constructor( @@ -88,9 +92,10 @@ class MainActivityViewModel @Inject constructor( private val _errorMessageStringResource = MutableLiveData() val errorMessageStringResource: LiveData = _errorMessageStringResource + lateinit var connectivityManager: ConnectivityManager + companion object { private const val TAG = "MainActivityViewModel" - private var isGoogleLoginRunning = false } fun getUser(): User { @@ -332,15 +337,68 @@ class MainActivityViewModel @Inject constructor( return Base64.encodeToString(byteArrayOS.toByteArray(), Base64.DEFAULT) } - val internetConnection = liveData { - emitSource( - ReactiveNetwork().observeInternetConnectivity( - InternetObservingSettings.builder() - .host("http://204.ecloud.global") - .strategy(SocketInternetObservingStrategy()) - .build() - ).asLiveData(Dispatchers.Default) - ) + fun setupConnectivityManager(context: Context) { + connectivityManager = + context.getSystemService(ConnectivityManager::class.java) as ConnectivityManager + } + + val internetConnection = + callbackFlow { + if (!this@MainActivityViewModel::connectivityManager.isInitialized) { + awaitClose { } + return@callbackFlow + } + + sendInternetStatus(connectivityManager) + val networkCallback = getNetworkCallback(this) + connectivityManager.registerNetworkCallback(networkRequest, networkCallback) + + awaitClose { + connectivityManager.unregisterNetworkCallback(networkCallback) + } + }.asLiveData().distinctUntilChanged() + + private val networkRequest = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .build() + + private fun getNetworkCallback( + callbackFlowScope: ProducerScope, + ): ConnectivityManager.NetworkCallback { + return object : ConnectivityManager.NetworkCallback() { + + override fun onAvailable(network: Network) { + super.onAvailable(network) + callbackFlowScope.sendInternetStatus(connectivityManager) + } + + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + super.onCapabilitiesChanged(network, networkCapabilities) + callbackFlowScope.sendInternetStatus(connectivityManager) + } + + override fun onLost(network: Network) { + super.onLost(network) + callbackFlowScope.sendInternetStatus(connectivityManager) + } + } + } + + private fun ProducerScope.sendInternetStatus(connectivityManager: ConnectivityManager) { + + val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + + val hasInternet = + capabilities != null + && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + + trySend(hasInternet) } fun updateStatusOfFusedApps( diff --git a/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt index 6802c84c7d70503b130cbefcb81f200a4632f629..12a9f3bd5ce0e309d2b0724de3887b5742213f83 100644 --- a/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt +++ b/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt @@ -119,7 +119,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { private val applicationViewModel: ApplicationViewModel by viewModels() private val privacyInfoViewModel: PrivacyInfoViewModel by viewModels() private val appInfoFetchViewModel: AppInfoFetchViewModel by viewModels() - private val mainActivityViewModel: MainActivityViewModel by activityViewModels() + override val mainActivityViewModel: MainActivityViewModel by activityViewModels() private var applicationIcon: ImageView? = null @@ -139,7 +139,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { authObjects.observe(viewLifecycleOwner) { if (it == null) return@observe - loadData(it) + loadDataWhenNetworkAvailable(it) } applicationViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { diff --git a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt index 186d7b61753beaba5f3370b54747f5759d65c411..881919e63ff4fd1ed30675e7ec12f70a62990b97 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt @@ -67,7 +67,7 @@ class ApplicationListFragment : private val viewModel: ApplicationListViewModel by viewModels() private val privacyInfoViewModel: PrivacyInfoViewModel by viewModels() private val appInfoFetchViewModel: AppInfoFetchViewModel by viewModels() - private val mainActivityViewModel: MainActivityViewModel by activityViewModels() + override val mainActivityViewModel: MainActivityViewModel by activityViewModels() private val appProgressViewModel: AppProgressViewModel by viewModels() private var _binding: FragmentApplicationListBinding? = null @@ -87,7 +87,7 @@ class ApplicationListFragment : authObjects.observe(viewLifecycleOwner) { if (it == null) return@observe - loadData(it) + loadDataWhenNetworkAvailable(it) } viewModel.exceptionsLiveData.observe(viewLifecycleOwner) { diff --git a/app/src/main/java/foundation/e/apps/categories/AppsFragment.kt b/app/src/main/java/foundation/e/apps/categories/AppsFragment.kt index 07497a782c24b47466508aff1f9050b777ca38ec..bf34cab5c5c2d1793301b72a11bc69fdd9d3ddcf 100644 --- a/app/src/main/java/foundation/e/apps/categories/AppsFragment.kt +++ b/app/src/main/java/foundation/e/apps/categories/AppsFragment.kt @@ -40,7 +40,7 @@ class AppsFragment : TimeoutFragment(R.layout.fragment_apps) { private val binding get() = _binding!! private val categoriesViewModel: CategoriesViewModel by viewModels() - private val mainActivityViewModel: MainActivityViewModel by activityViewModels() + override val mainActivityViewModel: MainActivityViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -50,7 +50,7 @@ class AppsFragment : TimeoutFragment(R.layout.fragment_apps) { authObjects.observe(viewLifecycleOwner) { if (it == null) return@observe - loadData(it) + loadDataWhenNetworkAvailable(it) } categoriesViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { diff --git a/app/src/main/java/foundation/e/apps/categories/GamesFragment.kt b/app/src/main/java/foundation/e/apps/categories/GamesFragment.kt index f5d9fe62bc4a5cdc1d93fbb5417a0bc7a28038be..a7c2614e2d7b82bb447917be26dd3a1a64e98633 100644 --- a/app/src/main/java/foundation/e/apps/categories/GamesFragment.kt +++ b/app/src/main/java/foundation/e/apps/categories/GamesFragment.kt @@ -40,7 +40,7 @@ class GamesFragment : TimeoutFragment(R.layout.fragment_games) { private val binding get() = _binding!! private val categoriesViewModel: CategoriesViewModel by viewModels() - private val mainActivityViewModel: MainActivityViewModel by activityViewModels() + override val mainActivityViewModel: MainActivityViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -50,7 +50,7 @@ class GamesFragment : TimeoutFragment(R.layout.fragment_games) { authObjects.observe(viewLifecycleOwner) { if (it == null) return@observe - loadData(it) + loadDataWhenNetworkAvailable(it) } categoriesViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { diff --git a/app/src/main/java/foundation/e/apps/home/HomeFragment.kt b/app/src/main/java/foundation/e/apps/home/HomeFragment.kt index 61b941c3cd76f4208bb8b46fed14f88ecfd78b1d..ab8e94b952a50e1651eab339bf0b4ee081a57721 100644 --- a/app/src/main/java/foundation/e/apps/home/HomeFragment.kt +++ b/app/src/main/java/foundation/e/apps/home/HomeFragment.kt @@ -64,7 +64,7 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface private val binding get() = _binding!! private val homeViewModel: HomeViewModel by viewModels() - private val mainActivityViewModel: MainActivityViewModel by activityViewModels() + override val mainActivityViewModel: MainActivityViewModel by activityViewModels() private val appProgressViewModel: AppProgressViewModel by viewModels() private val appInfoFetchViewModel: AppInfoFetchViewModel by viewModels() @@ -116,7 +116,7 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface authObjects.observe(viewLifecycleOwner) { if (it == null) return@observe - loadData(it) + loadDataWhenNetworkAvailable(it) } homeViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { diff --git a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt index dec405a72d8579c693786d5cc2bd5f8d12ee566d..f89298ed140a4211a9269cd9d60ca9c40b8582de 100644 --- a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt @@ -78,7 +78,7 @@ class SearchFragment : private val searchViewModel: SearchViewModel by viewModels() private val privacyInfoViewModel: PrivacyInfoViewModel by viewModels() private val appInfoFetchViewModel: AppInfoFetchViewModel by viewModels() - private val mainActivityViewModel: MainActivityViewModel by activityViewModels() + override val mainActivityViewModel: MainActivityViewModel by activityViewModels() private val appProgressViewModel: AppProgressViewModel by viewModels() private val SUGGESTION_KEY = "suggestion" @@ -119,7 +119,7 @@ class SearchFragment : authObjects.observe(viewLifecycleOwner) { val currentQuery = searchView?.query?.toString() ?: "" if (it == null || (currentQuery.isNotEmpty() && lastSearch == currentQuery)) return@observe - loadData(it) + loadDataWhenNetworkAvailable(it) } searchViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { diff --git a/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt b/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt index eeb5d87747ef61d05248c57c7bf7a0f9fdf56ee6..b40ecd8a7b683538084e21225805cc37e1451cc4 100644 --- a/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt +++ b/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt @@ -77,7 +77,7 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte private val updatesViewModel: UpdatesViewModel by viewModels() private val privacyInfoViewModel: PrivacyInfoViewModel by viewModels() private val appInfoFetchViewModel: AppInfoFetchViewModel by viewModels() - private val mainActivityViewModel: MainActivityViewModel by activityViewModels() + override val mainActivityViewModel: MainActivityViewModel by activityViewModels() private val appProgressViewModel: AppProgressViewModel by viewModels() private var isDownloadObserverAdded = false @@ -94,7 +94,7 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte if (!updatesViewModel.updatesList.value?.first.isNullOrEmpty()) { return@observe } - loadData(it) + loadDataWhenNetworkAvailable(it) } updatesViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { diff --git a/app/src/main/java/foundation/e/apps/utils/parentFragment/TimeoutFragment.kt b/app/src/main/java/foundation/e/apps/utils/parentFragment/TimeoutFragment.kt index 0744b390edcb67910994dd4134a62994ce78f6cf..870e5e9a13c6a981f217dfc29d0d63785b930020 100644 --- a/app/src/main/java/foundation/e/apps/utils/parentFragment/TimeoutFragment.kt +++ b/app/src/main/java/foundation/e/apps/utils/parentFragment/TimeoutFragment.kt @@ -21,8 +21,12 @@ import androidx.annotation.LayoutRes import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider +import foundation.e.apps.MainActivityViewModel import foundation.e.apps.R import foundation.e.apps.databinding.DialogErrorLogBinding import foundation.e.apps.login.AuthObject @@ -34,6 +38,7 @@ import foundation.e.apps.utils.exceptions.GPlayException import foundation.e.apps.utils.exceptions.GPlayLoginException import foundation.e.apps.utils.exceptions.GPlayValidationException import foundation.e.apps.utils.exceptions.UnknownSourceException +import timber.log.Timber /** * Parent class of all fragments. @@ -49,18 +54,61 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { ViewModelProvider(requireActivity())[LoginViewModel::class.java] } + abstract val mainActivityViewModel: MainActivityViewModel + /** * Fragments observe this list to load data. * Fragments should not observe [loginViewModel]'s authObjects. */ val authObjects: MutableLiveData?> = MutableLiveData() + /** + * Function to loadData using the fragment's viewmodel. + */ abstract fun loadData(authObjectList: List) abstract fun showLoadingUI() abstract fun stopLoadingUI() + /** + * Call this function instead of directly calling [loadData]. + * This function takes care of checking network availability. + */ + fun loadDataWhenNetworkAvailable(authObjectList: List) { + val hasInternet = mainActivityViewModel.internetConnection.value + Timber.d("class name: ${this::class.simpleName} internet: $hasInternet") + if (hasInternet == true) { + loadData(authObjectList) + } else { + mainActivityViewModel.internetConnection.loadDataOnce(this) { + if (it) { + if (authObjectList.any { !it.result.isSuccess() }) { + Timber.d("Refreshing authObjects failed due to unavailable network") + loginViewModel.startLoginFlow() + } else { + loadData(authObjectList) + } + } + } + } + } + + /** + * This function will help prevent loading data multiple times if network + * is disconnected and reconnected multiple times. + */ + private fun LiveData.loadDataOnce(lifecycleOwner: LifecycleOwner, observer: Observer) { + observe(lifecycleOwner, object : Observer { + override fun onChanged(t: Boolean) { + observer.onChanged(t) + if (t) { + removeObserver(this) + } + } + }) + } + /** * Override to contain code to execute in case of timeout. * Do not call this function directly, use [showTimeout] for that. @@ -117,7 +165,7 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { * 1. Dialog title set to [R.string.data_load_error]. * 2. Dialog content set to [R.string.data_load_error_desc]. * 3. Dialog can show technical error info on clicking "More Info" - * 4. Has a positive button "Retry" which calls [loadData]. + * 4. Has a positive button "Retry" which calls [loadDataWhenNetworkAvailable]. * 5. Has a negative button "Close" which just closes the dialog. * 6. Dialog is cancellable. */ @@ -298,7 +346,7 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { setView(dialogView.root) setPositiveButton(R.string.retry) { _, _ -> showLoadingUI() - authObjects.value?.let { loadData(it) } + authObjects.value?.let { loadDataWhenNetworkAvailable(it) } } setNegativeButton(R.string.close, null) setCancelable(true)