diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index e9a8adfb505916bce94b435db6c3af15456a63bd..dd3b075e295074e8697f30254a05fe537f0de78f 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -56,6 +56,7 @@ import foundation.e.apps.utils.eventBus.EventBus import foundation.e.apps.utils.exceptions.GPlayValidationException import foundation.e.apps.utils.modules.CommonUtilsFunctions import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import timber.log.Timber @@ -130,7 +131,10 @@ class MainActivity : AppCompatActivity() { viewModel.gPlayAuthData = data as AuthData } else if (exception is GPlayValidationException) { val email = otherPayload.toString() - viewModel.uploadFaultyTokenToEcloud(email, CommonUtilsFunctions.getAppBuildInfo()) + viewModel.uploadFaultyTokenToEcloud( + email, + CommonUtilsFunctions.getAppBuildInfo() + ) } } } @@ -213,6 +217,8 @@ class MainActivity : AppCompatActivity() { viewModel.updateAppWarningList() lifecycleScope.launchWhenResumed { + observeInvalidAuth() + EventBus.events.filter { appEvent -> appEvent is AppEvent.SignatureMissMatchError }.collectLatest { @@ -226,6 +232,19 @@ class MainActivity : AppCompatActivity() { } } + private suspend fun observeInvalidAuth() { + EventBus.events.filter { appEvent -> + appEvent is AppEvent.InvalidAuthEvent + }.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) + } + } + } + private fun setupBottomNavItemSelectedListener( bottomNavigationView: BottomNavigationView, navHostFragment: NavHostFragment, diff --git a/app/src/main/java/foundation/e/apps/api/gplay/utils/GPlayHttpClient.kt b/app/src/main/java/foundation/e/apps/api/gplay/utils/GPlayHttpClient.kt index 6725e07b1d197d83b151cab6064545fc5227581b..9876fa4d1fa3c46b39fe846cfd1b4b4d9897b1b9 100644 --- a/app/src/main/java/foundation/e/apps/api/gplay/utils/GPlayHttpClient.kt +++ b/app/src/main/java/foundation/e/apps/api/gplay/utils/GPlayHttpClient.kt @@ -21,7 +21,12 @@ package foundation.e.apps.api.gplay.utils import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.network.IHttpClient +import foundation.e.apps.login.AuthObject +import foundation.e.apps.utils.eventBus.AppEvent +import foundation.e.apps.utils.eventBus.EventBus import foundation.e.apps.utils.modules.CommonUtilsFunctions +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch import okhttp3.Cache import okhttp3.Headers.Companion.toHeaders import okhttp3.HttpUrl @@ -39,7 +44,7 @@ import java.net.UnknownHostException import javax.inject.Inject class GPlayHttpClient @Inject constructor( - cache: Cache + cache: Cache, ) : IHttpClient { private val POST = "POST" @@ -178,6 +183,16 @@ class GPlayHttpClient @Inject constructor( isSuccessful = response.isSuccessful code = response.code + Timber.d("$TAG: Url: ${response.request.url}\nStatus: $code") + + if (code == 401) { + MainScope().launch { + EventBus.invokeEvent( + AppEvent.InvalidAuthEvent(AuthObject.GPlayAuth::class.java.simpleName) + ) + } + } + if (response.body != null) { responseBytes = response.body!!.bytes() } diff --git a/app/src/main/java/foundation/e/apps/di/LoginModule.kt b/app/src/main/java/foundation/e/apps/di/LoginModule.kt index 559d7e84da66af3c455937cdbf1b4f05fe01cadd..ba12b0f08a626cde26fadbb3a563a86201af4e96 100644 --- a/app/src/main/java/foundation/e/apps/di/LoginModule.kt +++ b/app/src/main/java/foundation/e/apps/di/LoginModule.kt @@ -28,6 +28,7 @@ import foundation.e.apps.login.LoginSourceInterface @InstallIn(SingletonComponent::class) @Module object LoginModule { + @Provides fun providesLoginSources( gPlay: LoginSourceGPlay, diff --git a/app/src/main/java/foundation/e/apps/login/LoginSourceGPlay.kt b/app/src/main/java/foundation/e/apps/login/LoginSourceGPlay.kt index c48081ddbc68e46e2950bbf62231739b114f2782..b77c57a8b23c28acc641f67b61edf045403e6d38 100644 --- a/app/src/main/java/foundation/e/apps/login/LoginSourceGPlay.kt +++ b/app/src/main/java/foundation/e/apps/login/LoginSourceGPlay.kt @@ -26,6 +26,7 @@ import foundation.e.apps.login.api.GPlayApiFactory import foundation.e.apps.login.api.GPlayLoginInterface import foundation.e.apps.login.api.GoogleLoginApi import foundation.e.apps.login.api.LoginApiRepository +import foundation.e.apps.utils.enums.ResultStatus import foundation.e.apps.utils.enums.User import foundation.e.apps.utils.exceptions.GPlayValidationException import java.util.Locale @@ -86,13 +87,19 @@ class LoginSourceGPlay @Inject constructor( } ) - // validate authData and save it if nothing is saved (first time use.) - validateAuthData(authData).run { - if (isSuccess() && savedAuth == null) { - saveAuthData(authData) - } - return AuthObject.GPlayAuth(this, user) + val formattedAuthData = formatAuthData(authData) + formattedAuthData.locale = locale + val result: ResultSupreme = ResultSupreme.create( + status = ResultStatus.OK, + data = formattedAuthData + ) + result.otherPayload = formattedAuthData.email + + if (savedAuth == null) { + saveAuthData(formattedAuthData) } + + return AuthObject.GPlayAuth(result, user) } override suspend fun clearSavedAuth() { @@ -139,7 +146,7 @@ class LoginSourceGPlay @Inject constructor( * Aurora OSS GPlay API complains of missing headers sometimes. * Converting [authData] to Json and back to [AuthData] fixed it. */ - private fun formattedAuthData(authData: AuthData): AuthData { + private fun formatAuthData(authData: AuthData): AuthData { val localAuthDataJson = gson.toJson(authData) return gson.fromJson(localAuthDataJson, AuthData::class.java) } @@ -149,7 +156,7 @@ class LoginSourceGPlay @Inject constructor( */ private suspend fun getAuthData(): ResultSupreme { return loginApiRepository.fetchAuthData("", "", locale).run { - if (isSuccess()) ResultSupreme.Success(formattedAuthData(this.data!!)) + if (isSuccess()) ResultSupreme.Success(formatAuthData(this.data!!)) else this } } @@ -200,7 +207,7 @@ class LoginSourceGPlay @Inject constructor( */ loginDataStore.saveAasToken(aasTokenFetched) return loginApiRepository.fetchAuthData(email, aasTokenFetched, locale).run { - if (isSuccess()) ResultSupreme.Success(formattedAuthData(this.data!!)) + if (isSuccess()) ResultSupreme.Success(formatAuthData(this.data!!)) else this } } @@ -214,7 +221,7 @@ class LoginSourceGPlay @Inject constructor( authData: AuthData, ): ResultSupreme { - val formattedAuthData = formattedAuthData(authData) + val formattedAuthData = formatAuthData(authData) formattedAuthData.locale = locale val validityResponse = loginApiRepository.login(formattedAuthData) diff --git a/app/src/main/java/foundation/e/apps/login/LoginViewModel.kt b/app/src/main/java/foundation/e/apps/login/LoginViewModel.kt index d4bee02b9f24e53aaac3bac9b6e434d240c10007..6cb9b6bb5783226f61e0c805f491991ccf5ebed8 100644 --- a/app/src/main/java/foundation/e/apps/login/LoginViewModel.kt +++ b/app/src/main/java/foundation/e/apps/login/LoginViewModel.kt @@ -20,8 +20,13 @@ package foundation.e.apps.login import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.aurora.gplayapi.data.models.AuthData import dagger.hilt.android.lifecycle.HiltViewModel +import foundation.e.apps.api.ResultSupreme import foundation.e.apps.utils.enums.User +import foundation.e.apps.utils.exceptions.CleanApkException +import foundation.e.apps.utils.exceptions.GPlayValidationException +import foundation.e.apps.utils.parentFragment.LoadingViewModel import kotlinx.coroutines.launch import javax.inject.Inject @@ -99,6 +104,65 @@ class LoginViewModel @Inject constructor( } } + /** + * Once an AuthObject is marked as invalid, it will be refreshed + * automatically by LoadingViewModel. + * If GPlay auth is invalid, [LoadingViewModel.onLoadData] has a retry block, + * this block will clear existing GPlay AuthData and freshly start the login flow. + */ + fun markInvalidAuthObject(authObjectName: String) { + val authObjectsLocal = authObjects.value?.toMutableList() + val invalidObject = authObjectsLocal?.find { it::class.java.simpleName == authObjectName } + + val replacedObject = when (invalidObject) { + is AuthObject.GPlayAuth -> { + createInvalidGplayAuth(invalidObject) + } + is AuthObject.CleanApk -> + createInvalidCleanApkAuth(invalidObject) + else -> null + } + + authObjectsLocal?.apply { + if (invalidObject != null && replacedObject != null) { + remove(invalidObject) + add(replacedObject) + } + } + + authObjects.postValue(authObjectsLocal) + } + + private fun createInvalidCleanApkAuth(invalidObject: AuthObject.CleanApk) = + AuthObject.CleanApk( + ResultSupreme.Error( + message = "Unauthorized", + exception = CleanApkException( + isTimeout = false, + message = "Unauthorized", + ) + ), + invalidObject.user, + ) + + private fun createInvalidGplayAuth(invalidObject: AuthObject.GPlayAuth): AuthObject.GPlayAuth { + val message = "Validating AuthData failed.\nNetwork code: 401" + + return AuthObject.GPlayAuth( + ResultSupreme.Error( + message = message, + exception = GPlayValidationException( + message, + invalidObject.user, + 401, + ) + ).apply { + otherPayload = invalidObject.result.otherPayload + }, + invalidObject.user, + ) + } + /** * Clears all saved data and logs out the user to the sign in screen. */ 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 797f708c0b8336ce5f6044df05d602d3b9208142..1509729b2a1997d207f49d2b0d0a9c1d0ebf5de9 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 @@ -26,4 +26,6 @@ import foundation.e.apps.utils.enums.ResultStatus sealed class AppEvent(val data: Any) { class SignatureMissMatchError(packageName: String) : AppEvent(packageName) class UpdateEvent(result: ResultSupreme.WorkError) : AppEvent(result) + + class InvalidAuthEvent(authName: String) : AppEvent(authName) }