diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index 1abe7f4f77c695f19ad258bda96b2979add68f8e..4c023ae44405be51e48e09c37c9d8104eb92ee7c 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -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 @@ -35,15 +35,12 @@ 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 @@ -53,12 +50,11 @@ 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 @@ -66,7 +62,6 @@ 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 @@ -88,7 +83,6 @@ 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() @@ -96,11 +90,9 @@ class MainActivity : AppCompatActivity() { .build() navOptions.shouldLaunchSingleTop() - viewModel.tocStatus.distinctUntilChanged().observe(this) { + viewModel.tocStatus.observe(this) { if (it != true) { navController.navigate(R.id.TOSFragment, null, navOptions) - } else { - loginViewModel.startLoginFlow() } } @@ -109,34 +101,37 @@ 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() + } + } } } - 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.userType.observe(this) { user -> + viewModel.handleAuthDataJson() + } + + if (signInViewModel.authLiveData.value == null) { + signInViewModel.authLiveData.observe(this) { + viewModel.updateAuthData(it) } + } - 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.uploadFaultyTokenToEcloud(email, descriptionJson.toString()) + viewModel.errorAuthResponse.observe(this) { + onSignInError() + } + + 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() } } } @@ -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 { val path: File = Environment.getDataDirectory() val stat = StatFs(path.path) diff --git a/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt b/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt index f1b93d07ea1dbf263e3cfc7d5090aff1024c04e1..373c5903e352f7b737a8a5d8741daf8e57577ec6 100644 --- a/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt @@ -22,6 +22,7 @@ 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 @@ -34,12 +35,16 @@ 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 @@ -49,16 +54,21 @@ 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, @@ -66,16 +76,37 @@ class MainActivityViewModel @Inject constructor( private val pwaManagerModule: PWAManagerModule, private val ecloudRepository: EcloudRepository, private val blockedAppRepository: BlockedAppRepository, + private val aC2DMTask: AC2DMTask, ) : ViewModel() { + val authDataJson: LiveData = dataStoreModule.authData.asLiveData() val tocStatus: LiveData = dataStoreModule.tocStatus.asLiveData() + val userType: LiveData = dataStoreModule.userType.asLiveData() + private var _authData: MutableLiveData = MutableLiveData() + val authData: LiveData = _authData + val authValidity: MutableLiveData = MutableLiveData() private val _purchaseAppLiveData: MutableLiveData = MutableLiveData() val purchaseAppLiveData: LiveData = _purchaseAppLiveData val isAppPurchased: MutableLiveData = MutableLiveData() val purchaseDeclined: MutableLiveData = 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() + + /* + * Store the time when auth data is fetched for the first time. + * If we try to fetch auth data after timeout, then don't allow it. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404 + */ + var firstAuthDataFetchTime = 0L + + var isTokenValidationCompletedOnce = false // Downloads val downloadList = fusedManagerRepository.getDownloadLiveList() @@ -85,6 +116,9 @@ class MainActivityViewModel @Inject constructor( private val _errorMessageStringResource = MutableLiveData() val errorMessageStringResource: LiveData = _errorMessageStringResource + /* + * Authentication related functions + */ companion object { private const val TAG = "MainActivityViewModel" @@ -92,19 +126,270 @@ class MainActivityViewModel @Inject constructor( } 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 { - return dataStoreModule.getEmail() + private fun isTimeEligibleForTokenRefresh(): Boolean { + 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 { - ecloudRepository.uploadFaultyEmail(email, description) + 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 + 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 */ @@ -131,7 +416,7 @@ class MainActivityViewModel @Inject constructor( * Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/266 */ 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 return true } @@ -175,7 +460,7 @@ class MainActivityViewModel @Inject constructor( */ fun verifyUiFilter(fusedApp: FusedApp, method: () -> Unit) { viewModelScope.launch { - val authData = gPlayAuthData + val authData = authData.value if (fusedApp.filterLevel.isInitialized()) { method() } else { @@ -268,7 +553,7 @@ class MainActivityViewModel @Inject constructor( suspend fun updateAwaitingForPurchasedApp(packageName: String): FusedDownload? { val fusedDownload = fusedManagerRepository.getFusedDownload(packageName = packageName) - gPlayAuthData.let { + authData.value?.let { if (!it.isAnonymous) { try { fusedAPIRepository.updateFusedDownloadWithDownloadingInfo( @@ -309,7 +594,7 @@ class MainActivityViewModel @Inject constructor( fusedDownload: FusedDownload ) { val downloadList = mutableListOf() - gPlayAuthData.let { + authData.value?.let { if (app.type == Type.PWA) { downloadList.add(app.url) fusedDownload.downloadURLList = downloadList diff --git a/app/src/main/java/foundation/e/apps/api/ResultSupreme.kt b/app/src/main/java/foundation/e/apps/api/ResultSupreme.kt index 7a4763301b75172e3c27af5d3d68653e7ae3e96a..1528035137a6ab1d773317698d5071f0669650b3 100644 --- a/app/src/main/java/foundation/e/apps/api/ResultSupreme.kt +++ b/app/src/main/java/foundation/e/apps/api/ResultSupreme.kt @@ -70,7 +70,7 @@ sealed class ResultSupreme { * @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? = null) : this() { + constructor(message: String, exception: Exception = Exception()) : this() { this.message = message this.exception = exception } @@ -91,11 +91,6 @@ sealed class ResultSupreme { var data: T? = null 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. */ @@ -104,7 +99,7 @@ sealed class ResultSupreme { /** * Exception from try-catch block for error cases. */ - var exception: Exception? = null + var exception: Exception = Exception() fun isValidData() = data != null @@ -126,7 +121,7 @@ sealed class ResultSupreme { status: ResultStatus, data: T? = null, message: String = "", - exception: Exception? = null, + exception: Exception = Exception(), ): ResultSupreme { val resultObject = when { status == ResultStatus.OK && data != null -> Success(data) diff --git a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt index 6983e48af6d7c2ba6422462c4fe87b518b7c8e1e..38884b92d2afe347e70f190f636f9503a515e9f6 100644 --- a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +++ b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt @@ -29,6 +29,7 @@ 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 @@ -49,7 +50,6 @@ 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 @@ -57,6 +57,7 @@ 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 @@ -464,6 +465,18 @@ 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, @@ -1043,7 +1056,7 @@ class FusedAPIImpl @Inject constructor( private fun getCategoryIconName(category: FusedCategory): String { var categoryTitle = if (category.tag.getOperationalTag() - .contentEquals(AppTag.GPlay().getOperationalTag()) + .contentEquals(AppTag.GPlay().getOperationalTag()) ) category.id else category.title if (categoryTitle.contains(CATEGORY_TITLE_REPLACEABLE_CONJUNCTION)) { diff --git a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt index 6e99241b9a16c93a61efdfbb6173266347b54c9a..3ea8516d29bcd1a0a54e722dc6a9b3376d7ace11 100644 --- a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt +++ b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt @@ -23,6 +23,7 @@ import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category +import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.data.models.StreamBundle import com.aurora.gplayapi.data.models.StreamCluster import foundation.e.apps.api.ResultSupreme @@ -38,7 +39,9 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPIImpl) { +class FusedAPIRepository @Inject constructor( + private val fusedAPIImpl: FusedAPIImpl +) { var streamBundle = StreamBundle() private set @@ -90,6 +93,13 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPII return fusedAPIImpl.getApplicationCategoryPreference() } + suspend fun validateAuthData(authData: AuthData): PlayResponse { + if (authData.authToken.isNotEmpty() && authData.deviceInfoProvider != null) { + return fusedAPIImpl.validateAuthData(authData) + } + return PlayResponse() + } + suspend fun getApplicationDetails( packageNameList: List, authData: AuthData, @@ -155,6 +165,14 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPII return fusedAPIImpl.getSearchSuggestions(query, authData) } + suspend fun fetchAuthData(): Boolean { + return fusedAPIImpl.fetchAuthData() + } + + suspend fun fetchAuthData(email: String, aasToken: String): AuthData? { + return fusedAPIImpl.fetchAuthData(email, aasToken) + } + fun getSearchResults( query: String, authData: AuthData diff --git a/app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIImpl.kt b/app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIImpl.kt index 66f7ec934259e73e5984accab95f21e150e1a92a..a6d925734e3886464993cfbd469967134f8ae4eb 100644 --- a/app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIImpl.kt +++ b/app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIImpl.kt @@ -18,6 +18,7 @@ package foundation.e.apps.api.gplay +import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.liveData import com.aurora.gplayapi.SearchSuggestEntry @@ -25,6 +26,7 @@ import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category import com.aurora.gplayapi.data.models.File +import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.data.models.SearchBundle import com.aurora.gplayapi.data.models.StreamBundle import com.aurora.gplayapi.data.models.StreamCluster @@ -35,13 +37,66 @@ import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.gplayapi.helpers.SearchHelper import com.aurora.gplayapi.helpers.StreamHelper import com.aurora.gplayapi.helpers.TopChartsHelper +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.api.gplay.token.TokenRepository +import foundation.e.apps.api.gplay.utils.CustomAuthValidator import foundation.e.apps.api.gplay.utils.GPlayHttpClient +import foundation.e.apps.utils.modules.DataStoreModule import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext import javax.inject.Inject -class GPlayAPIImpl @Inject constructor(private val gPlayHttpClient: GPlayHttpClient) { +class GPlayAPIImpl @Inject constructor( + @ApplicationContext private val context: Context, + private val tokenRepository: TokenRepository, + private val dataStoreModule: DataStoreModule, + private val gPlayHttpClient: GPlayHttpClient +) { + + /** + * Save auth data to preferences. + * Updated for network failures. + * Issue: + * https://gitlab.e.foundation/e/backlog/-/issues/5413 + * https://gitlab.e.foundation/e/backlog/-/issues/5404 + * + * @return true or false based on if the request was successful. + */ + // TODO: DON'T HARDCODE DISPATCHERS IN ANY METHODS + suspend fun fetchAuthData(): Boolean = withContext(Dispatchers.IO) { + val data = async { tokenRepository.getAuthData() } + data.await().let { + if (it == null) return@withContext false + it.locale = context.resources.configuration.locales[0] // update locale with the default locale from settings + dataStoreModule.saveCredentials(it) + return@withContext true + } + } + + suspend fun fetchAuthData(email: String, aasToken: String): AuthData? { + val authData = tokenRepository.getAuthData(email, aasToken) + if (authData.authToken.isNotEmpty() && authData.deviceInfoProvider != null) { + dataStoreModule.saveCredentials(authData) + return authData + } + return null + } + + suspend fun validateAuthData(authData: AuthData): PlayResponse { + var result = PlayResponse() + withContext(Dispatchers.IO) { + try { + val authValidator = CustomAuthValidator(authData).using(gPlayHttpClient) + result = authValidator.getValidityResponse() + } catch (e: Exception) { + e.printStackTrace() + throw e + } + } + return result + } suspend fun getSearchSuggestions(query: String, authData: AuthData): List { val searchData = mutableListOf() diff --git a/app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIRepository.kt b/app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIRepository.kt index f8c735a7d7249d3dfd4d9e14af3768fc1a76c141..e0309bb85a97d66f42ed7598faa874735b07fb20 100644 --- a/app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIRepository.kt +++ b/app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIRepository.kt @@ -24,12 +24,27 @@ import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category import com.aurora.gplayapi.data.models.File +import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.data.models.StreamBundle import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.helpers.TopChartsHelper import javax.inject.Inject -class GPlayAPIRepository @Inject constructor(private val gPlayAPIImpl: GPlayAPIImpl) { +class GPlayAPIRepository @Inject constructor( + private val gPlayAPIImpl: GPlayAPIImpl +) { + + suspend fun fetchAuthData(): Boolean { + return gPlayAPIImpl.fetchAuthData() + } + + suspend fun fetchAuthData(email: String, aasToken: String): AuthData? { + return gPlayAPIImpl.fetchAuthData(email, aasToken) + } + + suspend fun validateAuthData(authData: AuthData): PlayResponse { + return gPlayAPIImpl.validateAuthData(authData) + } suspend fun getSearchSuggestions(query: String, authData: AuthData): List { return gPlayAPIImpl.getSearchSuggestions(query, authData) diff --git a/app/src/main/java/foundation/e/apps/api/gplay/token/TokenImpl.kt b/app/src/main/java/foundation/e/apps/api/gplay/token/TokenImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..e9578de027f924f163300590843b87ceea2c0845 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/gplay/token/TokenImpl.kt @@ -0,0 +1,51 @@ +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.api.gplay.token + +import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.helpers.AuthHelper +import com.google.gson.Gson +import foundation.e.apps.api.gplay.utils.GPlayHttpClient +import java.util.Properties +import javax.inject.Inject + +class TokenImpl @Inject constructor( + private val nativeDeviceProperty: Properties, + private val gson: Gson, + private val gPlayHttpClient: GPlayHttpClient +) { + + companion object { + const val BASE_URL = "https://eu.gtoken.ecloud.global" + } + + fun getAuthData(): AuthData? { + val playResponse = + gPlayHttpClient.postAuth(BASE_URL, gson.toJson(nativeDeviceProperty).toByteArray()) + return if (playResponse.isSuccessful) { + gson.fromJson(String(playResponse.responseBytes), AuthData::class.java) + } else { + null + } + } + + fun getAuthData(email: String, aasToken: String): AuthData { + return AuthHelper.build(email, aasToken, nativeDeviceProperty) + } +} diff --git a/app/src/main/java/foundation/e/apps/login/api/GPlayLoginInterface.kt b/app/src/main/java/foundation/e/apps/api/gplay/token/TokenRepository.kt similarity index 58% rename from app/src/main/java/foundation/e/apps/login/api/GPlayLoginInterface.kt rename to app/src/main/java/foundation/e/apps/api/gplay/token/TokenRepository.kt index 24b212bbcb7aa20a5c33554668eedf039b3ded91..f5c7c9a355f75c28d75f3f43b4e25f36c69065e0 100644 --- a/app/src/main/java/foundation/e/apps/login/api/GPlayLoginInterface.kt +++ b/app/src/main/java/foundation/e/apps/api/gplay/token/TokenRepository.kt @@ -1,26 +1,35 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.login.api - -import com.aurora.gplayapi.data.models.AuthData -import com.aurora.gplayapi.data.models.PlayResponse - -interface GPlayLoginInterface { - suspend fun login(authData: AuthData): PlayResponse - suspend fun fetchAuthData(email: String, aasToken: String): AuthData? -} +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.api.gplay.token + +import com.aurora.gplayapi.data.models.AuthData +import javax.inject.Inject + +class TokenRepository @Inject constructor( + private val tokenImpl: TokenImpl +) { + + fun getAuthData(): AuthData? { + return tokenImpl.getAuthData() + } + + fun getAuthData(email: String, aasToken: String): AuthData { + return tokenImpl.getAuthData(email, aasToken) + } +} diff --git a/app/src/main/java/foundation/e/apps/api/gplay/utils/GPlayHttpClient.kt b/app/src/main/java/foundation/e/apps/api/gplay/utils/GPlayHttpClient.kt index 8b222cae76f478fe5fa80186959b522bc8eb5c68..258f1b58acdfbb6f09c1613e24b9f6f660912ff9 100644 --- a/app/src/main/java/foundation/e/apps/api/gplay/utils/GPlayHttpClient.kt +++ b/app/src/main/java/foundation/e/apps/api/gplay/utils/GPlayHttpClient.kt @@ -21,6 +21,7 @@ package foundation.e.apps.api.gplay.utils import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.network.IHttpClient +import foundation.e.apps.utils.modules.CommonUtilsModule.timeoutDurationInMillis import okhttp3.Cache import okhttp3.Headers.Companion.toHeaders import okhttp3.HttpUrl @@ -35,6 +36,7 @@ import timber.log.Timber import java.io.IOException import java.net.SocketTimeoutException import java.net.UnknownHostException +import java.util.concurrent.TimeUnit import javax.inject.Inject class GPlayHttpClient @Inject constructor( @@ -49,6 +51,10 @@ class GPlayHttpClient @Inject constructor( } private val okHttpClient = OkHttpClient().newBuilder() + .connectTimeout(timeoutDurationInMillis, TimeUnit.MILLISECONDS) + .readTimeout(timeoutDurationInMillis, TimeUnit.MILLISECONDS) + .writeTimeout(timeoutDurationInMillis, TimeUnit.MILLISECONDS) + .callTimeout(timeoutDurationInMillis, TimeUnit.MILLISECONDS) .retryOnConnectionFailure(false) .followRedirects(true) .followSslRedirects(true) @@ -155,9 +161,7 @@ class GPlayHttpClient @Inject constructor( private fun handleExceptionOnGooglePlayRequest(e: Exception): PlayResponse { Timber.e("processRequest: ${e.localizedMessage}") - return PlayResponse().apply { - errorString = "${this@GPlayHttpClient::class.java.simpleName}: ${e.localizedMessage}" - } + return PlayResponse() } private fun buildUrl(url: String, params: Map): HttpUrl { diff --git a/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt index fb4335c9c41e84e95366e562cc30748f2aebdab3..b84fbca35a8e09f920b327e32a6642c2c5599cbe 100644 --- a/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt +++ b/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt @@ -27,7 +27,6 @@ import android.text.format.Formatter import android.view.View import android.widget.ImageView import android.widget.RelativeLayout -import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.core.graphics.BlendModeColorFilterCompat import androidx.core.graphics.BlendModeCompat @@ -39,6 +38,7 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import coil.load +import com.aurora.gplayapi.data.models.AuthData import com.google.android.material.button.MaterialButton import com.google.android.material.snackbar.Snackbar import com.google.android.material.textview.MaterialTextView @@ -53,7 +53,6 @@ import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.application.model.ApplicationScreenshotsRVAdapter import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.databinding.FragmentApplicationBinding -import foundation.e.apps.login.AuthObject import foundation.e.apps.manager.download.data.DownloadProgress import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.utils.enums.Origin @@ -61,7 +60,6 @@ import foundation.e.apps.utils.enums.ResultStatus import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.User import foundation.e.apps.utils.enums.isInitialized -import foundation.e.apps.utils.exceptions.GPlayLoginException import foundation.e.apps.utils.modules.CommonUtilsModule.LIST_OF_NULL import foundation.e.apps.utils.modules.PWAManagerModule import foundation.e.apps.utils.parentFragment.TimeoutFragment @@ -132,15 +130,15 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { super.onViewCreated(view, savedInstanceState) _binding = FragmentApplicationBinding.bind(view) - setupListening() + /* + * Explanation of double observers in HomeFragment.kt + */ - authObjects.observe(viewLifecycleOwner) { - if (it == null) return@observe - loadData(it) + mainActivityViewModel.internetConnection.observe(viewLifecycleOwner) { + refreshDataOrRefreshToken(mainActivityViewModel) } - - applicationViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { - handleExceptionsCommon(it) + mainActivityViewModel.authData.observe(viewLifecycleOwner) { + refreshDataOrRefreshToken(mainActivityViewModel) } setupToolbar(view) @@ -162,20 +160,23 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { resultPair: Pair, ) { if (resultPair.second != ResultStatus.OK) { + onTimeout() return } /* - * Previously fusedApp only had instance of FusedApp. - * As such previously all reference was simply using "it", the default variable in - * the scope. But now "it" is Pair(FusedApp, ResultStatus), not an instance of FusedApp. - * - * Avoid Git diffs by using a variable named "it". - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 - */ + * Previously fusedApp only had instance of FusedApp. + * As such previously all reference was simply using "it", the default variable in + * the scope. But now "it" is Pair(FusedApp, ResultStatus), not an instance of FusedApp. + * + * Avoid Git diffs by using a variable named "it". + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 + */ val it = resultPair.first + dismissTimeoutDialog() + isDetailsLoaded = true if (applicationViewModel.appStatus.value == null) { applicationViewModel.appStatus.value = it.status @@ -359,40 +360,46 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } } - override fun loadData(authObjectList: List) { + override fun onTimeout() { + if (!isTimeoutDialogDisplayed()) { + stopLoadingUI() + displayTimeoutAlertDialog( + timeoutFragment = this, + activity = requireActivity(), + message = getString(R.string.timeout_desc_cleanapk), + positiveButtonText = getString(R.string.retry), + positiveButtonBlock = { + showLoadingUI() + resetTimeoutDialogLock() + mainActivityViewModel.retryFetchingTokenAfterTimeout() + }, + negativeButtonText = getString(android.R.string.ok), + negativeButtonBlock = { + requireActivity().onBackPressed() + }, + allowCancel = false, + ) + } + } + + override fun refreshData(authData: AuthData) { if (isDetailsLoaded) return /* Show the loading bar. */ showLoadingUI() /* Remove trailing slash (if present) that can become part of the packageName */ val packageName = args.packageName.run { if (endsWith('/')) dropLast(1) else this } - - applicationViewModel.loadData(args.id, packageName, origin, isFdroidDeepLink, authObjectList) { - clearAndRestartGPlayLogin() - true + if (isFdroidDeepLink) { + applicationViewModel.getCleanapkAppDetails(packageName) + } else { + applicationViewModel.getApplicationDetails( + args.id, + packageName, + authData, + origin + ) } } - override fun onTimeout( - exception: Exception, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog - } - - override fun onSignInError( - exception: GPlayLoginException, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog - } - - override fun onDataLoadError( - exception: Exception, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog - } - private fun observeDownloadStatus(view: View) { applicationViewModel.appStatus.observe(viewLifecycleOwner) { status -> val installButton = binding.downloadInclude.installButton @@ -485,7 +492,11 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { view: View ) { installButton.setOnClickListener { - val errorMsg = when (mainActivityViewModel.getUser()) { + val errorMsg = when ( + User.valueOf( + mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name + ) + ) { User.ANONYMOUS, User.UNAVAILABLE -> getString(R.string.install_blocked_anonymous) User.GOOGLE -> getString(R.string.install_blocked_google) @@ -719,12 +730,12 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } } - override fun showLoadingUI() { + private fun showLoadingUI() { binding.applicationLayout.visibility = View.GONE binding.progressBar.visibility = View.VISIBLE } - override fun stopLoadingUI() { + private fun stopLoadingUI() { binding.applicationLayout.visibility = View.VISIBLE binding.progressBar.visibility = View.GONE } diff --git a/app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt b/app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt index 886bcadbb33bbb22829e69cfa4f63a5df3795712..794904192fa4527382bd138474219e54551b3a78 100644 --- a/app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt +++ b/app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt @@ -19,6 +19,7 @@ package foundation.e.apps.application import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.exceptions.ApiException @@ -26,7 +27,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.R import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp -import foundation.e.apps.login.AuthObject import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.download.data.DownloadProgress import foundation.e.apps.manager.download.data.DownloadProgressLD @@ -34,9 +34,6 @@ import foundation.e.apps.manager.fused.FusedManagerRepository import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.ResultStatus import foundation.e.apps.utils.enums.Status -import foundation.e.apps.utils.exceptions.CleanApkException -import foundation.e.apps.utils.exceptions.GPlayException -import foundation.e.apps.utils.parentFragment.LoadingViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject @@ -46,7 +43,7 @@ class ApplicationViewModel @Inject constructor( downloadProgressLD: DownloadProgressLD, private val fusedAPIRepository: FusedAPIRepository, private val fusedManagerRepository: FusedManagerRepository, -) : LoadingViewModel() { +) : ViewModel() { val fusedApp: MutableLiveData> = MutableLiveData() val appStatus: MutableLiveData = MutableLiveData() @@ -54,66 +51,17 @@ class ApplicationViewModel @Inject constructor( private val _errorMessageLiveData: MutableLiveData = MutableLiveData() val errorMessageLiveData: MutableLiveData = _errorMessageLiveData - fun loadData( - id: String, - packageName: String, - origin: Origin, - isFdroidLink: Boolean, - authObjectList: List, - retryBlock: (failedObjects: List) -> Boolean, - ) { - - if (isFdroidLink) { - getCleanapkAppDetails(packageName) - return - } - - val gPlayObj = authObjectList.find { it is AuthObject.GPlayAuth } - - /* - * If user is viewing only open source apps, auth object list will not have - * GPlayAuth, it will only have CleanApkAuth. - */ - if (gPlayObj == null && origin == Origin.GPLAY) { - _errorMessageLiveData.postValue(R.string.gplay_data_for_oss) - return - } - - super.onLoadData(authObjectList, { successAuthList, _ -> - - successAuthList.find { it is AuthObject.GPlayAuth }?.run { - getApplicationDetails(id, packageName, result.data!! as AuthData, origin) - return@onLoadData - } - - successAuthList.find { it is AuthObject.CleanApk }?.run { - getApplicationDetails(id, packageName, AuthData("", ""), origin) - return@onLoadData - } - }, retryBlock) - } - fun getApplicationDetails(id: String, packageName: String, authData: AuthData, origin: Origin) { viewModelScope.launch(Dispatchers.IO) { try { - val appData = + fusedApp.postValue( fusedAPIRepository.getApplicationDetails( id, packageName, authData, origin ) - fusedApp.postValue(appData) - - if (appData.second != ResultStatus.OK) { - val exception = - if (authData.aasToken.isNotBlank() || authData.authToken.isNotBlank()) - GPlayException(appData.second == ResultStatus.TIMEOUT, "Data load error") - else CleanApkException(appData.second == ResultStatus.TIMEOUT, "Data load error") - - exceptionsList.add(exception) - exceptionsLiveData.postValue(exceptionsList) - } + ) } catch (e: ApiException.AppNotFound) { _errorMessageLiveData.postValue(R.string.app_not_found) } catch (e: Exception) { diff --git a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt index 6209dbde8ce79f6f8d5984f57d85bfca6c2b1fd0..534d0b49ae4afdb8deeffdd34046399502675452 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt @@ -21,7 +21,6 @@ package foundation.e.apps.applicationlist import android.os.Bundle import android.view.View import android.widget.ImageView -import androidx.appcompat.app.AlertDialog import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope @@ -30,6 +29,7 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.aurora.gplayapi.data.models.AuthData import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.AppInfoFetchViewModel import foundation.e.apps.AppProgressViewModel @@ -41,11 +41,9 @@ import foundation.e.apps.api.fused.FusedAPIInterface import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.databinding.FragmentApplicationListBinding -import foundation.e.apps.login.AuthObject import foundation.e.apps.manager.download.data.DownloadProgress import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.utils.enums.Status -import foundation.e.apps.utils.exceptions.GPlayLoginException import foundation.e.apps.utils.modules.PWAManagerModule import foundation.e.apps.utils.parentFragment.TimeoutFragment import kotlinx.coroutines.launch @@ -83,15 +81,15 @@ class ApplicationListFragment : setupRecyclerView(view) observeAppListLiveData() - setupListening() + /* + * Explanation of double observers in HomeFragment.kt + */ - authObjects.observe(viewLifecycleOwner) { - if (it == null) return@observe - loadData(it) + mainActivityViewModel.internetConnection.observe(viewLifecycleOwner) { + refreshDataOrRefreshToken(mainActivityViewModel) } - - viewModel.exceptionsLiveData.observe(viewLifecycleOwner) { - handleExceptionsCommon(it) + mainActivityViewModel.authData.observe(viewLifecycleOwner) { + refreshDataOrRefreshToken(mainActivityViewModel) } } @@ -123,6 +121,11 @@ class ApplicationListFragment : mainActivityViewModel.updateStatusOfFusedApps(it, list) adapter.setData(it) } + + /* + * Done in one line, so that on Ctrl+click on appListLiveData, + * we can see that it is being updated here. + */ } } @@ -136,17 +139,18 @@ class ApplicationListFragment : super.onResume() if (listAdapter.currentList.isNotEmpty() && viewModel.hasAnyAppInstallStatusChanged(listAdapter.currentList)) { - /*mainActivityViewModel.authData.value?.let { + mainActivityViewModel.authData.value?.let { refreshData(it) - }*/ - repostAuthObjects() + } } } private fun observeAppListLiveData() { viewModel.appListLiveData.observe(viewLifecycleOwner) { stopLoadingUI() - if (it.isSuccess()) { + if (!it.isSuccess()) { + onTimeout() + } else { if (!isFusedAppsUpdated(it)) { return@observe } @@ -226,7 +230,31 @@ class ApplicationListFragment : currentList ) - override fun loadData(authObjectList: List) { + override fun onTimeout() { + if (!isTimeoutDialogDisplayed()) { + stopLoadingUI() + displayTimeoutAlertDialog( + timeoutFragment = this, + activity = requireActivity(), + message = getString(R.string.timeout_desc_cleanapk), + positiveButtonText = getString(R.string.retry), + positiveButtonBlock = { + showLoadingUI() + resetTimeoutDialogLock() + mainActivityViewModel.retryFetchingTokenAfterTimeout() + }, + negativeButtonText = getString(android.R.string.ok), + negativeButtonBlock = {}, + allowCancel = true, + ) + } + } + + override fun refreshData(authData: AuthData) { + + /* + * Code moved from onResume() + */ /* * If details are once loaded, do not load details again, @@ -239,10 +267,12 @@ class ApplicationListFragment : * Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/478 */ showLoadingUI() - viewModel.loadData(args.category, args.browseUrl, args.source, authObjectList) { - clearAndRestartGPlayLogin() - true - } + viewModel.getList( + args.category, + args.browseUrl, + authData, + args.source + ) if (args.source != "Open Source" && args.source != "PWA") { /* @@ -253,10 +283,7 @@ class ApplicationListFragment : override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) if (!recyclerView.canScrollVertically(1)) { - viewModel.loadMore( - authObjectList.find { it is AuthObject.GPlayAuth }, - args.browseUrl - ) + viewModel.loadMore(authData, args.browseUrl) } } }) @@ -270,44 +297,20 @@ class ApplicationListFragment : binding.recyclerView.adapter.apply { if (this is ApplicationListRVAdapter) { onPlaceHolderShow = { - viewModel.loadMore( - authObjectList.find { it is AuthObject.GPlayAuth }, - args.browseUrl - ) + viewModel.loadMore(authData, args.browseUrl) } } } } } - override fun onTimeout( - exception: Exception, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog - } - - override fun onSignInError( - exception: GPlayLoginException, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog - } - - override fun onDataLoadError( - exception: Exception, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog - } - - override fun showLoadingUI() { + private fun showLoadingUI() { binding.shimmerLayout.startShimmer() binding.shimmerLayout.visibility = View.VISIBLE binding.recyclerView.visibility = View.GONE } - override fun stopLoadingUI() { + private fun stopLoadingUI() { binding.shimmerLayout.stopShimmer() binding.shimmerLayout.visibility = View.GONE binding.recyclerView.visibility = View.VISIBLE diff --git a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt index 4fe45743905407dd0335a31bb409529526267e68..106d47379f48ffe1101ffffdcd488519adb4d406 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt @@ -19,16 +19,13 @@ package foundation.e.apps.applicationlist import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.AuthData import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.api.ResultSupreme import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp -import foundation.e.apps.login.AuthObject -import foundation.e.apps.utils.exceptions.CleanApkException -import foundation.e.apps.utils.exceptions.GPlayException -import foundation.e.apps.utils.parentFragment.LoadingViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject @@ -36,52 +33,21 @@ import javax.inject.Inject @HiltViewModel class ApplicationListViewModel @Inject constructor( private val fusedAPIRepository: FusedAPIRepository -) : LoadingViewModel() { +) : ViewModel() { val appListLiveData: MutableLiveData>> = MutableLiveData() var isLoading = false - fun loadData( - category: String, - browseUrl: String, - source: String, - authObjectList: List, - retryBlock: (failedObjects: List) -> Boolean, - ) { - super.onLoadData(authObjectList, { successAuthList, _ -> - - successAuthList.find { it is AuthObject.GPlayAuth }?.run { - getList(category, browseUrl, result.data!! as AuthData, source) - return@onLoadData - } - - successAuthList.find { it is AuthObject.CleanApk }?.run { - getList(category, browseUrl, AuthData("", ""), source) - return@onLoadData - } - }, retryBlock) - } - fun getList(category: String, browseUrl: String, authData: AuthData, source: String) { if (isLoading) { return } viewModelScope.launch(Dispatchers.IO) { isLoading = true - val result = fusedAPIRepository.getAppList(category, browseUrl, authData, source).apply { + fusedAPIRepository.getAppList(category, browseUrl, authData, source).apply { isLoading = false - } - appListLiveData.postValue(result) - - if (!result.isSuccess()) { - val exception = - if (authData.aasToken.isNotBlank() || authData.authToken.isNotBlank()) - GPlayException(result.isTimeout(), "Data load error") - else CleanApkException(result.isTimeout(), "Data load error") - - exceptionsList.add(exception) - exceptionsLiveData.postValue(exceptionsList) + appListLiveData.postValue(this) } } } @@ -96,16 +62,9 @@ class ApplicationListViewModel @Inject constructor( return fusedAPIRepository.isAnyFusedAppUpdated(newFusedApps, oldFusedApps) } - fun loadMore(gPlayAuth: AuthObject?, browseUrl: String) { + fun loadMore(authData: AuthData, browseUrl: String) { viewModelScope.launch { - - val authData: AuthData? = when { - gPlayAuth !is AuthObject.GPlayAuth -> null - !gPlayAuth.result.isSuccess() -> null - else -> gPlayAuth.result.data!! - } - - if (isLoading || authData == null) { + if (isLoading) { return@launch } @@ -132,7 +91,7 @@ class ApplicationListViewModel @Inject constructor( * for the same data. */ if (result.first.isSuccess() && !result.second && fusedAPIRepository.canLoadMore()) { - loadMore(gPlayAuth, browseUrl) + loadMore(authData, browseUrl) } } } diff --git a/app/src/main/java/foundation/e/apps/categories/AppsFragment.kt b/app/src/main/java/foundation/e/apps/categories/AppsFragment.kt index 07497a782c24b47466508aff1f9050b777ca38ec..0335553b1a9fec7e0dc4a209b4a338ad05c6be5b 100644 --- a/app/src/main/java/foundation/e/apps/categories/AppsFragment.kt +++ b/app/src/main/java/foundation/e/apps/categories/AppsFragment.kt @@ -20,18 +20,17 @@ package foundation.e.apps.categories import android.os.Bundle import android.view.View -import androidx.appcompat.app.AlertDialog import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager +import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.MainActivityViewModel import foundation.e.apps.R import foundation.e.apps.categories.model.CategoriesRVAdapter import foundation.e.apps.databinding.FragmentAppsBinding -import foundation.e.apps.login.AuthObject -import foundation.e.apps.utils.exceptions.GPlayLoginException +import foundation.e.apps.utils.enums.ResultStatus import foundation.e.apps.utils.parentFragment.TimeoutFragment @AndroidEntryPoint @@ -46,17 +45,22 @@ class AppsFragment : TimeoutFragment(R.layout.fragment_apps) { super.onViewCreated(view, savedInstanceState) _binding = FragmentAppsBinding.bind(view) - setupListening() + /* + * Explanation of double observers in HomeFragment.kt + */ - authObjects.observe(viewLifecycleOwner) { - if (it == null) return@observe - loadData(it) + mainActivityViewModel.internetConnection.observe(viewLifecycleOwner) { + refreshDataOrRefreshToken(mainActivityViewModel) } - - categoriesViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { - handleExceptionsCommon(it) + mainActivityViewModel.authData.observe(viewLifecycleOwner) { + refreshDataOrRefreshToken(mainActivityViewModel) } + /* + * Code regarding is just moved outside the observers. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 + */ + val categoriesRVAdapter = CategoriesRVAdapter() val recyclerView = binding.recyclerView @@ -69,44 +73,47 @@ class AppsFragment : TimeoutFragment(R.layout.fragment_apps) { categoriesViewModel.categoriesList.observe(viewLifecycleOwner) { stopLoadingUI() categoriesRVAdapter.setData(it.first) + if (it.third != ResultStatus.OK) { + onTimeout() + } } } - override fun loadData(authObjectList: List) { - categoriesViewModel.loadData(Category.Type.APPLICATION, authObjectList) { - clearAndRestartGPlayLogin() - true + override fun onTimeout() { + if (!isTimeoutDialogDisplayed()) { + stopLoadingUI() + displayTimeoutAlertDialog( + timeoutFragment = this, + activity = requireActivity(), + message = getString(R.string.timeout_desc_cleanapk), + positiveButtonText = getString(android.R.string.ok), + positiveButtonBlock = {}, + negativeButtonText = getString(R.string.retry), + negativeButtonBlock = { + showLoadingUI() + resetTimeoutDialogLock() + mainActivityViewModel.retryFetchingTokenAfterTimeout() + }, + allowCancel = true, + ) } } - override fun onTimeout( - exception: Exception, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog - } - - override fun onSignInError( - exception: GPlayLoginException, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog - } - - override fun onDataLoadError( - exception: Exception, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog + override fun refreshData(authData: AuthData) { + showLoadingUI() + categoriesViewModel.getCategoriesList( + Category.Type.APPLICATION, + authData + ) } - override fun showLoadingUI() { + private fun showLoadingUI() { binding.shimmerLayout.startShimmer() binding.shimmerLayout.visibility = View.VISIBLE binding.recyclerView.visibility = View.GONE } - override fun stopLoadingUI() { + private fun stopLoadingUI() { binding.shimmerLayout.stopShimmer() binding.shimmerLayout.visibility = View.GONE binding.recyclerView.visibility = View.VISIBLE @@ -114,6 +121,7 @@ class AppsFragment : TimeoutFragment(R.layout.fragment_apps) { override fun onResume() { super.onResume() + resetTimeoutDialogLock() binding.shimmerLayout.startShimmer() } diff --git a/app/src/main/java/foundation/e/apps/categories/CategoriesFragment.kt b/app/src/main/java/foundation/e/apps/categories/CategoriesFragment.kt index 59e94943538673006824dbf3a85234ad5edc0c1a..3eb07a1d81f05ce0509314e401e7408fcc3d5fd0 100644 --- a/app/src/main/java/foundation/e/apps/categories/CategoriesFragment.kt +++ b/app/src/main/java/foundation/e/apps/categories/CategoriesFragment.kt @@ -20,15 +20,17 @@ package foundation.e.apps.categories import android.os.Bundle import android.view.View -import androidx.fragment.app.Fragment +import com.aurora.gplayapi.data.models.AuthData import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.categories.model.CategoriesVPAdapter import foundation.e.apps.databinding.FragmentCategoriesBinding +import foundation.e.apps.utils.parentFragment.TimeoutFragment +import timber.log.Timber @AndroidEntryPoint -class CategoriesFragment : Fragment(R.layout.fragment_categories) { +class CategoriesFragment : TimeoutFragment(R.layout.fragment_categories) { private var _binding: FragmentCategoriesBinding? = null private val binding get() = _binding!! @@ -53,4 +55,25 @@ class CategoriesFragment : Fragment(R.layout.fragment_categories) { super.onDestroyView() _binding = null } + + override fun onTimeout() { + val position = binding.viewPager.currentItem + + val fragment = childFragmentManager.fragments.find { + when (position) { + 0 -> it is AppsFragment + 1 -> it is GamesFragment + else -> false + } + } + + fragment?.let { + if (it is TimeoutFragment) { + Timber.d("Showing timeout on Categories fragment: " + it::class.java.name) + it.onTimeout() + } + } + } + + override fun refreshData(authData: AuthData) {} } diff --git a/app/src/main/java/foundation/e/apps/categories/CategoriesViewModel.kt b/app/src/main/java/foundation/e/apps/categories/CategoriesViewModel.kt index 13456df3c577b58c62a059ac0ac37c4fd10d045f..ec72f9150b308b3c46fabdf755f2ff95a350d32c 100644 --- a/app/src/main/java/foundation/e/apps/categories/CategoriesViewModel.kt +++ b/app/src/main/java/foundation/e/apps/categories/CategoriesViewModel.kt @@ -19,61 +19,28 @@ package foundation.e.apps.categories import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedCategory -import foundation.e.apps.login.AuthObject import foundation.e.apps.utils.enums.ResultStatus -import foundation.e.apps.utils.exceptions.CleanApkException -import foundation.e.apps.utils.exceptions.GPlayException -import foundation.e.apps.utils.parentFragment.LoadingViewModel import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class CategoriesViewModel @Inject constructor( private val fusedAPIRepository: FusedAPIRepository -) : LoadingViewModel() { +) : ViewModel() { val categoriesList: MutableLiveData, String, ResultStatus>> = MutableLiveData() - fun loadData( - type: Category.Type, - authObjectList: List, - retryBlock: (failedObjects: List) -> Boolean, - ) { - super.onLoadData(authObjectList, { successAuthList, _ -> - - successAuthList.find { it is AuthObject.GPlayAuth }?.run { - getCategoriesList(type, result.data!! as AuthData) - return@onLoadData - } - - successAuthList.find { it is AuthObject.CleanApk }?.run { - getCategoriesList(type, AuthData("", "")) - return@onLoadData - } - }, retryBlock) - } - fun getCategoriesList(type: Category.Type, authData: AuthData) { viewModelScope.launch { - val categoriesData = fusedAPIRepository.getCategoriesList(type, authData) - categoriesList.postValue(categoriesData) - - if (categoriesData.third != ResultStatus.OK) { - val exception = - if (authData.aasToken.isNotBlank() || authData.authToken.isNotBlank()) - GPlayException(categoriesData.third == ResultStatus.TIMEOUT, "Data load error") - else CleanApkException(categoriesData.third == ResultStatus.TIMEOUT, "Data load error") - - exceptionsList.add(exception) - exceptionsLiveData.postValue(exceptionsList) - } + categoriesList.postValue(fusedAPIRepository.getCategoriesList(type, authData)) } } diff --git a/app/src/main/java/foundation/e/apps/categories/GamesFragment.kt b/app/src/main/java/foundation/e/apps/categories/GamesFragment.kt index f5d9fe62bc4a5cdc1d93fbb5417a0bc7a28038be..1094519744559f0633b6132f7bca100c2cfa09d1 100644 --- a/app/src/main/java/foundation/e/apps/categories/GamesFragment.kt +++ b/app/src/main/java/foundation/e/apps/categories/GamesFragment.kt @@ -20,18 +20,17 @@ package foundation.e.apps.categories import android.os.Bundle import android.view.View -import androidx.appcompat.app.AlertDialog import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager +import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.MainActivityViewModel import foundation.e.apps.R import foundation.e.apps.categories.model.CategoriesRVAdapter import foundation.e.apps.databinding.FragmentGamesBinding -import foundation.e.apps.login.AuthObject -import foundation.e.apps.utils.exceptions.GPlayLoginException +import foundation.e.apps.utils.enums.ResultStatus import foundation.e.apps.utils.parentFragment.TimeoutFragment @AndroidEntryPoint @@ -46,15 +45,15 @@ class GamesFragment : TimeoutFragment(R.layout.fragment_games) { super.onViewCreated(view, savedInstanceState) _binding = FragmentGamesBinding.bind(view) - setupListening() + /* + * Explanation of double observers in HomeFragment.kt + */ - authObjects.observe(viewLifecycleOwner) { - if (it == null) return@observe - loadData(it) + mainActivityViewModel.internetConnection.observe(viewLifecycleOwner) { + refreshDataOrRefreshToken(mainActivityViewModel) } - - categoriesViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { - handleExceptionsCommon(it) + mainActivityViewModel.authData.observe(viewLifecycleOwner) { + refreshDataOrRefreshToken(mainActivityViewModel) } val categoriesRVAdapter = CategoriesRVAdapter() @@ -69,44 +68,47 @@ class GamesFragment : TimeoutFragment(R.layout.fragment_games) { categoriesViewModel.categoriesList.observe(viewLifecycleOwner) { stopLoadingUI() categoriesRVAdapter.setData(it.first) + if (it.third != ResultStatus.OK) { + onTimeout() + } } } - override fun loadData(authObjectList: List) { - categoriesViewModel.loadData(Category.Type.GAME, authObjectList) { - clearAndRestartGPlayLogin() - true + override fun onTimeout() { + if (!isTimeoutDialogDisplayed()) { + stopLoadingUI() + displayTimeoutAlertDialog( + timeoutFragment = this, + activity = requireActivity(), + message = getString(R.string.timeout_desc_cleanapk), + positiveButtonText = getString(android.R.string.ok), + positiveButtonBlock = {}, + negativeButtonText = getString(R.string.retry), + negativeButtonBlock = { + showLoadingUI() + resetTimeoutDialogLock() + mainActivityViewModel.retryFetchingTokenAfterTimeout() + }, + allowCancel = true, + ) } } - override fun onTimeout( - exception: Exception, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog - } - - override fun onSignInError( - exception: GPlayLoginException, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog - } - - override fun onDataLoadError( - exception: Exception, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog + override fun refreshData(authData: AuthData) { + showLoadingUI() + categoriesViewModel.getCategoriesList( + Category.Type.GAME, + authData + ) } - override fun showLoadingUI() { + private fun showLoadingUI() { binding.shimmerLayout.startShimmer() binding.shimmerLayout.visibility = View.VISIBLE binding.recyclerView.visibility = View.GONE } - override fun stopLoadingUI() { + private fun stopLoadingUI() { binding.shimmerLayout.stopShimmer() binding.shimmerLayout.visibility = View.GONE binding.recyclerView.visibility = View.VISIBLE @@ -114,6 +116,7 @@ class GamesFragment : TimeoutFragment(R.layout.fragment_games) { override fun onResume() { super.onResume() + resetTimeoutDialogLock() binding.shimmerLayout.startShimmer() } diff --git a/app/src/main/java/foundation/e/apps/di/LoginModule.kt b/app/src/main/java/foundation/e/apps/di/LoginModule.kt deleted file mode 100644 index 559d7e84da66af3c455937cdbf1b4f05fe01cadd..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/di/LoginModule.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.di - -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import foundation.e.apps.login.LoginSourceCleanApk -import foundation.e.apps.login.LoginSourceGPlay -import foundation.e.apps.login.LoginSourceInterface - -@InstallIn(SingletonComponent::class) -@Module -object LoginModule { - @Provides - fun providesLoginSources( - gPlay: LoginSourceGPlay, - cleanApk: LoginSourceCleanApk, - ): List { - return listOf(gPlay, cleanApk) - } -} diff --git a/app/src/main/java/foundation/e/apps/home/HomeFragment.kt b/app/src/main/java/foundation/e/apps/home/HomeFragment.kt index 31dc533114dc8ff96b272a9b4d15db9b41145a7e..7866ead064c31a7069626c511fadaf55802e238f 100644 --- a/app/src/main/java/foundation/e/apps/home/HomeFragment.kt +++ b/app/src/main/java/foundation/e/apps/home/HomeFragment.kt @@ -21,18 +21,19 @@ package foundation.e.apps.home import android.os.Bundle import android.view.View import android.widget.ImageView -import androidx.appcompat.app.AlertDialog import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.aurora.gplayapi.data.models.AuthData import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.AppInfoFetchViewModel import foundation.e.apps.AppProgressViewModel import foundation.e.apps.MainActivityViewModel import foundation.e.apps.R +import foundation.e.apps.api.fused.FusedAPIImpl import foundation.e.apps.api.fused.FusedAPIInterface import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.api.fused.data.FusedHome @@ -40,13 +41,11 @@ import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.databinding.FragmentHomeBinding import foundation.e.apps.home.model.HomeChildRVAdapter import foundation.e.apps.home.model.HomeParentRVAdapter -import foundation.e.apps.login.AuthObject import foundation.e.apps.manager.download.data.DownloadProgress import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.utils.enums.ResultStatus import foundation.e.apps.utils.enums.Status -import foundation.e.apps.utils.exceptions.GPlayException -import foundation.e.apps.utils.exceptions.GPlayLoginException +import foundation.e.apps.utils.enums.User import foundation.e.apps.utils.modules.CommonUtilsModule.safeNavigate import foundation.e.apps.utils.modules.PWAManagerModule import foundation.e.apps.utils.parentFragment.TimeoutFragment @@ -79,6 +78,14 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface super.onViewCreated(view, savedInstanceState) _binding = FragmentHomeBinding.bind(view) + mainActivityViewModel.userType.observe(viewLifecycleOwner) { user -> + if (user.isNullOrEmpty() || User.valueOf(user) == User.UNAVAILABLE) { + mainActivityViewModel.tocStatus.observe(viewLifecycleOwner) { tosAccepted -> + onTosAccepted(tosAccepted) + } + } + } + loadHomePageData() homeParentRVAdapter = initHomeParentRVAdapter() @@ -95,6 +102,7 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface homeViewModel.homeScreenData.observe(viewLifecycleOwner) { stopLoadingUI() if (it.second != ResultStatus.OK) { + onTimeout() return@observe } @@ -102,6 +110,7 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface return@observe } + dismissTimeoutDialog() homeParentRVAdapter?.setData(it.first) } } @@ -110,7 +119,7 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface this, pkgManagerModule, pwaManagerModule, - mainActivityViewModel.getUser(), + User.valueOf(mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name), mainActivityViewModel, appInfoFetchViewModel, viewLifecycleOwner ) { fusedApp -> if (!mainActivityViewModel.shouldShowPaidAppsSnackBar(fusedApp)) { @@ -119,15 +128,49 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface } private fun loadHomePageData() { - setupListening() + /* + * Previous code: + * internetConnection.observe { + * authData.observe { + * // refresh data here. + * } + * } + * + * Code regarding data fetch is placed in two separate observers compared to nested + * observers as was done previously. + * + * refreshDataOrRefreshToken() already checks for internet connectivity and authData. + * If authData is null, it requests to fetch new token data. + * + * With previous nested observer code (commit 8ca1647d), try the following: + * 1. Put garbage value in "Proxy" of APN settings of device, + * this will cause host unreachable error. + * 2. Open App Lounge. Let it show timeout dialog. + * 3. Click "Open Settings", now immediately open Home tab again. + * 4. Home keeps loading without any timeout error. + * + * Why is this happening? + * In case of host unreachable error, the authData is itself blank/null. This does not allow + * it to get "observed". But mainActivityViewModel.internetConnection always has a value, + * and is observable. + * When we open Home tab again from Settings tab, no refresh action is performed as + * authData.observe {} does not observe anything. + * + * In the new code, the first observer will always be executed on fragment attach + * (as mainActivityViewModel.internetConnection always has a value and is observable), + * this will call refreshDataOrRefreshToken(), which will refresh authData if it is null. + * Now with new valid authData, the second observer (authData.observe{}) will again call + * refreshDataOrRefreshToken() which will now fetch correct data. + * + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 + */ - authObjects.observe(viewLifecycleOwner) { - if (it == null) return@observe - loadData(it) + mainActivityViewModel.internetConnection.observe(viewLifecycleOwner) { + refreshDataOrRefreshToken(mainActivityViewModel) } - - homeViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { - handleExceptionsCommon(it) + mainActivityViewModel.authData.observe(viewLifecycleOwner) { + refreshDataOrRefreshToken(mainActivityViewModel) } } @@ -153,61 +196,49 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface homeParentRVAdapter?.currentList as List ) - override fun onTimeout( - exception: Exception, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog.apply { - if (exception is GPlayException) { - setMessage(R.string.timeout_desc_gplay) - setNegativeButton(R.string.open_settings) { _, _ -> - openSettings() - } - } else { - setMessage(R.string.timeout_desc_cleanapk) - } - setCancelable(false) - } - } - - override fun onSignInError( - exception: GPlayLoginException, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog.apply { - setNegativeButton(R.string.open_settings) { _, _ -> - openSettings() - } - } - } - - override fun onDataLoadError( - exception: Exception, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog.apply { - if (exception is GPlayException) { - setNegativeButton(R.string.open_settings) { _, _ -> + override fun onTimeout() { + if (homeViewModel.isFusedHomesEmpty() && !isTimeoutDialogDisplayed()) { + mainActivityViewModel.uploadFaultyTokenToEcloud("From " + this::class.java.name) + stopLoadingUI() + displayTimeoutAlertDialog( + timeoutFragment = this, + activity = requireActivity(), + message = + if (homeViewModel.getApplicationCategoryPreference() == FusedAPIImpl.APP_TYPE_ANY) { + getString(R.string.timeout_desc_gplay) + } else { + getString(R.string.timeout_desc_cleanapk) + }, + positiveButtonText = getString(R.string.retry), + positiveButtonBlock = { + showLoadingUI() + resetTimeoutDialogLock() + mainActivityViewModel.retryFetchingTokenAfterTimeout() + }, + negativeButtonText = + if (homeViewModel.getApplicationCategoryPreference() == FusedAPIImpl.APP_TYPE_ANY) { + getString(R.string.open_settings) + } else null, + negativeButtonBlock = { openSettings() - } - } + }, + allowCancel = false, + ) } } - override fun loadData(authObjectList: List) { - homeViewModel.loadData(authObjectList) { _ -> - clearAndRestartGPlayLogin() - true - } + override fun refreshData(authData: AuthData) { + showLoadingUI() + homeViewModel.getHomeScreenData(authData) } - override fun showLoadingUI() { + private fun showLoadingUI() { binding.shimmerLayout.startShimmer() binding.shimmerLayout.visibility = View.VISIBLE binding.parentRV.visibility = View.GONE } - override fun stopLoadingUI() { + private fun stopLoadingUI() { binding.shimmerLayout.stopShimmer() binding.shimmerLayout.visibility = View.GONE binding.parentRV.visibility = View.VISIBLE @@ -266,13 +297,16 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface override fun onResume() { super.onResume() + resetTimeoutDialogLock() binding.shimmerLayout.startShimmer() appProgressViewModel.downloadProgress.observe(viewLifecycleOwner) { updateProgressOfDownloadingAppItemViews(homeParentRVAdapter, it) } if (homeViewModel.isAnyAppInstallStatusChanged(homeParentRVAdapter?.currentList)) { - repostAuthObjects() + mainActivityViewModel.authData.value?.let { + refreshData(it) + } } } diff --git a/app/src/main/java/foundation/e/apps/home/HomeViewModel.kt b/app/src/main/java/foundation/e/apps/home/HomeViewModel.kt index 596d07673a161a19bf07cd0542e9be4301a1b7fa..2e19e78b55aa590f108ffe43044e82910e89dcfa 100644 --- a/app/src/main/java/foundation/e/apps/home/HomeViewModel.kt +++ b/app/src/main/java/foundation/e/apps/home/HomeViewModel.kt @@ -19,24 +19,21 @@ package foundation.e.apps.home import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.AuthData import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.api.fused.data.FusedHome -import foundation.e.apps.login.AuthObject import foundation.e.apps.utils.enums.ResultStatus -import foundation.e.apps.utils.exceptions.CleanApkException -import foundation.e.apps.utils.exceptions.GPlayException -import foundation.e.apps.utils.parentFragment.LoadingViewModel import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( private val fusedAPIRepository: FusedAPIRepository, -) : LoadingViewModel() { +) : ViewModel() { /* * Hold list of applications, as well as application source type. @@ -46,38 +43,10 @@ class HomeViewModel @Inject constructor( */ var homeScreenData: MutableLiveData, ResultStatus>> = MutableLiveData() - fun loadData( - authObjectList: List, - retryBlock: (failedObjects: List) -> Boolean, - ) { - super.onLoadData(authObjectList, { successAuthList, _ -> - - successAuthList.find { it is AuthObject.GPlayAuth }?.run { - getHomeScreenData(result.data!! as AuthData) - return@onLoadData - } - - successAuthList.find { it is AuthObject.CleanApk }?.run { - getHomeScreenData(AuthData("", "")) - return@onLoadData - } - }, retryBlock) - } - fun getHomeScreenData(authData: AuthData) { viewModelScope.launch { val screenData = fusedAPIRepository.getHomeScreenData(authData) homeScreenData.postValue(screenData) - - if (screenData.second != ResultStatus.OK) { - val exception = - if (authData.aasToken.isNotBlank() || authData.authToken.isNotBlank()) - GPlayException(screenData.second == ResultStatus.TIMEOUT, "Data load error") - else CleanApkException(screenData.second == ResultStatus.TIMEOUT, "Data load error") - - exceptionsList.add(exception) - exceptionsLiveData.postValue(exceptionsList) - } } } diff --git a/app/src/main/java/foundation/e/apps/login/AuthObject.kt b/app/src/main/java/foundation/e/apps/login/AuthObject.kt deleted file mode 100644 index 84567b197a7be1664ecb387c257164b8079c30e5..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/login/AuthObject.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.login - -import com.aurora.gplayapi.data.models.AuthData -import foundation.e.apps.api.ResultSupreme -import foundation.e.apps.login.AuthObject.GPlayAuth -import foundation.e.apps.utils.enums.User - -/** - * Auth objects define which sources data is to be loaded from, for each source, also provides - * a means of authentication to get the data. - * Example, for Google Play, we have [GPlayAuth] which contains [AuthData]. - * For CleanApk, we don't need any authentication. - * - * In future, combination of sources is possible. - * - * https://gitlab.e.foundation/e/backlog/-/issues/5680 - */ -sealed class AuthObject { - - abstract val result: ResultSupreme<*> - - class GPlayAuth(override val result: ResultSupreme, val user: User) : AuthObject() - class CleanApk(override val result: ResultSupreme, val user: User) : AuthObject() - // Add more auth types here -} diff --git a/app/src/main/java/foundation/e/apps/login/LoginCommon.kt b/app/src/main/java/foundation/e/apps/login/LoginCommon.kt deleted file mode 100644 index 1f0e82aa5ddd8b7a598dacc33cf464cc092d7997..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/login/LoginCommon.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.login - -import foundation.e.apps.utils.enums.User -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Contains common function for first login, logout, get which type of authentication / source - * to be used etc... - * - * https://gitlab.e.foundation/e/backlog/-/issues/5680 - */ -@Singleton -class LoginCommon @Inject constructor( - private val loginDataStore: LoginDataStore, -) { - suspend fun saveUserType(user: User) { - loginDataStore.saveUserType(user) - } - - suspend fun saveGoogleLogin(email: String, oauth: String) { - loginDataStore.saveGoogleLogin(email, oauth) - } - - suspend fun logout() { - loginDataStore.destroyCredentials() - loginDataStore.clearUserType() - } -} diff --git a/app/src/main/java/foundation/e/apps/login/LoginDataStore.kt b/app/src/main/java/foundation/e/apps/login/LoginDataStore.kt deleted file mode 100644 index 6a10235289365d8b4d3e86ad01afe553eb2d1465..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/login/LoginDataStore.kt +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.login - -import android.content.Context -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.preference.PreferenceManager -import com.aurora.gplayapi.data.models.AuthData -import com.google.gson.Gson -import dagger.hilt.android.qualifiers.ApplicationContext -import foundation.e.apps.utils.Constants.PREFERENCE_SHOW_FOSS -import foundation.e.apps.utils.Constants.PREFERENCE_SHOW_GPLAY -import foundation.e.apps.utils.Constants.PREFERENCE_SHOW_PWA -import foundation.e.apps.utils.enums.User -import foundation.e.apps.utils.modules.DataStoreModule.Companion.dataStore -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.runBlocking -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class LoginDataStore @Inject constructor( - @ApplicationContext - private val context: Context, - private val gson: Gson -) { - - private val preferenceManager = PreferenceManager.getDefaultSharedPreferences(context) - - private val AUTHDATA = stringPreferencesKey("authData") - private val EMAIL = stringPreferencesKey("email") - private val OAUTHTOKEN = stringPreferencesKey("oauthtoken") - private val AASTOKEN = stringPreferencesKey("aasToken") - private val USERTYPE = stringPreferencesKey("userType") - - /* - * Difference between OAUTHTOKEN and AASTOKEN: - * - * These two are used only for Google login, not for Anonymous login. - * OAuthToken is obtained from the Google Login web page, from the cookies. - * This OAuthToken is then used by AC2DMTask in GPlayAPIImpl class - * to generate AasToken. - * - * To get Google Play Store data, we need to create an AuthData instance. - * For Google user, this can only be done using AasToken, not OAuthToken. - * - * Very important: AasToken can be generated only ONCE from one OAuthToken. - * We cannot get AasToken again from the same OAuthToken. Thus it is - * important to safely store the AasToken to regenerate AuthData if needed. - * If AasToken is not stored, user has to logout and login again. - */ - - val authData = context.dataStore.data.map { it[AUTHDATA] ?: "" } - val emailData = context.dataStore.data.map { it[EMAIL] ?: "" } - val aasToken = context.dataStore.data.map { it[AASTOKEN] ?: "" } - val oauthToken = context.dataStore.data.map { it[OAUTHTOKEN] ?: "" } - val userType = context.dataStore.data.map { it[USERTYPE] ?: "" } - - // Setters - - suspend fun saveAuthData(authData: AuthData) { - context.dataStore.edit { - it[AUTHDATA] = gson.toJson(authData) - } - } - - suspend fun saveUserType(user: User) { - context.dataStore.edit { - it[USERTYPE] = user.name - } - } - - suspend fun saveGoogleLogin(email: String, token: String) { - context.dataStore.edit { - it[EMAIL] = email - it[OAUTHTOKEN] = token - } - } - - suspend fun saveAasToken(aasToken: String) { - context.dataStore.edit { - it[AASTOKEN] = aasToken - } - } - - // Getters - - fun getAuthData(): String { - return runBlocking { - authData.first() - } - } - - /** - * Get the [User] type stored in the data store. - * In case nothing is stored, returns [User.UNAVAILABLE]. - * - * No need to wrap this function in try-catch block. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5680 - */ - fun getUserType(): User { - return runBlocking { - userType.first().run { - val userStrings = User.values().map { it.name } - if (this !in userStrings) User.UNAVAILABLE - else User.valueOf(this) - } - } - } - - fun getEmail(): String { - return runBlocking { - emailData.first() - } - } - - fun getOAuthToken(): String { - return runBlocking { - oauthToken.first() - } - } - - fun getAASToken(): String { - return runBlocking { - aasToken.first() - } - } - - fun isOpenSourceSelected() = preferenceManager.getBoolean(PREFERENCE_SHOW_FOSS, true) - fun isPWASelected() = preferenceManager.getBoolean(PREFERENCE_SHOW_PWA, true) - fun isGplaySelected() = preferenceManager.getBoolean(PREFERENCE_SHOW_GPLAY, true) - - // Clear data - - /** - * Destroy auth credentials if they are no longer valid. - * - * Modification for issue: https://gitlab.e.foundation/e/backlog/-/issues/5168 - * Previously this method would also remove [USERTYPE]. - * To clear this value, call [clearUserType]. - */ - suspend fun destroyCredentials() { - context.dataStore.edit { - it.remove(AUTHDATA) - it.remove(EMAIL) - it.remove(OAUTHTOKEN) - it.remove(AASTOKEN) - } - } - - suspend fun clearAuthData() { - context.dataStore.edit { - it.remove(AUTHDATA) - } - } - - suspend fun clearUserType() { - context.dataStore.edit { - it.remove(USERTYPE) - } - } -} diff --git a/app/src/main/java/foundation/e/apps/login/LoginSourceCleanApk.kt b/app/src/main/java/foundation/e/apps/login/LoginSourceCleanApk.kt deleted file mode 100644 index 2b8f4eadc37fdd213601d43fbe82a15c798c8022..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/login/LoginSourceCleanApk.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.login - -import foundation.e.apps.api.ResultSupreme -import foundation.e.apps.utils.enums.User -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Just a dummy class for CleanApk, as it requires no authentication. - * https://gitlab.e.foundation/e/backlog/-/issues/5680 - */ -@Singleton -class LoginSourceCleanApk @Inject constructor( - val loginDataStore: LoginDataStore, -) : LoginSourceInterface { - - private val user: User - get() = loginDataStore.getUserType() - - override fun isActive(): Boolean { - if (user == User.UNAVAILABLE) { - /* - * UNAVAILABLE user means first login is not completed. - */ - return false - } - return loginDataStore.isOpenSourceSelected() || loginDataStore.isPWASelected() - } - - override suspend fun getAuthObject(): AuthObject.CleanApk { - return AuthObject.CleanApk( - ResultSupreme.Success(Unit), - user, - ) - } - - override suspend fun clearSavedAuth() {} -} diff --git a/app/src/main/java/foundation/e/apps/login/LoginSourceGPlay.kt b/app/src/main/java/foundation/e/apps/login/LoginSourceGPlay.kt deleted file mode 100644 index 0126beaea3be0b8a9672b5b4d93096a87bd1f7b9..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/login/LoginSourceGPlay.kt +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.login - -import com.aurora.gplayapi.data.models.AuthData -import com.google.gson.Gson -import foundation.e.apps.api.ResultSupreme -import foundation.e.apps.login.api.GPlayApiFactory -import foundation.e.apps.login.api.GPlayLoginInterface -import foundation.e.apps.login.api.GoogleLoginApi -import foundation.e.apps.login.api.LoginApiRepository -import foundation.e.apps.utils.enums.User -import foundation.e.apps.utils.exceptions.GPlayValidationException -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Class to get GPlay auth data. Call [getAuthObject] to get an already saved auth data - * or to fetch a new one for first use. Handles auth validation internally. - * - * https://gitlab.e.foundation/e/backlog/-/issues/5680 - */ -@Singleton -class LoginSourceGPlay @Inject constructor( - private val gson: Gson, - private val loginDataStore: LoginDataStore, -) : LoginSourceInterface { - - @Inject - lateinit var gPlayApiFactory: GPlayApiFactory - - private val user: User - get() = loginDataStore.getUserType() - - private val gPlayLoginInterface: GPlayLoginInterface - get() = gPlayApiFactory.getGPlayApi(user) - - private val loginApiRepository: LoginApiRepository - get() = LoginApiRepository(gPlayLoginInterface, user) - - override fun isActive(): Boolean { - if (user == User.UNAVAILABLE) { - /* - * UNAVAILABLE user means first login is not completed. - */ - return false - } - return loginDataStore.isGplaySelected() - } - - /** - * Main entry point to get GPlay auth data. - */ - override suspend fun getAuthObject(): AuthObject.GPlayAuth { - val savedAuth = getSavedAuthData() - - val authData = ( - savedAuth ?: run { - // if no saved data, then generate new auth data. - generateAuthData().let { - if (it.isSuccess()) it.data!! - else return AuthObject.GPlayAuth(it, user) - } - } - ) - - // validate authData and save it if nothing is saved (first time use.) - validateAuthData(authData).run { - if (isSuccess() && savedAuth == null) { - saveAuthData(authData) - } - return AuthObject.GPlayAuth(this, user) - } - } - - override suspend fun clearSavedAuth() { - loginDataStore.clearAuthData() - } - - /** - * Get authData stored as JSON and convert to AuthData class. - * Returns null if nothing is saved. - */ - private fun getSavedAuthData(): AuthData? { - val authJson = loginDataStore.getAuthData() - return if (authJson.isBlank()) null - else try { - gson.fromJson(authJson, AuthData::class.java) - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - private suspend fun saveAuthData(authData: AuthData) { - loginDataStore.saveAuthData(authData) - } - - /** - * Generate new AuthData based on the user type. - */ - private suspend fun generateAuthData(): ResultSupreme { - return when (loginDataStore.getUserType()) { - User.ANONYMOUS -> getAuthData() - User.GOOGLE -> { - getAuthData( - loginDataStore.getEmail(), - loginDataStore.getOAuthToken(), - loginDataStore.getAASToken() - ) - } - else -> ResultSupreme.Error("User type not ANONYMOUS or GOOGLE") - } - } - - /** - * Aurora OSS GPlay API complains of missing headers sometimes. - * Converting [authData] to Json and back to [AuthData] fixed it. - */ - private fun formattedAuthData(authData: AuthData): AuthData { - val localAuthDataJson = gson.toJson(authData) - return gson.fromJson(localAuthDataJson, AuthData::class.java) - } - - /** - * Get AuthData for ANONYMOUS mode. - */ - private suspend fun getAuthData(): ResultSupreme { - return loginApiRepository.fetchAuthData("", "").run { - if (isSuccess()) ResultSupreme.Success(formattedAuthData(this.data!!)) - else this - } - } - - /** - * Get AuthData for GOOGLE login mode. - */ - private suspend fun getAuthData( - email: String, - oauthToken: String, - aasToken: String, - ): ResultSupreme { - - /* - * If aasToken is not blank, means it was stored successfully from a previous Google login. - * Use it to fetch auth data. - */ - if (aasToken.isNotBlank()) { - return loginApiRepository.fetchAuthData(email, aasToken) - } - - /* - * If aasToken is not yet saved / made, fetch it from email and oauthToken. - */ - val aasTokenResponse = loginApiRepository.getAasToken( - gPlayLoginInterface as GoogleLoginApi, - email, - oauthToken - ) - - /* - * If fetch was unsuccessful, return blank auth data. - * We replicate from the response, so that it will carry on any error message if present - * in the aasTokenResponse. - */ - if (!aasTokenResponse.isSuccess()) { - return ResultSupreme.replicate(aasTokenResponse, null) - } - - val aasTokenFetched = aasTokenResponse.data ?: "" - - if (aasTokenFetched.isBlank()) { - return ResultSupreme.Error("Fetched AAS Token is blank") - } - - /* - * Finally save the aasToken and create auth data. - */ - loginDataStore.saveAasToken(aasTokenFetched) - return loginApiRepository.fetchAuthData(email, aasTokenFetched) - } - - /** - * Check if a given [AuthData] from Google login or Anonymous login is valid or not. - * If valid, return the AuthData wrapped in [ResultSupreme], else return null, - * with error message. - */ - private suspend fun validateAuthData( - authData: AuthData, - ): ResultSupreme { - - val formattedAuthData = formattedAuthData(authData) - - val validityResponse = loginApiRepository.login(formattedAuthData) - - /* - * Send the email as payload. This is sent to ecloud in case of failure. - * See MainActivityViewModel.uploadFaultyTokenToEcloud. - */ - validityResponse.otherPayload = formattedAuthData.email - - val playResponse = validityResponse.data - return if (validityResponse.isSuccess() && playResponse?.code == 200 && playResponse.isSuccessful) { - ResultSupreme.Success(authData) - } else { - val message = - "Validating AuthData failed.\n\n" + - "Success: ${playResponse?.isSuccessful}" + - (validityResponse.exception?.let { "\n${it.message}" } ?: "") - - ResultSupreme.Error( - message, - GPlayValidationException(message, user, playResponse?.code ?: -1) - ) - } - } -} diff --git a/app/src/main/java/foundation/e/apps/login/LoginSourceInterface.kt b/app/src/main/java/foundation/e/apps/login/LoginSourceInterface.kt deleted file mode 100644 index 79250e87e52c8cca0cdbdffffca4debd6b4668e9..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/login/LoginSourceInterface.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.login - -/** - * Interface that defines what methods a login source must define. - * Login sources (can also be called - data sources): Google Play, CleanApk. - * - * https://gitlab.e.foundation/e/backlog/-/issues/5680 - */ -interface LoginSourceInterface { - suspend fun getAuthObject(): AuthObject - suspend fun clearSavedAuth() - fun isActive(): Boolean -} diff --git a/app/src/main/java/foundation/e/apps/login/LoginSourceRepository.kt b/app/src/main/java/foundation/e/apps/login/LoginSourceRepository.kt deleted file mode 100644 index e403c8ba170322d45acda9f1ca6c9f3fd1bd2ed6..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/login/LoginSourceRepository.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.login - -import foundation.e.apps.utils.enums.User -import javax.inject.Inject -import javax.inject.Singleton - -@JvmSuppressWildcards -@Singleton -class LoginSourceRepository @Inject constructor( - private val loginCommon: LoginCommon, - private val sources: List, -) { - - suspend fun getAuthObjects(clearAuthTypes: List = listOf()): List { - - val authObjectsLocal = ArrayList() - - for (source in sources) { - if (!source.isActive()) continue - if (source::class.java.simpleName in clearAuthTypes) { - source.clearSavedAuth() - } - authObjectsLocal.add(source.getAuthObject()) - } - - return authObjectsLocal - } - - suspend fun saveUserType(user: User) { - loginCommon.saveUserType(user) - } - - suspend fun saveGoogleLogin(email: String, oauth: String) { - loginCommon.saveGoogleLogin(email, oauth) - } - - suspend fun logout() { - loginCommon.logout() - } -} diff --git a/app/src/main/java/foundation/e/apps/login/LoginViewModel.kt b/app/src/main/java/foundation/e/apps/login/LoginViewModel.kt deleted file mode 100644 index e8ead132770c17f6ce28ca27da6d4d4d476d9d33..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/login/LoginViewModel.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.login - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import foundation.e.apps.utils.enums.User -import kotlinx.coroutines.launch -import javax.inject.Inject - -/** - * ViewModel to handle all login related operations. - * Use it as shared view model across all fragments. - */ -@HiltViewModel -class LoginViewModel @Inject constructor( - private val loginSourceRepository: LoginSourceRepository, -) : ViewModel() { - - /** - * List of authentication objects which will determine from where to load the data. - * (i.e. GPlay, CleanApk) - * - * Allow null as initial value. - * This prevents showing login screen before TOS is accepted. - * This also allows to set null immediately when sources are changed in Settings, - * and when [startLoginFlow] is called, fragments wait till actual AuthObjects - * are loaded instead of null. - */ - val authObjects: MutableLiveData?> = MutableLiveData(null) - - /** - * Main point of starting of entire authentication process. - */ - fun startLoginFlow(clearList: List = listOf()) { - viewModelScope.launch { - val authObjectsLocal = loginSourceRepository.getAuthObjects(clearList) - authObjects.postValue(authObjectsLocal) - } - } - - /** - * Call this to use ANONYMOUS mode. - * This method is called only for the first time when logging in the user. - * @param onUserSaved Code to execute once user is saved. Ends the sign in screen. - */ - fun initialAnonymousLogin(onUserSaved: () -> Unit) { - viewModelScope.launch { - loginSourceRepository.saveUserType(User.ANONYMOUS) - onUserSaved() - startLoginFlow() - } - } - - /** - * Call this to use GOOGLE login mode. - * This method is called only for the first time when logging in the user. - * @param onUserSaved Code to execute once email, oauthToken and user is saved. - * Ends the sign in screen. - */ - fun initialGoogleLogin(email: String, oauthToken: String, onUserSaved: () -> Unit) { - viewModelScope.launch { - loginSourceRepository.saveGoogleLogin(email, oauthToken) - loginSourceRepository.saveUserType(User.GOOGLE) - onUserSaved() - startLoginFlow() - } - } - - /** - * Clears all saved data and logs out the user to the sign in screen. - */ - fun logout() { - viewModelScope.launch { - loginSourceRepository.logout() - authObjects.postValue(listOf()) - } - } -} diff --git a/app/src/main/java/foundation/e/apps/login/api/AnonymousLoginApi.kt b/app/src/main/java/foundation/e/apps/login/api/AnonymousLoginApi.kt deleted file mode 100644 index dcc7f99b8bcc9edaa19052b1a73193a3208eb9f1..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/login/api/AnonymousLoginApi.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.login.api - -import com.aurora.gplayapi.data.models.AuthData -import com.aurora.gplayapi.data.models.PlayResponse -import com.google.gson.Gson -import foundation.e.apps.BuildConfig -import foundation.e.apps.api.gplay.utils.CustomAuthValidator -import foundation.e.apps.api.gplay.utils.GPlayHttpClient -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.util.Properties - -class AnonymousLoginApi( - private val gPlayHttpClient: GPlayHttpClient, - private val nativeDeviceProperty: Properties, - private val gson: Gson, -) : GPlayLoginInterface { - - private val tokenUrl: String - get() { - return if (BuildConfig.DEBUG) "https://eu.gtoken.eeo.one" - else "https://eu.gtoken.ecloud.global" - } - - /** - * Fetches AuthData for Anonymous login. - * @param email Keep it blank (""). - * @param aasToken Keep it blank (""). - */ - override suspend fun fetchAuthData(email: String, aasToken: String): AuthData? { - var authData: AuthData? = null - withContext(Dispatchers.IO) { - val response = - gPlayHttpClient.postAuth(tokenUrl, gson.toJson(nativeDeviceProperty).toByteArray()) - if (response.code != 200 || !response.isSuccessful) { - throw Exception("Error fetching Anonymous credentials: ${response.errorString}") - } else { - authData = gson.fromJson( - String(response.responseBytes), - AuthData::class.java - ) - } - } - return authData - } - - /** - * Check if an AuthData is valid. Returns a [PlayResponse]. - * Check [PlayResponse.isSuccessful] to see if the validation was successful. - */ - override suspend fun login(authData: AuthData): PlayResponse { - var result = PlayResponse() - withContext(Dispatchers.IO) { - try { - val authValidator = CustomAuthValidator(authData).using(gPlayHttpClient) - result = authValidator.getValidityResponse() - } catch (e: Exception) { - e.printStackTrace() - throw e - } - } - return result - } -} diff --git a/app/src/main/java/foundation/e/apps/login/api/GPlayApiFactory.kt b/app/src/main/java/foundation/e/apps/login/api/GPlayApiFactory.kt deleted file mode 100644 index a910d525dd39ab0804274a8c1ba368a03a03d14b..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/login/api/GPlayApiFactory.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.login.api - -import com.google.gson.Gson -import foundation.e.apps.api.gplay.utils.AC2DMTask -import foundation.e.apps.api.gplay.utils.GPlayHttpClient -import foundation.e.apps.utils.enums.User -import java.util.Properties -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class GPlayApiFactory @Inject constructor( - private val gPlayHttpClient: GPlayHttpClient, - private val nativeDeviceProperty: Properties, - private val aC2DMTask: AC2DMTask, - private val gson: Gson, -) { - - fun getGPlayApi(user: User): GPlayLoginInterface { - return when (user) { - User.GOOGLE -> GoogleLoginApi(gPlayHttpClient, nativeDeviceProperty, aC2DMTask) - else -> AnonymousLoginApi(gPlayHttpClient, nativeDeviceProperty, gson) - } - } -} diff --git a/app/src/main/java/foundation/e/apps/login/api/GoogleLoginApi.kt b/app/src/main/java/foundation/e/apps/login/api/GoogleLoginApi.kt deleted file mode 100644 index 47c7bb19adc31fc3f572dd97c4f11b88e38cbcf3..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/login/api/GoogleLoginApi.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.login.api - -import com.aurora.gplayapi.data.models.AuthData -import com.aurora.gplayapi.data.models.PlayResponse -import com.aurora.gplayapi.helpers.AuthHelper -import foundation.e.apps.api.gplay.utils.AC2DMTask -import foundation.e.apps.api.gplay.utils.CustomAuthValidator -import foundation.e.apps.api.gplay.utils.GPlayHttpClient -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.util.Properties - -class GoogleLoginApi( - private val gPlayHttpClient: GPlayHttpClient, - private val nativeDeviceProperty: Properties, - private val aC2DMTask: AC2DMTask, -) : GPlayLoginInterface { - - /** - * Get PlayResponse for AC2DM Map. This allows us to get an error message too. - * - * An aasToken is extracted from this map. This is passed to [fetchAuthData] - * to generate AuthData. This token is very important as it cannot be regenerated, - * hence it must be saved for future use. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5680 - */ - suspend fun getAC2DMResponse(email: String, oauthToken: String): PlayResponse { - var response = PlayResponse() - withContext(Dispatchers.IO) { - response = aC2DMTask.getAC2DMResponse(email, oauthToken) - } - return response - } - - /** - * Convert email and AASToken to AuthData class. - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5680 - */ - override suspend fun fetchAuthData(email: String, aasToken: String): AuthData? { - var authData: AuthData? = null - withContext(Dispatchers.IO) { - authData = AuthHelper.build(email, aasToken, nativeDeviceProperty) - } - return authData - } - - /** - * Check if an AuthData is valid. Returns a [PlayResponse]. - * Check [PlayResponse.isSuccessful] to see if the validation was successful. - */ - override suspend fun login(authData: AuthData): PlayResponse { - var result = PlayResponse() - withContext(Dispatchers.IO) { - try { - val authValidator = CustomAuthValidator(authData).using(gPlayHttpClient) - result = authValidator.getValidityResponse() - } catch (e: Exception) { - e.printStackTrace() - throw e - } - } - return result - } -} diff --git a/app/src/main/java/foundation/e/apps/login/api/LoginApiRepository.kt b/app/src/main/java/foundation/e/apps/login/api/LoginApiRepository.kt deleted file mode 100644 index 44dcd0c7d74c6e76afa622f94a0052b196751b20..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/login/api/LoginApiRepository.kt +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.login.api - -import com.aurora.gplayapi.data.models.AuthData -import com.aurora.gplayapi.data.models.PlayResponse -import foundation.e.apps.api.ResultSupreme -import foundation.e.apps.api.gplay.utils.AC2DMUtil -import foundation.e.apps.utils.Constants.timeoutDurationInMillis -import foundation.e.apps.utils.enums.User -import foundation.e.apps.utils.exceptions.GPlayLoginException -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.withTimeout - -/** - * Call methods of [GoogleLoginApi] and [AnonymousLoginApi] from here. - * - * Dependency Injection via hilt is not possible, - * we need to manually check login type, create an instance of either [GoogleLoginApi] - * or [AnonymousLoginApi] and pass it to [gPlayLoginInterface]. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5680 - */ -class LoginApiRepository constructor( - private val gPlayLoginInterface: GPlayLoginInterface, - private val user: User, -) { - - /** - * Gets the auth data from instance of [GPlayLoginInterface]. - * Applicable for both Google and Anonymous login. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5680 - * @param email Email address for Google login. Blank for Anonymous login. - * @param aasToken For Google login - Access token obtained from [getAasToken] function, - * else blank for Anonymous login. - */ - suspend fun fetchAuthData(email: String, aasToken: String): ResultSupreme { - val result = runCodeBlockWithTimeout({ - gPlayLoginInterface.fetchAuthData(email, aasToken) - }) - return result.apply { - this.exception = when (result) { - is ResultSupreme.Timeout -> GPlayLoginException(true, "GPlay API timeout", user) - is ResultSupreme.Error -> GPlayLoginException(false, result.message, user) - else -> null - } - } - } - - /** - * Get AuthData validity of in the form of PlayResponse. - * Advantage of not using a simple boolean is we get error message and - * network code of the request inside PlayResponse object. - * - * Applicable for both Google and Anonymous login. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5680 - */ - suspend fun login(authData: AuthData): ResultSupreme { - var response = PlayResponse() - val result = runCodeBlockWithTimeout({ - response = gPlayLoginInterface.login(authData) - if (response.code != 200) { - throw Exception("Validation network code: ${response.code}") - } - response - }) - return ResultSupreme.replicate(result, response).apply { - this.exception = when (result) { - is ResultSupreme.Timeout -> GPlayLoginException(true, "GPlay API timeout", user) - is ResultSupreme.Error -> GPlayLoginException(false, result.message, user) - else -> null - } - } - } - - /** - * Gets email and oauthToken from Google login, finds the AASToken from AC2DM response - * and returns it. This token is then used to fetch AuthData from [fetchAuthData]. - * - * Do note that for a given oauthToken, it has been observed that AASToken can - * only be generated once. So this token must be saved for future use. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5680 - * - * @param googleLoginApi An instance of [GoogleLoginApi] must be passed, this method - * cannot work on [gPlayLoginInterface] as it is a common interface for both Google and Anonymous - * login, but this method is only for Google login. - */ - suspend fun getAasToken( - googleLoginApi: GoogleLoginApi, - email: String, - oauthToken: String - ): ResultSupreme { - val result = runCodeBlockWithTimeout({ - var aasToken = "" - val response = googleLoginApi.getAC2DMResponse(email, oauthToken) - var error = response.errorString - if (response.isSuccessful) { - val responseMap = AC2DMUtil.parseResponse(String(response.responseBytes)) - aasToken = responseMap["Token"] ?: "" - if (aasToken.isBlank() && error.isBlank()) { - error = "AASToken not found in map." - } - } - /* - * Default value of PlayResponse.errorString is "No Error". - * https://gitlab.com/AuroraOSS/gplayapi/-/blob/master/src/main/java/com/aurora/gplayapi/data/models/PlayResponse.kt - */ - if (error != "No Error") { - throw Exception(error) - } - aasToken - }) - return result.apply { - this.exception = when (result) { - is ResultSupreme.Timeout -> GPlayLoginException(true, "GPlay API timeout", User.GOOGLE) - is ResultSupreme.Error -> GPlayLoginException(false, result.message, User.GOOGLE) - else -> null - } - } - } - - /** - * Utility method to run a specified code block in a fixed amount of time. - */ - private suspend fun runCodeBlockWithTimeout( - block: suspend () -> T, - timeoutBlock: (() -> T?)? = null, - exceptionBlock: (() -> T?)? = null, - ): ResultSupreme { - return try { - withTimeout(timeoutDurationInMillis) { - return@withTimeout ResultSupreme.Success(block()) - } - } catch (e: TimeoutCancellationException) { - ResultSupreme.Timeout(timeoutBlock?.invoke()).apply { - message = e.message ?: "" - } - } catch (e: Exception) { - e.printStackTrace() - ResultSupreme.Error(exceptionBlock?.invoke(), message = e.message ?: "") - } - } -} diff --git a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt index 90a95147d25d6c3e88d748dfca5dd1038d6e296b..35de693ce48cd94c1652670479a94a8cff74f1a0 100644 --- a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt @@ -27,7 +27,6 @@ import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.ImageView import android.widget.LinearLayout -import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible import androidx.cursoradapter.widget.CursorAdapter @@ -39,6 +38,7 @@ import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.aurora.gplayapi.SearchSuggestEntry +import com.aurora.gplayapi.data.models.AuthData import com.facebook.shimmer.ShimmerFrameLayout import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.AppInfoFetchViewModel @@ -52,11 +52,9 @@ import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.applicationlist.ApplicationListRVAdapter import foundation.e.apps.databinding.FragmentSearchBinding -import foundation.e.apps.login.AuthObject import foundation.e.apps.manager.download.data.DownloadProgress import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.utils.enums.Status -import foundation.e.apps.utils.exceptions.GPlayLoginException import foundation.e.apps.utils.modules.PWAManagerModule import foundation.e.apps.utils.parentFragment.TimeoutFragment import kotlinx.coroutines.launch @@ -94,7 +92,7 @@ class SearchFragment : private var noAppsFoundLayout: LinearLayout? = null /* - * Store the string from onQueryTextSubmit() and access it from loadData() + * Store the string from onQueryTextSubmit() and access it from refreshData() * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 */ private var searchText = "" @@ -116,17 +114,6 @@ class SearchFragment : val listAdapter = setupSearchResult(view) observeSearchResult(listAdapter) - - setupListening() - - authObjects.observe(viewLifecycleOwner) { - if (it == null) return@observe - loadData(it) - } - - searchViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { - handleExceptionsCommon(it) - } } private fun observeSearchResult(listAdapter: ApplicationListRVAdapter?) { @@ -142,6 +129,13 @@ class SearchFragment : } observeScrollOfSearchResult(listAdapter) + if (searchText.isNotBlank() && !it.isSuccess()) { + /* + * If blank check is not performed then timeout dialog keeps + * popping up whenever search tab is opened. + */ + onTimeout() + } } } @@ -259,42 +253,39 @@ class SearchFragment : } } - override fun onTimeout( - exception: Exception, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog - } - - override fun onDataLoadError( - exception: Exception, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog - } - - override fun onSignInError( - exception: GPlayLoginException, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog + override fun onTimeout() { + if (!isTimeoutDialogDisplayed()) { + binding.loadingProgressBar.isVisible = false + stopLoadingUI() + displayTimeoutAlertDialog( + timeoutFragment = this, + activity = requireActivity(), + message = getString(R.string.timeout_desc_cleanapk), + positiveButtonText = getString(R.string.retry), + positiveButtonBlock = { + showLoadingUI() + resetTimeoutDialogLock() + mainActivityViewModel.retryFetchingTokenAfterTimeout() + }, + negativeButtonText = getString(android.R.string.ok), + negativeButtonBlock = {}, + allowCancel = true, + ) + } } - override fun loadData(authObjectList: List) { + override fun refreshData(authData: AuthData) { showLoadingUI() - searchViewModel.loadData(searchText, viewLifecycleOwner, authObjectList) { - clearAndRestartGPlayLogin() - true - } + searchViewModel.getSearchResults(searchText, authData, viewLifecycleOwner) } - override fun showLoadingUI() { + private fun showLoadingUI() { binding.shimmerLayout.startShimmer() binding.shimmerLayout.visibility = View.VISIBLE binding.recyclerView.visibility = View.GONE } - override fun stopLoadingUI() { + private fun stopLoadingUI() { binding.shimmerLayout.stopShimmer() binding.shimmerLayout.visibility = View.GONE binding.recyclerView.visibility = View.VISIBLE @@ -324,13 +315,16 @@ class SearchFragment : override fun onResume() { super.onResume() + resetTimeoutDialogLock() binding.shimmerLayout.startShimmer() appProgressViewModel.downloadProgress.observe(viewLifecycleOwner) { updateProgressOfInstallingApps(it) } if (shouldRefreshData()) { - repostAuthObjects() + mainActivityViewModel.authData.value?.let { + refreshData(it) + } } if (searchText.isEmpty() && (recyclerView?.adapter as ApplicationListRVAdapter).currentList.isEmpty()) { @@ -364,15 +358,15 @@ class SearchFragment : * Set the search text and call for network result. */ searchText = text - repostAuthObjects() + refreshDataOrRefreshToken(mainActivityViewModel) } return false } override fun onQueryTextChange(newText: String?): Boolean { newText?.let { text -> - authObjects.value?.find { it is AuthObject.GPlayAuth }?.run { - searchViewModel.getSearchSuggestions(text, this as AuthObject.GPlayAuth) + mainActivityViewModel.authData.value?.let { + searchViewModel.getSearchSuggestions(text, it) } } return true diff --git a/app/src/main/java/foundation/e/apps/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/search/SearchViewModel.kt index 02c10cca4bbd8586417826ce71319c9e82644fba..1442ad98f347c9a7735219eaf1832f805308228a 100644 --- a/app/src/main/java/foundation/e/apps/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/search/SearchViewModel.kt @@ -20,6 +20,7 @@ package foundation.e.apps.search import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.AuthData @@ -27,10 +28,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.api.ResultSupreme import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp -import foundation.e.apps.login.AuthObject -import foundation.e.apps.utils.exceptions.CleanApkException -import foundation.e.apps.utils.exceptions.GPlayException -import foundation.e.apps.utils.parentFragment.LoadingViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject @@ -38,42 +35,18 @@ import javax.inject.Inject @HiltViewModel class SearchViewModel @Inject constructor( private val fusedAPIRepository: FusedAPIRepository, -) : LoadingViewModel() { +) : ViewModel() { val searchSuggest: MutableLiveData?> = MutableLiveData() val searchResult: MutableLiveData, Boolean>>> = MutableLiveData() - fun getSearchSuggestions(query: String, gPlayAuth: AuthObject.GPlayAuth) { + fun getSearchSuggestions(query: String, authData: AuthData) { viewModelScope.launch(Dispatchers.IO) { - if (gPlayAuth.result.isSuccess()) - searchSuggest.postValue(fusedAPIRepository.getSearchSuggestions(query, gPlayAuth.result.data!!)) + searchSuggest.postValue(fusedAPIRepository.getSearchSuggestions(query, authData)) } } - fun loadData( - query: String, - lifecycleOwner: LifecycleOwner, - authObjectList: List, - retryBlock: (failedObjects: List) -> Boolean - ) { - - if (query.isBlank()) return - - super.onLoadData(authObjectList, { successAuthList, _ -> - - successAuthList.find { it is AuthObject.GPlayAuth }?.run { - getSearchResults(query, result.data!! as AuthData, lifecycleOwner) - return@onLoadData - } - - successAuthList.find { it is AuthObject.CleanApk }?.run { - getSearchResults(query, AuthData("", ""), lifecycleOwner) - return@onLoadData - } - }, retryBlock) - } - /* * Observe data from Fused API and publish the result in searchResult. * This allows us to show apps as they are being fetched from the network, @@ -84,16 +57,6 @@ class SearchViewModel @Inject constructor( viewModelScope.launch(Dispatchers.Main) { fusedAPIRepository.getSearchResults(query, authData).observe(lifecycleOwner) { searchResult.postValue(it) - - if (!it.isSuccess()) { - val exception = - if (authData.aasToken.isNotBlank() || authData.authToken.isNotBlank()) { - GPlayException(it.isTimeout(), "Data load error") - } else CleanApkException(it.isTimeout(), "Data load error") - - exceptionsList.add(exception) - exceptionsLiveData.postValue(exceptionsList) - } } } } diff --git a/app/src/main/java/foundation/e/apps/settings/SettingsFragment.kt b/app/src/main/java/foundation/e/apps/settings/SettingsFragment.kt index 5c56f9b97e692c7e5bdc0d93d9f766a7d335b826..18cbb4164307a63681eefae720f9c6d83ab311db 100644 --- a/app/src/main/java/foundation/e/apps/settings/SettingsFragment.kt +++ b/app/src/main/java/foundation/e/apps/settings/SettingsFragment.kt @@ -21,16 +21,19 @@ package foundation.e.apps.settings import android.content.ClipboardManager import android.content.Intent import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.View import android.widget.Toast -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.viewModels import androidx.navigation.findNavController +import androidx.navigation.fragment.findNavController import androidx.preference.CheckBoxPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.work.ExistingPeriodicWorkPolicy import coil.load +import com.aurora.gplayapi.data.models.AuthData import com.google.gson.Gson import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.BuildConfig @@ -38,7 +41,7 @@ import foundation.e.apps.MainActivity import foundation.e.apps.MainActivityViewModel import foundation.e.apps.R import foundation.e.apps.databinding.CustomPreferenceBinding -import foundation.e.apps.login.LoginViewModel +import foundation.e.apps.setup.signin.SignInViewModel import foundation.e.apps.updates.manager.UpdatesWorkManager import foundation.e.apps.utils.enums.User import foundation.e.apps.utils.modules.CommonUtilsFunctions @@ -50,17 +53,12 @@ class SettingsFragment : PreferenceFragmentCompat() { private var _binding: CustomPreferenceBinding? = null private val binding get() = _binding!! - private val mainActivityViewModel: MainActivityViewModel by activityViewModels() + private val viewModel: SignInViewModel by viewModels() + private val mainActivityViewModel: MainActivityViewModel by viewModels() private var showAllApplications: CheckBoxPreference? = null private var showFOSSApplications: CheckBoxPreference? = null private var showPWAApplications: CheckBoxPreference? = null - val loginViewModel: LoginViewModel by lazy { - ViewModelProvider(requireActivity())[LoginViewModel::class.java] - } - - private var sourcesChangedFlag = false - @Inject lateinit var gson: Gson @@ -71,10 +69,6 @@ class SettingsFragment : PreferenceFragmentCompat() { private const val TAG = "SettingsFragment" } - private val allSourceCheckboxes by lazy { - listOf(showAllApplications, showFOSSApplications, showPWAApplications) - } - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.settings_preferences, rootKey) @@ -118,51 +112,25 @@ class SettingsFragment : PreferenceFragmentCompat() { true } } - - allSourceCheckboxes.forEach { - it?.onPreferenceChangeListener = sourceCheckboxListener - } } - /** - * Checkbox listener to prevent all checkboxes from getting unchecked. - */ - private val sourceCheckboxListener = - Preference.OnPreferenceChangeListener { preference: Preference, newValue: Any? -> - - sourcesChangedFlag = true - loginViewModel.authObjects.value = null - - val otherBoxesChecked = - allSourceCheckboxes.filter { it != preference }.any { it?.isChecked == true } - - if (newValue == false && !otherBoxesChecked) { - (preference as CheckBoxPreference).isChecked = true - Toast.makeText( - requireActivity(), - R.string.select_one_source_of_applications, - Toast.LENGTH_SHORT - ).show() - false - } else true - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) _binding = CustomPreferenceBinding.bind(view) super.onViewCreated(view, savedInstanceState) - mainActivityViewModel.gPlayAuthData.let { authData -> - mainActivityViewModel.getUser().name.let { user -> + mainActivityViewModel.authDataJson.observe(viewLifecycleOwner) { + val authData = gson.fromJson(it, AuthData::class.java) + viewModel.userType.observe(viewLifecycleOwner) { user -> when (user) { User.ANONYMOUS.name -> { binding.accountType.text = view.context.getString(R.string.user_anonymous) } User.GOOGLE.name -> { - if (!authData.isAnonymous) { + if (authData != null) { binding.accountType.text = authData.userProfile?.name - binding.email.text = mainActivityViewModel.getUserEmail() + binding.email.text = authData.userProfile?.email binding.avatar.load(authData.userProfile?.artwork?.url) } } @@ -175,7 +143,11 @@ class SettingsFragment : PreferenceFragmentCompat() { } binding.logout.setOnClickListener { - loginViewModel.logout() + viewModel.saveUserType(User.UNAVAILABLE) + Toast.makeText(requireContext(), "Signing out...", Toast.LENGTH_LONG).show() + Handler(Looper.getMainLooper()).postDelayed({ + backToMainActivity() + }, 1500) } } @@ -192,9 +164,6 @@ class SettingsFragment : PreferenceFragmentCompat() { } override fun onDestroyView() { - if (sourcesChangedFlag) { - loginViewModel.startLoginFlow() - } super.onDestroyView() _binding = null } diff --git a/app/src/main/java/foundation/e/apps/setup/signin/SignInFragment.kt b/app/src/main/java/foundation/e/apps/setup/signin/SignInFragment.kt index ffd568aa0a8e3282b9f6f84c9a82287314e4c9c0..b2e354b674df9e2fd145321194f877e7a61fba64 100644 --- a/app/src/main/java/foundation/e/apps/setup/signin/SignInFragment.kt +++ b/app/src/main/java/foundation/e/apps/setup/signin/SignInFragment.kt @@ -3,36 +3,46 @@ package foundation.e.apps.setup.signin import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.viewModels import androidx.navigation.findNavController import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.databinding.FragmentSignInBinding -import foundation.e.apps.login.LoginViewModel -import foundation.e.apps.utils.modules.CommonUtilsModule.safeNavigate +import foundation.e.apps.utils.enums.User @AndroidEntryPoint class SignInFragment : Fragment(R.layout.fragment_sign_in) { private var _binding: FragmentSignInBinding? = null private val binding get() = _binding!! - private val viewModel: LoginViewModel by lazy { - ViewModelProvider(requireActivity())[LoginViewModel::class.java] - } + private val viewModel: SignInViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) _binding = FragmentSignInBinding.bind(view) binding.googleBT.setOnClickListener { - view.findNavController() - .safeNavigate(R.id.signInFragment, R.id.action_signInFragment_to_googleSignInFragment) + view.findNavController().navigate(R.id.googleSignInFragment) } binding.anonymousBT.setOnClickListener { - viewModel.initialAnonymousLogin { - view.findNavController() - .safeNavigate(R.id.signInFragment, R.id.action_signInFragment_to_homeFragment) + viewModel.saveUserType(User.ANONYMOUS) + } + + viewModel.userType.observe(viewLifecycleOwner) { + if (it.isNotBlank()) { + when (User.valueOf(it)) { + User.ANONYMOUS -> { + view.findNavController() + .navigate(R.id.action_signInFragment_to_homeFragment) + } + User.GOOGLE -> { + view.findNavController() + .navigate(R.id.action_signInFragment_to_homeFragment) + } + else -> { + } + } } } } diff --git a/app/src/main/java/foundation/e/apps/setup/signin/google/GoogleSignInFragment.kt b/app/src/main/java/foundation/e/apps/setup/signin/google/GoogleSignInFragment.kt index 8470c359c846868c7bf590d7cfd5b576f57d5228..d21a536467e9210f5157e3c252e5bd9e717eeba1 100644 --- a/app/src/main/java/foundation/e/apps/setup/signin/google/GoogleSignInFragment.kt +++ b/app/src/main/java/foundation/e/apps/setup/signin/google/GoogleSignInFragment.kt @@ -27,23 +27,21 @@ import android.webkit.WebSettings import android.webkit.WebView import android.webkit.WebViewClient import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.viewModels import androidx.navigation.findNavController import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.api.gplay.utils.AC2DMUtil import foundation.e.apps.databinding.FragmentGoogleSigninBinding -import foundation.e.apps.login.LoginViewModel -import foundation.e.apps.utils.modules.CommonUtilsModule.safeNavigate +import foundation.e.apps.setup.signin.SignInViewModel +import foundation.e.apps.utils.enums.User @AndroidEntryPoint class GoogleSignInFragment : Fragment(R.layout.fragment_google_signin) { private var _binding: FragmentGoogleSigninBinding? = null private val binding get() = _binding!! - private val viewModel: LoginViewModel by lazy { - ViewModelProvider(requireActivity())[LoginViewModel::class.java] - } + private val viewModel: SignInViewModel by viewModels() companion object { private const val EMBEDDED_SETUP_URL = @@ -55,6 +53,18 @@ class GoogleSignInFragment : Fragment(R.layout.fragment_google_signin) { super.onViewCreated(view, savedInstanceState) _binding = FragmentGoogleSigninBinding.bind(view) setupWebView() + + viewModel.userType.observe(viewLifecycleOwner) { + if (it.isNotBlank()) { + when (User.valueOf(it)) { + User.GOOGLE -> { + view.findNavController() + .navigate(R.id.action_googleSignInFragment_to_homeFragment) + } + else -> {} + } + } + } } @SuppressLint("SetJavaScriptEnabled") @@ -74,17 +84,10 @@ class GoogleSignInFragment : Fragment(R.layout.fragment_google_signin) { val cookieMap = AC2DMUtil.parseCookieString(cookies) if (cookieMap.isNotEmpty() && cookieMap[AUTH_TOKEN] != null) { val oauthToken = cookieMap[AUTH_TOKEN] ?: "" - view.evaluateJavascript( - "document.querySelector(\"div[data-profile-identifier]\").textContent;" - ) { + view.evaluateJavascript("(function() { return document.getElementById('profileIdentifier').innerHTML; })();") { val email = it.replace("\"".toRegex(), "") - viewModel.initialGoogleLogin(email, oauthToken) { - view.findNavController() - .safeNavigate( - R.id.googleSignInFragment, - R.id.action_googleSignInFragment_to_homeFragment - ) - } + viewModel.saveEmailToken(email, oauthToken) + viewModel.saveUserType(User.GOOGLE) } } } diff --git a/app/src/main/java/foundation/e/apps/setup/tos/TOSFragment.kt b/app/src/main/java/foundation/e/apps/setup/tos/TOSFragment.kt index 9a0515c1584f0b35b163c04aff866218b7d80eb3..920a70f492e971d514fa8b965a5cbd7d80ce6a02 100644 --- a/app/src/main/java/foundation/e/apps/setup/tos/TOSFragment.kt +++ b/app/src/main/java/foundation/e/apps/setup/tos/TOSFragment.kt @@ -19,6 +19,10 @@ class TOSFragment : Fragment(R.layout.fragment_tos) { private var _binding: FragmentTosBinding? = null private val binding get() = _binding!! + /* + * Fix memory leaks related to WebView. + * Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/485 + */ private var webView: WebView? = null private val viewModel: TOSViewModel by viewModels() @@ -27,8 +31,12 @@ class TOSFragment : Fragment(R.layout.fragment_tos) { super.onViewCreated(view, savedInstanceState) _binding = FragmentTosBinding.bind(view) webView = binding.tosWebView + var canNavigate = false viewModel.tocStatus.observe(viewLifecycleOwner) { + if (canNavigate) { + view.findNavController().navigate(R.id.action_TOSFragment_to_signInFragment) + } if (it == true && webView != null) { binding.TOSWarning.visibility = View.GONE @@ -60,6 +68,7 @@ class TOSFragment : Fragment(R.layout.fragment_tos) { binding.agreeBT.setOnClickListener { viewModel.saveTOCStatus(true) + canNavigate = true } } diff --git a/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt b/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt index 06c7640e84630d49811ea57b2161d8c3b9b4cd96..e773e2cc4470fc300060c0d306cbf27c90f5bf7c 100644 --- a/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt +++ b/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt @@ -21,7 +21,6 @@ package foundation.e.apps.updates import android.os.Bundle import android.view.View import android.widget.ImageView -import androidx.appcompat.app.AlertDialog import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope @@ -31,25 +30,25 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.work.WorkInfo import androidx.work.WorkManager +import com.aurora.gplayapi.data.models.AuthData import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.AppInfoFetchViewModel import foundation.e.apps.AppProgressViewModel import foundation.e.apps.MainActivityViewModel import foundation.e.apps.PrivacyInfoViewModel import foundation.e.apps.R +import foundation.e.apps.api.fused.FusedAPIImpl import foundation.e.apps.api.fused.FusedAPIInterface import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.applicationlist.ApplicationListRVAdapter import foundation.e.apps.databinding.FragmentUpdatesBinding -import foundation.e.apps.login.AuthObject import foundation.e.apps.manager.download.data.DownloadProgress import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.manager.workmanager.InstallWorkManager.INSTALL_WORK_NAME import foundation.e.apps.updates.manager.UpdatesWorkManager +import foundation.e.apps.utils.enums.ResultStatus import foundation.e.apps.utils.enums.Status -import foundation.e.apps.utils.exceptions.GPlayException -import foundation.e.apps.utils.exceptions.GPlayLoginException import foundation.e.apps.utils.modules.CommonUtilsModule.safeNavigate import foundation.e.apps.utils.modules.PWAManagerModule import foundation.e.apps.utils.parentFragment.TimeoutFragment @@ -86,7 +85,7 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte * Explanation of double observers in HomeFragment.kt */ - /*mainActivityViewModel.internetConnection.observe(viewLifecycleOwner) { + mainActivityViewModel.internetConnection.observe(viewLifecycleOwner) { if (!updatesViewModel.updatesList.value?.first.isNullOrEmpty()) { return@observe } @@ -94,20 +93,6 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte } mainActivityViewModel.authData.observe(viewLifecycleOwner) { refreshDataOrRefreshToken(mainActivityViewModel) - }*/ - - setupListening() - - authObjects.observe(viewLifecycleOwner) { - if (it == null) return@observe - if (!updatesViewModel.updatesList.value?.first.isNullOrEmpty()) { - return@observe - } - loadData(it) - } - - updatesViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { - handleExceptionsCommon(it) } val recyclerView = binding.recyclerView @@ -159,9 +144,9 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte } } - /*if (it.second != ResultStatus.OK) { + if (it.second != ResultStatus.OK) { onTimeout() - }*/ + } } } @@ -181,7 +166,7 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte ).show(childFragmentManager, "UpdatesFragment") } - /*override fun onTimeout() { + override fun onTimeout() { if (!isTimeoutDialogDisplayed()) { stopLoadingUI() displayTimeoutAlertDialog( @@ -209,54 +194,11 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte allowCancel = true, ) } - }*/ - - override fun onTimeout( - exception: Exception, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog.apply { - if (exception is GPlayException) { - setMessage(R.string.timeout_desc_gplay) - setNegativeButton(R.string.open_settings) { _, _ -> - openSettings() - } - } else { - setMessage(R.string.timeout_desc_cleanapk) - } - } - } - - override fun onSignInError( - exception: GPlayLoginException, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog.apply { - setNegativeButton(R.string.open_settings) { _, _ -> - openSettings() - } - } - } - - override fun onDataLoadError( - exception: Exception, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog.apply { - if (exception is GPlayException) { - setNegativeButton(R.string.open_settings) { _, _ -> - openSettings() - } - } - } } - override fun loadData(authObjectList: List) { + override fun refreshData(authData: AuthData) { showLoadingUI() - updatesViewModel.loadData(authObjectList) { - clearAndRestartGPlayLogin() - true - } + updatesViewModel.getUpdates(authData) binding.button.setOnClickListener { UpdatesWorkManager.startUpdateAllWork(requireContext().applicationContext) observeUpdateWork() @@ -280,14 +222,14 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte } } - override fun showLoadingUI() { + private fun showLoadingUI() { binding.button.isEnabled = false binding.noUpdates.visibility = View.GONE binding.progressBar.visibility = View.VISIBLE binding.recyclerView.visibility = View.INVISIBLE } - override fun stopLoadingUI() { + private fun stopLoadingUI() { binding.progressBar.visibility = View.GONE binding.recyclerView.visibility = View.VISIBLE } @@ -297,7 +239,7 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte appProgressViewModel.downloadProgress.observe(viewLifecycleOwner) { updateProgressOfDownloadingItems(binding.recyclerView, it) } -// resetTimeoutDialogLock() + resetTimeoutDialogLock() } private fun observeDownloadList() { diff --git a/app/src/main/java/foundation/e/apps/updates/UpdatesViewModel.kt b/app/src/main/java/foundation/e/apps/updates/UpdatesViewModel.kt index 9be088227179babdc3f04b51deb2d5fbd788cbe0..fe1c036faa4f9e25fc2e27052d9054765c887e3a 100644 --- a/app/src/main/java/foundation/e/apps/updates/UpdatesViewModel.kt +++ b/app/src/main/java/foundation/e/apps/updates/UpdatesViewModel.kt @@ -19,19 +19,16 @@ package foundation.e.apps.updates import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.work.WorkInfo import com.aurora.gplayapi.data.models.AuthData import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp -import foundation.e.apps.login.AuthObject import foundation.e.apps.updates.manager.UpdatesManagerRepository import foundation.e.apps.utils.enums.ResultStatus import foundation.e.apps.utils.enums.Status -import foundation.e.apps.utils.exceptions.CleanApkException -import foundation.e.apps.utils.exceptions.GPlayException -import foundation.e.apps.utils.parentFragment.LoadingViewModel import kotlinx.coroutines.launch import javax.inject.Inject @@ -39,52 +36,14 @@ import javax.inject.Inject class UpdatesViewModel @Inject constructor( private val updatesManagerRepository: UpdatesManagerRepository, private val fusedAPIRepository: FusedAPIRepository -) : LoadingViewModel() { +) : ViewModel() { val updatesList: MutableLiveData, ResultStatus?>> = MutableLiveData() - fun loadData( - authObjectList: List, - retryBlock: (failedObjects: List) -> Boolean, - ) { - super.onLoadData(authObjectList, { successAuthList, _ -> - - successAuthList.find { it is AuthObject.GPlayAuth }?.run { - getUpdates(result.data!! as AuthData) - return@onLoadData - } - - successAuthList.find { it is AuthObject.CleanApk }?.run { - getUpdates(AuthData("", "")) - return@onLoadData - } - }, retryBlock) - } - - fun getUpdates(authData: AuthData?) { + fun getUpdates(authData: AuthData) { viewModelScope.launch { - val updatesResult = if (authData != null) - updatesManagerRepository.getUpdates(authData) - else updatesManagerRepository.getUpdatesOSS() + val updatesResult = updatesManagerRepository.getUpdates(authData) updatesList.postValue(updatesResult) - - if (updatesResult.second != ResultStatus.OK) { - val exception = - if (authData != null && - (authData.aasToken.isNotBlank() || authData.authToken.isNotBlank()) - ) { - GPlayException( - updatesResult.second == ResultStatus.TIMEOUT, - "Data load error" - ) - } else CleanApkException( - updatesResult.second == ResultStatus.TIMEOUT, - "Data load error" - ) - - exceptionsList.add(exception) - exceptionsLiveData.postValue(exceptionsList) - } } } diff --git a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt index 06d006aa06aad30933ba52ef2488fe2bef514cb2..5fd534f7ad62f1482751c1dc15916693f8fd578e 100644 --- a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt +++ b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt @@ -83,37 +83,6 @@ class UpdatesManagerImpl @Inject constructor( return Pair(nonFaultyUpdateList, status) } - suspend fun getUpdatesOSS(): Pair, ResultStatus> { - val pkgList = mutableListOf() - val updateList = mutableListOf() - var status = ResultStatus.OK - - val userApplications = pkgManagerModule.getAllUserApps() - userApplications.forEach { pkgList.add(it.packageName) } - - if (pkgList.isNotEmpty()) { - // Get updates from CleanAPK - val cleanAPKResult = fusedAPIRepository.getApplicationDetails( - pkgList, - AuthData("", ""), - Origin.CLEANAPK - ) - cleanAPKResult.first.forEach { - if (it.status == Status.UPDATABLE && it.filterLevel.isUnFiltered()) updateList.add( - it - ) - } - cleanAPKResult.second.let { - if (it != ResultStatus.OK) { - status = it - } - } - } - - val nonFaultyUpdateList = faultyAppRepository.removeFaultyApps(updateList) - return Pair(nonFaultyUpdateList, status) - } - fun getApplicationCategoryPreference(): String { return fusedAPIRepository.getApplicationCategoryPreference() } diff --git a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerRepository.kt b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerRepository.kt index c56c280c4f279d9fca453de49bf4f72cf3833379..0805a8a5bd6b54b23c9745153b65a8361c1d7250 100644 --- a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerRepository.kt +++ b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerRepository.kt @@ -39,10 +39,6 @@ class UpdatesManagerRepository @Inject constructor( } } - suspend fun getUpdatesOSS(): Pair, ResultStatus> { - return updatesManagerImpl.getUpdatesOSS() - } - fun getApplicationCategoryPreference(): String { return updatesManagerImpl.getApplicationCategoryPreference() } diff --git a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt index 7aa77888d4d1383e9c5fd326930cc28e01979f33..e8965a51d8f4624c9a4af43eab0379d74d14b453 100644 --- a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt +++ b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt @@ -26,7 +26,6 @@ import foundation.e.apps.manager.workmanager.InstallWorkManager import foundation.e.apps.updates.UpdatesNotifier import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.Type -import foundation.e.apps.utils.enums.User import foundation.e.apps.utils.modules.DataStoreModule import timber.log.Timber import java.io.ByteArrayOutputStream @@ -67,40 +66,11 @@ class UpdatesWorker @AssistedInject constructor( } } - private fun getUser(): User { - return dataStoreModule.getUserType() - } - private suspend fun checkForUpdates() { loadSettings() val isConnectedToUnmeteredNetwork = isConnectedToUnmeteredNetwork(applicationContext) - val appsNeededToUpdate = mutableListOf() - val user = getUser() val authData = getAuthData() - - if (user in listOf(User.ANONYMOUS, User.GOOGLE) && authData != null) { - /* - * Signifies valid Google user and valid auth data to update - * apps from Google Play store. - * The user check will be more useful in No Google mode. - */ - appsNeededToUpdate.addAll(updatesManagerRepository.getUpdates(authData).first) - } else if (user != User.UNAVAILABLE) { - /* - * If authData is null, update apps from cleanapk only. - */ - appsNeededToUpdate.addAll(updatesManagerRepository.getUpdatesOSS().first) - } else { - /* - * If user in UNAVAILABLE, don't do anything. - */ - return - } - - /* - * Show notification only if enabled. - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5376 - */ + val appsNeededToUpdate = updatesManagerRepository.getUpdates(authData).first if (isAutoUpdate && shouldShowNotification) { handleNotification(appsNeededToUpdate.size, isConnectedToUnmeteredNetwork) } @@ -108,12 +78,7 @@ class UpdatesWorker @AssistedInject constructor( triggerUpdateProcessOnSettings( isConnectedToUnmeteredNetwork, appsNeededToUpdate, - /* - * If authData is null, only cleanApk data will be present - * in appsNeededToUpdate list. Hence it is safe to proceed with - * blank AuthData. - */ - authData ?: AuthData("", ""), + authData ) } @@ -148,10 +113,9 @@ class UpdatesWorker @AssistedInject constructor( } } - private fun getAuthData(): AuthData? { + private fun getAuthData(): AuthData { val authDataJson = dataStoreModule.getAuthDataSync() - return if (authDataJson.isBlank()) return null - else gson.fromJson(authDataJson, AuthData::class.java) + return gson.fromJson(authDataJson, AuthData::class.java) } private suspend fun startUpdateProcess( diff --git a/app/src/main/java/foundation/e/apps/utils/Constants.kt b/app/src/main/java/foundation/e/apps/utils/Constants.kt deleted file mode 100644 index d8083adafa4dbd5b619ccbe6d92914660371aa7b..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/utils/Constants.kt +++ /dev/null @@ -1,9 +0,0 @@ -package foundation.e.apps.utils - -object Constants { - const val timeoutDurationInMillis: Long = 10000 - - const val PREFERENCE_SHOW_FOSS = "showFOSSApplications" - const val PREFERENCE_SHOW_PWA = "showPWAApplications" - const val PREFERENCE_SHOW_GPLAY = "showAllApplications" -} diff --git a/app/src/main/java/foundation/e/apps/utils/exceptions/CleanApkException.kt b/app/src/main/java/foundation/e/apps/utils/exceptions/CleanApkException.kt deleted file mode 100644 index 5fed1e3a0d3c36160f0b99b0bb25846ab7fccdf2..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/utils/exceptions/CleanApkException.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.utils.exceptions - -/** - * This exception is for all CleanApk data loading exceptions. - */ -class CleanApkException( - val isTimeout: Boolean, - message: String? = null, -) : LoginException(message) diff --git a/app/src/main/java/foundation/e/apps/utils/exceptions/GPlayException.kt b/app/src/main/java/foundation/e/apps/utils/exceptions/GPlayException.kt deleted file mode 100644 index 5a01fb4d36f69ee1f0ef1782e4f859fd02719239..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/utils/exceptions/GPlayException.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.utils.exceptions - -/** - * This exception is for all Google Play network calls or other GPlay related exceptions. - */ -open class GPlayException( - val isTimeout: Boolean, - message: String? = null, -) : LoginException(message) diff --git a/app/src/main/java/foundation/e/apps/utils/exceptions/GPlayLoginException.kt b/app/src/main/java/foundation/e/apps/utils/exceptions/GPlayLoginException.kt deleted file mode 100644 index 2902ba28f8ca59a83c36b29f85f78820eb8c1f95..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/utils/exceptions/GPlayLoginException.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2019-2022 MURENA SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.utils.exceptions - -import foundation.e.apps.utils.enums.User - -/** - * Parent class for all GPlay login related errors. - * Examples: - * unable to get anonymous token, AuthData validation failed, aasToken error etc. - */ -open class GPlayLoginException( - isTimeout: Boolean, - message: String? = null, - val user: User, -) : GPlayException(isTimeout, message) diff --git a/app/src/main/java/foundation/e/apps/utils/exceptions/GPlayValidationException.kt b/app/src/main/java/foundation/e/apps/utils/exceptions/GPlayValidationException.kt deleted file mode 100644 index 0257dc91cfeca16fd0e0d7e2f94f2423ff49ec75..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/utils/exceptions/GPlayValidationException.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.utils.exceptions - -import foundation.e.apps.utils.enums.User - -/** - * This exception is specifically used when a GPlay auth data could not be validated. - * This is not the case as timeout, this exception usually means the server informed that the - * current auth data is not valid. - * Use [networkCode] to be sure that the server call was successful (should be 200). - */ -class GPlayValidationException( - message: String, - user: User, - val networkCode: Int, -) : GPlayLoginException(false, message, user) diff --git a/app/src/main/java/foundation/e/apps/utils/exceptions/LoginException.kt b/app/src/main/java/foundation/e/apps/utils/exceptions/LoginException.kt deleted file mode 100644 index 01011dbdf64b004972573a5854caf1c19a69191f..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/utils/exceptions/LoginException.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.utils.exceptions - -/** - * Super class for all Login related exceptions. - * https://gitlab.e.foundation/e/backlog/-/issues/5680 - */ -open class LoginException(message: String?) : Exception(message) diff --git a/app/src/main/java/foundation/e/apps/utils/exceptions/UnknownSourceException.kt b/app/src/main/java/foundation/e/apps/utils/exceptions/UnknownSourceException.kt deleted file mode 100644 index 10e5ab36211413b305f2d8573c3d90c4a99d441a..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/utils/exceptions/UnknownSourceException.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2019-2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.utils.exceptions - -/** - * Generic exception class - used to define unknown errors. - */ -class UnknownSourceException : LoginException("") diff --git a/app/src/main/java/foundation/e/apps/utils/modules/CommonUtilsModule.kt b/app/src/main/java/foundation/e/apps/utils/modules/CommonUtilsModule.kt index a533a77e5ecb83dca582a1cdfcd61656957bad56..4b82fe260bcec174c1dcbf84c26f075776406891 100644 --- a/app/src/main/java/foundation/e/apps/utils/modules/CommonUtilsModule.kt +++ b/app/src/main/java/foundation/e/apps/utils/modules/CommonUtilsModule.kt @@ -48,6 +48,7 @@ import javax.inject.Singleton object CommonUtilsModule { val LIST_OF_NULL = listOf("null") + const val timeoutDurationInMillis: Long = 25000 // Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709 const val NETWORK_CODE_SUCCESS = 200 diff --git a/app/src/main/java/foundation/e/apps/utils/modules/DataStoreModule.kt b/app/src/main/java/foundation/e/apps/utils/modules/DataStoreModule.kt index e30305e9b33100047e3f9b1c65d9e64374bbbb75..a85d033654d3d08234b9b8e72583bc9f236b3b6f 100644 --- a/app/src/main/java/foundation/e/apps/utils/modules/DataStoreModule.kt +++ b/app/src/main/java/foundation/e/apps/utils/modules/DataStoreModule.kt @@ -40,10 +40,8 @@ class DataStoreModule @Inject constructor( private val gson: Gson ) { - companion object { - private const val preferenceDataStoreName = "Settings" - val Context.dataStore by preferencesDataStore(preferenceDataStoreName) - } + private val preferenceDataStoreName = "Settings" + private val Context.dataStore by preferencesDataStore(preferenceDataStoreName) private val AUTHDATA = stringPreferencesKey("authData") private val EMAIL = stringPreferencesKey("email") @@ -133,23 +131,11 @@ class DataStoreModule @Inject constructor( } } - fun getEmail(): String { - return runBlocking { - emailData.first() - } + suspend fun getEmail(): String { + return emailData.first() } suspend fun getAASToken(): String { return aasToken.first() } - - fun getUserType(): User { - return runBlocking { - userType.first().run { - val userStrings = User.values().map { it.name } - if (this !in userStrings) User.UNAVAILABLE - else User.valueOf(this) - } - } - } } diff --git a/app/src/main/java/foundation/e/apps/utils/modules/PreferenceManagerModule.kt b/app/src/main/java/foundation/e/apps/utils/modules/PreferenceManagerModule.kt index 0fa6cf9c9b9972f143b4aed1214f61d6ce587245..9be9b4a3a55e3826c2e95a048bb75acd07892c7d 100644 --- a/app/src/main/java/foundation/e/apps/utils/modules/PreferenceManagerModule.kt +++ b/app/src/main/java/foundation/e/apps/utils/modules/PreferenceManagerModule.kt @@ -22,9 +22,6 @@ import android.content.Context import androidx.preference.PreferenceManager import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.OpenForTesting -import foundation.e.apps.utils.Constants.PREFERENCE_SHOW_FOSS -import foundation.e.apps.utils.Constants.PREFERENCE_SHOW_GPLAY -import foundation.e.apps.utils.Constants.PREFERENCE_SHOW_PWA import javax.inject.Inject import javax.inject.Singleton @@ -37,8 +34,8 @@ class PreferenceManagerModule @Inject constructor( private val preferenceManager = PreferenceManager.getDefaultSharedPreferences(context) fun preferredApplicationType(): String { - val showFOSSApplications = preferenceManager.getBoolean(PREFERENCE_SHOW_FOSS, false) - val showPWAApplications = preferenceManager.getBoolean(PREFERENCE_SHOW_PWA, false) + val showFOSSApplications = preferenceManager.getBoolean("showFOSSApplications", false) + val showPWAApplications = preferenceManager.getBoolean("showPWAApplications", false) return when { showFOSSApplications -> "open" @@ -47,9 +44,9 @@ class PreferenceManagerModule @Inject constructor( } } - fun isOpenSourceSelected() = preferenceManager.getBoolean(PREFERENCE_SHOW_FOSS, true) - fun isPWASelected() = preferenceManager.getBoolean(PREFERENCE_SHOW_PWA, true) - fun isGplaySelected() = preferenceManager.getBoolean(PREFERENCE_SHOW_GPLAY, true) + fun isOpenSourceSelected() = preferenceManager.getBoolean("showFOSSApplications", true) + fun isPWASelected() = preferenceManager.getBoolean("showPWAApplications", true) + fun isGplaySelected() = preferenceManager.getBoolean("showAllApplications", true) fun autoUpdatePreferred(): Boolean { return preferenceManager.getBoolean("updateInstallAuto", false) diff --git a/app/src/main/java/foundation/e/apps/utils/parentFragment/LoadingViewModel.kt b/app/src/main/java/foundation/e/apps/utils/parentFragment/LoadingViewModel.kt deleted file mode 100644 index cbd0ab493ca7bef005ff9db5b96acf7ed4ace54a..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/utils/parentFragment/LoadingViewModel.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2019-2022 MURENA SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.utils.parentFragment - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import foundation.e.apps.login.AuthObject -import foundation.e.apps.utils.exceptions.GPlayValidationException -import foundation.e.apps.utils.exceptions.UnknownSourceException - -abstract class LoadingViewModel : ViewModel() { - - companion object { - private var autoRetried = false - } - - val exceptionsLiveData: MutableLiveData> = MutableLiveData() - val exceptionsList = ArrayList() - - /** - * Call this method from ViewModel. - * - * @param authObjectList List obtained from login process. - * @param loadingBlock Define how to load data in this method. - * @param retryBlock Define retry mechanism for failed AuthObject. - * Return `true` to signify the failure event is consumed by the block and no further - * processing on failed AuthObject is needed. - */ - fun onLoadData( - authObjectList: List, - loadingBlock: (successObjects: List, failedObjects: List) -> Unit, - retryBlock: (failedObjects: List) -> Boolean, - ) { - - exceptionsList.clear() - - val successAuthList = authObjectList.filter { it.result.isSuccess() } - val failedAuthList = authObjectList.filter { !it.result.isSuccess() } - - failedAuthList.forEach { - exceptionsList.add(it.result.exception ?: UnknownSourceException()) - } - - exceptionsList.find { - it is GPlayValidationException - }?.run { - if (!autoRetried && retryBlock(failedAuthList)) { - autoRetried = true - exceptionsList.clear() - return - } - } - - loadingBlock(successAuthList, failedAuthList) - - if (successAuthList.isEmpty() && exceptionsList.isNotEmpty()) { - /* - * As no authentication is successful, nothing can be loaded, - * post the exceptions. - */ - exceptionsLiveData.postValue(exceptionsList) - } - } -} diff --git a/app/src/main/java/foundation/e/apps/utils/parentFragment/TimeoutFragment.kt b/app/src/main/java/foundation/e/apps/utils/parentFragment/TimeoutFragment.kt index 0744b390edcb67910994dd4134a62994ce78f6cf..e5e4c6453d5244e475da70b6d227374ee9752fa4 100644 --- a/app/src/main/java/foundation/e/apps/utils/parentFragment/TimeoutFragment.kt +++ b/app/src/main/java/foundation/e/apps/utils/parentFragment/TimeoutFragment.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2022 MURENA SAS + * Copyright (C) 2022 ECORP * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,348 +17,202 @@ package foundation.e.apps.utils.parentFragment +import android.app.Activity +import android.view.KeyEvent import androidx.annotation.LayoutRes import androidx.appcompat.app.AlertDialog -import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModelProvider +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.MainActivityViewModel import foundation.e.apps.R -import foundation.e.apps.databinding.DialogErrorLogBinding -import foundation.e.apps.login.AuthObject -import foundation.e.apps.login.LoginSourceGPlay -import foundation.e.apps.login.LoginViewModel -import foundation.e.apps.utils.enums.User -import foundation.e.apps.utils.exceptions.CleanApkException -import foundation.e.apps.utils.exceptions.GPlayException -import foundation.e.apps.utils.exceptions.GPlayLoginException -import foundation.e.apps.utils.exceptions.GPlayValidationException -import foundation.e.apps.utils.exceptions.UnknownSourceException -/** - * Parent class of all fragments. - * - * Mostly contains UI related code regarding dialogs to display. - * Does also provide some interaction with [LoginViewModel]. - * - * https://gitlab.e.foundation/e/backlog/-/issues/5680 +/* + * Parent class (extending fragment) for fragments which can display a timeout dialog + * for network calls exceeding timeout limit. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 */ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { - val loginViewModel: LoginViewModel by lazy { - ViewModelProvider(requireActivity())[LoginViewModel::class.java] - } - - /** - * Fragments observe this list to load data. - * Fragments should not observe [loginViewModel]'s authObjects. - */ - val authObjects: MutableLiveData?> = MutableLiveData() - - abstract fun loadData(authObjectList: List) - - abstract fun showLoadingUI() - - abstract fun stopLoadingUI() - - /** - * Override to contain code to execute in case of timeout. - * Do not call this function directly, use [showTimeout] for that. - * - * @param predefinedDialog An AlertDialog builder, already having some properties, - * Fragment can change the dialog properties and return as the result. - * By default: - * 1. Dialog title set to [R.string.timeout_title] - * 2. Dialog content set to [R.string.timeout_desc_cleanapk]. - * 3. Dialog can show technical error info on clicking "More Info" - * 4. Has a positive button "Retry" which calls [LoginViewModel.startLoginFlow]. - * 5. Has a negative button "Close" which just closes the dialog. - * 6. Dialog is cancellable. + /* + * Alert dialog to show to user if App Lounge times out. * - * @return An alert dialog (created from [predefinedDialog]) to show a timeout dialog, - * or null to not show anything. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404 */ - abstract fun onTimeout( - exception: Exception, - predefinedDialog: AlertDialog.Builder, - ): AlertDialog.Builder? + private var timeoutAlertDialog: AlertDialog? = null - /** - * Override to contain code to execute in case of other sign in error. - * This can only happen for GPlay data as cleanapk does not need any login. - * Do not call this function directly, use [showSignInError] for that. - * - * @param predefinedDialog An AlertDialog builder, already having some properties, - * Fragment can change the dialog properties and return as the result. - * By default: - * 1. Dialog title set to [R.string.anonymous_login_failed] or [R.string.sign_in_failed_title] - * 2. Content set to [R.string.anonymous_login_failed_desc] or [R.string.sign_in_failed_desc] - * 3. Dialog can show technical error info on clicking "More Info" - * 4. Has a positive button "Retry" which calls [LoginViewModel.startLoginFlow], - * passing the list of failed auth types. - * 5. Has a negative button "Logout" which logs the user out of App Lounge. - * 6. Dialog is cancellable. - * - * @return An alert dialog (created from [predefinedDialog]) to show a timeout dialog, - * or null to not show anything. - */ - abstract fun onSignInError( - exception: GPlayLoginException, - predefinedDialog: AlertDialog.Builder, - ): AlertDialog.Builder? + abstract fun onTimeout() - /** - * Override to contain code to execute for error during loading data. - * Do not call this function directly, use [showDataLoadError] for that. + /* + * Set this to true when timeout dialog is once shown. + * Set to false if user clicks "Retry". + * Use this to prevent repeatedly showing timeout dialog. * - * @param predefinedDialog An AlertDialog builder, already having some properties, - * Fragment can change the dialog properties and return as the result. - * By default: - * 1. Dialog title set to [R.string.data_load_error]. - * 2. Dialog content set to [R.string.data_load_error_desc]. - * 3. Dialog can show technical error info on clicking "More Info" - * 4. Has a positive button "Retry" which calls [loadData]. - * 5. Has a negative button "Close" which just closes the dialog. - * 6. Dialog is cancellable. - */ - abstract fun onDataLoadError( - exception: Exception, - predefinedDialog: AlertDialog.Builder, - ): AlertDialog.Builder? - - /** - * Crucial to call this, other wise fragments will never receive any authentications. - */ - fun setupListening() { - loginViewModel.authObjects.observe(viewLifecycleOwner) { - authObjects.postValue(it) - } - } - - /** - * Call this to repopulate authObjects with old data, this can be used to refresh data - * from inside the observer on [authObjects]. + * Setting the value to true is automatically done from displayTimeoutAlertDialog(). + * To set it as false, call resetTimeoutDialogLock(). + * + * Timeout dialog maybe shown multiple times from MainActivity authData observer, + * MainActivityViewModel.downloadList observer, or simply from timing out while + * fetch the information for the fragment. */ - fun repostAuthObjects() { - authObjects.postValue(loginViewModel.authObjects.value) - } + private var timeoutDialogShownLock: Boolean = false - /** - * Clears saved GPlay AuthData and restarts login process to get + /* + * Do call this in the "Retry" button block of timeout dialog. + * Also call this in onResume(), otherwise after screen off, the timeout dialog may not appear. */ - fun clearAndRestartGPlayLogin() { - loginViewModel.startLoginFlow(listOf(LoginSourceGPlay::class.java.simpleName)) + fun resetTimeoutDialogLock() { + timeoutDialogShownLock = false } - /** - * Store the last shown dialog, so that when a new dialog is to be shown, - * the old dialog can be automatically dismissed. + /* + * Recommended to put code to refresh data inside this block. + * But call refreshDataOrRefreshToken() to execute the refresh. */ - private var lastDialog: AlertDialog? = null + abstract fun refreshData(authData: AuthData) - /** - * Show a dialog, dismiss previously shown dialog in [lastDialog]. + /* + * Checks if network connectivity is present. + * -- If yes, then checks if valid authData is present. + * ---- If yes, then dismiss timeout dialog (if showing) and call refreshData() + * ---- If no, then request new token data. */ - private fun showAndSetDialog(alertDialogBuilder: AlertDialog.Builder) { - alertDialogBuilder.create().run { - if (lastDialog?.isShowing == true) { - lastDialog?.dismiss() + fun refreshDataOrRefreshToken(mainActivityViewModel: MainActivityViewModel) { + if (mainActivityViewModel.internetConnection.value == true) { + mainActivityViewModel.authData.value?.let { authData -> + dismissTimeoutDialog() + refreshData(authData) + } ?: run { + if (mainActivityViewModel.authValidity.value != null) { // checking at least authvalidity is checked for once + mainActivityViewModel.retryFetchingTokenAfterTimeout() + } } - this.show() - lastDialog = this } } /** - * Call to trigger [onTimeout]. - * Can be called from anywhere in the fragment. + * Display timeout alert dialog. + * + * @param activity Activity class. Basically the MainActivity. + * @param message Alert dialog body. + * @param positiveButtonText Positive button text. Example "Retry" + * @param positiveButtonBlock Code block when [positiveButtonText] is pressed. + * @param negativeButtonText Negative button text. Example "Retry" + * @param negativeButtonBlock Code block when [negativeButtonText] is pressed. + * @param positiveButtonText Positive button text. Example "Retry" + * @param positiveButtonBlock Code block when [positiveButtonText] is pressed. * - * Calls [onTimeout], which may return a [AlertDialog.Builder] - * instance if it deems fit. Else it may return null, in which case no timeout dialog - * is shown to the user. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404 + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 */ - fun showTimeout(exception: Exception) { - val dialogView = DialogErrorLogBinding.inflate(requireActivity().layoutInflater) - dialogView.apply { - moreInfo.setOnClickListener { - logDisplay.isVisible = true - moreInfo.isVisible = false - } - - val logToDisplay = exception.message ?: "" + fun displayTimeoutAlertDialog( + timeoutFragment: TimeoutFragment, + activity: Activity, + message: String, + positiveButtonText: String? = null, + positiveButtonBlock: (() -> Unit)? = null, + negativeButtonText: String? = null, + negativeButtonBlock: (() -> Unit)? = null, + neutralButtonText: String? = null, + neutralButtonBlock: (() -> Unit)? = null, + allowCancel: Boolean = true, + ) { - if (logToDisplay.isNotBlank()) { - logDisplay.text = logToDisplay - moreInfo.isVisible = true - } - } - val predefinedDialog = AlertDialog.Builder(requireActivity()).apply { - setTitle(R.string.timeout_title) - setMessage(R.string.timeout_desc_cleanapk) - setView(dialogView.root) - setPositiveButton(R.string.retry) { _, _ -> - showLoadingUI() - loginViewModel.startLoginFlow() - } - setNegativeButton(R.string.close, null) - setCancelable(true) + /* + * If timeout dialog is already shown, don't proceed. + */ + if (timeoutFragment.timeoutDialogShownLock) { + return } - onTimeout( - exception, - predefinedDialog, - )?.run { - stopLoadingUI() - showAndSetDialog(this) - } - } + val timeoutAlertDialogBuilder = AlertDialog.Builder(activity).apply { - /** - * Call to trigger [onSignInError]. - * Only works if last loginUiAction was a failure case. Else nothing happens. - * - * Calls [onSignInError], which may return a [AlertDialog.Builder] - * instance if it deems fit. Else it may return null, at which case no error dialog - * is shown to the user. - */ - fun showSignInError(exception: GPlayLoginException) { - - val dialogView = DialogErrorLogBinding.inflate(requireActivity().layoutInflater) - dialogView.apply { - - moreInfo.setOnClickListener { - logDisplay.isVisible = true - moreInfo.isVisible = false - } + /* + * Set title. + */ + setTitle(R.string.timeout_title) - val logToDisplay = exception.message ?: "" - if (logToDisplay.isNotBlank()) { - logDisplay.text = logToDisplay - moreInfo.isVisible = true - } - } - val predefinedDialog = AlertDialog.Builder(requireActivity()).apply { - if (exception.user == User.GOOGLE) { - setTitle(R.string.sign_in_failed_title) - setMessage(R.string.sign_in_failed_desc) + if (!allowCancel) { + /* + * Prevent dismissing the dialog from pressing outside as it will only + * show a blank screen below the dialog. + */ + setCancelable(false) + /* + * If user presses back button to close the dialog without selecting anything, + * close App Lounge. + */ + setOnKeyListener { dialog, keyCode, _ -> + if (keyCode == KeyEvent.KEYCODE_BACK) { + dialog.dismiss() + activity.finish() + } + true + } } else { - setTitle(R.string.anonymous_login_failed) - setMessage(R.string.anonymous_login_failed_desc) + setCancelable(true) } - setView(dialogView.root) - - setPositiveButton(R.string.retry) { _, _ -> - showLoadingUI() - when (exception) { - is GPlayValidationException -> clearAndRestartGPlayLogin() - else -> loginViewModel.startLoginFlow() + /* + * Set message + */ + setMessage(message) + + /* + * Set buttons. + */ + positiveButtonText?.let { + setPositiveButton(it) { _, _ -> + positiveButtonBlock?.invoke() } } - setNegativeButton(R.string.logout) { _, _ -> - loginViewModel.logout() - } - setCancelable(true) - } - - onSignInError( - exception, - predefinedDialog, - )?.run { - stopLoadingUI() - showAndSetDialog(this) - } - } - - /** - * Call when there is an error during loading data (not error during authentication.) - * - * Calls [onDataLoadError] which may return a [AlertDialog.Builder] - * instance if it deems fit. Else it may return null, at which case no error dialog - * is shown to the user. - */ - fun showDataLoadError(exception: Exception) { - - val dialogView = DialogErrorLogBinding.inflate(requireActivity().layoutInflater) - dialogView.apply { - moreInfo.setOnClickListener { - logDisplay.isVisible = true - moreInfo.isVisible = false - } - val logToDisplay = exception.message ?: "" - if (logToDisplay.isNotBlank()) { - logDisplay.text = logToDisplay - moreInfo.isVisible = true + negativeButtonText?.let { + setNegativeButton(it) { _, _ -> + negativeButtonBlock?.invoke() + } } - } - - val predefinedDialog = AlertDialog.Builder(requireActivity()).apply { - setTitle(R.string.data_load_error) - setMessage(R.string.data_load_error_desc) - setView(dialogView.root) - setPositiveButton(R.string.retry) { _, _ -> - showLoadingUI() - authObjects.value?.let { loadData(it) } + neutralButtonText?.let { + setNeutralButton(it) { _, _ -> + neutralButtonBlock?.invoke() + } } - setNegativeButton(R.string.close, null) - setCancelable(true) } - onDataLoadError( - exception, - predefinedDialog, - )?.run { - stopLoadingUI() - showAndSetDialog(this) + /* + * Dismiss alert dialog if already being shown + */ + try { + timeoutAlertDialog?.dismiss() + } catch (_: Exception) { } - } - /** - * Common code to handle exceptions / errors during data loading. - * Can be overridden in child fragments. - */ - open fun handleExceptionsCommon(exceptions: List) { - val cleanApkException = exceptions.find { it is CleanApkException }?.run { - this as CleanApkException - } - val gPlayException = exceptions.find { it is GPlayException }?.run { - this as GPlayException - } - val unknownSourceException = exceptions.find { it is UnknownSourceException } + timeoutAlertDialog = timeoutAlertDialogBuilder.create() + timeoutAlertDialog?.show() /* - * Take caution altering the cases. - * Cases to be defined from most restrictive to least restrictive. + * Mark timeout dialog is already shown. */ - when { - // Handle timeouts - cleanApkException?.isTimeout == true -> showTimeout(cleanApkException) - gPlayException?.isTimeout == true -> showTimeout(gPlayException) - - // Handle sign-in error - gPlayException is GPlayLoginException -> showSignInError(gPlayException) - - // Other errors - data loading error - gPlayException != null -> showDataLoadError(gPlayException) - cleanApkException != null -> showDataLoadError(cleanApkException) + timeoutFragment.timeoutDialogShownLock = true + } - // Unknown exception - unknownSourceException != null -> { - showAndSetDialog( - AlertDialog.Builder(requireActivity()) - .setTitle(R.string.unknown_error) - .setPositiveButton(R.string.close, null) - ) - } - } + /** + * Returns true if [timeoutAlertDialog] is displaying. + * Returs false if it is not initialised. + */ + fun isTimeoutDialogDisplayed(): Boolean { + return timeoutAlertDialog?.isShowing == true } /** - * Clear stale AuthObjects on fragment destruction. - * Useful if sources are changed in Settings and new AuthObjects are needed. + * Dismisses the [timeoutAlertDialog] if it is being displayed. + * Does nothing if it is not being displayed. + * Caller need not check if the dialog is being displayed. */ - override fun onDestroyView() { - super.onDestroyView() - authObjects.value = null + fun dismissTimeoutDialog() { + if (isTimeoutDialogDisplayed()) { + try { + timeoutAlertDialog?.dismiss() + } catch (_: Exception) { + } + } } } diff --git a/app/src/main/res/layout/custom_preference.xml b/app/src/main/res/layout/custom_preference.xml index 84793f87f27e3f52166e0d848e810e2470506f7a..0947f7933c92fb57879ff130bf1748bb04550311 100644 --- a/app/src/main/res/layout/custom_preference.xml +++ b/app/src/main/res/layout/custom_preference.xml @@ -81,9 +81,9 @@ android:layout_marginStart="18dp" android:textColor="?android:textColorPrimary" android:textSize="15sp" + app:layout_constraintBottom_toBottomOf="@id/avatar" app:layout_constraintStart_toEndOf="@id/avatar" - app:layout_constraintTop_toTopOf="@id/avatar" - app:layout_constraintVertical_chainStyle="packed"/> + app:layout_constraintTop_toTopOf="@id/avatar" /> - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index ad2e66a6f39cd9762e1f69942b0be2727e1feb4b..917cadc46808008ffe90243db9e35d5d08e6ef9c 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -25,8 +25,6 @@ #262626 - #353535 - @color/colorNavBar \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 274a54b2966af91a8065835d3e4024ef81a0fa30..d76e68362aa32df4d395ca7d8445d6e52d085571 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -38,6 +38,4 @@ #FB3846 #FFEB3B - #EDEDED - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 702744b4190457d96eaa0b014e93da17834e0ed7..dc6227637de300b681ab533aa9aba343504dc79a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -84,7 +84,6 @@ N/A Open Retry - Close More Update Ratings @@ -112,7 +111,6 @@ Your application was not found. Something went wrong! Show more - Cannot show Google Play app when only open source apps are allowed. @@ -173,17 +171,10 @@ Some network issue is preventing fetching all applications. Open Settings - More info - - - Error occurred while loading apps. - This can be because server is not responding or other server error.\n\nPlease retry to try again, or try later. - + Google sign in failed! Due to a network error, App Lounge was not able to get Google sign-in data. - Anonymous login failed! - This can be because token could not be generated / verified or other reasons.\n\nPress Retry to try again. %1$s]]>