Merge Wholphin v1.1.0 while preserving Wolfskin features #27

Merged
justinzeus merged 72 commits from fm/wolfskin-upstream-1-1-0 into main 2026-11-05 01:49:35 +01:00
19 changed files with 786 additions and 493 deletions
Showing only changes of commit f01c5d367c - Show all commits

View file

@ -9,6 +9,7 @@ import android.view.WindowManager
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@ -75,7 +76,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -84,7 +84,6 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@ -436,120 +435,27 @@ class MainActivityViewModel
viewModelScope.launchDefault {
mutex.withLock {
try {
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@withLock
}
handleAppUpgrade()
} catch (ex: Exception) {
Timber.e(ex, "Error during app upgrade!")
showToast(
context,
"Error during app upgrade: ${ex.localizedMessage}",
)
}
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@withLock
}
IntentResult.NoOp,
null,
-> {
// No-op, proceed below
}
try {
val result = handleIntent(intent)
if (result) {
Timber.i("appStart was handled by intent")
return@withLock
}
} catch (ex: Exception) {
Timber.e(ex, "Error parsing intent")
}
try {
val needUpgrade = appUpgradeHandler.needUpgrade()
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)
}
}
handleAppStart()
} catch (ex: Exception) {
Timber.e(ex, "Error during appStart")
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

View file

@ -22,6 +22,7 @@ import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.github.damontecres.wholphin.data.RestoredSession
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.preferences.AppPreferences
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 dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
import okhttp3.OkHttpClient
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@ -75,8 +74,10 @@ class WholphinDreamService :
lifecycleRegistry.currentState = Lifecycle.State.CREATED
lifecycleScope.launchDefault {
if (serverRepository.current.value == null) {
val prefs = preferencesDataStore.data.first()
serverRepository.restoreSession(prefs.currentServerId.toUUIDOrNull(), prefs.currentUserId.toUUIDOrNull())
val result = serverRepository.restoreLastSession()
if (result !is RestoredSession.Success) {
Timber.w("Could not restore user session")
}
}
}
}

View file

@ -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)
}
}

View file

@ -1,15 +1,10 @@
package com.github.damontecres.wholphin.data
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.JellyfinUser
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.ui.toServerString
import com.github.damontecres.wholphin.util.WholphinDispatchers
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
@ -49,8 +44,8 @@ class ServerRepository
val jellyfin: Jellyfin,
val serverDao: JellyfinServerDao,
val apiClient: ApiClient,
val userPreferencesDataStore: DataStore<AppPreferences>,
@param:IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val mostRecentServerProvider: MostRecentServerProvider,
) {
private var _current = MutableStateFlow<CurrentUser?>(null)
val current: StateFlow<CurrentUser?> = _current
@ -88,10 +83,11 @@ class ServerRepository
}
apiClient.update(baseUrl = server.url, accessToken = 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(
server: JellyfinServer,
@ -119,70 +115,101 @@ class ServerRepository
)
serverDao.addOrUpdateServer(updatedServer)
updatedUser = serverDao.addOrUpdateUser(updatedUser)
userPreferencesDataStore.updateData {
it
.toBuilder()
.apply {
currentServerId = updatedServer.id.toServerString()
currentUserId = updatedUser.id.toServerString()
}.build()
}
val currentUser = CurrentUser(updatedServer, updatedUser)
withContext(WholphinDispatchers.Main) {
_current.value = currentUser
_currentUserDto.value = ServerUserConfig(userDto)
}
getServerSharedPreferences(context).edit(true) {
putString(SERVER_URL_KEY, updatedServer.url)
putString(ACCESS_TOKEN_KEY, updatedUser.accessToken)
}
mostRecentServerProvider.save(currentUser)
_current.value = currentUser
_currentUserDto.value = ServerUserConfig(userDto)
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?,
userId: UUID?,
): CurrentUser? {
if (serverId == null || userId == null) {
_current.value = null
return 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
): CurrentUser? =
withContext(ioDispatcher) {
return@withContext if (serverId == null || userId == null) {
_current.value = null
null
} else {
val user = serverAndUsers.users.firstOrNull { it.id == userId }
if (user != null) {
return changeUser(serverAndUsers.server, user)
val serverAndUsers = serverDao.getServer(serverId)
if (serverAndUsers != null) {
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
@ -244,13 +271,7 @@ class ServerRepository
withContext(WholphinDispatchers.Main) {
_current.value = null
}
userPreferencesDataStore.updateData {
it
.toBuilder()
.apply {
currentUserId = ""
}.build()
}
mostRecentServerProvider.clearUser()
apiClient.update(accessToken = null)
}
withContext(ioDispatcher) {
@ -263,14 +284,7 @@ class ServerRepository
withContext(WholphinDispatchers.Main) {
_current.value = null
}
userPreferencesDataStore.updateData {
it
.toBuilder()
.apply {
currentServerId = ""
currentUserId = ""
}.build()
}
mostRecentServerProvider.clear()
apiClient.update(baseUrl = null, accessToken = null)
}
withContext(ioDispatcher) {
@ -279,14 +293,7 @@ class ServerRepository
}
suspend fun switchServerOrUser() {
userPreferencesDataStore.updateData {
it
.toBuilder()
.apply {
currentServerId = ""
currentUserId = ""
}.build()
}
mostRecentServerProvider.clear()
}
suspend fun updateUserAuth(
@ -335,17 +342,6 @@ class ServerRepository
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

View file

@ -6,7 +6,9 @@ import android.os.Build
import androidx.core.content.edit
import androidx.datastore.core.DataStore
import androidx.preference.PreferenceManager
import com.github.damontecres.wholphin.data.CurrentUser
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.SeerrServerDao
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 dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber
@ -56,6 +59,7 @@ class AppUpgradeHandler
private val seerrServerDao: SeerrServerDao,
private val serverDao: JellyfinServerDao,
private val rememberedTabDao: RememberedTabDao,
private val mostRecentServerProvider: MostRecentServerProvider,
) {
val pkgInfo: PackageInfo get() = context.packageManager.getPackageInfo(context.packageName, 0)
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 = ""
}
}
}
}
}

