Add setting to protect a user profile with a PIN (#356)

Allows for setting a per-user profile PIN that must be entered to switch
to the user. The PIN is a sequence of directional buttons, ie D-Pad
left/right/up/down.

This PIN is per device per user per server. The PIN is stored locally
and not sent nor shared with the server. If you forget your PIN, can
still access the app using server credentials which also removes the
PIN.

Additionally, there is another setting to disable automatically sign in.
When enabled (default) the last user is automatically restored, even if
there is a PIN. If disabled, a server & user must always be selected
when opening the app.

Closes #268
Closes #321
This commit is contained in:
damontecres 2025-12-04 14:37:27 -05:00 committed by GitHub
parent a294661520
commit 8ea84b3efe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 845 additions and 30 deletions

View file

@ -1,6 +1,7 @@
package com.github.damontecres.wholphin
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.background
@ -9,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@ -42,6 +44,7 @@ import com.github.damontecres.wholphin.services.UpdateChecker
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
import com.github.damontecres.wholphin.ui.CoilConfig
import com.github.damontecres.wholphin.ui.LocalImageUrlService
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.ApplicationContent
import com.github.damontecres.wholphin.ui.nav.Destination
@ -49,6 +52,8 @@ import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.util.DebugLogTree
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
@ -129,13 +134,15 @@ class MainActivity : AppCompatActivity() {
) {
var isRestoringSession by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
try {
serverRepository.restoreSession(
appPreferences.currentServerId?.toUUIDOrNull(),
appPreferences.currentUserId?.toUUIDOrNull(),
)
} catch (ex: Exception) {
Timber.e(ex, "Exception restoring session")
if (appPreferences.signInAutomatically) {
try {
serverRepository.restoreSession(
appPreferences.currentServerId?.toUUIDOrNull(),
appPreferences.currentUserId?.toUUIDOrNull(),
)
} catch (ex: Exception) {
Timber.e(ex, "Exception restoring session")
}
}
isRestoringSession = false
}
@ -158,12 +165,30 @@ class MainActivity : AppCompatActivity() {
)
}
} else {
key(current) {
val initialDestination =
if (current != null) {
Destination.Home()
DisposableEffect(Unit) {
onDispose {
if (!appPreferences.signInAutomatically) {
serverRepository.closeSession()
}
}
}
key(current?.server?.id, current?.user?.id) {
LaunchedEffect(current?.user?.pin) {
if (current?.user?.pin?.isNotNullOrBlank() == true) {
// If user has a pin, then obscure the window in previews
window?.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
} else {
Destination.ServerList
window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
val initialDestination =
when {
current != null -> Destination.Home()
!appPreferences.signInAutomatically -> Destination.ServerList // TODO user list?
else -> Destination.ServerList
}
val backStack = rememberNavBackStack(initialDestination)
navigationManager.backStack = backStack
@ -206,4 +231,14 @@ class MainActivity : AppCompatActivity() {
appUpgradeHandler.run()
}
}
override fun onRestart() {
super.onRestart()
val signInAutomatically =
runBlocking { userPreferencesDataStore.data.firstOrNull()?.signInAutomatically } ?: true
Timber.i("onRestart: signInAutomatically=$signInAutomatically")
if (!signInAutomatically) {
serverRepository.closeSession()
}
}
}