diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index 874f049aa4dc96a5d4cba194e405f474c432973c..c4d95fceabed9f5e7ae9f5c3ad18de7e0f191b82 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -40,8 +40,10 @@ import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.data.fusedDownload.models.FusedDownload import foundation.e.apps.data.login.AuthObject +import foundation.e.apps.data.login.LoginSourceGPlay import foundation.e.apps.data.login.LoginViewModel import foundation.e.apps.data.login.exceptions.GPlayValidationException +import foundation.e.apps.data.preference.PreferenceManagerModule import foundation.e.apps.databinding.ActivityMainBinding import foundation.e.apps.install.updates.UpdatesNotifier import foundation.e.apps.ui.MainActivityViewModel @@ -52,10 +54,12 @@ import foundation.e.apps.ui.setup.signin.SignInViewModel import foundation.e.apps.utils.SystemInfoProvider import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus +import javax.inject.Inject import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch +import timber.log.Timber @AndroidEntryPoint class MainActivity : AppCompatActivity() { @@ -65,6 +69,9 @@ class MainActivity : AppCompatActivity() { private val TAG = MainActivity::class.java.simpleName private lateinit var viewModel: MainActivityViewModel + @Inject + lateinit var preferenceManagerModule: PreferenceManagerModule + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -130,6 +137,13 @@ class MainActivity : AppCompatActivity() { email, SystemInfoProvider.getAppBuildInfo() ) + } else if (exception != null) { + Timber.e(exception, "Login failed! message: ${exception?.localizedMessage}") + ApplicationDialogFragment( + title = getString(R.string.sign_in_failed_title), + message = getString(R.string.sign_in_failed_desc), + positiveButtonText = getString(R.string.ok) + ).show(supportFragmentManager, TAG) } } } @@ -207,6 +221,10 @@ class MainActivity : AppCompatActivity() { observeInvalidAuth() } + launch { + observeTooManyRequests() + } + launch { observeSignatureMissMatchError() } @@ -277,9 +295,25 @@ class MainActivity : AppCompatActivity() { }.distinctUntilChanged { old, new -> ((old.data is String) && (new.data is String) && old.data == new.data) }.collectLatest { - val data = it.data as String - if (data.isNotBlank()) { - loginViewModel.markInvalidAuthObject(data) + validatedAuthObject(it) + } + } + + private fun validatedAuthObject(appEvent: AppEvent) { + val data = appEvent.data as String + if (data.isNotBlank()) { + loginViewModel.markInvalidAuthObject(data) + } + } + + private suspend fun observeTooManyRequests() { + EventBus.events.filter { appEvent -> + appEvent is AppEvent.TooManyRequests + }.collectLatest { + binding.sessionErrorLayout.visibility = View.VISIBLE + binding.retrySessionButton.setOnClickListener { + binding.sessionErrorLayout.visibility = View.GONE + loginViewModel.startLoginFlow(listOf(LoginSourceGPlay::class.java.simpleName)) } } } diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt index cb4f227a6c36a8b94cc2a86cd10e01b117043c53..cb398a78c124c0e8ff6e03156570ac61a1ac0aae 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt @@ -280,7 +280,6 @@ class FusedApiImpl @Inject constructor( packageSpecificResults ) } - return finalSearchResult } @@ -427,7 +426,7 @@ class FusedApiImpl @Inject constructor( ): FusedApp? { try { getApplicationDetails(query, query, authData, Origin.GPLAY).let { - if (it.second == ResultStatus.OK) { + if (it.second == ResultStatus.OK && it.first.package_name.isNotEmpty()) { return it.first } } diff --git a/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt b/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt index 9a30a62a97e7c58b70460ab9ab9133652fe2bfa5..b502cd75672054783c1c4d63c6a76f4889ba570a 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt @@ -45,7 +45,7 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject class GPlayHttpClient @Inject constructor( - cache: Cache, + private val cache: Cache, ) : IHttpClient { private val POST = "POST" @@ -55,6 +55,9 @@ class GPlayHttpClient @Inject constructor( private const val TAG = "GPlayHttpClient" private const val HTTP_TIMEOUT_IN_SECOND = 10L private const val SEARCH = "search" + private const val SEARCH_SUGGEST = "searchSuggest" + private const val STATUS_CODE_UNAUTHORIZED = 401 + private const val STATUS_CODE_TOO_MANY_REQUESTS = 429 } private val okHttpClient = OkHttpClient().newBuilder() @@ -155,9 +158,11 @@ class GPlayHttpClient @Inject constructor( } private fun processRequest(request: Request): PlayResponse { + var response: Response? = null return try { val call = okHttpClient.newCall(request) - buildPlayResponse(call.execute()) + response = call.execute() + buildPlayResponse(response) } catch (e: Exception) { // TODO: exception will be thrown for all apis when all gplay api implementation // will handle the exceptions. this will be done in following issue. @@ -171,6 +176,8 @@ class GPlayHttpClient @Inject constructor( is SocketTimeoutException -> handleExceptionOnGooglePlayRequest(e) else -> handleExceptionOnGooglePlayRequest(e) } + } finally { + response?.close() } } @@ -195,12 +202,23 @@ class GPlayHttpClient @Inject constructor( code = response.code Timber.d("$TAG: Url: ${response.request.url}\nStatus: $code") - if (code == 401) { - MainScope().launch { + when (code) { + STATUS_CODE_UNAUTHORIZED -> MainScope().launch { EventBus.invokeEvent( AppEvent.InvalidAuthEvent(AuthObject.GPlayAuth::class.java.simpleName) ) } + + STATUS_CODE_TOO_MANY_REQUESTS -> MainScope().launch { + cache.evictAll() + if (response.request.url.toString().contains(SEARCH_SUGGEST)) { + return@launch + } + + EventBus.invokeEvent( + AppEvent.TooManyRequests() + ) + } } // TODO: exception will be thrown for all apis when all gplay api implementation diff --git a/app/src/main/java/foundation/e/apps/data/login/LoginViewModel.kt b/app/src/main/java/foundation/e/apps/data/login/LoginViewModel.kt index 25c54362e6912ddeb2618291cd1908ce94b35b01..3fbf6c10e96266a7cd879cc7d072565b8479476a 100644 --- a/app/src/main/java/foundation/e/apps/data/login/LoginViewModel.kt +++ b/app/src/main/java/foundation/e/apps/data/login/LoginViewModel.kt @@ -25,6 +25,7 @@ import foundation.e.apps.data.enums.User import foundation.e.apps.ui.parentFragment.LoadingViewModel import kotlinx.coroutines.launch import javax.inject.Inject +import okhttp3.Cache /** * ViewModel to handle all login related operations. @@ -33,6 +34,7 @@ import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( private val loginSourceRepository: LoginSourceRepository, + private val cache: Cache, ) : ViewModel() { /** @@ -120,6 +122,7 @@ class LoginViewModel @Inject constructor( } authObjects.postValue(authObjectsLocal) + cache.evictAll() } /** @@ -127,6 +130,7 @@ class LoginViewModel @Inject constructor( */ fun logout() { viewModelScope.launch { + cache.evictAll() loginSourceRepository.logout() authObjects.postValue(listOf()) } diff --git a/app/src/main/java/foundation/e/apps/data/preference/PreferenceManagerModule.kt b/app/src/main/java/foundation/e/apps/data/preference/PreferenceManagerModule.kt index 428daf1955cb2dd649994aa0e2ed396a8db548a0..0dba23101785c5ca3d386dcd728fad1494b18466 100644 --- a/app/src/main/java/foundation/e/apps/data/preference/PreferenceManagerModule.kt +++ b/app/src/main/java/foundation/e/apps/data/preference/PreferenceManagerModule.kt @@ -52,6 +52,8 @@ class PreferenceManagerModule @Inject constructor( fun isPWASelected() = preferenceManager.getBoolean(PREFERENCE_SHOW_PWA, true) fun isGplaySelected() = preferenceManager.getBoolean(PREFERENCE_SHOW_GPLAY, true) + fun disableGplay() = preferenceManager.edit().putBoolean(PREFERENCE_SHOW_GPLAY, false).apply() + fun autoUpdatePreferred(): Boolean { return preferenceManager.getBoolean("updateInstallAuto", false) } diff --git a/app/src/main/java/foundation/e/apps/ui/application/subFrags/ApplicationDialogFragment.kt b/app/src/main/java/foundation/e/apps/ui/application/subFrags/ApplicationDialogFragment.kt index f13657eeb353573748e69d0253977715e298dfb3..c5702ec9dea6965629e85f1373c4f84c4ba4bc2b 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/subFrags/ApplicationDialogFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/subFrags/ApplicationDialogFragment.kt @@ -19,6 +19,7 @@ package foundation.e.apps.ui.application.subFrags import android.app.Dialog +import android.content.DialogInterface import android.os.Bundle import android.text.Html import android.text.SpannableString @@ -41,6 +42,8 @@ class ApplicationDialogFragment() : DialogFragment() { private var positiveButtonAction: (() -> Unit)? = null private var cancelButtonText: String? = null private var cancelButtonAction: (() -> Unit)? = null + private var cancellable: Boolean = true + private var onDismissListener: (() -> Unit)? = null constructor( drawable: Int = -1, @@ -50,6 +53,8 @@ class ApplicationDialogFragment() : DialogFragment() { positiveButtonAction: (() -> Unit)? = null, cancelButtonText: String = "", cancelButtonAction: (() -> Unit)? = null, + cancellable: Boolean = true, + onDismissListener: (() -> Unit)? = null, ) : this() { this.drawable = drawable this.title = title @@ -58,6 +63,8 @@ class ApplicationDialogFragment() : DialogFragment() { this.positiveButtonAction = positiveButtonAction this.cancelButtonText = cancelButtonText this.cancelButtonAction = cancelButtonAction + this.cancellable = cancellable + this.onDismissListener = onDismissListener } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -70,6 +77,7 @@ class ApplicationDialogFragment() : DialogFragment() { positiveButtonAction?.invoke() this.dismiss() } + .setCancelable(cancellable) if (cancelButtonText?.isNotEmpty() == true) { materialAlertDialogBuilder.setNegativeButton(cancelButtonText) { _, _ -> cancelButtonAction?.invoke() @@ -91,6 +99,11 @@ class ApplicationDialogFragment() : DialogFragment() { } } + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + onDismissListener?.invoke() + } + private fun TextView.removeUnderlineFromLinks() { val spannable = SpannableString(text) for (urlSpan in spannable.getSpans(0, spannable.length, URLSpan::class.java)) { diff --git a/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt b/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt index 5173da6fecf6a5efdda1fd3fb097fcb0df58dd8a..1557c14dd7f41d3a142a01bf678045260361602f 100644 --- a/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt @@ -54,6 +54,10 @@ import timber.log.Timber */ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { + companion object { + private const val STATUS_TOO_MANY_REQUESTS = "Status: 429" + } + val loginViewModel: LoginViewModel by lazy { ViewModelProvider(requireActivity())[LoginViewModel::class.java] } @@ -353,7 +357,6 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { * is shown to the user. */ fun showDataLoadError(exception: Exception) { - val dialogView = DialogErrorLogBinding.inflate(requireActivity().layoutInflater) dialogView.apply { moreInfo.setOnClickListener { @@ -406,6 +409,10 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { } val unknownSourceException = exceptions.find { it is UnknownSourceException } + if (gPlayException?.message?.contains(STATUS_TOO_MANY_REQUESTS) == true) { + return + } + /* * Take caution altering the cases. * Cases to be defined from most restrictive to least restrictive. diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index 6ecc4c0a34c982a17314675133ebe98e5ad0f398..90e07f32bf5620d06ce6f2f7a8406ff1b8fb276d 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -42,6 +42,7 @@ import timber.log.Timber import javax.inject.Inject import kotlin.coroutines.coroutineContext + @HiltViewModel class SearchViewModel @Inject constructor( private val fusedAPIRepository: FusedAPIRepository, @@ -51,8 +52,7 @@ class SearchViewModel @Inject constructor( val searchResult: MutableLiveData, Boolean>>> = MutableLiveData() - private var searchResultLiveData: LiveData, Boolean>>> = - MutableLiveData() + private var lastAuthObjects: List? = null private var nextSubBundle: Set? = null @@ -159,8 +159,8 @@ class SearchViewModel @Inject constructor( val isFirstFetch = nextSubBundle == null nextSubBundle = gplaySearchResult.data?.second - // if first page has less data, then fetch next page data without waiting for users' scroll - if (isFirstFetch && gplaySearchResult.data?.first?.size!! < 4) { + //first page has less data, then fetch next page data without waiting for users' scroll + if (isFirstFetch) { CoroutineScope(coroutineContext).launch { fetchGplayData(query) } diff --git a/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt b/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt index 1074bbdbbf0e927b341bfd2f8b0f9fc8db9d2d89..f7fbf663aa65ce9bef55f94a50169c90eff09064 100644 --- a/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt +++ b/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt @@ -32,4 +32,5 @@ sealed class AppEvent(val data: Any) { class ErrorMessageEvent(stringResourceId: Int) : AppEvent(stringResourceId) class AppPurchaseEvent(fusedDownload: FusedDownload) : AppEvent(fusedDownload) class NoInternetEvent(isInternetAvailable: Boolean) : AppEvent(isInternetAvailable) + class TooManyRequests : AppEvent(Unit) } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 99610ac8388a1c755549b52314d28545de85114b..38aa95c20784c4c023752a397061b3b5881d6580 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -23,6 +23,56 @@ android:layout_height="match_parent" tools:context=".MainActivity"> + + + + + + + + + + + Open Settings More info + + The anonymous account you\'re currently using is unavailable. Please refresh the session to get another one. + REFRESH SESSION + Error occurred while loading apps. This can be because server is not responding or other server error.\n\nPlease retry to try again, or try later.