mirror of
https://github.com/whyorean/AuroraStore.git
synced 2026-06-11 09:16:06 -04:00
use a SessionExpired event instead of a retry loop
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<Screen.Splash> { SplashScreen(onNavigateTo = ::navigate) }
|
||||
entry<Screen.Splash> { screen ->
|
||||
SplashScreen(
|
||||
deepLinkPackageName = screen.packageName,
|
||||
onNavigateTo = ::navigate
|
||||
)
|
||||
}
|
||||
entry<Screen.Onboarding> { OnboardingScreen() }
|
||||
entry<Screen.Blacklist> { BlacklistScreen() }
|
||||
entry<Screen.Downloads> { DownloadsScreen(onNavigateTo = ::navigate) }
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -81,7 +81,7 @@ fun AccountsScreen(
|
||||
onConfirm = {
|
||||
shouldShowLogoutDialog = false
|
||||
AccountProvider.logout(context)
|
||||
onNavigateTo(Destination.Splash)
|
||||
onNavigateTo(Destination.Splash())
|
||||
},
|
||||
onDismiss = { shouldShowLogoutDialog = false }
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ private fun ScreenContent(
|
||||
when (result) {
|
||||
SnackbarResult.ActionPerformed -> {
|
||||
AccountProvider.logout(context)
|
||||
onNavigateTo(Destination.Splash)
|
||||
onNavigateTo(Destination.Splash())
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<Boolean> = _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<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
|
||||
* @param email E-mail ID
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String>,
|
||||
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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user