Merge pull request #1179 from yogeshpaliyal/addOptionTODisableAutoLock

feat: add option to disable auto lock
This commit is contained in:
Yogesh Choudhary Paliyal
2025-04-25 23:37:51 +05:30
committed by GitHub
4 changed files with 305 additions and 323 deletions

View File

@@ -79,137 +79,129 @@ val LocalUserSettings = compositionLocalOf { UserSettings() }
@AndroidEntryPoint
class DashboardComposeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
if (BuildConfig.DEBUG.not()) {
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
}
setContent {
val localUserSettings by getUserSettingsFlow().collectAsState(initial = UserSettings())
CompositionLocalProvider(LocalUserSettings provides localUserSettings) {
KeyPassTheme {
StoreProvider(store = KeyPassRedux.createStore()) {
Dashboard()
}
}
}
LaunchedEffect(key1 = Unit, block = {
migrateOldDataToNewerDataStore()
val userSettings = getUserSettings()
val buildConfigVersion = BuildConfig.VERSION_CODE
val currentAppVersion = userSettings.currentAppVersion
if (buildConfigVersion != currentAppVersion) {
applicationContext.setUserSettings(
userSettings.copy(
lastAppVersion = currentAppVersion,
currentAppVersion = buildConfigVersion
)
)
}
})
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
if (BuildConfig.DEBUG.not()) {
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
}
setContent {
val localUserSettings by getUserSettingsFlow().collectAsState(initial = UserSettings())
CompositionLocalProvider(LocalUserSettings provides localUserSettings) {
KeyPassTheme { StoreProvider(store = KeyPassRedux.createStore()) { Dashboard() } }
}
LaunchedEffect(
key1 = Unit,
block = {
migrateOldDataToNewerDataStore()
val userSettings = getUserSettings()
val buildConfigVersion = BuildConfig.VERSION_CODE
val currentAppVersion = userSettings.currentAppVersion
if (buildConfigVersion != currentAppVersion) {
applicationContext.setUserSettings(
userSettings.copy(
lastAppVersion = currentAppVersion, currentAppVersion = buildConfigVersion))
}
})
}
}
}
@Composable
fun Dashboard() {
val systemBackPress by selectState<KeyPassState, Boolean> { this.systemBackPress }
val systemBackPress by selectState<KeyPassState, Boolean> { this.systemBackPress }
val context = LocalContext.current
val dispatch = rememberDispatcher()
val context = LocalContext.current
val userSettings = LocalUserSettings.current
val dispatch = rememberDispatcher()
BackHandler(!systemBackPress) {
dispatch(GoBackAction)
BackHandler(!systemBackPress) { dispatch(GoBackAction) }
// Call this like any other SideEffect in your composable
LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) {
if (userSettings.autoLockEnabled == true &&
(context.applicationContext as? MyApplication)?.isActivityLaunchTriggered() == false) {
dispatch(NavigationAction(AuthState.Login))
}
}
// Call this like any other SideEffect in your composable
LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) {
if((context.applicationContext as? MyApplication)?.isActivityLaunchTriggered() == false) {
dispatch(NavigationAction(AuthState.Login))
}
}
LaunchedEffect(key1 = systemBackPress, block = {
LaunchedEffect(
key1 = systemBackPress,
block = {
if (systemBackPress) {
(context as? ComponentActivity)?.finishAffinity()
(context as? ComponentActivity)?.finishAffinity()
}
})
})
DisposableEffect(KeyPassRedux, context) {
dispatch(UpdateContextAction(context))
onDispose {
dispatch(UpdateContextAction(null))
}
}
Scaffold(bottomBar = {
KeyPassBottomBar()
}, modifier = Modifier.safeDrawingPadding()) { paddingValues ->
Surface(modifier = Modifier.padding(paddingValues)) {
CurrentPage()
DashboardBottomSheet()
}
DisposableEffect(KeyPassRedux, context) {
dispatch(UpdateContextAction(context))
onDispose { dispatch(UpdateContextAction(null)) }
}
Scaffold(bottomBar = { KeyPassBottomBar() }, modifier = Modifier.safeDrawingPadding()) {
paddingValues ->
Surface(modifier = Modifier.padding(paddingValues)) {
CurrentPage()
DashboardBottomSheet()
}
}
}
@Composable
fun CurrentPage() {
val currentScreen by selectState<KeyPassState, KeyPassState> { this }
val currentScreen by selectState<KeyPassState, KeyPassState> { this }
// val currentDialog by selectState<KeyPassState, DialogState?> { this.dialog }
// val currentDialog by selectState<KeyPassState, DialogState?> { this.dialog }
currentScreen.currentScreen.let {
when (it) {
is HomeState -> {
Homepage(homeState = it)
}
currentScreen.currentScreen.let {
when (it) {
is HomeState -> {
Homepage(homeState = it)
}
is SettingsState -> {
MySettingCompose()
}
is SettingsState -> {
MySettingCompose()
}
is AccountDetailState -> {
AccountDetailPage(it.accountId)
}
is AccountDetailState -> {
AccountDetailPage(it.accountId)
}
is AuthState -> {
AuthScreen(it)
}
is AuthState -> {
AuthScreen(it)
}
is BackupScreenState -> {
BackupScreen(state = it)
}
is BackupScreenState -> {
BackupScreen(state = it)
}
is ChangeAppPasswordState -> {
ChangePassword(it)
}
is ChangeAppPasswordState -> {
ChangePassword(it)
}
is ChangeDefaultPasswordLengthState -> {
ChangeDefaultPasswordLengthScreen()
}
is ChangeDefaultPasswordLengthState -> {
ChangeDefaultPasswordLengthScreen()
}
is BackupImporterState -> BackupImporter(state = it)
is AboutState -> AboutScreen()
is PasswordGeneratorState -> GeneratePasswordScreen()
is ChangeAppHintState -> PasswordHintScreen()
}
is BackupImporterState -> BackupImporter(state = it)
is AboutState -> AboutScreen()
is PasswordGeneratorState -> GeneratePasswordScreen()
is ChangeAppHintState -> PasswordHintScreen()
}
}
currentScreen.dialog?.let {
when (it) {
is ValidateKeyPhrase -> ValidateKeyPhraseDialog()
is ForgotKeyPhraseState -> ForgotKeyPhraseDialog()
is RestoreKeyPassBackupState -> RestoreKeyPassBackupDialog(it)
is RestoreChromeBackupState -> RestoreChromeBackupDialog(it)
is RestoreKeePassBackupState -> RestoreKeePassBackupDialog(it)
}
currentScreen.dialog?.let {
when (it) {
is ValidateKeyPhrase -> ValidateKeyPhraseDialog()
is ForgotKeyPhraseState -> ForgotKeyPhraseDialog()
is RestoreKeyPassBackupState -> RestoreKeyPassBackupDialog(it)
is RestoreChromeBackupState -> RestoreChromeBackupDialog(it)
is RestoreKeePassBackupState -> RestoreKeePassBackupDialog(it)
}
}
}

View File

@@ -36,13 +36,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.yogeshpaliyal.common.utils.email
import com.yogeshpaliyal.common.utils.enableAutoFillService
import com.yogeshpaliyal.common.utils.isAutoFillServiceEnabled
import com.yogeshpaliyal.common.utils.setBiometricEnable
import com.yogeshpaliyal.common.utils.setBiometricLoginTimeoutEnable
import com.yogeshpaliyal.common.utils.setUserSettings
import com.yogeshpaliyal.keypass.BuildConfig
import com.yogeshpaliyal.keypass.MyApplication
import com.yogeshpaliyal.keypass.R
@@ -66,258 +66,246 @@ import org.reduxkotlin.compose.rememberTypedDispatcher
@Composable
fun MySettingCompose() {
val dispatchAction = rememberTypedDispatcher<Action>()
val context = LocalContext.current
val userSettings = LocalUserSettings.current
var isAutoFillServiceEnable by remember { mutableStateOf(false) }
val dispatchAction = rememberTypedDispatcher<Action>()
val context = LocalContext.current
val userSettings = LocalUserSettings.current
var isAutoFillServiceEnable by remember { mutableStateOf(false) }
// Retrieving saved password length
var savedPasswordLength by remember { mutableStateOf(DEFAULT_PASSWORD_LENGTH) }
LaunchedEffect(key1 = Unit) {
userSettings.passwordConfig.length.let { value -> savedPasswordLength = value }
// Retrieving saved password length
var savedPasswordLength by remember { mutableStateOf(DEFAULT_PASSWORD_LENGTH) }
LaunchedEffect(key1 = Unit) {
userSettings.passwordConfig.length.let { value -> savedPasswordLength = value }
}
LaunchedEffect(context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
isAutoFillServiceEnable = context.isAutoFillServiceEnabled()
}
}
Column(modifier = Modifier.fillMaxSize(1f).verticalScroll(rememberScrollState())) {
PreferenceItem(title = R.string.security, isCategory = true)
PreferenceItem(
painter = painterResource(id = R.drawable.credentials_backup),
title = R.string.credentials_backups,
summary = R.string.credentials_backups_desc) {
dispatchAction(NavigationAction(BackupScreenState()))
}
PreferenceItem(
painter = painterResource(id = R.drawable.import_credentials),
title = R.string.restore_credentials,
summary = R.string.restore_credentials_desc) {
dispatchAction(NavigationAction(BackupImporterState()))
}
PreferenceItem(
title = R.string.change_app_password,
summary = R.string.change_app_password,
icon = Icons.Rounded.Password) {
dispatchAction(NavigationAction(ChangeAppPasswordState()))
}
PreferenceItem(
title = R.string.app_password_hint,
summary =
if (userSettings.passwordHint != null) R.string.change_app_password_hint
else R.string.set_app_password_hint,
icon = Icons.Outlined.Info) {
dispatchAction(NavigationAction(ChangeAppHintState))
}
val changePasswordLengthSummary = context.getString(R.string.default_password_length)
PreferenceItem(
title = R.string.change_password_length,
summaryStr = "$changePasswordLengthSummary: ${savedPasswordLength.toInt()}") {
dispatchAction(NavigationAction(ChangeDefaultPasswordLengthState()))
}
PreferenceItem(title = R.string.validate_keyphrase, summary = R.string.validate_keyphrase) {
dispatchAction(UpdateDialogAction(ValidateKeyPhrase))
}
LaunchedEffect(context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
isAutoFillServiceEnable = context.isAutoFillServiceEnabled()
}
}
AutoFillPreferenceItem()
Column(modifier = Modifier.fillMaxSize(1f).verticalScroll(rememberScrollState())) {
PreferenceItem(title = R.string.security, isCategory = true)
PreferenceItem(
painter = painterResource(id = R.drawable.credentials_backup),
title = R.string.credentials_backups,
summary = R.string.credentials_backups_desc
) {
dispatchAction(NavigationAction(BackupScreenState()))
BiometricsOption()
AutoDisableBiometric()
AutoLockPreferenceItem()
HorizontalDivider(modifier = Modifier.fillMaxWidth(1f).height(1.dp))
PreferenceItem(title = R.string.help, isCategory = true)
PreferenceItem(
title = R.string.send_feedback,
summary = R.string.send_feedback_desc,
icon = Icons.Rounded.Feedback) {
context.email(
context.getString(R.string.feedback_to_keypass), "yogeshpaliyal.foss@gmail.com")
}
PreferenceItem(
painter = painterResource(id = R.drawable.import_credentials),
title = R.string.restore_credentials,
summary = R.string.restore_credentials_desc
) {
dispatchAction(NavigationAction(BackupImporterState()))
}
PreferenceItem(
title = R.string.change_app_password,
summary = R.string.change_app_password,
icon = Icons.Rounded.Password
) {
dispatchAction(NavigationAction(ChangeAppPasswordState()))
PreferenceItem(
title = R.string.share, summary = R.string.share_desc, icon = Icons.Rounded.Share) {
dispatchAction(IntentNavigation.ShareApp)
}
PreferenceItem(
title = R.string.app_password_hint,
summary = if (userSettings.passwordHint != null) R.string.change_app_password_hint else R.string.set_app_password_hint,
icon = Icons.Outlined.Info
) {
dispatchAction(NavigationAction(ChangeAppHintState))
PreferenceItem(
title = R.string.about_us, summary = R.string.about_us, icon = Icons.Outlined.Info) {
dispatchAction(NavigationAction(AboutState()))
}
val changePasswordLengthSummary = context.getString(R.string.default_password_length)
PreferenceItem(
title = R.string.change_password_length,
summaryStr = "$changePasswordLengthSummary: ${savedPasswordLength.toInt()}"
) {
dispatchAction(NavigationAction(ChangeDefaultPasswordLengthState()))
Row(
modifier = Modifier.fillMaxWidth(1f).padding(16.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically) {
Text(
text = "App Version ${BuildConfig.VERSION_NAME}",
style = MaterialTheme.typography.labelSmall)
}
PreferenceItem(
title = R.string.validate_keyphrase,
summary = R.string.validate_keyphrase
) {
dispatchAction(UpdateDialogAction(ValidateKeyPhrase))
}
AutoFillPreferenceItem()
BiometricsOption()
AutoDisableBiometric()
HorizontalDivider(
modifier = Modifier
.fillMaxWidth(1f)
.height(1.dp)
)
PreferenceItem(title = R.string.help, isCategory = true)
PreferenceItem(
title = R.string.send_feedback,
summary = R.string.send_feedback_desc,
icon = Icons.Rounded.Feedback
) {
context.email(
context.getString(R.string.feedback_to_keypass),
"yogeshpaliyal.foss@gmail.com"
)
}
PreferenceItem(
title = R.string.share,
summary = R.string.share_desc,
icon = Icons.Rounded.Share
) {
dispatchAction(IntentNavigation.ShareApp)
}
PreferenceItem(
title = R.string.about_us,
summary = R.string.about_us,
icon = Icons.Outlined.Info
) {
dispatchAction(NavigationAction(AboutState()))
}
Row(
modifier = Modifier
.fillMaxWidth(1f)
.padding(16.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "App Version ${BuildConfig.VERSION_NAME}",
style = MaterialTheme.typography.labelSmall
)
}
}
}
}
@Composable
fun AutoFillPreferenceItem() {
val context = LocalContext.current
val context = LocalContext.current
var autoFillDescription = R.string.autofill_service
var onClick = {
(context.applicationContext as? MyApplication)?.activityLaunchTriggered()
context.enableAutoFillService()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (context.isAutoFillServiceEnabled()) {
autoFillDescription = R.string.autofill_service_disable
} else {
autoFillDescription = R.string.autofill_service_enable
}
var autoFillDescription = R.string.autofill_service
var onClick = {
(context.applicationContext as? MyApplication)?.activityLaunchTriggered()
context.enableAutoFillService()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (context.isAutoFillServiceEnabled()) {
autoFillDescription = R.string.autofill_service_disable
} else {
onClick = {}
autoFillDescription = R.string.autofill_not_available
autoFillDescription = R.string.autofill_service_enable
}
} else {
onClick = {}
autoFillDescription = R.string.autofill_not_available
}
PreferenceItem(
title = R.string.autofill_service,
summary = autoFillDescription,
onClickItem = onClick
)
PreferenceItem(
title = R.string.autofill_service, summary = autoFillDescription, onClickItem = onClick)
}
@Composable
fun BiometricsOption() {
val context = LocalContext.current
val userSettings = LocalUserSettings.current
val context = LocalContext.current
val userSettings = LocalUserSettings.current
val (canAuthenticate, setCanAuthenticate) = remember {
mutableStateOf(BiometricManager.BIOMETRIC_STATUS_UNKNOWN)
val (canAuthenticate, setCanAuthenticate) =
remember { mutableStateOf(BiometricManager.BIOMETRIC_STATUS_UNKNOWN) }
val (isBiometricEnable, setIsBiometricEnable) = remember { mutableStateOf(false) }
val (subtitle, setSubtitle) = remember { mutableStateOf<Int?>(null) }
val dispatch = rememberTypedDispatcher<Action>()
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(key1 = context) { setIsBiometricEnable(userSettings.isBiometricEnable) }
LaunchedEffect(key1 = context) {
val biometricManager = BiometricManager.from(context)
setCanAuthenticate(biometricManager.canAuthenticate(BIOMETRIC_STRONG))
}
LaunchedEffect(key1 = canAuthenticate, isBiometricEnable) {
when (canAuthenticate) {
BiometricManager.BIOMETRIC_SUCCESS ->
if (isBiometricEnable) {
setSubtitle(R.string.enabled)
} else {
setSubtitle(R.string.disabled)
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE ->
setSubtitle(R.string.biometric_error_no_hardware)
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE ->
setSubtitle(R.string.biometric_error_hw_unavailable)
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
setSubtitle(R.string.biometric_error_none_enrolled)
}
}
}
val (isBiometricEnable, setIsBiometricEnable) = remember {
mutableStateOf(false)
}
val (subtitle, setSubtitle) = remember {
mutableStateOf<Int?>(null)
}
val dispatch = rememberTypedDispatcher<Action>()
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(key1 = context) {
setIsBiometricEnable(userSettings.isBiometricEnable)
}
LaunchedEffect(key1 = context) {
val biometricManager = BiometricManager.from(context)
setCanAuthenticate(biometricManager.canAuthenticate(BIOMETRIC_STRONG))
}
LaunchedEffect(key1 = canAuthenticate, isBiometricEnable) {
PreferenceItem(
title = R.string.unlock_with_biometric,
summary = subtitle,
icon = Icons.Rounded.Fingerprint) {
when (canAuthenticate) {
BiometricManager.BIOMETRIC_SUCCESS ->
if (isBiometricEnable) {
setSubtitle(R.string.enabled)
} else {
setSubtitle(R.string.disabled)
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE ->
setSubtitle(R.string.biometric_error_no_hardware)
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE ->
setSubtitle(R.string.biometric_error_hw_unavailable)
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
setSubtitle(R.string.biometric_error_none_enrolled)
BiometricManager.BIOMETRIC_SUCCESS -> {
coroutineScope.launch {
context.setBiometricEnable(!isBiometricEnable)
setIsBiometricEnable(!isBiometricEnable)
}
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
// Prompts the user to create credentials that your app accepts.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val enrollIntent =
Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply {
putExtra(
Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
}
context.startActivity(enrollIntent)
} else {
dispatch(ToastAction(R.string.password_set_from_settings))
}
}
}
}
PreferenceItem(
title = R.string.unlock_with_biometric,
summary = subtitle,
icon = Icons.Rounded.Fingerprint
) {
when (canAuthenticate) {
BiometricManager.BIOMETRIC_SUCCESS -> {
coroutineScope.launch {
context.setBiometricEnable(!isBiometricEnable)
setIsBiometricEnable(!isBiometricEnable)
}
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
// Prompts the user to create credentials that your app accepts.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val enrollIntent = Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply {
putExtra(
Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
BIOMETRIC_STRONG or DEVICE_CREDENTIAL
)
}
context.startActivity(enrollIntent)
} else {
dispatch(ToastAction(R.string.password_set_from_settings))
}
}
}
}
}
}
@Composable
fun AutoLockPreferenceItem() {
val context = LocalContext.current
val userSettings = LocalUserSettings.current
val coroutineScope = rememberCoroutineScope()
val autoLockStatus =
if (userSettings.autoLockEnabled == true) {
R.string.enabled
} else {
R.string.disabled
}
PreferenceItem(
title = R.string.auto_lock, summary = autoLockStatus, icon = Icons.Rounded.LockReset) {
coroutineScope.launch {
context.setUserSettings(
userSettings.copy(autoLockEnabled = userSettings.autoLockEnabled == false))
}
}
}
@Composable
fun AutoDisableBiometric() {
val context = LocalContext.current
val userSettings = LocalUserSettings.current
val context = LocalContext.current
val userSettings = LocalUserSettings.current
val coroutineScope = rememberCoroutineScope()
val coroutineScope = rememberCoroutineScope()
val enableDisableStr =
if (userSettings.biometricLoginTimeoutEnable == true) {
R.string.enabled
} else {
R.string.disabled
}
val enableDisableStr =
if (userSettings.biometricLoginTimeoutEnable == true) {
R.string.enabled
} else {
R.string.disabled
}
PreferenceItem(
title = R.string.biometric_login_timeout,
summary = enableDisableStr,
icon = Icons.Rounded.LockReset,
onClickItem = if (userSettings.isBiometricEnable) {
PreferenceItem(
title = R.string.biometric_login_timeout,
summary = enableDisableStr,
icon = Icons.Rounded.LockReset,
onClickItem =
if (userSettings.isBiometricEnable) {
{
coroutineScope.launch {
context.setBiometricLoginTimeoutEnable(userSettings.biometricLoginTimeoutEnable != true)
}
coroutineScope.launch {
context.setBiometricLoginTimeoutEnable(
userSettings.biometricLoginTimeoutEnable != true)
}
}
} else null
)
} else null)
}

View File

@@ -139,5 +139,6 @@
<string name="got_it">Got it</string>
<string name="biometric_disabled_due_to_timeout">Please login via password because you haven\'t used password in last 24 hours</string>
<string name="invalid_package_signature">Invalid package signature</string>
<string name="auto_lock">Auto Lock on background</string>
</resources>

View File

@@ -24,7 +24,8 @@ data class UserSettings(
val passwordHint: String? = null,
val biometricLoginTimeoutEnable: Boolean? = null,
val lastPasswordLoginTime: Long? = null,
val lastKeyPhraseEnterTime: Long? = null
val lastKeyPhraseEnterTime: Long? = null,
val autoLockEnabled: Boolean? = true
) {
fun isKeyPresent() = backupKey != null
fun isKeyPresent() = backupKey != null
}