View file

@ -2,6 +2,7 @@ package com.github.damontecres.wholphin.services
import android.content.Context
import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem
@ -188,9 +189,8 @@ class HomeSettingsService
/**
* Decodes [HomePageSettings] from a [JsonElement] skipping any unknown/unparsable rows
*
* This is public only for testing
*/
@VisibleForTesting
fun decode(element: JsonElement): HomePageSettings {
val version = element.jsonObject["version"]?.jsonPrimitive?.intOrNull
if (version == null || version > SUPPORTED_HOME_PAGE_SETTINGS_VERSION) {

View file

@ -2,6 +2,8 @@ package com.github.damontecres.wholphin.services
import android.app.SearchManager
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.model.BaseItem
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
@ -114,6 +116,7 @@ class IntentService
return IntentResult.Target(destinations, true)
}
@VisibleForTesting
internal suspend fun prepare(intent: Intent): IntentResult? {
val appPrefs = userPreferencesService.flow.first().appPreferences
@ -121,39 +124,33 @@ class IntentService
val serverId = intent.getStringParam(INTENT_SERVER_ID)?.toUUIDOrNull()
return if (userId != null && serverId != null) {
Timber.v("Intent switches user")
val user = serverRepository.serverDao.getUser(serverId, userId)
if (user != null && !user.isProtected) {
serverRepository.restoreSession(serverId, userId)
?: IntentResult.Error("Error restoring user")
val user = serverRepository.tryChangeUser(serverId, userId)
if (user != null) {
null
} else {
IntentResult.Error("Cannot switch to specified user")
IntentResult.Error("Error restoring user")
}
} else {
val profileProtected =
serverRepository.current.value
?.user
?.isProtected == true
if (appPrefs.signInAutomatically && !profileProtected) {
Timber.v("No current user, so restoring last")
val userId = appPrefs.currentUserId?.toUUIDOrNull()
val serverId = appPrefs.currentServerId?.toUUIDOrNull()
} else if (appPrefs.signInAutomatically) {
Timber.v("No current user, so restoring last")
val result = serverRepository.restoreLastSession()
Timber.v("Restored result=%s", result)
when (result) {
RestoredSession.None,
is RestoredSession.ServerOnly,
-> {
IntentResult.Error("Could not auto sign-in, specify a server & user")
}
if (userId != null && serverId != null) {
val current =
serverRepository.restoreSession(serverId, userId)
?: return IntentResult.Error("Error restoring user")
if (current.user.isProtected) {
is RestoredSession.Success -> {
if (result.currentUser.user.isProtected) {
IntentResult.Error("Could not auto sign-in, user is protected")
} else {
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")
}
}

View file

@ -1,6 +1,7 @@
package com.github.damontecres.wholphin.services
import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AppCompatActivity
import androidx.hilt.work.HiltWorker
import androidx.lifecycle.lifecycleScope
@ -13,6 +14,7 @@ import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.github.damontecres.wholphin.data.RestoredSession
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.ui.collectLatestIn
import com.github.damontecres.wholphin.util.ExceptionHandler
@ -52,12 +54,8 @@ class LatestNextUpWorker
try {
if (api.baseUrl.isNullOrBlank() || api.accessToken.isNullOrBlank()) {
// Not active
var currentUser = serverRepository.current.value
if (currentUser == null) {
serverRepository.restoreSession(serverId, userId)
currentUser = serverRepository.current.value
}
if (currentUser == null) {
val result = serverRepository.restoreLastSession()
if (result !is RestoredSession.Success) {
Timber.w("No user found during run")
return Result.failure()
}
@ -91,7 +89,7 @@ class LatestNextUpSchedulerService
"SuggestionsSchedulerService requires an AppCompatActivity context, but received: ${context::class.java.name}",
)
// Exposed for testing
@VisibleForTesting
internal var dispatcher: CoroutineDispatcher = WholphinDispatchers.IO
init {

View file

@ -5,6 +5,7 @@ import androidx.datastore.core.DataStore
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.github.damontecres.wholphin.data.RestoredSession
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences
@ -63,12 +64,8 @@ class SuggestionsWorker
}
if (api.baseUrl.isNullOrBlank() || api.accessToken.isNullOrBlank()) {
var currentUser = serverRepository.current.value
if (currentUser == null) {
serverRepository.restoreSession(serverId, userId)
currentUser = serverRepository.current.value
}
if (currentUser == null) {
val result = serverRepository.restoreLastSession()
if (result !is RestoredSession.Success) {
Timber.w("No user found during run")
return Result.failure()
}

View file

@ -4,6 +4,8 @@ import android.content.Context
import androidx.work.WorkManager
import com.github.damontecres.wholphin.BuildConfig
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.services.SeerrApi
import com.github.damontecres.wholphin.util.CoroutineContextApiClientFactory
@ -188,6 +190,12 @@ object AppModule {
fun seerrApi(
@StandardOkHttpClient okHttpClient: OkHttpClient,
) = SeerrApi(okHttpClient)
@Provides
@Singleton
fun mostRecentServerProvider(
@ApplicationContext context: Context,
): MostRecentServerProvider = MostRecentServerSharedPreferences(context)
}
@Module

View file

@ -19,6 +19,7 @@ import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.github.damontecres.wholphin.MainActivity
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.model.BaseItem
import com.github.damontecres.wholphin.preferences.AppPreference
@ -70,12 +71,8 @@ class TvProviderWorker
if (api.baseUrl.isNullOrBlank() || api.accessToken.isNullOrBlank()) {
// Not active
var currentUser = serverRepository.current.value
if (currentUser == null) {
serverRepository.restoreSession(serverId, userId)
currentUser = serverRepository.current.value
}
if (currentUser == null) {
val result = serverRepository.restoreLastSession()
if (result !is RestoredSession.Success) {
Timber.w("No user found during run")
return Result.failure()
}

View file

@ -5,6 +5,7 @@ import android.media.MediaCodecList
import android.os.Build
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.annotation.VisibleForTesting
import androidx.compose.ui.unit.Density
import androidx.core.net.toUri
import androidx.lifecycle.ViewModel
@ -198,7 +199,7 @@ class PlaybackViewModel
val currentUserDto = serverRepository.currentUserDtoFlow
// Exposed for testing
@VisibleForTesting
val initJob: Job
init {
@ -1110,7 +1111,7 @@ class PlaybackViewModel
}
// Variables for tracking segment state
// Exposed for testing
@VisibleForTesting
internal var segmentJob: Job? = null
private val autoSkippedSegments = mutableSetOf<UUID>()
private val outroShownSegments = mutableSetOf<UUID>()

View file

@ -1,7 +1,7 @@
package com.github.damontecres.wholphin.util
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.DeviceModule
import com.google.auto.service.AutoService
@ -13,6 +13,7 @@ import org.acra.sender.ReportSender
import org.acra.sender.ReportSenderException
import org.acra.sender.ReportSenderFactory
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.okhttp.OkHttpFactory
import org.jellyfin.sdk.createJellyfin
@ -39,26 +40,10 @@ class CrashReportSender : ReportSender {
errorContent: CrashReportData,
) {
Timber.v("Attempting to send crash report")
val prefs = ServerRepository.getServerSharedPreferences(context)
val serverUrl = prefs.getString(ServerRepository.SERVER_URL_KEY, null)
val accessToken = prefs.getString(ServerRepository.ACCESS_TOKEN_KEY, null)
if (serverUrl != null && accessToken != 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)
try {
val recentServer = AppModule.mostRecentServerProvider(context).get()
val api = recentServer.createApi(context)
if (api != null) {
val obj = JSONObject()
for ((key, value) in errorContent.toMap()) {
obj.put(key, value)
@ -75,13 +60,32 @@ class CrashReportSender : ReportSender {
""".trimIndent() + jsonStr,
).content.fileName
Timber.i("Sent report to $serverUrl, filename=$filename")
Timber.i("Sent report to %s, filename=%s", api.baseUrl, filename)
}
} catch (ex: Exception) {
throw ReportSenderException("Exception while sending crash report", ex)
} else {
throw ReportSenderException("Could not find valid server and/or credentials to use")
}
} else {
throw ReportSenderException("Could not find valid server and/or credentials to use")
} catch (ex: Exception) {
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
}

View file

@ -233,9 +233,8 @@ message ExperimentalPreferences {
}
message AppPreferences {
// The currently signed in server and user IDs, mostly for restoring a session
string current_server_id = 1;
string current_user_id = 2;
string current_server_id = 1 [deprecated = true];
string current_user_id = 2 [deprecated = true];
PlaybackPreferences playback_preferences = 3;
HomePagePreferences home_page_preferences = 4;

View file

@ -1,14 +1,12 @@
package com.github.damontecres.wholphin.test
package com.github.damontecres.wholphin
import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.datastore.core.DataStore
import com.github.damontecres.wholphin.MainActivityViewModel
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.model.JellyfinServerUsers
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.update
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.SetupDestination
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.toServerString
import com.github.damontecres.wholphin.util.WholphinDispatchers
import com.github.damontecres.wholphin.util.configure
import com.github.damontecres.wholphin.util.reset
@ -36,11 +36,11 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.jellyfin.sdk.model.UUID
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.Before
import org.junit.Test
@ -58,7 +58,6 @@ class TestMainActivityViewModel {
private val backdropService: BackdropService = mockk(relaxed = true)
private val appUpgradeHandler: AppUpgradeHandler = mockk()
private val intentService: IntentService = mockk()
private val serverDao: JellyfinServerDao = mockk()
val viewModel =
MainActivityViewModel(
@ -74,6 +73,7 @@ class TestMainActivityViewModel {
)
private val serverId = UUID.randomUUID()
private val server = server(serverId)
private val userId = UUID.randomUUID()
private val currentUser = currentUser(serverId, userId)
@ -88,13 +88,6 @@ class TestMainActivityViewModel {
WholphinDispatchers.configure(testDispatcher)
every { appUpgradeHandler.needUpgrade() } returns false
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)
val mockToast = mockk<Toast>()
every { Toast.makeText(any(), any<CharSequence>(), any()) } returns mockToast
@ -121,22 +114,26 @@ class TestMainActivityViewModel {
runTest(testDispatcher) {
setupPreferences {
signInAutomatically = true
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
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)
advanceUntilIdle()
viewModel.handleAppStart()
val slot = slot<SetupDestination>()
verify { setupNavigationManager.navigateTo(capture(slot)) }
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
assertEquals(currentUser, destination.current)
Assert.assertEquals(currentUser, destination.current)
}
coVerify(exactly = 0) { serverRepository.restoreLastSession() }
coVerify(exactly = 0) { serverRepository.getMostRecentServer() }
}
@Test
@ -144,27 +141,27 @@ class TestMainActivityViewModel {
runTest {
setupPreferences {
signInAutomatically = false
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
// coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
coEvery { serverRepository.getMostRecentServer() } returns
RestoredSession.ServerOnly(server)
viewModel.appStart(null)
advanceUntilIdle()
viewModel.handleAppStart()
verify(exactly = 1) { serverDao.getServer(serverId) }
coVerify(exactly = 0) { serverRepository.restoreSession(any(), any()) }
coVerify(exactly = 0) { serverRepository.tryChangeUser(any(), any()) }
coVerify(exactly = 0) { serverRepository.restoreLastSession() }
coVerify(exactly = 1) { serverRepository.getMostRecentServer() }
val args = mutableListOf<SetupDestination>()
verify { setupNavigationManager.navigateTo(capture(args)) }
assertEquals(2, args.size)
Assert.assertEquals(1, args.size)
args[0].let { destination ->
assertTrue("destination is ${destination::class}", destination is SetupDestination.Loading)
}
args[1].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
assertEquals(currentUser.server, destination.server)
Assert.assertEquals(currentUser.server, destination.server)
}
}
@ -173,27 +170,30 @@ class TestMainActivityViewModel {
runTest {
setupPreferences {
signInAutomatically = true
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(protectedCurrentUser)
coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
every { serverRepository.current } returns
MutableStateFlow<CurrentUser?>(
protectedCurrentUser,
)
coEvery {
serverRepository.tryChangeUser(serverId, userId)
} returns null
coEvery { serverRepository.restoreLastSession() } returns
RestoredSession.ServerOnly(server)
viewModel.appStart(null)
advanceUntilIdle()
viewModel.handleAppStart()
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
verify { serverDao.getServer(serverId) }
coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) }
val args = mutableListOf<SetupDestination>()
verify { setupNavigationManager.navigateTo(capture(args)) }
assertEquals(2, args.size)
Assert.assertEquals(1, args.size)
args[0].let { destination ->
assertTrue("destination is ${destination::class}", destination is SetupDestination.Loading)
}
args[1].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
assertEquals(currentUser.server, destination.server)
Assert.assertEquals(currentUser.server, destination.server)
}
}
@ -202,22 +202,26 @@ class TestMainActivityViewModel {
runTest {
setupPreferences {
signInAutomatically = true
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
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)
advanceUntilIdle()
viewModel.handleAppStart()
coVerify(exactly = 1) { serverRepository.restoreSession(serverId, userId) }
coVerify(exactly = 1) { serverRepository.restoreLastSession() }
val slot = slot<SetupDestination>()
verify { setupNavigationManager.navigateTo(capture(slot)) }
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
assertEquals(currentUser.server, destination.server)
Assert.assertEquals(currentUser.server, destination.server)
}
}
@ -226,45 +230,49 @@ class TestMainActivityViewModel {
runTest {
setupPreferences {
signInAutomatically = true
currentServerId = ""
currentUserId = ""
}
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
coEvery { serverRepository.restoreSession(null, null) } returns null
coEvery { serverRepository.restoreLastSession() } returns RestoredSession.None
viewModel.appStart(null)
advanceUntilIdle()
viewModel.handleAppStart()
coVerify(exactly = 1) { serverRepository.restoreSession(null, null) }
coVerify(exactly = 1) { serverRepository.restoreLastSession() }
val slot = slot<SetupDestination>()
verify { setupNavigationManager.navigateTo(capture(slot)) }
slot.captured.let { destination ->
assertTrue("destination is ${destination::class}", destination is SetupDestination.ServerList)
Assert.assertTrue(
"destination is ${destination::class}",
destination is SetupDestination.ServerList,
)
}
}
@Test
fun `Test intent result is noop`() =
fun `Test intent result is noop, hot reload`() =
runTest(testDispatcher) {
setupPreferences {
signInAutomatically = true
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
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
viewModel.appStart(Intent())
advanceUntilIdle()
val result = viewModel.handleIntent(Intent())
assertFalse(result)
val slot = slot<SetupDestination>()
verify { setupNavigationManager.navigateTo(capture(slot)) }
slot.captured.let { destination ->
assertTrue("destination is ${destination::class}", destination is SetupDestination.AppContent)
destination as SetupDestination.AppContent
assertEquals(currentUser, destination.current)
}
// val slot = slot<SetupDestination>()
// verify { setupNavigationManager.navigateTo(capture(slot)) }
// slot.captured.let { destination ->
// Assert.assertTrue(
// "destination is ${destination::class}",
// destination is SetupDestination.AppContent,
// )
// destination as SetupDestination.AppContent
// Assert.assertEquals(currentUser, destination.current)
// }
coVerify(exactly = 0) { serverRepository.restoreLastSession() }
coVerify(exactly = 0) { serverRepository.getMostRecentServer() }
}
@Test
@ -272,22 +280,22 @@ class TestMainActivityViewModel {
runTest(testDispatcher) {
setupPreferences {
signInAutomatically = true
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
every { serverRepository.current } returns MutableStateFlow(currentUser)
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
coEvery { intentService.parseIntent(any()) } returns IntentResult.Error("Error")
viewModel.appStart(Intent())
advanceUntilIdle()
val result = viewModel.handleIntent(Intent())
assertTrue(result)
val slot = slot<SetupDestination>()
verify { setupNavigationManager.navigateTo(capture(slot)) }
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
assertEquals(currentUser.server, destination.server)
Assert.assertEquals(currentUser.server, destination.server)
}
}
@ -296,20 +304,20 @@ class TestMainActivityViewModel {
runTest(testDispatcher) {
setupPreferences {
signInAutomatically = true
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
every { serverRepository.current } returns MutableStateFlow(null)
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
coEvery { intentService.parseIntent(any()) } returns IntentResult.Error("Error")
viewModel.appStart(Intent())
advanceUntilIdle()
val result = viewModel.handleIntent(Intent())
assertTrue(result)
val slot = slot<SetupDestination>()
verify { setupNavigationManager.navigateTo(capture(slot)) }
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) {
setupPreferences {
signInAutomatically = true
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
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
IntentResult.Target(
destinations = listOf(Destination.Favorites),
@ -331,21 +337,27 @@ class TestMainActivityViewModel {
val backstack = mutableListOf<Destination>()
every { navigationManager.backStack } returns backstack
viewModel.appStart(Intent())
advanceUntilIdle()
val result = viewModel.handleIntent(Intent())
assertTrue(result)
val slot = slot<SetupDestination>()
verify { setupNavigationManager.navigateTo(capture(slot)) }
verify { navigationManager.reloadHome() }
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
assertEquals(currentUser, destination.current)
Assert.assertEquals(currentUser, destination.current)
}
// 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 ->
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) {
setupPreferences {
signInAutomatically = true
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
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
IntentResult.Target(
destinations = listOf(Destination.Favorites),
@ -367,20 +377,26 @@ class TestMainActivityViewModel {
val backstack = mutableListOf<Destination>()
every { navigationManager.backStack } returns backstack
viewModel.appStart(Intent())
advanceUntilIdle()
val result = viewModel.handleIntent(Intent())
assertTrue(result)
val slot = slot<SetupDestination>()
verify { setupNavigationManager.navigateTo(capture(slot)) }
verify(exactly = 0) { navigationManager.reloadHome() }
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
assertEquals(currentUser, destination.current)
Assert.assertEquals(currentUser, destination.current)
}
assertEquals(1, backstack.size)
Assert.assertEquals(1, backstack.size)
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) {
setupPreferences {
signInAutomatically = true
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
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
IntentResult.Target(
destinations = listOf(Destination.Favorites),
@ -402,14 +416,17 @@ class TestMainActivityViewModel {
val backstack = mutableListOf<Destination>()
every { navigationManager.backStack } returns backstack
viewModel.appStart(Intent())
advanceUntilIdle()
val result = viewModel.handleIntent(Intent())
assertTrue(result)
val slot = slot<SetupDestination>()
verify { setupNavigationManager.navigateTo(capture(slot)) }
verify(exactly = 0) { navigationManager.reloadHome() }
slot.captured.let { destination ->
assertTrue("destination is ${destination::class}", destination is SetupDestination.ServerList)
Assert.assertTrue(
"destination is ${destination::class}",
destination is SetupDestination.ServerList,
)
}
}
}

View file

@ -3,7 +3,7 @@ package com.github.damontecres.wholphin.services
import android.app.SearchManager
import android.content.Intent
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.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences
@ -20,7 +20,6 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest
@ -42,7 +41,6 @@ import org.robolectric.annotation.Config
class IntentServiceTest {
private val api: ApiClient = mockk()
private val serverRepository: ServerRepository = mockk()
private val serverDao: JellyfinServerDao = mockk()
private val userPreferencesService: UserPreferencesService = mockk()
private val userLibraryApi: UserLibraryApi = mockk()
@ -64,7 +62,6 @@ class IntentServiceTest {
intentService = IntentService(api, serverRepository, userPreferencesService)
every { api.userLibraryApi } returns userLibraryApi
coEvery { userLibraryApi.getItem(itemId) } returns successResponse(movie(itemId))
every { serverRepository.serverDao } returns serverDao
}
private fun setupPreferences(block: AppPreferences.Builder.() -> Unit) {
@ -82,11 +79,11 @@ class IntentServiceTest {
private fun setupAutoSignInUnprotected() {
setupPreferences {
signInAutomatically = true
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
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
@ -98,7 +95,7 @@ class IntentServiceTest {
val result = intentService.prepare(intent)
assertNull(result)
coVerify { serverRepository.restoreSession(serverId, userId) }
coVerify { serverRepository.restoreLastSession() }
}
@Test
@ -106,17 +103,17 @@ class IntentServiceTest {
runTest {
setupPreferences {
signInAutomatically = false
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
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 result = intentService.prepare(intent)
assertTrue(result is IntentResult.Error)
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) }
}
@Test
@ -124,17 +121,17 @@ class IntentServiceTest {
runTest {
setupPreferences {
signInAutomatically = true
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
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 result = intentService.prepare(intent)
assertTrue(result is IntentResult.Error)
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) }
}
@Test
@ -142,17 +139,17 @@ class IntentServiceTest {
runTest {
setupPreferences {
signInAutomatically = true
currentServerId = serverId.toServerString()
currentUserId = userId.toServerString()
}
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 result = intentService.prepare(intent)
assertTrue(result is IntentResult.Error)
coVerify(exactly = 1) { serverRepository.restoreSession(serverId, userId) }
coVerify(exactly = 1) { serverRepository.restoreLastSession() }
}
@Test
@ -160,17 +157,16 @@ class IntentServiceTest {
runTest {
setupPreferences {
signInAutomatically = true
currentServerId = ""
currentUserId = ""
}
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
coEvery { serverRepository.restoreLastSession() } returns RestoredSession.None
// coEvery { serverRepository.restoreSession(serverId, userId) } returns protectedCurrentUser
val intent = Intent()
val result = intentService.prepare(intent)
assertTrue(result is IntentResult.Error)
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) }
}
@Test
@ -178,12 +174,11 @@ class IntentServiceTest {
runTest {
setupPreferences {
signInAutomatically = true
currentServerId = ""
currentUserId = ""
}
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
coEvery { serverRepository.serverDao.getUser(serverId, userId) } returns currentUser.user
coEvery { serverRepository.restoreSession(serverId, userId) } returns currentUser
coEvery { serverRepository.tryChangeUser(serverId, userId) } returns currentUser
coEvery { serverRepository.restoreLastSession() } returns
RestoredSession.Success(currentUser)
val intent =
Intent().apply {
@ -193,7 +188,7 @@ class IntentServiceTest {
val result = intentService.prepare(intent)
assertNull(result)
coVerify(exactly = 1) { serverRepository.restoreSession(serverId, userId) }
coVerify(exactly = 1) { serverRepository.tryChangeUser(serverId, userId) }
}
@Test
@ -201,12 +196,11 @@ class IntentServiceTest {
runTest {
setupPreferences {
signInAutomatically = true
currentServerId = ""
currentUserId = ""
}
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
coEvery { serverRepository.serverDao.getUser(serverId, userId) } returns protectedCurrentUser.user
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().apply {
@ -216,7 +210,7 @@ class IntentServiceTest {
val result = intentService.prepare(intent)
assertTrue(result is IntentResult.Error)
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
coVerify(exactly = 1) { serverRepository.tryChangeUser(serverId, userId) }
}
@Test
@ -224,12 +218,9 @@ class IntentServiceTest {
runTest {
setupPreferences {
signInAutomatically = true
currentServerId = ""
currentUserId = ""
}
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(null)
coEvery { serverRepository.serverDao.getUser(serverId, userId) } returns null
coEvery { serverRepository.restoreSession(serverId, userId) } returns null
coEvery { serverRepository.tryChangeUser(serverId, userId) } returns null
val intent =
Intent().apply {
@ -239,7 +230,7 @@ class IntentServiceTest {
val result = intentService.prepare(intent)
assertTrue(result is IntentResult.Error)
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
coVerify(exactly = 1) { serverRepository.tryChangeUser(serverId, userId) }
}
@Test
@ -296,12 +287,11 @@ class IntentServiceTest {
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
coEvery {
serverRepository.restoreSession(
serverRepository.tryChangeUser(
newServerId,
newUserId,
)
} returns newCurrentUser
every { serverDao.getUser(newServerId, newUserId) } returns newCurrentUser.user
val intent =
Intent().apply {
@ -316,9 +306,8 @@ class IntentServiceTest {
assertEquals(0, destinations.size)
assertTrue(result.addHomeToBackStack)
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
verify { serverDao.getUser(newServerId, newUserId) }
coVerify { serverRepository.restoreSession(newServerId, newUserId) }
coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) }
coVerify { serverRepository.tryChangeUser(newServerId, newUserId) }
coVerify(exactly = 0) { userLibraryApi.getItem(itemId) }
}
@ -333,12 +322,11 @@ class IntentServiceTest {
every { serverRepository.current } returns MutableStateFlow<CurrentUser?>(currentUser)
coEvery {
serverRepository.restoreSession(
serverRepository.tryChangeUser(
newServerId,
newUserId,
)
} returns newCurrentUser
every { serverDao.getUser(newServerId, newUserId) } returns newCurrentUser.user
val intent =
Intent().apply {
@ -358,9 +346,8 @@ class IntentServiceTest {
assertEquals(itemId, first.itemId)
coVerify(exactly = 0) { serverRepository.restoreSession(serverId, userId) }
verify { serverDao.getUser(newServerId, newUserId) }
coVerify { serverRepository.restoreSession(newServerId, newUserId) }
coVerify(exactly = 0) { serverRepository.tryChangeUser(serverId, userId) }
coVerify { serverRepository.tryChangeUser(newServerId, newUserId) }
coVerify { userLibraryApi.getItem(itemId) }
}

View file

@ -1,11 +1,13 @@
package com.github.damontecres.wholphin.services
import android.content.Context
import android.content.SharedPreferences
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import com.github.damontecres.wholphin.data.CurrentUser
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.model.JellyfinServer
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.AppPreferencesSerializer
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.toServerString
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 io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
@ -29,7 +33,6 @@ import io.mockk.verify
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.StandardTestDispatcher
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.junit.After
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ -80,14 +85,13 @@ class ServerRepositoryTest {
private val mockUserApi = mockk<UserApi>()
private val mockSystemApi = mockk<SystemApi>()
private val mockSharedPreferences = mockk<SharedPreferences>(relaxed = true)
private val mockMostRecentServerProvider = mockk<MostRecentServerProvider>()
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setup() {
WholphinDispatchers.configure(testDispatcher)
every { mockContext.getSharedPreferences(any(), any()) } returns mockSharedPreferences
every { mockApiClient.userApi } returns mockUserApi
every { mockApiClient.systemApi } returns mockSystemApi
coEvery { mockUserApi.getCurrentUser() } returns successResponse(userDto)
@ -102,9 +106,15 @@ class ServerRepositoryTest {
emptyMap(),
)
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.deviceInfo } returns DeviceInfo("Wholphin test ID", "Wholphin test device")
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)
@ -119,8 +129,8 @@ class ServerRepositoryTest {
mockJellyfin,
mockJellyfinServerDao,
mockApiClient,
dataStore,
testDispatcher,
mockMostRecentServerProvider,
)
private val serverId = UUID.randomUUID()
@ -194,24 +204,24 @@ class ServerRepositoryTest {
Assert.assertEquals(user, serverRepository.currentUser)
Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
val appPreferences = dataStore.data.first()
Assert.assertEquals(serverId.toServerString(), appPreferences.currentServerId)
Assert.assertEquals(userId.toServerString(), appPreferences.currentUserId)
verify(exactly = 1) { mockSharedPreferences.edit() }
coVerify(exactly = 1) { mockMostRecentServerProvider.save(CurrentUser(server, user)) }
verify(exactly = 1) { mockJellyfinServerDao.addOrUpdateUser(user) }
}
@Test
fun `Test restoreUser via changeUser`() =
runTest {
val user = user.copy(pin = null)
every { mockJellyfinServerDao.addOrUpdateServer(any()) } just Runs
every { mockJellyfinServerDao.addOrUpdateUser(user) } returns user
every { mockJellyfinServerDao.getServer(serverId) } returns JellyfinServerUsers(server, listOf(user))
val serverRepository = create()
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) {
mockApiClient.update(
@ -226,34 +236,6 @@ class ServerRepositoryTest {
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
fun `Test remove user`() =
runTest {
@ -308,6 +290,7 @@ class ServerRepositoryTest {
)
}
verify(exactly = 1) { mockJellyfinServerDao.deleteUser(serverId, userId) }
coVerify { mockMostRecentServerProvider.clearUser() }
}
@Test
@ -351,6 +334,7 @@ class ServerRepositoryTest {
)
}
verify(exactly = 1) { mockJellyfinServerDao.deleteServer(serverId) }
coVerify { mockMostRecentServerProvider.clear() }
}
@Test
@ -387,11 +371,7 @@ class ServerRepositoryTest {
Assert.assertEquals(user, serverRepository.currentUser)
Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
val appPreferences = dataStore.data.first()
Assert.assertEquals(serverId.toServerString(), appPreferences.currentServerId)
Assert.assertEquals(userId.toServerString(), appPreferences.currentUserId)
verify(exactly = 1) { mockSharedPreferences.edit() }
coVerify(exactly = 1) { mockMostRecentServerProvider.save(CurrentUser(server, user)) }
}
@Test
@ -430,11 +410,7 @@ class ServerRepositoryTest {
Assert.assertEquals(user, serverRepository.currentUser)
Assert.assertEquals(serverUserConfig, serverRepository.currentUserDto)
val appPreferences = dataStore.data.first()
Assert.assertEquals(serverId.toServerString(), appPreferences.currentServerId)
Assert.assertEquals(userId.toServerString(), appPreferences.currentUserId)
verify(exactly = 1) { mockSharedPreferences.edit() }
coVerify(exactly = 1) { mockMostRecentServerProvider.save(CurrentUser(server, user)) }
}
@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)
}
}
}

View file

@ -5,8 +5,10 @@ import androidx.work.Data
import androidx.work.ListenableWorker
import androidx.work.WorkerParameters
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.preferences.AppPreferences
import com.github.damontecres.wholphin.test.currentUser
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import io.mockk.coEvery
import io.mockk.coVerify
@ -113,16 +115,16 @@ class SuggestionsWorkerTest {
val mockUser = mockk<CurrentUser>()
var restored = false
every { mockServerRepository.current } answers { MutableStateFlow(if (restored) mockUser else null) }
coEvery { mockServerRepository.restoreSession(testServerId, testUserId) } coAnswers {
coEvery { mockServerRepository.restoreLastSession() } coAnswers {
restored = true
mockUser
RestoredSession.Success(currentUser())
}
every { mockPreferences.data } returns flowOf(mockPrefs())
coEvery { mockUserViewsApi.getUserViews(userId = testUserId) } returns mockQueryResult()
val result = createWorker().doWork()
coVerify { mockServerRepository.restoreSession(testServerId, testUserId) }
coVerify { mockServerRepository.restoreLastSession() }
assertEquals(ListenableWorker.Result.success(), result)
}

View file

@ -17,6 +17,7 @@ import com.github.damontecres.wholphin.data.ItemPlaybackDao
import com.github.damontecres.wholphin.data.JellyfinServerDao
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
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.PlaybackLanguageChoiceDao
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.AppPreferencesSerializer
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.AuthOkHttpClient
import com.github.damontecres.wholphin.services.hilt.DatabaseModule
@ -201,6 +203,10 @@ object TestModule {
fun seerrApi(
@StandardOkHttpClient okHttpClient: OkHttpClient,
): SeerrApi = mockk()
@Provides
@Singleton
fun mostRecentServerProvider(): MostRecentServerProvider = TestMostRecentServerProvider()
}
@Module