use a SessionExpired event instead of a retry loop

This commit is contained in:
Rahul Patel
2026-05-25 03:03:18 +05:30
parent bae9233b27
commit 0bcb087be2
17 changed files with 98 additions and 144 deletions

View File

@@ -15,7 +15,6 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.core.content.IntentCompat import androidx.core.content.IntentCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.aurora.extensions.getPackageName import com.aurora.extensions.getPackageName
import com.aurora.store.compose.composition.LocalNetworkStatus import com.aurora.store.compose.composition.LocalNetworkStatus
import com.aurora.store.compose.composition.LocalUI import com.aurora.store.compose.composition.LocalUI
@@ -24,27 +23,18 @@ import com.aurora.store.compose.navigation.NavDisplay
import com.aurora.store.compose.navigation.Screen import com.aurora.store.compose.navigation.Screen
import com.aurora.store.compose.theme.AuroraTheme import com.aurora.store.compose.theme.AuroraTheme
import com.aurora.store.data.model.NetworkStatus import com.aurora.store.data.model.NetworkStatus
import com.aurora.store.data.providers.AccountProvider
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.data.providers.NetworkProvider import com.aurora.store.data.providers.NetworkProvider
import com.aurora.store.data.receiver.MigrationReceiver import com.aurora.store.data.receiver.MigrationReceiver
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
@AndroidEntryPoint @AndroidEntryPoint
class ComposeActivity : ComponentActivity() { class ComposeActivity : ComponentActivity() {
@Inject lateinit var networkProvider: NetworkProvider @Inject lateinit var networkProvider: NetworkProvider
@Inject lateinit var authProvider: AuthProvider
@Inject lateinit var okHttpClient: OkHttpClient
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
MigrationReceiver.runMigrationsIfRequired(this) MigrationReceiver.runMigrationsIfRequired(this)
enableEdgeToEdge() enableEdgeToEdge()
@@ -99,20 +89,6 @@ class ComposeActivity : ComponentActivity() {
private fun defaultStart(): Screen = when { private fun defaultStart(): Screen = when {
!Preferences.getBoolean(this, Preferences.PREFERENCE_INTRO) -> Screen.Onboarding !Preferences.getBoolean(this, Preferences.PREFERENCE_INTRO) -> Screen.Onboarding
else -> Screen.Splash else -> Screen.Splash()
}
override fun onStart() {
super.onStart()
if (!AccountProvider.isLoggedIn(this)) return
lifecycleScope.launch(Dispatchers.IO) {
// Ask Play directly if the saved token still works. If it does,
// sockets are fine too and there's nothing to do. If it doesn't,
// evict the pool (the same backgrounded state that invalidates
// tokens also leaves dead sockets) and refresh anonymous auth.
if (authProvider.isSavedAuthDataValid()) return@launch
okHttpClient.connectionPool.evictAll()
if (authProvider.isAnonymous) authProvider.refreshAnonymousAuth()
}
} }
} }

View File

@@ -15,7 +15,7 @@ import com.aurora.store.data.room.update.Update
* Screens emit one of these via a single `onNavigateTo: (Destination) -> Unit` callback. * Screens emit one of these via a single `onNavigateTo: (Destination) -> Unit` callback.
*/ */
sealed class Destination { sealed class Destination {
data object Splash : Destination() data class Splash(val packageName: String? = null) : Destination()
data class Main(val initialTab: Int) : Destination() data class Main(val initialTab: Int) : Destination()
data class AppDetails(val packageName: String) : Destination() data class AppDetails(val packageName: String) : Destination()

View File

@@ -58,6 +58,7 @@ import com.aurora.store.compose.ui.preferences.updates.UpdatesPreferenceScreen
import com.aurora.store.compose.ui.search.SearchScreen import com.aurora.store.compose.ui.search.SearchScreen
import com.aurora.store.compose.ui.splash.SplashScreen import com.aurora.store.compose.ui.splash.SplashScreen
import com.aurora.store.compose.ui.spoof.SpoofScreen import com.aurora.store.compose.ui.spoof.SpoofScreen
import com.aurora.store.data.event.AuthEvent
import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.model.AccountType import com.aurora.store.data.model.AccountType
import com.aurora.store.data.providers.AccountProvider import com.aurora.store.data.providers.AccountProvider
@@ -101,12 +102,24 @@ fun NavDisplay(startDestination: NavKey) {
} }
} }
// Send the user back to Splash whenever a ViewModel reports the saved Play
// token was rejected. SplashScreen re-validates via a live Play call and
// rebuilds auth on failure, then routes to AppDetails if a packageName was attached.
LaunchedEffect(Unit) {
AuroraApp.events.authEvent.collect { event ->
if (event is AuthEvent.SessionExpired) {
backstack.clear()
backstack.add(Screen.Splash(event.packageName))
}
}
}
fun navigate(destination: Destination) { fun navigate(destination: Destination) {
when (destination) { when (destination) {
Destination.Splash -> { is Destination.Splash -> {
// Clear the backstack when navigating to Splash to prevent going back to the previous screen when the user is sent back to the splash screen (e.g. after logout). // Clear the backstack when navigating to Splash to prevent going back to the previous screen when the user is sent back to the splash screen (e.g. after logout).
backstack.clear() backstack.clear()
backstack.add(Screen.Splash) backstack.add(Screen.Splash(destination.packageName))
} }
is Destination.Main -> { is Destination.Main -> {
@@ -249,7 +262,12 @@ fun NavDisplay(startDestination: NavKey) {
} }
) { SearchScreen() } ) { SearchScreen() }
entry<Screen.Splash> { SplashScreen(onNavigateTo = ::navigate) } entry<Screen.Splash> { screen ->
SplashScreen(
deepLinkPackageName = screen.packageName,
onNavigateTo = ::navigate
)
}
entry<Screen.Onboarding> { OnboardingScreen() } entry<Screen.Onboarding> { OnboardingScreen() }
entry<Screen.Blacklist> { BlacklistScreen() } entry<Screen.Blacklist> { BlacklistScreen() }
entry<Screen.Downloads> { DownloadsScreen(onNavigateTo = ::navigate) } entry<Screen.Downloads> { DownloadsScreen(onNavigateTo = ::navigate) }

View File

@@ -96,7 +96,7 @@ sealed class Screen : NavKey, Parcelable {
data object SourceFilters : Screen() data object SourceFilters : Screen()
@Serializable @Serializable
data object Splash : Screen() data class Splash(val packageName: String? = null) : Screen()
@Serializable @Serializable
data class Main(val initialTab: Int = 0) : Screen() data class Main(val initialTab: Int = 0) : Screen()

View File

@@ -81,7 +81,7 @@ fun AccountsScreen(
onConfirm = { onConfirm = {
shouldShowLogoutDialog = false shouldShowLogoutDialog = false
AccountProvider.logout(context) AccountProvider.logout(context)
onNavigateTo(Destination.Splash) onNavigateTo(Destination.Splash())
}, },
onDismiss = { shouldShowLogoutDialog = false } onDismiss = { shouldShowLogoutDialog = false }
) )

View File

@@ -74,7 +74,7 @@ fun GoogleLoginScreen(
} else { } else {
Toast.makeText(context, R.string.toast_aas_token_failed, Toast.LENGTH_LONG) Toast.makeText(context, R.string.toast_aas_token_failed, Toast.LENGTH_LONG)
.show() .show()
onNavigateTo(Destination.Splash) onNavigateTo(Destination.Splash())
} }
} }
} }
@@ -87,7 +87,7 @@ fun GoogleLoginScreen(
Preferences.getInteger(context, Preferences.PREFERENCE_DEFAULT_SELECTED_TAB) Preferences.getInteger(context, Preferences.PREFERENCE_DEFAULT_SELECTED_TAB)
) )
) )
is AuthState.Failed -> onNavigateTo(Destination.Splash) is AuthState.Failed -> onNavigateTo(Destination.Splash())
else -> Unit else -> Unit
} }
} }

