diff --git a/app/src/main/java/com/aurora/Constants.kt b/app/src/main/java/com/aurora/Constants.kt index a368a23bd..ed31cf23f 100644 --- a/app/src/main/java/com/aurora/Constants.kt +++ b/app/src/main/java/com/aurora/Constants.kt @@ -47,10 +47,10 @@ object Constants { //ACCOUNTS const val ACCOUNT_SIGNED_IN = "ACCOUNT_SIGNED_IN" - const val ACCOUNT_SIGNED_TIMESTAMP = "ACCOUNT_SIGNED_TIMESTAMP" const val ACCOUNT_TYPE = "ACCOUNT_TYPE" const val ACCOUNT_EMAIL_PLAIN = "ACCOUNT_EMAIL_PLAIN" const val ACCOUNT_AAS_PLAIN = "ACCOUNT_AAS_PLAIN" + const val ACCOUNT_AUTH_PLAIN = "ACCOUNT_AUTH_PLAIN" const val PAGE_TYPE = "PAGE_TYPE" const val TOP_CHART_TYPE = "TOP_CHART_TYPE" diff --git a/app/src/main/java/com/aurora/store/data/providers/AccountProvider.kt b/app/src/main/java/com/aurora/store/data/providers/AccountProvider.kt index 8012606b6..8044a79e7 100644 --- a/app/src/main/java/com/aurora/store/data/providers/AccountProvider.kt +++ b/app/src/main/java/com/aurora/store/data/providers/AccountProvider.kt @@ -21,6 +21,7 @@ package com.aurora.store.data.providers import android.content.Context import com.aurora.Constants +import com.aurora.gplayapi.helpers.AuthHelper import com.aurora.store.data.model.AccountType import com.aurora.store.util.Preferences @@ -34,9 +35,49 @@ object AccountProvider { } } - fun logout(context: Context) { - Preferences.putBoolean(context, Constants.ACCOUNT_SIGNED_IN, false) - Preferences.putString(context, Constants.ACCOUNT_EMAIL_PLAIN, "") - Preferences.putString(context, Constants.ACCOUNT_AAS_PLAIN, "") + fun isLoggedIn(context: Context): Boolean { + return Preferences.getBoolean(context, Constants.ACCOUNT_SIGNED_IN, false) } -} \ No newline at end of file + + fun getLoginEmail(context: Context): String? { + val email = Preferences.getString(context, Constants.ACCOUNT_EMAIL_PLAIN) + return email.ifBlank { null } + } + + fun getLoginToken(context: Context): Pair? { + val email = Preferences.getString(context, Constants.ACCOUNT_EMAIL_PLAIN) + val aasToken = Preferences.getString(context, Constants.ACCOUNT_AAS_PLAIN) + val authToken = Preferences.getString(context, Constants.ACCOUNT_AUTH_PLAIN) + + if (email.isBlank() && (aasToken.isBlank() || authToken.isBlank())) return null + + val tokenType = if (aasToken.isBlank()) AuthHelper.Token.AUTH else AuthHelper.Token.AAS + return Pair(aasToken.ifBlank { authToken }, tokenType) + } + + fun login( + context: Context, + email: String, + token: String, + tokenType: AuthHelper.Token, + accountType: AccountType + ) { + Preferences.putBoolean(context, Constants.ACCOUNT_SIGNED_IN, true) + Preferences.putString(context, Constants.ACCOUNT_EMAIL_PLAIN, email) + Preferences.putString(context, Constants.ACCOUNT_TYPE, accountType.name) + + if (tokenType == AuthHelper.Token.AAS) { + Preferences.putString(context, Constants.ACCOUNT_AAS_PLAIN, token) + } else { + Preferences.putString(context, Constants.ACCOUNT_AUTH_PLAIN, token) + } + } + + fun logout(context: Context) { + Preferences.remove(context, Constants.ACCOUNT_SIGNED_IN) + Preferences.remove(context, Constants.ACCOUNT_TYPE) + Preferences.remove(context, Constants.ACCOUNT_EMAIL_PLAIN) + Preferences.remove(context, Constants.ACCOUNT_AAS_PLAIN) + Preferences.remove(context, Constants.ACCOUNT_AUTH_PLAIN) + } +} diff --git a/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt b/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt index 167a8218e..f6e7d3df4 100644 --- a/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt +++ b/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt @@ -21,9 +21,8 @@ package com.aurora.store.data.providers import android.content.Context import android.util.Log -import com.aurora.Constants -import com.aurora.Constants.ACCOUNT_SIGNED_IN import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.helpers.AuthHelper import com.aurora.gplayapi.helpers.AuthValidator import com.aurora.gplayapi.network.IHttpClient @@ -53,7 +52,27 @@ class AuthProvider @Inject constructor( private val TAG = AuthProvider::class.java.simpleName - val properties: Properties + val dispenserURL: String? + get() { + val dispensers = Preferences.getStringSet(context, PREFERENCE_DISPENSER_URLS) + return if (dispensers.isNotEmpty()) dispensers.random() else null + } + + val authData: AuthData? + get() { + Log.i(TAG, "Loading saved AuthData") + val rawAuth: String = Preferences.getString(context, PREFERENCE_AUTH_DATA) + return if (rawAuth.isNotBlank()) { + gson.fromJson(rawAuth, AuthData::class.java) + } else { + null + } + } + + val isAnonymous: Boolean + get() = AccountProvider.getAccountType(context) == AccountType.ANONYMOUS + + private val properties: Properties get() { val currentProperties = if (spoofProvider.isDeviceSpoofEnabled()) { spoofProvider.getSpoofDeviceProperties() @@ -63,41 +82,14 @@ class AuthProvider @Inject constructor( setVendingVersion(currentProperties) return currentProperties } - val locale: Locale + + private val locale: Locale get() = if (spoofProvider.isLocaleSpoofEnabled()) { spoofProvider.getSpoofLocale() } else { Locale.getDefault() } - val dispenserURL: String? - get() { - val dispensers = Preferences.getStringSet(context, PREFERENCE_DISPENSER_URLS) - return if (dispensers.isNotEmpty()) dispensers.random() else null - } - - val authData: AuthData? get() = getSavedAuthData() - - val isAnonymous: Boolean - get() { - val name = Preferences.getString(context, Constants.ACCOUNT_TYPE, AccountType.GOOGLE.name) - return AccountType.valueOf(name) == AccountType.ANONYMOUS - } - - private val signedIn: Boolean - get() = Preferences.getBoolean(context, ACCOUNT_SIGNED_IN) - - /** - * Builds and returns AuthData based on signed-in account type - */ - suspend fun getTmpAuthData(): AuthData? { - return when { - !signedIn -> null - !isAnonymous -> buildGoogleAuthData() - else -> buildAnonymousAuthData() - } - } - /** * Checks whether saved AuthData is valid or not */ @@ -113,56 +105,79 @@ class AuthProvider @Inject constructor( } } - private fun getSavedAuthData(): AuthData? { - Log.i(TAG, "Loading saved AuthData") - val rawAuth: String = Preferences.getString(context, PREFERENCE_AUTH_DATA) - return if (rawAuth.isNotEmpty()) { - gson.fromJson(rawAuth, AuthData::class.java) - } else { - null - } - } - - private suspend fun buildGoogleAuthData(): AuthData? { + /** + * Builds [AuthData] for login using personal Google account + * @param email E-mail ID + * @param token AAS or Auth token + * @param tokenType Type of the token, one from [AuthHelper.Token] + * @return Result encapsulating [AuthData] or exception + */ + suspend fun buildGoogleAuthData( + email: String, + token: String, + tokenType: AuthHelper.Token + ): Result { return withContext(Dispatchers.IO) { try { - val email = Preferences.getString(context, Constants.ACCOUNT_EMAIL_PLAIN) - val aasToken = Preferences.getString(context, Constants.ACCOUNT_AAS_PLAIN) - - return@withContext AuthHelper.build( - email = email, - token = aasToken, - tokenType = AuthHelper.Token.AAS, - properties = properties, - locale = locale + return@withContext Result.success( + AuthHelper.build( + email = email, + token = token, + tokenType = tokenType, + properties = properties, + locale = locale, + ) ) } catch (exception: Exception) { Log.e(TAG, "Failed to generate Session", exception) - return@withContext null + return@withContext Result.failure(exception) } } } - private suspend fun buildAnonymousAuthData(): AuthData? { + /** + * Builds [AuthData] for login using one of the dispensers + * @return Result encapsulating [AuthData] or exception + */ + suspend fun buildAnonymousAuthData(): Result { return withContext(Dispatchers.IO) { try { - val playResponse = httpClient.getAuth(dispenserURL!!) + val playResponse = httpClient.getAuth(dispenserURL!!).also { + if (!it.isSuccessful) throwError(it, context) + } + val auth = gson.fromJson(String(playResponse.responseBytes), Auth::class.java) - return@withContext AuthHelper.build( - email = auth.email, - token = auth.auth, - tokenType = AuthHelper.Token.AUTH, - isAnonymous = true, - properties = properties, - locale = locale + return@withContext Result.success( + AuthHelper.build( + email = auth.email, + token = auth.auth, + tokenType = AuthHelper.Token.AUTH, + isAnonymous = true, + properties = properties, + locale = locale + ) ) } catch (exception: Exception) { Log.e(TAG, "Failed to generate AuthData", exception) - return@withContext null + return@withContext Result.failure(exception) } } } + /** + * Saves given [AuthData] + */ + fun saveAuthData(authData: AuthData) { + Preferences.putString(context, PREFERENCE_AUTH_DATA, gson.toJson(authData)) + } + + /** + * Removes saved [AuthData] + */ + fun removeAuthData(context: Context) { + Preferences.remove(context, PREFERENCE_AUTH_DATA) + } + private fun setVendingVersion(currentProperties: Properties) { val vendingVersionIndex = Preferences.getInteger(context, PREFERENCE_VENDING_VERSION) if (vendingVersionIndex > 0) { @@ -174,4 +189,27 @@ class AuthProvider @Inject constructor( currentProperties.setProperty("Vending.versionString", versionStrings[vendingVersionIndex]) } } + + @Throws(Exception::class) + private fun throwError(playResponse: PlayResponse, context: Context) { + when (playResponse.code) { + 400 -> throw Exception(context.getString(R.string.bad_request)) + 403 -> throw Exception(context.getString(R.string.access_denied_using_vpn)) + 404 -> throw Exception(context.getString(R.string.server_unreachable)) + 429 -> throw Exception(context.getString(R.string.login_rate_limited)) + 503 -> throw Exception(context.getString(R.string.server_maintenance)) + else -> { + if (playResponse.errorString.isNotBlank()) { + throw Exception(playResponse.errorString) + } else { + throw Exception( + context.getString( + R.string.failed_generating_session, + playResponse.code + ) + ) + } + } + } + } } diff --git a/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt b/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt index a19c9e86c..4343bd991 100644 --- a/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt +++ b/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt @@ -121,19 +121,13 @@ class UpdateWorker @AssistedInject constructor( } withContext(Dispatchers.IO) { - val authData = if (authProvider.isSavedAuthDataValid()) { - authProvider.authData - } else { - authProvider.getTmpAuthData() - } - - if (authData == null) { + if (!authProvider.isSavedAuthDataValid()) { Log.i(TAG, "AuthData is not valid, retrying later!") return@withContext Result.retry() } try { - val updatesList = appUtil.checkUpdates(authData) + val updatesList = appUtil.checkUpdates() .filter { it.hasValidCert } .filterNot { it.isSelfUpdate() } diff --git a/app/src/main/java/com/aurora/store/util/AppUtil.kt b/app/src/main/java/com/aurora/store/util/AppUtil.kt index 202091e21..52f86a460 100644 --- a/app/src/main/java/com/aurora/store/util/AppUtil.kt +++ b/app/src/main/java/com/aurora/store/util/AppUtil.kt @@ -6,7 +6,6 @@ import android.util.Log import androidx.core.content.pm.PackageInfoCompat import com.aurora.Constants import com.aurora.gplayapi.data.models.App -import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.network.IHttpClient import com.aurora.store.AuroraApp @@ -58,10 +57,10 @@ class AppUtil @Inject constructor( } } - suspend fun checkUpdates(tmpAuthData: AuthData? = null): List { + suspend fun checkUpdates(): List { Log.i(TAG, "Checking for updates") val packageInfoMap = PackageUtil.getPackageInfoMap(context) - val appUpdatesList = getFilteredInstalledApps(tmpAuthData, packageInfoMap) + val appUpdatesList = getFilteredInstalledApps(packageInfoMap) .filter { val packageInfo = packageInfoMap[it.packageName] if (packageInfo != null) { @@ -86,11 +85,10 @@ class AppUtil @Inject constructor( } suspend fun getFilteredInstalledApps( - tmpAuthData: AuthData? = null, packageInfoMap: MutableMap? = null ): List { return withContext(Dispatchers.IO) { - val appDetailsHelper = AppDetailsHelper(tmpAuthData?: authProvider.authData!!) + val appDetailsHelper = AppDetailsHelper(authProvider.authData!!) .using(httpClient) (packageInfoMap ?: PackageUtil.getPackageInfoMap(context)).keys.let { packages -> diff --git a/app/src/main/java/com/aurora/store/view/ui/account/GoogleFragment.kt b/app/src/main/java/com/aurora/store/view/ui/account/GoogleFragment.kt index bf2d7289c..c0a5e7603 100644 --- a/app/src/main/java/com/aurora/store/view/ui/account/GoogleFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/account/GoogleFragment.kt @@ -32,6 +32,7 @@ import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import com.aurora.gplayapi.helpers.AuthHelper import com.aurora.store.AuroraApp import com.aurora.store.R import com.aurora.store.data.event.AuthEvent @@ -117,7 +118,7 @@ class GoogleFragment : BaseFragment() { when (event) { is AuthEvent.GoogleLogin -> { if (event.success) { - viewModel.buildGoogleAuthData(event.email, event.token) + viewModel.buildGoogleAuthData(event.email, event.token, AuthHelper.Token.AAS) } else { Toast.makeText( requireContext(), diff --git a/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt index 97a3ce640..1a1c49abc 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt @@ -27,26 +27,20 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.Constants import com.aurora.gplayapi.data.models.AuthData -import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.helpers.AuthHelper -import com.aurora.gplayapi.network.IHttpClient import com.aurora.store.AuroraApp import com.aurora.store.R import com.aurora.store.data.event.AuthEvent import com.aurora.store.data.model.AccountType -import com.aurora.store.data.model.Auth import com.aurora.store.data.model.AuthState import com.aurora.store.data.providers.AccountProvider import com.aurora.store.data.providers.AuthProvider import com.aurora.store.util.AC2DMTask import com.aurora.store.util.Preferences -import com.aurora.store.util.Preferences.PREFERENCE_AUTH_DATA -import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.supervisorScope import java.net.ConnectException import java.net.UnknownHostException import javax.inject.Inject @@ -56,8 +50,6 @@ import javax.inject.Inject class AuthViewModel @Inject constructor( val authProvider: AuthProvider, @ApplicationContext private val context: Context, - private val gson: Gson, - private val httpClient: IHttpClient, private val aC2DMTask: AC2DMTask ) : ViewModel() { @@ -67,8 +59,7 @@ class AuthViewModel @Inject constructor( fun observe() { if (liveData.value != AuthState.Fetching) { - val signedIn = Preferences.getBoolean(context, Constants.ACCOUNT_SIGNED_IN) - if (signedIn) { + if (AccountProvider.isLoggedIn(context)) { liveData.postValue(AuthState.Available) buildSavedAuthData() } else { @@ -77,18 +68,14 @@ class AuthViewModel @Inject constructor( } } - fun buildGoogleAuthData(email: String, aasToken: String) { + fun buildGoogleAuthData(email: String, token: String, tokenType: AuthHelper.Token) { liveData.postValue(AuthState.Fetching) viewModelScope.launch(Dispatchers.IO) { try { - val authData = AuthHelper.build( - email = email, - token = aasToken, - tokenType = AuthHelper.Token.AAS, - properties = authProvider.properties, - locale = authProvider.locale + verifyAndSaveAuth( + authProvider.buildGoogleAuthData(email, token, tokenType).getOrThrow(), + AccountType.GOOGLE ) - verifyAndSaveAuth(authData, AccountType.GOOGLE) } catch (exception: Exception) { liveData.postValue( AuthState.Failed(context.getString(R.string.failed_to_generate_session)) @@ -102,28 +89,13 @@ class AuthViewModel @Inject constructor( liveData.postValue(AuthState.Fetching) viewModelScope.launch(Dispatchers.IO) { try { - val playResponse = httpClient.getAuth(authProvider.dispenserURL!!) - if (playResponse.isSuccessful) { - val tmpAuthData = gson.fromJson( - String(playResponse.responseBytes), - Auth::class.java - ) - - val authData = AuthHelper.build( - email = tmpAuthData.email, - token = tmpAuthData.auth, - tokenType = AuthHelper.Token.AUTH, - isAnonymous = true, - properties = authProvider.properties, - locale = authProvider.locale - ) - verifyAndSaveAuth(authData, AccountType.ANONYMOUS) - } else { - throwError(playResponse, context) - } + verifyAndSaveAuth( + authProvider.buildAnonymousAuthData().getOrThrow(), + AccountType.ANONYMOUS + ) } catch (exception: Exception) { - liveData.postValue(AuthState.Failed(exception.message.toString())) Log.e(TAG, "Failed to generate Session", exception) + liveData.postValue(AuthState.Failed(exception.message.toString())) } } } @@ -155,98 +127,54 @@ class AuthViewModel @Inject constructor( private fun buildSavedAuthData() { viewModelScope.launch(Dispatchers.IO) { - supervisorScope { - try { - if (authProvider.isSavedAuthDataValid()) { - liveData.postValue(AuthState.Valid) - } else { - //Generate and validate new auth - when (AccountProvider.getAccountType(context)) { - AccountType.GOOGLE -> { - val email = Preferences.getString( - context, - Constants.ACCOUNT_EMAIL_PLAIN - ) - val aasToken = Preferences.getString( - context, - Constants.ACCOUNT_AAS_PLAIN - ) - buildGoogleAuthData(email, aasToken) - } + try { + if (authProvider.isSavedAuthDataValid()) { + liveData.postValue(AuthState.Valid) + } else { + // Generate and validate new auth + when (AccountProvider.getAccountType(context)) { + AccountType.GOOGLE -> { + buildGoogleAuthData( + AccountProvider.getLoginEmail(context)!!, + AccountProvider.getLoginToken(context)!!.first, + AccountProvider.getLoginToken(context)!!.second + ) + } - AccountType.ANONYMOUS -> { - buildAnonymousAuthData() - } + AccountType.ANONYMOUS -> { + buildAnonymousAuthData() } } - } catch (e: Exception) { - val error = when (e) { - is UnknownHostException -> { - context.getString(R.string.title_no_network) - } - - is ConnectException -> { - context.getString(R.string.server_unreachable) - } - - else -> { - context.getString(R.string.bad_request) - } - } - liveData.postValue(AuthState.Failed(error)) } + } catch (exception: Exception) { + val error = when (exception) { + is UnknownHostException -> context.getString(R.string.title_no_network) + is ConnectException -> context.getString(R.string.server_unreachable) + else -> context.getString(R.string.bad_request) + } + liveData.postValue(AuthState.Failed(error)) } } } - private fun verifyAndSaveAuth(authData: AuthData, type: AccountType) { + private fun verifyAndSaveAuth(authData: AuthData, accountType: AccountType) { liveData.postValue(AuthState.Verifying) if (authData.authToken.isNotEmpty() && authData.deviceConfigToken.isNotEmpty()) { - configAuthPref(authData, type, true) + authProvider.saveAuthData(authData) + AccountProvider.login( + context, + authData.email, + authData.aasToken.ifBlank { authData.authToken }, + if (authData.aasToken.isBlank()) AuthHelper.Token.AUTH else AuthHelper.Token.AAS, + accountType + ) liveData.postValue(AuthState.SignedIn) } else { - configAuthPref(authData, type, false) - liveData.postValue(AuthState.SignedOut) - + authProvider.removeAuthData(context) + AccountProvider.logout(context) liveData.postValue( AuthState.Failed(context.getString(R.string.failed_to_generate_session)) ) } } - - private fun configAuthPref(authData: AuthData, type: AccountType, signedIn: Boolean) { - if (signedIn) { - //Save Auth Data - Preferences.putString(context, PREFERENCE_AUTH_DATA, gson.toJson(authData)) - } - - //Save Auth Type - Preferences.putString(context, Constants.ACCOUNT_TYPE, type.name) - - //Save Auth Status - Preferences.putBoolean(context, Constants.ACCOUNT_SIGNED_IN, signedIn) - } - - @Throws(Exception::class) - private fun throwError(playResponse: PlayResponse, context: Context) { - when (playResponse.code) { - 400 -> throw Exception(context.getString(R.string.bad_request)) - 403 -> throw Exception(context.getString(R.string.access_denied_using_vpn)) - 404 -> throw Exception(context.getString(R.string.server_unreachable)) - 429 -> throw Exception(context.getString(R.string.login_rate_limited)) - 503 -> throw Exception(context.getString(R.string.server_maintenance)) - else -> { - if (playResponse.errorString.isNotBlank()) { - throw Exception(playResponse.errorString) - } else { - throw Exception( - context.getString( - R.string.failed_generating_session, - playResponse.code - ) - ) - } - } - } - } }