Add user PIN protection (#719)

## Description
Adds (back) the ability to set a per-user PIN. This is required to
switch to the user. If the user has a PIN, this overrides and ignores
the "Sign in automatically" setting for that user.

The PIN consists of any combination of D-Pad directional buttons. If you
forget the PIN, you can click the button to login via the server instead
(ie QuickConnect or username/password) which will also remove the PIN so
you can set a new one if desired.

### Related issues
Closes #268 
Replaces #398
Related to #321 & #356

### Screenshots
![enter_pin
Large](https://github.com/user-attachments/assets/aa25364d-1266-410d-846e-8dc32c56a794)
This commit is contained in:
Ray 2026-01-19 11:39:29 -05:00 committed by GitHub
parent 946043342a
commit 0f52616d21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 20 additions and 21 deletions

View file

@ -3,6 +3,7 @@ package com.github.damontecres.wholphin
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.view.WindowManager
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -138,6 +139,16 @@ class MainActivity : AppCompatActivity() {
window.attributes = attrs.apply { preferredDisplayModeId = modeId } window.attributes = attrs.apply { preferredDisplayModeId = modeId }
} }
} }
viewModel.serverRepository.currentUser.observe(this) { user ->
if (user?.hasPin == true) {
window?.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
} else {
window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
viewModel.appStart() viewModel.appStart()
val requestedDestination = this.intent?.let(::extractDestination) val requestedDestination = this.intent?.let(::extractDestination)
setContent { setContent {
@ -292,14 +303,6 @@ class MainActivity : AppCompatActivity() {
super.onRestart() super.onRestart()
Timber.d("onRestart") Timber.d("onRestart")
viewModel.appStart() viewModel.appStart()
// val signInAutomatically =
// runBlocking { userPreferencesDataStore.data.firstOrNull()?.signInAutomatically } ?: true
// // TODO PIN-related
// // if (!signInAutomatically || serverRepository.currentUser.value?.hasPin == true) {
// if (!signInAutomatically) {
// serverRepository.closeSession()
// }
} }
override fun onStop() { override fun onStop() {
@ -392,7 +395,7 @@ class MainActivityViewModel
@Inject @Inject
constructor( constructor(
private val preferences: DataStore<AppPreferences>, private val preferences: DataStore<AppPreferences>,
private val serverRepository: ServerRepository, val serverRepository: ServerRepository,
private val navigationManager: SetupNavigationManager, private val navigationManager: SetupNavigationManager,
private val deviceProfileService: DeviceProfileService, private val deviceProfileService: DeviceProfileService,
private val backdropService: BackdropService, private val backdropService: BackdropService,
@ -402,7 +405,8 @@ class MainActivityViewModel
try { try {
val prefs = val prefs =
preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance() preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
if (prefs.signInAutomatically) { val userHasPin = serverRepository.currentUser.value?.hasPin == true
if (prefs.signInAutomatically && !userHasPin) {
val current = val current =
serverRepository.restoreSession( serverRepository.restoreSession(
prefs.currentServerId?.toUUIDOrNull(), prefs.currentServerId?.toUUIDOrNull(),

View file

@ -142,8 +142,6 @@ class ServerRepository
} else { } else {
val user = serverAndUsers.users.firstOrNull { it.id == userId } val user = serverAndUsers.users.firstOrNull { it.id == userId }
if (user != null) { if (user != null) {
// TODO pin-related
// if (user != null && !user.hasPin) {
return changeUser(serverAndUsers.server, user) return changeUser(serverAndUsers.server, user)
} }
} }

View file

@ -932,8 +932,7 @@ val basicPreferences =
title = R.string.profile_specific_settings, title = R.string.profile_specific_settings,
preferences = preferences =
listOf( listOf(
// TODO PIN-related AppPreference.RequireProfilePin,
// AppPreference.RequireProfilePin,
AppPreference.UserPinnedNavDrawerItems, AppPreference.UserPinnedNavDrawerItems,
), ),
), ),

View file

@ -118,13 +118,11 @@ fun SwitchUserContent(
users = users, users = users,
currentUser = currentUser, currentUser = currentUser,
onSwitchUser = { user -> onSwitchUser = { user ->
// TODO PIN-related if (user.hasPin) {
// if (user.pin.isNotNullOrBlank()) { switchUserWithPin = user
// switchUserWithPin = user } else {
// } else {
// viewModel.switchUser(user)
// }
viewModel.switchUser(user) viewModel.switchUser(user)
}
}, },
onAddUser = { showAddUser = true }, onAddUser = { showAddUser = true },
onRemoveUser = { user -> onRemoveUser = { user ->