Loading app/src/main/java/foundation/e/apps/MainActivity.kt +34 −42 Original line number Diff line number Diff line Loading @@ -25,9 +25,9 @@ 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 import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.NavOptions Loading @@ -35,12 +35,15 @@ import androidx.navigation.findNavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.setupWithNavController import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.exceptions.ApiException import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.application.subFrags.ApplicationDialogFragment 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.workmanager.InstallWorkManager import foundation.e.apps.purchase.AppPurchaseFragmentDirections Loading @@ -50,11 +53,12 @@ import foundation.e.apps.updates.UpdatesNotifier import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.eventBus.AppEvent 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.parentFragment.TimeoutFragment import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import org.json.JSONObject import timber.log.Timber import java.io.File import java.util.UUID Loading @@ -62,6 +66,7 @@ import java.util.UUID @AndroidEntryPoint class MainActivity : AppCompatActivity() { private lateinit var signInViewModel: SignInViewModel private lateinit var loginViewModel: LoginViewModel private lateinit var binding: ActivityMainBinding private val TAG = MainActivity::class.java.simpleName private lateinit var viewModel: MainActivityViewModel Loading @@ -83,6 +88,7 @@ class MainActivity : AppCompatActivity() { viewModel = ViewModelProvider(this)[MainActivityViewModel::class.java] signInViewModel = ViewModelProvider(this)[SignInViewModel::class.java] loginViewModel = ViewModelProvider(this)[LoginViewModel::class.java] // navOptions and activityNavController for TOS and SignIn Fragments val navOptions = NavOptions.Builder() Loading @@ -90,9 +96,11 @@ class MainActivity : AppCompatActivity() { .build() navOptions.shouldLaunchSingleTop() viewModel.tocStatus.observe(this) { viewModel.tocStatus.distinctUntilChanged().observe(this) { if (it != true) { navController.navigate(R.id.TOSFragment, null, navOptions) } else { loginViewModel.startLoginFlow() } } Loading @@ -101,37 +109,34 @@ class MainActivity : AppCompatActivity() { if (isInternetAvailable) { binding.noInternet.visibility = View.GONE 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() } if (signInViewModel.authLiveData.value == null) { signInViewModel.authLiveData.observe(this) { viewModel.updateAuthData(it) loginViewModel.authObjects.distinctUntilChanged().observe(this) { when { it == null -> return@observe 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 -> {} } viewModel.errorAuthResponse.observe(this) { onSignInError() it.find { it is AuthObject.GPlayAuth }?.result?.run { if (isSuccess()) { 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.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() viewModel.uploadFaultyTokenToEcloud(email, descriptionJson.toString()) } } } Loading Loading @@ -331,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 +10 −295 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,16 +34,12 @@ 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 Loading @@ -54,21 +49,16 @@ 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.CommonUtilsModule.timeoutDurationInMillis 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,37 +66,16 @@ 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 /* * 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 var gPlayAuthData = AuthData("", "") // Downloads val downloadList = fusedManagerRepository.getDownloadLiveList() Loading @@ -116,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 @@ -126,269 +92,18 @@ class MainActivityViewModel @Inject constructor( } fun getUser(): User { return User.valueOf(userType.value ?: User.UNAVAILABLE.name) } private fun setFirstTokenFetchTime() { if (firstAuthDataFetchTime == 0L) { firstAuthDataFetchTime = SystemClock.uptimeMillis() } return dataStoreModule.getUserType() } private fun isTimeEligibleForTokenRefresh(): Boolean { return (SystemClock.uptimeMillis() - firstAuthDataFetchTime) <= timeoutDurationInMillis fun getUserEmail(): String { return dataStoreModule.getEmail() } /* * 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) { fun uploadFaultyTokenToEcloud(email: String, description: String = "") { viewModelScope.launch { authData.value?.let { authData -> val email: String = authData.run { if (email != "null") email else userProfile?.email ?: "null" } 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 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 @@ -460,7 +175,7 @@ class MainActivityViewModel @Inject constructor( */ fun verifyUiFilter(fusedApp: FusedApp, method: () -> Unit) { viewModelScope.launch { val authData = authData.value val authData = gPlayAuthData if (fusedApp.filterLevel.isInitialized()) { method() } else { 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/ResultSupreme.kt +3 −3 Original line number 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 exception Optional exception from try-catch block. */ constructor(message: String, exception: Exception = Exception()) : this() { constructor(message: String, exception: Exception? = null) : this() { this.message = message this.exception = exception } Loading Loading @@ -107,7 +107,7 @@ sealed class ResultSupreme<T> { /** * Exception from try-catch block for error cases. */ var exception: Exception = Exception() var exception: Exception? = null fun isValidData() = data != null Loading @@ -129,7 +129,7 @@ sealed class ResultSupreme<T> { status: ResultStatus, data: T? = null, message: String = "", exception: Exception = Exception(), exception: Exception? = null, ): ResultSupreme<T> { val resultObject = when { status == ResultStatus.OK && data != null -> Success<T>(data) Loading app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +1 −14 Original line number Diff line number Diff line Loading @@ -29,7 +29,6 @@ import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.Artwork 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 com.aurora.gplayapi.helpers.TopChartsHelper Loading @@ -50,6 +49,7 @@ import foundation.e.apps.api.gplay.GPlayAPIRepository import foundation.e.apps.home.model.HomeChildFusedAppDiffUtil import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.utils.Constants.timeoutDurationInMillis import foundation.e.apps.utils.enums.AppTag import foundation.e.apps.utils.enums.FilterLevel import foundation.e.apps.utils.enums.Origin Loading @@ -57,7 +57,6 @@ import foundation.e.apps.utils.enums.ResultStatus import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.Type import foundation.e.apps.utils.enums.isUnFiltered import foundation.e.apps.utils.modules.CommonUtilsModule.timeoutDurationInMillis import foundation.e.apps.utils.modules.PWAManagerModule import foundation.e.apps.utils.modules.PreferenceManagerModule import kotlinx.coroutines.TimeoutCancellationException Loading Loading @@ -464,18 +463,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 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
app/src/main/java/foundation/e/apps/MainActivity.kt +34 −42 Original line number Diff line number Diff line Loading @@ -25,9 +25,9 @@ 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 import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.NavOptions Loading @@ -35,12 +35,15 @@ import androidx.navigation.findNavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.setupWithNavController import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.exceptions.ApiException import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.application.subFrags.ApplicationDialogFragment 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.workmanager.InstallWorkManager import foundation.e.apps.purchase.AppPurchaseFragmentDirections Loading @@ -50,11 +53,12 @@ import foundation.e.apps.updates.UpdatesNotifier import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.eventBus.AppEvent 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.parentFragment.TimeoutFragment import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import org.json.JSONObject import timber.log.Timber import java.io.File import java.util.UUID Loading @@ -62,6 +66,7 @@ import java.util.UUID @AndroidEntryPoint class MainActivity : AppCompatActivity() { private lateinit var signInViewModel: SignInViewModel private lateinit var loginViewModel: LoginViewModel private lateinit var binding: ActivityMainBinding private val TAG = MainActivity::class.java.simpleName private lateinit var viewModel: MainActivityViewModel Loading @@ -83,6 +88,7 @@ class MainActivity : AppCompatActivity() { viewModel = ViewModelProvider(this)[MainActivityViewModel::class.java] signInViewModel = ViewModelProvider(this)[SignInViewModel::class.java] loginViewModel = ViewModelProvider(this)[LoginViewModel::class.java] // navOptions and activityNavController for TOS and SignIn Fragments val navOptions = NavOptions.Builder() Loading @@ -90,9 +96,11 @@ class MainActivity : AppCompatActivity() { .build() navOptions.shouldLaunchSingleTop() viewModel.tocStatus.observe(this) { viewModel.tocStatus.distinctUntilChanged().observe(this) { if (it != true) { navController.navigate(R.id.TOSFragment, null, navOptions) } else { loginViewModel.startLoginFlow() } } Loading @@ -101,37 +109,34 @@ class MainActivity : AppCompatActivity() { if (isInternetAvailable) { binding.noInternet.visibility = View.GONE 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() } if (signInViewModel.authLiveData.value == null) { signInViewModel.authLiveData.observe(this) { viewModel.updateAuthData(it) loginViewModel.authObjects.distinctUntilChanged().observe(this) { when { it == null -> return@observe 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 -> {} } viewModel.errorAuthResponse.observe(this) { onSignInError() it.find { it is AuthObject.GPlayAuth }?.result?.run { if (isSuccess()) { 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.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() viewModel.uploadFaultyTokenToEcloud(email, descriptionJson.toString()) } } } Loading Loading @@ -331,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 +10 −295 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,16 +34,12 @@ 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 Loading @@ -54,21 +49,16 @@ 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.CommonUtilsModule.timeoutDurationInMillis 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,37 +66,16 @@ 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 /* * 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 var gPlayAuthData = AuthData("", "") // Downloads val downloadList = fusedManagerRepository.getDownloadLiveList() Loading @@ -116,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 @@ -126,269 +92,18 @@ class MainActivityViewModel @Inject constructor( } fun getUser(): User { return User.valueOf(userType.value ?: User.UNAVAILABLE.name) } private fun setFirstTokenFetchTime() { if (firstAuthDataFetchTime == 0L) { firstAuthDataFetchTime = SystemClock.uptimeMillis() } return dataStoreModule.getUserType() } private fun isTimeEligibleForTokenRefresh(): Boolean { return (SystemClock.uptimeMillis() - firstAuthDataFetchTime) <= timeoutDurationInMillis fun getUserEmail(): String { return dataStoreModule.getEmail() } /* * 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) { fun uploadFaultyTokenToEcloud(email: String, description: String = "") { viewModelScope.launch { authData.value?.let { authData -> val email: String = authData.run { if (email != "null") email else userProfile?.email ?: "null" } 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 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 @@ -460,7 +175,7 @@ class MainActivityViewModel @Inject constructor( */ fun verifyUiFilter(fusedApp: FusedApp, method: () -> Unit) { viewModelScope.launch { val authData = authData.value val authData = gPlayAuthData if (fusedApp.filterLevel.isInitialized()) { method() } else { 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/ResultSupreme.kt +3 −3 Original line number 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 exception Optional exception from try-catch block. */ constructor(message: String, exception: Exception = Exception()) : this() { constructor(message: String, exception: Exception? = null) : this() { this.message = message this.exception = exception } Loading Loading @@ -107,7 +107,7 @@ sealed class ResultSupreme<T> { /** * Exception from try-catch block for error cases. */ var exception: Exception = Exception() var exception: Exception? = null fun isValidData() = data != null Loading @@ -129,7 +129,7 @@ sealed class ResultSupreme<T> { status: ResultStatus, data: T? = null, message: String = "", exception: Exception = Exception(), exception: Exception? = null, ): ResultSupreme<T> { val resultObject = when { status == ResultStatus.OK && data != null -> Success<T>(data) Loading
app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +1 −14 Original line number Diff line number Diff line Loading @@ -29,7 +29,6 @@ import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.Artwork 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 com.aurora.gplayapi.helpers.TopChartsHelper Loading @@ -50,6 +49,7 @@ import foundation.e.apps.api.gplay.GPlayAPIRepository import foundation.e.apps.home.model.HomeChildFusedAppDiffUtil import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.utils.Constants.timeoutDurationInMillis import foundation.e.apps.utils.enums.AppTag import foundation.e.apps.utils.enums.FilterLevel import foundation.e.apps.utils.enums.Origin Loading @@ -57,7 +57,6 @@ import foundation.e.apps.utils.enums.ResultStatus import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.Type import foundation.e.apps.utils.enums.isUnFiltered import foundation.e.apps.utils.modules.CommonUtilsModule.timeoutDurationInMillis import foundation.e.apps.utils.modules.PWAManagerModule import foundation.e.apps.utils.modules.PreferenceManagerModule import kotlinx.coroutines.TimeoutCancellationException Loading Loading @@ -464,18 +463,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 File changed.Preview size limit exceeded, changes collapsed. Show changes