View File

@@ -96,7 +96,7 @@ private fun ScreenContent(
when (result) { when (result) {
SnackbarResult.ActionPerformed -> { SnackbarResult.ActionPerformed -> {
AccountProvider.logout(context) AccountProvider.logout(context)
onNavigateTo(Destination.Splash) onNavigateTo(Destination.Splash())
} }
else -> Unit else -> Unit

View File

@@ -30,6 +30,7 @@ sealed class BusEvent : Event() {
sealed class AuthEvent : Event() { sealed class AuthEvent : Event() {
data class GoogleLogin(val success: Boolean, val email: String, val token: String) : AuthEvent() data class GoogleLogin(val success: Boolean, val email: String, val token: String) : AuthEvent()
data class SessionExpired(val packageName: String? = null) : AuthEvent()
} }
open class InstallerEvent(open val packageName: String) : Event() { open class InstallerEvent(open val packageName: String) : Event() {

View File

@@ -36,12 +36,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@@ -73,45 +67,11 @@ class AuthProvider @Inject constructor(
val isAnonymous: Boolean val isAnonymous: Boolean
get() = AccountProvider.getAccountType(context) == AccountType.ANONYMOUS get() = AccountProvider.getAccountType(context) == AccountType.ANONYMOUS
private val _authReady = MutableStateFlow(true)
/**
* Emits false while an auth refresh is in flight so callers can defer
* network work until fresh credentials are persisted.
*/
val authReady: StateFlow<Boolean> = _authReady.asStateFlow()
private val refreshMutex = Mutex()
/** /**
* Checks whether saved AuthData is valid or not * Checks whether saved AuthData is valid or not
*/ */
fun isSavedAuthDataValid(): Boolean = AuthHelper.isValid(authData!!) fun isSavedAuthDataValid(): Boolean = AuthHelper.isValid(authData!!)
/**
* Suspends until no auth refresh is in flight. ViewModels should call this
* before issuing Play requests so a foreground refresh doesn't race the fetch.
*/
suspend fun awaitReady() {
_authReady.first { it }
}
/**
* Rebuilds anonymous [AuthData] from a dispenser and persists it. Serialized
* so concurrent callers don't double-fetch. On failure, the previously saved
* auth is left in place so requests can still attempt with stale credentials.
*/
suspend fun refreshAnonymousAuth(): Result<AuthData> = refreshMutex.withLock {
try {
_authReady.value = false
val result = buildAnonymousAuthData()
result.onSuccess { saveAuthData(it) }
result
} finally {
_authReady.value = true
}
}
/** /**
* Builds [AuthData] for login using personal Google account * Builds [AuthData] for login using personal Google account
* @param email E-mail ID * @param email E-mail ID

View File

@@ -13,10 +13,12 @@ import androidx.paging.PagingData
import androidx.paging.cachedIn import androidx.paging.cachedIn
import com.aurora.extensions.TAG import com.aurora.extensions.TAG
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.exceptions.GooglePlayException
import com.aurora.gplayapi.helpers.ExpandedBrowseHelper import com.aurora.gplayapi.helpers.ExpandedBrowseHelper
import com.aurora.store.AuroraApp
import com.aurora.store.data.PageResult import com.aurora.store.data.PageResult
import com.aurora.store.data.event.AuthEvent
import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager
import com.aurora.store.data.providers.AuthProvider
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@@ -30,7 +32,6 @@ import kotlinx.coroutines.flow.onEach
@HiltViewModel(assistedFactory = ExpandedStreamBrowseViewModel.Factory::class) @HiltViewModel(assistedFactory = ExpandedStreamBrowseViewModel.Factory::class)
class ExpandedStreamBrowseViewModel @AssistedInject constructor( class ExpandedStreamBrowseViewModel @AssistedInject constructor(
@Assisted val browseUrl: String, @Assisted val browseUrl: String,
private val authProvider: AuthProvider,
private val streamHelper: ExpandedBrowseHelper private val streamHelper: ExpandedBrowseHelper
) : ViewModel() { ) : ViewModel() {
@@ -55,7 +56,6 @@ class ExpandedStreamBrowseViewModel @AssistedInject constructor(
manualPager { page -> manualPager { page ->
val items = try { val items = try {
authProvider.awaitReady()
when (page) { when (page) {
1 -> { 1 -> {
val browseResponse = streamHelper.getBrowseStreamResponse(browseUrl) val browseResponse = streamHelper.getBrowseStreamResponse(browseUrl)
@@ -86,6 +86,10 @@ class ExpandedStreamBrowseViewModel @AssistedInject constructor(
} }
} }
} }
} catch (exception: GooglePlayException.AuthException) {
Log.w(TAG, "Expanded stream returned ${exception.code}, redirecting to Splash")
AuroraApp.events.send(AuthEvent.SessionExpired())
emptyList()
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, "Failed to fetch apps for page $page", exception) Log.e(TAG, "Failed to fetch apps for page $page", exception)
emptyList() emptyList()

View File

@@ -25,11 +25,13 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aurora.extensions.TAG import com.aurora.extensions.TAG
import com.aurora.gplayapi.data.models.Category import com.aurora.gplayapi.data.models.Category
import com.aurora.gplayapi.exceptions.GooglePlayException
import com.aurora.gplayapi.helpers.CategoryHelper import com.aurora.gplayapi.helpers.CategoryHelper
import com.aurora.gplayapi.helpers.contracts.CategoryContract import com.aurora.gplayapi.helpers.contracts.CategoryContract
import com.aurora.store.AuroraApp
import com.aurora.store.CategoryStash import com.aurora.store.CategoryStash
import com.aurora.store.data.event.AuthEvent
import com.aurora.store.data.model.ViewState import com.aurora.store.data.model.ViewState
import com.aurora.store.data.providers.AuthProvider
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -37,7 +39,6 @@ import kotlinx.coroutines.launch
@HiltViewModel @HiltViewModel
class CategoryViewModel @Inject constructor( class CategoryViewModel @Inject constructor(
private val authProvider: AuthProvider,
private val categoryHelper: CategoryHelper private val categoryHelper: CategoryHelper
) : ViewModel() { ) : ViewModel() {
@@ -62,9 +63,11 @@ class CategoryViewModel @Inject constructor(
liveData.postValue(ViewState.Loading) liveData.postValue(ViewState.Loading)
try { try {
authProvider.awaitReady()
stash[type] = contract().getAllCategories(type) stash[type] = contract().getAllCategories(type)
liveData.postValue(ViewState.Success(stash)) liveData.postValue(ViewState.Success(stash))
} catch (exception: GooglePlayException.AuthException) {
Log.w(TAG, "Categories fetch returned ${exception.code}, redirecting to Splash")
AuroraApp.events.send(AuthEvent.SessionExpired())
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, "Failed fetching list of categories", exception) Log.e(TAG, "Failed fetching list of categories", exception)
liveData.postValue(ViewState.Error(exception.message)) liveData.postValue(ViewState.Error(exception.message))

View File

@@ -27,6 +27,7 @@ import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper
import com.aurora.gplayapi.network.IHttpClient import com.aurora.gplayapi.network.IHttpClient
import com.aurora.store.AuroraApp import com.aurora.store.AuroraApp
import com.aurora.store.BuildConfig import com.aurora.store.BuildConfig
import com.aurora.store.data.event.AuthEvent
import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.helper.DownloadHelper import com.aurora.store.data.helper.DownloadHelper
import com.aurora.store.data.model.AppState import com.aurora.store.data.model.AppState
@@ -162,10 +163,7 @@ class AppDetailsViewModel @Inject constructor(
fun fetchAppDetails(packageName: String) { fun fetchAppDetails(packageName: String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
var retried = false
while (true) {
try { try {
authProvider.awaitReady()
_app.value = appDetailsHelper.getAppByPackageName(packageName).copy( _app.value = appDetailsHelper.getAppByPackageName(packageName).copy(
isInstalled = PackageUtil.isInstalled(context, packageName) isInstalled = PackageUtil.isInstalled(context, packageName)
) )
@@ -185,25 +183,16 @@ class AppDetailsViewModel @Inject constructor(
_state.value = _state.value =
existingDownload?.let { stateFromDownload(it) } ?: defaultAppState existingDownload?.let { stateFromDownload(it) } ?: defaultAppState
} }
break } catch (exception: GooglePlayException.AuthException) {
// The saved Play token has been rejected mid-session. Hand off to
// Splash to re-validate and rebuild auth, and ask it to bring the
// user back to this app's details once auth is good again.
Log.w(TAG, "App details fetch returned ${exception.code}, redirecting to Splash")
AuroraApp.events.send(AuthEvent.SessionExpired(packageName))
} catch (exception: Exception) { } catch (exception: Exception) {
// gplayapi throws AppNotFound(code=401) when the saved token is
// rejected mid-session; refresh anonymous auth once and retry.
if (!retried &&
authProvider.isAnonymous &&
exception is GooglePlayException.NotFound &&
exception.code == 401
) {
Log.w(TAG, "App details fetch returned 401, refreshing auth", exception)
retried = true
authProvider.refreshAnonymousAuth()
continue
}
Log.e(TAG, "Failed to fetch app details", exception) Log.e(TAG, "Failed to fetch app details", exception)
_app.value = null _app.value = null
_state.value = AppState.Error(exception.message) _state.value = AppState.Error(exception.message)
break
}
} }
}.invokeOnCompletion { throwable -> }.invokeOnCompletion { throwable ->
// Only proceed if there was no error while fetching the app details // Only proceed if there was no error while fetching the app details

View File

@@ -27,11 +27,13 @@ import com.aurora.extensions.TAG
import com.aurora.gplayapi.data.models.StreamBundle import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.data.models.details.DevStream import com.aurora.gplayapi.data.models.details.DevStream
import com.aurora.gplayapi.exceptions.GooglePlayException
import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.gplayapi.helpers.StreamHelper import com.aurora.gplayapi.helpers.StreamHelper
import com.aurora.gplayapi.helpers.contracts.StreamContract import com.aurora.gplayapi.helpers.contracts.StreamContract
import com.aurora.store.AuroraApp
import com.aurora.store.data.event.AuthEvent
import com.aurora.store.data.model.ViewState import com.aurora.store.data.model.ViewState
import com.aurora.store.data.providers.AuthProvider
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -40,7 +42,6 @@ import kotlinx.coroutines.supervisorScope
@HiltViewModel @HiltViewModel
class DevProfileViewModel @Inject constructor( class DevProfileViewModel @Inject constructor(
private val authProvider: AuthProvider,
private val appDetailsHelper: AppDetailsHelper, private val appDetailsHelper: AppDetailsHelper,
private val streamHelper: StreamHelper private val streamHelper: StreamHelper
) : ViewModel() { ) : ViewModel() {
@@ -56,10 +57,12 @@ class DevProfileViewModel @Inject constructor(
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
supervisorScope { supervisorScope {
try { try {
authProvider.awaitReady()
devStream = appDetailsHelper.getDeveloperStream(devId) devStream = appDetailsHelper.getDeveloperStream(devId)
streamBundle = devStream.streamBundle streamBundle = devStream.streamBundle
liveData.postValue(ViewState.Success(devStream)) liveData.postValue(ViewState.Success(devStream))
} catch (e: GooglePlayException.AuthException) {
Log.w(TAG, "Developer stream fetch returned ${e.code}, redirecting to Splash")
AuroraApp.events.send(AuthEvent.SessionExpired())
} catch (e: Exception) { } catch (e: Exception) {
liveData.postValue(ViewState.Error(e.message)) liveData.postValue(ViewState.Error(e.message))
} }
@@ -72,7 +75,6 @@ class DevProfileViewModel @Inject constructor(
supervisorScope { supervisorScope {
try { try {
if (streamCluster.hasNext()) { if (streamCluster.hasNext()) {
authProvider.awaitReady()
val newCluster = streamHelper.getNextStreamCluster( val newCluster = streamHelper.getNextStreamCluster(
streamCluster.id, streamCluster.id,
streamCluster.clusterNextPageUrl streamCluster.clusterNextPageUrl

View File

@@ -11,8 +11,10 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aurora.extensions.TAG import com.aurora.extensions.TAG
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.exceptions.GooglePlayException
import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.AuroraApp
import com.aurora.store.data.event.AuthEvent
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@@ -25,7 +27,6 @@ import kotlinx.coroutines.launch
@HiltViewModel(assistedFactory = MoreViewModel.Factory::class) @HiltViewModel(assistedFactory = MoreViewModel.Factory::class)
class MoreViewModel @AssistedInject constructor( class MoreViewModel @AssistedInject constructor(
@Assisted private val dependencies: List<String>, @Assisted private val dependencies: List<String>,
private val authProvider: AuthProvider,
private val appDetailsHelper: AppDetailsHelper private val appDetailsHelper: AppDetailsHelper
) : ViewModel() { ) : ViewModel() {
@@ -44,8 +45,10 @@ class MoreViewModel @AssistedInject constructor(
private fun fetchDependencies() { private fun fetchDependencies() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
authProvider.awaitReady()
_dependentApps.value = appDetailsHelper.getAppByPackageName(dependencies) _dependentApps.value = appDetailsHelper.getAppByPackageName(dependencies)
} catch (exception: GooglePlayException.AuthException) {
Log.w(TAG, "Dependencies fetch returned ${exception.code}, redirecting to Splash")
AuroraApp.events.send(AuthEvent.SessionExpired())
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, "Failed to fetch dependencies", exception) Log.e(TAG, "Failed to fetch dependencies", exception)
_dependentApps.value = null _dependentApps.value = null

View File

@@ -13,10 +13,12 @@ import androidx.paging.PagingData
import androidx.paging.cachedIn import androidx.paging.cachedIn
import com.aurora.extensions.TAG import com.aurora.extensions.TAG
import com.aurora.gplayapi.data.models.Review import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.exceptions.GooglePlayException
import com.aurora.gplayapi.helpers.ReviewsHelper import com.aurora.gplayapi.helpers.ReviewsHelper
import com.aurora.store.AuroraApp
import com.aurora.store.data.PageResult import com.aurora.store.data.PageResult
import com.aurora.store.data.event.AuthEvent
import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager
import com.aurora.store.data.providers.AuthProvider
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@@ -30,7 +32,6 @@ import kotlinx.coroutines.flow.onEach
@HiltViewModel(assistedFactory = ReviewViewModel.Factory::class) @HiltViewModel(assistedFactory = ReviewViewModel.Factory::class)
class ReviewViewModel @AssistedInject constructor( class ReviewViewModel @AssistedInject constructor(
@Assisted private val packageName: String, @Assisted private val packageName: String,
private val authProvider: AuthProvider,
private val reviewsHelper: ReviewsHelper private val reviewsHelper: ReviewsHelper
) : ViewModel() { ) : ViewModel() {
@@ -51,7 +52,6 @@ class ReviewViewModel @AssistedInject constructor(
manualPager { page -> manualPager { page ->
val items = try { val items = try {
authProvider.awaitReady()
when (page) { when (page) {
1 -> reviewsHelper.getReviews(packageName, filter).also { 1 -> reviewsHelper.getReviews(packageName, filter).also {
reviewsNextPageUrl = it.nextPageUrl reviewsNextPageUrl = it.nextPageUrl
@@ -67,6 +67,10 @@ class ReviewViewModel @AssistedInject constructor(
} }
} }
} }
} catch (exception: GooglePlayException.AuthException) {
Log.w(TAG, "Reviews fetch returned ${exception.code}, redirecting to Splash")
AuroraApp.events.send(AuthEvent.SessionExpired(packageName))
emptyList()
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, "Failed to fetch reviews for $page: $reviewsNextPageUrl", exception) Log.e(TAG, "Failed to fetch reviews for $page: $reviewsNextPageUrl", exception)
emptyList() emptyList()

View File

@@ -80,19 +80,8 @@ class StreamViewModel @Inject constructor(
streamContract.fetch(type, category) streamContract.fetch(type, category)
} }
// gplayapi 3.6.1 stamps every cluster in a bundle with the bundle id,
// so naive Map.plus drops the existing page's clusters. Re-key the new
// clusters with synthetic ids past the current max so pagination appends.
val baseKey = (bundle.streamClusters.keys.maxOrNull() ?: 0) + 1
val rekeyedNewClusters = newBundle.streamClusters.values
.mapIndexed { index, cluster ->
val newId = baseKey + index
newId to cluster.copy(id = newId)
}
.toMap()
val mergedBundle = bundle.copy( val mergedBundle = bundle.copy(
streamClusters = bundle.streamClusters + rekeyedNewClusters, streamClusters = bundle.streamClusters + newBundle.streamClusters,
streamNextPageUrl = newBundle.streamNextPageUrl streamNextPageUrl = newBundle.streamNextPageUrl
) )
stash[category] = mergedBundle stash[category] = mergedBundle

View File

@@ -18,9 +18,12 @@ import com.aurora.extensions.requiresGMS
import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.SearchSuggestEntry
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.exceptions.GooglePlayException
import com.aurora.gplayapi.helpers.contracts.SearchContract import com.aurora.gplayapi.helpers.contracts.SearchContract
import com.aurora.gplayapi.helpers.web.WebSearchHelper import com.aurora.gplayapi.helpers.web.WebSearchHelper
import com.aurora.store.AuroraApp
import com.aurora.store.data.PageResult import com.aurora.store.data.PageResult
import com.aurora.store.data.event.AuthEvent
import com.aurora.store.data.model.SearchFilter import com.aurora.store.data.model.SearchFilter
import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.providers.AuthProvider
@@ -81,7 +84,6 @@ class SearchViewModel @Inject constructor(
manualPager { page -> manualPager { page ->
val items = try { val items = try {
authProvider.awaitReady()
when (page) { when (page) {
1 -> contract.searchResults(query) 1 -> contract.searchResults(query)
.also { nextBundleUrl = it.streamNextPageUrl } .also { nextBundleUrl = it.streamNextPageUrl }
@@ -107,6 +109,10 @@ class SearchViewModel @Inject constructor(
} }
} }
} }
} catch (exception: GooglePlayException.AuthException) {
Log.w(TAG, "Search returned ${exception.code}, redirecting to Splash")
AuroraApp.events.send(AuthEvent.SessionExpired())
emptyList()
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, "Failed to search results for $query", exception) Log.e(TAG, "Failed to search results for $query", exception)
emptyList() emptyList()
@@ -120,7 +126,6 @@ class SearchViewModel @Inject constructor(
fun fetchSuggestions(query: String) { fun fetchSuggestions(query: String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
authProvider.awaitReady()
_suggestions.value = contract.searchSuggestions(query) _suggestions.value = contract.searchSuggestions(query)
.filter { it.title.isNotBlank() } .filter { it.title.isNotBlank() }
.take(5) .take(5)