Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 36aa09cb authored by Jonathan Klee's avatar Jonathan Klee
Browse files

Merge branch '5680-revert-login-refacto' into 'release-1.5-rc2'

Revert "Merge branch '1111-login-rewrite-full' into 'main'"

See merge request !221
parents 95c5ee72 edc7447d
Loading
Loading
Loading
Loading
Loading
+42 −34
Original line number Diff line number Diff line
@@ -25,9 +25,9 @@ import android.os.Environment
import android.os.StatFs
import android.os.storage.StorageManager
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavOptions
@@ -35,15 +35,12 @@ import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.NavigationUI
import androidx.navigation.ui.setupWithNavController
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.exceptions.ApiException
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import foundation.e.apps.application.subFrags.ApplicationDialogFragment
import foundation.e.apps.databinding.ActivityMainBinding
import foundation.e.apps.login.AuthObject
import foundation.e.apps.login.LoginViewModel
import foundation.e.apps.manager.database.fusedDownload.FusedDownload
import foundation.e.apps.manager.workmanager.InstallWorkManager
import foundation.e.apps.purchase.AppPurchaseFragmentDirections
@@ -53,12 +50,11 @@ import foundation.e.apps.updates.UpdatesNotifier
import foundation.e.apps.utils.enums.Status
import foundation.e.apps.utils.eventBus.AppEvent
import foundation.e.apps.utils.eventBus.EventBus
import foundation.e.apps.utils.exceptions.GPlayValidationException
import foundation.e.apps.utils.modules.CommonUtilsModule
import foundation.e.apps.utils.parentFragment.TimeoutFragment
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import org.json.JSONObject
import timber.log.Timber
import java.io.File
import java.util.UUID
@@ -66,7 +62,6 @@ import java.util.UUID
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private lateinit var signInViewModel: SignInViewModel
    private lateinit var loginViewModel: LoginViewModel
    private lateinit var binding: ActivityMainBinding
    private val TAG = MainActivity::class.java.simpleName
    private lateinit var viewModel: MainActivityViewModel
