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.res.Configuration
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
@ -138,6 +139,16 @@ class MainActivity : AppCompatActivity() {
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()
val requestedDestination = this.intent?.let(::extractDestination)
setContent {
@ -292,14 +303,6 @@ class MainActivity : AppCompatActivity() {
super.onRestart()
Timber.d("onRestart")
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() {
@ -392,7 +395,7 @@ class MainActivityViewModel
@Inject
constructor(
private val preferences: DataStore<AppPreferences>,
private val serverRepository: ServerRepository,
val serverRepository: ServerRepository,
private val navigationManager: SetupNavigationManager,
private val deviceProfileService: DeviceProfileService,
private val backdropService: BackdropService,
@ -402,7 +405,8 @@ class MainActivityViewModel
try {
val prefs =
preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
if (prefs.signInAutomatically) {
val userHasPin = serverRepository.currentUser.value?.hasPin == true
if (prefs.signInAutomatically && !userHasPin) {
val current =
serverRepository.restoreSession(
prefs.currentServerId?.toUUIDOrNull(),

View file

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

View file

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

View file

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