From f107e79b72633f02d824289235cf1d8930cf44a2 Mon Sep 17 00:00:00 2001
From: Jarne Demeulemeester
<32322857+jarnedemeulemeester@users.noreply.github.com>
Date: Sat, 14 Jan 2023 18:21:42 +0100
Subject: [PATCH] Log in with Quick Connect (#234)
* Log in with Quick Connect
* Clean up LoginViewModel
* Cancel Quick Connect by tapping the button again
* Make quickConnectJob private
---
.../jellyfin/fragments/LoginFragment.kt | 33 +++++++
.../src/main/res/layout/fragment_login.xml | 25 ++++++
.../jellyfin/viewmodels/LoginViewModel.kt | 88 +++++++++++++++----
core/src/main/res/values/strings.xml | 1 +
.../dev/jdtech/jellyfin/api/JellyfinApi.kt | 2 +
5 files changed, 134 insertions(+), 15 deletions(-)
diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/LoginFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/LoginFragment.kt
index b8f2bc3f..01d36218 100644
--- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/LoginFragment.kt
+++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/LoginFragment.kt
@@ -1,10 +1,12 @@
package dev.jdtech.jellyfin.fragments
import android.os.Bundle
+import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
+import androidx.annotation.ColorInt
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
@@ -19,6 +21,7 @@ import dev.jdtech.jellyfin.adapters.UserLoginListAdapter
import dev.jdtech.jellyfin.database.ServerDatabaseDao
import dev.jdtech.jellyfin.databinding.FragmentLoginBinding
import dev.jdtech.jellyfin.AppPreferences
+import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.viewmodels.LoginViewModel
import javax.inject.Inject
import kotlinx.coroutines.launch
@@ -66,6 +69,10 @@ class LoginFragment : Fragment() {
login()
}
+ binding.buttonQuickconnect.setOnClickListener {
+ viewModel.useQuickConnect()
+ }
+
binding.usersRecyclerView.adapter = UserLoginListAdapter { user ->
(binding.editTextUsername as AppCompatEditText).setText(user.name)
(binding.editTextPassword as AppCompatEditText).requestFocus()
@@ -95,6 +102,32 @@ class LoginFragment : Fragment() {
}
}
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.quickConnectUiState.collect { quickConnectUiState ->
+ when (quickConnectUiState) {
+ is LoginViewModel.QuickConnectUiState.Disabled -> {
+ binding.buttonQuickconnectLayout.isVisible = false
+ }
+ is LoginViewModel.QuickConnectUiState.Normal -> {
+ binding.buttonQuickconnectLayout.isVisible = true
+ binding.buttonQuickconnect.text = resources.getString(R.string.quick_connect)
+ val typedValue = TypedValue()
+ requireActivity().theme.resolveAttribute(R.attr.colorPrimary, typedValue, true)
+ @ColorInt val textColor: Int = typedValue.data
+ binding.buttonQuickconnect.setTextColor(textColor)
+ binding.buttonQuickconnectProgress.isVisible = false
+ }
+ is LoginViewModel.QuickConnectUiState.Waiting -> {
+ binding.buttonQuickconnect.text = quickConnectUiState.code
+ binding.buttonQuickconnect.setTextColor(resources.getColor(android.R.color.white, requireActivity().theme))
+ binding.buttonQuickconnectProgress.isVisible = true
+ }
+ }
+ }
+ }
+ }
+
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.navigateToMain.collect {
diff --git a/app/phone/src/main/res/layout/fragment_login.xml b/app/phone/src/main/res/layout/fragment_login.xml
index 794f1ba4..a34af782 100644
--- a/app/phone/src/main/res/layout/fragment_login.xml
+++ b/app/phone/src/main/res/layout/fragment_login.xml
@@ -116,6 +116,31 @@
android:visibility="invisible" />
+
+
+
+
+
+
+
diff --git a/core/src/main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt b/core/src/main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt
index 2820312e..d56085d5 100644
--- a/core/src/main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt
+++ b/core/src/main/java/dev/jdtech/jellyfin/viewmodels/LoginViewModel.kt
@@ -12,13 +12,17 @@ import dev.jdtech.jellyfin.AppPreferences
import javax.inject.Inject
import kotlin.Exception
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.jellyfin.sdk.api.client.extensions.authenticateWithQuickConnect
import org.jellyfin.sdk.model.api.AuthenticateUserByName
+import org.jellyfin.sdk.model.api.AuthenticationResult
@HiltViewModel
class LoginViewModel
@@ -32,9 +36,13 @@ constructor(
val uiState = _uiState.asStateFlow()
private val _usersState = MutableStateFlow(UsersState.Loading)
val usersState = _usersState.asStateFlow()
+ private val _quickConnectUiState = MutableStateFlow(QuickConnectUiState.Disabled)
+ val quickConnectUiState = _quickConnectUiState.asStateFlow()
private val _navigateToMain = MutableSharedFlow()
val navigateToMain = _navigateToMain.asSharedFlow()
+ private var quickConnectJob: Job? = null
+
sealed class UiState {
object Normal : UiState()
object Loading : UiState()
@@ -46,8 +54,15 @@ constructor(
data class Users(val users: List) : UsersState()
}
+ sealed class QuickConnectUiState {
+ object Disabled : QuickConnectUiState()
+ object Normal : QuickConnectUiState()
+ data class Waiting(val code: String) : QuickConnectUiState()
+ }
+
init {
loadPublicUsers()
+ loadQuickConnectAvailable()
}
private fun loadPublicUsers() {
@@ -74,6 +89,17 @@ constructor(
}
}
+ private fun loadQuickConnectAvailable() {
+ viewModelScope.launch {
+ try {
+ val isEnabled by jellyfinApi.quickConnectApi.getEnabled()
+ if (isEnabled) {
+ _quickConnectUiState.emit(QuickConnectUiState.Normal)
+ }
+ } catch (_: Exception) {}
+ }
+ }
+
/**
* Send a authentication request to the Jellyfin server
*
@@ -92,21 +118,7 @@ constructor(
)
)
- val serverInfo by jellyfinApi.systemApi.getPublicSystemInfo()
-
- val user = User(
- id = authenticationResult.user!!.id,
- name = authenticationResult.user!!.name!!,
- serverId = serverInfo.id!!,
- accessToken = authenticationResult.accessToken!!
- )
-
- insertUser(appPreferences.currentServer!!, user)
-
- jellyfinApi.apply {
- api.accessToken = authenticationResult.accessToken
- userId = authenticationResult.user?.id
- }
+ saveAuthenticationResult(authenticationResult)
_uiState.emit(UiState.Normal)
_navigateToMain.emit(true)
@@ -120,6 +132,52 @@ constructor(
}
}
+ fun useQuickConnect() {
+ if (quickConnectJob != null && quickConnectJob!!.isActive) {
+ quickConnectJob!!.cancel()
+ return
+ }
+ quickConnectJob = viewModelScope.launch {
+ try {
+ var quickConnectState = jellyfinApi.quickConnectApi.initiate().content
+ _quickConnectUiState.emit(QuickConnectUiState.Waiting(quickConnectState.code))
+
+ while (!quickConnectState.authenticated) {
+ quickConnectState = jellyfinApi.quickConnectApi.connect(quickConnectState.secret).content
+ delay(5000L)
+ }
+ val authenticationResult by jellyfinApi.userApi.authenticateWithQuickConnect(
+ secret = quickConnectState.secret
+ )
+
+ saveAuthenticationResult(authenticationResult)
+
+ _quickConnectUiState.emit(QuickConnectUiState.Normal)
+ _navigateToMain.emit(true)
+ } catch (_: Exception) {
+ _quickConnectUiState.emit(QuickConnectUiState.Normal)
+ }
+ }
+ }
+
+ private suspend fun saveAuthenticationResult(authenticationResult: AuthenticationResult) {
+ val serverInfo by jellyfinApi.systemApi.getPublicSystemInfo()
+
+ val user = User(
+ id = authenticationResult.user!!.id,
+ name = authenticationResult.user!!.name!!,
+ serverId = serverInfo.id!!,
+ accessToken = authenticationResult.accessToken!!
+ )
+
+ insertUser(appPreferences.currentServer!!, user)
+
+ jellyfinApi.apply {
+ api.accessToken = authenticationResult.accessToken
+ userId = authenticationResult.user?.id
+ }
+ }
+
private suspend fun insertUser(serverId: String, user: User) {
withContext(Dispatchers.IO) {
database.insertUser(user)
diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml
index 7bfff49e..7e7cbda5 100644
--- a/core/src/main/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
@@ -147,4 +147,5 @@
Add address
Add server address
Add
+ Quick Connect
\ No newline at end of file
diff --git a/data/src/main/java/dev/jdtech/jellyfin/api/JellyfinApi.kt b/data/src/main/java/dev/jdtech/jellyfin/api/JellyfinApi.kt
index 10c38d57..8300ad8b 100644
--- a/data/src/main/java/dev/jdtech/jellyfin/api/JellyfinApi.kt
+++ b/data/src/main/java/dev/jdtech/jellyfin/api/JellyfinApi.kt
@@ -9,6 +9,7 @@ import org.jellyfin.sdk.api.client.extensions.devicesApi
import org.jellyfin.sdk.api.client.extensions.itemsApi
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
import org.jellyfin.sdk.api.client.extensions.playStateApi
+import org.jellyfin.sdk.api.client.extensions.quickConnectApi
import org.jellyfin.sdk.api.client.extensions.sessionApi
import org.jellyfin.sdk.api.client.extensions.systemApi
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
@@ -57,6 +58,7 @@ class JellyfinApi(
val videosApi = api.videosApi
val mediaInfoApi = api.mediaInfoApi
val playStateApi = api.playStateApi
+ val quickConnectApi = api.quickConnectApi
companion object {
@Volatile