diff --git a/app/src/main/java/com/aurora/store/ComposeActivity.kt b/app/src/main/java/com/aurora/store/ComposeActivity.kt index b965f21e9..ffa594b24 100644 --- a/app/src/main/java/com/aurora/store/ComposeActivity.kt +++ b/app/src/main/java/com/aurora/store/ComposeActivity.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.core.content.IntentCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.lifecycleScope import com.aurora.extensions.getPackageName import com.aurora.store.compose.composition.LocalNetworkStatus 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.theme.AuroraTheme 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.receiver.MigrationReceiver import com.aurora.store.util.PackageUtil import com.aurora.store.util.Preferences import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import okhttp3.OkHttpClient @AndroidEntryPoint class ComposeActivity : ComponentActivity() { @Inject lateinit var networkProvider: NetworkProvider - @Inject lateinit var authProvider: AuthProvider - - @Inject lateinit var okHttpClient: OkHttpClient - override fun onCreate(savedInstanceState: Bundle?) { MigrationReceiver.runMigrationsIfRequired(this) enableEdgeToEdge() @@ -99,20 +89,6 @@ class ComposeActivity : ComponentActivity() { private fun defaultStart(): Screen = when { !Preferences.getBoolean(this, Preferences.PREFERENCE_INTRO) -> Screen.Onboarding - 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() - } + else -> Screen.Splash() } } diff --git a/app/src/main/java/com/aurora/store/compose/navigation/Destination.kt b/app/src/main/java/com/aurora/store/compose/navigation/Destination.kt index da381c775..8f8bc2b4f 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/Destination.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/Destination.kt @@ -15,7 +15,7 @@ import com.aurora.store.data.room.update.Update * Screens emit one of these via a single `onNavigateTo: (Destination) -> Unit` callback. */ sealed class Destination { - data object Splash : Destination() + data class Splash(val packageName: String? = null) : Destination() data class Main(val initialTab: Int) : Destination() data class AppDetails(val packageName: String) : Destination() diff --git a/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt b/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt index 0846dc0cf..47ee6896f 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/NavDisplay.kt @@ -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.splash.SplashScreen 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.model.AccountType 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) { 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). backstack.clear() - backstack.add(Screen.Splash) + backstack.add(Screen.Splash(destination.packageName)) } is Destination.Main -> { @@ -249,7 +262,12 @@ fun NavDisplay(startDestination: NavKey) { } ) { SearchScreen() } - entry { SplashScreen(onNavigateTo = ::navigate) } + entry { screen -> + SplashScreen( + deepLinkPackageName = screen.packageName, + onNavigateTo = ::navigate + ) + } entry { OnboardingScreen() } entry { BlacklistScreen() } entry { DownloadsScreen(onNavigateTo = ::navigate) } diff --git a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt index f8b0b0870..1ce32189a 100644 --- a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt +++ b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt @@ -96,7 +96,7 @@ sealed class Screen : NavKey, Parcelable { data object SourceFilters : Screen() @Serializable - data object Splash : Screen() + data class Splash(val packageName: String? = null) : Screen() @Serializable data class Main(val initialTab: Int = 0) : Screen() diff --git a/app/src/main/java/com/aurora/store/compose/ui/accounts/AccountsScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/accounts/AccountsScreen.kt index bbd9a6af6..733510a27 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/accounts/AccountsScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/accounts/AccountsScreen.kt @@ -81,7 +81,7 @@ fun AccountsScreen( onConfirm = { shouldShowLogoutDialog = false AccountProvider.logout(context) - onNavigateTo(Destination.Splash) + onNavigateTo(Destination.Splash()) }, onDismiss = { shouldShowLogoutDialog = false } ) diff --git a/app/src/main/java/com/aurora/store/compose/ui/accounts/GoogleLoginScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/accounts/GoogleLoginScreen.kt index ff20573bf..36764da81 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/accounts/GoogleLoginScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/accounts/GoogleLoginScreen.kt @@ -74,7 +74,7 @@ fun GoogleLoginScreen( } else { Toast.makeText(context, R.string.toast_aas_token_failed, Toast.LENGTH_LONG) .show() - onNavigateTo(Destination.Splash) + onNavigateTo(Destination.Splash()) } } } @@ -87,7 +87,7 @@ fun GoogleLoginScreen( Preferences.getInteger(context, Preferences.PREFERENCE_DEFAULT_SELECTED_TAB) ) ) - is AuthState.Failed -> onNavigateTo(Destination.Splash) + is AuthState.Failed -> onNavigateTo(Destination.Splash()) else -> Unit } } diff --git a/app/src/main/java/com/aurora/store/compose/ui/spoof/SpoofScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/spoof/SpoofScreen.kt index f43b42fa2..17c539c19 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/spoof/SpoofScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/spoof/SpoofScreen.kt @@ -96,7 +96,7 @@ private fun ScreenContent( when (result) { SnackbarResult.ActionPerformed -> { AccountProvider.logout(context) - onNavigateTo(Destination.Splash) + onNavigateTo(Destination.Splash()) } else -> Unit diff --git a/app/src/main/java/com/aurora/store/data/event/BusEvent.kt b/app/src/main/java/com/aurora/store/data/event/BusEvent.kt index 444e18c37..d7d4269a1 100644 --- a/app/src/main/java/com/aurora/store/data/event/BusEvent.kt +++ b/app/src/main/java/com/aurora/store/data/event/BusEvent.kt @@ -30,6 +30,7 @@ sealed class BusEvent : Event() { sealed class AuthEvent : Event() { 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() { diff --git a/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt b/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt index 70cef5d73..e82a5a882 100644 --- a/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt +++ b/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt @@ -36,12 +36,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton 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.serialization.json.Json @@ -73,45 +67,11 @@ class AuthProvider @Inject constructor( val isAnonymous: Boolean 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 = _authReady.asStateFlow() - - private val refreshMutex = Mutex() - /** * Checks whether saved AuthData is valid or not */ 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 = 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 * @param email E-mail ID diff --git a/app/src/main/java/com/aurora/store/viewmodel/browse/ExpandedStreamBrowseViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/browse/ExpandedStreamBrowseViewModel.kt index 6ab06a002..3acfcb5f2 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/browse/ExpandedStreamBrowseViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/browse/ExpandedStreamBrowseViewModel.kt @@ -13,10 +13,12 @@ import androidx.paging.PagingData import androidx.paging.cachedIn import com.aurora.extensions.TAG import com.aurora.gplayapi.data.models.App +import com.aurora.gplayapi.exceptions.GooglePlayException import com.aurora.gplayapi.helpers.ExpandedBrowseHelper +import com.aurora.store.AuroraApp 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.providers.AuthProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -30,7 +32,6 @@ import kotlinx.coroutines.flow.onEach @HiltViewModel(assistedFactory = ExpandedStreamBrowseViewModel.Factory::class) class ExpandedStreamBrowseViewModel @AssistedInject constructor( @Assisted val browseUrl: String, - private val authProvider: AuthProvider, private val streamHelper: ExpandedBrowseHelper ) : ViewModel() { @@ -55,7 +56,6 @@ class ExpandedStreamBrowseViewModel @AssistedInject constructor( manualPager { page -> val items = try { - authProvider.awaitReady() when (page) { 1 -> { 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) { Log.e(TAG, "Failed to fetch apps for page $page", exception) emptyList() diff --git a/app/src/main/java/com/aurora/store/viewmodel/category/CategoryViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/category/CategoryViewModel.kt index 305f1ab2d..5dbc811c1 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/category/CategoryViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/category/CategoryViewModel.kt @@ -25,11 +25,13 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.extensions.TAG import com.aurora.gplayapi.data.models.Category +import com.aurora.gplayapi.exceptions.GooglePlayException import com.aurora.gplayapi.helpers.CategoryHelper import com.aurora.gplayapi.helpers.contracts.CategoryContract +import com.aurora.store.AuroraApp import com.aurora.store.CategoryStash +import com.aurora.store.data.event.AuthEvent import com.aurora.store.data.model.ViewState -import com.aurora.store.data.providers.AuthProvider import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.Dispatchers @@ -37,7 +39,6 @@ import kotlinx.coroutines.launch @HiltViewModel class CategoryViewModel @Inject constructor( - private val authProvider: AuthProvider, private val categoryHelper: CategoryHelper ) : ViewModel() { @@ -62,9 +63,11 @@ class CategoryViewModel @Inject constructor( liveData.postValue(ViewState.Loading) try { - authProvider.awaitReady() stash[type] = contract().getAllCategories(type) 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) { Log.e(TAG, "Failed fetching list of categories", exception) liveData.postValue(ViewState.Error(exception.message)) diff --git a/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt index a6d6de453..b9b7aad0a 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt @@ -27,6 +27,7 @@ import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper import com.aurora.gplayapi.network.IHttpClient import com.aurora.store.AuroraApp import com.aurora.store.BuildConfig +import com.aurora.store.data.event.AuthEvent import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.helper.DownloadHelper import com.aurora.store.data.model.AppState @@ -162,48 +163,36 @@ class AppDetailsViewModel @Inject constructor( fun fetchAppDetails(packageName: String) { viewModelScope.launch(Dispatchers.IO) { - var retried = false - while (true) { - try { - authProvider.awaitReady() - _app.value = appDetailsHelper.getAppByPackageName(packageName).copy( - isInstalled = PackageUtil.isInstalled(context, packageName) - ) - val existingDownload = downloadHelper.getDownload(packageName) + try { + _app.value = appDetailsHelper.getAppByPackageName(packageName).copy( + isInstalled = PackageUtil.isInstalled(context, packageName) + ) + val existingDownload = downloadHelper.getDownload(packageName) - // A COMPLETED record for an app that is no longer installed means the app was - // installed then removed while Aurora held a stale record. - // Remove it so the live download observer doesn't lock the UI in Installing state - // indefinitely. - if (existingDownload?.status == DownloadStatus.COMPLETED && !isInstalled) { - downloadHelper.removeDownload(packageName) - _state.value = defaultAppState - } else { - // Seed state from any in-flight download for this package so reopening - // the screen doesn't briefly flash the default install action while the - // download flow catches up. - _state.value = - existingDownload?.let { stateFromDownload(it) } ?: defaultAppState - } - break - } 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) - _app.value = null - _state.value = AppState.Error(exception.message) - break + // A COMPLETED record for an app that is no longer installed means the app was + // installed then removed while Aurora held a stale record. + // Remove it so the live download observer doesn't lock the UI in Installing state + // indefinitely. + if (existingDownload?.status == DownloadStatus.COMPLETED && !isInstalled) { + downloadHelper.removeDownload(packageName) + _state.value = defaultAppState + } else { + // Seed state from any in-flight download for this package so reopening + // the screen doesn't briefly flash the default install action while the + // download flow catches up. + _state.value = + existingDownload?.let { stateFromDownload(it) } ?: defaultAppState } + } 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) { + Log.e(TAG, "Failed to fetch app details", exception) + _app.value = null + _state.value = AppState.Error(exception.message) } }.invokeOnCompletion { throwable -> // Only proceed if there was no error while fetching the app details diff --git a/app/src/main/java/com/aurora/store/viewmodel/details/DevProfileViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/details/DevProfileViewModel.kt index 377ffb8d0..ef92da55f 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/details/DevProfileViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/details/DevProfileViewModel.kt @@ -27,11 +27,13 @@ import com.aurora.extensions.TAG import com.aurora.gplayapi.data.models.StreamBundle import com.aurora.gplayapi.data.models.StreamCluster 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.StreamHelper 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.providers.AuthProvider import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.Dispatchers @@ -40,7 +42,6 @@ import kotlinx.coroutines.supervisorScope @HiltViewModel class DevProfileViewModel @Inject constructor( - private val authProvider: AuthProvider, private val appDetailsHelper: AppDetailsHelper, private val streamHelper: StreamHelper ) : ViewModel() { @@ -56,10 +57,12 @@ class DevProfileViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { supervisorScope { try { - authProvider.awaitReady() devStream = appDetailsHelper.getDeveloperStream(devId) streamBundle = devStream.streamBundle 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) { liveData.postValue(ViewState.Error(e.message)) } @@ -72,7 +75,6 @@ class DevProfileViewModel @Inject constructor( supervisorScope { try { if (streamCluster.hasNext()) { - authProvider.awaitReady() val newCluster = streamHelper.getNextStreamCluster( streamCluster.id, streamCluster.clusterNextPageUrl diff --git a/app/src/main/java/com/aurora/store/viewmodel/details/MoreViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/details/MoreViewModel.kt index bfe555bc0..1c8d3591b 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/details/MoreViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/details/MoreViewModel.kt @@ -11,8 +11,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.extensions.TAG import com.aurora.gplayapi.data.models.App +import com.aurora.gplayapi.exceptions.GooglePlayException 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.AssistedFactory import dagger.assisted.AssistedInject @@ -25,7 +27,6 @@ import kotlinx.coroutines.launch @HiltViewModel(assistedFactory = MoreViewModel.Factory::class) class MoreViewModel @AssistedInject constructor( @Assisted private val dependencies: List, - private val authProvider: AuthProvider, private val appDetailsHelper: AppDetailsHelper ) : ViewModel() { @@ -44,8 +45,10 @@ class MoreViewModel @AssistedInject constructor( private fun fetchDependencies() { viewModelScope.launch(Dispatchers.IO) { try { - authProvider.awaitReady() _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) { Log.e(TAG, "Failed to fetch dependencies", exception) _dependentApps.value = null diff --git a/app/src/main/java/com/aurora/store/viewmodel/details/ReviewViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/details/ReviewViewModel.kt index 618cf5d5a..54181181b 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/details/ReviewViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/details/ReviewViewModel.kt @@ -13,10 +13,12 @@ import androidx.paging.PagingData import androidx.paging.cachedIn import com.aurora.extensions.TAG import com.aurora.gplayapi.data.models.Review +import com.aurora.gplayapi.exceptions.GooglePlayException import com.aurora.gplayapi.helpers.ReviewsHelper +import com.aurora.store.AuroraApp 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.providers.AuthProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -30,7 +32,6 @@ import kotlinx.coroutines.flow.onEach @HiltViewModel(assistedFactory = ReviewViewModel.Factory::class) class ReviewViewModel @AssistedInject constructor( @Assisted private val packageName: String, - private val authProvider: AuthProvider, private val reviewsHelper: ReviewsHelper ) : ViewModel() { @@ -51,7 +52,6 @@ class ReviewViewModel @AssistedInject constructor( manualPager { page -> val items = try { - authProvider.awaitReady() when (page) { 1 -> reviewsHelper.getReviews(packageName, filter).also { 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) { Log.e(TAG, "Failed to fetch reviews for $page: $reviewsNextPageUrl", exception) emptyList() diff --git a/app/src/main/java/com/aurora/store/viewmodel/homestream/StreamViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/homestream/StreamViewModel.kt index b19442b99..158c9eb29 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/homestream/StreamViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/homestream/StreamViewModel.kt @@ -80,19 +80,8 @@ class StreamViewModel @Inject constructor( 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( - streamClusters = bundle.streamClusters + rekeyedNewClusters, + streamClusters = bundle.streamClusters + newBundle.streamClusters, streamNextPageUrl = newBundle.streamNextPageUrl ) stash[category] = mergedBundle diff --git a/app/src/main/java/com/aurora/store/viewmodel/search/SearchViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/search/SearchViewModel.kt index 3f60e3ef3..5ecc2dbf0 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/search/SearchViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/search/SearchViewModel.kt @@ -18,9 +18,12 @@ import com.aurora.extensions.requiresGMS import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App 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.web.WebSearchHelper +import com.aurora.store.AuroraApp 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.paging.GenericPagingSource.Companion.manualPager import com.aurora.store.data.providers.AuthProvider @@ -81,7 +84,6 @@ class SearchViewModel @Inject constructor( manualPager { page -> val items = try { - authProvider.awaitReady() when (page) { 1 -> contract.searchResults(query) .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) { Log.e(TAG, "Failed to search results for $query", exception) emptyList() @@ -120,7 +126,6 @@ class SearchViewModel @Inject constructor( fun fetchSuggestions(query: String) { viewModelScope.launch(Dispatchers.IO) { - authProvider.awaitReady() _suggestions.value = contract.searchSuggestions(query) .filter { it.title.isNotBlank() } .take(5)