Loading app/src/main/java/foundation/e/apps/MainActivity.kt +42 −34 Original line number Original line Diff line number Diff line Loading @@ -25,9 +25,9 @@ import android.os.Environment import android.os.StatFs import android.os.StatFs import android.os.storage.StorageManager import android.os.storage.StorageManager import android.view.View import android.view.View import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.NavController import androidx.navigation.NavOptions import androidx.navigation.NavOptions Loading @@ -35,15 +35,12 @@ import androidx.navigation.findNavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.exceptions.ApiException import com.aurora.gplayapi.exceptions.ApiException import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.databinding.ActivityMainBinding import foundation.e.apps.databinding.ActivityMainBinding import foundation.e.apps.login.AuthObject import foundation.e.apps.login.LoginViewModel import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.workmanager.InstallWorkManager import foundation.e.apps.manager.workmanager.InstallWorkManager import foundation.e.apps.purchase.AppPurchaseFragmentDirections import foundation.e.apps.purchase.AppPurchaseFragmentDirections Loading @@ -53,12 +50,11 @@ import foundation.e.apps.updates.UpdatesNotifier import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus import foundation.e.apps.utils.eventBus.EventBus import foundation.e.apps.utils.exceptions.GPlayValidationException import foundation.e.apps.utils.modules.CommonUtilsModule import foundation.e.apps.utils.modules.CommonUtilsModule import foundation.e.apps.utils.parentFragment.TimeoutFragment import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import kotlinx.coroutines.launch import org.json.JSONObject import timber.log.Timber import timber.log.Timber import java.io.File import java.io.File import java.util.UUID import java.util.UUID Loading @@ -66,7 +62,6 @@ import java.util.UUID @AndroidEntryPoint @AndroidEntryPoint class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() { private lateinit var signInViewModel: SignInViewModel private lateinit var signInViewModel: SignInViewModel private lateinit var loginViewModel: LoginViewModel private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding private val TAG = MainActivity::class.java.simpleName private val TAG = MainActivity::class.java.simpleName private lateinit var viewModel: MainActivityViewModel private lateinit var viewModel: MainActivityViewModel Loading @@ -88,7 +83,6 @@ class MainActivity : AppCompatActivity() { viewModel = ViewModelProvider(this)[MainActivityViewModel::class.java] viewModel = ViewModelProvider(this)[MainActivityViewModel::class.java] signInViewModel = ViewModelProvider(this)[SignInViewModel::class.java] signInViewModel = ViewModelProvider(this)[SignInViewModel::class.java] loginViewModel = ViewModelProvider(this)[LoginViewModel::class.java] // navOptions and activityNavController for TOS and SignIn Fragments // navOptions and activityNavController for TOS and SignIn Fragments val navOptions = NavOptions.Builder() val navOptions = NavOptions.Builder() Loading @@ -96,11 +90,9 @@ class MainActivity : AppCompatActivity() { .build() .build() navOptions.shouldLaunchSingleTop() navOptions.shouldLaunchSingleTop() viewModel.tocStatus.distinctUntilChanged().observe(this) { viewModel.tocStatus.observe(this) { if (it != true) { if (it != true) { navController.navigate(R.id.TOSFragment, null, navOptions) navController.navigate(R.id.TOSFragment, null, navOptions) } else { loginViewModel.startLoginFlow() } } } } Loading @@ -109,34 +101,37 @@ class MainActivity : AppCompatActivity() { if (isInternetAvailable) { if (isInternetAvailable) { binding.noInternet.visibility = View.GONE binding.noInternet.visibility = View.GONE binding.fragment.visibility = View.VISIBLE binding.fragment.visibility = View.VISIBLE // Watch and refresh authentication data if (viewModel.authDataJson.value == null) { viewModel.authDataJson.observe(this) { viewModel.handleAuthDataJson() } } } } } } viewModel.userType.observe(this) { user -> viewModel.handleAuthDataJson() } loginViewModel.authObjects.distinctUntilChanged().observe(this) { if (signInViewModel.authLiveData.value == null) { when { signInViewModel.authLiveData.observe(this) { it == null -> return@observe viewModel.updateAuthData(it) it.isEmpty() -> { // No auth type defined means user has not logged in yet // Pop back stack to prevent showing TOSFragment on pressing back button. navController.popBackStack() navController.navigate(R.id.signInFragment) } } else -> {} } } it.find { it is AuthObject.GPlayAuth }?.result?.run { viewModel.errorAuthResponse.observe(this) { if (isSuccess()) { onSignInError() viewModel.gPlayAuthData = data as AuthData } else if (exception is GPlayValidationException) { val email = otherPayload.toString() val descriptionJson = JSONObject().apply { put("versionName", BuildConfig.VERSION_NAME) put("versionCode", BuildConfig.VERSION_CODE) put("debuggable", BuildConfig.DEBUG) put("device", Build.DEVICE) put("api", Build.VERSION.SDK_INT) } } viewModel.uploadFaultyTokenToEcloud(email, descriptionJson.toString()) viewModel.authValidity.observe(this) { viewModel.handleAuthValidity(it) { Timber.d("Timeout validating auth data!") val lastFragment = navHostFragment.childFragmentManager.fragments[0] if (lastFragment is TimeoutFragment) { Timber.d("Displaying timeout from MainActivity on fragment: " + lastFragment.javaClass.name) lastFragment.onTimeout() } } } } } } Loading Loading @@ -336,6 +331,19 @@ 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 { private fun getAvailableInternalMemorySize(): Long { val path: File = Environment.getDataDirectory() val path: File = Environment.getDataDirectory() val stat = StatFs(path.path) val stat = StatFs(path.path) Loading app/src/main/java/foundation/e/apps/MainActivityViewModel.kt +295 −10 Original line number Original line Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.content.Context import android.content.Intent import android.content.Intent import android.graphics.Bitmap import android.graphics.Bitmap import android.os.Build import android.os.Build import android.os.SystemClock import android.util.Base64 import android.util.Base64 import android.widget.ImageView import android.widget.ImageView import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi Loading @@ -34,12 +35,16 @@ import androidx.lifecycle.asLiveData import androidx.lifecycle.liveData import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.exceptions.ApiException import com.aurora.gplayapi.exceptions.ApiException import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.api.cleanapk.blockedApps.BlockedAppRepository import foundation.e.apps.api.cleanapk.blockedApps.BlockedAppRepository import foundation.e.apps.api.ecloud.EcloudRepository import foundation.e.apps.api.ecloud.EcloudRepository import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp 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.database.fusedDownload.FusedDownload import foundation.e.apps.manager.fused.FusedManagerRepository import foundation.e.apps.manager.fused.FusedManagerRepository import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.manager.pkg.PkgManagerModule Loading @@ -49,16 +54,21 @@ import foundation.e.apps.utils.enums.Type import foundation.e.apps.utils.enums.User import foundation.e.apps.utils.enums.User import foundation.e.apps.utils.enums.isInitialized import foundation.e.apps.utils.enums.isInitialized import foundation.e.apps.utils.enums.isUnFiltered import foundation.e.apps.utils.enums.isUnFiltered import foundation.e.apps.utils.modules.CommonUtilsModule.NETWORK_CODE_SUCCESS import foundation.e.apps.utils.modules.CommonUtilsModule.timeoutDurationInMillis import foundation.e.apps.utils.modules.DataStoreModule import foundation.e.apps.utils.modules.DataStoreModule import foundation.e.apps.utils.modules.PWAManagerModule import foundation.e.apps.utils.modules.PWAManagerModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ru.beryukhov.reactivenetwork.ReactiveNetwork import ru.beryukhov.reactivenetwork.ReactiveNetwork import timber.log.Timber import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream import javax.inject.Inject import javax.inject.Inject @HiltViewModel @HiltViewModel class MainActivityViewModel @Inject constructor( class MainActivityViewModel @Inject constructor( private val gson: Gson, private val dataStoreModule: DataStoreModule, private val dataStoreModule: DataStoreModule, private val fusedAPIRepository: FusedAPIRepository, private val fusedAPIRepository: FusedAPIRepository, private val fusedManagerRepository: FusedManagerRepository, private val fusedManagerRepository: FusedManagerRepository, Loading @@ -66,16 +76,37 @@ class MainActivityViewModel @Inject constructor( private val pwaManagerModule: PWAManagerModule, private val pwaManagerModule: PWAManagerModule, private val ecloudRepository: EcloudRepository, private val ecloudRepository: EcloudRepository, private val blockedAppRepository: BlockedAppRepository, private val blockedAppRepository: BlockedAppRepository, private val aC2DMTask: AC2DMTask, ) : ViewModel() { ) : ViewModel() { val authDataJson: LiveData<String> = dataStoreModule.authData.asLiveData() val tocStatus: LiveData<Boolean> = dataStoreModule.tocStatus.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() private val _purchaseAppLiveData: MutableLiveData<FusedDownload> = MutableLiveData() val purchaseAppLiveData: LiveData<FusedDownload> = _purchaseAppLiveData val purchaseAppLiveData: LiveData<FusedDownload> = _purchaseAppLiveData val isAppPurchased: MutableLiveData<String> = MutableLiveData() val isAppPurchased: MutableLiveData<String> = MutableLiveData() val purchaseDeclined: 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 // Downloads val downloadList = fusedManagerRepository.getDownloadLiveList() val downloadList = fusedManagerRepository.getDownloadLiveList() Loading @@ -85,6 +116,9 @@ class MainActivityViewModel @Inject constructor( private val _errorMessageStringResource = MutableLiveData<Int>() private val _errorMessageStringResource = MutableLiveData<Int>() val errorMessageStringResource: LiveData<Int> = _errorMessageStringResource val errorMessageStringResource: LiveData<Int> = _errorMessageStringResource /* * Authentication related functions */ companion object { companion object { private const val TAG = "MainActivityViewModel" private const val TAG = "MainActivityViewModel" Loading @@ -92,18 +126,269 @@ class MainActivityViewModel @Inject constructor( } } fun getUser(): User { fun getUser(): User { return dataStoreModule.getUserType() return User.valueOf(userType.value ?: User.UNAVAILABLE.name) } private fun setFirstTokenFetchTime() { if (firstAuthDataFetchTime == 0L) { firstAuthDataFetchTime = SystemClock.uptimeMillis() } } } fun getUserEmail(): String { private fun isTimeEligibleForTokenRefresh(): Boolean { return dataStoreModule.getEmail() return (SystemClock.uptimeMillis() - firstAuthDataFetchTime) <= timeoutDurationInMillis } } fun uploadFaultyTokenToEcloud(email: String, description: String = "") { /* * 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(description: String) { viewModelScope.launch { viewModelScope.launch { authData.value?.let { authData -> val email: String = authData.run { if (email != "null") email else userProfile?.email ?: "null" } ecloudRepository.uploadFaultyEmail(email, description) 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 * Notification functions Loading Loading @@ -131,7 +416,7 @@ class MainActivityViewModel @Inject constructor( * Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/266 * Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/266 */ */ fun shouldShowPaidAppsSnackBar(app: FusedApp): Boolean { fun shouldShowPaidAppsSnackBar(app: FusedApp): Boolean { if (!app.isFree && gPlayAuthData.isAnonymous) { if (!app.isFree && authData.value?.isAnonymous == true) { _errorMessageStringResource.value = R.string.paid_app_anonymous_message _errorMessageStringResource.value = R.string.paid_app_anonymous_message return true return true } } Loading Loading @@ -175,7 +460,7 @@ class MainActivityViewModel @Inject constructor( */ */ fun verifyUiFilter(fusedApp: FusedApp, method: () -> Unit) { fun verifyUiFilter(fusedApp: FusedApp, method: () -> Unit) { viewModelScope.launch { viewModelScope.launch { val authData = gPlayAuthData val authData = authData.value if (fusedApp.filterLevel.isInitialized()) { if (fusedApp.filterLevel.isInitialized()) { method() method() } else { } else { Loading Loading @@ -268,7 +553,7 @@ class MainActivityViewModel @Inject constructor( suspend fun updateAwaitingForPurchasedApp(packageName: String): FusedDownload? { suspend fun updateAwaitingForPurchasedApp(packageName: String): FusedDownload? { val fusedDownload = fusedManagerRepository.getFusedDownload(packageName = packageName) val fusedDownload = fusedManagerRepository.getFusedDownload(packageName = packageName) gPlayAuthData.let { authData.value?.let { if (!it.isAnonymous) { if (!it.isAnonymous) { try { try { fusedAPIRepository.updateFusedDownloadWithDownloadingInfo( fusedAPIRepository.updateFusedDownloadWithDownloadingInfo( Loading Loading @@ -309,7 +594,7 @@ class MainActivityViewModel @Inject constructor( fusedDownload: FusedDownload fusedDownload: FusedDownload ) { ) { val downloadList = mutableListOf<String>() val downloadList = mutableListOf<String>() gPlayAuthData.let { authData.value?.let { if (app.type == Type.PWA) { if (app.type == Type.PWA) { downloadList.add(app.url) downloadList.add(app.url) fusedDownload.downloadURLList = downloadList fusedDownload.downloadURLList = downloadList Loading app/src/main/java/foundation/e/apps/api/ResultSupreme.kt +3 −8 Original line number Original line Diff line number Diff line Loading @@ -70,7 +70,7 @@ sealed class ResultSupreme<T> { * @param message A String message to log or display to the user. * @param message A String message to log or display to the user. * @param exception Optional exception from try-catch block. * @param exception Optional exception from try-catch block. */ */ constructor(message: String, exception: Exception? = null) : this() { constructor(message: String, exception: Exception = Exception()) : this() { this.message = message this.message = message this.exception = exception this.exception = exception } } Loading @@ -91,11 +91,6 @@ sealed class ResultSupreme<T> { var data: T? = null var data: T? = null private set private set /** * Any other information that needs to be transmitted. */ var otherPayload: Any? = null /** /** * A custom string message for logging or displaying to the user. * A custom string message for logging or displaying to the user. */ */ Loading @@ -104,7 +99,7 @@ sealed class ResultSupreme<T> { /** /** * Exception from try-catch block for error cases. * Exception from try-catch block for error cases. */ */ var exception: Exception? = null var exception: Exception = Exception() fun isValidData() = data != null fun isValidData() = data != null Loading @@ -126,7 +121,7 @@ sealed class ResultSupreme<T> { status: ResultStatus, status: ResultStatus, data: T? = null, data: T? = null, message: String = "", message: String = "", exception: Exception? = null, exception: Exception = Exception(), ): ResultSupreme<T> { ): ResultSupreme<T> { val resultObject = when { val resultObject = when { status == ResultStatus.OK && data != null -> Success<T>(data) status == ResultStatus.OK && data != null -> Success<T>(data) Loading app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +15 −2 File changed.Preview size limit exceeded, changes collapsed. Show changes app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt +19 −1 Original line number Original line Diff line number Diff line Loading @@ -23,6 +23,7 @@ import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category 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.StreamBundle import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.data.models.StreamCluster import foundation.e.apps.api.ResultSupreme import foundation.e.apps.api.ResultSupreme Loading @@ -38,7 +39,9 @@ import javax.inject.Inject import javax.inject.Singleton import javax.inject.Singleton @Singleton @Singleton class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPIImpl) { class FusedAPIRepository @Inject constructor( private val fusedAPIImpl: FusedAPIImpl ) { var streamBundle = StreamBundle() var streamBundle = StreamBundle() private set private set Loading Loading @@ -90,6 +93,13 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPII return fusedAPIImpl.getApplicationCategoryPreference() 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( suspend fun getApplicationDetails( packageNameList: List<String>, packageNameList: List<String>, authData: AuthData, authData: AuthData, Loading Loading @@ -155,6 +165,14 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPII return fusedAPIImpl.getSearchSuggestions(query, authData) 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( fun getSearchResults( query: String, query: String, authData: AuthData authData: AuthData Loading Loading
app/src/main/java/foundation/e/apps/MainActivity.kt +42 −34 Original line number Original line Diff line number Diff line Loading @@ -25,9 +25,9 @@ import android.os.Environment import android.os.StatFs import android.os.StatFs import android.os.storage.StorageManager import android.os.storage.StorageManager import android.view.View import android.view.View import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.NavController import androidx.navigation.NavOptions import androidx.navigation.NavOptions Loading @@ -35,15 +35,12 @@ import androidx.navigation.findNavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.exceptions.ApiException import com.aurora.gplayapi.exceptions.ApiException import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.databinding.ActivityMainBinding import foundation.e.apps.databinding.ActivityMainBinding import foundation.e.apps.login.AuthObject import foundation.e.apps.login.LoginViewModel import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.workmanager.InstallWorkManager import foundation.e.apps.manager.workmanager.InstallWorkManager import foundation.e.apps.purchase.AppPurchaseFragmentDirections import foundation.e.apps.purchase.AppPurchaseFragmentDirections Loading @@ -53,12 +50,11 @@ import foundation.e.apps.updates.UpdatesNotifier import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus import foundation.e.apps.utils.eventBus.EventBus import foundation.e.apps.utils.exceptions.GPlayValidationException import foundation.e.apps.utils.modules.CommonUtilsModule import foundation.e.apps.utils.modules.CommonUtilsModule import foundation.e.apps.utils.parentFragment.TimeoutFragment import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import kotlinx.coroutines.launch import org.json.JSONObject import timber.log.Timber import timber.log.Timber import java.io.File import java.io.File import java.util.UUID import java.util.UUID Loading @@ -66,7 +62,6 @@ import java.util.UUID @AndroidEntryPoint @AndroidEntryPoint class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() { private lateinit var signInViewModel: SignInViewModel private lateinit var signInViewModel: SignInViewModel private lateinit var loginViewModel: LoginViewModel private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding private val TAG = MainActivity::class.java.simpleName private val TAG = MainActivity::class.java.simpleName private lateinit var viewModel: MainActivityViewModel private lateinit var viewModel: MainActivityViewModel Loading @@ -88,7 +83,6 @@ class MainActivity : AppCompatActivity() { viewModel = ViewModelProvider(this)[MainActivityViewModel::class.java] viewModel = ViewModelProvider(this)[MainActivityViewModel::class.java] signInViewModel = ViewModelProvider(this)[SignInViewModel::class.java] signInViewModel = ViewModelProvider(this)[SignInViewModel::class.java] loginViewModel = ViewModelProvider(this)[LoginViewModel::class.java] // navOptions and activityNavController for TOS and SignIn Fragments // navOptions and activityNavController for TOS and SignIn Fragments val navOptions = NavOptions.Builder() val navOptions = NavOptions.Builder() Loading @@ -96,11 +90,9 @@ class MainActivity : AppCompatActivity() { .build() .build() navOptions.shouldLaunchSingleTop() navOptions.shouldLaunchSingleTop() viewModel.tocStatus.distinctUntilChanged().observe(this) { viewModel.tocStatus.observe(this) { if (it != true) { if (it != true) { navController.navigate(R.id.TOSFragment, null, navOptions) navController.navigate(R.id.TOSFragment, null, navOptions) } else { loginViewModel.startLoginFlow() } } } } Loading @@ -109,34 +101,37 @@ class MainActivity : AppCompatActivity() { if (isInternetAvailable) { if (isInternetAvailable) { binding.noInternet.visibility = View.GONE binding.noInternet.visibility = View.GONE binding.fragment.visibility = View.VISIBLE binding.fragment.visibility = View.VISIBLE // Watch and refresh authentication data if (viewModel.authDataJson.value == null) { viewModel.authDataJson.observe(this) { viewModel.handleAuthDataJson() } } } } } } viewModel.userType.observe(this) { user -> viewModel.handleAuthDataJson() } loginViewModel.authObjects.distinctUntilChanged().observe(this) { if (signInViewModel.authLiveData.value == null) { when { signInViewModel.authLiveData.observe(this) { it == null -> return@observe viewModel.updateAuthData(it) it.isEmpty() -> { // No auth type defined means user has not logged in yet // Pop back stack to prevent showing TOSFragment on pressing back button. navController.popBackStack() navController.navigate(R.id.signInFragment) } } else -> {} } } it.find { it is AuthObject.GPlayAuth }?.result?.run { viewModel.errorAuthResponse.observe(this) { if (isSuccess()) { onSignInError() viewModel.gPlayAuthData = data as AuthData } else if (exception is GPlayValidationException) { val email = otherPayload.toString() val descriptionJson = JSONObject().apply { put("versionName", BuildConfig.VERSION_NAME) put("versionCode", BuildConfig.VERSION_CODE) put("debuggable", BuildConfig.DEBUG) put("device", Build.DEVICE) put("api", Build.VERSION.SDK_INT) } } viewModel.uploadFaultyTokenToEcloud(email, descriptionJson.toString()) viewModel.authValidity.observe(this) { viewModel.handleAuthValidity(it) { Timber.d("Timeout validating auth data!") val lastFragment = navHostFragment.childFragmentManager.fragments[0] if (lastFragment is TimeoutFragment) { Timber.d("Displaying timeout from MainActivity on fragment: " + lastFragment.javaClass.name) lastFragment.onTimeout() } } } } } } Loading Loading @@ -336,6 +331,19 @@ 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 { private fun getAvailableInternalMemorySize(): Long { val path: File = Environment.getDataDirectory() val path: File = Environment.getDataDirectory() val stat = StatFs(path.path) val stat = StatFs(path.path) Loading
app/src/main/java/foundation/e/apps/MainActivityViewModel.kt +295 −10 Original line number Original line Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.content.Context import android.content.Intent import android.content.Intent import android.graphics.Bitmap import android.graphics.Bitmap import android.os.Build import android.os.Build import android.os.SystemClock import android.util.Base64 import android.util.Base64 import android.widget.ImageView import android.widget.ImageView import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi Loading @@ -34,12 +35,16 @@ import androidx.lifecycle.asLiveData import androidx.lifecycle.liveData import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.exceptions.ApiException import com.aurora.gplayapi.exceptions.ApiException import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.api.cleanapk.blockedApps.BlockedAppRepository import foundation.e.apps.api.cleanapk.blockedApps.BlockedAppRepository import foundation.e.apps.api.ecloud.EcloudRepository import foundation.e.apps.api.ecloud.EcloudRepository import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp 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.database.fusedDownload.FusedDownload import foundation.e.apps.manager.fused.FusedManagerRepository import foundation.e.apps.manager.fused.FusedManagerRepository import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.manager.pkg.PkgManagerModule Loading @@ -49,16 +54,21 @@ import foundation.e.apps.utils.enums.Type import foundation.e.apps.utils.enums.User import foundation.e.apps.utils.enums.User import foundation.e.apps.utils.enums.isInitialized import foundation.e.apps.utils.enums.isInitialized import foundation.e.apps.utils.enums.isUnFiltered import foundation.e.apps.utils.enums.isUnFiltered import foundation.e.apps.utils.modules.CommonUtilsModule.NETWORK_CODE_SUCCESS import foundation.e.apps.utils.modules.CommonUtilsModule.timeoutDurationInMillis import foundation.e.apps.utils.modules.DataStoreModule import foundation.e.apps.utils.modules.DataStoreModule import foundation.e.apps.utils.modules.PWAManagerModule import foundation.e.apps.utils.modules.PWAManagerModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ru.beryukhov.reactivenetwork.ReactiveNetwork import ru.beryukhov.reactivenetwork.ReactiveNetwork import timber.log.Timber import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream import javax.inject.Inject import javax.inject.Inject @HiltViewModel @HiltViewModel class MainActivityViewModel @Inject constructor( class MainActivityViewModel @Inject constructor( private val gson: Gson, private val dataStoreModule: DataStoreModule, private val dataStoreModule: DataStoreModule, private val fusedAPIRepository: FusedAPIRepository, private val fusedAPIRepository: FusedAPIRepository, private val fusedManagerRepository: FusedManagerRepository, private val fusedManagerRepository: FusedManagerRepository, Loading @@ -66,16 +76,37 @@ class MainActivityViewModel @Inject constructor( private val pwaManagerModule: PWAManagerModule, private val pwaManagerModule: PWAManagerModule, private val ecloudRepository: EcloudRepository, private val ecloudRepository: EcloudRepository, private val blockedAppRepository: BlockedAppRepository, private val blockedAppRepository: BlockedAppRepository, private val aC2DMTask: AC2DMTask, ) : ViewModel() { ) : ViewModel() { val authDataJson: LiveData<String> = dataStoreModule.authData.asLiveData() val tocStatus: LiveData<Boolean> = dataStoreModule.tocStatus.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() private val _purchaseAppLiveData: MutableLiveData<FusedDownload> = MutableLiveData() val purchaseAppLiveData: LiveData<FusedDownload> = _purchaseAppLiveData val purchaseAppLiveData: LiveData<FusedDownload> = _purchaseAppLiveData val isAppPurchased: MutableLiveData<String> = MutableLiveData() val isAppPurchased: MutableLiveData<String> = MutableLiveData() val purchaseDeclined: 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 // Downloads val downloadList = fusedManagerRepository.getDownloadLiveList() val downloadList = fusedManagerRepository.getDownloadLiveList() Loading @@ -85,6 +116,9 @@ class MainActivityViewModel @Inject constructor( private val _errorMessageStringResource = MutableLiveData<Int>() private val _errorMessageStringResource = MutableLiveData<Int>() val errorMessageStringResource: LiveData<Int> = _errorMessageStringResource val errorMessageStringResource: LiveData<Int> = _errorMessageStringResource /* * Authentication related functions */ companion object { companion object { private const val TAG = "MainActivityViewModel" private const val TAG = "MainActivityViewModel" Loading @@ -92,18 +126,269 @@ class MainActivityViewModel @Inject constructor( } } fun getUser(): User { fun getUser(): User { return dataStoreModule.getUserType() return User.valueOf(userType.value ?: User.UNAVAILABLE.name) } private fun setFirstTokenFetchTime() { if (firstAuthDataFetchTime == 0L) { firstAuthDataFetchTime = SystemClock.uptimeMillis() } } } fun getUserEmail(): String { private fun isTimeEligibleForTokenRefresh(): Boolean { return dataStoreModule.getEmail() return (SystemClock.uptimeMillis() - firstAuthDataFetchTime) <= timeoutDurationInMillis } } fun uploadFaultyTokenToEcloud(email: String, description: String = "") { /* * 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(description: String) { viewModelScope.launch { viewModelScope.launch { authData.value?.let { authData -> val email: String = authData.run { if (email != "null") email else userProfile?.email ?: "null" } ecloudRepository.uploadFaultyEmail(email, description) 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 * Notification functions Loading Loading @@ -131,7 +416,7 @@ class MainActivityViewModel @Inject constructor( * Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/266 * Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/266 */ */ fun shouldShowPaidAppsSnackBar(app: FusedApp): Boolean { fun shouldShowPaidAppsSnackBar(app: FusedApp): Boolean { if (!app.isFree && gPlayAuthData.isAnonymous) { if (!app.isFree && authData.value?.isAnonymous == true) { _errorMessageStringResource.value = R.string.paid_app_anonymous_message _errorMessageStringResource.value = R.string.paid_app_anonymous_message return true return true } } Loading Loading @@ -175,7 +460,7 @@ class MainActivityViewModel @Inject constructor( */ */ fun verifyUiFilter(fusedApp: FusedApp, method: () -> Unit) { fun verifyUiFilter(fusedApp: FusedApp, method: () -> Unit) { viewModelScope.launch { viewModelScope.launch { val authData = gPlayAuthData val authData = authData.value if (fusedApp.filterLevel.isInitialized()) { if (fusedApp.filterLevel.isInitialized()) { method() method() } else { } else { Loading Loading @@ -268,7 +553,7 @@ class MainActivityViewModel @Inject constructor( suspend fun updateAwaitingForPurchasedApp(packageName: String): FusedDownload? { suspend fun updateAwaitingForPurchasedApp(packageName: String): FusedDownload? { val fusedDownload = fusedManagerRepository.getFusedDownload(packageName = packageName) val fusedDownload = fusedManagerRepository.getFusedDownload(packageName = packageName) gPlayAuthData.let { authData.value?.let { if (!it.isAnonymous) { if (!it.isAnonymous) { try { try { fusedAPIRepository.updateFusedDownloadWithDownloadingInfo( fusedAPIRepository.updateFusedDownloadWithDownloadingInfo( Loading Loading @@ -309,7 +594,7 @@ class MainActivityViewModel @Inject constructor( fusedDownload: FusedDownload fusedDownload: FusedDownload ) { ) { val downloadList = mutableListOf<String>() val downloadList = mutableListOf<String>() gPlayAuthData.let { authData.value?.let { if (app.type == Type.PWA) { if (app.type == Type.PWA) { downloadList.add(app.url) downloadList.add(app.url) fusedDownload.downloadURLList = downloadList fusedDownload.downloadURLList = downloadList Loading
app/src/main/java/foundation/e/apps/api/ResultSupreme.kt +3 −8 Original line number Original line Diff line number Diff line Loading @@ -70,7 +70,7 @@ sealed class ResultSupreme<T> { * @param message A String message to log or display to the user. * @param message A String message to log or display to the user. * @param exception Optional exception from try-catch block. * @param exception Optional exception from try-catch block. */ */ constructor(message: String, exception: Exception? = null) : this() { constructor(message: String, exception: Exception = Exception()) : this() { this.message = message this.message = message this.exception = exception this.exception = exception } } Loading @@ -91,11 +91,6 @@ sealed class ResultSupreme<T> { var data: T? = null var data: T? = null private set private set /** * Any other information that needs to be transmitted. */ var otherPayload: Any? = null /** /** * A custom string message for logging or displaying to the user. * A custom string message for logging or displaying to the user. */ */ Loading @@ -104,7 +99,7 @@ sealed class ResultSupreme<T> { /** /** * Exception from try-catch block for error cases. * Exception from try-catch block for error cases. */ */ var exception: Exception? = null var exception: Exception = Exception() fun isValidData() = data != null fun isValidData() = data != null Loading @@ -126,7 +121,7 @@ sealed class ResultSupreme<T> { status: ResultStatus, status: ResultStatus, data: T? = null, data: T? = null, message: String = "", message: String = "", exception: Exception? = null, exception: Exception = Exception(), ): ResultSupreme<T> { ): ResultSupreme<T> { val resultObject = when { val resultObject = when { status == ResultStatus.OK && data != null -> Success<T>(data) status == ResultStatus.OK && data != null -> Success<T>(data) Loading
app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +15 −2 File changed.Preview size limit exceeded, changes collapsed. Show changes
app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt +19 −1 Original line number Original line Diff line number Diff line Loading @@ -23,6 +23,7 @@ import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category 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.StreamBundle import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.data.models.StreamCluster import foundation.e.apps.api.ResultSupreme import foundation.e.apps.api.ResultSupreme Loading @@ -38,7 +39,9 @@ import javax.inject.Inject import javax.inject.Singleton import javax.inject.Singleton @Singleton @Singleton class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPIImpl) { class FusedAPIRepository @Inject constructor( private val fusedAPIImpl: FusedAPIImpl ) { var streamBundle = StreamBundle() var streamBundle = StreamBundle() private set private set Loading Loading @@ -90,6 +93,13 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPII return fusedAPIImpl.getApplicationCategoryPreference() 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( suspend fun getApplicationDetails( packageNameList: List<String>, packageNameList: List<String>, authData: AuthData, authData: AuthData, Loading Loading @@ -155,6 +165,14 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPII return fusedAPIImpl.getSearchSuggestions(query, authData) 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( fun getSearchResults( query: String, query: String, authData: AuthData authData: AuthData Loading