diff --git a/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt b/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt index 2a4ee09891f02fecbfd545080095a7a45ee3fe2b..3fcf7e4291a670066c7431b8e8bd17270079b7cc 100644 --- a/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt +++ b/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt @@ -43,6 +43,8 @@ import timber.log.Timber import timber.log.Timber.Forest.plant import java.util.concurrent.Executors import javax.inject.Inject +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking @HiltAndroidApp @DelicateCoroutinesApi @@ -74,7 +76,7 @@ class AppLoungeApplication : Application(), Configuration.Provider { val pkgManagerBR = object : PkgManagerBR() {} registerReceiver(pkgManagerBR, pkgManagerModule.getFilter(), RECEIVER_EXPORTED) - val currentVersion = dataStoreModule.getTOSVersion() + val currentVersion = runBlocking { dataStoreModule.tosVersion.first() } if (!currentVersion.contentEquals(TOS_VERSION)) { MainScope().launch { dataStoreModule.saveTOCStatus(false, "") diff --git a/app/src/main/java/foundation/e/apps/data/login/CleanApkAuthenticator.kt b/app/src/main/java/foundation/e/apps/data/login/CleanApkAuthenticator.kt index d3879f586f72ad3bc0deb82588888d421acd4ef6..5a83b9a83c1628a75df9e1d7e0c19047ddcd433d 100644 --- a/app/src/main/java/foundation/e/apps/data/login/CleanApkAuthenticator.kt +++ b/app/src/main/java/foundation/e/apps/data/login/CleanApkAuthenticator.kt @@ -19,6 +19,8 @@ package foundation.e.apps.data.login import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.User +import foundation.e.apps.data.preference.DataStoreModule +import foundation.e.apps.data.preference.PreferenceManagerModule import javax.inject.Inject import javax.inject.Singleton @@ -28,11 +30,12 @@ import javax.inject.Singleton */ @Singleton class CleanApkAuthenticator @Inject constructor( - val loginData: LoginData, + private val dataStoreModule: DataStoreModule, + private val preferenceManagerModule: PreferenceManagerModule, ) : StoreAuthenticator { private val user: User - get() = loginData.getUserType() + get() = dataStoreModule.getUserType() override fun isStoreActive(): Boolean { if (user == User.UNAVAILABLE) { @@ -41,7 +44,7 @@ class CleanApkAuthenticator @Inject constructor( */ return false } - return loginData.isOpenSourceSelected() || loginData.isPWASelected() + return preferenceManagerModule.isOpenSourceSelected() || preferenceManagerModule.isPWASelected() } override suspend fun login(): AuthObject.CleanApk { diff --git a/app/src/main/java/foundation/e/apps/data/login/LoginCommon.kt b/app/src/main/java/foundation/e/apps/data/login/LoginCommon.kt index b041a29db6f07b62f8ce4d3818989ee053ed3adb..8af0004e3e73a18c63eeb2a73a18dea0874f496f 100644 --- a/app/src/main/java/foundation/e/apps/data/login/LoginCommon.kt +++ b/app/src/main/java/foundation/e/apps/data/login/LoginCommon.kt @@ -19,6 +19,8 @@ package foundation.e.apps.data.login import foundation.e.apps.data.Constants import foundation.e.apps.data.enums.User +import foundation.e.apps.data.preference.DataStoreModule +import foundation.e.apps.data.preference.PreferenceManagerModule import javax.inject.Inject import javax.inject.Singleton @@ -30,33 +32,34 @@ import javax.inject.Singleton */ @Singleton class LoginCommon @Inject constructor( - private val loginData: LoginData, + private val dataStoreModule: DataStoreModule, + private val preferenceManagerModule: PreferenceManagerModule, ) { suspend fun saveUserType(user: User) { - loginData.saveUserType(user) + dataStoreModule.saveUserType(user) } fun getUserType(): User { - return loginData.getUserType() + return dataStoreModule.getUserType() } suspend fun saveGoogleLogin(email: String, oauth: String) { - loginData.saveGoogleLogin(email, oauth) + dataStoreModule.saveGoogleLogin(email, oauth) } suspend fun setNoGoogleMode() { - loginData.setSource(Constants.PREFERENCE_SHOW_FOSS, true) - loginData.setSource(Constants.PREFERENCE_SHOW_PWA, true) - loginData.setSource(Constants.PREFERENCE_SHOW_GPLAY, false) - loginData.saveUserType(User.NO_GOOGLE) + preferenceManagerModule.setSource(Constants.PREFERENCE_SHOW_FOSS, true) + preferenceManagerModule.setSource(Constants.PREFERENCE_SHOW_PWA, true) + preferenceManagerModule.setSource(Constants.PREFERENCE_SHOW_GPLAY, false) + dataStoreModule.saveUserType(User.NO_GOOGLE) } suspend fun logout() { - loginData.destroyCredentials() - loginData.clearUserType() + dataStoreModule.destroyCredentials() + dataStoreModule.saveUserType(null) // reset app source preferences on logout. - loginData.setSource(Constants.PREFERENCE_SHOW_FOSS, true) - loginData.setSource(Constants.PREFERENCE_SHOW_PWA, true) - loginData.setSource(Constants.PREFERENCE_SHOW_GPLAY, true) + preferenceManagerModule.setSource(Constants.PREFERENCE_SHOW_FOSS, true) + preferenceManagerModule.setSource(Constants.PREFERENCE_SHOW_PWA, true) + preferenceManagerModule.setSource(Constants.PREFERENCE_SHOW_GPLAY, true) } } diff --git a/app/src/main/java/foundation/e/apps/data/login/LoginData.kt b/app/src/main/java/foundation/e/apps/data/login/LoginData.kt deleted file mode 100644 index c9da999b5d1647f58b547111adcb4d44ac0a91dd..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/data/login/LoginData.kt +++ /dev/null @@ -1,188 +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.data.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.data.Constants.PREFERENCE_SHOW_FOSS -import foundation.e.apps.data.Constants.PREFERENCE_SHOW_GPLAY -import foundation.e.apps.data.Constants.PREFERENCE_SHOW_PWA -import foundation.e.apps.data.enums.User -import foundation.e.apps.data.preference.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 LoginData @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. - */ - - private val authData = context.dataStore.data.map { it[AUTHDATA] ?: "" } - private val emailData = context.dataStore.data.map { it[EMAIL] ?: "" } - private val aasToken = context.dataStore.data.map { it[AASTOKEN] ?: "" } - private val oauthToken = context.dataStore.data.map { it[OAUTHTOKEN] ?: "" } - private 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) - - fun setSource(source: String, value: Boolean) { - val editor = preferenceManager.edit() - editor.run { - this.putBoolean(source, value) - } - editor.apply() - } - - // 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/data/login/PlayStoreAuthenticator.kt b/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt index b408ff7f9bdd847bf30c5e532de6af3302ad3e99..5f303860428a073f759910e035cfe2d99c4f0a13 100644 --- a/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt +++ b/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt @@ -28,7 +28,10 @@ import foundation.e.apps.data.login.api.PlayStoreLoginManagerFactory import foundation.e.apps.data.login.api.PlayStoreLoginManager import foundation.e.apps.data.login.api.GoogleLoginManager import foundation.e.apps.data.login.api.PlayStoreLoginWrapper +import foundation.e.apps.data.preference.DataStoreModule +import foundation.e.apps.data.preference.PreferenceManagerModule import foundation.e.apps.data.retryWithBackoff +import foundation.e.apps.data.preference.getSync import timber.log.Timber import java.util.Locale import javax.inject.Inject @@ -44,14 +47,15 @@ import javax.inject.Singleton class PlayStoreAuthenticator @Inject constructor( @ApplicationContext private val context: Context, private val gson: Gson, - private val loginData: LoginData, + private val dataStoreModule: DataStoreModule, + private val preferenceManagerModule: PreferenceManagerModule, ) : StoreAuthenticator, AuthDataValidator { @Inject lateinit var loginManagerFactory: PlayStoreLoginManagerFactory private val user: User - get() = loginData.getUserType() + get() = dataStoreModule.getUserType() private val loginManager: PlayStoreLoginManager get() = loginManagerFactory.createLoginManager(user) @@ -69,7 +73,7 @@ class PlayStoreAuthenticator @Inject constructor( */ return false } - return loginData.isGplaySelected() + return preferenceManagerModule.isGplaySelected() } /** @@ -106,7 +110,7 @@ class PlayStoreAuthenticator @Inject constructor( } override suspend fun logout() { - loginData.clearAuthData() + dataStoreModule.saveAuthData(null) } /** @@ -114,7 +118,7 @@ class PlayStoreAuthenticator @Inject constructor( * Returns null if nothing is saved. */ private fun getSavedAuthData(): AuthData? { - val authJson = loginData.getAuthData() + val authJson = dataStoreModule.authData.getSync() return if (authJson.isBlank()) null else try { gson.fromJson(authJson, AuthData::class.java) @@ -125,14 +129,14 @@ class PlayStoreAuthenticator @Inject constructor( } private suspend fun saveAuthData(authData: AuthData) { - loginData.saveAuthData(authData) + dataStoreModule.saveAuthData(authData) } /** * Generate new AuthData based on the user type. */ private suspend fun generateAuthData(): ResultSupreme { - return when (loginData.getUserType()) { + return when (dataStoreModule.getUserType()) { User.ANONYMOUS -> getAuthDataAnonymously() User.GOOGLE -> getAuthDataWithGoogleAccount() else -> ResultSupreme.Error("User type not ANONYMOUS or GOOGLE") @@ -157,9 +161,9 @@ class PlayStoreAuthenticator @Inject constructor( private suspend fun getAuthDataWithGoogleAccount(): ResultSupreme { - val email = loginData.getEmail() - val oauthToken = loginData.getOAuthToken() - val aasToken = loginData.getAASToken() + val email = dataStoreModule.emailData.getSync() + val oauthToken = dataStoreModule.oauthToken.getSync() + val aasToken = dataStoreModule.aasToken.getSync() /* * If aasToken is not blank, means it was stored successfully from a previous Google login. * Use it to fetch auth data. @@ -195,7 +199,7 @@ class PlayStoreAuthenticator @Inject constructor( /* * Finally save the aasToken and create auth data. */ - loginData.saveAasToken(aasTokenFetched) + dataStoreModule.saveAasToken(aasTokenFetched) return loginWrapper.login(locale).run { if (isSuccess()) ResultSupreme.Success(formatAuthData(this.data!!)) else this diff --git a/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt b/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt index c0d5eff3c09a2d7e2b6dcb27f0311f59b0feae23..d99fdb7f92a481a1ae67a6819fe175dc15ad4f58 100644 --- a/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt +++ b/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt @@ -23,7 +23,8 @@ import com.aurora.gplayapi.helpers.AuthHelper import foundation.e.apps.data.playstore.utils.AC2DMTask import foundation.e.apps.data.playstore.utils.CustomAuthValidator import foundation.e.apps.data.playstore.utils.GPlayHttpClient -import foundation.e.apps.data.login.LoginData +import foundation.e.apps.data.preference.DataStoreModule +import foundation.e.apps.data.preference.getSync import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.util.Properties @@ -32,7 +33,7 @@ class GoogleLoginManager( private val gPlayHttpClient: GPlayHttpClient, private val nativeDeviceProperty: Properties, private val aC2DMTask: AC2DMTask, - private val loginData: LoginData + private val dataStoreModule: DataStoreModule, ) : PlayStoreLoginManager { /** @@ -58,8 +59,8 @@ class GoogleLoginManager( * @return authData: authentication data */ override suspend fun login(): AuthData? { - val email = loginData.getEmail() - val aasToken = loginData.getAASToken() + val email = dataStoreModule.emailData.getSync() + val aasToken = dataStoreModule.aasToken.getSync() var authData: AuthData? withContext(Dispatchers.IO) { diff --git a/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManagerFactory.kt b/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManagerFactory.kt index 784532df940cbe9f4ec3daf96d71e0aef418730b..fc1c55156aaa747569a37d350591355f536e7a2e 100644 --- a/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManagerFactory.kt +++ b/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginManagerFactory.kt @@ -21,7 +21,7 @@ import com.google.gson.Gson import foundation.e.apps.data.enums.User import foundation.e.apps.data.playstore.utils.AC2DMTask import foundation.e.apps.data.playstore.utils.GPlayHttpClient -import foundation.e.apps.data.login.LoginData +import foundation.e.apps.data.preference.DataStoreModule import java.util.Properties import javax.inject.Inject import javax.inject.Singleton @@ -32,12 +32,12 @@ class PlayStoreLoginManagerFactory @Inject constructor( private val nativeDeviceProperty: Properties, private val aC2DMTask: AC2DMTask, private val gson: Gson, - private val loginData: LoginData + private val dataStoreModule: DataStoreModule, ) { fun createLoginManager(user: User): PlayStoreLoginManager { return when (user) { - User.GOOGLE -> GoogleLoginManager(gPlayHttpClient, nativeDeviceProperty, aC2DMTask, loginData) + User.GOOGLE -> GoogleLoginManager(gPlayHttpClient, nativeDeviceProperty, aC2DMTask, dataStoreModule) else -> AnonymousLoginManager(gPlayHttpClient, nativeDeviceProperty, gson) } } diff --git a/app/src/main/java/foundation/e/apps/data/preference/DataStoreManager.kt b/app/src/main/java/foundation/e/apps/data/preference/DataStoreManager.kt index 8bd36cbfe1db8bcdd163960a7b4a0891b6de5c44..240d0716cbdcd59db27d0fa321810be37cfc6b48 100644 --- a/app/src/main/java/foundation/e/apps/data/preference/DataStoreManager.kt +++ b/app/src/main/java/foundation/e/apps/data/preference/DataStoreManager.kt @@ -32,15 +32,11 @@ class DataStoreManager @Inject constructor() { lateinit var gson: Gson fun getAuthData(): AuthData { - val authDataJson = dataStoreModule.getAuthDataSync() + val authDataJson = dataStoreModule.authData.getSync() return gson.fromJson(authDataJson, AuthData::class.java) ?: AuthData("", "") } fun getUserType(): User { return dataStoreModule.getUserType() } - - fun getAuthDataJson(): String { - return dataStoreModule.getAuthDataSync() - } } diff --git a/app/src/main/java/foundation/e/apps/data/preference/DataStoreModule.kt b/app/src/main/java/foundation/e/apps/data/preference/DataStoreModule.kt index a8482aaeba868baa93c459411a360322ecd19965..7f042d00debf807c317d683e49ba23751ff2ce9c 100644 --- a/app/src/main/java/foundation/e/apps/data/preference/DataStoreModule.kt +++ b/app/src/main/java/foundation/e/apps/data/preference/DataStoreModule.kt @@ -1,145 +1,156 @@ -/* - * 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.data.preference - -import android.content.Context -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import com.aurora.gplayapi.data.models.AuthData -import com.google.gson.Gson -import dagger.hilt.android.qualifiers.ApplicationContext -import foundation.e.apps.data.enums.User -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.runBlocking -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class DataStoreModule @Inject constructor( - @ApplicationContext - private val context: Context, - private val gson: Gson -) { - - companion object { - private const val preferenceDataStoreName = "Settings" - val Context.dataStore by preferencesDataStore(preferenceDataStoreName) - } - - private val AUTHDATA = stringPreferencesKey("authData") - private val EMAIL = stringPreferencesKey("email") - private val OAUTHTOKEN = stringPreferencesKey("oauthtoken") - private val USERTYPE = stringPreferencesKey("userType") - private val TOCSTATUS = booleanPreferencesKey("tocStatus") - private val TOSVERSION = stringPreferencesKey("tosversion") - - val authData = context.dataStore.data.map { it[AUTHDATA] ?: "" } - val emailData = context.dataStore.data.map { it[EMAIL] ?: "" } - val aasToken = context.dataStore.data.map { it[OAUTHTOKEN] ?: "" } - val userType = context.dataStore.data.map { it[USERTYPE] ?: "" } - val tocStatus = context.dataStore.data.map { it[TOCSTATUS] ?: false } - val tosVersion = context.dataStore.data.map { it[TOSVERSION] ?: "" } - - /** - * Allows to save gplay API token data into datastore - */ - suspend fun saveCredentials(authData: AuthData) { - context.dataStore.edit { - it[AUTHDATA] = gson.toJson(authData) - } - } - - /** - * 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) - } - } - - suspend fun clearUserType() { - context.dataStore.edit { - it.remove(USERTYPE) - } - } - - /** - * TOC status - */ - suspend fun saveTOCStatus(status: Boolean, tosVersion: String) { - context.dataStore.edit { - it[TOCSTATUS] = status - it[TOSVERSION] = tosVersion - } - } - - fun getTOSVersion(): String { - return runBlocking { - tosVersion.first() - } - } - - /** - * User auth type - */ - suspend fun saveUserType(user: User) { - context.dataStore.edit { - it[USERTYPE] = user.name - } - } - - fun getAuthDataSync(): String { - return runBlocking { - authData.first() - } - } - - suspend fun saveEmail(email: String, token: String) { - context.dataStore.edit { - it[EMAIL] = email - it[OAUTHTOKEN] = token - } - } - - fun getEmail(): String { - return runBlocking { - emailData.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) - } - } - } -} +/* + * 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.data.preference + +import android.content.Context +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.aurora.gplayapi.data.models.AuthData +import com.google.gson.Gson +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.data.enums.User +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.flow.Flow + +/** + * 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. + */ + +@Singleton +class DataStoreModule @Inject constructor( + @ApplicationContext + private val context: Context, + private val gson: Gson +) { + + companion object { + private const val preferenceDataStoreName = "Settings" + val Context.dataStore by preferencesDataStore(preferenceDataStoreName) + } + + 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") + private val TOCSTATUS = booleanPreferencesKey("tocStatus") + private val TOSVERSION = stringPreferencesKey("tosversion") + + val authData = context.dataStore.data.map { it[AUTHDATA] ?: "" } + val emailData = context.dataStore.data.map { it[EMAIL] ?: "" } + val oauthToken = context.dataStore.data.map { it[OAUTHTOKEN] ?: "" } + val aasToken = context.dataStore.data.map { it[AASTOKEN] ?: "" } + val userType = context.dataStore.data.map { it[USERTYPE] ?: "" } + val tocStatus = context.dataStore.data.map { it[TOCSTATUS] ?: false } + val tosVersion = context.dataStore.data.map { it[TOSVERSION] ?: "" } + + /** + * Allows to save gplay API token data into datastore + */ + suspend fun saveAuthData(authData: AuthData?) { + context.dataStore.edit { + if (authData == null) it.remove(AUTHDATA) + else it[AUTHDATA] = gson.toJson(authData) + } + } + + /** + * 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 [saveUserType] with null. + */ + suspend fun destroyCredentials() { + context.dataStore.edit { + it.remove(AUTHDATA) + it.remove(EMAIL) + it.remove(OAUTHTOKEN) + it.remove(AASTOKEN) + } + } + + /** + * TOC status + */ + suspend fun saveTOCStatus(status: Boolean, tosVersion: String) { + context.dataStore.edit { + it[TOCSTATUS] = status + it[TOSVERSION] = tosVersion + } + } + + /** + * User auth type + */ + suspend fun saveUserType(user: User?) { + context.dataStore.edit { + if (user == null) it.remove(USERTYPE) + else it[USERTYPE] = user.name + } + } + + 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) + } + } + } + + suspend fun saveAasToken(aasToken: String) { + context.dataStore.edit { + it[AASTOKEN] = aasToken + } + } + + suspend fun saveGoogleLogin(email: String, token: String) { + context.dataStore.edit { + it[EMAIL] = email + it[OAUTHTOKEN] = token + } + } +} + +fun Flow.getSync(): String { + return runBlocking { + this@getSync.first() + } +} diff --git a/app/src/main/java/foundation/e/apps/data/preference/PreferenceManagerModule.kt b/app/src/main/java/foundation/e/apps/data/preference/PreferenceManagerModule.kt index 0dba23101785c5ca3d386dcd728fad1494b18466..813fa8584cb703ea1f8a0b0208479e5d0781bc2a 100644 --- a/app/src/main/java/foundation/e/apps/data/preference/PreferenceManagerModule.kt +++ b/app/src/main/java/foundation/e/apps/data/preference/PreferenceManagerModule.kt @@ -67,4 +67,12 @@ class PreferenceManagerModule @Inject constructor( context.getString(R.string.update_apps_from_other_stores), true ) + + fun setSource(source: String, value: Boolean) { + val editor = preferenceManager.edit() + editor.run { + this.putBoolean(source, value) + } + editor.apply() + } } diff --git a/app/src/main/java/foundation/e/apps/receivers/DumpAuthData.kt b/app/src/main/java/foundation/e/apps/receivers/DumpAuthData.kt index 2e23ddc7be4a6125c8a297f2f857703ba117f64f..02b8842ff9217f7c9554d22d64bd21035e5cff89 100644 --- a/app/src/main/java/foundation/e/apps/receivers/DumpAuthData.kt +++ b/app/src/main/java/foundation/e/apps/receivers/DumpAuthData.kt @@ -24,6 +24,7 @@ import com.google.gson.Gson import foundation.e.apps.data.Constants.ACTION_AUTHDATA_DUMP import foundation.e.apps.data.Constants.TAG_AUTHDATA_DUMP import foundation.e.apps.data.preference.DataStoreModule +import foundation.e.apps.data.preference.getSync import org.json.JSONObject import timber.log.Timber @@ -47,7 +48,7 @@ class DumpAuthData : BroadcastReceiver() { private fun getAuthDataDump(context: Context): String { val gson = Gson() // TODO: replace with context.configuration - val authData = DataStoreModule(context, gson).getAuthDataSync().let { + val authData = DataStoreModule(context, gson).authData.getSync().let { gson.fromJson(it, AuthData::class.java) } val filteredData = JSONObject().apply { diff --git a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt index d110ba5c7eabc5477ac88bdbd893a8f05b8e0df6..51a52c96575a90ce2291a3c0e3728bac2a2bff65 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt @@ -49,6 +49,7 @@ import foundation.e.apps.data.preference.DataStoreModule import foundation.e.apps.install.pkg.PWAManagerModule import foundation.e.apps.install.pkg.PkgManagerModule import foundation.e.apps.install.workmanager.AppInstallProcessor +import foundation.e.apps.data.preference.getSync import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow @@ -91,7 +92,7 @@ class MainActivityViewModel @Inject constructor( } fun getUserEmail(): String { - return dataStoreModule.getEmail() + return dataStoreModule.emailData.getSync() } fun uploadFaultyTokenToEcloud(email: String, description: String = "") { diff --git a/app/src/main/java/foundation/e/apps/ui/setup/signin/LocaleChangedBroadcastReceiver.kt b/app/src/main/java/foundation/e/apps/ui/setup/signin/LocaleChangedBroadcastReceiver.kt index 5be760a1b3c7aa07e34bbc7531d1866227a65487..f659e91832f9704a493ee486bcef4857eb292f56 100644 --- a/app/src/main/java/foundation/e/apps/ui/setup/signin/LocaleChangedBroadcastReceiver.kt +++ b/app/src/main/java/foundation/e/apps/ui/setup/signin/LocaleChangedBroadcastReceiver.kt @@ -25,6 +25,7 @@ import com.aurora.gplayapi.data.models.AuthData import com.google.gson.Gson import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.data.preference.DataStoreModule +import foundation.e.apps.data.preference.getSync import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -52,10 +53,10 @@ class LocaleChangedBroadcastReceiver : BroadcastReceiver() { } GlobalScope.launch { try { - val authDataJson = dataStoreModule.getAuthDataSync() + val authDataJson = dataStoreModule.authData.getSync() val authData = gson.fromJson(authDataJson, AuthData::class.java) authData.locale = context.resources.configuration.locales[0] - dataStoreModule.saveCredentials(authData) + dataStoreModule.saveAuthData(authData) withContext(Dispatchers.IO) { cache.evictAll() } diff --git a/app/src/main/java/foundation/e/apps/ui/setup/signin/SignInViewModel.kt b/app/src/main/java/foundation/e/apps/ui/setup/signin/SignInViewModel.kt index 61d7c57570176afe674c5337aeccec20f14e78e4..742cf51d9c723b77700af4c358ef05f6fbc6d0cd 100644 --- a/app/src/main/java/foundation/e/apps/ui/setup/signin/SignInViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/setup/signin/SignInViewModel.kt @@ -21,18 +21,4 @@ class SignInViewModel @Inject constructor( private val _authLiveData: MutableLiveData = MutableLiveData() val authLiveData: LiveData = _authLiveData - fun saveUserType(user: User) { - viewModelScope.launch { - dataStoreModule.saveUserType(user) - if (user == User.UNAVAILABLE) { - dataStoreModule.destroyCredentials() - } - } - } - - fun saveEmailToken(email: String, token: String) { - viewModelScope.launch { - dataStoreModule.saveEmail(email, token) - } - } }