Updates to session restoration (#1905)
## Description This PR reworks some of the internal handling of changing users and auto sign in. 1. Most recently used server/user is migrated from `AppPreferences` to `MostRecentServerProvider` 2. Checking if a user profile is protected occurs at a lower level in `ServerRepository` so fewer services need to check for this 3. Now always defaults to the most recent server's user list in cases where the most recent user can't be switched to such as if the profile is protected, auto signin is disabled, or and error occurred The default implementation for `MostRecentServerProvider` uses Android's `SharedPreferences`. `SharedPreferences` was already was being used to track the most recent server for crash reporting (the crash reporting process cannot use the Hilt DI). This PR enhances this and adds a better API for saving/retrieving. This also removes the last bit of "user specific" data from `AppPreferences`. ### Related issues None specific, but there have been a few related to auto sign-in & intents that were recently fixed. This PR is a more detailed approach to those same problems. ### Testing Manually via the emulator. There are also unit tests for most (all?) of the different settings and state combinations. ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
a11cd9fb74
commit
f01c5d367c
19 changed files with 786 additions and 493 deletions
|
|
@ -9,6 +9,7 @@ import android.view.WindowManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
|
@ -75,7 +76,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -84,7 +84,6 @@ import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
@ -436,120 +435,27 @@ class MainActivityViewModel
|
||||||
viewModelScope.launchDefault {
|
viewModelScope.launchDefault {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
try {
|
try {
|
||||||
val result = intent?.let { intentService.parseIntent(intent) }
|
handleAppUpgrade()
|
||||||
when (result) {
|
} catch (ex: Exception) {
|
||||||
is IntentResult.Error -> {
|
Timber.e(ex, "Error during app upgrade!")
|
||||||
val current = serverRepository.current.value
|
showToast(
|
||||||
val destination =
|
context,
|
||||||
if (current != null) {
|
"Error during app upgrade: ${ex.localizedMessage}",
|
||||||
SetupDestination.UserList(current.server)
|
)
|
||||||
} else {
|
}
|
||||||
SetupDestination.ServerList
|
|
||||||
}
|
|
||||||
setupNavigationManager.navigateTo(destination)
|
|
||||||
Timber.e("Error parsing intent: %s", result.message)
|
|
||||||
showToast(context, result.message)
|
|
||||||
return@withLock
|
|
||||||
}
|
|
||||||
|
|
||||||
is IntentResult.Target -> {
|
try {
|
||||||
val current = serverRepository.current.value
|
val result = handleIntent(intent)
|
||||||
if (current != null) {
|
if (result) {
|
||||||
Timber.i("Received valid intent, switching to AppContent")
|
Timber.i("appStart was handled by intent")
|
||||||
|
return@withLock
|
||||||
if (result.addHomeToBackStack) {
|
|
||||||
navigationManager.reloadHome()
|
|
||||||
} else {
|
|
||||||
navigationManager.backStack.clear()
|
|
||||||
}
|
|
||||||
navigationManager.backStack.addAll(result.destinations)
|
|
||||||
|
|
||||||
setupNavigationManager.navigateTo(
|
|
||||||
SetupDestination.AppContent(current),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// This should never happen, but just reset app state if it does
|
|
||||||
setupNavigationManager.navigateTo(SetupDestination.ServerList)
|
|
||||||
Timber.e("Error parsing intent, no user is active")
|
|
||||||
showToast(context, "An error occurred parsing the intent")
|
|
||||||
}
|
|
||||||
return@withLock
|
|
||||||
}
|
|
||||||
|
|
||||||
IntentResult.NoOp,
|
|
||||||
null,
|
|
||||||
-> {
|
|
||||||
// No-op, proceed below
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Timber.e(ex, "Error parsing intent")
|
Timber.e(ex, "Error parsing intent")
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val needUpgrade = appUpgradeHandler.needUpgrade()
|
handleAppStart()
|
||||||
if (needUpgrade) {
|
|
||||||
showToast(
|
|
||||||
context,
|
|
||||||
context.getString(
|
|
||||||
R.string.updated_toast,
|
|
||||||
appUpgradeHandler.currentVersion.toString(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
appUpgradeHandler.run()
|
|
||||||
}
|
|
||||||
appUpgradeHandler.copySubfont(false)
|
|
||||||
val prefs =
|
|
||||||
preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
|
|
||||||
val profileProtected =
|
|
||||||
serverRepository.current.value
|
|
||||||
?.user
|
|
||||||
?.isProtected == true
|
|
||||||
if (prefs.signInAutomatically && !profileProtected) {
|
|
||||||
val current =
|
|
||||||
serverRepository.restoreSession(
|
|
||||||
prefs.currentServerId?.toUUIDOrNull(),
|
|
||||||
prefs.currentUserId?.toUUIDOrNull(),
|
|
||||||
)
|
|
||||||
if (current != null) {
|
|
||||||
if (current.user.isProtected) {
|
|
||||||
setupNavigationManager.navigateTo(
|
|
||||||
SetupDestination.UserList(
|
|
||||||
current.server,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Restored
|
|
||||||
setupNavigationManager.navigateTo(
|
|
||||||
SetupDestination.AppContent(
|
|
||||||
current,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Did not restore
|
|
||||||
setupNavigationManager.navigateTo(SetupDestination.ServerList)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setupNavigationManager.navigateTo(SetupDestination.Loading)
|
|
||||||
backdropService.clearBackdrop()
|
|
||||||
val currentServerId = prefs.currentServerId?.toUUIDOrNull()
|
|
||||||
if (currentServerId != null) {
|
|
||||||
val currentServer =
|
|
||||||
serverRepository.serverDao.getServer(currentServerId)?.server
|
|
||||||
if (currentServer != null) {
|
|
||||||
setupNavigationManager.navigateTo(
|
|
||||||
SetupDestination.UserList(
|
|
||||||
currentServer,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
setupNavigationManager.navigateTo(SetupDestination.ServerList)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setupNavigationManager.navigateTo(SetupDestination.ServerList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Timber.e(ex, "Error during appStart")
|
Timber.e(ex, "Error during appStart")
|
||||||
setupNavigationManager.navigateTo(SetupDestination.ServerList)
|
setupNavigationManager.navigateTo(SetupDestination.ServerList)
|
||||||
|
|
@ -574,6 +480,103 @@ class MainActivityViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an [Intent], navigating if needed
|
||||||
|
*
|
||||||
|
* @return true if the intent was handled, false if normal app start should proceed
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
internal suspend fun handleIntent(intent: Intent?): Boolean {
|
||||||
|
val result = intent?.let { intentService.parseIntent(intent) }
|
||||||
|
when (result) {
|
||||||
|
is IntentResult.Error -> {
|
||||||
|
val current = serverRepository.current.value
|
||||||
|
val destination =
|
||||||
|
if (current != null) {
|
||||||
|
SetupDestination.UserList(current.server)
|
||||||
|
} else {
|
||||||
|
SetupDestination.ServerList
|
||||||
|
}
|
||||||
|
setupNavigationManager.navigateTo(destination)
|
||||||
|
Timber.e("Error parsing intent: %s", result.message)
|
||||||
|
showToast(context, result.message)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
is IntentResult.Target -> {
|
||||||
|
val current = serverRepository.current.value
|
||||||
|
if (current != null) {
|
||||||
|
Timber.i("Received valid intent, switching to AppContent")
|
||||||
|
|
||||||
|
if (result.addHomeToBackStack) {
|
||||||
|
navigationManager.reloadHome()
|
||||||
|
} else {
|
||||||
|
navigationManager.backStack.clear()
|
||||||
|
}
|
||||||
|
navigationManager.backStack.addAll(result.destinations)
|
||||||
|
|
||||||
|
setupNavigationManager.navigateTo(
|
||||||
|
SetupDestination.AppContent(current),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// This should never happen, but just reset app state if it does
|
||||||
|
setupNavigationManager.navigateTo(SetupDestination.ServerList)
|
||||||
|
Timber.e("Error parsing intent, no user is active")
|
||||||
|
showToast(context, "An error occurred parsing the intent")
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
IntentResult.NoOp,
|
||||||
|
null,
|
||||||
|
-> {
|
||||||
|
// Not handled
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleAppUpgrade() {
|
||||||
|
val needUpgrade = appUpgradeHandler.needUpgrade()
|
||||||
|
if (needUpgrade) {
|
||||||
|
showToast(
|
||||||
|
context,
|
||||||
|
context.getString(
|
||||||
|
R.string.updated_toast,
|
||||||
|
appUpgradeHandler.currentVersion.toString(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
appUpgradeHandler.run()
|
||||||
|
}
|
||||||
|
appUpgradeHandler.copySubfont(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle starting the app including auto sign in and navigating to the right starting page
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
internal suspend fun handleAppStart() {
|
||||||
|
val signInAutomatically = preferences.data.first().signInAutomatically
|
||||||
|
val currentUser = serverRepository.current.value
|
||||||
|
if (signInAutomatically && currentUser != null && !currentUser.user.isProtected) {
|
||||||
|
// Hot reload
|
||||||
|
Timber.v("Hot reload restoring state")
|
||||||
|
setupNavigationManager.navigateTo(
|
||||||
|
SetupDestination.AppContent(currentUser),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val restoredSession =
|
||||||
|
if (signInAutomatically) {
|
||||||
|
Timber.v("Sign in automatically")
|
||||||
|
serverRepository.restoreLastSession()
|
||||||
|
} else {
|
||||||
|
serverRepository.getMostRecentServer()
|
||||||
|
}
|
||||||
|
Timber.d("Restore last session result=%s", restoredSession)
|
||||||
|
setupNavigationManager.navigateTo(restoredSession.destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val Destination?.isPlayback: Boolean
|
private val Destination?.isPlayback: Boolean
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import androidx.savedstate.SavedStateRegistry
|
||||||
import androidx.savedstate.SavedStateRegistryController
|
import androidx.savedstate.SavedStateRegistryController
|
||||||
import androidx.savedstate.SavedStateRegistryOwner
|
import androidx.savedstate.SavedStateRegistryOwner
|
||||||
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||||
|
import com.github.damontecres.wholphin.data.RestoredSession
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.services.ScreensaverService
|
import com.github.damontecres.wholphin.services.ScreensaverService
|
||||||
|
|
@ -33,9 +34,7 @@ import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||||
import com.github.damontecres.wholphin.ui.util.ProvideLocalClock
|
import com.github.damontecres.wholphin.ui.util.ProvideLocalClock
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
@ -75,8 +74,10 @@ class WholphinDreamService :
|
||||||
lifecycleRegistry.currentState = Lifecycle.State.CREATED
|
lifecycleRegistry.currentState = Lifecycle.State.CREATED
|
||||||
lifecycleScope.launchDefault {
|
lifecycleScope.launchDefault {
|
||||||
if (serverRepository.current.value == null) {
|
if (serverRepository.current.value == null) {
|
||||||
val prefs = preferencesDataStore.data.first()
|
val result = serverRepository.restoreLastSession()
|
||||||
serverRepository.restoreSession(prefs.currentServerId.toUUIDOrNull(), prefs.currentUserId.toUUIDOrNull())
|
if (result !is RestoredSession.Success) {
|
||||||
|
Timber.w("Could not restore user session")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
package com.github.damontecres.wholphin.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import com.github.damontecres.wholphin.data.MostRecentServer.None
|
||||||
|
import com.github.damontecres.wholphin.data.MostRecentServer.Server
|
||||||
|
import com.github.damontecres.wholphin.data.MostRecentServer.ServerAndUser
|
||||||
|
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||||
|
import com.github.damontecres.wholphin.services.SetupDestination
|
||||||
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
|
import com.github.damontecres.wholphin.ui.toServerString
|
||||||
|
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the status of the most recently used server
|
||||||
|
*
|
||||||
|
* This can be read/written to shared preferences
|
||||||
|
*/
|
||||||
|
sealed interface MostRecentServer {
|
||||||
|
/**
|
||||||
|
* There is no recently used server
|
||||||
|
*/
|
||||||
|
data object None : MostRecentServer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The most recently used server without a recent user
|
||||||
|
*/
|
||||||
|
data class Server(
|
||||||
|
val serverId: UUID,
|
||||||
|
val serverUrl: String,
|
||||||
|
) : MostRecentServer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Most recently used server and user
|
||||||
|
*/
|
||||||
|
data class ServerAndUser(
|
||||||
|
val serverId: UUID,
|
||||||
|
val serverUrl: String,
|
||||||
|
val userId: UUID,
|
||||||
|
val accessToken: String,
|
||||||
|
) : MostRecentServer {
|
||||||
|
override fun toString(): String = "ServerAndUser(serverId=$serverId, serverUrl=$serverUrl, userId=$userId)"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(current: CurrentUser?): MostRecentServer =
|
||||||
|
if (current != null && current.user.accessToken.isNotNullOrBlank()) {
|
||||||
|
ServerAndUser(current.server.id, current.server.url, current.user.id, current.user.accessToken)
|
||||||
|
} else if (current != null) {
|
||||||
|
Server(current.server.id, current.server.url)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MostRecentServerProvider {
|
||||||
|
/**
|
||||||
|
* Get the [MostRecentServer] from the shared preferences in [context]
|
||||||
|
*/
|
||||||
|
fun get(): MostRecentServer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the [CurrentUser] as the most recent server into the shared preferences in [context]
|
||||||
|
*/
|
||||||
|
suspend fun save(current: CurrentUser)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the most recent server and user
|
||||||
|
*/
|
||||||
|
suspend fun clear()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLear the most recent user, leaving the server
|
||||||
|
*/
|
||||||
|
suspend fun clearUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [MostRecentServerProvider] that stores in [SharedPreferences]
|
||||||
|
*/
|
||||||
|
class MostRecentServerSharedPreferences(
|
||||||
|
context: Context,
|
||||||
|
) : MostRecentServerProvider {
|
||||||
|
private val prefs = getServerSharedPreferences(context)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the [MostRecentServer] from the shared preferences in [context]
|
||||||
|
*/
|
||||||
|
override fun get(): MostRecentServer {
|
||||||
|
val serverId = prefs.getString(SERVER_ID_KEY, null)?.toUUIDOrNull()
|
||||||
|
val serverUrl = prefs.getString(SERVER_URL_KEY, null)
|
||||||
|
val userId = prefs.getString(USER_ID_KEY, null)?.toUUIDOrNull()
|
||||||
|
val accessToken = prefs.getString(USER_TOKEN_KEY, null)
|
||||||
|
return if (serverId != null && serverUrl.isNotNullOrBlank() && userId != null && accessToken.isNotNullOrBlank()) {
|
||||||
|
ServerAndUser(serverId, serverUrl, userId, accessToken)
|
||||||
|
} else if (serverId != null && serverUrl.isNotNullOrBlank()) {
|
||||||
|
Server(serverId, serverUrl)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the [CurrentUser] as the most recent server into the shared preferences in [context]
|
||||||
|
*/
|
||||||
|
override suspend fun save(current: CurrentUser) {
|
||||||
|
prefs.edit(true) {
|
||||||
|
putString(SERVER_ID_KEY, current.server.id.toServerString())
|
||||||
|
putString(SERVER_URL_KEY, current.server.url)
|
||||||
|
putString(USER_ID_KEY, current.user.id.toServerString())
|
||||||
|
putString(USER_TOKEN_KEY, current.user.accessToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun clear() {
|
||||||
|
prefs.edit(true) {
|
||||||
|
remove(SERVER_ID_KEY)
|
||||||
|
remove(SERVER_URL_KEY)
|
||||||
|
remove(USER_ID_KEY)
|
||||||
|
remove(USER_TOKEN_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun clearUser() {
|
||||||
|
prefs.edit(true) {
|
||||||
|
remove(USER_ID_KEY)
|
||||||
|
remove(USER_TOKEN_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getServerSharedPreferences(context: Context): SharedPreferences =
|
||||||
|
context.getSharedPreferences(
|
||||||
|
"${context.packageName}_server",
|
||||||
|
Context.MODE_PRIVATE,
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SERVER_ID_KEY = "current.server.id"
|
||||||
|
private const val SERVER_URL_KEY = "current.server.url"
|
||||||
|
private const val USER_ID_KEY = "current.user.id"
|
||||||
|
private const val USER_TOKEN_KEY = "current.user.accessToken"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of restoring a session
|
||||||
|
*/
|
||||||
|
sealed interface RestoredSession {
|
||||||
|
val destination: SetupDestination
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session could not be restored
|
||||||
|
*/
|
||||||
|
data object None : RestoredSession {
|
||||||
|
override val destination: SetupDestination
|
||||||
|
get() = SetupDestination.ServerList
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only the server should be restored, so a user still need to be chosen
|
||||||
|
*/
|
||||||
|
data class ServerOnly(
|
||||||
|
val server: JellyfinServer,
|
||||||
|
) : RestoredSession {
|
||||||
|
override val destination: SetupDestination
|
||||||
|
get() = SetupDestination.UserList(server)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session was restored successfully
|
||||||
|
*/
|
||||||
|
data class Success(
|
||||||
|
val currentUser: CurrentUser,
|
||||||
|
) : RestoredSession {
|
||||||
|
override val destination: SetupDestination
|
||||||
|
get() = SetupDestination.AppContent(currentUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,10 @@
|
||||||
package com.github.damontecres.wholphin.data
|
package com.github.damontecres.wholphin.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.datastore.core.DataStore
|
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||||
import com.github.damontecres.wholphin.data.model.ServerUserConfig
|
import com.github.damontecres.wholphin.data.model.ServerUserConfig
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
|
||||||
import com.github.damontecres.wholphin.services.hilt.IoDispatcher
|
import com.github.damontecres.wholphin.services.hilt.IoDispatcher
|
||||||
import com.github.damontecres.wholphin.ui.toServerString
|
|
||||||
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
|
@ -49,8 +44,8 @@ class ServerRepository
|
||||||
val jellyfin: Jellyfin,
|
val jellyfin: Jellyfin,
|
||||||
val serverDao: JellyfinServerDao,
|
val serverDao: JellyfinServerDao,
|
||||||
val apiClient: ApiClient,
|
val apiClient: ApiClient,
|
||||||
val userPreferencesDataStore: DataStore<AppPreferences>,
|
|
||||||
@param:IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
@param:IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
|
private val mostRecentServerProvider: MostRecentServerProvider,
|
||||||
) {
|
) {
|
||||||
private var _current = MutableStateFlow<CurrentUser?>(null)
|
private var _current = MutableStateFlow<CurrentUser?>(null)
|
||||||
val current: StateFlow<CurrentUser?> = _current
|
val current: StateFlow<CurrentUser?> = _current
|
||||||
|
|
@ -88,10 +83,11 @@ class ServerRepository
|
||||||
}
|
}
|
||||||
apiClient.update(baseUrl = server.url, accessToken = null)
|
apiClient.update(baseUrl = server.url, accessToken = null)
|
||||||
_current.value = null
|
_current.value = null
|
||||||
|
mostRecentServerProvider.clearUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the server & User to the app database and updates the [ApiClient] to use this server & user
|
* Saves the server & user to the app database and updates the [ApiClient] to use this server & user
|
||||||
*/
|
*/
|
||||||
suspend fun changeUser(
|
suspend fun changeUser(
|
||||||
server: JellyfinServer,
|
server: JellyfinServer,
|
||||||
|
|
@ -119,70 +115,101 @@ class ServerRepository
|
||||||
)
|
)
|
||||||
serverDao.addOrUpdateServer(updatedServer)
|
serverDao.addOrUpdateServer(updatedServer)
|
||||||
updatedUser = serverDao.addOrUpdateUser(updatedUser)
|
updatedUser = serverDao.addOrUpdateUser(updatedUser)
|
||||||
userPreferencesDataStore.updateData {
|
|
||||||
it
|
|
||||||
.toBuilder()
|
|
||||||
.apply {
|
|
||||||
currentServerId = updatedServer.id.toServerString()
|
|
||||||
currentUserId = updatedUser.id.toServerString()
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
val currentUser = CurrentUser(updatedServer, updatedUser)
|
val currentUser = CurrentUser(updatedServer, updatedUser)
|
||||||
withContext(WholphinDispatchers.Main) {
|
mostRecentServerProvider.save(currentUser)
|
||||||
_current.value = currentUser
|
_current.value = currentUser
|
||||||
_currentUserDto.value = ServerUserConfig(userDto)
|
_currentUserDto.value = ServerUserConfig(userDto)
|
||||||
}
|
|
||||||
getServerSharedPreferences(context).edit(true) {
|
|
||||||
putString(SERVER_URL_KEY, updatedServer.url)
|
|
||||||
putString(ACCESS_TOKEN_KEY, updatedUser.accessToken)
|
|
||||||
}
|
|
||||||
return@withContext currentUser
|
return@withContext currentUser
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores a session for the given server & user such as when the app reopens
|
* Try to restore the most recent session
|
||||||
*
|
*
|
||||||
* If user has a PIN, this returns false
|
* Does not check if auto sign-in is enabled, but does check if the user profile is protected
|
||||||
*/
|
*/
|
||||||
suspend fun restoreSession(
|
suspend fun restoreLastSession(): RestoredSession {
|
||||||
|
val recentServer = mostRecentServerProvider.get()
|
||||||
|
Timber.d("restoreLastSession: recentServer=%s", recentServer)
|
||||||
|
val result =
|
||||||
|
when (recentServer) {
|
||||||
|
MostRecentServer.None -> {
|
||||||
|
RestoredSession.None
|
||||||
|
}
|
||||||
|
|
||||||
|
is MostRecentServer.Server -> {
|
||||||
|
val server = serverDao.getServer(recentServer.serverId)
|
||||||
|
if (server != null) {
|
||||||
|
RestoredSession.ServerOnly(server.server)
|
||||||
|
} else {
|
||||||
|
RestoredSession.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is MostRecentServer.ServerAndUser -> {
|
||||||
|
val currentUser = tryChangeUser(recentServer.serverId, recentServer.userId)
|
||||||
|
if (currentUser != null) {
|
||||||
|
if (currentUser.user.isProtected) {
|
||||||
|
RestoredSession.ServerOnly(currentUser.server)
|
||||||
|
} else {
|
||||||
|
RestoredSession.Success(currentUser)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getMostRecentServerInternal(recentServer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the most recently used server, if any
|
||||||
|
*/
|
||||||
|
suspend fun getMostRecentServer(): RestoredSession = getMostRecentServerInternal(mostRecentServerProvider.get())
|
||||||
|
|
||||||
|
suspend fun getMostRecentServerInternal(recentServer: MostRecentServer): RestoredSession {
|
||||||
|
val recentServer = recentServer ?: mostRecentServerProvider.get()
|
||||||
|
val serverId =
|
||||||
|
when (recentServer) {
|
||||||
|
MostRecentServer.None -> null
|
||||||
|
is MostRecentServer.Server -> recentServer.serverId
|
||||||
|
is MostRecentServer.ServerAndUser -> recentServer.serverId
|
||||||
|
}
|
||||||
|
val server = serverId?.let { serverDao.getServer(serverId)?.server }
|
||||||
|
return if (server != null) {
|
||||||
|
RestoredSession.ServerOnly(server)
|
||||||
|
} else {
|
||||||
|
RestoredSession.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to change to the given server & user. If the user's profile is protected, returns null
|
||||||
|
*
|
||||||
|
* @return the server/user or null if the session could not be restored
|
||||||
|
*/
|
||||||
|
suspend fun tryChangeUser(
|
||||||
serverId: UUID?,
|
serverId: UUID?,
|
||||||
userId: UUID?,
|
userId: UUID?,
|
||||||
): CurrentUser? {
|
): CurrentUser? =
|
||||||
if (serverId == null || userId == null) {
|
withContext(ioDispatcher) {
|
||||||
_current.value = null
|
return@withContext if (serverId == null || userId == null) {
|
||||||
return null
|
_current.value = null
|
||||||
}
|
null
|
||||||
val serverAndUsers =
|
|
||||||
withContext(ioDispatcher) {
|
|
||||||
serverDao.getServer(serverId)
|
|
||||||
}
|
|
||||||
if (serverAndUsers != null) {
|
|
||||||
val current = _current.value
|
|
||||||
if (current != null && current.server.id == serverId && current.user.id == userId) {
|
|
||||||
Timber.v("Restoring session for current user, so shortcut")
|
|
||||||
apiClient.update(
|
|
||||||
baseUrl = current.server.url,
|
|
||||||
accessToken = current.user.accessToken,
|
|
||||||
)
|
|
||||||
return current
|
|
||||||
} else {
|
} else {
|
||||||
val user = serverAndUsers.users.firstOrNull { it.id == userId }
|
val serverAndUsers = serverDao.getServer(serverId)
|
||||||
if (user != null) {
|
if (serverAndUsers != null) {
|
||||||
return changeUser(serverAndUsers.server, user)
|
val user = serverAndUsers.users.firstOrNull { it.id == userId }
|
||||||
|
if (user != null && !user.isProtected) {
|
||||||
|
changeUser(serverAndUsers.server, user)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun fetchLastUsedServer(serverId: UUID?): JellyfinServer? =
|
|
||||||
withContext(ioDispatcher) {
|
|
||||||
serverId?.let { serverDao.getServer(serverId)?.server }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun closeSession() {
|
|
||||||
_current.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a successful [AuthenticationResult], switch to the user that just authenticated
|
* Given a successful [AuthenticationResult], switch to the user that just authenticated
|
||||||
|
|
@ -244,13 +271,7 @@ class ServerRepository
|
||||||
withContext(WholphinDispatchers.Main) {
|
withContext(WholphinDispatchers.Main) {
|
||||||
_current.value = null
|
_current.value = null
|
||||||
}
|
}
|
||||||
userPreferencesDataStore.updateData {
|
mostRecentServerProvider.clearUser()
|
||||||
it
|
|
||||||
.toBuilder()
|
|
||||||
.apply {
|
|
||||||
currentUserId = ""
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
apiClient.update(accessToken = null)
|
apiClient.update(accessToken = null)
|
||||||
}
|
}
|
||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
|
|
@ -263,14 +284,7 @@ class ServerRepository
|
||||||
withContext(WholphinDispatchers.Main) {
|
withContext(WholphinDispatchers.Main) {
|
||||||
_current.value = null
|
_current.value = null
|
||||||
}
|
}
|
||||||
userPreferencesDataStore.updateData {
|
mostRecentServerProvider.clear()
|
||||||
it
|
|
||||||
.toBuilder()
|
|
||||||
.apply {
|
|
||||||
currentServerId = ""
|
|
||||||
currentUserId = ""
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
apiClient.update(baseUrl = null, accessToken = null)
|
apiClient.update(baseUrl = null, accessToken = null)
|
||||||
}
|
}
|
||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
|
|
@ -279,14 +293,7 @@ class ServerRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun switchServerOrUser() {
|
suspend fun switchServerOrUser() {
|
||||||
userPreferencesDataStore.updateData {
|
mostRecentServerProvider.clear()
|
||||||
it
|
|
||||||
.toBuilder()
|
|
||||||
.apply {
|
|
||||||
currentServerId = ""
|
|
||||||
currentUserId = ""
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateUserAuth(
|
suspend fun updateUserAuth(
|
||||||
|
|
@ -335,17 +342,6 @@ class ServerRepository
|
||||||
if (currentUser?.id == userDto.id) ServerUserConfig(userDto) else it
|
if (currentUser?.id == userDto.id) ServerUserConfig(userDto) else it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun getServerSharedPreferences(context: Context): SharedPreferences =
|
|
||||||
context.getSharedPreferences(
|
|
||||||
"${context.packageName}_server",
|
|
||||||
Context.MODE_PRIVATE,
|
|
||||||
)
|
|
||||||
|
|
||||||
const val SERVER_URL_KEY = "current.server"
|
|
||||||
const val ACCESS_TOKEN_KEY = "current.accessToken"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@ import android.os.Build
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.github.damontecres.wholphin.data.CurrentUser
|
||||||
import com.github.damontecres.wholphin.data.JellyfinServerDao
|
import com.github.damontecres.wholphin.data.JellyfinServerDao
|
||||||
|
import com.github.damontecres.wholphin.data.MostRecentServerProvider
|
||||||
import com.github.damontecres.wholphin.data.RememberedTabDao
|
import com.github.damontecres.wholphin.data.RememberedTabDao
|
||||||
import com.github.damontecres.wholphin.data.SeerrServerDao
|
import com.github.damontecres.wholphin.data.SeerrServerDao
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||||
|
|
@ -36,6 +38,7 @@ import com.github.damontecres.wholphin.ui.setup.seerr.migrateSeerrUrl
|
||||||
import com.github.damontecres.wholphin.util.Version
|
import com.github.damontecres.wholphin.util.Version
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -56,6 +59,7 @@ class AppUpgradeHandler
|
||||||
private val seerrServerDao: SeerrServerDao,
|
private val seerrServerDao: SeerrServerDao,
|
||||||
private val serverDao: JellyfinServerDao,
|
private val serverDao: JellyfinServerDao,
|
||||||
private val rememberedTabDao: RememberedTabDao,
|
private val rememberedTabDao: RememberedTabDao,
|
||||||
|
private val mostRecentServerProvider: MostRecentServerProvider,
|
||||||
) {
|
) {
|
||||||
val pkgInfo: PackageInfo get() = context.packageManager.getPackageInfo(context.packageName, 0)
|
val pkgInfo: PackageInfo get() = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||||
val currentVersion: Version get() = Version.fromString(pkgInfo.versionName!!)
|
val currentVersion: Version get() = Version.fromString(pkgInfo.versionName!!)
|
||||||
|
|
@ -442,5 +446,29 @@ class AppUpgradeHandler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (current.isEqualOrBefore(Version.fromString("1.0.8-0-g0"))) {
|
||||||
|
try {
|
||||||
|
val prefs = appPreferences.data.firstOrNull()
|
||||||
|
val serverId = prefs?.currentServerId?.toUUIDOrNull()
|
||||||
|
val userId = prefs?.currentUserId?.toUUIDOrNull()
|
||||||
|
if (serverId != null && userId != null) {
|
||||||
|
val server = serverDao.getServer(serverId)
|
||||||
|
val user = server?.users?.firstOrNull { it.id == userId }
|
||||||
|
if (server != null && user != null) {
|
||||||
|
Timber.i("Migrating most recent server")
|
||||||
|
mostRecentServerProvider.save(CurrentUser(server.server, user))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(ex, "Error during migration for most recent server")
|
||||||
|
}
|
||||||
|
appPreferences.updateData {
|
||||||
|
it.update {
|
||||||
|
currentServerId = ""
|
||||||
|
currentUserId = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.services
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
|
@ -188,9 +189,8 @@ class HomeSettingsService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes [HomePageSettings] from a [JsonElement] skipping any unknown/unparsable rows
|
* Decodes [HomePageSettings] from a [JsonElement] skipping any unknown/unparsable rows
|
||||||
*
|
|
||||||
* This is public only for testing
|
|
||||||
*/
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
fun decode(element: JsonElement): HomePageSettings {
|
fun decode(element: JsonElement): HomePageSettings {
|
||||||
val version = element.jsonObject["version"]?.jsonPrimitive?.intOrNull
|
val version = element.jsonObject["version"]?.jsonPrimitive?.intOrNull
|
||||||
if (version == null || version > SUPPORTED_HOME_PAGE_SETTINGS_VERSION) {
|
if (version == null || version > SUPPORTED_HOME_PAGE_SETTINGS_VERSION) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package com.github.damontecres.wholphin.services
|
||||||
|
|
||||||
import android.app.SearchManager
|
import android.app.SearchManager
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import com.github.damontecres.wholphin.data.RestoredSession
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
|
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
|
||||||
|
|
@ -114,6 +116,7 @@ class IntentService
|
||||||
return IntentResult.Target(destinations, true)
|
return IntentResult.Target(destinations, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
internal suspend fun prepare(intent: Intent): IntentResult? {
|
internal suspend fun prepare(intent: Intent): IntentResult? {
|
||||||
val appPrefs = userPreferencesService.flow.first().appPreferences
|
val appPrefs = userPreferencesService.flow.first().appPreferences
|
||||||
|
|
||||||
|
|
@ -121,39 +124,33 @@ class IntentService
|
||||||
val serverId = intent.getStringParam(INTENT_SERVER_ID)?.toUUIDOrNull()
|
val serverId = intent.getStringParam(INTENT_SERVER_ID)?.toUUIDOrNull()
|
||||||
return if (userId != null && serverId != null) {
|
return if (userId != null && serverId != null) {
|
||||||
Timber.v("Intent switches user")
|
Timber.v("Intent switches user")
|
||||||
val user = serverRepository.serverDao.getUser(serverId, userId)
|
val user = serverRepository.tryChangeUser(serverId, userId)
|
||||||
if (user != null && !user.isProtected) {
|
if (user != null) {
|
||||||
serverRepository.restoreSession(serverId, userId)
|
|
||||||
?: IntentResult.Error("Error restoring user")
|
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
IntentResult.Error("Cannot switch to specified user")
|
IntentResult.Error("Error restoring user")
|
||||||
}
|
}
|
||||||
} else {
|
} else if (appPrefs.signInAutomatically) {
|
||||||
val profileProtected =
|
Timber.v("No current user, so restoring last")
|
||||||
serverRepository.current.value
|
val result = serverRepository.restoreLastSession()
|
||||||
?.user
|
Timber.v("Restored result=%s", result)
|
||||||
?.isProtected == true
|
when (result) {
|
||||||
if (appPrefs.signInAutomatically && !profileProtected) {
|
RestoredSession.None,
|
||||||
Timber.v("No current user, so restoring last")
|
is RestoredSession.ServerOnly,
|
||||||
val userId = appPrefs.currentUserId?.toUUIDOrNull()
|
-> {
|
||||||
val serverId = appPrefs.currentServerId?.toUUIDOrNull()
|
IntentResult.Error("Could not auto sign-in, specify a server & user")
|
||||||
|
}
|
||||||
|
|
||||||
if (userId != null && serverId != null) {
|
is RestoredSession.Success -> {
|
||||||
val current =
|
if (result.currentUser.user.isProtected) {
|
||||||
serverRepository.restoreSession(serverId, userId)
|
|
||||||
?: return IntentResult.Error("Error restoring user")
|
|
||||||
if (current.user.isProtected) {
|
|
||||||
IntentResult.Error("Could not auto sign-in, user is protected")
|
IntentResult.Error("Could not auto sign-in, user is protected")
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
IntentResult.Error("Could not auto sign-in, specify a server & user")
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
IntentResult.Error("Auto sign-in not enabled or user is protected")
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
IntentResult.Error("Auto sign-in not enabled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.github.damontecres.wholphin.services
|
package com.github.damontecres.wholphin.services
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.hilt.work.HiltWorker
|
import androidx.hilt.work.HiltWorker
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
|
@ -13,6 +14,7 @@ import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import androidx.work.workDataOf
|
import androidx.work.workDataOf
|
||||||
|
import com.github.damontecres.wholphin.data.RestoredSession
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.ui.collectLatestIn
|
import com.github.damontecres.wholphin.ui.collectLatestIn
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
|
@ -52,12 +54,8 @@ class LatestNextUpWorker
|
||||||
try {
|
try {
|
||||||
if (api.baseUrl.isNullOrBlank() || api.accessToken.isNullOrBlank()) {
|
if (api.baseUrl.isNullOrBlank() || api.accessToken.isNullOrBlank()) {
|
||||||
// Not active
|
// Not active
|
||||||
var currentUser = serverRepository.current.value
|
val result = serverRepository.restoreLastSession()
|
||||||
if (currentUser == null) {
|
if (result !is RestoredSession.Success) {
|
||||||
serverRepository.restoreSession(serverId, userId)
|
|
||||||
currentUser = serverRepository.current.value
|
|
||||||
}
|
|
||||||
if (currentUser == null) {
|
|
||||||
Timber.w("No user found during run")
|
Timber.w("No user found during run")
|
||||||
return Result.failure()
|
return Result.failure()
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +89,7 @@ class LatestNextUpSchedulerService
|
||||||
"SuggestionsSchedulerService requires an AppCompatActivity context, but received: ${context::class.java.name}",
|
"SuggestionsSchedulerService requires an AppCompatActivity context, but received: ${context::class.java.name}",
|
||||||
)
|
)
|
||||||
|
|
||||||
// Exposed for testing
|
@VisibleForTesting
|
||||||
internal var dispatcher: CoroutineDispatcher = WholphinDispatchers.IO
|
internal var dispatcher: CoroutineDispatcher = WholphinDispatchers.IO
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import androidx.datastore.core.DataStore
|
||||||
import androidx.hilt.work.HiltWorker
|
import androidx.hilt.work.HiltWorker
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
import com.github.damontecres.wholphin.data.RestoredSession
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
|
|
@ -63,12 +64,8 @@ class SuggestionsWorker
|
||||||
}
|
}
|
||||||
|
|
||||||
if (api.baseUrl.isNullOrBlank() || api.accessToken.isNullOrBlank()) {
|
if (api.baseUrl.isNullOrBlank() || api.accessToken.isNullOrBlank()) {
|
||||||
var currentUser = serverRepository.current.value
|
val result = serverRepository.restoreLastSession()
|
||||||
if (currentUser == null) {
|
if (result !is RestoredSession.Success) {
|
||||||
serverRepository.restoreSession(serverId, userId)
|
|
||||||
currentUser = serverRepository.current.value
|
|
||||||
}
|
|
||||||
if (currentUser == null) {
|
|
||||||
Timber.w("No user found during run")
|
Timber.w("No user found during run")
|
||||||
return Result.failure()
|
return Result.failure()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import android.content.Context
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.github.damontecres.wholphin.BuildConfig
|
import com.github.damontecres.wholphin.BuildConfig
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.data.MostRecentServerProvider
|
||||||
|
import com.github.damontecres.wholphin.data.MostRecentServerSharedPreferences
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.services.SeerrApi
|
import com.github.damontecres.wholphin.services.SeerrApi
|
||||||
import com.github.damontecres.wholphin.util.CoroutineContextApiClientFactory
|
import com.github.damontecres.wholphin.util.CoroutineContextApiClientFactory
|
||||||
|
|
@ -188,6 +190,12 @@ object AppModule {
|
||||||
fun seerrApi(
|
fun seerrApi(
|
||||||
@StandardOkHttpClient okHttpClient: OkHttpClient,
|
@StandardOkHttpClient okHttpClient: OkHttpClient,
|
||||||
) = SeerrApi(okHttpClient)
|
) = SeerrApi(okHttpClient)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun mostRecentServerProvider(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
): MostRecentServerProvider = MostRecentServerSharedPreferences(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.github.damontecres.wholphin.MainActivity
|
import com.github.damontecres.wholphin.MainActivity
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.data.RestoredSession
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
|
|
@ -70,12 +71,8 @@ class TvProviderWorker
|
||||||
|
|
||||||
if (api.baseUrl.isNullOrBlank() || api.accessToken.isNullOrBlank()) {
|
if (api.baseUrl.isNullOrBlank() || api.accessToken.isNullOrBlank()) {
|
||||||
// Not active
|
// Not active
|
||||||
var currentUser = serverRepository.current.value
|
val result = serverRepository.restoreLastSession()
|
||||||
if (currentUser == null) {
|
if (result !is RestoredSession.Success) {
|
||||||
serverRepository.restoreSession(serverId, userId)
|
|
||||||
currentUser = serverRepository.current.value
|
|
||||||
}
|
|
||||||
if (currentUser == null) {
|
|
||||||
Timber.w("No user found during run")
|
Timber.w("No user found during run")
|
||||||
return Result.failure()
|
return Result.failure()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import android.media.MediaCodecList
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.compose.ui.unit.Density
|
import androidx.compose.ui.unit.Density
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
@ -198,7 +199,7 @@ class PlaybackViewModel
|
||||||
|
|
||||||
val currentUserDto = serverRepository.currentUserDtoFlow
|
val currentUserDto = serverRepository.currentUserDtoFlow
|
||||||
|
|
||||||
// Exposed for testing
|
@VisibleForTesting
|
||||||
val initJob: Job
|
val initJob: Job
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
@ -1110,7 +1111,7 @@ class PlaybackViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
// Variables for tracking segment state
|
// Variables for tracking segment state
|
||||||
// Exposed for testing
|
@VisibleForTesting
|
||||||
internal var segmentJob: Job? = null
|
internal var segmentJob: Job? = null
|
||||||
private val autoSkippedSegments = mutableSetOf<UUID>()
|
private val autoSkippedSegments = mutableSetOf<UUID>()
|
||||||
private val outroShownSegments = mutableSetOf<UUID>()
|
private val outroShownSegments = mutableSetOf<UUID>()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package com.github.damontecres.wholphin.util
|
package com.github.damontecres.wholphin.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.MostRecentServer
|
||||||
import com.github.damontecres.wholphin.services.hilt.AppModule
|
import com.github.damontecres.wholphin.services.hilt.AppModule
|
||||||
import com.github.damontecres.wholphin.services.hilt.DeviceModule
|
import com.github.damontecres.wholphin.services.hilt.DeviceModule
|
||||||
import com.google.auto.service.AutoService
|
import com.google.auto.service.AutoService
|
||||||
|
|
@ -13,6 +13,7 @@ import org.acra.sender.ReportSender
|
||||||
import org.acra.sender.ReportSenderException
|
import org.acra.sender.ReportSenderException
|
||||||
import org.acra.sender.ReportSenderFactory
|
import org.acra.sender.ReportSenderFactory
|
||||||
import org.jellyfin.sdk.Jellyfin
|
import org.jellyfin.sdk.Jellyfin
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.clientLogApi
|
import org.jellyfin.sdk.api.client.extensions.clientLogApi
|
||||||
import org.jellyfin.sdk.api.okhttp.OkHttpFactory
|
import org.jellyfin.sdk.api.okhttp.OkHttpFactory
|
||||||
import org.jellyfin.sdk.createJellyfin
|
import org.jellyfin.sdk.createJellyfin
|
||||||
|
|
@ -39,26 +40,10 @@ class CrashReportSender : ReportSender {
|
||||||
errorContent: CrashReportData,
|
errorContent: CrashReportData,
|
||||||
) {
|
) {
|
||||||
Timber.v("Attempting to send crash report")
|
Timber.v("Attempting to send crash report")
|
||||||
val prefs = ServerRepository.getServerSharedPreferences(context)
|
try {
|
||||||
val serverUrl = prefs.getString(ServerRepository.SERVER_URL_KEY, null)
|
val recentServer = AppModule.mostRecentServerProvider(context).get()
|
||||||
val accessToken = prefs.getString(ServerRepository.ACCESS_TOKEN_KEY, null)
|
val api = recentServer.createApi(context)
|
||||||
if (serverUrl != null && accessToken != null) {
|
if (api != null) {
|
||||||
try {
|
|
||||||
val okHttpClient =
|
|
||||||
OkHttpClient
|
|
||||||
.Builder()
|
|
||||||
.build()
|
|
||||||
val okHttpFactory = OkHttpFactory(okHttpClient)
|
|
||||||
val api =
|
|
||||||
createJellyfin {
|
|
||||||
this.context = context
|
|
||||||
clientInfo = AppModule.clientInfo(context)
|
|
||||||
deviceInfo = DeviceModule.deviceInfo(context)
|
|
||||||
apiClientFactory = okHttpFactory
|
|
||||||
socketConnectionFactory = okHttpFactory
|
|
||||||
minimumServerVersion = Jellyfin.minimumVersion
|
|
||||||
}.createApi(baseUrl = serverUrl, accessToken = accessToken)
|
|
||||||
|
|
||||||
val obj = JSONObject()
|
val obj = JSONObject()
|
||||||
for ((key, value) in errorContent.toMap()) {
|
for ((key, value) in errorContent.toMap()) {
|
||||||
obj.put(key, value)
|
obj.put(key, value)
|
||||||
|
|
@ -75,13 +60,32 @@ class CrashReportSender : ReportSender {
|
||||||
|
|
||||||
""".trimIndent() + jsonStr,
|
""".trimIndent() + jsonStr,
|
||||||
).content.fileName
|
).content.fileName
|
||||||
Timber.i("Sent report to $serverUrl, filename=$filename")
|
Timber.i("Sent report to %s, filename=%s", api.baseUrl, filename)
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
} else {
|
||||||
throw ReportSenderException("Exception while sending crash report", ex)
|
throw ReportSenderException("Could not find valid server and/or credentials to use")
|
||||||
}
|
}
|
||||||
} else {
|
} catch (ex: Exception) {
|
||||||
throw ReportSenderException("Could not find valid server and/or credentials to use")
|
throw ReportSenderException("Exception while sending crash report", ex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun MostRecentServer.createApi(context: Context): ApiClient? =
|
||||||
|
(this as? MostRecentServer.ServerAndUser)?.let {
|
||||||
|
val okHttpClient =
|
||||||
|
OkHttpClient
|
||||||
|
.Builder()
|
||||||
|
.build()
|
||||||
|
val okHttpFactory = OkHttpFactory(okHttpClient)
|
||||||
|
val api =
|
||||||
|
createJellyfin {
|
||||||
|
this.context = context
|
||||||
|
clientInfo = AppModule.clientInfo(context)
|
||||||
|
deviceInfo = DeviceModule.deviceInfo(context)
|
||||||
|
apiClientFactory = okHttpFactory
|
||||||
|
socketConnectionFactory = okHttpFactory
|
||||||
|
minimumServerVersion = Jellyfin.minimumVersion
|
||||||
|
}.createApi(baseUrl = serverUrl, accessToken = accessToken)
|
||||||
|
api
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -233,9 +233,8 @@ message ExperimentalPreferences {
|
||||||
}
|
}
|
||||||
|
|
||||||
message AppPreferences {
|
message AppPreferences {
|
||||||
// The currently signed in server and user IDs, mostly for restoring a session
|
string current_server_id = 1 [deprecated = true];
|
||||||
string current_server_id = 1;
|
string current_user_id = 2 [deprecated = true];
|
||||||
string current_user_id = 2;
|
|
||||||
|
|
||||||
PlaybackPreferences playback_preferences = 3;
|
PlaybackPreferences playback_preferences = 3;
|
||||||
HomePagePreferences home_page_preferences = 4;
|
HomePagePreferences home_page_preferences = 4;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
package com.github.damontecres.wholphin.test
|
package com.github.damontecres.wholphin
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import com.github.damontecres.wholphin.MainActivityViewModel
|
|
||||||
import com.github.damontecres.wholphin.data.CurrentUser
|
import com.github.damontecres.wholphin.data.CurrentUser
|
||||||
import com.github.damontecres.wholphin.data.JellyfinServerDao
|
import com.github.damontecres.wholphin.data.RestoredSession
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinServerUsers
|
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.update
|
import com.github.damontecres.wholphin.preferences.update
|
||||||
import com.github.damontecres.wholphin.services.AppUpgradeHandler
|
import com.github.damontecres.wholphin.services.AppUpgradeHandler
|
||||||
|
|
@ -19,8 +17,10 @@ import com.github.damontecres.wholphin.services.IntentService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.SetupDestination
|
import com.github.damontecres.wholphin.services.SetupDestination
|
||||||
import com.github.damontecres.wholphin.services.SetupNavigationManager
|
import com.github.damontecres.wholphin.services.SetupNavigationManager
|
||||||
|
import com.github.damontecres.wholphin.test.currentUser
|
||||||
|
import com.github.damontecres.wholphin.test.server
|
||||||
|
import com.github.damontecres.wholphin.test.user
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.toServerString
|
|
||||||
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
||||||
import com.github.damontecres.wholphin.util.configure
|
import com.github.damontecres.wholphin.util.configure
|
||||||
import com.github.damontecres.wholphin.util.reset
|
import com.github.damontecres.wholphin.util.reset
|
||||||
|
|
@ -36,11 +36,11 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.jellyfin.sdk.model.UUID
|
import org.jellyfin.sdk.model.UUID
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -58,7 +58,6 @@ class TestMainActivityViewModel {
|
||||||
private val backdropService: BackdropService = mockk(relaxed = true)
|
private val backdropService: BackdropService = mockk(relaxed = true)
|
||||||
private val appUpgradeHandler: AppUpgradeHandler = mockk()
|
private val appUpgradeHandler: AppUpgradeHandler = mockk()
|
||||||
private val intentService: IntentService = mockk()
|
private val intentService: IntentService = mockk()
|
||||||
private val serverDao: JellyfinServerDao = mockk()
|
|
||||||
|
|
||||||
val viewModel =
|
val viewModel =
|
||||||
MainActivityViewModel(
|
MainActivityViewModel(
|
||||||
|
|
@ -74,6 +73,7 @@ class TestMainActivityViewModel {
|
||||||
)
|
)
|
||||||
|
|
||||||
private val serverId = UUID.randomUUID()
|
private val serverId = UUID.randomUUID()
|
||||||
|
private val server = server(serverId)
|
||||||
private val userId = UUID.randomUUID()
|
private val userId = UUID.randomUUID()
|
||||||
private val currentUser = currentUser(serverId, userId)
|
private val currentUser = currentUser(serverId, userId)
|
||||||
|
|
||||||
|
|
@ -88,13 +88,6 @@ class TestMainActivityViewModel {
|
||||||
WholphinDispatchers.configure(testDispatcher)
|
WholphinDispatchers.configure(testDispatcher)
|
||||||
every { appUpgradeHandler.needUpgrade() } returns false
|
every { appUpgradeHandler.needUpgrade() } returns false
|
||||||
every { appUpgradeHandler.copySubfont(any()) } returns Unit
|
every { appUpgradeHandler.copySubfont(any()) } returns Unit
|
||||||
every { serverRepository.serverDao } returns serverDao
|
|
||||||
coEvery { serverDao.getServer(serverId) } returns
|
|
||||||
JellyfinServerUsers(
|
|
||||||
currentUser.server,
|
|
||||||
// User list is unused
|
|
||||||
emptyList(),
|
|
||||||
)
|
|
||||||
mockkStatic(Toast::class)
|
mockkStatic(Toast::class)
|
||||||
val mockToast = mockk<Toast>()
|
val mockToast = mockk<Toast>()
|
||||||
every { Toast.makeText(any(), any<CharSequence>(), any()) } returns mockToast
|
every { Toast.makeText(any(), any<CharSequence>(), any()) } returns mockToast
|
||||||
|
|
@ -121,22 +114,26 @@ class TestMainActivityViewModel {
|
||||||
runTest(testDispatcher) {
|
runTest(testDispatcher) {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = true
|
signInAutomatically = true
|
||||||
currentServerId = serverId.toServerString()
|
|
||||||
currentUserId = userId.toServerString()
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow(currentUser)
|
every { serverRepository.current } returns MutableStateFlow(currentUser)
|
||||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
// coEvery { serverRepository.tryRestoreSession(serverId, userId) } returns currentUser
|
||||||
|
// coEvery { serverRepository.restoreLastSession() } returns
|
||||||
|
// RestoredSession.Success(currentUser)
|
||||||
|
|
||||||
viewModel.appStart(null)
|
viewModel.handleAppStart()
|
||||||
advanceUntilIdle()
|
|
||||||
|
|
||||||
val slot = slot<SetupDestination>()
|
val slot = slot<SetupDestination>()
|
||||||
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
||||||
slot.captured.let { destination ->
|
slot.captured.let { destination ->
|
||||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.AppContent)
|
Assert.assertTrue(
|
||||||
|
"destination is ${destination::class}",
|
||||||
|
destination is SetupDestination.AppContent,
|
||||||
|
)
|
||||||
destination as SetupDestination.AppContent
|
destination as SetupDestination.AppContent
|
||||||
assertEquals(currentUser, destination.current)
|
Assert.assertEquals(currentUser, destination.current)
|
||||||
}
|
}
|
||||||
|
coVerify(exactly = 0) { serverRepository.restoreLastSession() }
|
||||||
|
coVerify(exactly = 0) { serverRepository.getMostRecentServer() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -144,27 +141,27 @@ class TestMainActivityViewModel {
|
||||||
runTest {
|
runTest {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = false
|
signInAutomatically = false
|
||||||
currentServerId = serverId.toServerString()
|
|
||||||
currentUserId = userId.toServerString()
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
|
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
|
||||||
// coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
// coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
||||||
|
coEvery { serverRepository.getMostRecentServer() } returns
|
||||||
|
RestoredSession.ServerOnly(server)
|
||||||
|
|
||||||
viewModel.appStart(null)
|
viewModel.handleAppStart()
|
||||||
advanceUntilIdle()
|
|
||||||
|
|
||||||
verify(exactly = 1) { serverDao.getServer(serverId) }
|
coVerify(exactly = 0) { serverRepository.tryChangeUser(any(), any()) }
|
||||||
coVerify(exactly = 0) { serverRepository.restoreSession(any(), any()) }
|
coVerify(exactly = 0) { serverRepository.restoreLastSession() }
|
||||||
|
coVerify(exactly = 1) { serverRepository.getMostRecentServer() }
|
||||||
val args = mutableListOf<SetupDestination>()
|
val args = mutableListOf<SetupDestination>()
|
||||||
verify { setupNavigationManager.navigateTo(capture(args)) }
|
verify { setupNavigationManager.navigateTo(capture(args)) }
|
||||||
assertEquals(2, args.size)
|
Assert.assertEquals(1, args.size)
|
||||||
args[0].let { destination ->
|
args[0].let { destination ->
|
||||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.Loading)
|
Assert.assertTrue(
|
||||||
}
|
"destination is ${destination::class}",
|
||||||
args[1].let { destination ->
|
destination is SetupDestination.UserList,
|
||||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.UserList)
|
)
|
||||||
destination as SetupDestination.UserList
|
destination as SetupDestination.UserList
|
||||||
assertEquals(currentUser.server, destination.server)
|
Assert.assertEquals(currentUser.server, destination.server)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,27 +170,30 @@ class TestMainActivityViewModel {
|
||||||
runTest {
|
runTest {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = true
|
signInAutomatically = true
|
||||||
currentServerId = serverId.toServerString()
|
|
||||||
currentUserId = userId.toServerString()
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(protectedCurrentUser)
|
every { serverRepository.current } returns
|
||||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
|
MutableStateFlow<CurrentUser?>(
|
||||||
|
protectedCurrentUser,
|
||||||
|
)
|
||||||
|
coEvery {
|
||||||
|
serverRepository.tryChangeUser(serverId, userId)
|
||||||
|
} returns null
|
||||||
|
coEvery { serverRepository.restoreLastSession() } returns
|
||||||
|
RestoredSession.ServerOnly(server)
|
||||||
|
|
||||||
viewModel.appStart(null)
|
viewModel.handleAppStart()
|
||||||
advanceUntilIdle()
|
|
||||||
|
|
||||||
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
|
coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) }
|
||||||
verify { serverDao.getServer(serverId) }
|
|
||||||
val args = mutableListOf<SetupDestination>()
|
val args = mutableListOf<SetupDestination>()
|
||||||
verify { setupNavigationManager.navigateTo(capture(args)) }
|
verify { setupNavigationManager.navigateTo(capture(args)) }
|
||||||
assertEquals(2, args.size)
|
Assert.assertEquals(1, args.size)
|
||||||
args[0].let { destination ->
|
args[0].let { destination ->
|
||||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.Loading)
|
Assert.assertTrue(
|
||||||
}
|
"destination is ${destination::class}",
|
||||||
args[1].let { destination ->
|
destination is SetupDestination.UserList,
|
||||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.UserList)
|
)
|
||||||
destination as SetupDestination.UserList
|
destination as SetupDestination.UserList
|
||||||
assertEquals(currentUser.server, destination.server)
|
Assert.assertEquals(currentUser.server, destination.server)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,22 +202,26 @@ class TestMainActivityViewModel {
|
||||||
runTest {
|
runTest {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = true
|
signInAutomatically = true
|
||||||
currentServerId = serverId.toServerString()
|
|
||||||
currentUserId = userId.toServerString()
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
||||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
|
coEvery { serverRepository.restoreLastSession() } returns
|
||||||
|
RestoredSession.ServerOnly(server)
|
||||||
|
coEvery {
|
||||||
|
serverRepository.tryChangeUser(serverId, userId)
|
||||||
|
} returns null
|
||||||
|
|
||||||
viewModel.appStart(null)
|
viewModel.handleAppStart()
|
||||||
advanceUntilIdle()
|
|
||||||
|
|
||||||
coVerify(exactly = 1) { serverRepository.restoreSession(serverId, userId) }
|
coVerify(exactly = 1) { serverRepository.restoreLastSession() }
|
||||||
val slot = slot<SetupDestination>()
|
val slot = slot<SetupDestination>()
|
||||||
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
||||||
slot.captured.let { destination ->
|
slot.captured.let { destination ->
|
||||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.UserList)
|
Assert.assertTrue(
|
||||||
|
"destination is ${destination::class}",
|
||||||
|
destination is SetupDestination.UserList,
|
||||||
|
)
|
||||||
destination as SetupDestination.UserList
|
destination as SetupDestination.UserList
|
||||||
assertEquals(currentUser.server, destination.server)
|
Assert.assertEquals(currentUser.server, destination.server)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,45 +230,49 @@ class TestMainActivityViewModel {
|
||||||
runTest {
|
runTest {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = true
|
signInAutomatically = true
|
||||||
currentServerId = ""
|
|
||||||
currentUserId = ""
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
||||||
coEvery { serverRepository.restoreSession(null, null) } returns null
|
coEvery { serverRepository.restoreLastSession() } returns RestoredSession.None
|
||||||
|
|
||||||
viewModel.appStart(null)
|
viewModel.handleAppStart()
|
||||||
advanceUntilIdle()
|
|
||||||
|
|
||||||
coVerify(exactly = 1) { serverRepository.restoreSession(null, null) }
|
coVerify(exactly = 1) { serverRepository.restoreLastSession() }
|
||||||
val slot = slot<SetupDestination>()
|
val slot = slot<SetupDestination>()
|
||||||
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
||||||
slot.captured.let { destination ->
|
slot.captured.let { destination ->
|
||||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.ServerList)
|
Assert.assertTrue(
|
||||||
|
"destination is ${destination::class}",
|
||||||
|
destination is SetupDestination.ServerList,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Test intent result is noop`() =
|
fun `Test intent result is noop, hot reload`() =
|
||||||
runTest(testDispatcher) {
|
runTest(testDispatcher) {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = true
|
signInAutomatically = true
|
||||||
currentServerId = serverId.toServerString()
|
|
||||||
currentUserId = userId.toServerString()
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow(currentUser)
|
every { serverRepository.current } returns MutableStateFlow(currentUser)
|
||||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
coEvery { serverRepository.restoreLastSession() } returns
|
||||||
|
RestoredSession.Success(currentUser)
|
||||||
coEvery { intentService.parseIntent(any()) } returns IntentResult.NoOp
|
coEvery { intentService.parseIntent(any()) } returns IntentResult.NoOp
|
||||||
|
|
||||||
viewModel.appStart(Intent())
|
val result = viewModel.handleIntent(Intent())
|
||||||
advanceUntilIdle()
|
assertFalse(result)
|
||||||
|
|
||||||
val slot = slot<SetupDestination>()
|
// val slot = slot<SetupDestination>()
|
||||||
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
// verify { setupNavigationManager.navigateTo(capture(slot)) }
|
||||||
slot.captured.let { destination ->
|
// slot.captured.let { destination ->
|
||||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.AppContent)
|
// Assert.assertTrue(
|
||||||
destination as SetupDestination.AppContent
|
// "destination is ${destination::class}",
|
||||||
assertEquals(currentUser, destination.current)
|
// destination is SetupDestination.AppContent,
|
||||||
}
|
// )
|
||||||
|
// destination as SetupDestination.AppContent
|
||||||
|
// Assert.assertEquals(currentUser, destination.current)
|
||||||
|
// }
|
||||||
|
coVerify(exactly = 0) { serverRepository.restoreLastSession() }
|
||||||
|
coVerify(exactly = 0) { serverRepository.getMostRecentServer() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -272,22 +280,22 @@ class TestMainActivityViewModel {
|
||||||
runTest(testDispatcher) {
|
runTest(testDispatcher) {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = true
|
signInAutomatically = true
|
||||||
currentServerId = serverId.toServerString()
|
|
||||||
currentUserId = userId.toServerString()
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow(currentUser)
|
every { serverRepository.current } returns MutableStateFlow(currentUser)
|
||||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
|
||||||
coEvery { intentService.parseIntent(any()) } returns IntentResult.Error("Error")
|
coEvery { intentService.parseIntent(any()) } returns IntentResult.Error("Error")
|
||||||
|
|
||||||
viewModel.appStart(Intent())
|
val result = viewModel.handleIntent(Intent())
|
||||||
advanceUntilIdle()
|
assertTrue(result)
|
||||||
|
|
||||||
val slot = slot<SetupDestination>()
|
val slot = slot<SetupDestination>()
|
||||||
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
||||||
slot.captured.let { destination ->
|
slot.captured.let { destination ->
|
||||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.UserList)
|
Assert.assertTrue(
|
||||||
|
"destination is ${destination::class}",
|
||||||
|
destination is SetupDestination.UserList,
|
||||||
|
)
|
||||||
destination as SetupDestination.UserList
|
destination as SetupDestination.UserList
|
||||||
assertEquals(currentUser.server, destination.server)
|
Assert.assertEquals(currentUser.server, destination.server)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,20 +304,20 @@ class TestMainActivityViewModel {
|
||||||
runTest(testDispatcher) {
|
runTest(testDispatcher) {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = true
|
signInAutomatically = true
|
||||||
currentServerId = serverId.toServerString()
|
|
||||||
currentUserId = userId.toServerString()
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow(null)
|
every { serverRepository.current } returns MutableStateFlow(null)
|
||||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
|
||||||
coEvery { intentService.parseIntent(any()) } returns IntentResult.Error("Error")
|
coEvery { intentService.parseIntent(any()) } returns IntentResult.Error("Error")
|
||||||
|
|
||||||
viewModel.appStart(Intent())
|
val result = viewModel.handleIntent(Intent())
|
||||||
advanceUntilIdle()
|
assertTrue(result)
|
||||||
|
|
||||||
val slot = slot<SetupDestination>()
|
val slot = slot<SetupDestination>()
|
||||||
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
||||||
slot.captured.let { destination ->
|
slot.captured.let { destination ->
|
||||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.ServerList)
|
Assert.assertTrue(
|
||||||
|
"destination is ${destination::class}",
|
||||||
|
destination is SetupDestination.ServerList,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -318,11 +326,9 @@ class TestMainActivityViewModel {
|
||||||
runTest(testDispatcher) {
|
runTest(testDispatcher) {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = true
|
signInAutomatically = true
|
||||||
currentServerId = serverId.toServerString()
|
|
||||||
currentUserId = userId.toServerString()
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow(currentUser)
|
every { serverRepository.current } returns MutableStateFlow(currentUser)
|
||||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
// coEvery { serverRepository.tryRestoreSession(serverId, userId) } returns currentUser
|
||||||
coEvery { intentService.parseIntent(any()) } returns
|
coEvery { intentService.parseIntent(any()) } returns
|
||||||
IntentResult.Target(
|
IntentResult.Target(
|
||||||
destinations = listOf(Destination.Favorites),
|
destinations = listOf(Destination.Favorites),
|
||||||
|
|
@ -331,21 +337,27 @@ class TestMainActivityViewModel {
|
||||||
val backstack = mutableListOf<Destination>()
|
val backstack = mutableListOf<Destination>()
|
||||||
every { navigationManager.backStack } returns backstack
|
every { navigationManager.backStack } returns backstack
|
||||||
|
|
||||||
viewModel.appStart(Intent())
|
val result = viewModel.handleIntent(Intent())
|
||||||
advanceUntilIdle()
|
assertTrue(result)
|
||||||
|
|
||||||
val slot = slot<SetupDestination>()
|
val slot = slot<SetupDestination>()
|
||||||
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
||||||
verify { navigationManager.reloadHome() }
|
verify { navigationManager.reloadHome() }
|
||||||
slot.captured.let { destination ->
|
slot.captured.let { destination ->
|
||||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.AppContent)
|
Assert.assertTrue(
|
||||||
|
"destination is ${destination::class}",
|
||||||
|
destination is SetupDestination.AppContent,
|
||||||
|
)
|
||||||
destination as SetupDestination.AppContent
|
destination as SetupDestination.AppContent
|
||||||
assertEquals(currentUser, destination.current)
|
Assert.assertEquals(currentUser, destination.current)
|
||||||
}
|
}
|
||||||
// Note: navigation manager internally handles Home destination, so it won't be in this list
|
// Note: navigation manager internally handles Home destination, so it won't be in this list
|
||||||
assertEquals(1, backstack.size)
|
Assert.assertEquals(1, backstack.size)
|
||||||
backstack[0].let { destination ->
|
backstack[0].let { destination ->
|
||||||
assertTrue("destination is ${destination::class}", destination is Destination.Favorites)
|
Assert.assertTrue(
|
||||||
|
"destination is ${destination::class}",
|
||||||
|
destination is Destination.Favorites,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -354,11 +366,9 @@ class TestMainActivityViewModel {
|
||||||
runTest(testDispatcher) {
|
runTest(testDispatcher) {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = true
|
signInAutomatically = true
|
||||||
currentServerId = serverId.toServerString()
|
|
||||||
currentUserId = userId.toServerString()
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow(currentUser)
|
every { serverRepository.current } returns MutableStateFlow(currentUser)
|
||||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
// coEvery { serverRepository.tryRestoreSession(serverId, userId) } returns currentUser
|
||||||
coEvery { intentService.parseIntent(any()) } returns
|
coEvery { intentService.parseIntent(any()) } returns
|
||||||
IntentResult.Target(
|
IntentResult.Target(
|
||||||
destinations = listOf(Destination.Favorites),
|
destinations = listOf(Destination.Favorites),
|
||||||
|
|
@ -367,20 +377,26 @@ class TestMainActivityViewModel {
|
||||||
val backstack = mutableListOf<Destination>()
|
val backstack = mutableListOf<Destination>()
|
||||||
every { navigationManager.backStack } returns backstack
|
every { navigationManager.backStack } returns backstack
|
||||||
|
|
||||||
viewModel.appStart(Intent())
|
val result = viewModel.handleIntent(Intent())
|
||||||
advanceUntilIdle()
|
assertTrue(result)
|
||||||
|
|
||||||
val slot = slot<SetupDestination>()
|
val slot = slot<SetupDestination>()
|
||||||
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
||||||
verify(exactly = 0) { navigationManager.reloadHome() }
|
verify(exactly = 0) { navigationManager.reloadHome() }
|
||||||
slot.captured.let { destination ->
|
slot.captured.let { destination ->
|
||||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.AppContent)
|
Assert.assertTrue(
|
||||||
|
"destination is ${destination::class}",
|
||||||
|
destination is SetupDestination.AppContent,
|
||||||
|
)
|
||||||
destination as SetupDestination.AppContent
|
destination as SetupDestination.AppContent
|
||||||
assertEquals(currentUser, destination.current)
|
Assert.assertEquals(currentUser, destination.current)
|
||||||
}
|
}
|
||||||
assertEquals(1, backstack.size)
|
Assert.assertEquals(1, backstack.size)
|
||||||
backstack[0].let { destination ->
|
backstack[0].let { destination ->
|
||||||
assertTrue("destination is ${destination::class}", destination is Destination.Favorites)
|
Assert.assertTrue(
|
||||||
|
"destination is ${destination::class}",
|
||||||
|
destination is Destination.Favorites,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -389,11 +405,9 @@ class TestMainActivityViewModel {
|
||||||
runTest(testDispatcher) {
|
runTest(testDispatcher) {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = true
|
signInAutomatically = true
|
||||||
currentServerId = serverId.toServerString()
|
|
||||||
currentUserId = userId.toServerString()
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow(null)
|
every { serverRepository.current } returns MutableStateFlow(null)
|
||||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
// coEvery { serverRepository.tryRestoreSession(serverId, userId) } returns currentUser
|
||||||
coEvery { intentService.parseIntent(any()) } returns
|
coEvery { intentService.parseIntent(any()) } returns
|
||||||
IntentResult.Target(
|
IntentResult.Target(
|
||||||
destinations = listOf(Destination.Favorites),
|
destinations = listOf(Destination.Favorites),
|
||||||
|
|
@ -402,14 +416,17 @@ class TestMainActivityViewModel {
|
||||||
val backstack = mutableListOf<Destination>()
|
val backstack = mutableListOf<Destination>()
|
||||||
every { navigationManager.backStack } returns backstack
|
every { navigationManager.backStack } returns backstack
|
||||||
|
|
||||||
viewModel.appStart(Intent())
|
val result = viewModel.handleIntent(Intent())
|
||||||
advanceUntilIdle()
|
assertTrue(result)
|
||||||
|
|
||||||
val slot = slot<SetupDestination>()
|
val slot = slot<SetupDestination>()
|
||||||
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
verify { setupNavigationManager.navigateTo(capture(slot)) }
|
||||||
verify(exactly = 0) { navigationManager.reloadHome() }
|
verify(exactly = 0) { navigationManager.reloadHome() }
|
||||||
slot.captured.let { destination ->
|
slot.captured.let { destination ->
|
||||||
assertTrue("destination is ${destination::class}", destination is SetupDestination.ServerList)
|
Assert.assertTrue(
|
||||||
|
"destination is ${destination::class}",
|
||||||
|
destination is SetupDestination.ServerList,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ package com.github.damontecres.wholphin.services
|
||||||
import android.app.SearchManager
|
import android.app.SearchManager
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.github.damontecres.wholphin.data.CurrentUser
|
import com.github.damontecres.wholphin.data.CurrentUser
|
||||||
import com.github.damontecres.wholphin.data.JellyfinServerDao
|
import com.github.damontecres.wholphin.data.RestoredSession
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
|
@ -20,7 +20,6 @@ import io.mockk.coEvery
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -42,7 +41,6 @@ import org.robolectric.annotation.Config
|
||||||
class IntentServiceTest {
|
class IntentServiceTest {
|
||||||
private val api: ApiClient = mockk()
|
private val api: ApiClient = mockk()
|
||||||
private val serverRepository: ServerRepository = mockk()
|
private val serverRepository: ServerRepository = mockk()
|
||||||
private val serverDao: JellyfinServerDao = mockk()
|
|
||||||
private val userPreferencesService: UserPreferencesService = mockk()
|
private val userPreferencesService: UserPreferencesService = mockk()
|
||||||
private val userLibraryApi: UserLibraryApi = mockk()
|
private val userLibraryApi: UserLibraryApi = mockk()
|
||||||
|
|
||||||
|
|
@ -64,7 +62,6 @@ class IntentServiceTest {
|
||||||
intentService = IntentService(api, serverRepository, userPreferencesService)
|
intentService = IntentService(api, serverRepository, userPreferencesService)
|
||||||
every { api.userLibraryApi } returns userLibraryApi
|
every { api.userLibraryApi } returns userLibraryApi
|
||||||
coEvery { userLibraryApi.getItem(itemId) } returns successResponse(movie(itemId))
|
coEvery { userLibraryApi.getItem(itemId) } returns successResponse(movie(itemId))
|
||||||
every { serverRepository.serverDao } returns serverDao
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupPreferences(block: AppPreferences.Builder.() -> Unit) {
|
private fun setupPreferences(block: AppPreferences.Builder.() -> Unit) {
|
||||||
|
|
@ -82,11 +79,11 @@ class IntentServiceTest {
|
||||||
private fun setupAutoSignInUnprotected() {
|
private fun setupAutoSignInUnprotected() {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = true
|
signInAutomatically = true
|
||||||
currentServerId = serverId.toServerString()
|
|
||||||
currentUserId = userId.toServerString()
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
|
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
|
||||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
coEvery { serverRepository.restoreLastSession() } returns
|
||||||
|
RestoredSession.Success(currentUser)
|
||||||
|
coEvery { serverRepository.tryChangeUser(serverId, userId) } returns currentUser
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -98,7 +95,7 @@ class IntentServiceTest {
|
||||||
val result = intentService.prepare(intent)
|
val result = intentService.prepare(intent)
|
||||||
assertNull(result)
|
assertNull(result)
|
||||||
|
|
||||||
coVerify { serverRepository.restoreSession(serverId, userId) }
|
coVerify { serverRepository.restoreLastSession() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -106,17 +103,17 @@ class IntentServiceTest {
|
||||||
runTest {
|
runTest {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = false
|
signInAutomatically = false
|
||||||
currentServerId = serverId.toServerString()
|
|
||||||
currentUserId = userId.toServerString()
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
|
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
|
||||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
coEvery { serverRepository.restoreLastSession() } returns
|
||||||
|
RestoredSession.Success(currentUser)
|
||||||
|
coEvery { serverRepository.tryChangeUser(serverId, userId) } returns currentUser
|
||||||
|
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
val result = intentService.prepare(intent)
|
val result = intentService.prepare(intent)
|
||||||
assertTrue(result is IntentResult.Error)
|
assertTrue(result is IntentResult.Error)
|
||||||
|
|
||||||
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
|
coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -124,17 +121,17 @@ class IntentServiceTest {
|
||||||
runTest {
|
runTest {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = true
|
signInAutomatically = true
|
||||||
currentServerId = serverId.toServerString()
|
|
||||||
currentUserId = userId.toServerString()
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(protectedCurrentUser)
|
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(protectedCurrentUser)
|
||||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
|
coEvery { serverRepository.tryChangeUser(serverId, userId) } returns null
|
||||||
|
coEvery { serverRepository.restoreLastSession() } returns
|
||||||
|
RestoredSession.ServerOnly(protectedCurrentUser.server)
|
||||||
|
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
val result = intentService.prepare(intent)
|
val result = intentService.prepare(intent)
|
||||||
assertTrue(result is IntentResult.Error)
|
assertTrue(result is IntentResult.Error)
|
||||||
|
|
||||||
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
|
coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -142,17 +139,17 @@ class IntentServiceTest {
|
||||||
runTest {
|
runTest {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = true
|
signInAutomatically = true
|
||||||
currentServerId = serverId.toServerString()
|
|
||||||
currentUserId = userId.toServerString()
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
||||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
|
coEvery { serverRepository.tryChangeUser(serverId, userId) } returns null
|
||||||
|
coEvery { serverRepository.restoreLastSession() } returns
|
||||||
|
RestoredSession.ServerOnly(protectedCurrentUser.server)
|
||||||
|
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
val result = intentService.prepare(intent)
|
val result = intentService.prepare(intent)
|
||||||
assertTrue(result is IntentResult.Error)
|
assertTrue(result is IntentResult.Error)
|
||||||
|
|
||||||
coVerify(exactly = 1) { serverRepository.restoreSession(serverId, userId) }
|
coVerify(exactly = 1) { serverRepository.restoreLastSession() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -160,17 +157,16 @@ class IntentServiceTest {
|
||||||
runTest {
|
runTest {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = true
|
signInAutomatically = true
|
||||||
currentServerId = ""
|
|
||||||
currentUserId = ""
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
||||||
|
coEvery { serverRepository.restoreLastSession() } returns RestoredSession.None
|
||||||
// coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
|
// coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
|
||||||
|
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
val result = intentService.prepare(intent)
|
val result = intentService.prepare(intent)
|
||||||
assertTrue(result is IntentResult.Error)
|
assertTrue(result is IntentResult.Error)
|
||||||
|
|
||||||
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
|
coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -178,12 +174,11 @@ class IntentServiceTest {
|
||||||
runTest {
|
runTest {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = true
|
signInAutomatically = true
|
||||||
currentServerId = ""
|
|
||||||
currentUserId = ""
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
||||||
coEvery { serverRepository.serverDao.getUser(serverId, userId) } returns currentUser.user
|
coEvery { serverRepository.tryChangeUser(serverId, userId) } returns currentUser
|
||||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
|
coEvery { serverRepository.restoreLastSession() } returns
|
||||||
|
RestoredSession.Success(currentUser)
|
||||||
|
|
||||||
val intent =
|
val intent =
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
|
|
@ -193,7 +188,7 @@ class IntentServiceTest {
|
||||||
val result = intentService.prepare(intent)
|
val result = intentService.prepare(intent)
|
||||||
assertNull(result)
|
assertNull(result)
|
||||||
|
|
||||||
coVerify(exactly = 1) { serverRepository.restoreSession(serverId, userId) }
|
coVerify(exactly = 1) { serverRepository.tryChangeUser(serverId, userId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -201,12 +196,11 @@ class IntentServiceTest {
|
||||||
runTest {
|
runTest {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = true
|
signInAutomatically = true
|
||||||
currentServerId = ""
|
|
||||||
currentUserId = ""
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
||||||
coEvery { serverRepository.serverDao.getUser(serverId, userId) } returns protectedCurrentUser.user
|
coEvery { serverRepository.tryChangeUser(serverId, userId) } returns null
|
||||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
|
// coEvery { serverRepository.restoreLastSession() } returns
|
||||||
|
// RestoredSession.ServerOnly(protectedCurrentUser.server)
|
||||||
|
|
||||||
val intent =
|
val intent =
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
|
|
@ -216,7 +210,7 @@ class IntentServiceTest {
|
||||||
val result = intentService.prepare(intent)
|
val result = intentService.prepare(intent)
|
||||||
assertTrue(result is IntentResult.Error)
|
assertTrue(result is IntentResult.Error)
|
||||||
|
|
||||||
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
|
coVerify(exactly = 1) { serverRepository.tryChangeUser(serverId, userId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -224,12 +218,9 @@ class IntentServiceTest {
|
||||||
runTest {
|
runTest {
|
||||||
setupPreferences {
|
setupPreferences {
|
||||||
signInAutomatically = true
|
signInAutomatically = true
|
||||||
currentServerId = ""
|
|
||||||
currentUserId = ""
|
|
||||||
}
|
}
|
||||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
|
||||||
coEvery { serverRepository.serverDao.getUser(serverId, userId) } returns null
|
coEvery { serverRepository.tryChangeUser(serverId, userId) } returns null
|
||||||
coEvery { serverRepository.restoreSession(serverId, userId) } returns null
|
|
||||||
|
|
||||||
val intent =
|
val intent =
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
|
|
@ -239,7 +230,7 @@ class IntentServiceTest {
|
||||||
val result = intentService.prepare(intent)
|
val result = intentService.prepare(intent)
|
||||||
assertTrue(result is IntentResult.Error)
|
assertTrue(result is IntentResult.Error)
|
||||||
|
|
||||||
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
|
coVerify(exactly = 1) { serverRepository.tryChangeUser(serverId, userId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -296,12 +287,11 @@ class IntentServiceTest {
|
||||||
|
|
||||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
|
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
|
||||||
coEvery {
|
coEvery {
|
||||||
serverRepository.restoreSession(
|
serverRepository.tryChangeUser(
|
||||||
newServerId,
|
newServerId,
|
||||||
newUserId,
|
newUserId,
|
||||||
)
|
)
|
||||||
} returns newCurrentUser
|
} returns newCurrentUser
|
||||||
every { serverDao.getUser(newServerId, newUserId) } returns newCurrentUser.user
|
|
||||||
|
|
||||||
val intent =
|
val intent =
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
|
|
@ -316,9 +306,8 @@ class IntentServiceTest {
|
||||||
assertEquals(0, destinations.size)
|
assertEquals(0, destinations.size)
|
||||||
assertTrue(result.addHomeToBackStack)
|
assertTrue(result.addHomeToBackStack)
|
||||||
|
|
||||||
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
|
coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) }
|
||||||
verify { serverDao.getUser(newServerId, newUserId) }
|
coVerify { serverRepository.tryChangeUser(newServerId, newUserId) }
|
||||||
coVerify { serverRepository.restoreSession(newServerId, newUserId) }
|
|
||||||
coVerify(exactly = 0) { userLibraryApi.getItem(itemId) }
|
coVerify(exactly = 0) { userLibraryApi.getItem(itemId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -333,12 +322,11 @@ class IntentServiceTest {
|
||||||
|
|
||||||
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
|
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
|
||||||
coEvery {
|
coEvery {
|
||||||
serverRepository.restoreSession(
|
serverRepository.tryChangeUser(
|
||||||
newServerId,
|
newServerId,
|
||||||
newUserId,
|
newUserId,
|
||||||
)
|
)
|
||||||
} returns newCurrentUser
|
} returns newCurrentUser
|
||||||
every { serverDao.getUser(newServerId, newUserId) } returns newCurrentUser.user
|
|
||||||
|
|
||||||
val intent =
|
val intent =
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
|
|
@ -358,9 +346,8 @@ class IntentServiceTest {
|
||||||
|
|
||||||
assertEquals(itemId, first.itemId)
|
assertEquals(itemId, first.itemId)
|
||||||
|
|
||||||
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
|
coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) }
|
||||||
verify { serverDao.getUser(newServerId, newUserId) }
|
coVerify { serverRepository.tryChangeUser(newServerId, newUserId) }
|
||||||
coVerify { serverRepository.restoreSession(newServerId, newUserId) }
|
|
||||||
coVerify { userLibraryApi.getItem(itemId) }
|
coVerify { userLibraryApi.getItem(itemId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
package com.github.damontecres.wholphin.services
|
package com.github.damontecres.wholphin.services
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.datastore.core.DataStoreFactory
|
import androidx.datastore.core.DataStoreFactory
|
||||||
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||||
import com.github.damontecres.wholphin.data.CurrentUser
|
import com.github.damontecres.wholphin.data.CurrentUser
|
||||||
import com.github.damontecres.wholphin.data.JellyfinServerDao
|
import com.github.damontecres.wholphin.data.JellyfinServerDao
|
||||||
|
import com.github.damontecres.wholphin.data.MostRecentServer
|
||||||
|
import com.github.damontecres.wholphin.data.MostRecentServerProvider
|
||||||
|
import com.github.damontecres.wholphin.data.RestoredSession
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinServerUsers
|
import com.github.damontecres.wholphin.data.model.JellyfinServerUsers
|
||||||
|
|
@ -14,6 +16,7 @@ import com.github.damontecres.wholphin.data.model.ServerUserConfig
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer
|
import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer
|
||||||
import com.github.damontecres.wholphin.test.nonBlankString
|
import com.github.damontecres.wholphin.test.nonBlankString
|
||||||
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.successResponse
|
import com.github.damontecres.wholphin.ui.successResponse
|
||||||
import com.github.damontecres.wholphin.ui.toServerString
|
import com.github.damontecres.wholphin.ui.toServerString
|
||||||
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
||||||
|
|
@ -21,6 +24,7 @@ import com.github.damontecres.wholphin.util.configure
|
||||||
import com.github.damontecres.wholphin.util.reset
|
import com.github.damontecres.wholphin.util.reset
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
|
@ -29,7 +33,6 @@ import io.mockk.verify
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -48,6 +51,8 @@ import org.jellyfin.sdk.model.api.PublicSystemInfo
|
||||||
import org.jellyfin.sdk.model.api.UserDto
|
import org.jellyfin.sdk.model.api.UserDto
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -80,14 +85,13 @@ class ServerRepositoryTest {
|
||||||
|
|
||||||
private val mockUserApi = mockk<UserApi>()
|
private val mockUserApi = mockk<UserApi>()
|
||||||
private val mockSystemApi = mockk<SystemApi>()
|
private val mockSystemApi = mockk<SystemApi>()
|
||||||
private val mockSharedPreferences = mockk<SharedPreferences>(relaxed = true)
|
private val mockMostRecentServerProvider = mockk<MostRecentServerProvider>()
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
WholphinDispatchers.configure(testDispatcher)
|
WholphinDispatchers.configure(testDispatcher)
|
||||||
|
|
||||||
every { mockContext.getSharedPreferences(any(), any()) } returns mockSharedPreferences
|
|
||||||
every { mockApiClient.userApi } returns mockUserApi
|
every { mockApiClient.userApi } returns mockUserApi
|
||||||
every { mockApiClient.systemApi } returns mockSystemApi
|
every { mockApiClient.systemApi } returns mockSystemApi
|
||||||
coEvery { mockUserApi.getCurrentUser() } returns successResponse(userDto)
|
coEvery { mockUserApi.getCurrentUser() } returns successResponse(userDto)
|
||||||
|
|
@ -102,9 +106,15 @@ class ServerRepositoryTest {
|
||||||
emptyMap(),
|
emptyMap(),
|
||||||
)
|
)
|
||||||
coEvery { mockJellyfinServerDao.addOrUpdateUser(user) } returns user
|
coEvery { mockJellyfinServerDao.addOrUpdateUser(user) } returns user
|
||||||
|
coEvery { mockJellyfinServerDao.getServer(serverId) } returns
|
||||||
|
JellyfinServerUsers(server, listOf(user))
|
||||||
|
coEvery { mockJellyfinServerDao.addOrUpdateServer(server) } just Runs
|
||||||
every { mockApiClient.clientInfo } returns ClientInfo("Wholphin test", "0.0.1")
|
every { mockApiClient.clientInfo } returns ClientInfo("Wholphin test", "0.0.1")
|
||||||
every { mockApiClient.deviceInfo } returns DeviceInfo("Wholphin test ID", "Wholphin test device")
|
every { mockApiClient.deviceInfo } returns DeviceInfo("Wholphin test ID", "Wholphin test device")
|
||||||
every { mockApiClient.update(any(), any(), any(), any()) } just Runs
|
every { mockApiClient.update(any(), any(), any(), any()) } just Runs
|
||||||
|
coEvery { mockMostRecentServerProvider.save(any()) } just Runs
|
||||||
|
coEvery { mockMostRecentServerProvider.clear() } just Runs
|
||||||
|
coEvery { mockMostRecentServerProvider.clearUser() } just Runs
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
@ -119,8 +129,8 @@ class ServerRepositoryTest {
|
||||||
mockJellyfin,
|
mockJellyfin,
|
||||||
mockJellyfinServerDao,
|
mockJellyfinServerDao,
|
||||||
mockApiClient,
|
mockApiClient,
|
||||||
dataStore,
|
|
||||||
testDispatcher,
|
testDispatcher,
|
||||||
|
mockMostRecentServerProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val serverId = UUID.randomUUID()
|
private val serverId = UUID.randomUUID()
|
||||||
|
|
@ -194,24 +204,24 @@ class ServerRepositoryTest {
|
||||||
Assert.assertEquals(user, serverRepository.currentUser)
|
Assert.assertEquals(user, serverRepository.currentUser)
|
||||||
Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
|
Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
|
||||||
|
|
||||||
val appPreferences = dataStore.data.first()
|
coVerify(exactly = 1) { mockMostRecentServerProvider.save(CurrentUser(server, user)) }
|
||||||
Assert.assertEquals(serverId.toServerString(), appPreferences.currentServerId)
|
|
||||||
Assert.assertEquals(userId.toServerString(), appPreferences.currentUserId)
|
|
||||||
|
|
||||||
verify(exactly = 1) { mockSharedPreferences.edit() }
|
|
||||||
verify(exactly = 1) { mockJellyfinServerDao.addOrUpdateUser(user) }
|
verify(exactly = 1) { mockJellyfinServerDao.addOrUpdateUser(user) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Test restoreUser via changeUser`() =
|
fun `Test restoreUser via changeUser`() =
|
||||||
runTest {
|
runTest {
|
||||||
|
val user = user.copy(pin = null)
|
||||||
every { mockJellyfinServerDao.addOrUpdateServer(any()) } just Runs
|
every { mockJellyfinServerDao.addOrUpdateServer(any()) } just Runs
|
||||||
every { mockJellyfinServerDao.addOrUpdateUser(user) } returns user
|
every { mockJellyfinServerDao.addOrUpdateUser(user) } returns user
|
||||||
every { mockJellyfinServerDao.getServer(serverId) } returns JellyfinServerUsers(server, listOf(user))
|
every { mockJellyfinServerDao.getServer(serverId) } returns JellyfinServerUsers(server, listOf(user))
|
||||||
|
|
||||||
val serverRepository = create()
|
val serverRepository = create()
|
||||||
Assert.assertNull(serverRepository.currentUser)
|
Assert.assertNull(serverRepository.currentUser)
|
||||||
serverRepository.restoreSession(server.id, user.id)
|
val result = serverRepository.tryChangeUser(server.id, user.id)
|
||||||
|
assertTrue(result != null)
|
||||||
|
assertEquals(serverId, result?.server?.id)
|
||||||
|
assertEquals(user.id, result?.user?.id)
|
||||||
|
|
||||||
verify(exactly = 1) {
|
verify(exactly = 1) {
|
||||||
mockApiClient.update(
|
mockApiClient.update(
|
||||||
|
|
@ -226,34 +236,6 @@ class ServerRepositoryTest {
|
||||||
Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
|
Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Test restoreUser via shortcut`() =
|
|
||||||
runTest {
|
|
||||||
every { mockJellyfinServerDao.getServer(serverId) } returns JellyfinServerUsers(server, listOf(user))
|
|
||||||
|
|
||||||
val serverRepository = create()
|
|
||||||
setUpCurrentUser(serverRepository)
|
|
||||||
|
|
||||||
serverRepository.restoreSession(server.id, user.id)
|
|
||||||
|
|
||||||
verify(exactly = 1) {
|
|
||||||
mockApiClient.update(
|
|
||||||
baseUrl = server.url,
|
|
||||||
accessToken = user.accessToken,
|
|
||||||
clientInfo = any(),
|
|
||||||
deviceInfo = any(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
verify(exactly = 0) { mockJellyfinServerDao.addOrUpdateServer(any()) }
|
|
||||||
verify(exactly = 0) { mockJellyfinServerDao.addOrUpdateUser(any()) }
|
|
||||||
|
|
||||||
Assert.assertEquals(server, serverRepository.currentServer)
|
|
||||||
Assert.assertEquals(user, serverRepository.currentUser)
|
|
||||||
|
|
||||||
// TODO fix this, need to check userDto too
|
|
||||||
// Assert.assertEquals(userDto, serverRepository.currentUserDto)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Test remove user`() =
|
fun `Test remove user`() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|
@ -308,6 +290,7 @@ class ServerRepositoryTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
verify(exactly = 1) { mockJellyfinServerDao.deleteUser(serverId, userId) }
|
verify(exactly = 1) { mockJellyfinServerDao.deleteUser(serverId, userId) }
|
||||||
|
coVerify { mockMostRecentServerProvider.clearUser() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -351,6 +334,7 @@ class ServerRepositoryTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
verify(exactly = 1) { mockJellyfinServerDao.deleteServer(serverId) }
|
verify(exactly = 1) { mockJellyfinServerDao.deleteServer(serverId) }
|
||||||
|
coVerify { mockMostRecentServerProvider.clear() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -387,11 +371,7 @@ class ServerRepositoryTest {
|
||||||
Assert.assertEquals(user, serverRepository.currentUser)
|
Assert.assertEquals(user, serverRepository.currentUser)
|
||||||
Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
|
Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
|
||||||
|
|
||||||
val appPreferences = dataStore.data.first()
|
coVerify(exactly = 1) { mockMostRecentServerProvider.save(CurrentUser(server, user)) }
|
||||||
Assert.assertEquals(serverId.toServerString(), appPreferences.currentServerId)
|
|
||||||
Assert.assertEquals(userId.toServerString(), appPreferences.currentUserId)
|
|
||||||
|
|
||||||
verify(exactly = 1) { mockSharedPreferences.edit() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -430,11 +410,7 @@ class ServerRepositoryTest {
|
||||||
Assert.assertEquals(user, serverRepository.currentUser)
|
Assert.assertEquals(user, serverRepository.currentUser)
|
||||||
Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
|
Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
|
||||||
|
|
||||||
val appPreferences = dataStore.data.first()
|
coVerify(exactly = 1) { mockMostRecentServerProvider.save(CurrentUser(server, user)) }
|
||||||
Assert.assertEquals(serverId.toServerString(), appPreferences.currentServerId)
|
|
||||||
Assert.assertEquals(userId.toServerString(), appPreferences.currentUserId)
|
|
||||||
|
|
||||||
verify(exactly = 1) { mockSharedPreferences.edit() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -484,4 +460,99 @@ class ServerRepositoryTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test restoreLastSession, success`() =
|
||||||
|
runTest {
|
||||||
|
val user = user.copy(pin = null)
|
||||||
|
coEvery { mockJellyfinServerDao.addOrUpdateUser(user) } returns user
|
||||||
|
coEvery { mockJellyfinServerDao.getServer(serverId) } returns
|
||||||
|
JellyfinServerUsers(server, listOf(user))
|
||||||
|
every { mockMostRecentServerProvider.get() } returns
|
||||||
|
MostRecentServer.from(CurrentUser(server, user))
|
||||||
|
|
||||||
|
val result = create().restoreLastSession()
|
||||||
|
assertTrue(result is RestoredSession.Success)
|
||||||
|
val current = (result as RestoredSession.Success).currentUser
|
||||||
|
assertEquals(server, current.server)
|
||||||
|
assertEquals(user, current.user)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
mockApiClient.update(
|
||||||
|
baseUrl = server.url,
|
||||||
|
accessToken = user.accessToken,
|
||||||
|
clientInfo = any(),
|
||||||
|
deviceInfo = any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
coVerify { mockMostRecentServerProvider.save(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test restoreLastSession, user protected`() =
|
||||||
|
runTest {
|
||||||
|
every { mockMostRecentServerProvider.get() } returns
|
||||||
|
MostRecentServer.ServerAndUser(serverId, server.url, userId, "token")
|
||||||
|
|
||||||
|
val result = create().restoreLastSession()
|
||||||
|
assertTrue(result is RestoredSession.ServerOnly)
|
||||||
|
val current = (result as RestoredSession.ServerOnly)
|
||||||
|
assertEquals(server, current.server)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test restoreLastSession, server only`() =
|
||||||
|
runTest {
|
||||||
|
every { mockMostRecentServerProvider.get() } returns
|
||||||
|
MostRecentServer.Server(server.id, server.url)
|
||||||
|
|
||||||
|
val result = create().restoreLastSession()
|
||||||
|
assertTrue(result is RestoredSession.ServerOnly)
|
||||||
|
val current = (result as RestoredSession.ServerOnly)
|
||||||
|
assertEquals(server, current.server)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test restoreLastSession, nothing to restore`() =
|
||||||
|
runTest {
|
||||||
|
every { mockMostRecentServerProvider.get() } returns MostRecentServer.None
|
||||||
|
|
||||||
|
val result = create().restoreLastSession()
|
||||||
|
assertTrue(result is RestoredSession.None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestMostRecentServerProvider : MostRecentServerProvider {
|
||||||
|
var mostRecentServer: MostRecentServer = MostRecentServer.None
|
||||||
|
|
||||||
|
override fun get(): MostRecentServer = mostRecentServer
|
||||||
|
|
||||||
|
override suspend fun save(current: CurrentUser) {
|
||||||
|
mostRecentServer =
|
||||||
|
if (current.user.accessToken.isNotNullOrBlank()) {
|
||||||
|
MostRecentServer.ServerAndUser(
|
||||||
|
current.server.id,
|
||||||
|
current.server.url,
|
||||||
|
current.user.id,
|
||||||
|
current.user.accessToken,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
MostRecentServer.Server(
|
||||||
|
current.server.id,
|
||||||
|
current.server.url,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun clear() {
|
||||||
|
mostRecentServer = MostRecentServer.None
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun clearUser() {
|
||||||
|
when (val s = mostRecentServer) {
|
||||||
|
MostRecentServer.None -> Unit
|
||||||
|
is MostRecentServer.Server -> Unit
|
||||||
|
is MostRecentServer.ServerAndUser -> MostRecentServer.Server(s.serverId, s.serverUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,10 @@ import androidx.work.Data
|
||||||
import androidx.work.ListenableWorker
|
import androidx.work.ListenableWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.github.damontecres.wholphin.data.CurrentUser
|
import com.github.damontecres.wholphin.data.CurrentUser
|
||||||
|
import com.github.damontecres.wholphin.data.RestoredSession
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
|
import com.github.damontecres.wholphin.test.currentUser
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
|
|
@ -113,16 +115,16 @@ class SuggestionsWorkerTest {
|
||||||
val mockUser = mockk<CurrentUser>()
|
val mockUser = mockk<CurrentUser>()
|
||||||
var restored = false
|
var restored = false
|
||||||
every { mockServerRepository.current } answers { MutableStateFlow(if (restored) mockUser else null) }
|
every { mockServerRepository.current } answers { MutableStateFlow(if (restored) mockUser else null) }
|
||||||
coEvery { mockServerRepository.restoreSession(testServerId, testUserId) } coAnswers {
|
coEvery { mockServerRepository.restoreLastSession() } coAnswers {
|
||||||
restored = true
|
restored = true
|
||||||
mockUser
|
RestoredSession.Success(currentUser())
|
||||||
}
|
}
|
||||||
every { mockPreferences.data } returns flowOf(mockPrefs())
|
every { mockPreferences.data } returns flowOf(mockPrefs())
|
||||||
coEvery { mockUserViewsApi.getUserViews(userId = testUserId) } returns mockQueryResult()
|
coEvery { mockUserViewsApi.getUserViews(userId = testUserId) } returns mockQueryResult()
|
||||||
|
|
||||||
val result = createWorker().doWork()
|
val result = createWorker().doWork()
|
||||||
|
|
||||||
coVerify { mockServerRepository.restoreSession(testServerId, testUserId) }
|
coVerify { mockServerRepository.restoreLastSession() }
|
||||||
assertEquals(ListenableWorker.Result.success(), result)
|
assertEquals(ListenableWorker.Result.success(), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import com.github.damontecres.wholphin.data.ItemPlaybackDao
|
||||||
import com.github.damontecres.wholphin.data.JellyfinServerDao
|
import com.github.damontecres.wholphin.data.JellyfinServerDao
|
||||||
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
|
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
|
||||||
import com.github.damontecres.wholphin.data.Migrations
|
import com.github.damontecres.wholphin.data.Migrations
|
||||||
|
import com.github.damontecres.wholphin.data.MostRecentServerProvider
|
||||||
import com.github.damontecres.wholphin.data.PlaybackEffectDao
|
import com.github.damontecres.wholphin.data.PlaybackEffectDao
|
||||||
import com.github.damontecres.wholphin.data.PlaybackLanguageChoiceDao
|
import com.github.damontecres.wholphin.data.PlaybackLanguageChoiceDao
|
||||||
import com.github.damontecres.wholphin.data.RememberedTabDao
|
import com.github.damontecres.wholphin.data.RememberedTabDao
|
||||||
|
|
@ -26,6 +27,7 @@ import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer
|
import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer
|
||||||
import com.github.damontecres.wholphin.services.SeerrApi
|
import com.github.damontecres.wholphin.services.SeerrApi
|
||||||
|
import com.github.damontecres.wholphin.services.TestMostRecentServerProvider
|
||||||
import com.github.damontecres.wholphin.services.hilt.AppModule
|
import com.github.damontecres.wholphin.services.hilt.AppModule
|
||||||
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
|
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
|
||||||
import com.github.damontecres.wholphin.services.hilt.DatabaseModule
|
import com.github.damontecres.wholphin.services.hilt.DatabaseModule
|
||||||
|
|
@ -201,6 +203,10 @@ object TestModule {
|
||||||
fun seerrApi(
|
fun seerrApi(
|
||||||
@StandardOkHttpClient okHttpClient: OkHttpClient,
|
@StandardOkHttpClient okHttpClient: OkHttpClient,
|
||||||
): SeerrApi = mockk()
|
): SeerrApi = mockk()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun mostRecentServerProvider(): MostRecentServerProvider = TestMostRecentServerProvider()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue