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.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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user