diff --git a/app/build.gradle b/app/build.gradle index ce11c62342567ee55510344f5e2bdc2937146936..3f9cb145d8f57f1f4dd9c3b241a6fa6fe829028a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -133,6 +133,12 @@ android { kotlin.sourceSets.all { languageSettings.optIn("kotlin.RequiresOptIn") } + + testOptions { + unitTests { + includeAndroidResources = true + } + } } kapt { @@ -147,6 +153,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') @@ -162,6 +170,7 @@ dependencies { implementation "androidx.datastore:datastore-preferences:1.0.0" implementation 'com.facebook.shimmer:shimmer:0.5.0' implementation 'androidx.core:core-google-shortcuts:1.0.0' + implementation 'androidx.test.ext:junit-ktx:1.1.5' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' testImplementation "com.google.truth:truth:1.1.3" testImplementation 'junit:junit:4.13.2' @@ -250,4 +259,8 @@ 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' } 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 23d4e842771cba5651803d124090403875ffd95b..4da1870d7656cd63b7f23ca06ae6dd734b406513 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 @@ -19,6 +19,7 @@ package foundation.e.apps.data.gplay import android.content.Context +import app.lounge.storage.cache.configurations import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData @@ -33,6 +34,7 @@ import com.aurora.gplayapi.helpers.Chart import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.gplayapi.helpers.SearchHelper import com.aurora.gplayapi.helpers.TopChartsHelper +import com.google.gson.Gson import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R import foundation.e.apps.data.fused.utils.CategoryType @@ -55,7 +57,7 @@ class GplayStoreRepositoryImpl @Inject constructor( override suspend fun getHomeScreenData(): Any { val homeScreenData = mutableMapOf>() val homeElements = createTopChartElements() - val authData = loginSourceRepository.gplayAuth ?: return homeScreenData + val authData = getAuthData()//loginSourceRepository.gplayAuth ?: return homeScreenData homeElements.forEach { val chart = it.value.keys.iterator().next() @@ -311,4 +313,8 @@ class GplayStoreRepositoryImpl @Inject constructor( } return downloadData } + + private fun getAuthData(): AuthData { + return Gson().fromJson(context.configurations.authData, AuthData::class.java) + } } 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 33a3b837011870e1321ebc3981bbdf2fa677dba7..0dd6be93cc5b6c6a7b534c3b1a3033bff6b219af 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 @@ -185,6 +185,10 @@ class GPlayHttpClient @Inject constructor( Timber.d("$TAG: Url: ${response.request.url}\nStatus: $code") + if (code != 200) { + throw GplayException(code, response.message) + } + if (code == 401) { MainScope().launch { EventBus.invokeEvent( @@ -203,3 +207,5 @@ class GPlayHttpClient @Inject constructor( } } } + +class GplayException (val errorCode: Int, message: String) : Exception(message) 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..23b32bcd3d4b452a2ac80c343bae20188a6fc515 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,10 @@ 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.login.repository.GoogleLoginRepositoryImpl +import foundation.e.apps.domain.login.repository.LoginRepositoryImpl +import foundation.e.apps.domain.login.usecase.GplayLoginUseCase +import foundation.e.apps.domain.login.usecase.UserLoginUseCase @InstallIn(SingletonComponent::class) @Module @@ -36,4 +40,18 @@ object LoginModule { ): List { return listOf(gPlay, cleanApk) } + + @Provides + fun provideLoginUserCase( + loginRepositoryImpl: LoginRepositoryImpl + ): UserLoginUseCase { + return UserLoginUseCase(loginRepositoryImpl) + } + + @Provides + fun provideGoogleLoginUseCase( + loginRepositoryImpl: GoogleLoginRepositoryImpl + ): GplayLoginUseCase { + return GplayLoginUseCase(loginRepositoryImpl) + } } diff --git a/app/src/main/java/foundation/e/apps/domain/login/repository/AuthTokenPlayResponseParser.kt b/app/src/main/java/foundation/e/apps/domain/login/repository/AuthTokenPlayResponseParser.kt new file mode 100644 index 0000000000000000000000000000000000000000..b8af011d650966cff6dc66c27e7a3de1dfd36f75 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/login/repository/AuthTokenPlayResponseParser.kt @@ -0,0 +1,17 @@ +package foundation.e.apps.domain.login.repository + +import java.util.StringTokenizer + +object AuthTokenPlayResponseParser { + fun parseResponse(response: String?): Map { + val keyValueMap: MutableMap = HashMap() + val st = StringTokenizer(response, "\n\r") + while (st.hasMoreTokens()) { + val keyValue = st.nextToken().split("=") + if (keyValue.size >= 2) { + keyValueMap[keyValue[0]] = keyValue[1] + } + } + return keyValueMap + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/domain/login/repository/GoogleLoginRepository.kt b/app/src/main/java/foundation/e/apps/domain/login/repository/GoogleLoginRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..3f1f2791cf67d03b7ef3e014d65d9fb3d7ef2971 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/login/repository/GoogleLoginRepository.kt @@ -0,0 +1,8 @@ +package foundation.e.apps.domain.login.repository + +import com.aurora.gplayapi.data.models.AuthData + +interface GoogleLoginRepository { + suspend fun getGoogleLoginAuthData(email: String, oauthToken: String?): AuthData? + suspend fun validate(): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/domain/login/repository/GoogleLoginRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/domain/login/repository/GoogleLoginRepositoryImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..eedcf1e7d5d65d12b518d6e312dee3a40624f821 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/login/repository/GoogleLoginRepositoryImpl.kt @@ -0,0 +1,70 @@ +package foundation.e.apps.domain.login.repository + +import android.content.Context +import app.lounge.login.google.GoogleLoginApi +import app.lounge.networking.NetworkResult +import app.lounge.storage.cache.configurations +import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.data.models.PlayResponse +import com.aurora.gplayapi.helpers.AuthHelper +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.data.enums.User +import timber.log.Timber +import java.lang.Exception +import java.util.Properties +import javax.inject.Inject + +class GoogleLoginRepositoryImpl @Inject constructor( + @ApplicationContext private val context: Context, + private val googleLoginApi: GoogleLoginApi, + private val properties: Properties +) : GoogleLoginRepository { + + override suspend fun getGoogleLoginAuthData(email: String, oauthToken: String?): AuthData? { + context.configurations.email = email + context.configurations.userType = User.GOOGLE.name + oauthToken?.let { + context.configurations.oauthtoken = oauthToken + } + + val aasToken = context.configurations.aasToken + if (aasToken.isNotEmpty()) { + return AuthHelper.build(email, aasToken, properties) + } + + return fetchAuthData(oauthToken, email) + } + + private suspend fun fetchAuthData(oauthToken: String?, email: String): AuthData? { + oauthToken?.let { + val result = googleLoginApi.getAuthTokenPlayResponse(email, oauthToken) + return when (result) { + is NetworkResult.Success -> handleAuthTokenPlayResponseSuccess(result, email) + is NetworkResult.Error -> throw Exception(result.errorMessage, result.exception) + } + } + + return null + } + + private fun handleAuthTokenPlayResponseSuccess( + result: NetworkResult.Success, + email: String + ): AuthData? { + if (result.data.isSuccessful) { + val parsedResult = + AuthTokenPlayResponseParser.parseResponse(String(result.data.responseBytes)) + + val token = parsedResult["Token"] ?: "" + context.configurations.aasToken = token + return AuthHelper.build(email, token, properties) + } + return null + } + + override suspend fun validate(): Boolean { + val authData = AuthHelper.build("", "") // TODO authdata will be fetched from preferences + val result = googleLoginApi.validate(authData) + return result is NetworkResult.Success && result.data + } +} \ No newline at end of file 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..19ff48c344eab5112515a85fe11e9482b5a2c881 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/login/repository/LoginRepository.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.login.repository + +import app.lounge.model.AnonymousAuthDataRequestBody +import com.aurora.gplayapi.data.models.AuthData + +interface LoginRepository { + + suspend fun anonymousUser( + authDataRequestBody: AnonymousAuthDataRequestBody + ): AuthData +} 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..6477e7e699ea5cff23f9c3a72f90374073dcd6b4 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/login/repository/LoginRepositoryImpl.kt @@ -0,0 +1,52 @@ +/* + * 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 com.google.gson.Gson +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.data.enums.User +import javax.inject.Inject + +class LoginRepositoryImpl @Inject constructor( + private val anonymousUser: AnonymousUser, + @ApplicationContext val applicationContext: Context +) : LoginRepository { + + override suspend fun anonymousUser(authDataRequestBody: AnonymousAuthDataRequestBody): AuthData { + val result = anonymousUser.requestAuthData( + anonymousAuthDataRequestBody = authDataRequestBody + ) + + when (result) { + is NetworkResult.Error -> + throw Exception(result.errorMessage, result.exception) + is NetworkResult.Success -> { + applicationContext.configurations.userType = User.ANONYMOUS.toString() + applicationContext.configurations.authData = Gson().toJson(result.data) + return result.data + } + } + } +} diff --git a/app/src/main/java/foundation/e/apps/domain/login/usecase/BaseUseCase.kt b/app/src/main/java/foundation/e/apps/domain/login/usecase/BaseUseCase.kt new file mode 100644 index 0000000000000000000000000000000000000000..72a37d1bb147fb2b1bfebc29ce857a64a38ccf4f --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/login/usecase/BaseUseCase.kt @@ -0,0 +1,3 @@ +package foundation.e.apps.domain.login.usecase + +open class BaseUseCase \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/domain/login/usecase/GplayLoginUseCase.kt b/app/src/main/java/foundation/e/apps/domain/login/usecase/GplayLoginUseCase.kt new file mode 100644 index 0000000000000000000000000000000000000000..31969099d625697926d5e60ee1c14bd77314cf88 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/login/usecase/GplayLoginUseCase.kt @@ -0,0 +1,31 @@ +package foundation.e.apps.domain.login.usecase + +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.enums.User +import foundation.e.apps.data.login.AuthObject +import foundation.e.apps.domain.login.repository.GoogleLoginRepository +import foundation.e.apps.utils.Resource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.single +import javax.inject.Inject + +class GplayLoginUseCase @Inject constructor(private val googleLoginRepository: GoogleLoginRepository) : + BaseUseCase() { + + suspend operator fun invoke( + email: String, + oauthToken: String, + ): Flow> = flow { + try { + emit(Resource.Loading()) + val authData = googleLoginRepository.getGoogleLoginAuthData(email, oauthToken) + val authObject = AuthObject.GPlayAuth(ResultSupreme.Success(authData), User.ANONYMOUS) as AuthObject + + emit(Resource.Success(authObject)) + } catch (e: Exception) { + emit(Resource.Error(e.localizedMessage ?: "")) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/domain/login/usecase/LoginUseCase.kt b/app/src/main/java/foundation/e/apps/domain/login/usecase/LoginUseCase.kt new file mode 100644 index 0000000000000000000000000000000000000000..f454003735ff249fbb8df57eb827d3e4b4d49b76 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/login/usecase/LoginUseCase.kt @@ -0,0 +1,116 @@ +package foundation.e.apps.domain.login.usecase + +import android.content.Context +import app.lounge.model.AnonymousAuthDataRequestBody +import app.lounge.storage.cache.configurations +import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.helpers.AuthHelper +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.R +import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.enums.User +import foundation.e.apps.data.login.AuthObject +import foundation.e.apps.domain.login.repository.GoogleLoginRepositoryImpl +import foundation.e.apps.domain.login.repository.LoginRepositoryImpl +import foundation.e.apps.utils.Resource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import timber.log.Timber +import java.util.Properties +import javax.inject.Inject +import kotlin.Exception + +class LoginUseCase @Inject constructor( + @ApplicationContext private val context: Context, + private val googleLoginRepository: GoogleLoginRepositoryImpl, + private val loginRepositoryImpl: LoginRepositoryImpl, + private val properties: Properties +) { + suspend fun getAuthObject( + user: User?, + email: String = "", + oauthToken: String = "" + ): Flow> { + return flow { + try { + emit(Resource.Loading()) + val currentUser = + user ?: User.valueOf(context.configurations.userType.ifEmpty { "UNAVAILABLE" }) + val currentAuthData = getAuthData() + Timber.d("currentAuthData: $(currentAuthData != null) currentUser: $currentUser") + + if (currentAuthData != null && currentUser != User.UNAVAILABLE) { + Timber.d("User is already available!") + emit( + createResourceSuccess(currentAuthData, currentUser) + ) + return@flow + } + + if (currentUser == User.GOOGLE) { + val currentEmail = email.ifEmpty { context.configurations.email } + fetchGplayAuthObject(currentEmail, oauthToken, currentUser)?.let { + emit(Resource.Success(it)) + } + return@flow + } + + if (currentUser == User.ANONYMOUS) { + val authData = loginRepositoryImpl.anonymousUser( + authDataRequestBody = AnonymousAuthDataRequestBody( + properties = properties, + userAgent = "" + ) + ) + emit(createResourceSuccess(authData, currentUser)) + return@flow + } + + emit(Resource.Error("User is not available!")) + + } catch (e: Exception) { + Timber.w(e) + emit( + Resource.Error( + e.localizedMessage ?: context.getString(R.string.unknown_error) + ) + ) + } + }.flowOn(Dispatchers.IO) + } + + private fun getAuthData(): AuthData? { + return try { + Gson().fromJson(context.configurations.authData, AuthData::class.java) + } catch (e: Exception) { + null + } + } + + private fun createResourceSuccess( + authData: AuthData, + currentUser: User + ): Resource.Success = + Resource.Success( + AuthObject.GPlayAuth( + ResultSupreme.Success(authData), + currentUser + ) + ) + + private suspend fun fetchGplayAuthObject( + currentEmail: String, + oauthToken: String, + user: User + ): AuthObject? { + var authObject: AuthObject? = null + googleLoginRepository.getGoogleLoginAuthData(currentEmail, oauthToken)?.let { + authObject = AuthObject.GPlayAuth(ResultSupreme.Success(it), user) + } + return authObject + } +} \ No newline at end of file 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..3b4e6ef6394ad94d8402fedda75ae129a8fe92d7 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/domain/login/usecase/UserLoginUseCase.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.domain.login.usecase + +import app.lounge.model.AnonymousAuthDataRequestBody +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.enums.User +import foundation.e.apps.data.login.AuthObject +import foundation.e.apps.domain.login.repository.LoginRepository +import foundation.e.apps.utils.Resource +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.single +import java.util.Properties +import javax.inject.Inject + +class UserLoginUseCase @Inject constructor( + private val loginRepository: LoginRepository, +): BaseUseCase() { + + suspend operator fun invoke( + properties: Properties, + userAgent: String + ): Resource = flow { + try { + emit(Resource.Loading()) + val userResponse = loginRepository.anonymousUser( + authDataRequestBody = AnonymousAuthDataRequestBody( + properties = properties, + userAgent = userAgent + ) + ) + val authObject = AuthObject.GPlayAuth(ResultSupreme.Success(userResponse), User.ANONYMOUS) as AuthObject + emit(Resource.Success(authObject)) + } catch (e: Exception) { + emit(Resource.Error(e.localizedMessage)) + } + }.single() +} 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..23f54bc343f977a11a681314eda371c56b9b2415 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/presentation/login/LoginState.kt @@ -0,0 +1,25 @@ +/* + * 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 + +data class LoginState( + val isLoading: Boolean = false, + val isLoggedIn: Boolean = false, + val error: String = "" +) diff --git a/app/src/main/java/foundation/e/apps/data/login/LoginViewModel.kt b/app/src/main/java/foundation/e/apps/presentation/login/LoginViewModel.kt similarity index 51% rename from app/src/main/java/foundation/e/apps/data/login/LoginViewModel.kt rename to app/src/main/java/foundation/e/apps/presentation/login/LoginViewModel.kt index 25c54362e6912ddeb2618291cd1908ce94b35b01..9adc1b140f13255149abf774da3efacb7e2068d8 100644 --- a/app/src/main/java/foundation/e/apps/data/login/LoginViewModel.kt +++ b/app/src/main/java/foundation/e/apps/presentation/login/LoginViewModel.kt @@ -15,15 +15,33 @@ * along with this program. If not, see . */ -package foundation.e.apps.data.login +package foundation.e.apps.presentation.login +import android.content.Context +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.lounge.storage.cache.configurations +import com.aurora.gplayapi.data.models.AuthData import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext 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.domain.login.usecase.BaseUseCase +import foundation.e.apps.domain.login.usecase.GplayLoginUseCase +import foundation.e.apps.domain.login.usecase.LoginUseCase +import foundation.e.apps.domain.login.usecase.UserLoginUseCase import foundation.e.apps.ui.parentFragment.LoadingViewModel +import foundation.e.apps.utils.Resource +import foundation.e.apps.utils.SystemInfoProvider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.Properties import javax.inject.Inject /** @@ -33,6 +51,9 @@ import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( private val loginSourceRepository: LoginSourceRepository, + private val userLoginUseCase: UserLoginUseCase, + private val gplayLoginUseCase: GplayLoginUseCase, + private val loginUseCase: LoginUseCase ) : ViewModel() { /** @@ -50,10 +71,38 @@ class LoginViewModel @Inject constructor( /** * Main point of starting of entire authentication process. */ - fun startLoginFlow(clearList: List = listOf()) { + fun startLoginFlow( + clearList: List = listOf(), + user: User? = null, + email: String = "", + oauthToken: String = "" + ) { viewModelScope.launch { - val authObjectsLocal = loginSourceRepository.getAuthObjects(clearList) - authObjects.postValue(authObjectsLocal) + val authObjectList = mutableListOf() + loginUseCase.getAuthObject(user, email, oauthToken).onEach { result -> + when (result) { + is Resource.Success -> { + _loginState.value = LoginState(isLoggedIn = true) + result.data?.let { + authObjectList.add(it) + } + authObjects.postValue(authObjectList) + } + + is Resource.Error -> { + Timber.d("AuthObject Error: ${result.message}") + + _loginState.value = LoginState( + error = result.message ?: "An unexpected error occurred!" + ) + authObjects.postValue(authObjectList) + } + + is Resource.Loading -> { + _loginState.value = LoginState(isLoading = true) + } + } + }.collect() } } @@ -66,7 +115,7 @@ class LoginViewModel @Inject constructor( viewModelScope.launch { loginSourceRepository.saveUserType(User.ANONYMOUS) onUserSaved() - startLoginFlow() + startLoginFlow(user = User.ANONYMOUS) } } @@ -78,10 +127,8 @@ class LoginViewModel @Inject constructor( */ fun initialGoogleLogin(email: String, oauthToken: String, onUserSaved: () -> Unit) { viewModelScope.launch { - loginSourceRepository.saveGoogleLogin(email, oauthToken) - loginSourceRepository.saveUserType(User.GOOGLE) + startLoginFlow(user = User.GOOGLE, email = email, oauthToken = oauthToken) onUserSaved() - startLoginFlow() } } @@ -131,4 +178,57 @@ class LoginViewModel @Inject constructor( authObjects.postValue(listOf()) } } + + private val _loginState: MutableLiveData = MutableLiveData() + val loginState: LiveData = _loginState + + fun authenticateAnonymousUser( + properties: Properties, + userAgent: String = SystemInfoProvider.getAppBuildInfo(), + ) { + viewModelScope.launch { + userLoginUseCase( + properties = properties, + userAgent = userAgent + ).also { result -> + when (result) { + is Resource.Success -> { + _loginState.value = LoginState(isLoggedIn = true) + } + + is Resource.Error -> { + _loginState.value = LoginState( + error = result.message ?: "An unexpected error occured" + ) + } + + is Resource.Loading -> { + _loginState.value = LoginState(isLoading = true) + } + } + } + } + } + + fun authenticateGoogleUser(email: String, oauthToken: String) { + viewModelScope.launch(Dispatchers.IO) { + gplayLoginUseCase(email, oauthToken).onEach { result -> + when (result) { + is Resource.Success -> { + _loginState.value = LoginState(isLoggedIn = true) + } + + is Resource.Error -> { + _loginState.value = LoginState( + error = result.message ?: "An unexpected error occured" + ) + } + + is Resource.Loading -> { + _loginState.value = LoginState(isLoading = true) + } + } + } + } + } } diff --git a/app/src/main/java/foundation/e/apps/ui/MainActivity.kt b/app/src/main/java/foundation/e/apps/ui/MainActivity.kt index 5d302687edd5997e75de25d15e5de4fffbb0cae7..dbed9495e6cbbe2b3cb60d97bcb8c296c71f3727 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivity.kt @@ -41,7 +41,7 @@ import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.fusedDownload.models.FusedDownload import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.LoginViewModel +import foundation.e.apps.presentation.login.LoginViewModel import foundation.e.apps.data.login.exceptions.GPlayValidationException import foundation.e.apps.databinding.ActivityMainBinding import foundation.e.apps.install.updates.UpdatesNotifier @@ -59,7 +59,6 @@ import kotlinx.coroutines.launch @AndroidEntryPoint class MainActivity : AppCompatActivity() { - private lateinit var signInViewModel: SignInViewModel private lateinit var loginViewModel: LoginViewModel private lateinit var binding: ActivityMainBinding private val TAG = MainActivity::class.java.simpleName @@ -81,7 +80,6 @@ class MainActivity : AppCompatActivity() { var hasInternet = true viewModel = ViewModelProvider(this)[MainActivityViewModel::class.java] - signInViewModel = ViewModelProvider(this)[SignInViewModel::class.java] loginViewModel = ViewModelProvider(this)[LoginViewModel::class.java] // navOptions and activityNavController for TOS and SignIn Fragments 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 5173da6fecf6a5efdda1fd3fb097fcb0df58dd8a..9a7f8eb1025168e2f922665735f747b0064d785e 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,7 +34,7 @@ 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.presentation.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 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..4a717877d02493d4d0ad0daba145b5d7a4c6ceb3 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 @@ -40,7 +40,7 @@ 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.presentation.login.LoginViewModel import foundation.e.apps.databinding.CustomPreferenceBinding import foundation.e.apps.install.updates.UpdatesWorkManager import foundation.e.apps.ui.MainActivityViewModel 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..7ed9eb765a77ca117f9abf6c91c5df9c8a26c0ea 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,7 +7,7 @@ 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.presentation.login.LoginViewModel import foundation.e.apps.databinding.FragmentSignInBinding import foundation.e.apps.di.CommonUtilsModule.safeNavigate import foundation.e.apps.utils.showGoogleSignInAlertDialog 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..4e7204cec1b58c8e3a420f37137d1e3101948f15 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 @@ -29,10 +29,12 @@ import android.webkit.WebViewClient import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.navigation.findNavController +import app.lounge.storage.cache.configurations import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R +import foundation.e.apps.data.enums.User import foundation.e.apps.data.gplay.utils.AC2DMUtil -import foundation.e.apps.data.login.LoginViewModel +import foundation.e.apps.presentation.login.LoginViewModel import foundation.e.apps.databinding.FragmentGoogleSigninBinding import foundation.e.apps.di.CommonUtilsModule.safeNavigate @@ -80,6 +82,7 @@ class GoogleSignInFragment : Fragment(R.layout.fragment_google_signin) { ) { val email = it.replace("\"".toRegex(), "") viewModel.initialGoogleLogin(email, oauthToken) { + view.findNavController() .safeNavigate( R.id.googleSignInFragment, 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..7a471a5f1f1eba7fb6b2c758019fb4817d54ba42 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/utils/Resource.kt @@ -0,0 +1,25 @@ +/* + * 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 + +sealed class Resource(val data: T? = null, val message: String? = null) { + class Success(data: T) : Resource(data) + class Error(message: String, data: T? = null) : Resource(data, message) + class Loading(data: T? = null) : Resource(data) +} 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..2199b514fc8c26b57e75bcd2e9a0561722521c5c --- /dev/null +++ b/app/src/test/java/foundation/e/apps/Shared.kt @@ -0,0 +1,34 @@ +/* + * 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 java.util.Properties + +val testAnonymousRequestBodyData = AnonymousAuthDataRequestBody( + properties = Properties(), + userAgent = "testUserAgent" +) + +val testAnonymousResponseData = AuthData("eOS@murena.io", "") + +const val loginFailureMessage = "Fail to login" + +val testFailureException: Exception = Exception(loginFailureMessage) 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..3d1cf6c271a0fe39458706bd2afe7054a02e4827 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/domain/login/repository/LoginRepositoryTest.kt @@ -0,0 +1,84 @@ +/* + * 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.testAnonymousRequestBodyData +import foundation.e.apps.testAnonymousResponseData +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 instrumentationContext: Context + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + instrumentationContext = ApplicationProvider.getApplicationContext() + } + + @Test + fun testOnSuccessReturnAuthData() = runTest { + Mockito.`when`(anonymousUser.requestAuthData(testAnonymousRequestBodyData)) + .thenReturn(NetworkResult.Success(testAnonymousResponseData)) + + val result = LoginRepositoryImpl(anonymousUser, instrumentationContext) + .run { + anonymousUser(testAnonymousRequestBodyData) + } + + Assert.assertNotNull(result) + Assert.assertEquals("eOS@murena.io", result.email) + } + + @Test + fun testOnFailureReturnErrorWithException() = runTest { + Mockito.`when`(anonymousUser.requestAuthData(testAnonymousRequestBodyData)) + .thenReturn( + NetworkResult.Error( + exception = testFailureException, + code = 1, + errorMessage = loginFailureMessage + ) + ) + runCatching { + LoginRepositoryImpl(anonymousUser, instrumentationContext) + .run { anonymousUser(testAnonymousRequestBodyData) } + }.onFailure { error -> + Assert.assertEquals(testFailureException.message, error.message) + } + } +} diff --git a/app/src/test/java/foundation/e/apps/presentation/login/LoginViewModelTest.kt b/app/src/test/java/foundation/e/apps/presentation/login/LoginViewModelTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..993638af5d54acef4aaba1146586d4b4b7ffb921 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/presentation/login/LoginViewModelTest.kt @@ -0,0 +1,129 @@ +/* + * 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 androidx.arch.core.executor.testing.InstantTaskExecutorRule +import foundation.e.apps.data.login.LoginSourceRepository +import foundation.e.apps.domain.login.usecase.UserLoginUseCase +import foundation.e.apps.loginFailureMessage +import foundation.e.apps.testAnonymousRequestBodyData +import foundation.e.apps.testAnonymousResponseData +import foundation.e.apps.util.getOrAwaitValue +import foundation.e.apps.utils.Resource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +class LoginViewModelTest { + + private val testDispatcher = StandardTestDispatcher() + + @get:Rule + val rule = InstantTaskExecutorRule() + + @Mock + lateinit var mockUserLoginUseCase: UserLoginUseCase + + @Mock + lateinit var loginRepository: LoginSourceRepository + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + Dispatchers.setMain(testDispatcher) + } + + @Test + fun testOnSuccessReturnLogInStateTrue() = runTest { + Mockito.`when`( + mockUserLoginUseCase.invoke( + properties = testAnonymousRequestBodyData.properties, + userAgent = testAnonymousRequestBodyData.userAgent + ) + ).thenReturn(Resource.Success(testAnonymousResponseData)) + + val loginViewModel = LoginViewModel(loginRepository, mockUserLoginUseCase) + loginViewModel.authenticateAnonymousUser( + properties = testAnonymousRequestBodyData.properties, + userAgent = testAnonymousRequestBodyData.userAgent + ) + testDispatcher.scheduler.advanceUntilIdle() + val result = loginViewModel.loginState.getOrAwaitValue() + Assert.assertEquals(true, result.isLoggedIn) + Assert.assertEquals(false, result.isLoading) + } + + @Test + fun testOnFailureReturnLogInStateFalseWithError() = runTest { + Mockito.`when`( + mockUserLoginUseCase.invoke( + properties = testAnonymousRequestBodyData.properties, + userAgent = testAnonymousRequestBodyData.userAgent + ) + ).thenReturn(Resource.Error(loginFailureMessage)) + + val loginViewModel = LoginViewModel(loginRepository, mockUserLoginUseCase) + loginViewModel.authenticateAnonymousUser( + properties = testAnonymousRequestBodyData.properties, + userAgent = testAnonymousRequestBodyData.userAgent + ) + testDispatcher.scheduler.advanceUntilIdle() + val result = loginViewModel.loginState.getOrAwaitValue() + Assert.assertEquals(false, result.isLoggedIn) + Assert.assertEquals(false, result.isLoading) + Assert.assertEquals(loginFailureMessage, result.error) + } + + @Test + fun testOnLoadingReturnLogInStateFalse() = runTest { + Mockito.`when`( + mockUserLoginUseCase.invoke( + properties = testAnonymousRequestBodyData.properties, + userAgent = testAnonymousRequestBodyData.userAgent + ) + ).thenReturn(Resource.Loading()) + + val loginViewModel = LoginViewModel(loginRepository, mockUserLoginUseCase) + loginViewModel.authenticateAnonymousUser( + properties = testAnonymousRequestBodyData.properties, + userAgent = testAnonymousRequestBodyData.userAgent + ) + testDispatcher.scheduler.advanceUntilIdle() + val result = loginViewModel.loginState.getOrAwaitValue() + Assert.assertEquals(true, result.isLoading) + Assert.assertEquals(false, result.isLoggedIn) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } +} 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..c233bd53e25ef46ae62fa8acf88d22835ffe9db9 --- /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 { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = '11' + } +} + +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") + + //logger + implementation 'com.jakewharton.timber:timber:5.0.1' + + 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..6161b1cfba3294ba56a5addb2b6477083c11a26a --- /dev/null +++ b/modules/src/androidTest/java/app/lounge/PersistentStorageTest.kt @@ -0,0 +1,116 @@ +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..2a7393a691252ba05f021d4d9bc1e7801684d49a --- /dev/null +++ b/modules/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + \ 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..973cecc0a8deb08ac2fdac85bad11ca971306ade --- /dev/null +++ b/modules/src/main/java/app/lounge/di/NetworkModule.kt @@ -0,0 +1,124 @@ +/* + * 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.gplay.GplayHttpClient +import app.lounge.login.anonymous.AnonymousUser +import app.lounge.login.anonymous.AnonymousUserRetrofitAPI +import app.lounge.login.anonymous.AnonymousUserRetrofitImpl +import app.lounge.login.google.AuthTokenFetchApi +import app.lounge.login.google.GoogleLoginApi +import app.lounge.login.google.GoogleLoginApiImpl +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 provideGplayHttpClient(@Named("privateOkHttpClient") okHttpClient: OkHttpClient): GplayHttpClient { + return GplayHttpClient(okHttpClient) + } + + @Provides + @Singleton + fun provideAnonymousUser( + @Named("ECloudRetrofit") ecloud: Retrofit + ): AnonymousUser { + return AnonymousUserRetrofitImpl( + eCloud = ecloud + ) + } + + @Provides + @Singleton + fun provideAuthTokenFetchApi(gplayHttpClient: GplayHttpClient): AuthTokenFetchApi { + return AuthTokenFetchApi(gplayHttpClient) + } + + @Provides + @Singleton + fun provideGoogleLoginApi( + gplayHttpClient: GplayHttpClient, + authTokenFetchApi: AuthTokenFetchApi + ): GoogleLoginApi { + return GoogleLoginApiImpl(gplayHttpClient, authTokenFetchApi) + } + +} \ 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/gplay/GplayException.kt b/modules/src/main/java/app/lounge/gplay/GplayException.kt new file mode 100644 index 0000000000000000000000000000000000000000..9d64c79d1bcf0baa3d9b156eae71bea4374d63e5 --- /dev/null +++ b/modules/src/main/java/app/lounge/gplay/GplayException.kt @@ -0,0 +1,3 @@ +package app.lounge.gplay + +class GplayException(var errorCode: Int, message: String) : Exception(message) \ No newline at end of file diff --git a/modules/src/main/java/app/lounge/gplay/GplayHttpClient.kt b/modules/src/main/java/app/lounge/gplay/GplayHttpClient.kt new file mode 100644 index 0000000000000000000000000000000000000000..d144081df2ff1a92d264a9dec1c23d9e7ae6bcbc --- /dev/null +++ b/modules/src/main/java/app/lounge/gplay/GplayHttpClient.kt @@ -0,0 +1,150 @@ +package app.lounge.gplay + +import com.aurora.gplayapi.data.models.PlayResponse +import com.aurora.gplayapi.network.IHttpClient +import okhttp3.Headers.Companion.toHeaders +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import timber.log.Timber +import java.io.IOException +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import javax.inject.Inject +import javax.inject.Named + +class GplayHttpClient @Inject constructor(@Named("privateOkHttpClient") val okHttpClient: OkHttpClient): IHttpClient { + + companion object { + private const val POST = "POST" + private const val GET = "GET" + } + + + override fun get(url: String, headers: Map): PlayResponse { + return get(url, headers, mapOf()) + } + + override fun get(url: String, headers: Map, paramString: String): PlayResponse { + val request = Request.Builder() + .url(url + paramString) + .headers(headers.toHeaders()) + .method(GET, null) + .build() + return processRequest(request) + } + + override fun get( + url: String, + headers: Map, + params: Map + ): PlayResponse { + val request = Request.Builder() + .url(buildUrlWithQueryParameters(url, params)) + .headers(headers.toHeaders()) + .method(GET, null) + .build() + return processRequest(request) + } + + override fun getAuth(url: String): PlayResponse { + val request = Request.Builder() + .url(url) + .method(GET, null) + .build() + Timber.d("get auth request", request.toString()) + return processRequest(request) + } + + override fun post(url: String, headers: Map, body: ByteArray): PlayResponse { + val requestBody = body.toRequestBody( + "application/x-protobuf".toMediaType(), + 0, + body.size + ) + val request = Request.Builder() + .url(url) + .headers(headers.toHeaders()) + .method(POST, requestBody) + .build() + return processRequest(request) + } + + override fun post( + url: String, + headers: Map, + params: Map + ): PlayResponse { + val request = Request.Builder() + .url(buildUrlWithQueryParameters(url, params)) + .headers(headers.toHeaders()) + .method(POST, "".toRequestBody(null)) + .build() + return processRequest(request) + } + + @Throws(IOException::class) + fun post(url: String, headers: Map, requestBody: RequestBody): PlayResponse { + val request = Request.Builder() + .url(url) + .headers(headers.toHeaders()) + .method(POST, requestBody) + .build() + return processRequest(request) + } + + override fun postAuth(url: String, body: ByteArray): PlayResponse { + TODO("Not yet implemented") + } + + private fun buildUrlWithQueryParameters(url: String, params: Map): HttpUrl { + val urlBuilder = url.toHttpUrl().newBuilder() + params.forEach { + urlBuilder.addQueryParameter(it.key, it.value) + } + return urlBuilder.build() + } + + private fun processRequest(request: Request): PlayResponse { + return try { + val call = okHttpClient.newCall(request) + buildPlayResponse(call.execute()) + } catch (e: Exception) { + throw e + } + } + + private fun handleExceptionOnGooglePlayRequest(e: Exception): PlayResponse { + Timber.e("processRequest: ${e.localizedMessage}") + return PlayResponse().apply { + errorString = "${this@GplayHttpClient::class.java.simpleName}: ${e.localizedMessage}" + } + } + + private fun buildPlayResponse(response: Response): PlayResponse { + return PlayResponse().apply { + isSuccessful = response.isSuccessful + code = response.code + + Timber.d("Url: ${response.request.url}\nStatus: $code") + + if (code != 200) { + throw GplayException(code, response.message) + } + + if (response.body != null) { + responseBytes = response.body!!.bytes() + } + + if (!isSuccessful) { + errorString = response.message + } + } + } +} + 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..0721d8bfdca05ae56217e85815c2712ade2431cc --- /dev/null +++ b/modules/src/main/java/app/lounge/login/anonymous/AnonymousUserRetrofitImpl.kt @@ -0,0 +1,92 @@ +/* + * 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/login/google/AuthTokenFetchApi.kt b/modules/src/main/java/app/lounge/login/google/AuthTokenFetchApi.kt new file mode 100644 index 0000000000000000000000000000000000000000..c20e836bdffff972bd4e8a374e94c63bff8bc620 --- /dev/null +++ b/modules/src/main/java/app/lounge/login/google/AuthTokenFetchApi.kt @@ -0,0 +1,47 @@ +package app.lounge.login.google + +import app.lounge.gplay.GplayHttpClient +import com.aurora.gplayapi.data.models.PlayResponse +import okhttp3.RequestBody.Companion.toRequestBody +import java.util.Locale +import javax.inject.Inject + +class AuthTokenFetchApi @Inject constructor(private val gplayHttpClient: GplayHttpClient) { + companion object { + private const val TOKEN_AUTH_URL = "https://android.clients.google.com/auth" + private const val BUILD_VERSION_SDK = 28 + private const val PLAY_SERVICES_VERSION_CODE = 19629032 + } + + fun getAuthTokenPlayResponse(email: String?, oAuthToken: String?): PlayResponse { + if (email == null || oAuthToken == null) + return PlayResponse() + + val params: MutableMap = hashMapOf() + params["lang"] = Locale.getDefault().toString().replace("_", "-") + params["google_play_services_version"] = PLAY_SERVICES_VERSION_CODE + params["sdk_version"] = BUILD_VERSION_SDK + params["device_country"] = Locale.getDefault().country.lowercase(Locale.US) + params["Email"] = email + params["service"] = "ac2dm" + params["get_accountid"] = 1 + params["ACCESS_TOKEN"] = 1 + params["callerPkg"] = "com.google.android.gms" + params["add_account"] = 1 + params["Token"] = oAuthToken + params["callerSig"] = "38918a453d07199354f8b19af05ec6562ced5788" + + val body = params.map { "${it.key}=${it.value}" }.joinToString(separator = "&") + val header = mapOf( + "app" to "com.google.android.gms", + "User-Agent" to "", + "Content-Type" to "application/x-www-form-urlencoded" + ) + + /* + * Returning PlayResponse instead of map so that we can get the network response code. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709 + */ + return gplayHttpClient.post(TOKEN_AUTH_URL, header, body.toRequestBody()) + } +} \ No newline at end of file diff --git a/modules/src/main/java/app/lounge/login/google/GoogeLoginApi.kt b/modules/src/main/java/app/lounge/login/google/GoogeLoginApi.kt new file mode 100644 index 0000000000000000000000000000000000000000..fad4e80c6039f7a4638093bcc8e236c29727e470 --- /dev/null +++ b/modules/src/main/java/app/lounge/login/google/GoogeLoginApi.kt @@ -0,0 +1,11 @@ +package app.lounge.login.google + +import app.lounge.model.GoogleAuthDataRequestBody +import app.lounge.networking.NetworkResult +import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.data.models.PlayResponse + +interface GoogleLoginApi { + suspend fun getAuthTokenPlayResponse(email: String, oauthToken: String): NetworkResult + suspend fun validate(authData: AuthData): NetworkResult +} \ No newline at end of file diff --git a/modules/src/main/java/app/lounge/login/google/GoogleLoginApiImpl.kt b/modules/src/main/java/app/lounge/login/google/GoogleLoginApiImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..e98d680bca3eb1d5840b640c3aab0fabed52f601 --- /dev/null +++ b/modules/src/main/java/app/lounge/login/google/GoogleLoginApiImpl.kt @@ -0,0 +1,32 @@ +package app.lounge.login.google + +import app.lounge.gplay.GplayHttpClient +import app.lounge.networking.fetchPlayResponse +import app.lounge.networking.NetworkResult +import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.data.models.PlayResponse +import com.aurora.gplayapi.helpers.AuthValidator +import javax.inject.Inject + +class GoogleLoginApiImpl @Inject constructor( + private val gplayHttpClient: GplayHttpClient, + private val authTokenFetchApi: AuthTokenFetchApi +) : GoogleLoginApi { + + override suspend fun getAuthTokenPlayResponse( + email: String, + oauthToken: String + ): NetworkResult { + return fetchPlayResponse { + authTokenFetchApi.getAuthTokenPlayResponse(email, oauthToken) + } + } + + override suspend fun validate(authData: AuthData): NetworkResult { + return fetchPlayResponse { + val authValidator = AuthValidator(authData).using(gplayHttpClient) + authValidator.isValid() + } + } + +} \ 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..3d72737211efc81e68440f9d4801c93a034c918e --- /dev/null +++ b/modules/src/main/java/app/lounge/model/Model.kt @@ -0,0 +1,34 @@ +/* + * 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 +) + +data class GoogleAuthDataRequestBody( + val email: String, + val token: String, + val properties: Properties +) \ No newline at end of file diff --git a/modules/src/main/java/app/lounge/networking/PlayResponseHandler.kt b/modules/src/main/java/app/lounge/networking/PlayResponseHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..87dc1cc51a5eafaff4147203a8548a4c519b9e7b --- /dev/null +++ b/modules/src/main/java/app/lounge/networking/PlayResponseHandler.kt @@ -0,0 +1,18 @@ +package app.lounge.networking + +import app.lounge.gplay.GplayException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +suspend fun fetchPlayResponse(call: suspend () -> T) : NetworkResult { + return withContext(Dispatchers.IO) { + try { + val result = call() + NetworkResult.Success(result) + } catch (e: GplayException) { + NetworkResult.Error(e, e.errorCode, e.localizedMessage) + } catch (e: Exception) { + NetworkResult.Error(e, -1, e.localizedMessage) + } + } +} \ 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..fa3a93daa9ead54b2b05d88c2b34ab6d23fa4f59 --- /dev/null +++ b/modules/src/main/java/app/lounge/storage/cache/Persistence.kt @@ -0,0 +1,87 @@ +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, + aastoken +} + +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, "") + var aasToken by context.persistent(PersistenceKey.aastoken, "") +} + +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'