diff --git a/app/build.gradle b/app/build.gradle index 1a50799bb1642441c521d1f59caea9ddf2844248..15bfb95cc3aaef39fe95238201eae0cbea7ea002 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,7 +3,7 @@ plugins { id 'com.android.application' id 'kotlin-android' id 'kotlin-kapt' - id 'org.jlleitschuh.gradle.ktlint' version '10.2.0' + id 'org.jlleitschuh.gradle.ktlint' version '11.5.1' id 'androidx.navigation.safeargs.kotlin' id 'com.google.dagger.hilt.android' id 'kotlin-allopen' @@ -135,6 +135,13 @@ android { kotlin.sourceSets.all { languageSettings.optIn("kotlin.RequiresOptIn") } + + testOptions { + unitTests { + includeAndroidResources = true + returnDefaultValues = true + } + } } kapt { @@ -149,6 +156,8 @@ allOpen { dependencies { + implementation project(':modules') + // TODO: Add splitinstall-lib to a repo https://gitlab.e.foundation/e/os/backlog/-/issues/628 api files('libs/splitinstall-lib.jar') @@ -252,4 +261,9 @@ dependencies { // elib implementation 'foundation.e:elib:0.0.1-alpha11' + + testImplementation 'org.mockito:mockito-core:5.0.0' + testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0' + testImplementation 'org.robolectric:robolectric:4.9' + testImplementation 'org.json:json:20180813' // Added to avoid SystemInfoProvider.getAppBuildInfo() mock error } diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index ae7d1a5e6a6805b2de69683a88871c777ed488b0..a31f46ced1d3b913e44f7dec9e301a2cf86ddeae 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -33,32 +33,29 @@ 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.data.ResultSupreme import foundation.e.apps.data.fusedDownload.models.FusedDownload -import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.LoginSourceGPlay -import foundation.e.apps.data.login.LoginViewModel -import foundation.e.apps.data.login.exceptions.GPlayValidationException import foundation.e.apps.data.preference.PreferenceManagerModule import foundation.e.apps.databinding.ActivityMainBinding +import foundation.e.apps.domain.errors.RetryMechanism import foundation.e.apps.install.updates.UpdatesNotifier +import foundation.e.apps.presentation.login.LoginViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment +import foundation.e.apps.ui.errors.CentralErrorHandler import foundation.e.apps.ui.purchase.AppPurchaseFragmentDirections import foundation.e.apps.ui.settings.SettingsFragment import foundation.e.apps.ui.setup.signin.SignInViewModel -import foundation.e.apps.utils.SystemInfoProvider import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint @@ -69,9 +66,16 @@ class MainActivity : AppCompatActivity() { private val TAG = MainActivity::class.java.simpleName private lateinit var viewModel: MainActivityViewModel + private val retryMechanism by lazy { RetryMechanism() } + private val errorHandler by lazy { CentralErrorHandler() } + @Inject lateinit var preferenceManagerModule: PreferenceManagerModule + companion object { + private const val STATUS_TOO_MANY_REQUESTS = "Status: 429" + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -101,11 +105,11 @@ class MainActivity : AppCompatActivity() { if (it != true) { navController.navigate(R.id.TOSFragment, null, navOptions) } else { - loginViewModel.startLoginFlow() + loginViewModel.checkLogin() } } - viewModel.setupConnectivityManager(this) + viewModel.setupConnectivityManager(applicationContext) viewModel.internetConnection.observe(this) { isInternetAvailable -> hasInternet = isInternetAvailable @@ -115,30 +119,16 @@ class MainActivity : AppCompatActivity() { } } - loginViewModel.authObjects.distinctUntilChanged().observe(this) { + loginViewModel.loginState.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) + it.isLoading -> { + // TODO ? } - - else -> {} - } - - it.find { it is AuthObject.GPlayAuth }?.result?.run { - if (isSuccess()) { - viewModel.gPlayAuthData = data as AuthData - } else if (exception is GPlayValidationException) { - val email = otherPayload.toString() - viewModel.uploadFaultyTokenToEcloud( - email, - SystemInfoProvider.getAppBuildInfo() - ) - } else if (exception != null) { - Timber.e(exception, "Login failed! message: ${exception?.localizedMessage}") + it.error.isNotBlank() -> { + it.authData ?: run { showLoginScreen(navController) } + } + !it.isLoggedIn -> { + showLoginScreen(navController) } } } @@ -235,6 +225,10 @@ class MainActivity : AppCompatActivity() { launch { observeNoInternetEvent() } + + launch { + observeDataLoadError() + } } } } @@ -290,7 +284,40 @@ class MainActivity : AppCompatActivity() { }.distinctUntilChanged { old, new -> ((old.data is String) && (new.data is String) && old.data == new.data) }.collectLatest { - validatedAuthObject(it) + val currentUser = loginViewModel.currentUser() + retryMechanism.wrapWithRetry( + { loginViewModel.getNewToken() }, + { + errorHandler.getDialogForUnauthorized( + context = this@MainActivity, + logToDisplay = it.data.toString(), + user = currentUser, + retryAction = { loginViewModel.getNewToken() }, + logoutAction = { loginViewModel.logout() } + ).run { errorHandler.dismissAllAndShow(this) } + } + ) + } + } + + private suspend fun observeDataLoadError() { + EventBus.events.filter { appEvent -> + appEvent is AppEvent.DataLoadError<*> + }.collectLatest { + if (it.data is ResultSupreme<*> && it.data.message.contains(STATUS_TOO_MANY_REQUESTS)) { + return@collectLatest + } + + retryMechanism.wrapWithRetry( + { loginViewModel.checkLogin() }, + { + errorHandler.getDialogForDataLoadError( + context = this@MainActivity, + result = it.data as ResultSupreme<*>, + retryAction = { loginViewModel.checkLogin() } + )?.run { errorHandler.dismissAllAndShow(this) } + } + ) } } @@ -308,7 +335,7 @@ class MainActivity : AppCompatActivity() { binding.sessionErrorLayout.visibility = View.VISIBLE binding.retrySessionButton.setOnClickListener { binding.sessionErrorLayout.visibility = View.GONE - loginViewModel.startLoginFlow(listOf(LoginSourceGPlay::class.java.simpleName)) + loginViewModel.getNewToken() } } } @@ -367,4 +394,9 @@ class MainActivity : AppCompatActivity() { binding.noInternet.visibility = View.VISIBLE binding.fragment.visibility = View.GONE } + + private fun showLoginScreen(navController: NavController) { + navController.popBackStack() + navController.navigate(R.id.signInFragment) + } } diff --git a/app/src/main/java/foundation/e/apps/data/Constants.kt b/app/src/main/java/foundation/e/apps/data/Constants.kt index 20e9c0bc81421192e74dc7d8959f6c7efc554739..f9d73d75d6be695c945592d61331535d0d280a20 100644 --- a/app/src/main/java/foundation/e/apps/data/Constants.kt +++ b/app/src/main/java/foundation/e/apps/data/Constants.kt @@ -9,4 +9,7 @@ object Constants { const val ACTION_AUTHDATA_DUMP = "foundation.e.apps.action.DUMP_GACCOUNT_INFO" const val TAG_AUTHDATA_DUMP = "AUTHDATA_DUMP" + + const val GOOGLE_LOGIN_FAIL = "Google login failed" + const val UNEXPECTED_ERROR = "An unexpected error occurred" } diff --git a/app/src/main/java/foundation/e/apps/data/DownloadManager.kt b/app/src/main/java/foundation/e/apps/data/DownloadManager.kt index 56ba57137d23856dd0a36f81b9d203019824db3c..1bdfa2aa066eda774fc042753510202990f3b71f 100644 --- a/app/src/main/java/foundation/e/apps/data/DownloadManager.kt +++ b/app/src/main/java/foundation/e/apps/data/DownloadManager.kt @@ -46,7 +46,7 @@ class DownloadManager @Inject constructor( @ApplicationContext private val context: Context, private val downloadManager: DownloadManager, @Named("cacheDir") private val cacheDir: String, - private val downloadManagerQuery: DownloadManager.Query, + private val downloadManagerQuery: DownloadManager.Query ) { private val downloadsMaps = HashMap() @@ -75,7 +75,6 @@ class DownloadManager @Inject constructor( fileName: String, downloadCompleted: ((Boolean, String) -> Unit)? ): Long { - val directoryFile = File("$EXTERNAL_STORAGE_TEMP_CACHE_DIR/$subDirectoryPath") if (!directoryFile.exists()) { directoryFile.mkdirs() diff --git a/app/src/main/java/foundation/e/apps/data/ResultSupreme.kt b/app/src/main/java/foundation/e/apps/data/ResultSupreme.kt index a7a773f60ae33131d12c9ec6f68cae8adda48356..d31a29b693c106c7a9bcd69fb5e2191241b89064 100644 --- a/app/src/main/java/foundation/e/apps/data/ResultSupreme.kt +++ b/app/src/main/java/foundation/e/apps/data/ResultSupreme.kt @@ -143,7 +143,7 @@ sealed class ResultSupreme { status: ResultStatus, data: T? = null, message: String = "", - exception: Exception? = null, + exception: Exception? = null ): ResultSupreme { val resultObject = when { status == ResultStatus.OK && data != null -> Success(data) @@ -176,7 +176,7 @@ sealed class ResultSupreme { result: ResultSupreme<*>, newData: T?, message: String? = null, - exception: Exception? = null, + exception: Exception? = null ): ResultSupreme { val status = when (result) { is Success -> ResultStatus.OK @@ -184,7 +184,9 @@ sealed class ResultSupreme { is Error -> ResultStatus.UNKNOWN } return create( - status, newData, message ?: result.message, + status, + newData, + message ?: result.message, exception ?: result.exception ) } diff --git a/app/src/main/java/foundation/e/apps/data/blockedApps/BlockedAppRepository.kt b/app/src/main/java/foundation/e/apps/data/blockedApps/BlockedAppRepository.kt index 5962030bd06741123abd52c1737c0be9fc6dfcb4..26117b5585e5b2b8db87831570a046aa8da0dda2 100644 --- a/app/src/main/java/foundation/e/apps/data/blockedApps/BlockedAppRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/blockedApps/BlockedAppRepository.kt @@ -32,7 +32,7 @@ import kotlin.coroutines.resume class BlockedAppRepository @Inject constructor( private val downloadManager: DownloadManager, private val gson: Gson, - @Named("cacheDir") private val cacheDir: String, + @Named("cacheDir") private val cacheDir: String ) { companion object { diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt index d6660cd60a019a0d05f5465d526a316fed4be97a..540224991f6782a994b275606f44c3603bc1c30e 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkRetrofit.kt @@ -47,7 +47,7 @@ interface CleanApkRetrofit { @GET("apps?action=list_home") suspend fun getHomeScreenData( @Query("type") type: String = APP_TYPE_ANY, - @Query("source") source: String = APP_SOURCE_ANY, + @Query("source") source: String = APP_SOURCE_ANY ): Response // TODO: Reminder that this function is for search App and PWA both @@ -65,7 +65,7 @@ interface CleanApkRetrofit { @Query("type") type: String = APP_TYPE_ANY, @Query("nres") nres: Int = 20, @Query("page") page: Int = 1, - @Query("by") by: String? = null, + @Query("by") by: String? = null ): Response @GET("apps?action=list_apps") @@ -74,7 +74,7 @@ interface CleanApkRetrofit { @Query("source") source: String = APP_SOURCE_FOSS, @Query("type") type: String = APP_TYPE_ANY, @Query("nres") nres: Int = 20, - @Query("page") page: Int = 1, + @Query("page") page: Int = 1 ): Response @GET("apps?action=download") @@ -87,6 +87,6 @@ interface CleanApkRetrofit { @GET("apps?action=list_cat") suspend fun getCategoriesList( @Query("type") type: String = APP_TYPE_ANY, - @Query("source") source: String = APP_SOURCE_ANY, + @Query("source") source: String = APP_SOURCE_ANY ): Response } diff --git a/app/src/main/java/foundation/e/apps/data/ecloud/EcloudApiInterface.kt b/app/src/main/java/foundation/e/apps/data/ecloud/EcloudApiInterface.kt index e22a3d1217a1fbd4dd4205c6ebb456a1e595f7d2..ae73438eb8da41900e033b4a99eec2ba3e293cdf 100644 --- a/app/src/main/java/foundation/e/apps/data/ecloud/EcloudApiInterface.kt +++ b/app/src/main/java/foundation/e/apps/data/ecloud/EcloudApiInterface.kt @@ -31,6 +31,6 @@ interface EcloudApiInterface { @Headers("Content-Type: application/json") @POST("report") suspend fun uploadFaultyEmail( - @Body faultyToken: FaultyToken, + @Body faultyToken: FaultyToken ) } diff --git a/app/src/main/java/foundation/e/apps/data/ecloud/EcloudRepository.kt b/app/src/main/java/foundation/e/apps/data/ecloud/EcloudRepository.kt index 941735dc85ff8761dcbc46024e01bf050147a32f..a5667aa8557567d209515118306e4ed068814d18 100644 --- a/app/src/main/java/foundation/e/apps/data/ecloud/EcloudRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/ecloud/EcloudRepository.kt @@ -23,7 +23,7 @@ import javax.inject.Singleton @Singleton class EcloudRepository @Inject constructor( - private val ecloudApi: EcloudApiInterface, + private val ecloudApi: EcloudApiInterface ) { suspend fun uploadFaultyEmail(email: String, description: String) { try { diff --git a/app/src/main/java/foundation/e/apps/data/ecloud/modules/FaultyToken.kt b/app/src/main/java/foundation/e/apps/data/ecloud/modules/FaultyToken.kt index 8091d36d4f41e67ea73f5595ba87d1155fb0feff..fae19eb58339873eee22a01535c3a09980251c05 100644 --- a/app/src/main/java/foundation/e/apps/data/ecloud/modules/FaultyToken.kt +++ b/app/src/main/java/foundation/e/apps/data/ecloud/modules/FaultyToken.kt @@ -19,5 +19,5 @@ package foundation.e.apps.data.ecloud.modules data class FaultyToken( val email: String, - val description: String, + val description: String ) diff --git a/app/src/main/java/foundation/e/apps/data/enums/AppTag.kt b/app/src/main/java/foundation/e/apps/data/enums/AppTag.kt index 2b16116558e2b324e2684dbb6d35108d72c3e032..a6e3f49ec59cc1f6d8601056d0c5efd099f8c2c7 100644 --- a/app/src/main/java/foundation/e/apps/data/enums/AppTag.kt +++ b/app/src/main/java/foundation/e/apps/data/enums/AppTag.kt @@ -35,7 +35,10 @@ sealed class AppTag(val displayTag: String) { * This method allows for all those check to work without modification. */ fun getOperationalTag(): String { - return if (this is OpenSource) "Open Source" - else this::class.java.simpleName + return if (this is OpenSource) { + "Open Source" + } else { + this::class.java.simpleName + } } } diff --git a/app/src/main/java/foundation/e/apps/data/enums/FilterLevel.kt b/app/src/main/java/foundation/e/apps/data/enums/FilterLevel.kt index 13dcc7b7acc536612d93d1b7db5602034edc0e21..4cbf05300a6ccca6e7c8a4a58f83584b1868736a 100644 --- a/app/src/main/java/foundation/e/apps/data/enums/FilterLevel.kt +++ b/app/src/main/java/foundation/e/apps/data/enums/FilterLevel.kt @@ -37,7 +37,7 @@ enum class FilterLevel { UI, // Show the app in lists, but show "N/A" in the install button. DATA, // Filter the app out from lists and search results, don't show the app at all. NONE, // No restrictions - UNKNOWN, // Not initialised yet + UNKNOWN // Not initialised yet } fun FilterLevel.isUnFiltered(): Boolean = this == FilterLevel.NONE diff --git a/app/src/main/java/foundation/e/apps/data/enums/User.kt b/app/src/main/java/foundation/e/apps/data/enums/User.kt index e9190cfd96cf09227f510ae032045b1288e9d7bf..f265730944b2506709686c018db7d8d82ade0fa1 100644 --- a/app/src/main/java/foundation/e/apps/data/enums/User.kt +++ b/app/src/main/java/foundation/e/apps/data/enums/User.kt @@ -1,8 +1,19 @@ -package foundation.e.apps.data.enums - -enum class User { - NO_GOOGLE, - ANONYMOUS, - GOOGLE, - UNAVAILABLE -} +package foundation.e.apps.data.enums + +enum class User { + NO_GOOGLE, + ANONYMOUS, + GOOGLE, + UNAVAILABLE; + + companion object { + fun getUser(userString: String): User { + val userStrings = values().map { it.name } + return if (userString in userStrings) { + valueOf(userString) + } else { + UNAVAILABLE + } + } + } +} diff --git a/app/src/main/java/foundation/e/apps/data/exodus/ExodusTrackerApi.kt b/app/src/main/java/foundation/e/apps/data/exodus/ExodusTrackerApi.kt index 9c9c35ea56b931ca21e5e32394469abeea675f9b..169570664799cd594dce4ea31011ef07a8e546fd 100644 --- a/app/src/main/java/foundation/e/apps/data/exodus/ExodusTrackerApi.kt +++ b/app/src/main/java/foundation/e/apps/data/exodus/ExodusTrackerApi.kt @@ -17,6 +17,6 @@ interface ExodusTrackerApi { @GET("search/{appHandle}/details") suspend fun getTrackerInfoOfApp( @Path("appHandle") appHandle: String, - @Query("v") versionCode: Int, + @Query("v") versionCode: Int ): Response> } diff --git a/app/src/main/java/foundation/e/apps/data/exodus/models/Trackers.kt b/app/src/main/java/foundation/e/apps/data/exodus/models/Trackers.kt index 49f19e31fee6a19dd4ceaecf335cc88f6745d988..c5a3647bafdb6d09f9cd0663d8308c06d72bbe0c 100644 --- a/app/src/main/java/foundation/e/apps/data/exodus/models/Trackers.kt +++ b/app/src/main/java/foundation/e/apps/data/exodus/models/Trackers.kt @@ -16,5 +16,5 @@ data class Tracker( val creationDate: String?, val codeSignature: String?, val networkSignature: String?, - val website: String?, + val website: String? ) diff --git a/app/src/main/java/foundation/e/apps/data/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/data/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt index b3df01c3baec613e5a741a97ab006428363d2ec8..439ebd7c4a65521fe81db4dc80abc565efc79181 100644 --- a/app/src/main/java/foundation/e/apps/data/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt @@ -59,7 +59,7 @@ class AppPrivacyInfoRepositoryImpl @Inject constructor( val appTrackerInfoResult = getResult { exodusTrackerApi.getTrackerInfoOfApp( appHandle, - fusedApp.latest_version_code, + fusedApp.latest_version_code ) } @@ -94,7 +94,7 @@ class AppPrivacyInfoRepositoryImpl @Inject constructor( private suspend fun handleAppPrivacyInfoResultSuccess( fusedApp: FusedApp, - appTrackerResult: Result>, + appTrackerResult: Result> ): Result { if (trackers.isEmpty()) { generateTrackerList() @@ -129,7 +129,7 @@ class AppPrivacyInfoRepositoryImpl @Inject constructor( private fun createAppPrivacyInfo( fusedApp: FusedApp, - appTrackerResult: Result>, + appTrackerResult: Result> ): Result { appTrackerResult.data?.let { return Result.success(getAppPrivacyInfo(fusedApp, it)) @@ -139,7 +139,7 @@ class AppPrivacyInfoRepositoryImpl @Inject constructor( private fun getAppPrivacyInfo( fusedApp: FusedApp, - appTrackerData: List, + appTrackerData: List ): AppPrivacyInfo { /* * If the response is empty, that means there is no data on Exodus API about this app, diff --git a/app/src/main/java/foundation/e/apps/data/exodus/repositories/PrivacyScoreRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/data/exodus/repositories/PrivacyScoreRepositoryImpl.kt index 871e82640d5698bb78fd4296a0e8fdba85733d27..6c8dc7e45635451cf7ddbc7c3e5bc7c4030b42e8 100644 --- a/app/src/main/java/foundation/e/apps/data/exodus/repositories/PrivacyScoreRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/exodus/repositories/PrivacyScoreRepositoryImpl.kt @@ -48,9 +48,13 @@ class PrivacyScoreRepositoryImpl @Inject constructor() : PrivacyScoreRepository fusedApp.permsFromExodus.filter { it.contains("android.permission") }.size private fun calculatePermissionsScore(numberOfPermission: Int): Int { - return if (numberOfPermission > THRESHOLD_OF_NON_ZERO_PERMISSION_SCORE) MIN_PERMISSION_SCORE else round( - FACTOR_OF_PERMISSION_SCORE * ceil((MAX_PERMISSION_SCORE - numberOfPermission) / DIVIDER_OF_PERMISSION_SCORE) - ).toInt() + return if (numberOfPermission > THRESHOLD_OF_NON_ZERO_PERMISSION_SCORE) { + MIN_PERMISSION_SCORE + } else { + round( + FACTOR_OF_PERMISSION_SCORE * ceil((MAX_PERMISSION_SCORE - numberOfPermission) / DIVIDER_OF_PERMISSION_SCORE) + ).toInt() + } } // please do not put in the top of the class, as it can break the privacy calculation source code link. diff --git a/app/src/main/java/foundation/e/apps/data/fdroid/FdroidRepository.kt b/app/src/main/java/foundation/e/apps/data/fdroid/FdroidRepository.kt index 237592fae83ce89c21b0adeb5f0c60db00459dc7..f98aa553f3d63e7f9288fdef018643d237d69b01 100644 --- a/app/src/main/java/foundation/e/apps/data/fdroid/FdroidRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/fdroid/FdroidRepository.kt @@ -12,7 +12,7 @@ import javax.inject.Singleton @Singleton class FdroidRepository @Inject constructor( private val fdroidApi: FdroidApiInterface, - private val fdroidDao: FdroidDao, + private val fdroidDao: FdroidDao ) : IFdroidRepository { companion object { diff --git a/app/src/main/java/foundation/e/apps/data/fdroid/models/BuildInfo.kt b/app/src/main/java/foundation/e/apps/data/fdroid/models/BuildInfo.kt index 6b70811d66e19509bfa0941ff19d0a7d0e741e33..e02e70933c9ee2ddffa4bf96bc778d677b4ee6e2 100644 --- a/app/src/main/java/foundation/e/apps/data/fdroid/models/BuildInfo.kt +++ b/app/src/main/java/foundation/e/apps/data/fdroid/models/BuildInfo.kt @@ -29,7 +29,7 @@ class BuildInfo() { @JsonCreator constructor( @JsonProperty("versionCode") versionCode: String?, - @JsonProperty("versionName") versionName: String?, + @JsonProperty("versionName") versionName: String? ) : this() { this.versionCode = versionCode ?: "" this.versionName = versionName ?: "" diff --git a/app/src/main/java/foundation/e/apps/data/fdroid/models/FdroidApiModel.kt b/app/src/main/java/foundation/e/apps/data/fdroid/models/FdroidApiModel.kt index 172bb88023cefe35f5ad594a338714503ff2e5ee..978cf9e6d458d2a61d435c3cbda0ea21b5e789f3 100644 --- a/app/src/main/java/foundation/e/apps/data/fdroid/models/FdroidApiModel.kt +++ b/app/src/main/java/foundation/e/apps/data/fdroid/models/FdroidApiModel.kt @@ -24,7 +24,7 @@ class FdroidApiModel() { @JsonCreator constructor( @JsonProperty("AuthorName") AuthorName: String?, - @JsonProperty("Builds") Builds: List?, + @JsonProperty("Builds") Builds: List? ) : this() { this.authorName = AuthorName ?: "" this.builds = Builds ?: emptyList() diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt index 00dd50683738bdcc6da11490f52790cf822c3575..bd6614018c8e23aaf285a4ca84378a587addb3dc 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt @@ -99,7 +99,7 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedApi) } suspend fun getCategoriesList( - type: CategoryType, + type: CategoryType ): Triple, String, ResultStatus> { return fusedAPIImpl.getCategoriesList(type) } diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt index 7267f634b42767665972f8e00101e193e88bac9b..4dd9b71756cc75f0ad7549c801dfc28e1016a305 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt @@ -36,7 +36,7 @@ interface FusedApi { fun getApplicationCategoryPreference(): List suspend fun getHomeScreenData( - authData: AuthData, + authData: AuthData ): LiveData>> /* @@ -51,7 +51,7 @@ interface FusedApi { * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 */ suspend fun getCategoriesList( - type: CategoryType, + type: CategoryType ): Triple, String, ResultStatus> /** @@ -118,7 +118,7 @@ interface FusedApi { */ suspend fun filterRestrictedGPlayApps( authData: AuthData, - appList: List, + appList: List ): ResultSupreme> /** diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt index d992aa61d13330c5a147961598a0deefc1c61dcd..5fdca3954e137b1ae4c2a129ccb691565fb4a5d6 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt @@ -121,9 +121,8 @@ class FusedApiImpl @Inject constructor( } override suspend fun getHomeScreenData( - authData: AuthData, + authData: AuthData ): LiveData>> { - val list = mutableListOf() var resultGplay: FusedHomeDeferred? = null var resultOpenSource: FusedHomeDeferred? = null @@ -131,7 +130,6 @@ class FusedApiImpl @Inject constructor( return liveData { coroutineScope { - if (preferenceManagerModule.isGplaySelected()) { resultGplay = async { loadHomeData(list, Source.GPLAY, authData) } } @@ -160,9 +158,8 @@ class FusedApiImpl @Inject constructor( private suspend fun loadHomeData( priorList: MutableList, source: Source, - authData: AuthData, + authData: AuthData ): ResultSupreme> { - val result = when (source) { Source.GPLAY -> handleNetworkResult> { priorList.addAll(fetchGPlayHome(authData)) @@ -221,7 +218,7 @@ class FusedApiImpl @Inject constructor( * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 */ override suspend fun getCategoriesList( - type: CategoryType, + type: CategoryType ): Triple, String, ResultStatus> { val categoriesList = mutableListOf() val preferredApplicationType = preferenceManagerModule.preferredApplicationType() @@ -414,7 +411,7 @@ class FusedApiImpl @Inject constructor( } private suspend fun getCleanApkPackageResult( - query: String, + query: String ): FusedApp? { getCleanapkSearchResult(query).let { if (it.isSuccess() && it.data!!.package_name.isNotBlank()) { @@ -426,7 +423,7 @@ class FusedApiImpl @Inject constructor( private suspend fun getGplayPackagResult( query: String, - authData: AuthData, + authData: AuthData ): FusedApp? { try { getApplicationDetails(query, query, authData, Origin.GPLAY).let { @@ -482,7 +479,7 @@ class FusedApiImpl @Inject constructor( packageName, moduleName, versionCode, - offerType, + offerType ) for (element in list) { if (element.name == "$moduleName.apk") { @@ -609,7 +606,7 @@ class FusedApiImpl @Inject constructor( * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 */ private suspend fun getAppDetailsListFromCleanapk( - packageNameList: List, + packageNameList: List ): Pair, ResultStatus> { var status = ResultStatus.OK val fusedAppList = mutableListOf() @@ -653,7 +650,7 @@ class FusedApiImpl @Inject constructor( */ private suspend fun getAppDetailsListFromGPlay( packageNameList: List, - authData: AuthData, + authData: AuthData ): Pair, ResultStatus> { val fusedAppList = mutableListOf() @@ -695,7 +692,7 @@ class FusedApiImpl @Inject constructor( */ override suspend fun filterRestrictedGPlayApps( authData: AuthData, - appList: List, + appList: List ): ResultSupreme> { val filteredFusedApps = mutableListOf() return handleNetworkResult { @@ -748,7 +745,7 @@ class FusedApiImpl @Inject constructor( gplayRepository.getDownloadInfo( fusedApp.package_name, fusedApp.latest_version_code, - fusedApp.offer_type, + fusedApp.offer_type ) }.isSuccess } @@ -777,7 +774,6 @@ class FusedApiImpl @Inject constructor( authData: AuthData, origin: Origin ): Pair { - var response: FusedApp? = null val result = handleNetworkResult { @@ -811,7 +807,7 @@ class FusedApiImpl @Inject constructor( */ private suspend fun handleAllSourcesCategories( categoriesList: MutableList, - type: CategoryType, + type: CategoryType ): Pair { var apiStatus = ResultStatus.OK var errorApplicationCategory = "" @@ -832,7 +828,7 @@ class FusedApiImpl @Inject constructor( if (preferenceManagerModule.isGplaySelected()) { val gplayCategoryResult = fetchGplayCategories( - type, + type ) categoriesList.addAll(gplayCategoryResult.data ?: listOf()) apiStatus = gplayCategoryResult.getResultStatus() @@ -843,7 +839,7 @@ class FusedApiImpl @Inject constructor( } private suspend fun fetchGplayCategories( - type: CategoryType, + type: CategoryType ): ResultSupreme> { val categoryList = mutableListOf() @@ -859,14 +855,16 @@ class FusedApiImpl @Inject constructor( } private suspend fun fetchPWACategories( - type: CategoryType, + type: CategoryType ): Triple, String> { val fusedCategoriesList = mutableListOf() val result = handleNetworkResult { getPWAsCategories()?.let { fusedCategoriesList.addAll( getFusedCategoryBasedOnCategoryType( - it, type, AppTag.PWA(context.getString(R.string.pwa)) + it, + type, + AppTag.PWA(context.getString(R.string.pwa)) ) ) } @@ -876,7 +874,7 @@ class FusedApiImpl @Inject constructor( } private suspend fun fetchOpenSourceCategories( - type: CategoryType, + type: CategoryType ): Triple, String> { val fusedCategoryList = mutableListOf() val result = handleNetworkResult { @@ -895,15 +893,18 @@ class FusedApiImpl @Inject constructor( } private fun updateCategoryDrawable( - category: FusedCategory, + category: FusedCategory ) { category.drawable = getCategoryIconResource(getCategoryIconName(category)) } private fun getCategoryIconName(category: FusedCategory): String { - var categoryTitle = if (category.tag.getOperationalTag().contentEquals(AppTag.GPlay().getOperationalTag())) - category.id else category.title + var categoryTitle = if (category.tag.getOperationalTag().contentEquals(AppTag.GPlay().getOperationalTag())) { + category.id + } else { + category.title + } if (categoryTitle.contains(CATEGORY_TITLE_REPLACEABLE_CONJUNCTION)) { categoryTitle = categoryTitle.replace(CATEGORY_TITLE_REPLACEABLE_CONJUNCTION, "and") @@ -981,13 +982,13 @@ class FusedApiImpl @Inject constructor( private suspend fun getOpenSourceAppsResponse(category: String): Search? { return cleanApkAppsRepository.getAppsByCategory( - category, + category ).body() } private suspend fun getPWAAppsResponse(category: String): Search? { return cleanApkPWARepository.getAppsByCategory( - category, + category ).body() } @@ -997,7 +998,7 @@ class FusedApiImpl @Inject constructor( id = id.lowercase(), title = this.title, browseUrl = this.browseUrl, - imageUrl = this.imageUrl, + imageUrl = this.imageUrl ) } @@ -1007,7 +1008,7 @@ class FusedApiImpl @Inject constructor( private suspend fun getCleanAPKSearchResults( keyword: String, - source: String = CleanApkRetrofit.APP_SOURCE_FOSS, + source: String = CleanApkRetrofit.APP_SOURCE_FOSS ): List { val list = mutableListOf() val response = @@ -1242,7 +1243,9 @@ class FusedApiImpl @Inject constructor( this.labeledRating.run { if (isNotEmpty()) { this.replace(",", ".").toDoubleOrNull() ?: -1.0 - } else -1.0 + } else { + -1.0 + } } ), offer_type = this.offerType, @@ -1252,7 +1255,7 @@ class FusedApiImpl @Inject constructor( appSize = Formatter.formatFileSize(context, this.size), isFree = this.isFree, price = this.price, - restriction = this.restriction, + restriction = this.restriction ) app.updateStatus() return app @@ -1284,9 +1287,13 @@ class FusedApiImpl @Inject constructor( private fun FusedApp.updateSource() { this.apply { - source = if (origin == Origin.CLEANAPK && is_pwa) context.getString(R.string.pwa) - else if (origin == Origin.CLEANAPK) context.getString(R.string.open_source) - else "" + source = if (origin == Origin.CLEANAPK && is_pwa) { + context.getString(R.string.pwa) + } else if (origin == Origin.CLEANAPK) { + context.getString(R.string.open_source) + } else { + "" + } } } @@ -1320,7 +1327,7 @@ class FusedApiImpl @Inject constructor( private fun areFusedAppsUpdated( oldFusedHome: FusedHome, - newFusedHome: FusedHome, + newFusedHome: FusedHome ): Boolean { val fusedAppDiffUtil = HomeChildFusedAppDiffUtil() if (oldFusedHome.list.size != newFusedHome.list.size) { diff --git a/app/src/main/java/foundation/e/apps/data/fused/UpdatesDao.kt b/app/src/main/java/foundation/e/apps/data/fused/UpdatesDao.kt index 65e5edc9f34170f4758aa24eb0f7b885c3dc5795..4fc69e67c6c2a09a8efa11baf37af93e5447661b 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/UpdatesDao.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/UpdatesDao.kt @@ -43,4 +43,8 @@ object UpdatesDao { fun clearSuccessfullyUpdatedApps() { _successfulUpdatedApps.clear() } + + fun clearAllUpdatableApps() { + _appsAwaitingForUpdate.clear() + } } diff --git a/app/src/main/java/foundation/e/apps/data/fusedDownload/FileManager.kt b/app/src/main/java/foundation/e/apps/data/fusedDownload/FileManager.kt index 32a9fab55325f6a618b7d408e98f17f7feca5c31..93b35ef14513cb7ef4640fcea3a950cc0a555cd4 100644 --- a/app/src/main/java/foundation/e/apps/data/fusedDownload/FileManager.kt +++ b/app/src/main/java/foundation/e/apps/data/fusedDownload/FileManager.kt @@ -14,7 +14,6 @@ object FileManager { var inputStream: InputStream? = null var outputStream: OutputStream? = null try { - // create output directory if it doesn't exist val dir = File(outputPath) if (!dir.exists()) { diff --git a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt index 383f8e7c6e43a3914944d9e2708fbb45bfbc0bfe..e348052f978a21ca1443263f164773e4a262328f 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt @@ -70,7 +70,7 @@ class GplayStoreRepositoryImpl @Inject constructor( context.getString(R.string.topgrossing_apps) to mapOf(Chart.TOP_GROSSING to TopChartsHelper.Type.APPLICATION), context.getString(R.string.topgrossing_games) to mapOf(Chart.TOP_GROSSING to TopChartsHelper.Type.GAME), context.getString(R.string.movers_shakers_apps) to mapOf(Chart.MOVERS_SHAKERS to TopChartsHelper.Type.APPLICATION), - context.getString(R.string.movers_shakers_games) to mapOf(Chart.MOVERS_SHAKERS to TopChartsHelper.Type.GAME), + context.getString(R.string.movers_shakers_games) to mapOf(Chart.MOVERS_SHAKERS to TopChartsHelper.Type.GAME) ) override suspend fun getSearchResult( diff --git a/app/src/main/java/foundation/e/apps/data/gplay/utils/AC2DMTask.kt b/app/src/main/java/foundation/e/apps/data/gplay/utils/AC2DMTask.kt index 448c459044841a391aae8cc289786f8ac95a1224..88d4c5494ab97c0d805f1a6fc13030d6b3c17d88 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/utils/AC2DMTask.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/utils/AC2DMTask.kt @@ -34,8 +34,9 @@ class AC2DMTask @Inject constructor( } fun getAC2DMResponse(email: String?, oAuthToken: String?): PlayResponse { - if (email == null || oAuthToken == null) + if (email == null || oAuthToken == null) { return PlayResponse() + } val params: MutableMap = hashMapOf() params["lang"] = Locale.getDefault().toString().replace("_", "-") diff --git a/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt b/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt index 2bfd5578aa5ff7dc8d8ef530a04dd50c0f1c7438..dd2e47798e923892e131277d0ed32651db669551 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt @@ -40,12 +40,13 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import timber.log.Timber import java.io.IOException +import java.io.InterruptedIOException import java.net.SocketTimeoutException import java.util.concurrent.TimeUnit import javax.inject.Inject class GPlayHttpClient @Inject constructor( - private val cache: Cache, + private val cache: Cache ) : IHttpClient { private val POST = "POST" @@ -169,7 +170,11 @@ class GPlayHttpClient @Inject constructor( } catch (e: GplayHttpRequestException) { throw e } catch (e: Exception) { - val status = if (e is SocketTimeoutException) STATUS_CODE_TIMEOUT else -1 + val status = when { + e is SocketTimeoutException -> STATUS_CODE_TIMEOUT + e is InterruptedIOException && e.message == "timeout" -> STATUS_CODE_TIMEOUT + else -> -1 + } throw GplayHttpRequestException(status, e.localizedMessage ?: "") } finally { response?.close() diff --git a/app/src/main/java/foundation/e/apps/data/gplay/utils/NativeDeviceInfoProviderModule.kt b/app/src/main/java/foundation/e/apps/data/gplay/utils/NativeDeviceInfoProviderModule.kt index 53a70ff9359eeb5f3cc6fe5389c829051af43967..fd5760da70103ac58c81bd404abf30676be5d6a3 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/utils/NativeDeviceInfoProviderModule.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/utils/NativeDeviceInfoProviderModule.kt @@ -40,7 +40,7 @@ object NativeDeviceInfoProviderModule { @Singleton @Provides fun provideNativeDeviceProperties( - @ApplicationContext context: Context, + @ApplicationContext context: Context ): Properties { val properties = Properties().apply { // Build Props @@ -48,10 +48,11 @@ object NativeDeviceInfoProviderModule { setProperty("Build.HARDWARE", Build.HARDWARE) setProperty( "Build.RADIO", - if (Build.getRadioVersion() != null) + if (Build.getRadioVersion() != null) { Build.getRadioVersion() - else + } else { "unknown" + } ) setProperty("Build.FINGERPRINT", Build.FINGERPRINT) setProperty("Build.BRAND", Build.BRAND) diff --git a/app/src/main/java/foundation/e/apps/data/gplay/utils/NativeGsfVersionProvider.kt b/app/src/main/java/foundation/e/apps/data/gplay/utils/NativeGsfVersionProvider.kt index 40ab7b2fc83cdfb8a0526abe4a92deb3987fc3e9..9dbb09d63979eb9e7c11662d49b40e8230cd95f0 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/utils/NativeGsfVersionProvider.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/utils/NativeGsfVersionProvider.kt @@ -46,24 +46,27 @@ class NativeGsfVersionProvider(context: Context) { } fun getGsfVersionCode(defaultIfNotFound: Boolean): Int { - return if (defaultIfNotFound && gsfVersionCode < GOOGLE_SERVICES_VERSION_CODE) + return if (defaultIfNotFound && gsfVersionCode < GOOGLE_SERVICES_VERSION_CODE) { GOOGLE_SERVICES_VERSION_CODE - else + } else { gsfVersionCode + } } fun getVendingVersionCode(defaultIfNotFound: Boolean): Int { - return if (defaultIfNotFound && vendingVersionCode < GOOGLE_VENDING_VERSION_CODE) + return if (defaultIfNotFound && vendingVersionCode < GOOGLE_VENDING_VERSION_CODE) { GOOGLE_VENDING_VERSION_CODE - else + } else { vendingVersionCode + } } fun getVendingVersionString(defaultIfNotFound: Boolean): String { - return if (defaultIfNotFound && vendingVersionCode < GOOGLE_VENDING_VERSION_CODE) + return if (defaultIfNotFound && vendingVersionCode < GOOGLE_VENDING_VERSION_CODE) { GOOGLE_VENDING_VERSION_STRING - else + } else { vendingVersionString + } } companion object { diff --git a/app/src/main/java/foundation/e/apps/data/gplay/utils/TimeoutEvaluation.kt b/app/src/main/java/foundation/e/apps/data/gplay/utils/TimeoutEvaluation.kt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/src/main/java/foundation/e/apps/data/login/AuthObject.kt b/app/src/main/java/foundation/e/apps/data/login/AuthObject.kt index e63f051de5d8438b1bd1ed2b3845ed4c5aeafb21..c5f656075c5d6c7d55f60128335a06aac0a613ce 100644 --- a/app/src/main/java/foundation/e/apps/data/login/AuthObject.kt +++ b/app/src/main/java/foundation/e/apps/data/login/AuthObject.kt @@ -51,12 +51,12 @@ sealed class AuthObject { exception = GPlayValidationException( message, this.user, - 401, + 401 ) ).apply { otherPayload = this@GPlayAuth.result.otherPayload }, - this.user, + this.user ) } } @@ -68,10 +68,10 @@ sealed class AuthObject { message = "Unauthorized", exception = CleanApkException( isTimeout = false, - message = "Unauthorized", + message = "Unauthorized" ) ), - this.user, + this.user ) } } 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 cc38464e217caff6daac4c85b38fa194d8e58700..302a16cc10e8a13ee78eaad8affc3ebcefb102ea 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,7 @@ package foundation.e.apps.data.login import foundation.e.apps.data.Constants import foundation.e.apps.data.enums.User +import foundation.e.apps.data.fused.UpdatesDao import javax.inject.Inject import javax.inject.Singleton @@ -30,7 +31,7 @@ import javax.inject.Singleton */ @Singleton class LoginCommon @Inject constructor( - private val loginDataStore: LoginDataStore, + private val loginDataStore: LoginDataStore ) { suspend fun saveUserType(user: User) { loginDataStore.saveUserType(user) @@ -58,5 +59,6 @@ class LoginCommon @Inject constructor( loginDataStore.setSource(Constants.PREFERENCE_SHOW_FOSS, true) loginDataStore.setSource(Constants.PREFERENCE_SHOW_PWA, true) loginDataStore.setSource(Constants.PREFERENCE_SHOW_GPLAY, true) + UpdatesDao.clearAllUpdatableApps() } } diff --git a/app/src/main/java/foundation/e/apps/data/login/LoginDataStore.kt b/app/src/main/java/foundation/e/apps/data/login/LoginDataStore.kt index 468cbc667fc4df06e96b5c2457ff6134179df1ae..7d8669e7831072eeb8116819f728b79906a0d87a 100644 --- a/app/src/main/java/foundation/e/apps/data/login/LoginDataStore.kt +++ b/app/src/main/java/foundation/e/apps/data/login/LoginDataStore.kt @@ -120,8 +120,11 @@ class LoginDataStore @Inject constructor( return runBlocking { userType.first().run { val userStrings = User.values().map { it.name } - if (this !in userStrings) User.UNAVAILABLE - else User.valueOf(this) + if (this in userStrings) { + User.valueOf(this) + } else { + User.UNAVAILABLE + } } } } diff --git a/app/src/main/java/foundation/e/apps/data/login/LoginSourceCleanApk.kt b/app/src/main/java/foundation/e/apps/data/login/LoginSourceCleanApk.kt index a5f49569b99a62e1a9bcea1b506a42ddb09a78d1..69c846e14b7219e5cc87ac5cba53c94ce43196cd 100644 --- a/app/src/main/java/foundation/e/apps/data/login/LoginSourceCleanApk.kt +++ b/app/src/main/java/foundation/e/apps/data/login/LoginSourceCleanApk.kt @@ -28,7 +28,7 @@ import javax.inject.Singleton */ @Singleton class LoginSourceCleanApk @Inject constructor( - val loginDataStore: LoginDataStore, + val loginDataStore: LoginDataStore ) : LoginSourceInterface { private val user: User @@ -47,7 +47,7 @@ class LoginSourceCleanApk @Inject constructor( override suspend fun getAuthObject(): AuthObject.CleanApk { return AuthObject.CleanApk( ResultSupreme.Success(Unit), - user, + user ) } diff --git a/app/src/main/java/foundation/e/apps/data/login/LoginSourceGPlay.kt b/app/src/main/java/foundation/e/apps/data/login/LoginSourceGPlay.kt index d5cf5e45b584a4f5fdb3c294a2482007dc9bda92..165dcecbea8041aec2acd99541eb640fc6104a29 100644 --- a/app/src/main/java/foundation/e/apps/data/login/LoginSourceGPlay.kt +++ b/app/src/main/java/foundation/e/apps/data/login/LoginSourceGPlay.kt @@ -43,7 +43,7 @@ import javax.inject.Singleton class LoginSourceGPlay @Inject constructor( @ApplicationContext private val context: Context, private val gson: Gson, - private val loginDataStore: LoginDataStore, + private val loginDataStore: LoginDataStore ) : LoginSourceInterface, AuthDataValidator { @Inject @@ -81,8 +81,11 @@ class LoginSourceGPlay @Inject constructor( savedAuth ?: run { // if no saved data, then generate new auth data. generateAuthData().let { - if (it.isSuccess()) it.data!! - else return AuthObject.GPlayAuth(it, user) + if (it.isSuccess()) { + it.data!! + } else { + return AuthObject.GPlayAuth(it, user) + } } } ) @@ -112,12 +115,15 @@ class LoginSourceGPlay @Inject constructor( */ 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() + return if (authJson.isBlank()) { null + } else { + try { + gson.fromJson(authJson, AuthData::class.java) + } catch (e: Exception) { + Timber.e(e) + null + } } } @@ -156,8 +162,11 @@ class LoginSourceGPlay @Inject constructor( */ private suspend fun getAuthData(): ResultSupreme { return loginApiRepository.fetchAuthData("", "", locale).run { - if (isSuccess()) ResultSupreme.Success(formatAuthData(this.data!!)) - else this + if (isSuccess()) { + ResultSupreme.Success(formatAuthData(this.data!!)) + } else { + this + } } } @@ -167,9 +176,8 @@ class LoginSourceGPlay @Inject constructor( private suspend fun getAuthData( email: String, oauthToken: String, - aasToken: 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. @@ -207,8 +215,11 @@ class LoginSourceGPlay @Inject constructor( */ loginDataStore.saveAasToken(aasTokenFetched) return loginApiRepository.fetchAuthData(email, aasTokenFetched, locale).run { - if (isSuccess()) ResultSupreme.Success(formatAuthData(this.data!!)) - else this + if (isSuccess()) { + ResultSupreme.Success(formatAuthData(this.data!!)) + } else { + this + } } } diff --git a/app/src/main/java/foundation/e/apps/data/login/LoginSourceRepository.kt b/app/src/main/java/foundation/e/apps/data/login/LoginSourceRepository.kt index fea7939b6a31e8cbf2353b172add38ed9bac312b..606de937e3f1b287b6cdc5ac4c608ce85cba45a6 100644 --- a/app/src/main/java/foundation/e/apps/data/login/LoginSourceRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/login/LoginSourceRepository.kt @@ -28,14 +28,13 @@ import javax.inject.Singleton @Singleton class LoginSourceRepository @Inject constructor( private val loginCommon: LoginCommon, - private val sources: List, + private val sources: List ) { var gplayAuth: AuthData? = null get() = field ?: throw GPlayLoginException(false, "AuthData is not available!", getUserType()) suspend fun getAuthObjects(clearAuthTypes: List = listOf()): List { - val authObjectsLocal = ArrayList() for (source in sources) { diff --git a/app/src/main/java/foundation/e/apps/data/login/LoginViewModel.kt b/app/src/main/java/foundation/e/apps/data/login/LoginViewModel.kt deleted file mode 100644 index 659ed83187d1230832e038f1e9cceca5a41bf1ed..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/data/login/LoginViewModel.kt +++ /dev/null @@ -1,138 +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 androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import foundation.e.apps.data.enums.User -import foundation.e.apps.ui.parentFragment.LoadingViewModel -import kotlinx.coroutines.launch -import okhttp3.Cache -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, - private val cache: Cache, -) : 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() - } - } - - /** - * Call this to use No-Google mode, i.e. show only PWAs and F-droid apps, - * without contacting Google servers. - * 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 initialNoGoogleLogin(onUserSaved: () -> Unit) { - viewModelScope.launch { - loginSourceRepository.setNoGoogleMode() - onUserSaved() - startLoginFlow() - } - } - - /** - * Once an AuthObject is marked as invalid, it will be refreshed - * automatically by LoadingViewModel. - * If GPlay auth is invalid, [LoadingViewModel.onLoadData] has a retry block, - * this block will clear existing GPlay AuthData and freshly start the login flow. - */ - fun markInvalidAuthObject(authObjectName: String) { - val authObjectsLocal = authObjects.value?.toMutableList() - val invalidObject = authObjectsLocal?.find { it::class.java.simpleName == authObjectName } - - val replacedObject = invalidObject?.createInvalidAuthObject() - - authObjectsLocal?.apply { - if (invalidObject != null && replacedObject != null) { - remove(invalidObject) - add(replacedObject) - } - } - - authObjects.postValue(authObjectsLocal) - cache.evictAll() - } - - /** - * Clears all saved data and logs out the user to the sign in screen. - */ - fun logout() { - viewModelScope.launch { - cache.evictAll() - loginSourceRepository.logout() - authObjects.postValue(listOf()) - } - } -} diff --git a/app/src/main/java/foundation/e/apps/data/login/api/AnonymousLoginApi.kt b/app/src/main/java/foundation/e/apps/data/login/api/AnonymousLoginApi.kt index edd0d15a23f00ff10b1d1fe24f5f5ef664a0c287..15bbc9f7fdbaa701fcd9bff1f189a28a19e26b4a 100644 --- a/app/src/main/java/foundation/e/apps/data/login/api/AnonymousLoginApi.kt +++ b/app/src/main/java/foundation/e/apps/data/login/api/AnonymousLoginApi.kt @@ -29,7 +29,7 @@ import java.util.Properties class AnonymousLoginApi( private val gPlayHttpClient: GPlayHttpClient, private val nativeDeviceProperty: Properties, - private val gson: Gson, + private val gson: Gson ) : GPlayLoginInterface { private val tokenUrl: String = "https://eu.gtoken.ecloud.global" @@ -50,8 +50,11 @@ class AnonymousLoginApi( "Network code: ${response.code}\n" + "Success: ${response.isSuccessful}" + response.errorString.run { - if (isNotBlank()) "\nError message: $this" - else "" + if (isNotBlank()) { + "\nError message: $this" + } else { + "" + } } ) } else { diff --git a/app/src/main/java/foundation/e/apps/data/login/api/GPlayApiFactory.kt b/app/src/main/java/foundation/e/apps/data/login/api/GPlayApiFactory.kt index 4b5a71052da94b45addaea68a417c491b8776ebe..4334401fa9527f8d6d86a1b5d15245b9702266b2 100644 --- a/app/src/main/java/foundation/e/apps/data/login/api/GPlayApiFactory.kt +++ b/app/src/main/java/foundation/e/apps/data/login/api/GPlayApiFactory.kt @@ -30,7 +30,7 @@ class GPlayApiFactory @Inject constructor( private val gPlayHttpClient: GPlayHttpClient, private val nativeDeviceProperty: Properties, private val aC2DMTask: AC2DMTask, - private val gson: Gson, + private val gson: Gson ) { fun getGPlayApi(user: User): GPlayLoginInterface { diff --git a/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginApi.kt b/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginApi.kt index 4331d473a9f8ebe7bc170c2119ab7b9c4a882a69..d8cd69d87206abdad71003ca823da93ed176be11 100644 --- a/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginApi.kt +++ b/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginApi.kt @@ -30,7 +30,7 @@ import java.util.Properties class GoogleLoginApi( private val gPlayHttpClient: GPlayHttpClient, private val nativeDeviceProperty: Properties, - private val aC2DMTask: AC2DMTask, + private val aC2DMTask: AC2DMTask ) : GPlayLoginInterface { /** diff --git a/app/src/main/java/foundation/e/apps/data/login/api/LoginApiRepository.kt b/app/src/main/java/foundation/e/apps/data/login/api/LoginApiRepository.kt index b2fbd026c9919c4b62a0e97c0736fa367b786048..5030cb0e28c0dd9752fb99e9d2faeace06a095bb 100644 --- a/app/src/main/java/foundation/e/apps/data/login/api/LoginApiRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/login/api/LoginApiRepository.kt @@ -37,7 +37,7 @@ import java.util.Locale */ class LoginApiRepository constructor( private val gPlayLoginInterface: GPlayLoginInterface, - private val user: User, + private val user: User ) { /** diff --git a/app/src/main/java/foundation/e/apps/data/login/exceptions/CleanApkException.kt b/app/src/main/java/foundation/e/apps/data/login/exceptions/CleanApkException.kt index 293819914aac64751e60c5da12cf5b94caa2a5c2..71cbd5b4ddee87550204025998cea7847a9368c7 100644 --- a/app/src/main/java/foundation/e/apps/data/login/exceptions/CleanApkException.kt +++ b/app/src/main/java/foundation/e/apps/data/login/exceptions/CleanApkException.kt @@ -22,5 +22,5 @@ package foundation.e.apps.data.login.exceptions */ class CleanApkException( val isTimeout: Boolean, - message: String? = null, + message: String? = null ) : LoginException(message) diff --git a/app/src/main/java/foundation/e/apps/data/login/exceptions/GPlayException.kt b/app/src/main/java/foundation/e/apps/data/login/exceptions/GPlayException.kt index 345208778528b1d12c330fe7477111c98c7e7501..16a84a836db1f2e3d33bb1aa282c7d50e69bf10f 100644 --- a/app/src/main/java/foundation/e/apps/data/login/exceptions/GPlayException.kt +++ b/app/src/main/java/foundation/e/apps/data/login/exceptions/GPlayException.kt @@ -22,5 +22,5 @@ package foundation.e.apps.data.login.exceptions */ open class GPlayException( val isTimeout: Boolean, - message: String? = null, + message: String? = null ) : LoginException(message) diff --git a/app/src/main/java/foundation/e/apps/data/login/exceptions/GPlayLoginException.kt b/app/src/main/java/foundation/e/apps/data/login/exceptions/GPlayLoginException.kt index d6a64a7bb3d20c52243c9f30434122b431642716..6e3a403bb9229a04b5c45bf6f8309c4a79273792 100644 --- a/app/src/main/java/foundation/e/apps/data/login/exceptions/GPlayLoginException.kt +++ b/app/src/main/java/foundation/e/apps/data/login/exceptions/GPlayLoginException.kt @@ -27,5 +27,5 @@ import foundation.e.apps.data.enums.User open class GPlayLoginException( isTimeout: Boolean, message: String? = null, - val user: User, + val user: User ) : GPlayException(isTimeout, message) diff --git a/app/src/main/java/foundation/e/apps/data/login/exceptions/GPlayValidationException.kt b/app/src/main/java/foundation/e/apps/data/login/exceptions/GPlayValidationException.kt index 5b4322a4cc955487b91ee65578af872f7be67876..94cb6affad07b261d4f9401f2d119698c6317239 100644 --- a/app/src/main/java/foundation/e/apps/data/login/exceptions/GPlayValidationException.kt +++ b/app/src/main/java/foundation/e/apps/data/login/exceptions/GPlayValidationException.kt @@ -28,5 +28,5 @@ import foundation.e.apps.data.enums.User class GPlayValidationException( message: String, user: User, - val networkCode: Int, + val networkCode: Int ) : GPlayLoginException(false, message, user) 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..787da78729340a27f72fbb493b33635b96c2a4b4 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,148 @@ -/* - * 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 + +@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.valueOf(this) + } else { + User.UNAVAILABLE + } + } + } + } +} diff --git a/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt b/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt index 226b43bcbbd2ed67b40b40754c948b26e03f5617..e80b3464b1c0ea916fc123fabf0dda9e33d28280 100644 --- a/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt @@ -49,8 +49,8 @@ class UpdatesManagerImpl @Inject constructor( private val fusedAPIRepository: FusedAPIRepository, private val faultyAppRepository: FaultyAppRepository, private val preferenceManagerModule: PreferenceManagerModule, - private val fdroidRepository: FdroidRepository, private val blockedAppRepository: BlockedAppRepository, + private val fdroidRepository: FdroidRepository ) { companion object { @@ -107,7 +107,6 @@ class UpdatesManagerImpl @Inject constructor( if (getApplicationCategoryPreference().contains(APP_TYPE_ANY) && gPlayInstalledApps.isNotEmpty() ) { - val gplayStatus = getUpdatesFromApi({ getGPlayUpdates( gPlayInstalledApps, @@ -169,7 +168,7 @@ class UpdatesManagerImpl @Inject constructor( pkgManagerModule.getInstallerName(it.packageName) in listOf( context.packageName, PACKAGE_NAME_F_DROID, - PACKAGE_NAME_F_DROID_PRIVILEGED, + PACKAGE_NAME_F_DROID_PRIVILEGED ) }.map { it.packageName } } @@ -183,7 +182,7 @@ class UpdatesManagerImpl @Inject constructor( private fun getGPlayInstalledApps(): List { return userApplications.filter { pkgManagerModule.getInstallerName(it.packageName) in listOf( - PACKAGE_NAME_ANDROID_VENDING, + PACKAGE_NAME_ANDROID_VENDING ) }.map { it.packageName } } @@ -214,7 +213,7 @@ class UpdatesManagerImpl @Inject constructor( */ private suspend fun getUpdatesFromApi( apiFunction: suspend () -> Pair, ResultStatus>, - updateAccumulationList: MutableList, + updateAccumulationList: MutableList ): ResultStatus { val apiResult = apiFunction() val updatableApps = apiResult.first.filter { @@ -234,7 +233,6 @@ class UpdatesManagerImpl @Inject constructor( packageNames: List, authData: AuthData ): Pair, ResultStatus> { - val appsResults = coroutineScope { val deferredResults = packageNames.map { packageName -> async { @@ -309,7 +307,7 @@ class UpdatesManagerImpl @Inject constructor( * in [installedPackageNames], it will not be present in the list returned by this method. */ private suspend fun findPackagesMatchingFDroidSignatures( - installedPackageNames: List, + installedPackageNames: List ): List { val fDroidAppsAndSignatures = getFDroidAppsAndSignatures(installedPackageNames) diff --git a/app/src/main/java/foundation/e/apps/di/LoginModule.kt b/app/src/main/java/foundation/e/apps/di/LoginModule.kt index f59a084b8abfbab057fc725a488be61096d43095..4ebbd29077627619d44dd2fd6954422c109e989a 100644 --- a/app/src/main/java/foundation/e/apps/di/LoginModule.kt +++ b/app/src/main/java/foundation/e/apps/di/LoginModule.kt @@ -24,6 +24,13 @@ import dagger.hilt.components.SingletonComponent import foundation.e.apps.data.login.LoginSourceCleanApk import foundation.e.apps.data.login.LoginSourceGPlay import foundation.e.apps.data.login.LoginSourceInterface +import foundation.e.apps.domain.common.repository.CacheRepositoryImpl +import foundation.e.apps.domain.install.usecase.AppInstallerUseCase +import foundation.e.apps.domain.login.repository.LoginRepositoryImpl +import foundation.e.apps.domain.login.usecase.UserLoginUseCase +import foundation.e.apps.domain.main.usecase.MainActivityUseCase +import foundation.e.apps.domain.settings.usecase.SettingsUseCase +import foundation.e.apps.domain.updates.usecase.UpdatesUseCase @InstallIn(SingletonComponent::class) @Module @@ -32,8 +39,44 @@ object LoginModule { @Provides fun providesLoginSources( gPlay: LoginSourceGPlay, - cleanApk: LoginSourceCleanApk, + cleanApk: LoginSourceCleanApk ): List { return listOf(gPlay, cleanApk) } + + @Provides + fun provideLoginUserCase( + loginRepositoryImpl: LoginRepositoryImpl, + cacheRepositoryImpl: CacheRepositoryImpl + ): UserLoginUseCase { + return UserLoginUseCase(loginRepositoryImpl, cacheRepositoryImpl) + } + + @Provides + fun provideSettingsUserCase( + cacheRepositoryImpl: CacheRepositoryImpl + ): SettingsUseCase { + return SettingsUseCase(cacheRepositoryImpl) + } + + @Provides + fun provideMainActivityUseCase( + cacheRepositoryImpl: CacheRepositoryImpl + ): MainActivityUseCase { + return MainActivityUseCase(cacheRepositoryImpl) + } + + @Provides + fun provideUpdatesUseCase( + cacheRepositoryImpl: CacheRepositoryImpl + ): UpdatesUseCase { + return UpdatesUseCase(cacheRepositoryImpl) + } + + @Provides + fun provideAppInstallerUseCase( + cacheRepositoryImpl: CacheRepositoryImpl + ): AppInstallerUseCase { + return AppInstallerUseCase(cacheRepositoryImpl) + } } diff --git a/app/src/main/java/foundation/e/apps/domain/common/repository/CacheRepository.kt b/app/src/main/java/foundation/e/apps/domain/common/repository/CacheRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..ef35d2d6344fbf0672159cad22d8efafa80e21eb --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/common/repository/CacheRepository.kt @@ -0,0 +1,29 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.domain.common.repository + +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.data.enums.User + +interface CacheRepository { + fun currentUser(): User + fun cachedAuthData(): AuthData + fun resetCachedData() + fun clearAuthData() +} diff --git a/app/src/main/java/foundation/e/apps/domain/common/repository/CacheRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/domain/common/repository/CacheRepositoryImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..dab4d730b759a4bc6eccddd8833329f02a5e1afc --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/common/repository/CacheRepositoryImpl.kt @@ -0,0 +1,61 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.domain.common.repository + +import android.content.Context +import app.lounge.storage.cache.configurations +import com.aurora.gplayapi.data.models.AuthData +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.data.enums.User +import foundation.e.apps.utils.toAuthData +import javax.inject.Inject + +class CacheRepositoryImpl @Inject constructor( + @ApplicationContext val applicationContext: Context +) : CacheRepository { + + override fun currentUser(): User { + return applicationContext.configurations.userType.takeIf { it.isNotEmpty() } + ?.let { User.getUser(it) } + ?: run { User.UNAVAILABLE } + } + + override fun resetCachedData() { + applicationContext.configurations.apply { + authData = "" + userType = User.UNAVAILABLE.name + email = "" + oauthtoken = "" + showAllApplications = true + showFOSSApplications = true + showPWAApplications = true + // TODO: reset access token for Google login. It is not defined yet. + } + } + + override fun cachedAuthData(): AuthData = + applicationContext.configurations.authData.let { data -> + if (data.isEmpty()) throw RuntimeException("Auth Data not available") + return data.toAuthData() + } + + override fun clearAuthData() { + applicationContext.configurations.authData = "" + } +} diff --git a/app/src/main/java/foundation/e/apps/domain/errors/RetryMechanism.kt b/app/src/main/java/foundation/e/apps/domain/errors/RetryMechanism.kt new file mode 100644 index 0000000000000000000000000000000000000000..593131043387b01fd01c524d61aecc9b83032b76 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/errors/RetryMechanism.kt @@ -0,0 +1,67 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.domain.errors + +class RetryMechanism { + + private var autoRetryCount = 0 + + /** + * Wrap the code to code to retry execution. + */ + fun wrapWithRetry( + retryBlock: () -> Unit, + retryFailureBlock: () -> Unit + ) { + if (!retryEvaluator(retryBlock)) { + retryFailureBlock() + } + } + + /** + * Example of where this function can be called: + * - If user presses "Log out" or "Retry" from an error dialog. + */ + fun resetRetryCondition() { + autoRetryCount = 0 + } + + /** + * The actual block to do multiple retries. + * We can do some fancy stuff like exponential back-off using recursions. + * + * @return true if retry conditions have not expired, false otherwise. + */ + private fun retryEvaluator( + retryBlock: () -> Unit + ): Boolean { + if (shouldFailRetry()) return false + retryBlock() + incrementAutoRetryCounter() + return true + } + + private fun incrementAutoRetryCounter() { + autoRetryCount++ + } + + private fun shouldFailRetry(): Boolean { + return autoRetryCount > 0 + } +} diff --git a/app/src/main/java/foundation/e/apps/domain/install/usecase/AppInstallerUseCase.kt b/app/src/main/java/foundation/e/apps/domain/install/usecase/AppInstallerUseCase.kt new file mode 100644 index 0000000000000000000000000000000000000000..b7a41ec1fb9543c2f38053df7b6ca6a10d094e84 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/install/usecase/AppInstallerUseCase.kt @@ -0,0 +1,44 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.domain.install.usecase + +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.data.enums.User +import foundation.e.apps.domain.common.repository.CacheRepository +import java.util.Locale +import javax.inject.Inject + +class AppInstallerUseCase@Inject constructor( + private val cacheRepository: CacheRepository +) { + + fun currentAuthData(): AuthData? { + return try { + cacheRepository.cachedAuthData() + } catch (e: RuntimeException) { + if (cacheRepository.currentUser() == User.NO_GOOGLE) { + return AuthData("", "").apply { + this.isAnonymous = false + this.locale = Locale.getDefault() + } + } + return null + } + } +} diff --git a/app/src/main/java/foundation/e/apps/domain/login/repository/LoginRepository.kt b/app/src/main/java/foundation/e/apps/domain/login/repository/LoginRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..823ab9ead115f34abdda0e7f11b0260d50169867 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/login/repository/LoginRepository.kt @@ -0,0 +1,26 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.domain.login.repository + +import com.aurora.gplayapi.data.models.AuthData + +interface LoginRepository { + suspend fun anonymousUser(): AuthData + suspend fun googleUser(authData: AuthData, oauthToken: String) +} diff --git a/app/src/main/java/foundation/e/apps/domain/login/repository/LoginRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/domain/login/repository/LoginRepositoryImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..2ffe42ef7b2df8d41bcfc8b3bcd586e7ebecf144 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/login/repository/LoginRepositoryImpl.kt @@ -0,0 +1,69 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.domain.login.repository + +import android.content.Context +import app.lounge.login.anonymous.AnonymousUser +import app.lounge.model.AnonymousAuthDataRequestBody +import app.lounge.networking.NetworkResult +import app.lounge.storage.cache.configurations +import com.aurora.gplayapi.data.models.AuthData +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.data.enums.User +import foundation.e.apps.utils.SystemInfoProvider +import foundation.e.apps.utils.toJsonString +import java.util.Properties +import javax.inject.Inject + +class LoginRepositoryImpl @Inject constructor( + @ApplicationContext val applicationContext: Context, + private val properties: Properties, + private val anonymousUser: AnonymousUser +) : LoginRepository { + + private val userAgent: String by lazy { SystemInfoProvider.getAppBuildInfo() } + + override suspend fun anonymousUser(): AuthData { + val result = anonymousUser.requestAuthData( + anonymousAuthDataRequestBody = AnonymousAuthDataRequestBody( + properties = properties, + userAgent = userAgent + ) + ) + + when (result) { + is NetworkResult.Error -> + throw Exception(result.errorMessage, result.exception) + is NetworkResult.Success -> { + val authData = result.data + applicationContext.configurations.authData = authData.toJsonString() + applicationContext.configurations.userType = User.ANONYMOUS.name + return authData + } + } + } + + // TODO: Remove function parameter once we refactor Google User APIs + override suspend fun googleUser(authData: AuthData, oauthToken: String) { + applicationContext.configurations.authData = authData.toJsonString() + applicationContext.configurations.email = authData.email + applicationContext.configurations.oauthtoken = oauthToken + applicationContext.configurations.userType = User.GOOGLE.name + } +} diff --git a/app/src/main/java/foundation/e/apps/domain/login/usecase/NoGoogleModeUseCase.kt b/app/src/main/java/foundation/e/apps/domain/login/usecase/NoGoogleModeUseCase.kt new file mode 100644 index 0000000000000000000000000000000000000000..f975f83061dba1a63dc4265f7dbb92d2aac21586 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/login/usecase/NoGoogleModeUseCase.kt @@ -0,0 +1,44 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.domain.login.usecase + +import android.content.Context +import app.lounge.storage.cache.configurations +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.enums.User +import foundation.e.apps.data.login.AuthObject +import javax.inject.Inject + +class NoGoogleModeUseCase @Inject constructor(@ApplicationContext private val context: Context) { + fun performNoGoogleLogin(): AuthObject { + context.configurations.userType = User.NO_GOOGLE.toString() + context.configurations.showAllApplications = false + context.configurations.showFOSSApplications = true + context.configurations.showPWAApplications = true + return getAuthObject() + } + + private fun getAuthObject(): AuthObject.CleanApk { + return AuthObject.CleanApk( + ResultSupreme.Success(Unit), + User.NO_GOOGLE + ) + } +} diff --git a/app/src/main/java/foundation/e/apps/domain/login/usecase/UserLoginUseCase.kt b/app/src/main/java/foundation/e/apps/domain/login/usecase/UserLoginUseCase.kt new file mode 100644 index 0000000000000000000000000000000000000000..0f920d8869032613f3c9b30f531eedc1bd9d550c --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/login/usecase/UserLoginUseCase.kt @@ -0,0 +1,93 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.domain.login.usecase + +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.domain.common.repository.CacheRepository +import foundation.e.apps.domain.login.repository.LoginRepository +import foundation.e.apps.utils.Resource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +class UserLoginUseCase @Inject constructor( + private val loginRepository: LoginRepository, + private val cacheRepository: CacheRepository +) { + + fun anonymousUser(): Flow> = flow { + kotlin.runCatching { + emit(Resource.Loading()) + emit(Resource.Success(loginRepository.anonymousUser())) + }.onFailure { failure -> emit(Resource.Error(failure.localizedMessage)) } + } + + fun googleUser(authData: AuthData, token: String): Flow> = flow { + kotlin.runCatching { + emit( + Resource.Success( + loginRepository.googleUser( + authData = authData, + oauthToken = token + ) + ) + ) + }.onFailure { failure -> emit(Resource.Error(failure.localizedMessage)) } + } + + fun retrieveCachedAuthData(): Flow> = flow { + kotlin.runCatching { + emit(Resource.Loading()) + emit(Resource.Success(cacheRepository.cachedAuthData())) + }.onFailure { failure -> emit(Resource.Error(failure.localizedMessage)) } + } + + fun logoutUser() { + cacheRepository.resetCachedData() + } + + fun currentUser() = cacheRepository.currentUser() + + fun clearAuthData() = cacheRepository.clearAuthData() + + fun performAnonymousUserAuthentication(): Flow> = flow { + anonymousUser().onEach { anonymousAuth -> + // TODO -> If we are not using auth data then + when (anonymousAuth) { + is Resource.Error -> emit(Resource.Error(anonymousAuth.message ?: "An unexpected error occured")) + is Resource.Loading -> emit(Resource.Loading()) + is Resource.Success -> { + retrieveCachedAuthData().onEach { + when (it) { + is Resource.Error -> { + emit(Resource.Error(anonymousAuth.message ?: "An unexpected error occured")) + } + is Resource.Loading -> emit(Resource.Loading()) + is Resource.Success -> { + emit(Resource.Success(cacheRepository.cachedAuthData())) + } + } + }.collect() + } + } + }.collect() + } +} diff --git a/app/src/main/java/foundation/e/apps/domain/main/usecase/MainActivityUseCase.kt b/app/src/main/java/foundation/e/apps/domain/main/usecase/MainActivityUseCase.kt new file mode 100644 index 0000000000000000000000000000000000000000..348c5bb61c3c5869675ef4fd1573a628dea35e73 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/main/usecase/MainActivityUseCase.kt @@ -0,0 +1,37 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.domain.main.usecase + +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.domain.common.repository.CacheRepository +import javax.inject.Inject + +class MainActivityUseCase @Inject constructor( + private val cacheRepository: CacheRepository +) { + fun currentUser() = cacheRepository.currentUser() + + fun currentAuthData(): AuthData { + return try { + cacheRepository.cachedAuthData() + } catch (e: RuntimeException) { + AuthData("", "") + } + } +} diff --git a/app/src/main/java/foundation/e/apps/domain/settings/usecase/SettingsUseCase.kt b/app/src/main/java/foundation/e/apps/domain/settings/usecase/SettingsUseCase.kt new file mode 100644 index 0000000000000000000000000000000000000000..3a3e3c057d14ccea4d6c774cdb1c08a7246ae61e --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/settings/usecase/SettingsUseCase.kt @@ -0,0 +1,51 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.domain.settings.usecase + +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.data.enums.User +import foundation.e.apps.domain.common.repository.CacheRepository +import foundation.e.apps.utils.Resource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class SettingsUseCase @Inject constructor( + private val cacheRepository: CacheRepository +) { + fun currentUser(): Flow> = flow { + kotlin.runCatching { + val currentUser = cacheRepository.currentUser() + emit(Resource.Success(currentUser)) + }.onFailure { emit(Resource.Error("Something went wrong in fun currentUser()")) } + } + + fun currentAuthData(): Flow> = flow { + kotlin.runCatching { + emit(Resource.Success(cacheRepository.cachedAuthData())) + }.onFailure { emit(Resource.Error("Something went wrong in fun currentUser()")) } + } + + fun logoutUser(): Flow> = flow { + runCatching { + cacheRepository.resetCachedData() + emit(Resource.Success(Unit)) + }.onFailure { emit(Resource.Error("Error during logout")) } + } +} diff --git a/app/src/main/java/foundation/e/apps/domain/updates/usecase/UpdatesUseCase.kt b/app/src/main/java/foundation/e/apps/domain/updates/usecase/UpdatesUseCase.kt new file mode 100644 index 0000000000000000000000000000000000000000..d26d6718a2c837a6ee066a178ed1a35a64228485 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/updates/usecase/UpdatesUseCase.kt @@ -0,0 +1,30 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.domain.updates.usecase + +import foundation.e.apps.domain.common.repository.CacheRepository +import javax.inject.Inject + +class UpdatesUseCase @Inject constructor( + private val cacheRepository: CacheRepository +) { + fun currentUser() = cacheRepository.currentUser() + + fun currentAuthData() = cacheRepository.cachedAuthData() +} diff --git a/app/src/main/java/foundation/e/apps/install/download/DownloadManagerUtils.kt b/app/src/main/java/foundation/e/apps/install/download/DownloadManagerUtils.kt index 1438ad18f5a4e1b96ea9ed0cf0e961102c46a357..7a15e92a736c57049f721428540283070d754341 100644 --- a/app/src/main/java/foundation/e/apps/install/download/DownloadManagerUtils.kt +++ b/app/src/main/java/foundation/e/apps/install/download/DownloadManagerUtils.kt @@ -44,7 +44,7 @@ class DownloadManagerUtils @Inject constructor( @ApplicationContext private val context: Context, private val fusedManagerRepository: FusedManagerRepository, private val downloadManager: DownloadManager, - private val storageNotificationManager: StorageNotificationManager, + private val storageNotificationManager: StorageNotificationManager ) { private val mutex = Mutex() @@ -107,7 +107,8 @@ class DownloadManagerUtils @Inject constructor( fusedDownload: FusedDownload, downloadId: Long ) = downloadManager.isDownloadSuccessful(downloadId) && areAllFilesDownloaded( - numberOfDownloadedItems, fusedDownload + numberOfDownloadedItems, + fusedDownload ) && checkCleanApkSignatureOK(fusedDownload) private fun areAllFilesDownloaded( @@ -126,7 +127,8 @@ class DownloadManagerUtils @Inject constructor( private suspend fun checkCleanApkSignatureOK(fusedDownload: FusedDownload): Boolean { if (fusedDownload.origin != Origin.CLEANAPK || fusedManagerRepository.isFdroidApplicationSigned( - context, fusedDownload + context, + fusedDownload ) ) { Timber.d("Apk signature is OK") diff --git a/app/src/main/java/foundation/e/apps/install/notification/StorageNotificationManager.kt b/app/src/main/java/foundation/e/apps/install/notification/StorageNotificationManager.kt index be74895ea1fe501207d33b01a5585214bcbaac14..3018337b3f5feab20b639adf586911ff50686473 100644 --- a/app/src/main/java/foundation/e/apps/install/notification/StorageNotificationManager.kt +++ b/app/src/main/java/foundation/e/apps/install/notification/StorageNotificationManager.kt @@ -35,14 +35,13 @@ import javax.inject.Inject class StorageNotificationManager @Inject constructor( @ApplicationContext private val context: Context, - private val downloadManager: DownloadManager, + private val downloadManager: DownloadManager ) { companion object { const val NOT_ENOUGH_SPACE_NOTIFICATION_ID = 7874 } fun showNotEnoughSpaceNotification(fusedDownload: FusedDownload, downloadId: Long? = null) { - with(NotificationManagerCompat.from(context)) { if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED diff --git a/app/src/main/java/foundation/e/apps/install/pkg/PWAManagerModule.kt b/app/src/main/java/foundation/e/apps/install/pkg/PWAManagerModule.kt index 56e27c74fbba7a8ea293f45c986c2ee225e959fc..857f81d2204f034958a87269d4476752fd179c0b 100644 --- a/app/src/main/java/foundation/e/apps/install/pkg/PWAManagerModule.kt +++ b/app/src/main/java/foundation/e/apps/install/pkg/PWAManagerModule.kt @@ -28,7 +28,7 @@ import javax.inject.Singleton @OpenForTesting class PWAManagerModule @Inject constructor( @ApplicationContext private val context: Context, - private val fusedDownloadRepository: FusedDownloadRepository, + private val fusedDownloadRepository: FusedDownloadRepository ) { companion object { @@ -60,7 +60,10 @@ class PWAManagerModule @Inject constructor( fun getPwaStatus(fusedApp: FusedApp): Status { context.contentResolver.query( Uri.parse(PWA_PLAYER), - null, null, null, null + null, + null, + null, + null )?.let { cursor -> if (cursor.count > 0) { if (cursor.moveToFirst()) { diff --git a/app/src/main/java/foundation/e/apps/install/pkg/PkgManagerModule.kt b/app/src/main/java/foundation/e/apps/install/pkg/PkgManagerModule.kt index 91e0d10d1358e4906f10a71d98f4da055f98d86c..99601a753cb106f4279b27d580b845487b83ffe5 100644 --- a/app/src/main/java/foundation/e/apps/install/pkg/PkgManagerModule.kt +++ b/app/src/main/java/foundation/e/apps/install/pkg/PkgManagerModule.kt @@ -165,7 +165,6 @@ class PkgManagerModule @Inject constructor( */ @OptIn(DelicateCoroutinesApi::class) fun installApplication(list: List, packageName: String) { - val sessionId = createInstallSession(packageName, SessionParams.MODE_FULL_INSTALL) val session = packageManager.packageInstaller.openSession(sessionId) @@ -178,9 +177,11 @@ class PkgManagerModule @Inject constructor( val callBackIntent = Intent(context, InstallerService::class.java) callBackIntent.putExtra(PACKAGE_NAME, packageName) - val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE else + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + } else { PendingIntent.FLAG_UPDATE_CURRENT + } val servicePendingIntent = PendingIntent.getService( context, sessionId, @@ -205,7 +206,6 @@ class PkgManagerModule @Inject constructor( } private fun createInstallSession(packageName: String, mode: Int): Int { - val packageInstaller = packageManager.packageInstaller val params = SessionParams(mode).apply { setAppPackageName(packageName) @@ -222,7 +222,6 @@ class PkgManagerModule @Inject constructor( } private fun syncFile(session: Session, file: File) { - val inputStream = file.inputStream() val outputStream = session.openWrite(file.nameWithoutExtension, 0, -1) inputStream.copyTo(outputStream) diff --git a/app/src/main/java/foundation/e/apps/install/splitinstall/SplitInstallBinder.kt b/app/src/main/java/foundation/e/apps/install/splitinstall/SplitInstallBinder.kt index 60c33aacecc71bee202182fd01e56c0c9d918561..de4ee6ae58949efe3b6cb74ecb34b49c950a2332 100644 --- a/app/src/main/java/foundation/e/apps/install/splitinstall/SplitInstallBinder.kt +++ b/app/src/main/java/foundation/e/apps/install/splitinstall/SplitInstallBinder.kt @@ -72,7 +72,9 @@ class SplitInstallBinder( } downloadManager.downloadFileInExternalStorage( - url, packageName, "$packageName.split.$moduleName.apk" + url, + packageName, + "$packageName.split.$moduleName.apk" ) { success, path -> if (success) { Timber.i("Split module has been downloaded: $path") diff --git a/app/src/main/java/foundation/e/apps/install/splitinstall/SplitInstallService.kt b/app/src/main/java/foundation/e/apps/install/splitinstall/SplitInstallService.kt index ac561e3e53ca7f1404945c6ceaca2f75b20238f7..fff0a1a0dbfd1a6f3d15b0e6ab5a506ac049c3db 100644 --- a/app/src/main/java/foundation/e/apps/install/splitinstall/SplitInstallService.kt +++ b/app/src/main/java/foundation/e/apps/install/splitinstall/SplitInstallService.kt @@ -43,8 +43,11 @@ class SplitInstallService : LifecycleService() { } @Inject lateinit var dataStoreModule: DataStoreModule + @Inject lateinit var fusedAPIRepository: FusedAPIRepository + @Inject lateinit var downloadManager: DownloadManager + @Inject lateinit var gson: Gson private lateinit var binder: SplitInstallBinder private var authData: AuthData? = null diff --git a/app/src/main/java/foundation/e/apps/install/updates/UpdatesNotifier.kt b/app/src/main/java/foundation/e/apps/install/updates/UpdatesNotifier.kt index 63eafed676847ffe53f5c36d68a79a3eafbce215..6dd70f1eccd8fe287f61b36b9525a4029c8e7d8d 100644 --- a/app/src/main/java/foundation/e/apps/install/updates/UpdatesNotifier.kt +++ b/app/src/main/java/foundation/e/apps/install/updates/UpdatesNotifier.kt @@ -150,7 +150,7 @@ object UpdatesNotifier { fun showNotification( context: Context, title: String, - message: String, + message: String ) { with(NotificationManagerCompat.from(context)) { createNotificationChannel(context) diff --git a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorkManager.kt b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorkManager.kt index fa41365edc1f4379d44ce68aa7fb68a0db1c4803..b3b794c88da0f4772ee1070917f203c6260480a0 100644 --- a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorkManager.kt +++ b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorkManager.kt @@ -75,7 +75,8 @@ object UpdatesWorkManager { Timber.i("UpdatesWorker interval: $interval hours") WorkManager.getInstance(context).enqueueUniquePeriodicWork( UPDATES_WORK_NAME, - existingPeriodicWorkPolicy, buildPeriodicWorkRequest(interval) + existingPeriodicWorkPolicy, + buildPeriodicWorkRequest(interval) ) Timber.i("UpdatesWorker started") } diff --git a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt index 1f7804cb6bb6a43fc82d04019938cb27bbde3cd2..e2eb0bcb17534a3f78a95454f81a2e378a53c2c9 100644 --- a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt +++ b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt @@ -23,6 +23,7 @@ import foundation.e.apps.data.fused.data.FusedApp import foundation.e.apps.data.login.LoginSourceRepository import foundation.e.apps.data.preference.DataStoreManager import foundation.e.apps.data.updates.UpdatesManagerRepository +import foundation.e.apps.domain.updates.usecase.UpdatesUseCase import foundation.e.apps.install.workmanager.AppInstallProcessor import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus @@ -38,8 +39,9 @@ class UpdatesWorker @AssistedInject constructor( private val updatesManagerRepository: UpdatesManagerRepository, private val dataStoreManager: DataStoreManager, private val loginSourceRepository: LoginSourceRepository, - private val appInstallProcessor: AppInstallProcessor, private val blockedAppRepository: BlockedAppRepository, + private val appInstallProcessor: AppInstallProcessor, + private val updatesUseCase: UpdatesUseCase ) : CoroutineWorker(context, params) { companion object { @@ -54,6 +56,9 @@ class UpdatesWorker @AssistedInject constructor( private var isAutoUpdate = true // indicates it is auto update or user initiated update private var retryCount = 0 + private val authData: AuthData + get() = updatesUseCase.currentAuthData() ?: AuthData("", "") + override suspend fun doWork(): Result { return try { isAutoUpdate = params.inputData.getBoolean(IS_AUTO_UPDATE, true) @@ -98,7 +103,7 @@ class UpdatesWorker @AssistedInject constructor( } private fun getUser(): User { - return dataStoreManager.getUserType() + return updatesUseCase.currentUser() } private suspend fun checkForUpdates() { @@ -106,10 +111,9 @@ class UpdatesWorker @AssistedInject constructor( val isConnectedToUnMeteredNetwork = isConnectedToUnMeteredNetwork(applicationContext) val appsNeededToUpdate = mutableListOf() val user = getUser() - val authData = loginSourceRepository.getValidatedAuthData().data val resultStatus: ResultStatus - if (user in listOf(User.ANONYMOUS, User.GOOGLE) && authData != null) { + if (user in listOf(User.ANONYMOUS, User.GOOGLE)) { /* * Signifies valid Google user and valid auth data to update * apps from Google Play store. @@ -157,7 +161,7 @@ class UpdatesWorker @AssistedInject constructor( * in appsNeededToUpdate list. Hence it is safe to proceed with * blank AuthData. */ - authData ?: AuthData("", ""), + authData ?: AuthData("", "") ) } } diff --git a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt index e17278ddeaf4172adc65630160b1e375d22043e9..d83edee247818dd2883ed0c6952be4f37a09e7a7 100644 --- a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt @@ -32,7 +32,7 @@ import foundation.e.apps.data.fused.data.FusedApp import foundation.e.apps.data.fusedDownload.FusedDownloadRepository import foundation.e.apps.data.fusedDownload.FusedManagerRepository import foundation.e.apps.data.fusedDownload.models.FusedDownload -import foundation.e.apps.data.preference.DataStoreManager +import foundation.e.apps.domain.install.usecase.AppInstallerUseCase import foundation.e.apps.install.notification.StorageNotificationManager import foundation.e.apps.install.updates.UpdatesNotifier import foundation.e.apps.utils.StorageComputer @@ -44,6 +44,7 @@ import kotlinx.coroutines.flow.transformWhile import timber.log.Timber import java.text.NumberFormat import java.util.Date +import java.util.Locale import javax.inject.Inject class AppInstallProcessor @Inject constructor( @@ -51,7 +52,7 @@ class AppInstallProcessor @Inject constructor( private val fusedDownloadRepository: FusedDownloadRepository, private val fusedManagerRepository: FusedManagerRepository, private val fusedAPIRepository: FusedAPIRepository, - private val dataStoreManager: DataStoreManager, + private val appInstallerUseCase: AppInstallerUseCase, private val storageNotificationManager: StorageNotificationManager ) { @@ -108,8 +109,8 @@ class AppInstallProcessor @Inject constructor( isAnUpdate: Boolean = false ) { try { - val authData = dataStoreManager.getAuthData() - if (!fusedDownload.isFree && authData.isAnonymous) { + val authData = appInstallerUseCase.currentAuthData() + if (!fusedDownload.isFree && authData?.isAnonymous == true) { EventBus.invokeEvent(AppEvent.ErrorMessageEvent(R.string.paid_app_anonymous_message)) return } @@ -163,7 +164,8 @@ class AppInstallProcessor @Inject constructor( EventBus.invokeEvent( AppEvent.UpdateEvent( ResultSupreme.WorkError( - ResultStatus.UNKNOWN, fusedDownload + ResultStatus.UNKNOWN, + fusedDownload ) ) ) @@ -176,7 +178,8 @@ class AppInstallProcessor @Inject constructor( fusedDownload: FusedDownload ) { fusedAPIRepository.updateFusedDownloadWithDownloadingInfo( - fusedDownload.origin, fusedDownload + fusedDownload.origin, + fusedDownload ) } @@ -193,9 +196,8 @@ class AppInstallProcessor @Inject constructor( Timber.i(">>> dowork started for Fused download name " + fusedDownload?.name + " " + fusedDownloadId) fusedDownload?.let { - - this.isItUpdateWork = - isItUpdateWork && fusedManagerRepository.isFusedDownloadInstalled(fusedDownload) + this.isItUpdateWork = isItUpdateWork && + fusedManagerRepository.isFusedDownloadInstalled(fusedDownload) if (!fusedDownload.isAppInstalling()) { Timber.d("!!! returned") @@ -257,7 +259,8 @@ class AppInstallProcessor @Inject constructor( private suspend fun isUpdateCompleted(): Boolean { val downloadListWithoutAnyIssue = fusedDownloadRepository.getDownloadList().filter { !listOf( - Status.INSTALLATION_ISSUE, Status.PURCHASE_NEEDED + Status.INSTALLATION_ISSUE, + Status.PURCHASE_NEEDED ).contains(it.status) } @@ -265,16 +268,19 @@ class AppInstallProcessor @Inject constructor( } private fun showNotificationOnUpdateEnded() { - val locale = dataStoreManager.getAuthData().locale + val locale = appInstallerUseCase.currentAuthData()?.locale ?: Locale.getDefault() val date = Date().getFormattedString(DATE_FORMAT, locale) val numberOfUpdatedApps = NumberFormat.getNumberInstance(locale).format(UpdatesDao.successfulUpdatedApps.size) .toString() UpdatesNotifier.showNotification( - context, context.getString(R.string.update), + context, + context.getString(R.string.update), context.getString( - R.string.message_last_update_triggered, numberOfUpdatedApps, date + R.string.message_last_update_triggered, + numberOfUpdatedApps, + date ) ) } @@ -350,7 +356,8 @@ class AppInstallProcessor @Inject constructor( else -> { Timber.wtf( - TAG, "===> ${fusedDownload.name} is in wrong state ${fusedDownload.status}" + TAG, + "===> ${fusedDownload.name} is in wrong state ${fusedDownload.status}" ) finishInstallation(fusedDownload) } diff --git a/app/src/main/java/foundation/e/apps/presentation/login/LoginState.kt b/app/src/main/java/foundation/e/apps/presentation/login/LoginState.kt new file mode 100644 index 0000000000000000000000000000000000000000..d7ca3bd9ba635482a85a0d38e3f010d6f2e26b23 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/presentation/login/LoginState.kt @@ -0,0 +1,30 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.presentation.login + +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.data.enums.User + +data class LoginState( + val isLoading: Boolean = false, + val isLoggedIn: Boolean = false, + val error: String = "", + val authData: AuthData? = null, + val user: User = User.UNAVAILABLE +) diff --git a/app/src/main/java/foundation/e/apps/presentation/login/LoginViewModel.kt b/app/src/main/java/foundation/e/apps/presentation/login/LoginViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..31c92c2da7e43683bb4aa8ac547bbe7dab06991b --- /dev/null +++ b/app/src/main/java/foundation/e/apps/presentation/login/LoginViewModel.kt @@ -0,0 +1,271 @@ +/* + * 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.presentation.login + +import androidx.lifecycle.LiveData +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.data.Constants +import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.enums.User +import foundation.e.apps.data.enums.User.NO_GOOGLE +import foundation.e.apps.data.login.AuthObject +import foundation.e.apps.data.login.LoginSourceRepository +import foundation.e.apps.domain.login.usecase.NoGoogleModeUseCase +import foundation.e.apps.domain.login.usecase.UserLoginUseCase +import foundation.e.apps.ui.parentFragment.LoadingViewModel +import foundation.e.apps.utils.Resource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +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, + private val userLoginUseCase: UserLoginUseCase, + private val noGoogleModeUseCase: NoGoogleModeUseCase +) : 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) + if (authObjectsLocal.isNotEmpty()) { + _loginState.postValue(LoginState(isLoggedIn = authObjectsLocal[0].result.isSuccess())) + } + } + } + + /** + * 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) + val authObjectsLocal = loginSourceRepository.getAuthObjects(listOf()) + authObjects.postValue(authObjectsLocal) + + if (authObjectsLocal.isNotEmpty() && + authObjectsLocal[0] is AuthObject.GPlayAuth + ) { + val authObject = authObjectsLocal[0] as AuthObject.GPlayAuth + authObject.result.data?.let { authData -> + userLoginUseCase.googleUser(authData, oauthToken).collect() + _loginState.value = LoginState(isLoading = false, isLoggedIn = true) + } ?: kotlin.run { + _loginState.value = + LoginState( + isLoading = false, + isLoggedIn = false, + error = Constants.GOOGLE_LOGIN_FAIL + ) + } + } else { + _loginState.value = + LoginState( + isLoading = false, + isLoggedIn = false, + error = Constants.GOOGLE_LOGIN_FAIL + ) + } + + onUserSaved() + } + } + + /** + * Call this to use No-Google mode, i.e. show only PWAs and F-droid apps, + * without contacting Google servers. + * 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 initialNoGoogleLogin(onUserSaved: () -> Unit) { + viewModelScope.launch { + loginSourceRepository.setNoGoogleMode() + val authObject = noGoogleModeUseCase.performNoGoogleLogin() + _loginState.value = LoginState(isLoading = false, isLoggedIn = true) + authObjects.postValue(listOf(authObject)) + onUserSaved() + } + } + + /** + * Once an AuthObject is marked as invalid, it will be refreshed + * automatically by LoadingViewModel. + * If GPlay auth is invalid, [LoadingViewModel.onLoadData] has a retry block, + * this block will clear existing GPlay AuthData and freshly start the login flow. + */ + fun markInvalidAuthObject(authObjectName: String) { + val authObjectsLocal = authObjects.value?.toMutableList() + val invalidObject = authObjectsLocal?.find { it::class.java.simpleName == authObjectName } + + val replacedObject = invalidObject?.createInvalidAuthObject() + + authObjectsLocal?.apply { + if (invalidObject != null && replacedObject != null) { + remove(invalidObject) + add(replacedObject) + } + } + + authObjects.postValue(authObjectsLocal) + } + + /** + * Clears all saved data and logs out the user to the sign in screen. + */ + fun logout() { + viewModelScope.launch { + loginSourceRepository.logout() + authObjects.postValue(listOf()) + } + userLoginUseCase.logoutUser() + _loginState.value = LoginState() + } + + fun currentUser(): User { + return userLoginUseCase.currentUser() + } + + private val _loginState: MutableLiveData = MutableLiveData() + val loginState: LiveData = _loginState + + fun authenticateAnonymousUser() { + viewModelScope.launch(Dispatchers.IO) { + userLoginUseCase.performAnonymousUserAuthentication().onEach { result -> + withContext(Dispatchers.Main) { + when (result) { + is Resource.Success -> { + result.data?.let { updateSavedAuthData() } + } + + is Resource.Error -> { + _loginState.value = LoginState( + error = result.message ?: Constants.UNEXPECTED_ERROR + ) + } + + is Resource.Loading -> { + _loginState.value = LoginState(isLoading = true) + } + } + } + }.collect() + } + } + + fun checkLogin() { + viewModelScope.launch { + val user = currentUser() + if (user == NO_GOOGLE) { + _loginState.value = + LoginState(isLoggedIn = true, authData = null, user = user) + } else { + updateSavedAuthData() + } + } + } + + private suspend fun updateSavedAuthData() { + userLoginUseCase.retrieveCachedAuthData().onEach { + when (it) { + is Resource.Error -> { + val error = it.message.let { message -> + when (message) { + null -> Constants.UNEXPECTED_ERROR + else -> message + } + } + _loginState.value = LoginState(error = error) + } + is Resource.Loading -> _loginState.value = LoginState(isLoading = true) + is Resource.Success -> { + // TODO + it.data?.let { authData -> updateAuthObjectForAnonymousUser(authData) } + _loginState.value = + LoginState(isLoggedIn = true, authData = it.data, user = User.ANONYMOUS) + } + } + }.collect() + } + + fun updateAuthObjectForAnonymousUser(authData: AuthData) { + // TODO : Refine after Google User API is refactored. + loginSourceRepository.gplayAuth = authData + + authObjects.postValue( + listOf( + AuthObject.GPlayAuth(ResultSupreme.Success(authData), User.ANONYMOUS) + ) + ) + } + + fun getNewToken() { + userLoginUseCase.clearAuthData() + when (currentUser()) { + User.NO_GOOGLE -> {} + User.UNAVAILABLE -> {} + User.ANONYMOUS -> authenticateAnonymousUser() + User.GOOGLE -> { + // TODO + } + } + } +} diff --git a/app/src/main/java/foundation/e/apps/presentation/settings/SettingsState.kt b/app/src/main/java/foundation/e/apps/presentation/settings/SettingsState.kt new file mode 100644 index 0000000000000000000000000000000000000000..a7da9bfc003059faf14a264e2930b5559e7f38a7 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/presentation/settings/SettingsState.kt @@ -0,0 +1,30 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.presentation.settings + +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.data.enums.User + +var settingUserState = SettingUserState() + +data class SettingUserState( + var error: String = "", + var authData: AuthData? = null, + var user: User = User.UNAVAILABLE +) diff --git a/app/src/main/java/foundation/e/apps/presentation/settings/SettingsViewModel.kt b/app/src/main/java/foundation/e/apps/presentation/settings/SettingsViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..36dcbffe0135c728b567ebd67c609e6c0b7e338f --- /dev/null +++ b/app/src/main/java/foundation/e/apps/presentation/settings/SettingsViewModel.kt @@ -0,0 +1,90 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.presentation.settings + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import foundation.e.apps.data.Constants +import foundation.e.apps.domain.settings.usecase.SettingsUseCase +import foundation.e.apps.utils.Resource +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SettingsViewModel @Inject constructor( + private val settingsUseCase: SettingsUseCase +) : ViewModel() { + + private val _currentUserState: MutableLiveData = MutableLiveData() + val currentUserState: LiveData = _currentUserState + + fun currentUser() { + viewModelScope.launch { + settingsUseCase.currentUser().onEach { result -> + when (result) { + is Resource.Success -> { + _currentUserState.value = + result.data?.let { settingUserState.apply { user = it } } + } + is Resource.Error -> { + _currentUserState.value = + settingUserState.apply { error = result.message ?: Constants.UNEXPECTED_ERROR } + } + + is Resource.Loading -> TODO() + } + }.collect() + } + } + + fun currentAuthData() { + viewModelScope.launch { + settingsUseCase.currentAuthData().onEach { result -> + when (result) { + is Resource.Success -> { + _currentUserState.value = + result.data?.let { settingUserState.apply { authData = it } } + } + is Resource.Error -> { + _currentUserState.value = + settingUserState.apply { error = result.message ?: Constants.UNEXPECTED_ERROR } + } + + is Resource.Loading -> { + // NO NEED + } + } + }.collect() + } + } + + fun logout() { + settingsUseCase.logoutUser().launchIn(viewModelScope) + } + + fun resetSettingState() { + settingUserState = SettingUserState() + } +} 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..519c14c21a7da07185d53b75ea8d0fc9e5fbd4d3 100644 --- a/app/src/main/java/foundation/e/apps/receivers/DumpAuthData.kt +++ b/app/src/main/java/foundation/e/apps/receivers/DumpAuthData.kt @@ -19,11 +19,11 @@ package foundation.e.apps.receivers import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import app.lounge.storage.cache.configurations import com.aurora.gplayapi.data.models.AuthData 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 org.json.JSONObject import timber.log.Timber @@ -47,9 +47,10 @@ 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 = context.configurations.authData.let { gson.fromJson(it, AuthData::class.java) - } + } ?: return "" + val filteredData = JSONObject().apply { put("email", authData.email) put("authToken", authData.authToken) diff --git a/app/src/main/java/foundation/e/apps/ui/AppInfoFetchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/AppInfoFetchViewModel.kt index 3cf70583f1b9a5fd1f37f9f6f4a8d57e548551a0..942535361593e0a65decbb06b0e69d967f884aa4 100644 --- a/app/src/main/java/foundation/e/apps/ui/AppInfoFetchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/AppInfoFetchViewModel.kt @@ -40,7 +40,7 @@ class AppInfoFetchViewModel @Inject constructor( gplayRepository.getDownloadInfo( app.package_name, app.latest_version_code, - app.offer_type, + app.offer_type ) app.isPurchased = true emit(true) 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 48e89be36c425c07aab3bd3cefa49ec9ff4be811..2bb64408a215b6bc9d418c4b011ffe0ca73be88a 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt @@ -46,6 +46,7 @@ import foundation.e.apps.data.fused.data.FusedApp import foundation.e.apps.data.fusedDownload.FusedManagerRepository import foundation.e.apps.data.fusedDownload.models.FusedDownload import foundation.e.apps.data.preference.DataStoreModule +import foundation.e.apps.domain.main.usecase.MainActivityUseCase import foundation.e.apps.install.pkg.PWAManagerModule import foundation.e.apps.install.pkg.PkgManagerModule import foundation.e.apps.install.workmanager.AppInstallProcessor @@ -64,7 +65,8 @@ class MainActivityViewModel @Inject constructor( private val pwaManagerModule: PWAManagerModule, private val ecloudRepository: EcloudRepository, private val blockedAppRepository: BlockedAppRepository, - private val appInstallProcessor: AppInstallProcessor + private val appInstallProcessor: AppInstallProcessor, + private val mainActivityUseCase: MainActivityUseCase ) : ViewModel() { val tocStatus: LiveData = dataStoreModule.tocStatus.asLiveData() @@ -74,7 +76,7 @@ class MainActivityViewModel @Inject constructor( val isAppPurchased: MutableLiveData = MutableLiveData() val purchaseDeclined: MutableLiveData = MutableLiveData() - var gPlayAuthData = AuthData("", "") + private val gPlayAuthData: AuthData by lazy { mainActivityUseCase.currentAuthData() } // Downloads val downloadList = fusedManagerRepository.getDownloadLiveList() @@ -91,7 +93,7 @@ class MainActivityViewModel @Inject constructor( } fun getUser(): User { - return dataStoreModule.getUserType() + return mainActivityUseCase.currentUser() } fun getUserEmail(): String { @@ -168,11 +170,10 @@ class MainActivityViewModel @Inject constructor( */ fun verifyUiFilter(fusedApp: FusedApp, method: () -> Unit) { viewModelScope.launch { - val authData = gPlayAuthData if (fusedApp.filterLevel.isInitialized()) { method() } else { - fusedAPIRepository.getAppFilterLevel(fusedApp, authData).run { + fusedAPIRepository.getAppFilterLevel(fusedApp, gPlayAuthData).run { if (isInitialized()) { fusedApp.filterLevel = this method() @@ -240,13 +241,13 @@ class MainActivityViewModel @Inject constructor( .build() private fun getNetworkCallback( - callbackFlowScope: ProducerScope, + callbackFlowScope: ProducerScope ): ConnectivityManager.NetworkCallback { return object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { super.onAvailable(network) - callbackFlowScope.sendInternetStatus(connectivityManager) + callbackFlowScope.trySend(true) } override fun onCapabilitiesChanged( @@ -259,14 +260,13 @@ class MainActivityViewModel @Inject constructor( override fun onLost(network: Network) { super.onLost(network) - callbackFlowScope.sendInternetStatus(connectivityManager) + callbackFlowScope.trySend(false) } } } // protected to avoid SyntheticAccessor protected fun ProducerScope.sendInternetStatus(connectivityManager: ConnectivityManager) { - val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) diff --git a/app/src/main/java/foundation/e/apps/ui/PrivacyInfoViewModel.kt b/app/src/main/java/foundation/e/apps/ui/PrivacyInfoViewModel.kt index fb7521da92272b03dbdf8fc930353dcb9c89ddaa..16e103aefffeba8e1ff45480b4116a099b8a4f3d 100644 --- a/app/src/main/java/foundation/e/apps/ui/PrivacyInfoViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/PrivacyInfoViewModel.kt @@ -17,7 +17,7 @@ import javax.inject.Inject @HiltViewModel class PrivacyInfoViewModel @Inject constructor( private val privacyInfoRepository: IAppPrivacyInfoRepository, - private val privacyScoreRepository: PrivacyScoreRepository, + private val privacyScoreRepository: PrivacyScoreRepository ) : ViewModel() { private val singularAppPrivacyInfoLiveData: MutableLiveData> = @@ -60,11 +60,13 @@ class PrivacyInfoViewModel @Inject constructor( } private fun handleAppPrivacyInfoResult( - appPrivacyPrivacyInfoResult: Result, + appPrivacyPrivacyInfoResult: Result ): Result { return if (!appPrivacyPrivacyInfoResult.isSuccess()) { Result.error("Tracker not found!") - } else appPrivacyPrivacyInfoResult + } else { + appPrivacyPrivacyInfoResult + } } fun getTrackerListText(fusedApp: FusedApp?): String { diff --git a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt index a71ef52638e86fe54b161c22e16fe6bc7242a49f..872733c911cf9870b33180f7bb9737979dc132a8 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt @@ -28,19 +28,21 @@ 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 import androidx.core.view.isVisible +import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController 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 @@ -54,19 +56,18 @@ import foundation.e.apps.data.enums.Status import foundation.e.apps.data.enums.User import foundation.e.apps.data.enums.isInitialized import foundation.e.apps.data.fused.data.FusedApp -import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.databinding.FragmentApplicationBinding import foundation.e.apps.di.CommonUtilsModule.LIST_OF_NULL import foundation.e.apps.install.download.data.DownloadProgress import foundation.e.apps.install.pkg.PWAManagerModule import foundation.e.apps.install.pkg.PkgManagerModule +import foundation.e.apps.presentation.login.LoginViewModel import foundation.e.apps.ui.AppInfoFetchViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.PrivacyInfoViewModel import foundation.e.apps.ui.application.model.ApplicationScreenshotsRVAdapter import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment -import foundation.e.apps.ui.parentFragment.TimeoutFragment +import foundation.e.apps.utils.loadDataOnce import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import timber.log.Timber @@ -74,7 +75,7 @@ import java.util.Locale import javax.inject.Inject @AndroidEntryPoint -class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { +class ApplicationFragment : Fragment(R.layout.fragment_application) { private val args: ApplicationFragmentArgs by navArgs() private val TAG = ApplicationFragment::class.java.simpleName @@ -120,7 +121,13 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { private val applicationViewModel: ApplicationViewModel by viewModels() private val privacyInfoViewModel: PrivacyInfoViewModel by viewModels() private val appInfoFetchViewModel: AppInfoFetchViewModel by viewModels() - override val mainActivityViewModel: MainActivityViewModel by activityViewModels() + private val mainActivityViewModel: MainActivityViewModel by activityViewModels() + + private val loginViewModel: LoginViewModel by lazy { + ViewModelProvider(requireActivity())[LoginViewModel::class.java] + } + + private var authData: AuthData? = null private var applicationIcon: ImageView? = null @@ -140,15 +147,13 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { super.onViewCreated(view, savedInstanceState) _binding = FragmentApplicationBinding.bind(view) - setupListening() - - authObjects.observe(viewLifecycleOwner) { - if (it == null) return@observe - loadDataWhenNetworkAvailable(it) - } - - applicationViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { - handleExceptionsCommon(it) + mainActivityViewModel.internetConnection.loadDataOnce(this) { + loginViewModel.loginState.observe(viewLifecycleOwner) { + if (it.isLoggedIn) { + this.authData = it.authData + loadData() + } + } } setupToolbar(view) @@ -167,7 +172,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } private fun updateUi( - resultPair: Pair, + resultPair: Pair ) { if (resultPair.second != ResultStatus.OK) { return @@ -294,7 +299,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } private fun updateAppInformation( - it: FusedApp, + it: FusedApp ) { binding.infoInclude.apply { appUpdatedOn.text = getString( @@ -322,11 +327,15 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { applicationViewModel.handleRatingFormat(it.ratings.usageQualityScore) appRating.text = getString( - R.string.rating_out_of, rating + R.string.rating_out_of, + rating ) appRating.setCompoundDrawablesWithIntrinsicBounds( - null, null, getRatingDrawable(rating), null + null, + null, + getRatingDrawable(rating), + null ) appRating.compoundDrawablePadding = 15 } @@ -447,7 +456,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } } - override fun loadData(authObjectList: List) { + private fun loadData() { if (isDetailsLoaded) return /* Show the loading bar. */ showLoadingUI() @@ -459,32 +468,8 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { packageName, origin, isFdroidDeepLink, - authObjectList - ) { - clearAndRestartGPlayLogin() - 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 + authData + ) } private fun observeDownloadStatus(view: View) { @@ -679,7 +664,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { positiveButtonAction = { installApplication(fusedApp, it) }, - cancelButtonText = getString(R.string.dialog_cancel), + cancelButtonText = getString(R.string.dialog_cancel) ).show(childFragmentManager, "ApplicationFragment") } } @@ -735,9 +720,11 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { ) { installButton.apply { enableInstallButton(R.string.not_available) - text = if (mainActivityViewModel.checkUnsupportedApplication(fusedApp)) + text = if (mainActivityViewModel.checkUnsupportedApplication(fusedApp)) { getString(R.string.not_available) - else getString(R.string.update) + } else { + getString(R.string.update) + } setTextColor(Color.WHITE) backgroundTintList = ContextCompat.getColorStateList(view.context, R.color.colorAccent) @@ -779,7 +766,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } private suspend fun updateProgress( - downloadProgress: DownloadProgress, + downloadProgress: DownloadProgress ) { val progressResult = applicationViewModel.calculateProgress(downloadProgress) if (view == null || progressResult.first < 1) { @@ -829,12 +816,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 } @@ -850,7 +837,10 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { ) appPrivacyScore.setCompoundDrawablesRelativeWithIntrinsicBounds( - null, null, getPrivacyDrawable(privacyScore.toString()), null + null, + null, + getPrivacyDrawable(privacyScore.toString()), + null ) appPrivacyScore.compoundDrawablePadding = 15 } diff --git a/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt b/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt index 3f1889ae157a023058d7c313e5040fc0739d1b89..57ad4f7493464080da5979e82851186095132529 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt @@ -19,11 +19,13 @@ package foundation.e.apps.ui.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 import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.R +import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.Origin import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Status @@ -31,12 +33,10 @@ import foundation.e.apps.data.fused.FusedAPIRepository import foundation.e.apps.data.fused.data.FusedApp import foundation.e.apps.data.fusedDownload.FusedManagerRepository import foundation.e.apps.data.fusedDownload.models.FusedDownload -import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.exceptions.CleanApkException -import foundation.e.apps.data.login.exceptions.GPlayException import foundation.e.apps.install.download.data.DownloadProgress import foundation.e.apps.install.download.data.DownloadProgressLD -import foundation.e.apps.ui.parentFragment.LoadingViewModel +import foundation.e.apps.utils.eventBus.AppEvent +import foundation.e.apps.utils.eventBus.EventBus import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject @@ -45,8 +45,8 @@ import javax.inject.Inject class ApplicationViewModel @Inject constructor( downloadProgressLD: DownloadProgressLD, private val fusedAPIRepository: FusedAPIRepository, - private val fusedManagerRepository: FusedManagerRepository, -) : LoadingViewModel() { + private val fusedManagerRepository: FusedManagerRepository +) : ViewModel() { val fusedApp: MutableLiveData> = MutableLiveData() val appStatus: MutableLiveData = MutableLiveData() @@ -59,41 +59,17 @@ class ApplicationViewModel @Inject constructor( packageName: String, origin: Origin, isFdroidLink: Boolean, - authObjectList: List, - retryBlock: (failedObjects: List) -> Boolean, + authData: AuthData? ) { - 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) + getApplicationDetails(id, packageName, authData ?: AuthData("", ""), origin) } - fun getApplicationDetails(id: String, packageName: String, authData: AuthData, origin: Origin) { + private fun getApplicationDetails(id: String, packageName: String, authData: AuthData, origin: Origin) { viewModelScope.launch(Dispatchers.IO) { try { val appData = @@ -107,20 +83,12 @@ class ApplicationViewModel @Inject constructor( val status = appData.second - if (appData.second != ResultStatus.OK) { - val exception = - if (authData.aasToken.isNotBlank() || authData.authToken.isNotBlank()) - GPlayException( - appData.second == ResultStatus.TIMEOUT, - status.message.ifBlank { "Data load error" } - ) - else CleanApkException( - appData.second == ResultStatus.TIMEOUT, - status.message.ifBlank { "Data load error" } + if (status != ResultStatus.OK) { + EventBus.invokeEvent( + AppEvent.DataLoadError( + ResultSupreme.create(status, appData.first) ) - - exceptionsList.add(exception) - exceptionsLiveData.postValue(exceptionsList) + ) } } catch (e: ApiException.AppNotFound) { _errorMessageLiveData.postValue(R.string.app_not_found) @@ -134,7 +102,7 @@ class ApplicationViewModel @Inject constructor( * Dedicated method to get app details from cleanapk using package name. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5509 */ - fun getCleanapkAppDetails(packageName: String) { + private fun getCleanapkAppDetails(packageName: String) { viewModelScope.launch { try { fusedAPIRepository.getCleanapkAppDetails(packageName).run { diff --git a/app/src/main/java/foundation/e/apps/ui/application/model/ApplicationScreenshotsDiffUtil.kt b/app/src/main/java/foundation/e/apps/ui/application/model/ApplicationScreenshotsDiffUtil.kt index d68ba25f8f707c1d12f8f253e2f2fa7540d9c24a..42ec1188df9b7483fa06dc3be83882af551b449b 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/model/ApplicationScreenshotsDiffUtil.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/model/ApplicationScreenshotsDiffUtil.kt @@ -22,7 +22,7 @@ import androidx.recyclerview.widget.DiffUtil class ApplicationScreenshotsDiffUtil( private val oldList: List, - private val newList: List, + private val newList: List ) : DiffUtil.Callback() { override fun getOldListSize(): Int { return oldList.size diff --git a/app/src/main/java/foundation/e/apps/ui/application/subFrags/ApplicationDialogFragment.kt b/app/src/main/java/foundation/e/apps/ui/application/subFrags/ApplicationDialogFragment.kt index c5702ec9dea6965629e85f1373c4f84c4ba4bc2b..b652bcd1db7629080855342921cc209f346e7a34 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/subFrags/ApplicationDialogFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/subFrags/ApplicationDialogFragment.kt @@ -54,7 +54,7 @@ class ApplicationDialogFragment() : DialogFragment() { cancelButtonText: String = "", cancelButtonAction: (() -> Unit)? = null, cancellable: Boolean = true, - onDismissListener: (() -> Unit)? = null, + onDismissListener: (() -> Unit)? = null ) : this() { this.drawable = drawable this.title = title @@ -114,7 +114,9 @@ class ApplicationDialogFragment() : DialogFragment() { textPaint.isUnderlineText = false } }, - spannable.getSpanStart(urlSpan), spannable.getSpanEnd(urlSpan), 0 + spannable.getSpanStart(urlSpan), + spannable.getSpanEnd(urlSpan), + 0 ) } text = spannable diff --git a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListFragment.kt b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListFragment.kt index ccf2abcbb8b032f827d07bbc8c0040f85cbe8a73..3a516e6f8d9507ff6b0d55cf87ef6a447f1f8284 100644 --- a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListFragment.kt @@ -21,39 +21,40 @@ package foundation.e.apps.ui.applicationlist import android.os.Bundle import android.view.View import android.widget.ImageView -import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController 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.R import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.Status import foundation.e.apps.data.fused.FusedAPIInterface import foundation.e.apps.data.fused.data.FusedApp -import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.databinding.FragmentApplicationListBinding import foundation.e.apps.install.download.data.DownloadProgress import foundation.e.apps.install.pkg.PWAManagerModule import foundation.e.apps.install.pkg.PkgManagerModule +import foundation.e.apps.presentation.login.LoginViewModel import foundation.e.apps.ui.AppInfoFetchViewModel import foundation.e.apps.ui.AppProgressViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.PrivacyInfoViewModel import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment -import foundation.e.apps.ui.parentFragment.TimeoutFragment +import foundation.e.apps.utils.loadDataOnce import kotlinx.coroutines.launch import javax.inject.Inject @AndroidEntryPoint class ApplicationListFragment : - TimeoutFragment(R.layout.fragment_application_list), + Fragment(R.layout.fragment_application_list), FusedAPIInterface { // protected to avoid SyntheticAccessor @@ -69,8 +70,13 @@ class ApplicationListFragment : protected val viewModel: ApplicationListViewModel by viewModels() private val privacyInfoViewModel: PrivacyInfoViewModel by viewModels() private val appInfoFetchViewModel: AppInfoFetchViewModel by viewModels() - override val mainActivityViewModel: MainActivityViewModel by activityViewModels() + private val mainActivityViewModel: MainActivityViewModel by activityViewModels() private val appProgressViewModel: AppProgressViewModel by viewModels() + private val loginViewModel: LoginViewModel by lazy { + ViewModelProvider(requireActivity())[LoginViewModel::class.java] + } + + private var authData: AuthData? = null private var _binding: FragmentApplicationListBinding? = null private val binding get() = _binding!! @@ -85,15 +91,13 @@ class ApplicationListFragment : setupRecyclerView(view) observeAppListLiveData() - setupListening() - - authObjects.observe(viewLifecycleOwner) { - if (it == null) return@observe - loadDataWhenNetworkAvailable(it) - } - - viewModel.exceptionsLiveData.observe(viewLifecycleOwner) { - handleExceptionsCommon(it) + mainActivityViewModel.internetConnection.loadDataOnce(this) { + loginViewModel.loginState.observe(viewLifecycleOwner) { + if (it.isLoggedIn) { + this.authData = it.authData + loadData() + } + } } } @@ -149,7 +153,7 @@ class ApplicationListFragment : listAdapter.currentList ) ) { - repostAuthObjects() + loadData() } } @@ -198,7 +202,7 @@ class ApplicationListFragment : positiveButtonAction = { getApplication(fusedApp) }, - cancelButtonText = getString(R.string.dialog_cancel), + cancelButtonText = getString(R.string.dialog_cancel) ).show(childFragmentManager, "HomeFragment") } @@ -216,8 +220,7 @@ class ApplicationListFragment : currentList ) - override fun loadData(authObjectList: List) { - + private fun loadData() { /* * If details are once loaded, do not load details again, * Only set the scroll listeners. @@ -229,10 +232,7 @@ class ApplicationListFragment : * Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/478 */ showLoadingUI() - viewModel.loadData(args.category, args.source, authObjectList) { - clearAndRestartGPlayLogin() - true - } + viewModel.loadData(args.category, args.source, authData) if (args.source != "Open Source" && args.source != "PWA") { /* @@ -244,7 +244,7 @@ class ApplicationListFragment : super.onScrollStateChanged(recyclerView, newState) if (!recyclerView.canScrollVertically(1)) { viewModel.loadMore( - authObjectList.find { it is AuthObject.GPlayAuth }, + authData, args.category ) } @@ -261,7 +261,7 @@ class ApplicationListFragment : if (this is ApplicationListRVAdapter) { onPlaceHolderShow = { viewModel.loadMore( - authObjectList.find { it is AuthObject.GPlayAuth }, + authData, args.category ) } @@ -270,34 +270,13 @@ class ApplicationListFragment : } } - 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/ui/applicationlist/ApplicationListRVAdapter.kt b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt index d28e864b0b900c172e98d821aa29dba99399f647..bdcad5036bb5421028f34dc2eed6f541a2371560 100644 --- a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt @@ -109,8 +109,11 @@ class ApplicationListRVAdapter( if (searchApp.isPlaceHolder) { val progressBar = holder.binding.placeholderProgressBar holder.binding.root.children.forEach { - it.visibility = if (it != progressBar) View.INVISIBLE - else View.VISIBLE + it.visibility = if (it != progressBar) { + View.INVISIBLE + } else { + View.VISIBLE + } } onPlaceHolderShow?.invoke() // Do not process anything else for this entry @@ -118,8 +121,11 @@ class ApplicationListRVAdapter( } else { val progressBar = holder.binding.placeholderProgressBar holder.binding.root.children.forEach { - it.visibility = if (it != progressBar) View.VISIBLE - else View.INVISIBLE + it.visibility = if (it != progressBar) { + View.VISIBLE + } else { + View.INVISIBLE + } } } @@ -155,8 +161,9 @@ class ApplicationListRVAdapter( item.post { val maxAllowedWidth = item.measuredWidth / 2 installButton.apply { - if (width > maxAllowedWidth) + if (width > maxAllowedWidth) { width = maxAllowedWidth + } } } } @@ -303,7 +310,7 @@ class ApplicationListRVAdapter( private fun ApplicationListItemBinding.handleInstallationIssue( view: View, - searchApp: FusedApp, + searchApp: FusedApp ) { progressBarInstall.visibility = View.GONE if (lifecycleOwner == null) { @@ -335,10 +342,11 @@ class ApplicationListRVAdapter( faultyAppResult: Pair, view: View ) = - if (faultyAppResult.second.contentEquals(InstallerService.INSTALL_FAILED_UPDATE_INCOMPATIBLE)) + if (faultyAppResult.second.contentEquals(InstallerService.INSTALL_FAILED_UPDATE_INCOMPATIBLE)) { view.context.getText(R.string.update) - else + } else { view.context.getString(R.string.retry) + } private fun ApplicationListItemBinding.handleBlocked(view: View) { installButton.apply { @@ -421,7 +429,7 @@ class ApplicationListRVAdapter( } private fun ApplicationListItemBinding.handleDownloading( - searchApp: FusedApp, + searchApp: FusedApp ) { installButton.apply { enableInstallButton() @@ -436,7 +444,7 @@ class ApplicationListRVAdapter( private fun ApplicationListItemBinding.handleUnavailable( searchApp: FusedApp, - holder: ViewHolder, + holder: ViewHolder ) { installButton.apply { updateUIByPaymentType(searchApp, this, this@handleUnavailable, holder) @@ -497,9 +505,11 @@ class ApplicationListRVAdapter( ) { installButton.apply { enableInstallButton(Status.UPDATABLE) - text = if (mainActivityViewModel.checkUnsupportedApplication(searchApp)) + text = if (mainActivityViewModel.checkUnsupportedApplication(searchApp)) { context.getString(R.string.not_available) - else context.getString(R.string.update) + } else { + context.getString(R.string.update) + } setOnClickListener { if (mainActivityViewModel.checkUnsupportedApplication(searchApp, context)) { return@setOnClickListener @@ -511,7 +521,7 @@ class ApplicationListRVAdapter( } private fun ApplicationListItemBinding.handleInstalled( - searchApp: FusedApp, + searchApp: FusedApp ) { installButton.apply { enableInstallButton(Status.INSTALLED) diff --git a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListViewModel.kt b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListViewModel.kt index 86be6867d85fb6e9970d9bbdec2faf078de10a32..82ea3cc4a80ff0ca0f61452b99e1f6877aa9d590 100644 --- a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListViewModel.kt @@ -19,6 +19,7 @@ package foundation.e.apps.ui.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 @@ -27,10 +28,8 @@ import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Source import foundation.e.apps.data.fused.FusedAPIRepository import foundation.e.apps.data.fused.data.FusedApp -import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.exceptions.CleanApkException -import foundation.e.apps.data.login.exceptions.GPlayException -import foundation.e.apps.ui.parentFragment.LoadingViewModel +import foundation.e.apps.utils.eventBus.AppEvent +import foundation.e.apps.utils.eventBus.EventBus import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject @@ -38,7 +37,7 @@ import javax.inject.Inject @HiltViewModel class ApplicationListViewModel @Inject constructor( private val fusedAPIRepository: FusedAPIRepository -) : LoadingViewModel() { +) : ViewModel() { val appListLiveData: MutableLiveData>?> = MutableLiveData() @@ -46,38 +45,12 @@ class ApplicationListViewModel @Inject constructor( private var nextPageUrl: String? = null - private var currentAuthListObject: List? = null - fun loadData( category: String, source: String, - authObjectList: List, - retryBlock: (failedObjects: List) -> Boolean, + authData: AuthData? ) { - super.onLoadData(authObjectList, { successAuthList, _ -> - - // if token is refreshed, then reset all data - if (currentAuthListObject != null && currentAuthListObject != authObjectList) { - appListLiveData.postValue(ResultSupreme.Success(emptyList())) - nextPageUrl = null - } - - if (appListLiveData.value?.data?.isNotEmpty() == true && currentAuthListObject == authObjectList) { - appListLiveData.postValue(appListLiveData.value) - return@onLoadData - } - - this.currentAuthListObject = authObjectList - successAuthList.find { it is AuthObject.GPlayAuth }?.run { - getList(category, result.data!! as AuthData, source) - return@onLoadData - } - - successAuthList.find { it is AuthObject.CleanApk }?.run { - getList(category, AuthData("", ""), source) - return@onLoadData - } - }, retryBlock) + getList(category, authData ?: AuthData("", ""), source) } private fun getList(category: String, authData: AuthData, source: String) { @@ -85,6 +58,7 @@ class ApplicationListViewModel @Inject constructor( return } + this.nextPageUrl = null viewModelScope.launch(Dispatchers.IO) { isLoading = true val result = fusedAPIRepository.getAppsListBasedOnCategory( @@ -101,27 +75,12 @@ class ApplicationListViewModel @Inject constructor( updateNextPageUrl(it.second) } - if (!result.isSuccess()) { - val exception = getException(authData, result) - exceptionsList.add(exception) - exceptionsLiveData.postValue(exceptionsList) + if (result.isSuccess()) { + loadMore(authData, category) } - } - } - private fun getException( - authData: AuthData, - result: ResultSupreme, String>> - ) = if (authData.aasToken.isNotBlank() || authData.authToken.isNotBlank()) { - GPlayException( - result.isTimeout(), - result.message.ifBlank { "Data load error" } - ) - } else { - CleanApkException( - result.isTimeout(), - result.message.ifBlank { "Data load error" } - ) + if (!result.isSuccess()) EventBus.invokeEvent(AppEvent.DataLoadError(result)) + } } private fun updateNextPageUrl(nextPageUrl: String?) { @@ -138,14 +97,8 @@ class ApplicationListViewModel @Inject constructor( return fusedAPIRepository.isAnyFusedAppUpdated(newFusedApps, oldFusedApps) } - fun loadMore(gPlayAuth: AuthObject?, category: String) { + fun loadMore(authData: AuthData?, category: String) { viewModelScope.launch(Dispatchers.IO) { - val authData: AuthData? = when { - gPlayAuth !is AuthObject.GPlayAuth -> null - !gPlayAuth.result.isSuccess() -> null - else -> gPlayAuth.result.data!! - } - if (isLoading || authData == null || nextPageUrl.isNullOrEmpty()) { return@launch } diff --git a/app/src/main/java/foundation/e/apps/ui/categories/AppsFragment.kt b/app/src/main/java/foundation/e/apps/ui/categories/AppsFragment.kt index af0383c0dca7bf2bf80a5a227364cfe4669721fe..cc3ced80c9150a6499134a03ae4ba5ce17f8ad82 100644 --- a/app/src/main/java/foundation/e/apps/ui/categories/AppsFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/categories/AppsFragment.kt @@ -20,41 +20,42 @@ package foundation.e.apps.ui.categories import android.os.Bundle import android.view.View -import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.fused.utils.CategoryType -import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.databinding.FragmentAppsBinding +import foundation.e.apps.presentation.login.LoginViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.categories.model.CategoriesRVAdapter -import foundation.e.apps.ui.parentFragment.TimeoutFragment +import foundation.e.apps.utils.loadDataOnce @AndroidEntryPoint -class AppsFragment : TimeoutFragment(R.layout.fragment_apps) { +class AppsFragment : Fragment(R.layout.fragment_apps) { private var _binding: FragmentAppsBinding? = null private val binding get() = _binding!! private val categoriesViewModel: CategoriesViewModel by viewModels() - override val mainActivityViewModel: MainActivityViewModel by activityViewModels() + private val mainActivityViewModel: MainActivityViewModel by activityViewModels() + + private val loginViewModel: LoginViewModel by lazy { + ViewModelProvider(requireActivity())[LoginViewModel::class.java] + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) _binding = FragmentAppsBinding.bind(view) - setupListening() - - authObjects.observe(viewLifecycleOwner) { - if (it == null) return@observe - loadDataWhenNetworkAvailable(it) - } - - categoriesViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { - handleExceptionsCommon(it) + mainActivityViewModel.internetConnection.loadDataOnce(this) { + loginViewModel.loginState.observe(viewLifecycleOwner) { + if (it.isLoggedIn) { + loadData() + } + } } val categoriesRVAdapter = CategoriesRVAdapter() @@ -72,41 +73,18 @@ class AppsFragment : TimeoutFragment(R.layout.fragment_apps) { } } - override fun loadData(authObjectList: List) { - categoriesViewModel.loadData(CategoryType.APPLICATION, authObjectList) { - clearAndRestartGPlayLogin() - 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 + private fun loadData() { + showLoadingUI() + categoriesViewModel.loadData(CategoryType.APPLICATION) } - 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/ui/categories/CategoriesViewModel.kt b/app/src/main/java/foundation/e/apps/ui/categories/CategoriesViewModel.kt index acf7c933c1595cfa1d1b5eecac30b7748e67e609..7777d05cebe81276b2ed9d8bdc11a5dfd4c1f42b 100644 --- a/app/src/main/java/foundation/e/apps/ui/categories/CategoriesViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/categories/CategoriesViewModel.kt @@ -19,48 +19,34 @@ package foundation.e.apps.ui.categories 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.data.ResultSupreme import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.fused.FusedAPIRepository import foundation.e.apps.data.fused.data.FusedCategory import foundation.e.apps.data.fused.utils.CategoryType -import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.exceptions.CleanApkException -import foundation.e.apps.data.login.exceptions.GPlayException -import foundation.e.apps.ui.parentFragment.LoadingViewModel +import foundation.e.apps.utils.eventBus.AppEvent +import foundation.e.apps.utils.eventBus.EventBus 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: CategoryType, - authObjectList: List, - retryBlock: (failedObjects: List) -> Boolean, + type: CategoryType ) { - 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) + getCategoriesList(type) } - fun getCategoriesList(type: CategoryType, authData: AuthData) { + private fun getCategoriesList(type: CategoryType) { viewModelScope.launch { val categoriesData = fusedAPIRepository.getCategoriesList(type) categoriesList.postValue(categoriesData) @@ -68,19 +54,11 @@ class CategoriesViewModel @Inject constructor( val status = categoriesData.third if (status != ResultStatus.OK) { - val exception = - if (authData.aasToken.isNotBlank() || authData.authToken.isNotBlank()) - GPlayException( - categoriesData.third == ResultStatus.TIMEOUT, - status.message.ifBlank { "Data load error" } - ) - else CleanApkException( - categoriesData.third == ResultStatus.TIMEOUT, - status.message.ifBlank { "Data load error" } + EventBus.invokeEvent( + AppEvent.DataLoadError( + ResultSupreme.create(status, categoriesData.second) ) - - exceptionsList.add(exception) - exceptionsLiveData.postValue(exceptionsList) + ) } } } diff --git a/app/src/main/java/foundation/e/apps/ui/categories/GamesFragment.kt b/app/src/main/java/foundation/e/apps/ui/categories/GamesFragment.kt index 8f8f11fdbf5c6d0cc83d7240de196a13da0094e3..4551194a2d686e4753a0ec715652fdd3d9346618 100644 --- a/app/src/main/java/foundation/e/apps/ui/categories/GamesFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/categories/GamesFragment.kt @@ -20,41 +20,42 @@ package foundation.e.apps.ui.categories import android.os.Bundle import android.view.View -import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.fused.utils.CategoryType -import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.databinding.FragmentGamesBinding +import foundation.e.apps.presentation.login.LoginViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.categories.model.CategoriesRVAdapter -import foundation.e.apps.ui.parentFragment.TimeoutFragment +import foundation.e.apps.utils.loadDataOnce @AndroidEntryPoint -class GamesFragment : TimeoutFragment(R.layout.fragment_games) { +class GamesFragment : Fragment(R.layout.fragment_games) { private var _binding: FragmentGamesBinding? = null private val binding get() = _binding!! private val categoriesViewModel: CategoriesViewModel by viewModels() - override val mainActivityViewModel: MainActivityViewModel by activityViewModels() + private val mainActivityViewModel: MainActivityViewModel by activityViewModels() + + private val loginViewModel: LoginViewModel by lazy { + ViewModelProvider(requireActivity())[LoginViewModel::class.java] + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) _binding = FragmentGamesBinding.bind(view) - setupListening() - - authObjects.observe(viewLifecycleOwner) { - if (it == null) return@observe - loadDataWhenNetworkAvailable(it) - } - - categoriesViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { - handleExceptionsCommon(it) + mainActivityViewModel.internetConnection.loadDataOnce(this) { + loginViewModel.loginState.observe(viewLifecycleOwner) { + if (it.isLoggedIn) { + loadData() + } + } } val categoriesRVAdapter = CategoriesRVAdapter() @@ -72,41 +73,18 @@ class GamesFragment : TimeoutFragment(R.layout.fragment_games) { } } - override fun loadData(authObjectList: List) { - categoriesViewModel.loadData(CategoryType.GAMES, authObjectList) { - clearAndRestartGPlayLogin() - 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 + private fun loadData() { + showLoadingUI() + categoriesViewModel.loadData(CategoryType.GAMES) } - 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/ui/errors/CentralErrorHandler.kt b/app/src/main/java/foundation/e/apps/ui/errors/CentralErrorHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..868c511c6d4ae7c5a1015a8d148b466b0b658cf0 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/ui/errors/CentralErrorHandler.kt @@ -0,0 +1,175 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.ui.errors + +import android.app.Activity +import android.content.Intent +import android.graphics.Paint +import android.net.Uri +import android.view.View +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import foundation.e.apps.R +import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.enums.User +import foundation.e.apps.databinding.DialogErrorLogBinding + +class CentralErrorHandler { + + private var lastDialog: AlertDialog? = null + + fun getDialogForDataLoadError( + context: Activity, + result: ResultSupreme, + retryAction: () -> Unit + ): AlertDialog.Builder? { + return when (result) { + is ResultSupreme.Timeout -> { + getDialogForTimeout( + context, + result.message.ifBlank { result.exception?.message ?: "Timeout - ${result.exception}" }, + retryAction + ) + } + is ResultSupreme.Error -> { + getDialogForOtherErrors( + context, + result.message.ifBlank { result.exception?.message ?: "Error - ${result.exception}" }, + retryAction + ) + } + else -> null + } + } + + fun getDialogForUnauthorized( + context: Activity, + logToDisplay: String = "", + user: User, + retryAction: () -> Unit, + logoutAction: () -> Unit + ): AlertDialog.Builder { + val customDialogView = getDialogCustomView(context, logToDisplay) + val dialog = AlertDialog.Builder(context).apply { + if (user == User.GOOGLE) { + setTitle(R.string.sign_in_failed_title) + setMessage(R.string.sign_in_failed_desc) + } else { + setTitle(R.string.anonymous_login_failed) + setMessage(R.string.anonymous_login_failed_desc) + } + + setView(customDialogView) + + setPositiveButton(R.string.retry) { _, _ -> + retryAction() + } + setNegativeButton(R.string.logout) { _, _ -> + logoutAction() + } + setCancelable(true) + } + return dialog + } + + private fun getDialogForTimeout( + context: Activity, + logToDisplay: String = "", + retryAction: () -> Unit + ): AlertDialog.Builder { + val customDialogView = getDialogCustomView(context, logToDisplay) + val dialog = AlertDialog.Builder(context).apply { + setTitle(R.string.timeout_title) + setMessage(R.string.timeout_desc_cleanapk) + setView(customDialogView) + setPositiveButton(R.string.retry) { _, _ -> + retryAction() + } + setNegativeButton(R.string.close, null) + setCancelable(true) + } + return dialog + } + + private fun getDialogForOtherErrors( + context: Activity, + logToDisplay: String = "", + retryAction: () -> Unit + ): AlertDialog.Builder { + val customDialogView = getDialogCustomView(context, logToDisplay) + val dialog = AlertDialog.Builder(context).apply { + setTitle(R.string.data_load_error) + setMessage(R.string.data_load_error_desc) + setView(customDialogView) + setPositiveButton(R.string.retry) { _, _ -> + retryAction() + } + setNegativeButton(R.string.close, null) + setCancelable(true) + } + return dialog + } + + private fun getDialogCustomView( + context: Activity, + logToDisplay: String + ): View { + val dialogLayout = DialogErrorLogBinding.inflate(context.layoutInflater) + dialogLayout.apply { + moreInfo.setOnClickListener { + logDisplay.isVisible = true + moreInfo.isVisible = false + } + setTextviewUnderlined(troubleshootingLink) + troubleshootingLink.setOnClickListener { + openTroubleshootingPage(context) + } + + if (logToDisplay.isNotBlank()) { + logDisplay.text = logToDisplay + moreInfo.isVisible = true + } + } + return dialogLayout.root + } + + fun dismissAllAndShow(alertDialogBuilder: AlertDialog.Builder) { + if (lastDialog?.isShowing == true) { + lastDialog?.dismiss() + } + alertDialogBuilder.create().run { + this.show() + lastDialog = this + } + } + + private fun setTextviewUnderlined(textView: TextView) { + textView.paintFlags = textView.paintFlags or Paint.UNDERLINE_TEXT_FLAG + } + + private fun openTroubleshootingPage(context: Activity) { + context.run { + val troubleshootUrl = getString(R.string.troubleshootURL) + val openUrlIntent = Intent(Intent.ACTION_VIEW) + openUrlIntent.data = Uri.parse(troubleshootUrl) + startActivity(openUrlIntent) + } + } +} diff --git a/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt b/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt index 73142126f295ee9494c35f2b0c6c0656ebb62a18..617857054c0da57998e21f0744a1e5815d8303f3 100644 --- a/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt @@ -21,13 +21,14 @@ package foundation.e.apps.ui.home import android.os.Bundle import android.view.View import android.widget.ImageView -import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider 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.R import foundation.e.apps.data.ResultSupreme @@ -35,25 +36,22 @@ import foundation.e.apps.data.enums.Status import foundation.e.apps.data.fused.FusedAPIInterface import foundation.e.apps.data.fused.data.FusedApp import foundation.e.apps.data.fused.data.FusedHome -import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.exceptions.GPlayException -import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.databinding.FragmentHomeBinding -import foundation.e.apps.di.CommonUtilsModule.safeNavigate import foundation.e.apps.install.download.data.DownloadProgress import foundation.e.apps.install.pkg.PWAManagerModule +import foundation.e.apps.presentation.login.LoginViewModel import foundation.e.apps.ui.AppInfoFetchViewModel import foundation.e.apps.ui.AppProgressViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.home.model.HomeChildRVAdapter import foundation.e.apps.ui.home.model.HomeParentRVAdapter -import foundation.e.apps.ui.parentFragment.TimeoutFragment +import foundation.e.apps.utils.loadDataOnce import kotlinx.coroutines.launch import javax.inject.Inject @AndroidEntryPoint -class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface { +class HomeFragment : Fragment(R.layout.fragment_home), FusedAPIInterface { /* * Make adapter nullable to avoid memory leaks. @@ -64,10 +62,16 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface private val binding get() = _binding!! private val homeViewModel: HomeViewModel by viewModels() - override val mainActivityViewModel: MainActivityViewModel by activityViewModels() + private val mainActivityViewModel: MainActivityViewModel by activityViewModels() private val appProgressViewModel: AppProgressViewModel by viewModels() private val appInfoFetchViewModel: AppInfoFetchViewModel by viewModels() + private val loginViewModel: LoginViewModel by lazy { + ViewModelProvider(requireActivity())[LoginViewModel::class.java] + } + + private var authData: AuthData? = null + @Inject lateinit var pwaManagerModule: PWAManagerModule @@ -104,7 +108,9 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface private fun initHomeParentRVAdapter() = HomeParentRVAdapter( this, - mainActivityViewModel, appInfoFetchViewModel, viewLifecycleOwner + mainActivityViewModel, + appInfoFetchViewModel, + viewLifecycleOwner ) { fusedApp -> if (!mainActivityViewModel.shouldShowPaidAppsSnackBar(fusedApp)) { showPaidAppMessage(fusedApp) @@ -112,15 +118,13 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface } private fun loadHomePageData() { - setupListening() - - authObjects.observe(viewLifecycleOwner) { - if (it == null) return@observe - loadDataWhenNetworkAvailable(it) - } - - homeViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { - handleExceptionsCommon(it) + mainActivityViewModel.internetConnection.loadDataOnce(this) { + loginViewModel.loginState.observe(viewLifecycleOwner) { + if (it.isLoggedIn) { + this.authData = it.authData + loadData() + } + } } } @@ -136,7 +140,7 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface positiveButtonAction = { getApplication(fusedApp) }, - cancelButtonText = getString(R.string.dialog_cancel), + cancelButtonText = getString(R.string.dialog_cancel) ).show(childFragmentManager, "HomeFragment") } @@ -146,61 +150,17 @@ 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) { _, _ -> - openSettings() - } - } - } - } - - override fun loadData(authObjectList: List) { - homeViewModel.loadData(authObjectList, viewLifecycleOwner) { _ -> - clearAndRestartGPlayLogin() - true - } + fun loadData() { + homeViewModel.loadData(authData, viewLifecycleOwner) } - override fun showLoadingUI() { + fun showLoadingUI() { binding.shimmerLayout.startShimmer() binding.shimmerLayout.visibility = View.VISIBLE binding.parentRV.visibility = View.GONE } - override fun stopLoadingUI() { + fun stopLoadingUI() { binding.shimmerLayout.stopShimmer() binding.shimmerLayout.visibility = View.GONE binding.parentRV.visibility = View.VISIBLE @@ -265,7 +225,7 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface } if (homeViewModel.isAnyAppInstallStatusChanged(homeParentRVAdapter?.currentList)) { - repostAuthObjects() + loadData() } } @@ -291,23 +251,4 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface override fun cancelDownload(app: FusedApp) { mainActivityViewModel.cancelDownload(app) } - - private fun onTosAccepted(isTosAccepted: Boolean) { - if (isTosAccepted) { - /* - * "safeNavigate" is an extension function, to prevent calling this navigation multiple times. - * This is taken from: - * https://nezspencer.medium.com/navigation-components-a-fix-for-navigation-action-cannot-be-found-in-the-current-destination-95b63e16152e - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5166 - * Also related: https://gitlab.e.foundation/ecorp/apps/apps/-/merge_requests/28 - */ - view?.findNavController() - ?.safeNavigate(R.id.homeFragment, R.id.action_homeFragment_to_signInFragment) - } - } - - private fun openSettings() { - view?.findNavController() - ?.safeNavigate(R.id.homeFragment, R.id.action_homeFragment_to_SettingsFragment) - } } diff --git a/app/src/main/java/foundation/e/apps/ui/home/HomeViewModel.kt b/app/src/main/java/foundation/e/apps/ui/home/HomeViewModel.kt index 7714c2b9c5fd64bb3bcaac5507a3484ed5c47788..c0714c79bc26225083446244246b51d2f5be3cbe 100644 --- a/app/src/main/java/foundation/e/apps/ui/home/HomeViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/home/HomeViewModel.kt @@ -20,6 +20,7 @@ package foundation.e.apps.ui.home import androidx.lifecycle.LifecycleOwner 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 @@ -27,68 +28,38 @@ import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.fused.FusedAPIRepository import foundation.e.apps.data.fused.data.FusedApp import foundation.e.apps.data.fused.data.FusedHome -import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.exceptions.CleanApkException -import foundation.e.apps.data.login.exceptions.GPlayException -import foundation.e.apps.ui.parentFragment.LoadingViewModel +import foundation.e.apps.utils.eventBus.AppEvent +import foundation.e.apps.utils.eventBus.EventBus import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( - private val fusedAPIRepository: FusedAPIRepository, -) : LoadingViewModel() { + private val fusedAPIRepository: FusedAPIRepository +) : ViewModel() { - /* - * Hold list of applications, as well as application source type. - * Source type may change from user selected preference in case of timeout. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404 - */ var homeScreenData: MutableLiveData>> = MutableLiveData() fun loadData( - authObjectList: List, - lifecycleOwner: LifecycleOwner, - retryBlock: (failedObjects: List) -> Boolean, + authData: AuthData?, + lifecycleOwner: LifecycleOwner ) { - super.onLoadData(authObjectList, { successAuthList, _ -> - - successAuthList.find { it is AuthObject.GPlayAuth }?.run { - getHomeScreenData(result.data!! as AuthData, lifecycleOwner) - return@onLoadData - } - - successAuthList.find { it is AuthObject.CleanApk }?.run { - getHomeScreenData(AuthData("", ""), lifecycleOwner) - return@onLoadData - } - }, retryBlock) + getHomeScreenData(authData ?: AuthData("", ""), lifecycleOwner) } - fun getHomeScreenData( + private fun getHomeScreenData( authData: AuthData, - lifecycleOwner: LifecycleOwner, + lifecycleOwner: LifecycleOwner ) { viewModelScope.launch { fusedAPIRepository.getHomeScreenData(authData).observe(lifecycleOwner) { homeScreenData.postValue(it) - if (it.isSuccess()) return@observe - - val exception = - if (authData.aasToken.isNotBlank() || authData.authToken.isNotBlank()) - GPlayException( - it.isTimeout(), - it.message.ifBlank { "Data load error" } - ) - else CleanApkException( - it.isTimeout(), - it.message.ifBlank { "Data load error" } - ) - - exceptionsList.add(exception) - exceptionsLiveData.postValue(exceptionsList) + if (!it.isSuccess()) { + viewModelScope.launch { + EventBus.invokeEvent(AppEvent.DataLoadError(it)) + } + } } } } diff --git a/app/src/main/java/foundation/e/apps/ui/home/model/HomeChildRVAdapter.kt b/app/src/main/java/foundation/e/apps/ui/home/model/HomeChildRVAdapter.kt index 1dc7933ab89d508dfd6921af0fe2ee8a46018984..9be2af9e66d3d1c530c683572d551aea599530ba 100644 --- a/app/src/main/java/foundation/e/apps/ui/home/model/HomeChildRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/ui/home/model/HomeChildRVAdapter.kt @@ -190,7 +190,7 @@ class HomeChildRVAdapter( private fun HomeChildListItemBinding.handleUnavailable( homeApp: FusedApp, - holder: ViewHolder, + holder: ViewHolder ) { installButton.apply { updateUIByPaymentType(homeApp, this, holder.binding) @@ -214,9 +214,11 @@ class HomeChildRVAdapter( ) { installButton.apply { enableInstallButton(Status.UPDATABLE) - text = if (mainActivityViewModel.checkUnsupportedApplication(homeApp)) + text = if (mainActivityViewModel.checkUnsupportedApplication(homeApp)) { context.getString(R.string.not_available) - else context.getString(R.string.update) + } else { + context.getString(R.string.update) + } setOnClickListener { if (mainActivityViewModel.checkUnsupportedApplication(homeApp, context)) { return@setOnClickListener diff --git a/app/src/main/java/foundation/e/apps/ui/parentFragment/LoadingViewModel.kt b/app/src/main/java/foundation/e/apps/ui/parentFragment/LoadingViewModel.kt index a59e502ca0bc1aa937f4afe847ac73102c5ae07f..e9fb95aef3cf688a6ab4cd0c65dc7d40151a2fa9 100644 --- a/app/src/main/java/foundation/e/apps/ui/parentFragment/LoadingViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/parentFragment/LoadingViewModel.kt @@ -44,9 +44,8 @@ abstract class LoadingViewModel : ViewModel() { fun onLoadData( authObjectList: List, loadingBlock: (successObjects: List, failedObjects: List) -> Unit, - retryBlock: (failedObjects: List) -> Boolean, + retryBlock: (failedObjects: List) -> Boolean ) { - exceptionsList.clear() val successAuthList = authObjectList.filter { it.result.isSuccess() } diff --git a/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt b/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt index 1557c14dd7f41d3a142a01bf678045260361602f..830d1607871130c62aab0962bd30209384412daf 100644 --- a/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt @@ -34,13 +34,13 @@ import foundation.e.apps.R import foundation.e.apps.data.enums.User import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.LoginSourceGPlay -import foundation.e.apps.data.login.LoginViewModel import foundation.e.apps.data.login.exceptions.CleanApkException import foundation.e.apps.data.login.exceptions.GPlayException import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.data.login.exceptions.GPlayValidationException import foundation.e.apps.data.login.exceptions.UnknownSourceException import foundation.e.apps.databinding.DialogErrorLogBinding +import foundation.e.apps.presentation.login.LoginViewModel import foundation.e.apps.ui.MainActivityViewModel import timber.log.Timber @@ -139,7 +139,7 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { */ abstract fun onTimeout( exception: Exception, - predefinedDialog: AlertDialog.Builder, + predefinedDialog: AlertDialog.Builder ): AlertDialog.Builder? /** @@ -163,7 +163,7 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { */ abstract fun onSignInError( exception: GPlayLoginException, - predefinedDialog: AlertDialog.Builder, + predefinedDialog: AlertDialog.Builder ): AlertDialog.Builder? /** @@ -182,7 +182,7 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { */ abstract fun onDataLoadError( exception: Exception, - predefinedDialog: AlertDialog.Builder, + predefinedDialog: AlertDialog.Builder ): AlertDialog.Builder? /** @@ -281,7 +281,7 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { onTimeout( exception, - predefinedDialog, + predefinedDialog )?.run { stopLoadingUI() showAndSetDialog(this) @@ -297,10 +297,8 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { * 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 @@ -342,7 +340,7 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { onSignInError( exception, - predefinedDialog, + predefinedDialog )?.run { stopLoadingUI() showAndSetDialog(this) @@ -389,7 +387,7 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { onDataLoadError( exception, - predefinedDialog, + predefinedDialog )?.run { stopLoadingUI() showAndSetDialog(this) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt index 5e2a519ab519e5b30f0a9847cf53eadbd5487118..15213d9e302a204d63133ca5e2f6d474f0abf519 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt @@ -28,12 +28,13 @@ import android.view.inputmethod.InputMethodManager import android.widget.EditText import android.widget.ImageView import android.widget.LinearLayout -import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView import androidx.cursoradapter.widget.CursorAdapter import androidx.cursoradapter.widget.SimpleCursorAdapter +import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager @@ -46,25 +47,24 @@ import foundation.e.apps.data.enums.Status import foundation.e.apps.data.fused.FusedAPIInterface import foundation.e.apps.data.fused.data.FusedApp import foundation.e.apps.data.fusedDownload.models.FusedDownload -import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.databinding.FragmentSearchBinding import foundation.e.apps.install.download.data.DownloadProgress import foundation.e.apps.install.pkg.PWAManagerModule +import foundation.e.apps.presentation.login.LoginViewModel import foundation.e.apps.ui.AppInfoFetchViewModel import foundation.e.apps.ui.AppProgressViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.PrivacyInfoViewModel import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.applicationlist.ApplicationListRVAdapter -import foundation.e.apps.ui.parentFragment.TimeoutFragment import foundation.e.apps.utils.isNetworkAvailable +import foundation.e.apps.utils.loadDataOnce import kotlinx.coroutines.launch import javax.inject.Inject @AndroidEntryPoint class SearchFragment : - TimeoutFragment(R.layout.fragment_search), + Fragment(R.layout.fragment_search), SearchView.OnQueryTextListener, SearchView.OnSuggestionListener, FusedAPIInterface { @@ -75,12 +75,17 @@ class SearchFragment : private var _binding: FragmentSearchBinding? = null private val binding get() = _binding!! + // To avoid SyntheticAccessor, declared as protected protected val searchViewModel: SearchViewModel by viewModels() private val privacyInfoViewModel: PrivacyInfoViewModel by viewModels() private val appInfoFetchViewModel: AppInfoFetchViewModel by viewModels() - override val mainActivityViewModel: MainActivityViewModel by activityViewModels() + private val mainActivityViewModel: MainActivityViewModel by activityViewModels() private val appProgressViewModel: AppProgressViewModel by viewModels() + private val loginViewModel: LoginViewModel by lazy { + ViewModelProvider(requireActivity())[LoginViewModel::class.java] + } + private val SUGGESTION_KEY = "suggestion" private var lastSearch = "" @@ -96,6 +101,24 @@ class SearchFragment : */ private var searchText = "" + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + mainActivityViewModel.internetConnection.loadDataOnce(this) { + observeLoginState() + } + } + + private fun observeLoginState() { + loginViewModel.loginState.observe(viewLifecycleOwner) { + if (it.authData == null || !it.isLoggedIn) { + return@observe + } + + searchViewModel.authData = it.authData + loadData() + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) _binding = FragmentSearchBinding.bind(view) @@ -114,49 +137,16 @@ class SearchFragment : observeSearchResult(listAdapter) - setupListening() - - authObjects.observe(viewLifecycleOwner) { - val currentQuery = searchView?.query?.toString() ?: "" - if (it == null || shouldIgnore(it, currentQuery)) { - showData() - return@observe - } - - val applicationListRVAdapter = recyclerView?.adapter as ApplicationListRVAdapter - applicationListRVAdapter.setData(mutableListOf()) - - loadDataWhenNetworkAvailable(it) - } - - searchViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { - handleExceptionsCommon(it) - } - binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) - if (!recyclerView.canScrollVertically(1)) { - if (!requireContext().isNetworkAvailable()) { - return - } - - if (authObjects.value?.none { it is AuthObject.GPlayAuth } == true) { - return - } - + if (!recyclerView.canScrollVertically(1) && requireContext().isNetworkAvailable()) { searchViewModel.loadMore(searchText) } } }) } - private fun shouldIgnore( - authObjectList: List?, - currentQuery: String - ) = currentQuery.isNotEmpty() && searchViewModel.isAuthObjectListSame(authObjectList) && - lastSearch == currentQuery - private fun observeSearchResult(listAdapter: ApplicationListRVAdapter?) { searchViewModel.searchResult.observe(viewLifecycleOwner) { if (it.data?.first.isNullOrEmpty() && it.data?.second == false) { @@ -198,22 +188,19 @@ class SearchFragment : */ private fun updateSearchResult( listAdapter: ApplicationListRVAdapter?, - appList: List, + appList: List?, + hasMore: Boolean ): Boolean { val currentList = listAdapter?.currentList ?: listOf() - if (!searchViewModel.isAnyAppUpdated(appList, currentList)) { + if (appList != null && !searchViewModel.isAnyAppUpdated(appList, currentList)) { return false } - showData() - listAdapter?.setData(appList) - return true - } - - private fun showData() { stopLoadingUI() noAppsFoundLayout?.visibility = View.GONE searchHintLayout?.visibility = View.GONE + listAdapter?.setData(appList!!) + return true } private fun setupSearchResult(view: View): ApplicationListRVAdapter? { @@ -244,7 +231,10 @@ class SearchFragment : val to = intArrayOf(android.R.id.text1) searchView?.suggestionsAdapter = SimpleCursorAdapter( context, - R.layout.custom_simple_list_item, null, from, to, + R.layout.custom_simple_list_item, + null, + from, + to, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER ) @@ -272,7 +262,7 @@ class SearchFragment : positiveButtonAction = { getApplication(fusedApp) }, - cancelButtonText = getString(R.string.dialog_cancel), + cancelButtonText = getString(R.string.dialog_cancel) ).show(childFragmentManager, "SearchFragment") } @@ -290,45 +280,22 @@ class SearchFragment : val searchList = searchViewModel.searchResult.value?.data?.first?.toMutableList() ?: emptyList() + val hasMoreDataToLoad = searchViewModel.searchResult.value?.data?.second == true mainActivityViewModel.updateStatusOfFusedApps(searchList, fusedDownloadList) - updateSearchResult(applicationListRVAdapter, searchList) - } - - override fun onTimeout( - exception: Exception, - predefinedDialog: AlertDialog.Builder - ): AlertDialog.Builder? { - return predefinedDialog + updateSearchResult(applicationListRVAdapter, searchList, hasMoreDataToLoad) } - 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 loadData(authObjectList: List) { + private fun loadData() { showLoadingUI() - searchViewModel.loadData(searchText, viewLifecycleOwner, authObjectList) { - clearAndRestartGPlayLogin() - true - } + searchViewModel.loadData(searchText, viewLifecycleOwner) } - override fun showLoadingUI() { + private fun showLoadingUI() { binding.shimmerLayout.startShimmer() binding.shimmerLayout.visibility = View.VISIBLE } - override fun stopLoadingUI() { + private fun stopLoadingUI() { binding.shimmerLayout.stopShimmer() binding.shimmerLayout.visibility = View.GONE } @@ -394,7 +361,6 @@ class SearchFragment : if (text.isNotEmpty()) { hideKeyboard(activity as Activity) } - view?.requestFocus() searchHintLayout?.visibility = View.GONE shimmerLayout?.visibility = View.VISIBLE @@ -403,16 +369,16 @@ class SearchFragment : * Set the search text and call for network result. */ searchText = text - repostAuthObjects() + val applicationListRVAdapter = recyclerView?.adapter as ApplicationListRVAdapter + applicationListRVAdapter.setData(mutableListOf()) + loadData() } 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) - } + searchViewModel.getSearchSuggestions(text) } return true } @@ -456,11 +422,10 @@ class SearchFragment : } private fun showKeyboard() { - val inputMethodManager = - requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + val inputMethodManager = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager searchView?.javaClass?.getDeclaredField("mSearchSrcTextView")?.runCatching { isAccessible = true - get(searchView) as EditText + get(searchView)as EditText }?.onSuccess { inputMethodManager.showSoftInput(it, InputMethodManager.SHOW_FORCED) } diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index dbf0b1ba968b7f90a4861ecc74fe1e05d5192228..3557c0772629f13fe6687f65bcb0f5c35b16fa74 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -20,6 +20,7 @@ package foundation.e.apps.ui.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 @@ -29,11 +30,8 @@ import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.fused.FusedAPIRepository import foundation.e.apps.data.fused.GplaySearchResult import foundation.e.apps.data.fused.data.FusedApp -import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.exceptions.CleanApkException -import foundation.e.apps.data.login.exceptions.GPlayException -import foundation.e.apps.data.login.exceptions.UnknownSourceException -import foundation.e.apps.ui.parentFragment.LoadingViewModel +import foundation.e.apps.utils.eventBus.AppEvent +import foundation.e.apps.utils.eventBus.EventBus import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -43,15 +41,14 @@ import kotlin.coroutines.coroutineContext @HiltViewModel class SearchViewModel @Inject constructor( - private val fusedAPIRepository: FusedAPIRepository, -) : LoadingViewModel() { + private val fusedAPIRepository: FusedAPIRepository +) : ViewModel() { val searchSuggest: MutableLiveData?> = MutableLiveData() - val searchResult: MutableLiveData, Boolean>>> = MutableLiveData() - private var lastAuthObjects: List? = null + var authData: AuthData? = null private var nextSubBundle: Set? = null @@ -61,42 +58,29 @@ class SearchViewModel @Inject constructor( private const val DATA_LOAD_ERROR = "Data load error" } - fun getSearchSuggestions(query: String, gPlayAuth: AuthObject.GPlayAuth) { + fun getSearchSuggestions(query: String) { viewModelScope.launch(Dispatchers.IO) { - if (gPlayAuth.result.isSuccess()) + if (query.isNotBlank() && authData != null) { searchSuggest.postValue( fusedAPIRepository.getSearchSuggestions( query, - gPlayAuth.result.data!! + this@SearchViewModel.getNonNullAuthData() ) ) + } } } fun loadData( query: String, - lifecycleOwner: LifecycleOwner, - authObjectList: List, - retryBlock: (failedObjects: List) -> Boolean + lifecycleOwner: LifecycleOwner ) { - if (query.isBlank()) return - - this.lastAuthObjects = authObjectList - 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, null, lifecycleOwner) - return@onLoadData - } - }, retryBlock) + getSearchResults(query, getNonNullAuthData(), lifecycleOwner) } + private fun getNonNullAuthData() = authData ?: AuthData("", "") + /* * 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, @@ -105,32 +89,21 @@ class SearchViewModel @Inject constructor( */ private fun getSearchResults( query: String, - authData: AuthData?, + authData: AuthData, lifecycleOwner: LifecycleOwner ) { viewModelScope.launch(Dispatchers.IO) { - val searchResultSupreme = fusedAPIRepository.getCleanApkSearchResults( - query, - authData ?: AuthData("", "") - ) + val searchResultSupreme = fusedAPIRepository.getCleanApkSearchResults(query, authData) searchResult.postValue(searchResultSupreme) if (!searchResultSupreme.isSuccess()) { - val exception = - if (authData != null) { - GPlayException( - searchResultSupreme.isTimeout(), - searchResultSupreme.message.ifBlank { DATA_LOAD_ERROR } - ) - } else { - CleanApkException( - searchResultSupreme.isTimeout(), - searchResultSupreme.message.ifBlank { DATA_LOAD_ERROR } - ) - } - - handleException(exception) + EventBus.invokeEvent(AppEvent.DataLoadError(searchResultSupreme)) + } + + // if authadata is not available or valid, no need to fetch gplay data + if (authData.email.isEmpty() || authData.authToken.isEmpty()) { + return@launch } if (authData == null) { @@ -148,6 +121,10 @@ class SearchViewModel @Inject constructor( return } + if (authData == null) { + return + } + viewModelScope.launch(Dispatchers.IO) { fetchGplayData(query) } @@ -156,9 +133,8 @@ class SearchViewModel @Inject constructor( private suspend fun fetchGplayData(query: String) { isLoading = true val gplaySearchResult = fusedAPIRepository.getGplaySearchResults(query, nextSubBundle) - if (!gplaySearchResult.isSuccess()) { - handleException(gplaySearchResult.exception ?: UnknownSourceException()) + EventBus.invokeEvent(AppEvent.DataLoadError(gplaySearchResult)) } val isFirstFetch = nextSubBundle == null @@ -188,11 +164,6 @@ class SearchViewModel @Inject constructor( return currentAppList.distinctBy { it.package_name } } - private fun handleException(exception: Exception) { - exceptionsList.add(exception) - exceptionsLiveData.postValue(exceptionsList) - } - /** * @return returns true if there is changes in data, otherwise false */ @@ -200,8 +171,4 @@ class SearchViewModel @Inject constructor( newFusedApps: List, oldFusedApps: List ) = fusedAPIRepository.isAnyFusedAppUpdated(newFusedApps, oldFusedApps) - - fun isAuthObjectListSame(authObjectList: List?): Boolean { - return lastAuthObjects == authObjectList - } } diff --git a/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt b/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt index d3c507cad16681bb6ca6c907bcb3d24d3a156528..00f715511442da03f274c018df01e3cc078034e4 100644 --- a/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt @@ -24,7 +24,6 @@ import android.os.Bundle import android.view.View import android.widget.Toast import androidx.core.view.isVisible -import androidx.fragment.app.activityViewModels import androidx.lifecycle.ViewModelProvider import androidx.navigation.findNavController import androidx.preference.CheckBoxPreference @@ -40,10 +39,10 @@ import foundation.e.apps.BuildConfig import foundation.e.apps.R import foundation.e.apps.data.enums.User import foundation.e.apps.data.fused.UpdatesDao -import foundation.e.apps.data.login.LoginViewModel import foundation.e.apps.databinding.CustomPreferenceBinding import foundation.e.apps.install.updates.UpdatesWorkManager -import foundation.e.apps.ui.MainActivityViewModel +import foundation.e.apps.presentation.login.LoginViewModel +import foundation.e.apps.presentation.settings.SettingsViewModel import foundation.e.apps.utils.SystemInfoProvider import timber.log.Timber import javax.inject.Inject @@ -53,7 +52,6 @@ class SettingsFragment : PreferenceFragmentCompat() { private var _binding: CustomPreferenceBinding? = null private val binding get() = _binding!! - private val mainActivityViewModel: MainActivityViewModel by activityViewModels() private var showAllApplications: CheckBoxPreference? = null private var showFOSSApplications: CheckBoxPreference? = null private var showPWAApplications: CheckBoxPreference? = null @@ -62,6 +60,10 @@ class SettingsFragment : PreferenceFragmentCompat() { ViewModelProvider(requireActivity())[LoginViewModel::class.java] } + val settingsViewModel: SettingsViewModel by lazy { + ViewModelProvider(requireActivity())[SettingsViewModel::class.java] + } + private var sourcesChangedFlag = false @Inject @@ -70,10 +72,6 @@ class SettingsFragment : PreferenceFragmentCompat() { @Inject lateinit var clipboardManager: ClipboardManager - private val user by lazy { - mainActivityViewModel.getUser() - } - private val allSourceCheckboxes by lazy { listOf(showAllApplications, showFOSSApplications, showPWAApplications) } @@ -103,7 +101,6 @@ class SettingsFragment : PreferenceFragmentCompat() { versionInfo?.apply { summary = BuildConfig.VERSION_NAME setOnLongClickListener { - val osVersion = SystemInfoProvider.getSystemProperty(SystemInfoProvider.KEY_LINEAGE_VERSION) val appVersionLabel = getString(R.string.app_version_label) var contents = "$appVersionLabel: $summary" @@ -147,7 +144,9 @@ class SettingsFragment : PreferenceFragmentCompat() { Toast.LENGTH_SHORT ).show() false - } else true + } else { + true + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -164,25 +163,29 @@ class SettingsFragment : PreferenceFragmentCompat() { // This is useful if a user from older App Lounge updates to this version disableDependentCheckbox(onlyUnmeteredNetwork, autoInstallUpdate) - mainActivityViewModel.gPlayAuthData.let { authData -> - mainActivityViewModel.getUser().name.let { user -> - when (user) { - User.ANONYMOUS.name -> { - binding.accountType.setText(R.string.user_anonymous) - binding.email.isVisible = false - } - User.GOOGLE.name -> { - if (!authData.isAnonymous) { - binding.accountType.text = authData.userProfile?.name - binding.email.text = mainActivityViewModel.getUserEmail() - binding.avatar.load(authData.userProfile?.artwork?.url) - } - } - User.NO_GOOGLE.name -> { - binding.accountType.setText(R.string.logged_out) - binding.email.isVisible = false + settingsViewModel.currentUser() + settingsViewModel.currentUserState.observe(viewLifecycleOwner) { + when (it.user) { + User.ANONYMOUS -> { + binding.accountType.setText(R.string.user_anonymous) + binding.email.isVisible = false + } + User.GOOGLE -> { + it.authData?.let { authData -> + binding.accountType.text = authData.userProfile?.name + binding.email.text = authData.email + binding.avatar.load(authData.userProfile?.artwork?.url) + } ?: run { + settingsViewModel.currentAuthData() } } + User.NO_GOOGLE -> { + binding.accountType.setText(R.string.logged_out) + binding.email.isVisible = false + setCheckboxForNoGoogle() + } + + else -> {} } } @@ -193,40 +196,40 @@ class SettingsFragment : PreferenceFragmentCompat() { binding.logout.setOnClickListener { loginViewModel.logout() } + } - if (user == User.NO_GOOGLE) { - /* - * For No-Google mode, do not allow the user to click - * on the option to show GPlay apps. - * Instead show a message and prompt them to login. - */ - showAllApplications?.apply { - setOnPreferenceChangeListener { _, _ -> - Snackbar.make( - binding.root, - R.string.login_to_see_gplay_apps, - Snackbar.LENGTH_SHORT - ).setAction(R.string.login) { - /* - * The login and logout logic is the same, - * it clears all login data (authdata, user selection) - * and restarts the login flow. - * Hence it's the same as logout. - */ - binding.logout.performClick() - }.show() - this.isChecked = false - return@setOnPreferenceChangeListener false - } + private fun setCheckboxForNoGoogle() { + /* + * For No-Google mode, do not allow the user to click + * on the option to show GPlay apps. + * Instead show a message and prompt them to login. + */ + showAllApplications?.apply { + setOnPreferenceChangeListener { _, _ -> + Snackbar.make( + binding.root, + R.string.login_to_see_gplay_apps, + Snackbar.LENGTH_SHORT + ).setAction(R.string.login) { + /* + * The login and logout logic is the same, + * it clears all login data (authdata, user selection) + * and restarts the login flow. + * Hence it's the same as logout. + */ + binding.logout.performClick() + }.show() + this.isChecked = false + return@setOnPreferenceChangeListener false } + } - /* - * For no-google mode, just show a "Login" instead of "Logout". - * The background logic is the same. - */ - binding.logout.apply { - setText(R.string.login) - } + /* + * For no-google mode, just show a "Login" instead of "Logout". + * The background logic is the same. + */ + binding.logout.apply { + setText(R.string.login) } } @@ -252,7 +255,7 @@ class SettingsFragment : PreferenceFragmentCompat() { private fun setCheckboxDependency( checkBox: CheckBoxPreference?, parentCheckBox: CheckBoxPreference?, - parentCheckBoxPreferenceChangeListener: OnPreferenceChangeListener? = null, + parentCheckBoxPreferenceChangeListener: OnPreferenceChangeListener? = null ) { checkBox?.dependency = parentCheckBox?.key parentCheckBox?.onPreferenceChangeListener = @@ -268,7 +271,7 @@ class SettingsFragment : PreferenceFragmentCompat() { private fun copyTextToClipboard( clipboard: ClipboardManager, label: String, - text: String, + text: String ) { val clip = ClipData.newPlainText(label, text) clipboard.setPrimaryClip(clip) @@ -277,9 +280,10 @@ class SettingsFragment : PreferenceFragmentCompat() { override fun onDestroyView() { if (sourcesChangedFlag) { UpdatesDao.addItemsForUpdate(emptyList()) - loginViewModel.startLoginFlow() + loginViewModel.checkLogin() } super.onDestroyView() _binding = null + settingsViewModel.resetSettingState() } } 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..21a82c68bb3b51c1881ec41b36bbbcf63b86a74a 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 @@ -40,8 +40,10 @@ class LocaleChangedBroadcastReceiver : BroadcastReceiver() { @Inject lateinit var dataStoreModule: DataStoreModule + @Inject lateinit var gson: Gson + @Inject lateinit var cache: Cache diff --git a/app/src/main/java/foundation/e/apps/ui/setup/signin/SignInFragment.kt b/app/src/main/java/foundation/e/apps/ui/setup/signin/SignInFragment.kt index 213e84148393d2c6c1a0965bddde44ce7cb9d5c9..3affbbf7b8700055492afc64d81247ef56d5c1ab 100644 --- a/app/src/main/java/foundation/e/apps/ui/setup/signin/SignInFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/setup/signin/SignInFragment.kt @@ -7,10 +7,11 @@ import androidx.lifecycle.ViewModelProvider import androidx.navigation.findNavController import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R -import foundation.e.apps.data.login.LoginViewModel import foundation.e.apps.databinding.FragmentSignInBinding import foundation.e.apps.di.CommonUtilsModule.safeNavigate +import foundation.e.apps.presentation.login.LoginViewModel import foundation.e.apps.utils.showGoogleSignInAlertDialog +import timber.log.Timber @AndroidEntryPoint class SignInFragment : Fragment(R.layout.fragment_sign_in) { @@ -33,10 +34,12 @@ class SignInFragment : Fragment(R.layout.fragment_sign_in) { } binding.anonymousBT.setOnClickListener { - viewModel.initialAnonymousLogin { - view.findNavController() - .safeNavigate(R.id.signInFragment, R.id.action_signInFragment_to_homeFragment) - } + viewModel.authenticateAnonymousUser() + view.findNavController() + .safeNavigate( + R.id.signInFragment, + R.id.action_signInFragment_to_homeFragment + ) } binding.noGoogleBT.setOnClickListener { @@ -45,6 +48,15 @@ class SignInFragment : Fragment(R.layout.fragment_sign_in) { .safeNavigate(R.id.signInFragment, R.id.action_signInFragment_to_homeFragment) } } + + viewModel.loginState.observe(this.viewLifecycleOwner) { loginState -> + if (loginState.isLoggedIn) { + loginState.authData?.let { data -> + + viewModel.updateAuthObjectForAnonymousUser(data) + } ?: run { Timber.d("Auth Data is null") } + } + } } override fun onDestroyView() { 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..5230882b1a1d6cee91e46f6c73bb40421ef70754 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 @@ -14,7 +14,7 @@ import javax.inject.Inject @HiltViewModel class SignInViewModel @Inject constructor( - private val dataStoreModule: DataStoreModule, + private val dataStoreModule: DataStoreModule ) : ViewModel() { val userType: LiveData = dataStoreModule.userType.asLiveData() diff --git a/app/src/main/java/foundation/e/apps/ui/setup/signin/google/GoogleSignInFragment.kt b/app/src/main/java/foundation/e/apps/ui/setup/signin/google/GoogleSignInFragment.kt index 8dc3081b306bee1790d961c2a2ed725765f8cf13..3ecc77843e4792036ea7e4bb96d11d2b30625130 100644 --- a/app/src/main/java/foundation/e/apps/ui/setup/signin/google/GoogleSignInFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/setup/signin/google/GoogleSignInFragment.kt @@ -32,9 +32,9 @@ import androidx.navigation.findNavController import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.gplay.utils.AC2DMUtil -import foundation.e.apps.data.login.LoginViewModel import foundation.e.apps.databinding.FragmentGoogleSigninBinding import foundation.e.apps.di.CommonUtilsModule.safeNavigate +import foundation.e.apps.presentation.login.LoginViewModel @AndroidEntryPoint class GoogleSignInFragment : Fragment(R.layout.fragment_google_signin) { diff --git a/app/src/main/java/foundation/e/apps/ui/setup/tos/TOSFragment.kt b/app/src/main/java/foundation/e/apps/ui/setup/tos/TOSFragment.kt index 82a282be03a64b5606534f2922bf7171a3918c51..4d230de207ee449bf542558b568ba81ce6298a01 100644 --- a/app/src/main/java/foundation/e/apps/ui/setup/tos/TOSFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/setup/tos/TOSFragment.kt @@ -29,7 +29,6 @@ class TOSFragment : Fragment(R.layout.fragment_tos) { webView = binding.tosWebView viewModel.tocStatus.observe(viewLifecycleOwner) { - if (it == true && webView != null) { binding.TOSWarning.visibility = View.GONE binding.TOSButtons.visibility = View.GONE @@ -102,8 +101,11 @@ class TOSFragment : Fragment(R.layout.fragment_tos) { .append(body.toString()) .append("") webView?.loadDataWithBaseURL( - "file:///android_asset/", sb.toString(), - "text/html", "utf-8", null + "file:///android_asset/", + sb.toString(), + "text/html", + "utf-8", + null ) webView?.setOnScrollChangeListener { _, scrollX, scrollY, _, _ -> diff --git a/app/src/main/java/foundation/e/apps/ui/updates/UpdatesFragment.kt b/app/src/main/java/foundation/e/apps/ui/updates/UpdatesFragment.kt index 6c5fd6a6f3bb21c1d2bb185773e4bb57fd2ace09..5ceade8b345946790406ac56b2947a2ba5dcfe5c 100644 --- a/app/src/main/java/foundation/e/apps/ui/updates/UpdatesFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/updates/UpdatesFragment.kt @@ -21,9 +21,10 @@ package foundation.e.apps.ui.updates import android.os.Bundle import android.view.View import android.widget.ImageView -import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController @@ -32,6 +33,7 @@ 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.R import foundation.e.apps.data.ResultSupreme @@ -40,24 +42,22 @@ import foundation.e.apps.data.enums.Status import foundation.e.apps.data.fused.FusedAPIInterface import foundation.e.apps.data.fused.data.FusedApp import foundation.e.apps.data.fusedDownload.models.FusedDownload -import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.exceptions.GPlayException -import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.databinding.FragmentUpdatesBinding import foundation.e.apps.di.CommonUtilsModule.safeNavigate import foundation.e.apps.install.download.data.DownloadProgress import foundation.e.apps.install.pkg.PWAManagerModule import foundation.e.apps.install.updates.UpdatesWorkManager import foundation.e.apps.install.workmanager.InstallWorkManager.INSTALL_WORK_NAME +import foundation.e.apps.presentation.login.LoginViewModel import foundation.e.apps.ui.AppInfoFetchViewModel import foundation.e.apps.ui.AppProgressViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.PrivacyInfoViewModel import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.applicationlist.ApplicationListRVAdapter -import foundation.e.apps.ui.parentFragment.TimeoutFragment import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus +import foundation.e.apps.utils.loadDataOnce import foundation.e.apps.utils.toast import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter @@ -66,7 +66,7 @@ import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint -class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInterface { +class UpdatesFragment : Fragment(R.layout.fragment_updates), FusedAPIInterface { private var _binding: FragmentUpdatesBinding? = null private val binding get() = _binding!! @@ -77,9 +77,15 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte private val updatesViewModel: UpdatesViewModel by viewModels() private val privacyInfoViewModel: PrivacyInfoViewModel by viewModels() private val appInfoFetchViewModel: AppInfoFetchViewModel by viewModels() - override val mainActivityViewModel: MainActivityViewModel by activityViewModels() + private val mainActivityViewModel: MainActivityViewModel by activityViewModels() private val appProgressViewModel: AppProgressViewModel by viewModels() + private val loginViewModel: LoginViewModel by lazy { + ViewModelProvider(requireActivity())[LoginViewModel::class.java] + } + + private var authData: AuthData? = null + private var isDownloadObserverAdded = false override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -87,18 +93,15 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte _binding = FragmentUpdatesBinding.bind(view) binding.button.isEnabled = false - setupListening() - authObjects.observe(viewLifecycleOwner) { - if (it == null) return@observe - if (!updatesViewModel.updatesList.value?.first.isNullOrEmpty()) { - return@observe + mainActivityViewModel.internetConnection.loadDataOnce(this) { + loginViewModel.loginState.observe(viewLifecycleOwner) { + if (!it.isLoggedIn) return@observe + if (!updatesViewModel.updatesList.value?.first.isNullOrEmpty()) { + return@observe + } + loadData() } - loadDataWhenNetworkAvailable(it) - } - - updatesViewModel.exceptionsLiveData.observe(viewLifecycleOwner) { - handleExceptionsCommon(it) } val recyclerView = binding.recyclerView @@ -109,7 +112,7 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte appInfoFetchViewModel, mainActivityViewModel, it, - viewLifecycleOwner, + viewLifecycleOwner ) { fusedApp -> if (!mainActivityViewModel.shouldShowPaidAppsSnackBar(fusedApp)) { showPurchasedAppMessage(fusedApp) @@ -144,11 +147,6 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte stopLoadingUI() Timber.d("===>> observeupdate list called") - if (it.second != ResultStatus.OK) { - val exception = GPlayException(it.second == ResultStatus.TIMEOUT) - val alertDialogBuilder = AlertDialog.Builder(requireContext()) - onTimeout(exception, alertDialogBuilder) - } } } @@ -217,62 +215,21 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte ApplicationDialogFragment( title = getString(R.string.dialog_title_paid_app, fusedApp.name), message = getString( - R.string.dialog_paidapp_message, fusedApp.name, fusedApp.price + R.string.dialog_paidapp_message, + fusedApp.name, + fusedApp.price ), positiveButtonText = getString(R.string.dialog_confirm), positiveButtonAction = { getApplication(fusedApp) }, - cancelButtonText = getString(R.string.dialog_cancel), + cancelButtonText = getString(R.string.dialog_cancel) ).show(childFragmentManager, "UpdatesFragment") } - 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) { + private fun loadData() { showLoadingUI() - updatesViewModel.loadData(authObjectList) { - clearAndRestartGPlayLogin() - true - } + updatesViewModel.loadData(authData) initUpdataAllButton() } @@ -306,14 +263,14 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte updatesViewModel.hasAnyUpdatableApp() && !updatesViewModel.hasAnyPendingAppsForUpdate() } - 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 if ((binding.recyclerView.adapter?.itemCount ?: 0) > 0) { diff --git a/app/src/main/java/foundation/e/apps/ui/updates/UpdatesViewModel.kt b/app/src/main/java/foundation/e/apps/ui/updates/UpdatesViewModel.kt index bf6998e177c766bdd5a3d112b7494ab343238b3c..836d1c0659b7f2c60ef8b5e5da680837401620f7 100644 --- a/app/src/main/java/foundation/e/apps/ui/updates/UpdatesViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/updates/UpdatesViewModel.kt @@ -19,20 +19,20 @@ package foundation.e.apps.ui.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.data.ResultSupreme import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Status import foundation.e.apps.data.fused.FusedAPIRepository import foundation.e.apps.data.fused.data.FusedApp -import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.exceptions.CleanApkException -import foundation.e.apps.data.login.exceptions.GPlayException import foundation.e.apps.data.preference.PreferenceManagerModule import foundation.e.apps.data.updates.UpdatesManagerRepository -import foundation.e.apps.ui.parentFragment.LoadingViewModel +import foundation.e.apps.utils.eventBus.AppEvent +import foundation.e.apps.utils.eventBus.EventBus import kotlinx.coroutines.launch import javax.inject.Inject @@ -41,53 +41,33 @@ class UpdatesViewModel @Inject constructor( private val updatesManagerRepository: UpdatesManagerRepository, private val fusedAPIRepository: FusedAPIRepository, private val preferenceManagerModule: PreferenceManagerModule -) : LoadingViewModel() { +) : ViewModel() { val updatesList: MutableLiveData, ResultStatus?>> = MutableLiveData() fun loadData( - authObjectList: List, - retryBlock: (failedObjects: List) -> Boolean, + authData: AuthData? ) { - 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) + getUpdates(authData ?: AuthData("", "")) } - fun getUpdates(authData: AuthData?) { + private fun getUpdates(authData: AuthData?) { viewModelScope.launch { - val updatesResult = if (authData != null) + val updatesResult = if (authData != null) { updatesManagerRepository.getUpdates(authData) - else updatesManagerRepository.getUpdatesOSS() + } else { + updatesManagerRepository.getUpdatesOSS() + } updatesList.postValue(updatesResult) val status = updatesResult.second if (status != ResultStatus.OK) { - val exception = - if (authData != null && - (authData.aasToken.isNotBlank() || authData.authToken.isNotBlank()) - ) { - GPlayException( - updatesResult.second == ResultStatus.TIMEOUT, - status.message.ifBlank { "Data load error" } - ) - } else CleanApkException( - updatesResult.second == ResultStatus.TIMEOUT, - status.message.ifBlank { "Data load error" } + EventBus.invokeEvent( + AppEvent.DataLoadError( + ResultSupreme.create(status, updatesResult.first) ) - - exceptionsList.add(exception) - exceptionsLiveData.postValue(exceptionsList) + ) } } } diff --git a/app/src/main/java/foundation/e/apps/utils/Extensions.kt b/app/src/main/java/foundation/e/apps/utils/Extensions.kt index aa763f6a816a0d2f6d8d73b135c7e6a1528c247d..0319853599174d98ab2284346b74c51d03e6eb62 100644 --- a/app/src/main/java/foundation/e/apps/utils/Extensions.kt +++ b/app/src/main/java/foundation/e/apps/utils/Extensions.kt @@ -4,6 +4,11 @@ import android.content.Context import android.net.ConnectivityManager import android.net.NetworkCapabilities import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import com.aurora.gplayapi.data.models.AuthData +import com.google.gson.Gson import foundation.e.apps.R import java.text.SimpleDateFormat import java.util.Date @@ -42,3 +47,21 @@ fun Context.isNetworkAvailable(): Boolean { return false } + +fun AuthData.toJsonString(): String = Gson().toJson(this) + +fun String.toAuthData(): AuthData = Gson().fromJson(this, AuthData::class.java) + +fun LiveData.loadDataOnce(lifecycleOwner: LifecycleOwner, observer: Observer) { + this.observe( + lifecycleOwner, + object : Observer { + override fun onChanged(value: Boolean) { + if (value) { + observer.onChanged(true) + this@loadDataOnce.removeObserver(this) + } + } + } + ) +} diff --git a/app/src/main/java/foundation/e/apps/utils/MaterialButtonUtils.kt b/app/src/main/java/foundation/e/apps/utils/MaterialButtonUtils.kt index 6e1ff37456e1ff7f74f137dbce4c1fc1645f2cc8..966d24ba4b7d4998c620819c8d5b2931038f70f5 100644 --- a/app/src/main/java/foundation/e/apps/utils/MaterialButtonUtils.kt +++ b/app/src/main/java/foundation/e/apps/utils/MaterialButtonUtils.kt @@ -42,11 +42,12 @@ private fun MaterialButton.toggleEnableMaterialButton(isEnabled: Boolean, status private fun MaterialButton.getBackgroundTintList(status: Status?) = if (status == Status.INSTALLED || status == Status.UPDATABLE) { ContextCompat.getColorStateList(this.context, R.color.colorAccent) - } else + } else { ContextCompat.getColorStateList(this.context, android.R.color.transparent) + } private fun MaterialButton.getStrokeColor( - isEnabled: Boolean, + isEnabled: Boolean ) = if (isEnabled) { ContextCompat.getColorStateList(this.context, R.color.colorAccent) } else { diff --git a/app/src/main/java/foundation/e/apps/utils/Resource.kt b/app/src/main/java/foundation/e/apps/utils/Resource.kt new file mode 100644 index 0000000000000000000000000000000000000000..bb1b6e27c8256f58c0f582eb40e989979caf7e2c --- /dev/null +++ b/app/src/main/java/foundation/e/apps/utils/Resource.kt @@ -0,0 +1,41 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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 + +/** + * Class represents the different states of a resource for user case layer + */ +sealed class Resource(val data: T? = null, val message: String? = null) { + /** + * Represents a successful state of the resource with data. + * @param data The data associated with the resource. + */ + class Success(data: T) : Resource(data) + + /** + * Represents an error state of the resource with an error message. + * @param message The error message associated with the resource. + */ + class Error(message: String, data: T? = null) : Resource(data, message) + + /** + * Represents a loading state of the resource. + */ + class Loading(data: T? = null) : Resource(data) +} diff --git a/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt b/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt index f7fbf663aa65ce9bef55f94a50169c90eff09064..a250ad595895df311b6e302db558e7c3e71ab902 100644 --- a/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt +++ b/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt @@ -29,6 +29,7 @@ sealed class AppEvent(val data: Any) { class UpdateEvent(result: ResultSupreme.WorkError) : AppEvent(result) class InvalidAuthEvent(authName: String) : AppEvent(authName) + class DataLoadError(result: ResultSupreme) : AppEvent(result) class ErrorMessageEvent(stringResourceId: Int) : AppEvent(stringResourceId) class AppPurchaseEvent(fusedDownload: FusedDownload) : AppEvent(fusedDownload) class NoInternetEvent(isInternetAvailable: Boolean) : AppEvent(isInternetAvailable) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index aafa2cb17d8e926650d38f08b02d34bf1909485d..e3e068561aea1a08f3b4342772f31a741d9ec52d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -102,10 +102,6 @@ Es ist keine Tracker-Information für diese App verfügbar. Die %s App wird zurzeit nicht unterstützt. Ein Grund könnte sein, dass die App noch nicht sehr verbreitet, oder ein anderer Fehler aufgetreten ist. Diese App wird nicht unterstützt! - Einstellungen öffnen - Gewisse Netzwerkfehler verhindern das Holen der Apps. -\n -\nÖffne die Einstellungen und suche nur nach Quelloffenen Apps oder PWAs. Gewisse Netzwerkfehler verhindern das Holen der Apps. Zeit abgelaufen beim Holen von Apps! Deine App wurde nicht gefunden. @@ -134,7 +130,6 @@ \nBitte versuche es später noch einmal. Fehler beim Laden der Apps. Mehr Info - Google Play Apps können nicht angezeigt werden, wenn nur Open Source Apps anzeigen erlaubt sind. Schließen Dies kann verursacht sein durch einen Fehler bei der Generierung oder Verifizierung des Tokens. \n diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ac0af2df98976529b486815962fef5fca1274939..85b5cabe3063c73c04de0da86124208a91f3b86f 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -120,11 +120,7 @@ \nVuelva a intentarlo o inténtelo más tarde. Ha ocurrido un error al cargar aplicaciones. Más información - Abrir ajustes Un problema de red está impidiendo la obtención de aplicaciones. - Un problema de red está impidiendo la obtención de aplicaciones. -\n -\nAbra la configuración para buscar solo aplicaciones de código abierto o PWA. ¡Límite de tiempo buscando aplicaciones! No hay información de rastreo disponible para esta aplicación. Actualización de %1$s aplicaciones completada en %2$s. @@ -137,7 +133,6 @@ Algunas aplicaciones propietarias pueden tener también una versión de código abierto. En ese caso, App Lounge sólo muestra la versión de código abierto, para evitar duplicados. Aplicaciones de código abierto ¿Por qué veo la versión de código abierto\? - Google Play no se puede mostrar cuando sólo se permiten aplicaciones de código abierto. Mostrar más ¡Algo salió mal! Su aplicación no fue encontrada. diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 97a732be583e4e3f553590c23b02187baeab906c..2f1f9a6d15ae7bf55863d82dd05079d74aab6b9f 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -124,16 +124,11 @@ \nYritä uudelleen tai kokeile myöhemmin. Sovellusten lataamisessa tapahtui virhe. Lisätietoja - Avaa asetukset Jokin verkko-ongelma estää kaikkien sovellusten hakemisen. - Jokin verkko-ongelma estää kaikkien sovellusten hakemisen. -\n -\nAvaa asetukset ja etsi vain avoimen lähdekoodin sovelluksia tai PWA:ita. Aikakatkaisu sovellusten hakemisessa! Tutustu PWA:n Päivitysvirhe! Päivitystä ei voida suorittaa koska allekirjoitus on ristiriidassa %1$s ja puhelimeesi asennetun version välillä. Voit korjata tämän poistamalla %1$s ja asentamalla se uudestaan App Loungesta.

Huomautus: Tämä viesti ei tule näkyviin uudelleen.
- Google Play -sovelluksia ei voida näyttää, kun vain avoimen lähdekoodin sovellukset ovat valittuna. Näytä lisää Tapahtui virhe! Sovellusta ei löytynyt. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 7ec04744cb76218a6088e045d9eb7a38b43afb12..726257da66d4aa745e4038f64b807e7ca0c3a3d0 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -118,17 +118,12 @@ \nMerci de réessayer maintenant ou plus tard.
Une erreur est survenue pendant le chargement de l\'application. Plus d\'informations - Ouvrir les Paramètres Des problèmes de connexion empêchent de récupérer toutes les applications. - Des problèmes de connexion empêchent de récupérer toutes les applications. -\n -\nOuvrez les paramètres pour n\'afficher que les applications Open Source et PWA. Expiration du délai de récupération des applications ! Aucune information concernant les pisteurs pour cette application. PWA à découvrir Erreur lors de la mise à jour ! La mise à jour ne peut être appliquée car la signature de la mise à jour de %1$s ne correspond pas à la signature de la version installée sur votre téléphone. Pour y remédier vous pouvez désinstaller %1$s puis la réinstaller depuis App Lounge.

Note : Ce message ne s\'affichera plus.
- Il est impossible d\'afficher les applications Google Play quand seules les applications Open Source sont sélectionnées. Afficher plus Une erreur s\'est produite ! Application non trouvée. diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index f60f9e174cefb942e4bff7823948766a02cd00a7..da6ce781eab113b6ad759f0f48bbb801f5a9c521 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -72,7 +72,6 @@ Uppfærsluvilla! Sótt gögn Uppfærslur - Opna stillingar Nánari upplýsingar Innskráning með Google mistókst! Nafnlaus innskráning mistókst! @@ -109,9 +108,6 @@ Vinsælir PWA-leikir Næ ekki að tengjast! Athugaðu internettenginguna þína og prófaðu svo aftur Féll á tímamörkum við að sækja forrit! - Einhver vandamál í netkerfinu koma í veg fyrir að hægt sé að sækja öll forrit. -\n -\nOpnaðu stillingar til að leita einungis að forritum með opinn grunnkóða eða PWA-vefforritum. Það er ekki nægt pláss tiltækt til að sækja þetta forrit! Að uppfæra allt mistókst. Sjálfvirkar tilraunir til endurtekningar í gangi. Kynntu þér @@ -126,7 +122,6 @@ Uppfærslur forrita verða sjálfkrafa settar inn Uppfærslur forrita verða ekki settar sjálfkrafa inn Bíð eftir gjaldfrjálsu neti - Get ekki birt forrit úr Google Play þegar einungis eru leyfð forrit með opinn grunnkóða. Greining persónuverndar Rekjarar Vegna tímabundinna vandamála er ekki hægt að uppfæra öll forritin þín. Reyndu aftur síðar. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 537c56f538459b9d299cc21a6cb519913afaf0fe..765a9c25d78662ea80041dde2e6967f26c9f46c8 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -108,11 +108,7 @@ Acquista %1$s A causa di un errore di rete, App Lounge non è riuscita a ottenere i dati di accesso a Google. Accesso a Google non riuscito! - Apri Impostazioni Un problema di rete impedisce il recupero di tutte le app. - Un problema di rete impedisce il recupero di tutte le app. -\n -\nAprire le impostazioni per selezionare solo app open source o le PWA. Timeout nel recupero delle app! Informazioni sui tracker di questa app non disponibili. Errore nell\'aggiornamento! @@ -138,7 +134,6 @@ Si è verificato un errore durante il caricamento delle App. Maggiori informazioni Scopri le PWA - Non posso avviare l\'App Google Play se sono permesse solo App open source. Chiudi Mensilmente Settimanalmente diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index c9580b5b3e41dac4536bc0b7906d52cdaf5c187d..8a8962dda32c7d7e819baeccc8baa91dfda020c3 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -22,11 +22,7 @@ \nPrøv igjen nå, eller prøv igjen senere.
Feil oppstod ved applikasjonslasting. Mer informasjon - Åpne innstillinger Nettverksproblem forhindrer henting av alle applikasjoner. - Nettverksproblem forhindrer henting av alle applikasjoner. -\n -\nÅpne instillinger for å kun se etter åpen kildekode-applikasjoner eller PWAer. Tidsavbrudd ved henting av applikasjoner! Kan ikke koble til! Kontroller internettilgangen og prøv igjen Utregnet med <a href=%1$s> Exodus Privacy-analyse @@ -49,7 +45,6 @@ Oppdater alle Åpen kildekode-applikasjoner Hvorfor ser jeg åpen kildekode-versjonen\? - Kan ikke vise Google Play-applikasjoner når kun åpen kildekode-applikasjoner er tillatt. Vis mer Applikasjonen den ble ikke funnet. Der er ikke nok tilgjengelig lagringsplass for å laste ned denne applikasjonen! diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index cd5d91f7b09e469311befa83b9ccb4b7bafebcea..71ecfc7a3fda8c78f7490fe79e51e387ad2d18d4 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -16,7 +16,6 @@ Google aanmelden mislukt! Fout tijdens het laden van apps. Meer info - Open Instellingen Kan niet verbinden! Kijk alstublieft je internet verbinding na en probeer opnieuw Geen tracker informatie beschikbaar voor deze app. Geen trackers gevonden! @@ -40,7 +39,6 @@ %1$d app updates zijn beschikbaar Alles updaten - Kan geen Google Play app tonen wanneer enkel open source apps toegelaten zijn. Toon meer Er is iets fout gegaan! Je applicatie is niet gevonden. @@ -129,9 +127,6 @@ \n \nProbeer a.u.b. opnieuw op een later tijdstip.
Een netwerk probleem verhindert het ophalen van de applicaties. - Een netwerk probleem verhindert het ophalen van de applicaties. -\n -\nOpen Instellingen om enkel voor Open source apps of PWA\'s te kijken. Time-out bij het ophalen van applicaties! Geen \"runtime android\" machtiging gevonden! Top Populaire Games diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index dcd14689e80e25a70def86b8ca424ac01ef15ef1..66b47e8f98c7a38fb2ca1fd658adfa5ff5723912 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -77,11 +77,7 @@ \nПожалуйста, повторите попытку или попробуйте позже.
Произошла ошибка при загрузке приложений. Больше информации - Открыть настройки Какая-то сетевая проблема препятствует загрузке всех приложений. - Какая-то сетевая проблема препятствует получению всех приложений. -\n -\nОткройте настройки, чтобы искать только Open Source приложения или PWA. Тайм-аут загрузки приложений! Не удается подключиться! Пожалуйста, проверьте подключение к Интернету и повторите попытку Информация о трекере для этого приложения отсутствует. @@ -115,7 +111,6 @@ Обновление не может быть применено из-за несоответствия подписи между обновлением %1$s и версией, установленной на вашем телефоне. Чтобы исправить это, вы можете удалить %1$s, а затем снова установить его из App Lounge.

Примечание: это сообщение больше не появится.
Все приложения обновлены Обновить все - Невозможно показать приложение Google Play, когда разрешены только Open Source приложения. Некоторые проприетарные приложения могут также иметь Open Source версию. Когда это происходит, App Lounge показывает только Open Source версию, чтобы избежать дублирования. Почему я вижу Open Source версию\? Open Source приложения diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 5f5a69acf84ac23ce5b5a6cd0e3db856192db34f..39985a720b207a3c4fe87372420e99dabfe5155b 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -56,7 +56,6 @@ Din app hittades inte. Något gick fel! Visa mer - Kan inte visa Google Play-appar när endast appar med öppen källkod tillåts. Varför visas versionen med öppen källkod\? Appar med öppen källkod Hämtar … @@ -106,7 +105,6 @@ Ingen spårare hittades! Ingen information tillgänglig om spårare för denna app. Kan inte ansluta! Kontrollera din internetanslutning och försök igen - Öppna inställningar Det anonyma kontot du använder är inte tillgängligt. Uppdatera sessionen för att få ett annat. UPPADATERA SESSIONEN Ett fel uppstod när appar lästes in. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 12b9d97a85f886257937dbd1a718503b890f2d37..a5da30926a36128fccc7b5fbecb446841eaeb2a5 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -89,7 +89,6 @@ %1$d оновлень застосунків доступно %1$d оновлень застосунків доступно - Неможливо відобразити застосунок Google Play коли дозволені тільки застосунки з відкритим кодом. Оновлено до: %1$s Рейтинг Приватності автоматично обчислюється з дозволів та трекерів, що були знайдені в застосунку.<br /><br /> Його алгоритм обчислення можна <a href=%1$s>побачити тут</a>.<br /><br />Виявлення трекерів робиться за допомогою<a href=%2$s> Інструментів Exodus Privacy</a>.<br /><br />Рейтинг від 0 до 10.<br /><br />Дізнайтесь більше про те, як обчислюється Рейтинг Приватності, які в нього є обмеження та як він може допомогти Вам захистити себе від мікротаргетингу <a href=%3$s>на цій сторінці</a>. Закрити @@ -126,11 +125,7 @@ \nБудь ласка, повторіть спробу, або спробуйте пізніше.
Виникла проблема при завантаженні застосунків. Більше інформації - Відкрити налаштування Деяка проблема мережі не дає отримувати всі застосунки. - Деяка проблема з мережою не дає отримувати всі застосунки. -\n -\nВідкрийте налаштування, щоб перевірити опції \"Тільки застосунки з відкритим кодом\" та \"Тільки прогресивні вебзастосунки\". Час запиту на отримання застосунків вичерпано! Неможливо приєднатись до мережі! Будь ласка, перевірте налаштування Вашої мережі та спробуйте знову Трекерів не було знайдено! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c404c57a8a2786baf15288be3751a73491c97e4b..d319a47e61e3bab5f980e3e7e5c00da1e341bae9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -119,7 +119,6 @@ Your application was not found. Something went wrong! Show more - Cannot show Google Play app when only open source apps are allowed. Why am I seeing the Open Source version? Open Source apps Some proprietary apps may also have an Open Source version. Whenever this happens App Lounge shows the Open Source version only, in order to avoid duplicates. @@ -185,11 +184,7 @@ Timeout fetching applications! - Some network issue is preventing fetching all applications. - \n\nOpen settings to look for Open source apps or PWAs only. - Some network issue is preventing fetching all applications. - Open Settings More info diff --git a/app/src/test/java/foundation/e/apps/FakePkgManagerModule.kt b/app/src/test/java/foundation/e/apps/FakePkgManagerModule.kt index b24f4af3666bcc21e69796318edeeae9cd80127b..f73e27ec16b197a0c946ec800b4a870070c07030 100644 --- a/app/src/test/java/foundation/e/apps/FakePkgManagerModule.kt +++ b/app/src/test/java/foundation/e/apps/FakePkgManagerModule.kt @@ -27,7 +27,7 @@ import foundation.e.apps.install.pkg.PkgManagerModule class FakePkgManagerModule( context: Context, - val gplayApps: List, + val gplayApps: List ) : PkgManagerModule(context) { val applicationInfo = mutableListOf( diff --git a/app/src/test/java/foundation/e/apps/Shared.kt b/app/src/test/java/foundation/e/apps/Shared.kt new file mode 100644 index 0000000000000000000000000000000000000000..95f108bec6885b70965da612d97b6450c968c933 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/Shared.kt @@ -0,0 +1,35 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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 + +import app.lounge.model.AnonymousAuthDataRequestBody +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.utils.SystemInfoProvider +import java.util.Properties + +const val testEmailAddress: String = "eOS@murena.io" +const val loginFailureMessage = "Fail to login" +val testFailureException: Exception = Exception(loginFailureMessage) + +val testAnonymousRequestData = AnonymousAuthDataRequestBody( + properties = Properties(), + userAgent = SystemInfoProvider.getAppBuildInfo() +) + +val testAnonymousResponseData = AuthData(testEmailAddress, "") diff --git a/app/src/test/java/foundation/e/apps/UpdateManagerImptTest.kt b/app/src/test/java/foundation/e/apps/UpdateManagerImptTest.kt index b7a4c8ed76ddf9d70f11dcf75e93d4223e9cf6fc..a65f8b8ffacfd452ec43cdb0ca894243b20d0595 100644 --- a/app/src/test/java/foundation/e/apps/UpdateManagerImptTest.kt +++ b/app/src/test/java/foundation/e/apps/UpdateManagerImptTest.kt @@ -92,8 +92,8 @@ class UpdateManagerImptTest { fusedAPIRepository, faultyAppRepository, preferenceModule, - fdroidRepository, - blockedAppRepository + blockedAppRepository, + fdroidRepository ) } @@ -132,7 +132,7 @@ class UpdateManagerImptTest { package_name = "foundation.e.demotwo", origin = Origin.GPLAY, filterLevel = FilterLevel.NONE - ), + ) ) @Test diff --git a/app/src/test/java/foundation/e/apps/data/UserTest.kt b/app/src/test/java/foundation/e/apps/data/UserTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..6ba7252fec1478dade258c8fdfbafb4c6ebfcb48 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/UserTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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 + +import foundation.e.apps.data.enums.User +import org.junit.Assert +import org.junit.Test + +class UserTest { + + @Test + fun testUserStringEmptyReturnUnavailable() { + val result = User.getUser("") + Assert.assertEquals(User.UNAVAILABLE.name, result.name) + } + + @Test + fun testUserStringGoogleReturnUserAsGoogle() { + val result = User.getUser("Google") + Assert.assertEquals(User.UNAVAILABLE.name, result.name) + } + + @Test + fun testUserStringAnonymousReturnUserAsAnonymous() { + val result = User.getUser("") + Assert.assertEquals(User.UNAVAILABLE.name, result.name) + } + + @Test + fun testUserStringForNoGoogleReturnUserAsNoGoogle() { + // TODO - No idea for No Google + } + + @Test + fun testRandomStringReturnUnavailable() { + val result = User.getUser("dmflmfle") + Assert.assertEquals(User.UNAVAILABLE.name, result.name) + } +} diff --git a/app/src/test/java/foundation/e/apps/domain/login/repository/LoginRepositoryTest.kt b/app/src/test/java/foundation/e/apps/domain/login/repository/LoginRepositoryTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..5e9f368b07952b0fb45b1de44dfd61ed5007a51d --- /dev/null +++ b/app/src/test/java/foundation/e/apps/domain/login/repository/LoginRepositoryTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.domain.login.repository + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import app.lounge.login.anonymous.AnonymousUser +import app.lounge.networking.NetworkResult +import foundation.e.apps.loginFailureMessage +import foundation.e.apps.testAnonymousRequestData +import foundation.e.apps.testAnonymousResponseData +import foundation.e.apps.testEmailAddress +import foundation.e.apps.testFailureException +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class LoginRepositoryTest { + + @Mock + lateinit var anonymousUser: AnonymousUser + + private lateinit var context: Context + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + context = ApplicationProvider.getApplicationContext() + } + + @Test + fun testOnSuccessReturnAuthData() = runTest { + Mockito.`when`( + anonymousUser.requestAuthData(testAnonymousRequestData) + ).thenReturn(NetworkResult.Success(testAnonymousResponseData)) + + val result = LoginRepositoryImpl(context, testAnonymousRequestData.properties, anonymousUser) + .anonymousUser() + + Assert.assertNotNull(result) + Assert.assertEquals(testEmailAddress, result.email) + } + + @Test + fun testOnFailureReturnErrorWithException() = runTest { + Mockito.`when`( + anonymousUser.requestAuthData(testAnonymousRequestData) + ).thenReturn( + NetworkResult.Error( + exception = testFailureException, + code = 1, + errorMessage = loginFailureMessage + ) + ) + runCatching { + LoginRepositoryImpl(context, testAnonymousRequestData.properties, anonymousUser) + .run { anonymousUser() } + }.onFailure { error -> + Assert.assertEquals(testFailureException.message, error.message) + } + } +} diff --git a/app/src/test/java/foundation/e/apps/exodus/AppPrivacyInfoRepositoryImplTest.kt b/app/src/test/java/foundation/e/apps/exodus/AppPrivacyInfoRepositoryImplTest.kt index 399aee6fba6949eb47e3b78c720ccb671dddd231..5b0893fe2667aa46e35407730d54e17fc36c80f1 100644 --- a/app/src/test/java/foundation/e/apps/exodus/AppPrivacyInfoRepositoryImplTest.kt +++ b/app/src/test/java/foundation/e/apps/exodus/AppPrivacyInfoRepositoryImplTest.kt @@ -63,7 +63,7 @@ class AppPrivacyInfoRepositoryImplTest { name = "Demo Three", package_name = "foundation.e.demothree", latest_version_code = 123, - is_pwa = true, + is_pwa = true ) val result = appPrivacyInfoRepository.getAppPrivacyInfo(fusedApp, fusedApp.package_name) assertEquals("getAppPrivacyInfo", true, result.isSuccess()) @@ -78,7 +78,7 @@ class AppPrivacyInfoRepositoryImplTest { name = "Demo Three", package_name = "", latest_version_code = 123, - is_pwa = true, + is_pwa = true ) val result = appPrivacyInfoRepository.getAppPrivacyInfo(fusedApp, fusedApp.package_name) assertEquals("getAppPrivacyInfo", false, result.isSuccess()) @@ -92,7 +92,7 @@ class AppPrivacyInfoRepositoryImplTest { name = "Demo Three", package_name = "a.b.c", latest_version_code = 123, - is_pwa = true, + is_pwa = true ) fakeTrackerDao.trackers.clear() val result = appPrivacyInfoRepository.getAppPrivacyInfo(fusedApp, fusedApp.package_name) diff --git a/app/src/test/java/foundation/e/apps/fused/FusedApiImplTest.kt b/app/src/test/java/foundation/e/apps/fused/FusedApiImplTest.kt index bc91a6ff9f3b743b58d9fd15e64fbc879a2d90fd..4c18526679782bdc2d8ec1a0fafda66db466ea7d 100644 --- a/app/src/test/java/foundation/e/apps/fused/FusedApiImplTest.kt +++ b/app/src/test/java/foundation/e/apps/fused/FusedApiImplTest.kt @@ -443,12 +443,13 @@ class FusedApiImplTest { _id = "113", name = "Demo Three", package_name = "foundation.e.demothree", - latest_version_code = 123, + latest_version_code = 123 ) Mockito.`when`( pkgManagerModule.getPackageStatus( - fusedApp.package_name, fusedApp.latest_version_code + fusedApp.package_name, + fusedApp.latest_version_code ) ).thenReturn(Status.INSTALLED) @@ -462,7 +463,7 @@ class FusedApiImplTest { _id = "113", name = "Demo Three", package_name = "", - latest_version_code = 123, + latest_version_code = 123 ) val filterLevel = fusedAPIImpl.getAppFilterLevel(fusedApp, AUTH_DATA) @@ -533,7 +534,7 @@ class FusedApiImplTest { gPlayAPIRepository.getDownloadInfo( fusedApp.package_name, fusedApp.latest_version_code, - fusedApp.offer_type, + fusedApp.offer_type ) ).thenReturn(listOf()) @@ -553,7 +554,9 @@ class FusedApiImplTest { Mockito.`when`( gPlayAPIRepository.getDownloadInfo( - fusedApp.package_name, fusedApp.latest_version_code, fusedApp.offer_type + fusedApp.package_name, + fusedApp.latest_version_code, + fusedApp.offer_type ) ).thenReturn(listOf()) @@ -573,7 +576,9 @@ class FusedApiImplTest { Mockito.`when`( gPlayAPIRepository.getDownloadInfo( - fusedApp.package_name, fusedApp.latest_version_code, fusedApp.offer_type + fusedApp.package_name, + fusedApp.latest_version_code, + fusedApp.offer_type ) ).thenThrow(RuntimeException()) @@ -662,7 +667,9 @@ class FusedApiImplTest { fun `getCategory when All source is selected`() = runTest { val gplayCategories = listOf(Category(), Category(), Category(), Category()) val openSourcecategories = Categories( - listOf("app one", "app two", "app three", "app four"), listOf("game 1", "game 2"), true + listOf("app one", "app two", "app three", "app four"), + listOf("game 1", "game 2"), + true ) val openSourceResponse = Response.success(openSourcecategories) val pwaCategories = @@ -729,11 +736,14 @@ class FusedApiImplTest { preferenceManagerModule.isOpenSourceelectedFake = true preferenceManagerModule.isGplaySelectedFake = true val gplayFlow: Pair, MutableSet> = Pair( - listOf(App("a.b.c"), App("c.d.e"), App("d.e.f"), App("d.e.g")), mutableSetOf() + listOf(App("a.b.c"), App("c.d.e"), App("d.e.f"), App("d.e.g")), + mutableSetOf() ) setupMockingSearchApp( - packageNameSearchResponse, gplayPackageResult, gplayFlow + packageNameSearchResponse, + gplayPackageResult, + gplayFlow ) val searchResultLiveData = @@ -754,7 +764,8 @@ class FusedApiImplTest { .thenReturn(Status.UNAVAILABLE) Mockito.`when`( cleanApkAppsRepository.getSearchResult( - query = "com.search.package", searchBy = "package_name" + query = "com.search.package", + searchBy = "package_name" ) ).thenReturn(packageNameSearchResponse) formatterMocked.`when` { Formatter.formatFileSize(any(), any()) }.thenReturn("15MB") @@ -818,11 +829,15 @@ class FusedApiImplTest { val gplayPackageResult = App("com.search.package") val gplayFlow: Pair, MutableSet> = Pair( - listOf(App("a.b.c"), App("c.d.e"), App("d.e.f"), App("d.e.g")), mutableSetOf() + listOf(App("a.b.c"), App("c.d.e"), App("d.e.f"), App("d.e.g")), + mutableSetOf() ) setupMockingSearchApp( - packageNameSearchResponse, gplayPackageResult, gplayFlow, true + packageNameSearchResponse, + gplayPackageResult, + gplayFlow, + true ) preferenceManagerModule.isPWASelectedFake = false diff --git a/app/src/test/java/foundation/e/apps/fused/FusedApiRepositoryTest.kt b/app/src/test/java/foundation/e/apps/fused/FusedApiRepositoryTest.kt index 801ae4437c2ef778c07e7fd80d0c709c953040d8..71910fdf72c4e6bb619ceed903f04d7bafbce2a2 100644 --- a/app/src/test/java/foundation/e/apps/fused/FusedApiRepositoryTest.kt +++ b/app/src/test/java/foundation/e/apps/fused/FusedApiRepositoryTest.kt @@ -29,6 +29,7 @@ import org.mockito.kotlin.any class FusedApiRepositoryTest { private lateinit var fusedApiRepository: FusedAPIRepository + @Mock private lateinit var fusedAPIImpl: FusedApiImpl diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt index 8aca9593ea4edd278337082ee14c474d64c87d26..10b8960104b7206591e6946188f54c62b6cb911c 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -20,14 +20,15 @@ package foundation.e.apps.installProcessor import android.content.Context import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import com.aurora.gplayapi.data.models.AuthData +import app.lounge.storage.cache.PersistentConfiguration import foundation.e.apps.data.enums.Status import foundation.e.apps.data.fdroid.FdroidRepository import foundation.e.apps.data.fused.FusedAPIRepository import foundation.e.apps.data.fusedDownload.FusedDownloadRepository import foundation.e.apps.data.fusedDownload.IFusedManager import foundation.e.apps.data.fusedDownload.models.FusedDownload -import foundation.e.apps.data.preference.DataStoreManager +import foundation.e.apps.domain.common.repository.CacheRepositoryImpl +import foundation.e.apps.domain.install.usecase.AppInstallerUseCase import foundation.e.apps.install.notification.StorageNotificationManager import foundation.e.apps.install.workmanager.AppInstallProcessor import foundation.e.apps.util.MainCoroutineRule @@ -68,10 +69,10 @@ class AppInstallProcessorTest { private lateinit var context: Context @Mock - private lateinit var dataStoreManager: DataStoreManager + private lateinit var fusedAPIRepository: FusedAPIRepository @Mock - private lateinit var fusedAPIRepository: FusedAPIRepository + private lateinit var persistentConfiguration: PersistentConfiguration private lateinit var appInstallProcessor: AppInstallProcessor @@ -91,7 +92,7 @@ class AppInstallProcessorTest { fusedDownloadRepository, fakeFusedManagerRepository, fusedAPIRepository, - dataStoreManager, + AppInstallerUseCase(CacheRepositoryImpl(context)), storageNotificationManager ) } @@ -110,7 +111,7 @@ class AppInstallProcessorTest { ): FusedDownload { val fusedDownload = createFusedDownload(packageName, downloadUrlList) fakeFusedDownloadDAO.addDownload(fusedDownload) - Mockito.`when`(dataStoreManager.getAuthData()).thenReturn(AuthData("", "")) + Mockito.`when`(persistentConfiguration.authData).thenReturn("{{aasToken:\"\",email:\"\"}") return fusedDownload } diff --git a/app/src/test/java/foundation/e/apps/installProcessor/FakeFusedManagerRepository.kt b/app/src/test/java/foundation/e/apps/installProcessor/FakeFusedManagerRepository.kt index 9b39077a2319b34ff5f9c7a686b54cec078aa5ba..63713f32f0940005f7cd53457adf2d7901f3a4a2 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/FakeFusedManagerRepository.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/FakeFusedManagerRepository.kt @@ -28,7 +28,7 @@ import kotlinx.coroutines.delay class FakeFusedManagerRepository( private val fusedDownloadDAO: FakeFusedDownloadDAO, fusedManager: IFusedManager, - fdroidRepository: FdroidRepository, + fdroidRepository: FdroidRepository ) : FusedManagerRepository(fusedManager, fdroidRepository) { var isAppInstalled = false var installationStatus = Status.INSTALLED diff --git a/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt b/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt index 662ede447617490667aa1d4fb91f69748a4b082c..c36bdc2d6b1c2b616dfb31d483215d86390af6f9 100644 --- a/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt +++ b/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt @@ -24,8 +24,9 @@ import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.User import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.LoginSourceRepository -import foundation.e.apps.data.login.LoginViewModel -import okhttp3.Cache +import foundation.e.apps.domain.login.usecase.NoGoogleModeUseCase +import foundation.e.apps.domain.login.usecase.UserLoginUseCase +import foundation.e.apps.presentation.login.LoginViewModel import org.junit.Before import org.junit.Rule import org.junit.Test @@ -36,8 +37,6 @@ class LoginViewModelTest { @Mock private lateinit var loginSourceRepository: LoginSourceRepository - @Mock - private lateinit var cache: Cache private lateinit var loginViewModel: LoginViewModel @@ -45,17 +44,28 @@ class LoginViewModelTest { @get:Rule val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule() + @Mock + lateinit var mockUserLoginUseCase: UserLoginUseCase + + @Mock + lateinit var mockNoGoogleModeUseCase: NoGoogleModeUseCase + @Before fun setup() { MockitoAnnotations.openMocks(this) - loginViewModel = LoginViewModel(loginSourceRepository, cache) + loginViewModel = LoginViewModel( + loginSourceRepository, + mockUserLoginUseCase, + mockNoGoogleModeUseCase + ) } @Test fun testMarkInvalidAuthObject() { val authObjectList = mutableListOf( AuthObject.GPlayAuth( - ResultSupreme.Success(AuthData("aa@aa.com", "feri4234")), User.GOOGLE + ResultSupreme.Success(AuthData("aa@aa.com", "feri4234")), + User.GOOGLE ) ) loginViewModel.authObjects.value = authObjectList diff --git a/modules/.gitignore b/modules/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..42afabfd2abebf31384ca7797186a27a4b7dbee8 --- /dev/null +++ b/modules/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/modules/build.gradle b/modules/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..780e770a07aeabc91dcf80a97a3b2bd7b6022c18 --- /dev/null +++ b/modules/build.gradle @@ -0,0 +1,95 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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 . + */ + + +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' + id("com.google.dagger.hilt.android") +} + +ext { + android_compile_sdk_version = 33 + android_target_sdk_version = 33 + core_version = '1.10.1' + gson_version = '2.9.0' + kotlin_reflection = '1.8.20' + retrofit_version = '2.9.0' + retrofit_interceptor_version = '5.0.0-alpha.2' + kotlin_coroutines_core = '1.7.2' + google_play_api = '3.0.1' + protobuf_java = '3.19.3' + javax_version = '1' + dagger_hilt_version = '2.46.1' +} + +android { + namespace 'app.lounge' + compileSdk android_compile_sdk_version + + defaultConfig { + minSdk 24 + targetSdk android_target_sdk_version + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + debug { minifyEnabled false } + releaseDev { minifyEnabled false } + releaseStable { minifyEnabled false } + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = '17' + } +} + +dependencies { + + implementation "androidx.core:core-ktx:$core_version" + implementation "com.google.code.gson:gson:$gson_version" + implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_reflection" + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_core") + + implementation("com.squareup.retrofit2:retrofit:$retrofit_version") + implementation("com.squareup.retrofit2:converter-gson:$retrofit_version") + implementation("com.squareup.retrofit2:converter-scalars:$retrofit_version") + implementation("com.squareup.okhttp3:logging-interceptor:$retrofit_interceptor_version") + + implementation("foundation.e:gplayapi:$google_play_api") + implementation("com.google.protobuf:protobuf-java:$protobuf_java") + + implementation("javax.inject:javax.inject:$javax_version") + + implementation("com.google.dagger:hilt-android:$dagger_hilt_version") + kapt("com.google.dagger:hilt-android-compiler:$dagger_hilt_version") + + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' +} \ No newline at end of file diff --git a/modules/consumer-rules.pro b/modules/consumer-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/modules/proguard-rules.pro b/modules/proguard-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..481bb434814107eb79d7a30b676d344b0df2f8ce --- /dev/null +++ b/modules/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/modules/src/androidTest/java/app/lounge/AnonymousUserAPITest.kt b/modules/src/androidTest/java/app/lounge/AnonymousUserAPITest.kt new file mode 100644 index 0000000000000000000000000000000000000000..d12ea1fefaf52023b3a3e2b9b1a91bd93e28c8d1 --- /dev/null +++ b/modules/src/androidTest/java/app/lounge/AnonymousUserAPITest.kt @@ -0,0 +1,118 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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 app.lounge + +import app.lounge.model.AnonymousAuthDataRequestBody +import app.lounge.login.anonymous.AnonymousUser +import app.lounge.login.anonymous.AnonymousUserRetrofitAPI +import app.lounge.login.anonymous.AnonymousUserRetrofitImpl +import app.lounge.networking.NetworkResult +import com.aurora.gplayapi.data.models.AuthData +import kotlinx.coroutines.runBlocking +import okhttp3.OkHttpClient +import org.junit.Test +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.Properties +import java.util.concurrent.TimeUnit + +class AnonymousUserAPITest { + + companion object { + var authData: AuthData? = null + } + + @Test + fun testOnSuccessReturnsAuthData() = runBlocking { + val response = anonymousUser.requestAuthData( + anonymousAuthDataRequestBody = requestBodyData, + ) + when(response){ + is NetworkResult.Success -> authData = response.data + is NetworkResult.Error -> { } + } + + assert(authData is AuthData) { "Assert!! Success must return data" } + } + + + private fun retrofitTestConfig( + baseUrl: String, + timeoutInMillisecond: Long = 10000L + ): Retrofit = Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .client( + OkHttpClient.Builder() + .callTimeout(timeoutInMillisecond, TimeUnit.MILLISECONDS) + .build() + ) + .build() + + private val eCloudTest = retrofitTestConfig(AnonymousUserRetrofitAPI.tokenBaseURL) + + private val anonymousUser: AnonymousUser = AnonymousUserRetrofitImpl(eCloud = eCloudTest) + + private val requestBodyData = AnonymousAuthDataRequestBody( + properties = testSystemProperties, + userAgent = testUserAgent + ) +} + +const val testUserAgent: String = "{\"package\":\"foundation.e.apps.debug\",\"version\":\"2.5.5.debug\",\"device\":\"coral\",\"api\":32,\"os_version\":\"1.11-s-20230511288805-dev-coral\",\"build_id\":\"319e25cd.20230630224839\"}" + +val testSystemProperties = Properties().apply { + setProperty("UserReadableName", "coral-default") + setProperty("Build.HARDWARE", "coral") + setProperty("Build.RADIO", "g8150-00123-220402-B-8399852") + setProperty("Build.FINGERPRINT","google/coral/coral:12/SQ3A.220705.003.A1/8672226:user/release-keys") + setProperty("Build.BRAND", "google") + setProperty("Build.DEVICE", "coral") + setProperty("Build.VERSION.SDK_INT", "32") + setProperty("Build.VERSION.RELEASE", "12") + setProperty("Build.MODEL", "Pixel 4 XL") + setProperty("Build.MANUFACTURER", "Google") + setProperty("Build.PRODUCT", "coral") + setProperty("Build.ID", "SQ3A.220705.004") + setProperty("Build.BOOTLOADER", "c2f2-0.4-8351033") + setProperty("TouchScreen", "3") + setProperty("Keyboard", "1") + setProperty("Navigation", "1") + setProperty("ScreenLayout", "2") + setProperty("HasHardKeyboard", "false") + setProperty("HasFiveWayNavigation", "false") + setProperty("Screen.Density", "560") + setProperty("Screen.Width", "1440") + setProperty("Screen.Height", "2984") + setProperty("Platforms", "arm64-v8a,armeabi-v7a,armeabi") + setProperty("Features", "android.hardware.sensor.proximity,com.verizon.hardware.telephony.lte,com.verizon.hardware.telephony.ehrpd,android.hardware.sensor.accelerometer,android.software.controls,android.hardware.faketouch,com.google.android.feature.D2D_CABLE_MIGRATION_FEATURE,android.hardware.telephony.euicc,android.hardware.reboot_escrow,android.hardware.usb.accessory,android.hardware.telephony.cdma,android.software.backup,android.hardware.touchscreen,android.hardware.touchscreen.multitouch,android.software.print,org.lineageos.weather,android.software.activities_on_secondary_displays,android.hardware.wifi.rtt,com.google.android.feature.PIXEL_2017_EXPERIENCE,android.software.voice_recognizers,android.software.picture_in_picture,android.hardware.sensor.gyroscope,android.hardware.audio.low_latency,android.software.vulkan.deqp.level,android.software.cant_save_state,com.google.android.feature.PIXEL_2018_EXPERIENCE,android.hardware.security.model.compatible,com.google.android.feature.PIXEL_2019_EXPERIENCE,android.hardware.opengles.aep,org.lineageos.livedisplay,org.lineageos.profiles,android.hardware.bluetooth,android.hardware.camera.autofocus,android.hardware.telephony.gsm,android.hardware.telephony.ims,android.software.incremental_delivery,android.software.sip.voip,android.hardware.se.omapi.ese,android.software.opengles.deqp.level,android.hardware.usb.host,android.hardware.audio.output,android.software.verified_boot,android.hardware.camera.flash,android.hardware.camera.front,android.hardware.sensor.hifi_sensors,com.google.android.apps.photos.PIXEL_2019_PRELOAD,android.hardware.se.omapi.uicc,android.hardware.strongbox_keystore,android.hardware.screen.portrait,android.hardware.nfc,com.google.android.feature.TURBO_PRELOAD,com.nxp.mifare,android.hardware.sensor.stepdetector,android.software.home_screen,android.hardware.context_hub,android.hardware.microphone,android.software.autofill,org.lineageos.hardware,org.lineageos.globalactions,android.software.securely_removes_users,com.google.android.feature.PIXEL_EXPERIENCE,android.hardware.bluetooth_le,android.hardware.sensor.compass,com.google.android.feature.GOOGLE_FI_BUNDLED,android.hardware.touchscreen.multitouch.jazzhand,android.hardware.sensor.barometer,android.software.app_widgets,android.hardware.telephony.carrierlock,android.software.input_methods,android.hardware.sensor.light,android.hardware.vulkan.version,android.software.companion_device_setup,android.software.device_admin,com.google.android.feature.WELLBEING,android.hardware.wifi.passpoint,android.hardware.camera,org.lineageos.trust,android.hardware.device_unique_attestation,android.hardware.screen.landscape,android.software.device_id_attestation,com.google.android.feature.AER_OPTIMIZED,android.hardware.ram.normal,org.lineageos.android,com.google.android.feature.PIXEL_2019_MIDYEAR_EXPERIENCE,android.software.managed_users,android.software.webview,android.hardware.sensor.stepcounter,android.hardware.camera.capability.manual_post_processing,android.hardware.camera.any,android.hardware.camera.capability.raw,android.hardware.vulkan.compute,android.software.connectionservice,android.hardware.touchscreen.multitouch.distinct,android.hardware.location.network,android.software.cts,android.software.sip,android.hardware.camera.capability.manual_sensor,android.software.app_enumeration,android.hardware.camera.level.full,android.hardware.identity_credential,android.hardware.wifi.direct,android.software.live_wallpaper,android.software.ipsec_tunnels,org.lineageos.settings,android.hardware.sensor.assist,android.hardware.audio.pro,android.hardware.nfc.hcef,android.hardware.nfc.uicc,android.hardware.location.gps,android.sofware.nfc.beam,android.software.midi,android.hardware.nfc.any,android.hardware.nfc.ese,android.hardware.nfc.hce,android.hardware.wifi,android.hardware.location,android.hardware.vulkan.level,android.hardware.wifi.aware,android.software.secure_lock_screen,android.hardware.biometrics.face,android.hardware.telephony,android.software.file_based_encryption") + setProperty("Locales", "af,af_ZA,am,am_ET,ar,ar_EG,ar_XB,as,ast_ES,az,be,bg,bg_BG,bn,bs,ca,ca_ES,cs,cs_CZ,cy,da,da_DK,de,de_DE,el,el_GR,en,en_AU,en_CA,en_GB,en_IN,en_US,en_XA,en_XC,es,es_419,es_ES,es_US,et,eu,fa,fa_IR,fi,fi_FI,fil,fil_PH,fr,fr_CA,fr_FR,gd,gl,gu,hi,hi_IN,hr,hr_HR,hu,hu_HU,hy,in,in_ID,is,it,it_IT,iw,iw_IL,ja,ja_JP,ka,kk,km,kn,ko,ko_KR,ky,lo,lt,lt_LT,lv,lv_LV,mk,ml,mn,mr,ms,ms_MY,my,nb,nb_NO,ne,nl,nl_NL,or,pa,pl,pl_PL,pt,pt_BR,pt_PT,ro,ro_RO,ru,ru_RU,si,sk,sk_SK,sl,sl_SI,sq,sr,sr_Latn,sr_RS,sv,sv_SE,sw,sw_TZ,ta,te,th,th_TH,tr,tr_TR,uk,uk_UA,ur,uz,vi,vi_VN,zh_CN,zh_HK,zh_TW,zu,zu_ZA") + setProperty("SharedLibraries", "android.test.base,android.test.mock,com.vzw.apnlib,android.hidl.manager-V1.0-java,qti-telephony-hidl-wrapper,libfastcvopt.so,google-ril,qti-telephony-utils,com.android.omadm.radioconfig,libcdsprpc.so,android.hidl.base-V1.0-java,com.qualcomm.qmapbridge,libairbrush-pixel.so,com.google.android.camera.experimental2019,libOpenCL-pixel.so,libadsprpc.so,com.android.location.provider,android.net.ipsec.ike,com.android.future.usb.accessory,libsdsprpc.so,android.ext.shared,javax.obex,izat.xt.srv,com.google.android.gms,lib_aion_buffer.so,com.qualcomm.uimremoteclientlibrary,libqdMetaData.so,com.qualcomm.uimremoteserverlibrary,com.qualcomm.qcrilhook,android.test.runner,org.apache.http.legacy,com.google.android.camera.extensions,com.google.android.hardwareinfo,com.android.cts.ctsshim.shared_library,com.android.nfc_extras,com.android.media.remotedisplay,com.android.mediadrm.signer,com.qualcomm.qti.imscmservice-V2.0-java,qti-telephony-hidl-wrapper-prd,com.qualcomm.qti.imscmservice-V2.1-java,com.qualcomm.qti.imscmservice-V2.2-java") + setProperty("GL.Version", "196610") + setProperty("GL.Extensions", ",GL_AMD_compressed_ATC_texture,GL_AMD_performance_monitor,GL_ANDROID_extension_pack_es31a,GL_APPLE_texture_2D_limited_npot,GL_ARB_vertex_buffer_object,GL_ARM_shader_framebuffer_fetch_depth_stencil,GL_EXT_EGL_image_array,GL_EXT_EGL_image_external_wrap_modes,GL_EXT_EGL_image_storage,GL_EXT_YUV_target,GL_EXT_blend_func_extended,GL_EXT_blit_framebuffer_params,GL_EXT_buffer_storage,GL_EXT_clip_control,GL_EXT_clip_cull_distance,GL_EXT_color_buffer_float,GL_EXT_color_buffer_half_float,GL_EXT_copy_image,GL_EXT_debug_label,GL_EXT_debug_marker,GL_EXT_discard_framebuffer,GL_EXT_disjoint_timer_query,GL_EXT_draw_buffers_indexed,GL_EXT_external_buffer,GL_EXT_fragment_invocation_density,GL_EXT_geometry_shader,GL_EXT_gpu_shader5,GL_EXT_memory_object,GL_EXT_memory_object_fd,GL_EXT_multisampled_render_to_texture,GL_EXT_multisampled_render_to_texture2,GL_EXT_primitive_bounding_box,GL_EXT_protected_textures,GL_EXT_read_format_bgra,GL_EXT_robustness,GL_EXT_sRGB,GL_EXT_sRGB_write_control,GL_EXT_shader_framebuffer_fetch,GL_EXT_shader_io_blocks,GL_EXT_shader_non_constant_global_initializers,GL_EXT_tessellation_shader,GL_EXT_texture_border_clamp,GL_EXT_texture_buffer,GL_EXT_texture_cube_map_array,GL_EXT_texture_filter_anisotropic,GL_EXT_texture_format_BGRA8888,GL_EXT_texture_format_sRGB_override,GL_EXT_texture_norm16,GL_EXT_texture_sRGB_R8,GL_EXT_texture_sRGB_decode,GL_EXT_texture_type_2_10_10_10_REV,GL_KHR_blend_equation_advanced,GL_KHR_blend_equation_advanced_coherent,GL_KHR_debug,GL_KHR_no_error,GL_KHR_robust_buffer_access_behavior,GL_KHR_texture_compression_astc_hdr,GL_KHR_texture_compression_astc_ldr,GL_NV_shader_noperspective_interpolation,GL_OES_EGL_image,GL_OES_EGL_image_external,GL_OES_EGL_image_external_essl3,GL_OES_EGL_sync,GL_OES_blend_equation_separate,GL_OES_blend_func_separate,GL_OES_blend_subtract,GL_OES_compressed_ETC1_RGB8_texture,GL_OES_compressed_paletted_texture,GL_OES_depth24,GL_OES_depth_texture,GL_OES_depth_texture_cube_map,GL_OES_draw_texture,GL_OES_element_index_uint,GL_OES_framebuffer_object,GL_OES_get_program_binary,GL_OES_matrix_palette,GL_OES_packed_depth_stencil,GL_OES_point_size_array,GL_OES_point_sprite,GL_OES_read_format,GL_OES_rgb8_rgba8,GL_OES_sample_shading,GL_OES_sample_variables,GL_OES_shader_image_atomic,GL_OES_shader_multisample_interpolation,GL_OES_standard_derivatives,GL_OES_stencil_wrap,GL_OES_surfaceless_context,GL_OES_texture_3D,GL_OES_texture_compression_astc,GL_OES_texture_cube_map,GL_OES_texture_env_crossbar,GL_OES_texture_float,GL_OES_texture_float_linear,GL_OES_texture_half_float,GL_OES_texture_half_float_linear,GL_OES_texture_mirrored_repeat,GL_OES_texture_npot,GL_OES_texture_stencil8,GL_OES_texture_storage_multisample_2d_array,GL_OES_texture_view,GL_OES_vertex_array_object,GL_OES_vertex_half_float,GL_OVR_multiview,GL_OVR_multiview2,GL_OVR_multiview_multisampled_render_to_texture,GL_QCOM_YUV_texture_gather,GL_QCOM_alpha_test,GL_QCOM_extended_get,GL_QCOM_motion_estimation,GL_QCOM_shader_framebuffer_fetch_noncoherent,GL_QCOM_shader_framebuffer_fetch_rate,GL_QCOM_texture_foveated,GL_QCOM_texture_foveated_subsampled_layout,GL_QCOM_tiled_rendering,GL_QCOM_validate_shader_binary") + setProperty("Client", "android-google") + setProperty("GSF.version", "223616055") + setProperty("Vending.version", "82151710") + setProperty("Vending.versionString", "21.5.17-21 [0] [PR] 326734551") + setProperty("Roaming", "mobile-notroaming") + setProperty("TimeZone", "UTC-10") + setProperty("CellOperator", "310") + setProperty("SimOperator", "38") +} + diff --git a/modules/src/androidTest/java/app/lounge/PersistentStorageTest.kt b/modules/src/androidTest/java/app/lounge/PersistentStorageTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..4705cddcd121dd97c12c3ce15ca060e474d1346a --- /dev/null +++ b/modules/src/androidTest/java/app/lounge/PersistentStorageTest.kt @@ -0,0 +1,134 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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 app.lounge + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import app.lounge.storage.cache.PersistentConfiguration +import app.lounge.storage.cache.PersistenceKey +import app.lounge.storage.cache.configurations +import com.google.gson.Gson +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.reflect.KClassifier + +@RunWith(AndroidJUnit4::class) +class PersistentStorageTest { + + private lateinit var testConfiguration: PersistentConfiguration + + @Before + fun setupPersistentConfiguration(){ + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + testConfiguration = appContext.configurations + } + + @Test + fun testOnMethodInvokeReturnCorrectType(){ + PersistenceKey.values().toList().forEach { persistenceKey -> + + val propertyReturnType = testConfiguration.getPropertyReturnType(persistenceKey.name) + val propertyValue = testConfiguration.callMethod(persistenceKey.name) + + when(propertyReturnType){ + Int::class -> Assert.assertNotEquals(propertyValue, 0) + String::class -> Assert.assertNotEquals(propertyValue, null) + Boolean::class -> Assert.assertNotEquals(propertyValue, null) + } + } + } + + @Test + fun testOnSetPersistentKeyReturnsSameExpectedValue() { + PersistenceKey.values().toList().forEach { persistentKey -> + val returnType: KClassifier? = testConfiguration.getPropertyReturnType(persistentKey.name) + when(persistentKey) { + PersistenceKey.updateInstallAuto -> testConfiguration.updateInstallAuto = testBooleanValue + PersistenceKey.updateCheckIntervals -> testConfiguration.updateCheckIntervals = testIntValue + PersistenceKey.updateAppsFromOtherStores -> testConfiguration.updateAppsFromOtherStores = testBooleanValue + PersistenceKey.showAllApplications -> testConfiguration.showAllApplications = testBooleanValue + PersistenceKey.showPWAApplications -> testConfiguration.showPWAApplications = testBooleanValue + PersistenceKey.showFOSSApplications -> testConfiguration.showFOSSApplications = testBooleanValue + PersistenceKey.authData -> testConfiguration.authData = testStringValue + PersistenceKey.email -> testConfiguration.email = testStringValue + PersistenceKey.oauthtoken -> testConfiguration.oauthtoken = testStringValue + PersistenceKey.userType -> testConfiguration.userType = testStringValue + PersistenceKey.tocStatus -> testConfiguration.tocStatus = testBooleanValue + PersistenceKey.tosversion -> testConfiguration.tosversion = testStringValue + } + testConfiguration.evaluateValue(classifier = returnType, key = persistentKey) + } + } + + @Test + fun testSetStringJsonReturnSerializedObject() { + testConfiguration.authData = sampleTokensJson + val result: String = testConfiguration.callMethod("authData") as String + + val expectedTokens = Tokens( + aasToken = "ya29.a0AWY7Cknq6ueSCNVN6F7jB", + ac2dmToken = "ABFEt1X6tnDsra6QUsjVsjIWz0T5F", + authToken = "gXKyx_qC5EO64ECheZFonpJOtxbY") + + Assert.assertEquals( + "Tokens should match with $expectedTokens", + expectedTokens, + result.toTokens() + ) + } +} + +// Utils function for `Persistence` Testcase only + +private inline fun T.callMethod(name: String, vararg args: Any?): Any? = + T::class + .members + .firstOrNull { it.name == name } + ?.call(this, *args) + +private inline fun T.getPropertyReturnType(name: String): KClassifier? = + T::class + .members + .firstOrNull { it.name == name } + ?.returnType + ?.classifier +private fun PersistentConfiguration.evaluateValue(classifier: KClassifier?, key: PersistenceKey) { + when(classifier){ + Int::class -> Assert.assertEquals( + "Expected to be `$testIntValue`", testIntValue, this.callMethod(key.name) as Int) + String::class -> Assert.assertEquals( + "Expected to be `$testStringValue`", testStringValue, this.callMethod(key.name) as String) + Boolean::class -> Assert.assertTrue( + "Expected to be `$testBooleanValue`", this.callMethod(key.name) as Boolean) + } +} + +// region test sample data for shared preference verification +private val testIntValue : Int = (1..10).random() +private const val testStringValue: String = "quick brown fox jump over the lazy dog" +private const val testBooleanValue: Boolean = true + +private const val sampleTokensJson = "{\"aasToken\": \"ya29.a0AWY7Cknq6ueSCNVN6F7jB\"," + + "\"ac2dmToken\": \"ABFEt1X6tnDsra6QUsjVsjIWz0T5F\"," + + "\"authToken\": \"gXKyx_qC5EO64ECheZFonpJOtxbY\"}" +data class Tokens(val aasToken: String, val ac2dmToken: String, val authToken: String) +fun String.toTokens() = Gson().fromJson(this, Tokens::class.java) +// endregion \ No newline at end of file diff --git a/modules/src/main/AndroidManifest.xml b/modules/src/main/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..a03c6a1db4edd4bf038856691e33e1ccacffd925 --- /dev/null +++ b/modules/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/modules/src/main/java/app/lounge/di/NetworkModule.kt b/modules/src/main/java/app/lounge/di/NetworkModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..63d354cf1045e668bd09e3a3194edb9e9335de69 --- /dev/null +++ b/modules/src/main/java/app/lounge/di/NetworkModule.kt @@ -0,0 +1,99 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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 app.lounge.di + +import app.lounge.login.anonymous.AnonymousUser +import app.lounge.login.anonymous.AnonymousUserRetrofitAPI +import app.lounge.login.anonymous.AnonymousUserRetrofitImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit +import javax.inject.Named +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object NetworkModule { + + private const val HTTP_TIMEOUT_IN_SECOND = 10L + + private fun retrofit( + okHttpClient: OkHttpClient, + baseUrl: String + ) : Retrofit { + return Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build() + } + + @Provides + @Singleton + @Named("ECloudRetrofit") + internal fun provideECloudRetrofit( + okHttpClient: OkHttpClient + ): Retrofit { + return retrofit( + okHttpClient = okHttpClient, + baseUrl = AnonymousUserRetrofitAPI.tokenBaseURL + ) + } + + @Provides + @Singleton + @Named("privateOkHttpClient") + internal fun providesOkHttpClient( + httpLogger: HttpLoggingInterceptor + ): OkHttpClient { + return OkHttpClient.Builder() + .addNetworkInterceptor(httpLogger) + .callTimeout(HTTP_TIMEOUT_IN_SECOND, TimeUnit.SECONDS) + .build() + } + + @Provides + @Singleton + internal fun providesHttpLogger() : HttpLoggingInterceptor { + return run { + val httpLoggingInterceptor = HttpLoggingInterceptor() + httpLoggingInterceptor.apply { + httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY + } + } + } + + @Provides + @Singleton + fun provideAnonymousUser( + @Named("ECloudRetrofit") ecloud: Retrofit + ) : AnonymousUser { + return AnonymousUserRetrofitImpl( + eCloud = ecloud + ) + } + +} \ No newline at end of file diff --git a/modules/src/main/java/app/lounge/extension/Extension.kt b/modules/src/main/java/app/lounge/extension/Extension.kt new file mode 100644 index 0000000000000000000000000000000000000000..98ac278b0dc999d699c2f23573305e79bf4ef1dd --- /dev/null +++ b/modules/src/main/java/app/lounge/extension/Extension.kt @@ -0,0 +1,29 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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 app.lounge.extension + +import com.google.gson.Gson +import java.util.Properties + +/** + * Convert Properties parameter to byte array + * @return Byte Array of Properties + * */ +fun Properties.toByteArray() = Gson().toJson(this).toByteArray() \ No newline at end of file diff --git a/modules/src/main/java/app/lounge/login/anonymous/AnonymousUser.kt b/modules/src/main/java/app/lounge/login/anonymous/AnonymousUser.kt new file mode 100644 index 0000000000000000000000000000000000000000..d02bb05c42e42442c3a6422f564c56e912ac0edd --- /dev/null +++ b/modules/src/main/java/app/lounge/login/anonymous/AnonymousUser.kt @@ -0,0 +1,31 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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 app.lounge.login.anonymous + +import app.lounge.model.AnonymousAuthDataRequestBody +import app.lounge.networking.NetworkResult +import com.aurora.gplayapi.data.models.AuthData + +interface AnonymousUser { + suspend fun requestAuthData( + anonymousAuthDataRequestBody: AnonymousAuthDataRequestBody + ) : NetworkResult +} + diff --git a/modules/src/main/java/app/lounge/login/anonymous/AnonymousUserRetrofitImpl.kt b/modules/src/main/java/app/lounge/login/anonymous/AnonymousUserRetrofitImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..20b9403ae950492a1322f133aa82646eb2dfa9d5 --- /dev/null +++ b/modules/src/main/java/app/lounge/login/anonymous/AnonymousUserRetrofitImpl.kt @@ -0,0 +1,93 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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 app.lounge.login.anonymous + +import app.lounge.extension.toByteArray +import app.lounge.model.AnonymousAuthDataRequestBody +import app.lounge.networking.NetworkResult +import app.lounge.networking.fetch +import com.aurora.gplayapi.data.models.AuthData +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.http.Body +import retrofit2.http.HeaderMap +import retrofit2.http.POST +import javax.inject.Inject +import javax.inject.Singleton + +interface AnonymousUserRetrofitAPI { + + companion object { + const val tokenBaseURL: String = "https://eu.gtoken.ecloud.global" + } + + @POST(Path.authData) + suspend fun authDataRequest( + @HeaderMap headers: Map, + @Body requestBody: RequestBody + ): Response + + object Header { + val authData: (() -> String) -> Map = { + mapOf(Pair("User-Agent", it.invoke())) + } + } + + private object Path { + const val authData = "/" + } + +} + +@Singleton +class AnonymousUserRetrofitImpl @Inject constructor( + val eCloud: Retrofit +) : AnonymousUser { + + private val eCloudRetrofitAPI = eCloud.create( + AnonymousUserRetrofitAPI::class.java + ) + + override suspend fun requestAuthData( + anonymousAuthDataRequestBody: AnonymousAuthDataRequestBody + ): NetworkResult { + val requestBody: RequestBody = + anonymousAuthDataRequestBody.properties.toByteArray().let { result -> + result.toRequestBody( + contentType = "application/json".toMediaTypeOrNull(), + offset = 0, + byteCount = result.size + ) + } + + return fetch { + eCloudRetrofitAPI.authDataRequest( + requestBody = requestBody, + headers = AnonymousUserRetrofitAPI.Header.authData { + anonymousAuthDataRequestBody.userAgent + } + ) + } + } + +} \ No newline at end of file diff --git a/modules/src/main/java/app/lounge/model/Model.kt b/modules/src/main/java/app/lounge/model/Model.kt new file mode 100644 index 0000000000000000000000000000000000000000..e77a91db489ac4840dad08c2fd502a821d790b30 --- /dev/null +++ b/modules/src/main/java/app/lounge/model/Model.kt @@ -0,0 +1,28 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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 app.lounge.model + +import java.util.Properties + + +/** AnonymousAuthDataRequestBody */ +data class AnonymousAuthDataRequestBody( + val properties: Properties, + val userAgent: String +) \ No newline at end of file diff --git a/modules/src/main/java/app/lounge/networking/RetrofitHandler.kt b/modules/src/main/java/app/lounge/networking/RetrofitHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..9138167f7fc7febad7a256ae9935414b7cae4eaf --- /dev/null +++ b/modules/src/main/java/app/lounge/networking/RetrofitHandler.kt @@ -0,0 +1,54 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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 app.lounge.networking + +import retrofit2.Response + +sealed interface NetworkResult { + data class Success(val data: T) : NetworkResult + data class Error( + val exception: Throwable, + val code: Int, + val errorMessage: String, + ) : NetworkResult +} + +suspend fun fetch(call: suspend () -> Response): NetworkResult { + try { + val response = call() + if (response.isSuccessful) { + response.body()?.let { result -> + return NetworkResult.Success(result) + } + } + + return NetworkResult.Error( + exception = Exception(response.message()), + code = response.code(), + errorMessage = " ${response.code()} ${response.message()}" + ) + } catch (exception: Exception) { + return NetworkResult.Error( + exception = exception, + code = exception.hashCode(), + errorMessage = exception.toString() + ) + } +} diff --git a/modules/src/main/java/app/lounge/storage/cache/Persistence.kt b/modules/src/main/java/app/lounge/storage/cache/Persistence.kt new file mode 100644 index 0000000000000000000000000000000000000000..96460f1bc9eb8d7ebd9affa65001ac0f07123f25 --- /dev/null +++ b/modules/src/main/java/app/lounge/storage/cache/Persistence.kt @@ -0,0 +1,103 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * 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 app.lounge.storage.cache + +import android.content.Context +import kotlin.reflect.KProperty + + +val Context.configurations: PersistentConfiguration get() = PersistentConfiguration(context = this) + +internal enum class PersistenceKey { + updateInstallAuto, + updateCheckIntervals, + updateAppsFromOtherStores, + showAllApplications, + showPWAApplications, + showFOSSApplications, + // OLD datastore + authData, + email, + oauthtoken, + userType, + tocStatus, + tosversion +} + +class PersistentConfiguration(context: Context) { + var updateInstallAuto by context.persistent(PersistenceKey.updateInstallAuto, false) + var updateCheckIntervals by context.persistent(PersistenceKey.updateCheckIntervals, 24) + var updateAppsFromOtherStores by context.persistent(PersistenceKey.updateAppsFromOtherStores, false) + var showAllApplications by context.persistent(PersistenceKey.showAllApplications, true) + var showPWAApplications by context.persistent(PersistenceKey.showPWAApplications, true) + var showFOSSApplications by context.persistent(PersistenceKey.showFOSSApplications, true) + var authData by context.persistent(PersistenceKey.authData, "") + var email by context.persistent(PersistenceKey.email, "") + var oauthtoken by context.persistent(PersistenceKey.oauthtoken, "") + var userType by context.persistent(PersistenceKey.userType, "") + var tocStatus by context.persistent(PersistenceKey.tocStatus, false) + var tosversion by context.persistent(PersistenceKey.tosversion, "") +} + +internal class PersistentItem( + context: Context, + val key: PersistenceKey, + var defaultValue: T +) { + + private val sharedPref = + context.getSharedPreferences("Settings", Context.MODE_PRIVATE) + private val sharedPrefKey = "${context.packageName}." + key.name + + + operator fun getValue(thisRef: Any?, property: KProperty<*>): T { + return try { + when (property.returnType.classifier) { + Int::class -> sharedPref.getInt(sharedPrefKey, defaultValue as Int) + Long::class -> sharedPref.getLong(sharedPrefKey, defaultValue as Long) + Boolean::class -> sharedPref.getBoolean(sharedPrefKey, defaultValue as Boolean) + String::class -> sharedPref.getString(sharedPrefKey, defaultValue as String) + else -> IllegalArgumentException( + "TODO: Missing accessor for type -- ${property.returnType.classifier}" + ) + } as T + } catch (e: ClassCastException) { + defaultValue + } + } + + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + sharedPref.edit().apply { + when (value) { + is Int -> putInt(sharedPrefKey, value) + is Long -> putLong(sharedPrefKey, value) + is Boolean -> putBoolean(sharedPrefKey, value) + is String -> putString(sharedPrefKey, value) + else -> IllegalArgumentException( + "TODO: Missing setter for type -- ${property.returnType.classifier}" + ) + } + apply() + } + } +} + +internal fun Context.persistent(key: PersistenceKey, defaultValue: T) : PersistentItem { + return PersistentItem(context = this, key = key, defaultValue = defaultValue) +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 4f3c63f186ad758687c7fae4fc47ab773fbd7f17..011889897e706e50891d341cfc1bfc79cc37946b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -62,3 +62,4 @@ dependencyResolutionManagement { } rootProject.name = "App Lounge" include ':app' +include ':modules'