@@ -88,7 +83,6 @@ class MainActivity : AppCompatActivity() {

        viewModel = ViewModelProvider(this)[MainActivityViewModel::class.java]
        signInViewModel = ViewModelProvider(this)[SignInViewModel::class.java]
        loginViewModel = ViewModelProvider(this)[LoginViewModel::class.java]

        // navOptions and activityNavController for TOS and SignIn Fragments
        val navOptions = NavOptions.Builder()
@@ -96,11 +90,9 @@ class MainActivity : AppCompatActivity() {
            .build()
        navOptions.shouldLaunchSingleTop()

        viewModel.tocStatus.distinctUntilChanged().observe(this) {
        viewModel.tocStatus.observe(this) {
            if (it != true) {
                navController.navigate(R.id.TOSFragment, null, navOptions)
            } else {
                loginViewModel.startLoginFlow()
            }
        }

@@ -109,34 +101,37 @@ class MainActivity : AppCompatActivity() {
            if (isInternetAvailable) {
                binding.noInternet.visibility = View.GONE
                binding.fragment.visibility = View.VISIBLE

                // Watch and refresh authentication data
                if (viewModel.authDataJson.value == null) {
                    viewModel.authDataJson.observe(this) {
                        viewModel.handleAuthDataJson()
                    }
                }
            }
        }

        viewModel.userType.observe(this) { user ->
            viewModel.handleAuthDataJson()
        }

        loginViewModel.authObjects.distinctUntilChanged().observe(this) {
            when {
                it == null -> return@observe
                it.isEmpty() -> {
                    // No auth type defined means user has not logged in yet
                    // Pop back stack to prevent showing TOSFragment on pressing back button.
                    navController.popBackStack()
                    navController.navigate(R.id.signInFragment)
        if (signInViewModel.authLiveData.value == null) {
            signInViewModel.authLiveData.observe(this) {
                viewModel.updateAuthData(it)
            }
                else -> {}
        }

            it.find { it is AuthObject.GPlayAuth }?.result?.run {
                if (isSuccess()) {
                    viewModel.gPlayAuthData = data as AuthData
                } else if (exception is GPlayValidationException) {
                    val email = otherPayload.toString()
                    val descriptionJson = JSONObject().apply {
                        put("versionName", BuildConfig.VERSION_NAME)
                        put("versionCode", BuildConfig.VERSION_CODE)
                        put("debuggable", BuildConfig.DEBUG)
                        put("device", Build.DEVICE)
                        put("api", Build.VERSION.SDK_INT)
        viewModel.errorAuthResponse.observe(this) {
            onSignInError()
        }
                    viewModel.uploadFaultyTokenToEcloud(email, descriptionJson.toString())

        viewModel.authValidity.observe(this) {
            viewModel.handleAuthValidity(it) {
                Timber.d("Timeout validating auth data!")
                val lastFragment = navHostFragment.childFragmentManager.fragments[0]
                if (lastFragment is TimeoutFragment) {
                    Timber.d("Displaying timeout from MainActivity on fragment: " + lastFragment.javaClass.name)
                    lastFragment.onTimeout()
                }
            }
        }
@@ -336,6 +331,19 @@ class MainActivity : AppCompatActivity() {
        }
    }

    private fun onSignInError() {
        AlertDialog.Builder(this).apply {
            setTitle(R.string.sign_in_failed_title)
            setMessage(R.string.sign_in_failed_desc)
            setPositiveButton(R.string.retry) { _, _ ->
                viewModel.retryFetchingTokenAfterTimeout()
            }
            setNegativeButton(R.string.logout) { _, _ ->
                viewModel.postFalseAuthValidity()
            }
        }.show()
    }

    private fun getAvailableInternalMemorySize(): Long {
        val path: File = Environment.getDataDirectory()
        val stat = StatFs(path.path)
+295 −10
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.os.Build
import android.os.SystemClock
import android.util.Base64
import android.widget.ImageView
import androidx.annotation.RequiresApi
@@ -34,12 +35,16 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.data.models.PlayResponse
import com.aurora.gplayapi.exceptions.ApiException
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel
import foundation.e.apps.api.cleanapk.blockedApps.BlockedAppRepository
import foundation.e.apps.api.ecloud.EcloudRepository
import foundation.e.apps.api.fused.FusedAPIRepository
import foundation.e.apps.api.fused.data.FusedApp
import foundation.e.apps.api.gplay.utils.AC2DMTask
import foundation.e.apps.api.gplay.utils.AC2DMUtil
import foundation.e.apps.manager.database.fusedDownload.FusedDownload
import foundation.e.apps.manager.fused.FusedManagerRepository
import foundation.e.apps.manager.pkg.PkgManagerModule
@@ -49,16 +54,21 @@ import foundation.e.apps.utils.enums.Type
import foundation.e.apps.utils.enums.User
import foundation.e.apps.utils.enums.isInitialized
import foundation.e.apps.utils.enums.isUnFiltered
import foundation.e.apps.utils.modules.CommonUtilsModule.NETWORK_CODE_SUCCESS
import foundation.e.apps.utils.modules.CommonUtilsModule.timeoutDurationInMillis
import foundation.e.apps.utils.modules.DataStoreModule
import foundation.e.apps.utils.modules.PWAManagerModule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ru.beryukhov.reactivenetwork.ReactiveNetwork
import timber.log.Timber
import java.io.ByteArrayOutputStream
import javax.inject.Inject

@HiltViewModel
class MainActivityViewModel @Inject constructor(
    private val gson: Gson,
    private val dataStoreModule: DataStoreModule,
    private val fusedAPIRepository: FusedAPIRepository,
    private val fusedManagerRepository: FusedManagerRepository,
@@ -66,16 +76,37 @@ class MainActivityViewModel @Inject constructor(
    private val pwaManagerModule: PWAManagerModule,
    private val ecloudRepository: EcloudRepository,
    private val blockedAppRepository: BlockedAppRepository,
    private val aC2DMTask: AC2DMTask,
) : ViewModel() {

    val authDataJson: LiveData<String> = dataStoreModule.authData.asLiveData()
    val tocStatus: LiveData<Boolean> = dataStoreModule.tocStatus.asLiveData()
    val userType: LiveData<String> = dataStoreModule.userType.asLiveData()

    private var _authData: MutableLiveData<AuthData> = MutableLiveData()
    val authData: LiveData<AuthData> = _authData
    val authValidity: MutableLiveData<Boolean> = MutableLiveData()
    private val _purchaseAppLiveData: MutableLiveData<FusedDownload> = MutableLiveData()
    val purchaseAppLiveData: LiveData<FusedDownload> = _purchaseAppLiveData
    val isAppPurchased: MutableLiveData<String> = MutableLiveData()
    val purchaseDeclined: MutableLiveData<String> = MutableLiveData()
    var authRequestRunning = false

    var gPlayAuthData = AuthData("", "")
    /*
     * If this live data is populated, it means Google sign in failed.
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709
     */
    val errorAuthResponse = MutableLiveData<PlayResponse>()

    /*
     * Store the time when auth data is fetched for the first time.
     * If we try to fetch auth data after timeout, then don't allow it.
     *
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404
     */
    var firstAuthDataFetchTime = 0L

    var isTokenValidationCompletedOnce = false

    // Downloads
    val downloadList = fusedManagerRepository.getDownloadLiveList()
@@ -85,6 +116,9 @@ class MainActivityViewModel @Inject constructor(

    private val _errorMessageStringResource = MutableLiveData<Int>()
    val errorMessageStringResource: LiveData<Int> = _errorMessageStringResource
    /*
     * Authentication related functions
     */

    companion object {
        private const val TAG = "MainActivityViewModel"
@@ -92,18 +126,269 @@ class MainActivityViewModel @Inject constructor(
    }

    fun getUser(): User {
        return dataStoreModule.getUserType()
        return User.valueOf(userType.value ?: User.UNAVAILABLE.name)
    }

    private fun setFirstTokenFetchTime() {
        if (firstAuthDataFetchTime == 0L) {
            firstAuthDataFetchTime = SystemClock.uptimeMillis()
        }
    }

    fun getUserEmail(): String {
        return dataStoreModule.getEmail()
    private fun isTimeEligibleForTokenRefresh(): Boolean {
        return (SystemClock.uptimeMillis() - firstAuthDataFetchTime) <= timeoutDurationInMillis
    }

    fun uploadFaultyTokenToEcloud(email: String, description: String = "") {
    /*
     * This method resets the last recorded token fetch time.
     * Then it posts authValidity as false. This causes the observer in MainActivity to destroyCredentials
     * and fetch new token.
     *
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404
     */
    fun retryFetchingTokenAfterTimeout() {
        firstAuthDataFetchTime = 0
        setFirstTokenFetchTime()
        if (isUserTypeGoogle()) {
            /*
             * Change done to show sign in error dialog for Google login.
             * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709
             */
            if (authDataJson.value.isNullOrEmpty()) {
                generateAuthDataBasedOnUserType(User.GOOGLE.name)
            } else {
                validateAuthData()
            }
        } else {
            postFalseAuthValidity()
        }
    }

    fun uploadFaultyTokenToEcloud(description: String) {
        viewModelScope.launch {
            authData.value?.let { authData ->
                val email: String = authData.run {
                    if (email != "null") email
                    else userProfile?.email ?: "null"
                }
                ecloudRepository.uploadFaultyEmail(email, description)
            }
        }
    }

    fun getAuthData() {
        if (!authRequestRunning) {
            authRequestRunning = true
            viewModelScope.launch {
                /*
                 * If getting auth data failed, try getting again.
                 * Sending false in authValidity, triggers observer in MainActivity,
                 * causing it to destroy credentials and try to regenerate auth data.
                 *
                 * Issue:
                 * https://gitlab.e.foundation/e/backlog/-/issues/5413
                 * https://gitlab.e.foundation/e/backlog/-/issues/5404
                 */
                if (!fusedAPIRepository.fetchAuthData()) {
                    authRequestRunning = false
                    postFalseAuthValidity()
                }
            }
        }
    }

    fun updateAuthData(authData: AuthData) {
        _authData.value = authData
    }

    fun destroyCredentials(regenerateFunction: ((user: String) -> Unit)?) {
        viewModelScope.launch {
            /*
             * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5168
             *
             * Now destroyCredentials() no longer removes the user type from data store.
             * (i.e. Google login or Anonymous).
             * - If the type is User.ANONYMOUS then we do not prompt the user to login again,
             *   we directly generate new auth data; which is the main Gitlab issue described above.
             * - If not anonymous user, i.e. type is User.GOOGLE, in that case we clear
             *   the USERTYPE value. This causes HomeFragment.onTosAccepted() to open
             *   SignInFragment as we need fresh login from the user.
             */
            dataStoreModule.destroyCredentials()
            if (regenerateFunction != null) {
                dataStoreModule.userType.collect { user ->
                    if (!user.isBlank() && User.valueOf(user) == User.ANONYMOUS) {
                        Timber.d("Regenerating auth data for Anonymous user")
                        regenerateFunction(user)
                    } else {
                        Timber.d("Ask Google user to log in again")
                        dataStoreModule.clearUserType()
                    }
                }
            }
        }
    }

    fun generateAuthData() {
        val data = jsonToAuthData()
        _authData.value = data
    }

    private fun jsonToAuthData() = gson.fromJson(authDataJson.value, AuthData::class.java)

    fun validateAuthData() {
        viewModelScope.launch {
            jsonToAuthData()?.let {
                val validityResponse = getAuthValidityResponse(it)
                if (isUserTypeGoogle() && validityResponse.code != NETWORK_CODE_SUCCESS) {
                    /*
                     * Change done to show sign in error dialog for Google login.
                     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709
                     */
                    errorAuthResponse.postValue(validityResponse)
                } else {
                    authValidity.postValue(validityResponse.isSuccessful)
                }
                authRequestRunning = false
            }
        }
    }

    // Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709
    fun isUserTypeGoogle(): Boolean {
        return userType.value == User.GOOGLE.name
    }

    /**
     * Useful to destroy credentials.
     */
    fun postFalseAuthValidity() {
        authValidity.postValue(false)
    }

    fun handleAuthDataJson() {
        val user = userType.value
        val json = authDataJson.value

        if (user == null || json == null) {
            return
        }
        Timber.d(">>> handleAuthDataJson: internet: ${internetConnection.value}")
        if (!isUserLoggedIn(user, json)) {
            generateAuthDataBasedOnUserType(user)
        } else if (isEligibleToValidateJson(json) && internetConnection.value == true) {
            validateAuthData()
            Timber.d(">>> Authentication data is available!")
        }
    }

    private fun isUserLoggedIn(user: String, json: String) =
        user.isNotEmpty() && !user.contentEquals(User.UNAVAILABLE.name) && json.isNotEmpty()

    private fun isEligibleToValidateJson(authDataJson: String?) =
        !authDataJson.isNullOrEmpty() && !userType.value.isNullOrEmpty() && !userType.value.contentEquals(
            User.UNAVAILABLE.name
        ) && authValidity.value != true

    fun handleAuthValidity(isValid: Boolean, handleTimeoOut: () -> Unit) {
        if (isGoogleLoginRunning) {
            return
        }
        isTokenValidationCompletedOnce = true
        if (isValid) {
            Timber.d("Authentication data is valid!")
            generateAuthData()
            return
        }
        Timber.d(">>> Authentication data validation failed!")
        destroyCredentials { user ->
            if (isTimeEligibleForTokenRefresh()) {
                generateAuthDataBasedOnUserType(user)
            } else {
                handleTimeoOut()
            }
        }
    }

    private fun generateAuthDataBasedOnUserType(user: String) {
        if (user.isEmpty() || tocStatus.value == false || isGoogleLoginRunning) {
            return
        }
        when (User.valueOf(user)) {
            User.ANONYMOUS -> {
                if (authDataJson.value.isNullOrEmpty() && !authRequestRunning) {
                    Timber.d(">>> Fetching new authentication data")
                    setFirstTokenFetchTime()
                    getAuthData()
                }
            }
            User.UNAVAILABLE -> {
                destroyCredentials(null)
            }
            User.GOOGLE -> {
                if (authData.value == null && !authRequestRunning) {
                    Timber.d(">>> Fetching new authentication data")
                    setFirstTokenFetchTime()
                    doFetchAuthData()
                }
            }
        }
    }

    private suspend fun doFetchAuthData(email: String, oauthToken: String) {
        var responseMap: Map<String, String>
        withContext(Dispatchers.IO) {
            val response = aC2DMTask.getAC2DMResponse(email, oauthToken)
            responseMap = if (response.isSuccessful) {
                AC2DMUtil.parseResponse(String(response.responseBytes))
            } else {
                mapOf()
            }
            if (isUserTypeGoogle() && response.code != NETWORK_CODE_SUCCESS) {
                /*
                 * For google login, the email and aasToken gets stored when
                 * we login through the webview, but that does not mean we have a valid authData.
                 *
                 * For first login, control flow is as below:
                 * In MainActivity, from userType observer -> handleAuthDataJson
                 * -> generateAuthDataBasedOnUserType -> doFetchAuthData -> this function
                 *
                 * If for first google login, google sign in portal was available
                 * but android.clients.google.com is unreachable, then responseMap is blank.
                 *
                 * We see validateAuthData is never called (which had a check for incorrect response)
                 * Hence we have to check the response code is NETWORK_CODE_SUCCESS (200) or not
                 * and show the Google sign in failed dialog.
                 *
                 * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709
                 */
                errorAuthResponse.postValue(response)
                return@withContext
            }
            responseMap["Token"]?.let {
                if (fusedAPIRepository.fetchAuthData(email, it) == null) {
                    dataStoreModule.clearUserType()
                    _errorMessageStringResource.value = R.string.unknown_error
                }
            }
        }
    }

    private fun doFetchAuthData() {
        viewModelScope.launch {
            isGoogleLoginRunning = true
            val email = dataStoreModule.getEmail()
            val oauthToken = dataStoreModule.getAASToken()
            if (email.isNotEmpty() && oauthToken.isNotEmpty()) {
                doFetchAuthData(email, oauthToken)
            }
            isGoogleLoginRunning = false
        }
    }

    private suspend fun getAuthValidityResponse(authData: AuthData): PlayResponse {
        return fusedAPIRepository.validateAuthData(authData)
    }

    /*
     * Notification functions
@@ -131,7 +416,7 @@ class MainActivityViewModel @Inject constructor(
     * Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/266
     */
    fun shouldShowPaidAppsSnackBar(app: FusedApp): Boolean {
        if (!app.isFree && gPlayAuthData.isAnonymous) {
        if (!app.isFree && authData.value?.isAnonymous == true) {
            _errorMessageStringResource.value = R.string.paid_app_anonymous_message
            return true
        }
@@ -175,7 +460,7 @@ class MainActivityViewModel @Inject constructor(
     */
    fun verifyUiFilter(fusedApp: FusedApp, method: () -> Unit) {
        viewModelScope.launch {
            val authData = gPlayAuthData
            val authData = authData.value
            if (fusedApp.filterLevel.isInitialized()) {
                method()
            } else {
@@ -268,7 +553,7 @@ class MainActivityViewModel @Inject constructor(

    suspend fun updateAwaitingForPurchasedApp(packageName: String): FusedDownload? {
        val fusedDownload = fusedManagerRepository.getFusedDownload(packageName = packageName)
        gPlayAuthData.let {
        authData.value?.let {
            if (!it.isAnonymous) {
                try {
                    fusedAPIRepository.updateFusedDownloadWithDownloadingInfo(
@@ -309,7 +594,7 @@ class MainActivityViewModel @Inject constructor(
        fusedDownload: FusedDownload
    ) {
        val downloadList = mutableListOf<String>()
        gPlayAuthData.let {
        authData.value?.let {
            if (app.type == Type.PWA) {
                downloadList.add(app.url)
                fusedDownload.downloadURLList = downloadList
+3 −8
Original line number Diff line number Diff line
@@ -70,7 +70,7 @@ sealed class ResultSupreme<T> {
         * @param message A String message to log or display to the user.
         * @param exception Optional exception from try-catch block.
         */
        constructor(message: String, exception: Exception? = null) : this() {
        constructor(message: String, exception: Exception = Exception()) : this() {
            this.message = message
            this.exception = exception
        }
@@ -91,11 +91,6 @@ sealed class ResultSupreme<T> {
    var data: T? = null
        private set

    /**
     * Any other information that needs to be transmitted.
     */
    var otherPayload: Any? = null

    /**
     * A custom string message for logging or displaying to the user.
     */
@@ -104,7 +99,7 @@ sealed class ResultSupreme<T> {
    /**
     * Exception from try-catch block for error cases.
     */
    var exception: Exception? = null
    var exception: Exception = Exception()

    fun isValidData() = data != null

@@ -126,7 +121,7 @@ sealed class ResultSupreme<T> {
            status: ResultStatus,
            data: T? = null,
            message: String = "",
            exception: Exception? = null,
            exception: Exception = Exception(),
        ): ResultSupreme<T> {
            val resultObject = when {
                status == ResultStatus.OK && data != null -> Success<T>(data)
+15 −2

File changed.

Preview size limit exceeded, changes collapsed.

+19 −1
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import com.aurora.gplayapi.SearchSuggestEntry
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.data.models.Category
import com.aurora.gplayapi.data.models.PlayResponse
import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster
import foundation.e.apps.api.ResultSupreme
@@ -38,7 +39,9 @@ import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPIImpl) {
class FusedAPIRepository @Inject constructor(
    private val fusedAPIImpl: FusedAPIImpl
) {

    var streamBundle = StreamBundle()
        private set
@@ -90,6 +93,13 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPII
        return fusedAPIImpl.getApplicationCategoryPreference()
    }

    suspend fun validateAuthData(authData: AuthData): PlayResponse {
        if (authData.authToken.isNotEmpty() && authData.deviceInfoProvider != null) {
            return fusedAPIImpl.validateAuthData(authData)
        }
        return PlayResponse()
    }

    suspend fun getApplicationDetails(
        packageNameList: List<String>,
        authData: AuthData,
@@ -155,6 +165,14 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPII
        return fusedAPIImpl.getSearchSuggestions(query, authData)
    }

    suspend fun fetchAuthData(): Boolean {
        return fusedAPIImpl.fetchAuthData()
    }

    suspend fun fetchAuthData(email: String, aasToken: String): AuthData? {
        return fusedAPIImpl.fetchAuthData(email, aasToken)
    }

    fun getSearchResults(
        query: String,
        authData: AuthData
Loading