Loading app/src/main/java/foundation/e/apps/MainActivity.kt +0 −14 Original line number Diff line number Diff line Loading @@ -25,7 +25,6 @@ import android.os.Environment import android.os.StatFs import android.os.storage.StorageManager import android.view.View import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.distinctUntilChanged Loading Loading @@ -337,19 +336,6 @@ class MainActivity : AppCompatActivity() { } } private fun onSignInError() { AlertDialog.Builder(this).apply { setTitle(R.string.sign_in_failed_title) setMessage(R.string.sign_in_failed_desc) setPositiveButton(R.string.retry) { _, _ -> viewModel.retryFetchingTokenAfterTimeout() } setNegativeButton(R.string.logout) { _, _ -> viewModel.postFalseAuthValidity() } }.show() } private fun getAvailableInternalMemorySize(): Long { val path: File = Environment.getDataDirectory() val stat = StatFs(path.path) Loading app/src/main/java/foundation/e/apps/MainActivityViewModel.kt +3 −288 Original line number Diff line number Diff line Loading @@ -22,7 +22,6 @@ import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.os.Build import android.os.SystemClock import android.util.Base64 import android.widget.ImageView import androidx.annotation.RequiresApi Loading @@ -35,40 +34,31 @@ import androidx.lifecycle.asLiveData import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.exceptions.ApiException import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.api.cleanapk.blockedApps.BlockedAppRepository import foundation.e.apps.api.ecloud.EcloudRepository import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.api.gplay.utils.AC2DMTask import foundation.e.apps.api.gplay.utils.AC2DMUtil import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.fused.FusedManagerRepository import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.utils.Constants.timeoutDurationInMillis import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.Status 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.NETWORK_CODE_SUCCESS import foundation.e.apps.utils.modules.DataStoreModule import foundation.e.apps.utils.modules.PWAManagerModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ru.beryukhov.reactivenetwork.ReactiveNetwork import timber.log.Timber import java.io.ByteArrayOutputStream import javax.inject.Inject @HiltViewModel class MainActivityViewModel @Inject constructor( private val gson: Gson, private val dataStoreModule: DataStoreModule, private val fusedAPIRepository: FusedAPIRepository, private val fusedManagerRepository: FusedManagerRepository, Loading @@ -76,40 +66,17 @@ class MainActivityViewModel @Inject constructor( private val pwaManagerModule: PWAManagerModule, private val ecloudRepository: EcloudRepository, private val blockedAppRepository: BlockedAppRepository, private val aC2DMTask: AC2DMTask, ) : ViewModel() { val authDataJson: LiveData<String> = dataStoreModule.authData.asLiveData() val tocStatus: LiveData<Boolean> = dataStoreModule.tocStatus.asLiveData() val userType: LiveData<String> = dataStoreModule.userType.asLiveData() private var _authData: MutableLiveData<AuthData> = MutableLiveData() val authData: LiveData<AuthData> = _authData val authValidity: MutableLiveData<Boolean> = MutableLiveData() private val _purchaseAppLiveData: MutableLiveData<FusedDownload> = MutableLiveData() val purchaseAppLiveData: LiveData<FusedDownload> = _purchaseAppLiveData val isAppPurchased: MutableLiveData<String> = MutableLiveData() val purchaseDeclined: MutableLiveData<String> = MutableLiveData() var authRequestRunning = false var gPlayAuthData = AuthData("", "") /* * If this live data is populated, it means Google sign in failed. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709 */ val errorAuthResponse = MutableLiveData<PlayResponse>() /* * Store the time when auth data is fetched for the first time. * If we try to fetch auth data after timeout, then don't allow it. * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404 */ var firstAuthDataFetchTime = 0L var isTokenValidationCompletedOnce = false // Downloads val downloadList = fusedManagerRepository.getDownloadLiveList() var installInProgress = false Loading @@ -118,9 +85,6 @@ class MainActivityViewModel @Inject constructor( private val _errorMessageStringResource = MutableLiveData<Int>() val errorMessageStringResource: LiveData<Int> = _errorMessageStringResource /* * Authentication related functions */ companion object { private const val TAG = "MainActivityViewModel" Loading @@ -135,261 +99,12 @@ class MainActivityViewModel @Inject constructor( return dataStoreModule.getEmail() } private fun setFirstTokenFetchTime() { if (firstAuthDataFetchTime == 0L) { firstAuthDataFetchTime = SystemClock.uptimeMillis() } } private fun isTimeEligibleForTokenRefresh(): Boolean { return (SystemClock.uptimeMillis() - firstAuthDataFetchTime) <= timeoutDurationInMillis } /* * This method resets the last recorded token fetch time. * Then it posts authValidity as false. This causes the observer in MainActivity to destroyCredentials * and fetch new token. * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404 */ fun retryFetchingTokenAfterTimeout() { firstAuthDataFetchTime = 0 setFirstTokenFetchTime() if (isUserTypeGoogle()) { /* * Change done to show sign in error dialog for Google login. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709 */ if (authDataJson.value.isNullOrEmpty()) { generateAuthDataBasedOnUserType(User.GOOGLE.name) } else { validateAuthData() } } else { postFalseAuthValidity() } } fun uploadFaultyTokenToEcloud(email: String, description: String = "") { viewModelScope.launch { ecloudRepository.uploadFaultyEmail(email, description) } } fun getAuthData() { if (!authRequestRunning) { authRequestRunning = true viewModelScope.launch { /* * If getting auth data failed, try getting again. * Sending false in authValidity, triggers observer in MainActivity, * causing it to destroy credentials and try to regenerate auth data. * * Issue: * https://gitlab.e.foundation/e/backlog/-/issues/5413 * https://gitlab.e.foundation/e/backlog/-/issues/5404 */ if (!fusedAPIRepository.fetchAuthData()) { authRequestRunning = false postFalseAuthValidity() } } } } fun updateAuthData(authData: AuthData) { _authData.value = authData } fun destroyCredentials(regenerateFunction: ((user: String) -> Unit)?) { viewModelScope.launch { /* * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5168 * * Now destroyCredentials() no longer removes the user type from data store. * (i.e. Google login or Anonymous). * - If the type is User.ANONYMOUS then we do not prompt the user to login again, * we directly generate new auth data; which is the main Gitlab issue described above. * - If not anonymous user, i.e. type is User.GOOGLE, in that case we clear * the USERTYPE value. This causes HomeFragment.onTosAccepted() to open * SignInFragment as we need fresh login from the user. */ dataStoreModule.destroyCredentials() if (regenerateFunction != null) { dataStoreModule.userType.collect { user -> if (!user.isBlank() && User.valueOf(user) == User.ANONYMOUS) { Timber.d("Regenerating auth data for Anonymous user") regenerateFunction(user) } else { Timber.d("Ask Google user to log in again") dataStoreModule.clearUserType() } } } } } fun generateAuthData() { val data = jsonToAuthData() _authData.value = data } private fun jsonToAuthData() = gson.fromJson(authDataJson.value, AuthData::class.java) fun validateAuthData() { viewModelScope.launch { jsonToAuthData()?.let { val validityResponse = getAuthValidityResponse(it) if (isUserTypeGoogle() && validityResponse.code != NETWORK_CODE_SUCCESS) { /* * Change done to show sign in error dialog for Google login. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709 */ errorAuthResponse.postValue(validityResponse) } else { authValidity.postValue(validityResponse.isSuccessful) } authRequestRunning = false } } } // Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709 fun isUserTypeGoogle(): Boolean { return userType.value == User.GOOGLE.name } /** * Useful to destroy credentials. */ fun postFalseAuthValidity() { authValidity.postValue(false) } fun handleAuthDataJson() { val user = userType.value val json = authDataJson.value if (user == null || json == null) { return } Timber.d(">>> handleAuthDataJson: internet: ${internetConnection.value}") if (!isUserLoggedIn(user, json)) { generateAuthDataBasedOnUserType(user) } else if (isEligibleToValidateJson(json) && internetConnection.value == true) { validateAuthData() Timber.d(">>> Authentication data is available!") } } private fun isUserLoggedIn(user: String, json: String) = user.isNotEmpty() && !user.contentEquals(User.UNAVAILABLE.name) && json.isNotEmpty() private fun isEligibleToValidateJson(authDataJson: String?) = !authDataJson.isNullOrEmpty() && !userType.value.isNullOrEmpty() && !userType.value.contentEquals( User.UNAVAILABLE.name ) && authValidity.value != true fun handleAuthValidity(isValid: Boolean, handleTimeoOut: () -> Unit) { if (isGoogleLoginRunning) { return } isTokenValidationCompletedOnce = true if (isValid) { Timber.d("Authentication data is valid!") generateAuthData() return } Timber.d(">>> Authentication data validation failed!") destroyCredentials { user -> if (isTimeEligibleForTokenRefresh()) { generateAuthDataBasedOnUserType(user) } else { handleTimeoOut() } } } private fun generateAuthDataBasedOnUserType(user: String) { if (user.isEmpty() || tocStatus.value == false || isGoogleLoginRunning) { return } when (User.valueOf(user)) { User.ANONYMOUS -> { if (authDataJson.value.isNullOrEmpty() && !authRequestRunning) { Timber.d(">>> Fetching new authentication data") setFirstTokenFetchTime() getAuthData() } } User.UNAVAILABLE -> { destroyCredentials(null) } User.GOOGLE -> { if (authData.value == null && !authRequestRunning) { Timber.d(">>> Fetching new authentication data") setFirstTokenFetchTime() doFetchAuthData() } } } } private suspend fun doFetchAuthData(email: String, oauthToken: String) { var responseMap: Map<String, String> withContext(Dispatchers.IO) { val response = aC2DMTask.getAC2DMResponse(email, oauthToken) responseMap = if (response.isSuccessful) { AC2DMUtil.parseResponse(String(response.responseBytes)) } else { mapOf() } if (isUserTypeGoogle() && response.code != NETWORK_CODE_SUCCESS) { /* * For google login, the email and aasToken gets stored when * we login through the webview, but that does not mean we have a valid authData. * * For first login, control flow is as below: * In MainActivity, from userType observer -> handleAuthDataJson * -> generateAuthDataBasedOnUserType -> doFetchAuthData -> this function * * If for first google login, google sign in portal was available * but android.clients.google.com is unreachable, then responseMap is blank. * * We see validateAuthData is never called (which had a check for incorrect response) * Hence we have to check the response code is NETWORK_CODE_SUCCESS (200) or not * and show the Google sign in failed dialog. * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709 */ errorAuthResponse.postValue(response) return@withContext } responseMap["Token"]?.let { if (fusedAPIRepository.fetchAuthData(email, it) == null) { dataStoreModule.clearUserType() _errorMessageStringResource.value = R.string.unknown_error } } } } private fun doFetchAuthData() { viewModelScope.launch { isGoogleLoginRunning = true val email = dataStoreModule.getEmail() val oauthToken = dataStoreModule.getAASToken() if (email.isNotEmpty() && oauthToken.isNotEmpty()) { doFetchAuthData(email, oauthToken) } isGoogleLoginRunning = false } } private suspend fun getAuthValidityResponse(authData: AuthData): PlayResponse { return fusedAPIRepository.validateAuthData(authData) } /* * Notification functions */ Loading @@ -416,7 +131,7 @@ class MainActivityViewModel @Inject constructor( * Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/266 */ fun shouldShowPaidAppsSnackBar(app: FusedApp): Boolean { if (!app.isFree && authData.value?.isAnonymous == true) { if (!app.isFree && gPlayAuthData.isAnonymous) { _errorMessageStringResource.value = R.string.paid_app_anonymous_message return true } Loading Loading @@ -553,7 +268,7 @@ class MainActivityViewModel @Inject constructor( suspend fun updateAwaitingForPurchasedApp(packageName: String): FusedDownload? { val fusedDownload = fusedManagerRepository.getFusedDownload(packageName = packageName) authData.value?.let { gPlayAuthData.let { if (!it.isAnonymous) { try { fusedAPIRepository.updateFusedDownloadWithDownloadingInfo( Loading Loading @@ -594,7 +309,7 @@ class MainActivityViewModel @Inject constructor( fusedDownload: FusedDownload ) { val downloadList = mutableListOf<String>() authData.value?.let { gPlayAuthData.let { if (app.type == Type.PWA) { downloadList.add(app.url) fusedDownload.downloadURLList = downloadList Loading app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +0 −12 Original line number Diff line number Diff line Loading @@ -465,18 +465,6 @@ class FusedAPIImpl @Inject constructor( return gPlayAPIRepository.getSearchSuggestions(query, authData) } suspend fun fetchAuthData(): Boolean { return gPlayAPIRepository.fetchAuthData() } suspend fun fetchAuthData(email: String, aasToken: String): AuthData? { return gPlayAPIRepository.fetchAuthData(email, aasToken) } suspend fun validateAuthData(authData: AuthData): PlayResponse { return gPlayAPIRepository.validateAuthData(authData) } suspend fun getOnDemandModule( authData: AuthData, packageName: String, Loading app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt +1 −19 Original line number Diff line number Diff line Loading @@ -23,7 +23,6 @@ import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.data.models.StreamBundle import com.aurora.gplayapi.data.models.StreamCluster import foundation.e.apps.api.ResultSupreme Loading @@ -39,9 +38,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton class FusedAPIRepository @Inject constructor( private val fusedAPIImpl: FusedAPIImpl ) { class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPIImpl) { var streamBundle = StreamBundle() private set Loading Loading @@ -93,13 +90,6 @@ class FusedAPIRepository @Inject constructor( return fusedAPIImpl.getApplicationCategoryPreference() } suspend fun validateAuthData(authData: AuthData): PlayResponse { if (authData.authToken.isNotEmpty() && authData.deviceInfoProvider != null) { return fusedAPIImpl.validateAuthData(authData) } return PlayResponse() } suspend fun getApplicationDetails( packageNameList: List<String>, authData: AuthData, Loading Loading @@ -165,14 +155,6 @@ class FusedAPIRepository @Inject constructor( return fusedAPIImpl.getSearchSuggestions(query, authData) } suspend fun fetchAuthData(): Boolean { return fusedAPIImpl.fetchAuthData() } suspend fun fetchAuthData(email: String, aasToken: String): AuthData? { return fusedAPIImpl.fetchAuthData(email, aasToken) } fun getSearchResults( query: String, authData: AuthData Loading app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIImpl.kt +1 −55 Original line number Diff line number Diff line Loading @@ -26,7 +26,6 @@ import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category import com.aurora.gplayapi.data.models.File import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.data.models.SearchBundle import com.aurora.gplayapi.data.models.StreamBundle import com.aurora.gplayapi.data.models.StreamCluster Loading @@ -37,66 +36,13 @@ import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.gplayapi.helpers.SearchHelper import com.aurora.gplayapi.helpers.StreamHelper import com.aurora.gplayapi.helpers.TopChartsHelper import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.api.gplay.token.TokenRepository import foundation.e.apps.api.gplay.utils.CustomAuthValidator import foundation.e.apps.api.gplay.utils.GPlayHttpClient import foundation.e.apps.utils.modules.DataStoreModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext import javax.inject.Inject class GPlayAPIImpl @Inject constructor( @ApplicationContext private val context: Context, private val tokenRepository: TokenRepository, private val dataStoreModule: DataStoreModule, private val gPlayHttpClient: GPlayHttpClient ) { /** * Save auth data to preferences. * Updated for network failures. * Issue: * https://gitlab.e.foundation/e/backlog/-/issues/5413 * https://gitlab.e.foundation/e/backlog/-/issues/5404 * * @return true or false based on if the request was successful. */ // TODO: DON'T HARDCODE DISPATCHERS IN ANY METHODS suspend fun fetchAuthData(): Boolean = withContext(Dispatchers.IO) { val data = async { tokenRepository.getAuthData() } data.await().let { if (it == null) return@withContext false it.locale = context.resources.configuration.locales[0] // update locale with the default locale from settings dataStoreModule.saveCredentials(it) return@withContext true } } suspend fun fetchAuthData(email: String, aasToken: String): AuthData? { val authData = tokenRepository.getAuthData(email, aasToken) if (authData.authToken.isNotEmpty() && authData.deviceInfoProvider != null) { dataStoreModule.saveCredentials(authData) return authData } return null } suspend fun validateAuthData(authData: AuthData): PlayResponse { var result = PlayResponse() withContext(Dispatchers.IO) { try { val authValidator = CustomAuthValidator(authData).using(gPlayHttpClient) result = authValidator.getValidityResponse() } catch (e: Exception) { e.printStackTrace() throw e } } return result } class GPlayAPIImpl @Inject constructor(private val gPlayHttpClient: GPlayHttpClient) { suspend fun getSearchSuggestions(query: String, authData: AuthData): List<SearchSuggestEntry> { val searchData = mutableListOf<SearchSuggestEntry>() Loading Loading
app/src/main/java/foundation/e/apps/MainActivity.kt +0 −14 Original line number Diff line number Diff line Loading @@ -25,7 +25,6 @@ import android.os.Environment import android.os.StatFs import android.os.storage.StorageManager import android.view.View import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.distinctUntilChanged Loading Loading @@ -337,19 +336,6 @@ class MainActivity : AppCompatActivity() { } } private fun onSignInError() { AlertDialog.Builder(this).apply { setTitle(R.string.sign_in_failed_title) setMessage(R.string.sign_in_failed_desc) setPositiveButton(R.string.retry) { _, _ -> viewModel.retryFetchingTokenAfterTimeout() } setNegativeButton(R.string.logout) { _, _ -> viewModel.postFalseAuthValidity() } }.show() } private fun getAvailableInternalMemorySize(): Long { val path: File = Environment.getDataDirectory() val stat = StatFs(path.path) Loading
app/src/main/java/foundation/e/apps/MainActivityViewModel.kt +3 −288 Original line number Diff line number Diff line Loading @@ -22,7 +22,6 @@ import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.os.Build import android.os.SystemClock import android.util.Base64 import android.widget.ImageView import androidx.annotation.RequiresApi Loading @@ -35,40 +34,31 @@ import androidx.lifecycle.asLiveData import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.exceptions.ApiException import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.api.cleanapk.blockedApps.BlockedAppRepository import foundation.e.apps.api.ecloud.EcloudRepository import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.api.gplay.utils.AC2DMTask import foundation.e.apps.api.gplay.utils.AC2DMUtil import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.fused.FusedManagerRepository import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.utils.Constants.timeoutDurationInMillis import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.Status 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.NETWORK_CODE_SUCCESS import foundation.e.apps.utils.modules.DataStoreModule import foundation.e.apps.utils.modules.PWAManagerModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ru.beryukhov.reactivenetwork.ReactiveNetwork import timber.log.Timber import java.io.ByteArrayOutputStream import javax.inject.Inject @HiltViewModel class MainActivityViewModel @Inject constructor( private val gson: Gson, private val dataStoreModule: DataStoreModule, private val fusedAPIRepository: FusedAPIRepository, private val fusedManagerRepository: FusedManagerRepository, Loading @@ -76,40 +66,17 @@ class MainActivityViewModel @Inject constructor( private val pwaManagerModule: PWAManagerModule, private val ecloudRepository: EcloudRepository, private val blockedAppRepository: BlockedAppRepository, private val aC2DMTask: AC2DMTask, ) : ViewModel() { val authDataJson: LiveData<String> = dataStoreModule.authData.asLiveData() val tocStatus: LiveData<Boolean> = dataStoreModule.tocStatus.asLiveData() val userType: LiveData<String> = dataStoreModule.userType.asLiveData() private var _authData: MutableLiveData<AuthData> = MutableLiveData() val authData: LiveData<AuthData> = _authData val authValidity: MutableLiveData<Boolean> = MutableLiveData() private val _purchaseAppLiveData: MutableLiveData<FusedDownload> = MutableLiveData() val purchaseAppLiveData: LiveData<FusedDownload> = _purchaseAppLiveData val isAppPurchased: MutableLiveData<String> = MutableLiveData() val purchaseDeclined: MutableLiveData<String> = MutableLiveData() var authRequestRunning = false var gPlayAuthData = AuthData("", "") /* * If this live data is populated, it means Google sign in failed. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709 */ val errorAuthResponse = MutableLiveData<PlayResponse>() /* * Store the time when auth data is fetched for the first time. * If we try to fetch auth data after timeout, then don't allow it. * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404 */ var firstAuthDataFetchTime = 0L var isTokenValidationCompletedOnce = false // Downloads val downloadList = fusedManagerRepository.getDownloadLiveList() var installInProgress = false Loading @@ -118,9 +85,6 @@ class MainActivityViewModel @Inject constructor( private val _errorMessageStringResource = MutableLiveData<Int>() val errorMessageStringResource: LiveData<Int> = _errorMessageStringResource /* * Authentication related functions */ companion object { private const val TAG = "MainActivityViewModel" Loading @@ -135,261 +99,12 @@ class MainActivityViewModel @Inject constructor( return dataStoreModule.getEmail() } private fun setFirstTokenFetchTime() { if (firstAuthDataFetchTime == 0L) { firstAuthDataFetchTime = SystemClock.uptimeMillis() } } private fun isTimeEligibleForTokenRefresh(): Boolean { return (SystemClock.uptimeMillis() - firstAuthDataFetchTime) <= timeoutDurationInMillis } /* * This method resets the last recorded token fetch time. * Then it posts authValidity as false. This causes the observer in MainActivity to destroyCredentials * and fetch new token. * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404 */ fun retryFetchingTokenAfterTimeout() { firstAuthDataFetchTime = 0 setFirstTokenFetchTime() if (isUserTypeGoogle()) { /* * Change done to show sign in error dialog for Google login. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709 */ if (authDataJson.value.isNullOrEmpty()) { generateAuthDataBasedOnUserType(User.GOOGLE.name) } else { validateAuthData() } } else { postFalseAuthValidity() } } fun uploadFaultyTokenToEcloud(email: String, description: String = "") { viewModelScope.launch { ecloudRepository.uploadFaultyEmail(email, description) } } fun getAuthData() { if (!authRequestRunning) { authRequestRunning = true viewModelScope.launch { /* * If getting auth data failed, try getting again. * Sending false in authValidity, triggers observer in MainActivity, * causing it to destroy credentials and try to regenerate auth data. * * Issue: * https://gitlab.e.foundation/e/backlog/-/issues/5413 * https://gitlab.e.foundation/e/backlog/-/issues/5404 */ if (!fusedAPIRepository.fetchAuthData()) { authRequestRunning = false postFalseAuthValidity() } } } } fun updateAuthData(authData: AuthData) { _authData.value = authData } fun destroyCredentials(regenerateFunction: ((user: String) -> Unit)?) { viewModelScope.launch { /* * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5168 * * Now destroyCredentials() no longer removes the user type from data store. * (i.e. Google login or Anonymous). * - If the type is User.ANONYMOUS then we do not prompt the user to login again, * we directly generate new auth data; which is the main Gitlab issue described above. * - If not anonymous user, i.e. type is User.GOOGLE, in that case we clear * the USERTYPE value. This causes HomeFragment.onTosAccepted() to open * SignInFragment as we need fresh login from the user. */ dataStoreModule.destroyCredentials() if (regenerateFunction != null) { dataStoreModule.userType.collect { user -> if (!user.isBlank() && User.valueOf(user) == User.ANONYMOUS) { Timber.d("Regenerating auth data for Anonymous user") regenerateFunction(user) } else { Timber.d("Ask Google user to log in again") dataStoreModule.clearUserType() } } } } } fun generateAuthData() { val data = jsonToAuthData() _authData.value = data } private fun jsonToAuthData() = gson.fromJson(authDataJson.value, AuthData::class.java) fun validateAuthData() { viewModelScope.launch { jsonToAuthData()?.let { val validityResponse = getAuthValidityResponse(it) if (isUserTypeGoogle() && validityResponse.code != NETWORK_CODE_SUCCESS) { /* * Change done to show sign in error dialog for Google login. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709 */ errorAuthResponse.postValue(validityResponse) } else { authValidity.postValue(validityResponse.isSuccessful) } authRequestRunning = false } } } // Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709 fun isUserTypeGoogle(): Boolean { return userType.value == User.GOOGLE.name } /** * Useful to destroy credentials. */ fun postFalseAuthValidity() { authValidity.postValue(false) } fun handleAuthDataJson() { val user = userType.value val json = authDataJson.value if (user == null || json == null) { return } Timber.d(">>> handleAuthDataJson: internet: ${internetConnection.value}") if (!isUserLoggedIn(user, json)) { generateAuthDataBasedOnUserType(user) } else if (isEligibleToValidateJson(json) && internetConnection.value == true) { validateAuthData() Timber.d(">>> Authentication data is available!") } } private fun isUserLoggedIn(user: String, json: String) = user.isNotEmpty() && !user.contentEquals(User.UNAVAILABLE.name) && json.isNotEmpty() private fun isEligibleToValidateJson(authDataJson: String?) = !authDataJson.isNullOrEmpty() && !userType.value.isNullOrEmpty() && !userType.value.contentEquals( User.UNAVAILABLE.name ) && authValidity.value != true fun handleAuthValidity(isValid: Boolean, handleTimeoOut: () -> Unit) { if (isGoogleLoginRunning) { return } isTokenValidationCompletedOnce = true if (isValid) { Timber.d("Authentication data is valid!") generateAuthData() return } Timber.d(">>> Authentication data validation failed!") destroyCredentials { user -> if (isTimeEligibleForTokenRefresh()) { generateAuthDataBasedOnUserType(user) } else { handleTimeoOut() } } } private fun generateAuthDataBasedOnUserType(user: String) { if (user.isEmpty() || tocStatus.value == false || isGoogleLoginRunning) { return } when (User.valueOf(user)) { User.ANONYMOUS -> { if (authDataJson.value.isNullOrEmpty() && !authRequestRunning) { Timber.d(">>> Fetching new authentication data") setFirstTokenFetchTime() getAuthData() } } User.UNAVAILABLE -> { destroyCredentials(null) } User.GOOGLE -> { if (authData.value == null && !authRequestRunning) { Timber.d(">>> Fetching new authentication data") setFirstTokenFetchTime() doFetchAuthData() } } } } private suspend fun doFetchAuthData(email: String, oauthToken: String) { var responseMap: Map<String, String> withContext(Dispatchers.IO) { val response = aC2DMTask.getAC2DMResponse(email, oauthToken) responseMap = if (response.isSuccessful) { AC2DMUtil.parseResponse(String(response.responseBytes)) } else { mapOf() } if (isUserTypeGoogle() && response.code != NETWORK_CODE_SUCCESS) { /* * For google login, the email and aasToken gets stored when * we login through the webview, but that does not mean we have a valid authData. * * For first login, control flow is as below: * In MainActivity, from userType observer -> handleAuthDataJson * -> generateAuthDataBasedOnUserType -> doFetchAuthData -> this function * * If for first google login, google sign in portal was available * but android.clients.google.com is unreachable, then responseMap is blank. * * We see validateAuthData is never called (which had a check for incorrect response) * Hence we have to check the response code is NETWORK_CODE_SUCCESS (200) or not * and show the Google sign in failed dialog. * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709 */ errorAuthResponse.postValue(response) return@withContext } responseMap["Token"]?.let { if (fusedAPIRepository.fetchAuthData(email, it) == null) { dataStoreModule.clearUserType() _errorMessageStringResource.value = R.string.unknown_error } } } } private fun doFetchAuthData() { viewModelScope.launch { isGoogleLoginRunning = true val email = dataStoreModule.getEmail() val oauthToken = dataStoreModule.getAASToken() if (email.isNotEmpty() && oauthToken.isNotEmpty()) { doFetchAuthData(email, oauthToken) } isGoogleLoginRunning = false } } private suspend fun getAuthValidityResponse(authData: AuthData): PlayResponse { return fusedAPIRepository.validateAuthData(authData) } /* * Notification functions */ Loading @@ -416,7 +131,7 @@ class MainActivityViewModel @Inject constructor( * Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/266 */ fun shouldShowPaidAppsSnackBar(app: FusedApp): Boolean { if (!app.isFree && authData.value?.isAnonymous == true) { if (!app.isFree && gPlayAuthData.isAnonymous) { _errorMessageStringResource.value = R.string.paid_app_anonymous_message return true } Loading Loading @@ -553,7 +268,7 @@ class MainActivityViewModel @Inject constructor( suspend fun updateAwaitingForPurchasedApp(packageName: String): FusedDownload? { val fusedDownload = fusedManagerRepository.getFusedDownload(packageName = packageName) authData.value?.let { gPlayAuthData.let { if (!it.isAnonymous) { try { fusedAPIRepository.updateFusedDownloadWithDownloadingInfo( Loading Loading @@ -594,7 +309,7 @@ class MainActivityViewModel @Inject constructor( fusedDownload: FusedDownload ) { val downloadList = mutableListOf<String>() authData.value?.let { gPlayAuthData.let { if (app.type == Type.PWA) { downloadList.add(app.url) fusedDownload.downloadURLList = downloadList Loading
app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +0 −12 Original line number Diff line number Diff line Loading @@ -465,18 +465,6 @@ class FusedAPIImpl @Inject constructor( return gPlayAPIRepository.getSearchSuggestions(query, authData) } suspend fun fetchAuthData(): Boolean { return gPlayAPIRepository.fetchAuthData() } suspend fun fetchAuthData(email: String, aasToken: String): AuthData? { return gPlayAPIRepository.fetchAuthData(email, aasToken) } suspend fun validateAuthData(authData: AuthData): PlayResponse { return gPlayAPIRepository.validateAuthData(authData) } suspend fun getOnDemandModule( authData: AuthData, packageName: String, Loading
app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt +1 −19 Original line number Diff line number Diff line Loading @@ -23,7 +23,6 @@ import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.data.models.StreamBundle import com.aurora.gplayapi.data.models.StreamCluster import foundation.e.apps.api.ResultSupreme Loading @@ -39,9 +38,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton class FusedAPIRepository @Inject constructor( private val fusedAPIImpl: FusedAPIImpl ) { class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPIImpl) { var streamBundle = StreamBundle() private set Loading Loading @@ -93,13 +90,6 @@ class FusedAPIRepository @Inject constructor( return fusedAPIImpl.getApplicationCategoryPreference() } suspend fun validateAuthData(authData: AuthData): PlayResponse { if (authData.authToken.isNotEmpty() && authData.deviceInfoProvider != null) { return fusedAPIImpl.validateAuthData(authData) } return PlayResponse() } suspend fun getApplicationDetails( packageNameList: List<String>, authData: AuthData, Loading Loading @@ -165,14 +155,6 @@ class FusedAPIRepository @Inject constructor( return fusedAPIImpl.getSearchSuggestions(query, authData) } suspend fun fetchAuthData(): Boolean { return fusedAPIImpl.fetchAuthData() } suspend fun fetchAuthData(email: String, aasToken: String): AuthData? { return fusedAPIImpl.fetchAuthData(email, aasToken) } fun getSearchResults( query: String, authData: AuthData Loading
app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIImpl.kt +1 −55 Original line number Diff line number Diff line Loading @@ -26,7 +26,6 @@ import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category import com.aurora.gplayapi.data.models.File import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.data.models.SearchBundle import com.aurora.gplayapi.data.models.StreamBundle import com.aurora.gplayapi.data.models.StreamCluster Loading @@ -37,66 +36,13 @@ import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.gplayapi.helpers.SearchHelper import com.aurora.gplayapi.helpers.StreamHelper import com.aurora.gplayapi.helpers.TopChartsHelper import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.api.gplay.token.TokenRepository import foundation.e.apps.api.gplay.utils.CustomAuthValidator import foundation.e.apps.api.gplay.utils.GPlayHttpClient import foundation.e.apps.utils.modules.DataStoreModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext import javax.inject.Inject class GPlayAPIImpl @Inject constructor( @ApplicationContext private val context: Context, private val tokenRepository: TokenRepository, private val dataStoreModule: DataStoreModule, private val gPlayHttpClient: GPlayHttpClient ) { /** * Save auth data to preferences. * Updated for network failures. * Issue: * https://gitlab.e.foundation/e/backlog/-/issues/5413 * https://gitlab.e.foundation/e/backlog/-/issues/5404 * * @return true or false based on if the request was successful. */ // TODO: DON'T HARDCODE DISPATCHERS IN ANY METHODS suspend fun fetchAuthData(): Boolean = withContext(Dispatchers.IO) { val data = async { tokenRepository.getAuthData() } data.await().let { if (it == null) return@withContext false it.locale = context.resources.configuration.locales[0] // update locale with the default locale from settings dataStoreModule.saveCredentials(it) return@withContext true } } suspend fun fetchAuthData(email: String, aasToken: String): AuthData? { val authData = tokenRepository.getAuthData(email, aasToken) if (authData.authToken.isNotEmpty() && authData.deviceInfoProvider != null) { dataStoreModule.saveCredentials(authData) return authData } return null } suspend fun validateAuthData(authData: AuthData): PlayResponse { var result = PlayResponse() withContext(Dispatchers.IO) { try { val authValidator = CustomAuthValidator(authData).using(gPlayHttpClient) result = authValidator.getValidityResponse() } catch (e: Exception) { e.printStackTrace() throw e } } return result } class GPlayAPIImpl @Inject constructor(private val gPlayHttpClient: GPlayHttpClient) { suspend fun getSearchSuggestions(query: String, authData: AuthData): List<SearchSuggestEntry> { val searchData = mutableListOf<SearchSuggestEntry>() Loading