allow custom reset password token

use a dialog to confirm password before reset password
hide reset password token and reset password by default
This commit is contained in:
BinTianqi
2024-08-09 18:39:27 +08:00
parent 8f93c85d6c
commit d56983790f
6 changed files with 156 additions and 80 deletions

View File

@@ -191,7 +191,7 @@ private fun PackageItem(pkg: PkgInfo, navCtrl: NavHostController, pkgName: Mutab
modifier = Modifier
.fillMaxWidth()
.clickable{ pkgName.value = pkg.pkgName; navCtrl.navigateUp() }
.padding(vertical = 3.dp)
.padding(vertical = 6.dp)
) {
Spacer(Modifier.padding(start = 15.dp))
Image(
@@ -202,7 +202,6 @@ private fun PackageItem(pkg: PkgInfo, navCtrl: NavHostController, pkgName: Mutab
Column {
Text(text = pkg.label, style = typography.titleLarge)
Text(text = pkg.pkgName, modifier = Modifier.alpha(0.8F))
Spacer(Modifier.padding(top = 3.dp))
}
}
}

View File

@@ -35,15 +35,19 @@ import android.os.Build.VERSION
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme.colorScheme
@@ -51,10 +55,12 @@ import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -65,6 +71,7 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat.startActivity
import androidx.navigation.NavHostController
@@ -73,6 +80,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.toggle
import com.bintianqi.owndroid.ui.Animations
import com.bintianqi.owndroid.ui.CheckBoxItem
import com.bintianqi.owndroid.ui.Information
@@ -124,18 +132,21 @@ fun Password(navCtrl: NavHostController) {
@Composable
private fun Home(navCtrl:NavHostController,scrollState: ScrollState) {
val context = LocalContext.current
val sharedPrefs = context.getSharedPreferences("data", Context.MODE_PRIVATE)
Column(modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(start = 30.dp, end = 12.dp)) {
Text(
text = stringResource(R.string.password_and_keyguard),
style = typography.headlineLarge,
modifier = Modifier.padding(top = 8.dp, bottom = 5.dp)
modifier = Modifier.padding(top = 8.dp, bottom = 5.dp).offset(x = (-8).dp)
)
SubPageItem(R.string.password_info, "", R.drawable.info_fill0) { navCtrl.navigate("PasswordInfo") }
if(VERSION.SDK_INT >= 26 && (context.isDeviceOwner || context.isProfileOwner)) {
SubPageItem(R.string.reset_password_token, "", R.drawable.key_vertical_fill0) { navCtrl.navigate("ResetPasswordToken") }
}
if(context.isDeviceAdmin || context.isDeviceOwner || context.isProfileOwner) {
SubPageItem(R.string.reset_password, "", R.drawable.lock_reset_fill0) { navCtrl.navigate("ResetPassword") }
if(sharedPrefs.getBoolean("dangerous_features", false)) {
if(VERSION.SDK_INT >= 26 && (context.isDeviceOwner || context.isProfileOwner)) {
SubPageItem(R.string.reset_password_token, "", R.drawable.key_vertical_fill0) { navCtrl.navigate("ResetPasswordToken") }
}
if(context.isDeviceAdmin || context.isDeviceOwner || context.isProfileOwner) {
SubPageItem(R.string.reset_password, "", R.drawable.lock_reset_fill0) { navCtrl.navigate("ResetPassword") }
}
}
if(VERSION.SDK_INT >= 31 && (context.isDeviceOwner || context.isProfileOwner)) {
SubPageItem(R.string.required_password_complexity, "", R.drawable.password_fill0) { navCtrl.navigate("RequirePasswordComplexity") }
@@ -200,23 +211,25 @@ private fun ResetPasswordToken() {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
val tokenByteArray by remember { mutableStateOf(byteArrayOf(1,1,4,5,1,4,1,9,1,9,8,1,0,1,1,4,5,1,4,1,9,1,9,8,1,0,1,1,4,5,1,4,1,9,1,9,8,1,0)) }
var token by remember { mutableStateOf("") }
val tokenByteArray = token.toByteArray()
val focusMgr = LocalFocusManager.current
Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) {
Spacer(Modifier.padding(vertical = 10.dp))
Text(text = stringResource(R.string.reset_password_token), style = typography.headlineLarge)
Spacer(Modifier.padding(vertical = 5.dp))
Button(
onClick = {
Toast.makeText(
context,
if(dpm.clearResetPasswordToken(receiver)) R.string.success else R.string.failed,
Toast.LENGTH_SHORT
).show()
OutlinedTextField(
value = token, onValueChange = { token = it },
label = { Text(stringResource(R.string.token)) },
keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
supportingText = {
AnimatedVisibility(tokenByteArray.size < 32) {
Text(stringResource(R.string.token_must_longer_than_32_byte))
}
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.clear))
}
)
Button(
onClick = {
try {
@@ -229,20 +242,38 @@ private fun ResetPasswordToken() {
Toast.makeText(context, R.string.security_exception, Toast.LENGTH_SHORT).show()
}
},
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp),
enabled = tokenByteArray.size >= 32
) {
Text(stringResource(R.string.set))
}
Button(
onClick = {
if(!dpm.isResetPasswordTokenActive(receiver)) {
try { activateToken(context) }
catch(e:NullPointerException) { Toast.makeText(context, R.string.please_set_a_token, Toast.LENGTH_SHORT).show() }
} else { Toast.makeText(context, R.string.token_already_activated, Toast.LENGTH_SHORT).show() }
},
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.activate))
Button(
onClick = {
if(!dpm.isResetPasswordTokenActive(receiver)) {
try { activateToken(context) }
catch(e:NullPointerException) { Toast.makeText(context, R.string.please_set_a_token, Toast.LENGTH_SHORT).show() }
} else { Toast.makeText(context, R.string.token_already_activated, Toast.LENGTH_SHORT).show() }
},
modifier = Modifier.fillMaxWidth(0.49F)
) {
Text(stringResource(R.string.activate))
}
Button(
onClick = {
Toast.makeText(
context,
if(dpm.clearResetPasswordToken(receiver)) R.string.success else R.string.failed,
Toast.LENGTH_SHORT
).show()
},
modifier = Modifier.fillMaxWidth(0.96F)
) {
Text(stringResource(R.string.clear))
}
}
Spacer(Modifier.padding(vertical = 5.dp))
Information{ Text(stringResource(R.string.activate_token_not_required_when_no_password)) }
@@ -255,84 +286,123 @@ private fun ResetPassword() {
val dpm = context.getDPM()
val receiver = context.getReceiver()
val focusMgr = LocalFocusManager.current
var newPwd by remember { mutableStateOf("") }
val tokenByteArray by remember { mutableStateOf(byteArrayOf(1,1,4,5,1,4,1,9,1,9,8,1,0,1,1,4,5,1,4,1,9,1,9,8,1,0,1,1,4,5,1,4,1,9,1,9,8,1,0)) }
var confirmed by remember { mutableStateOf(false) }
var resetPwdFlag by remember { mutableIntStateOf(0) }
var password by remember { mutableStateOf("") }
var useToken by remember { mutableStateOf(false) }
var token by remember { mutableStateOf("") }
val tokenByteArray = token.toByteArray()
var flags = remember { mutableStateListOf<Int>() }
var confirmDialog by remember { mutableStateOf(false) }
Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) {
Spacer(Modifier.padding(vertical = 10.dp))
Text(text = stringResource(R.string.reset_password),style = typography.headlineLarge)
Spacer(Modifier.padding(vertical = 5.dp))
if(VERSION.SDK_INT >= 26) {
OutlinedTextField(
value = token, onValueChange = { token = it },
label = { Text(stringResource(R.string.token)) },
keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
modifier = Modifier.fillMaxWidth()
)
}
OutlinedTextField(
value = newPwd,
onValueChange = { newPwd=it },
enabled = !confirmed,
value = password,
onValueChange = { password = it },
label = { Text(stringResource(R.string.password)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }),
supportingText = { Text(stringResource(R.string.reset_pwd_desc)) },
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.padding(vertical = 3.dp))
Text(text = stringResource(R.string.reset_pwd_desc))
Spacer(Modifier.padding(vertical = 5.dp))
if(VERSION.SDK_INT >= 23) {
RadioButtonItem(
CheckBoxItem(
R.string.do_not_ask_credentials_on_boot,
resetPwdFlag == RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT,
{ resetPwdFlag = RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT }
RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT in flags,
{ flags.toggle(it, RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT) }
)
}
RadioButtonItem(
CheckBoxItem(
R.string.reset_password_require_entry,
resetPwdFlag == RESET_PASSWORD_REQUIRE_ENTRY,
{ resetPwdFlag = RESET_PASSWORD_REQUIRE_ENTRY }
RESET_PASSWORD_REQUIRE_ENTRY in flags,
{ flags.toggle(it, RESET_PASSWORD_REQUIRE_ENTRY) }
)
RadioButtonItem(R.string.none, resetPwdFlag == 0, { resetPwdFlag = 0 })
Spacer(Modifier.padding(vertical = 5.dp))
Button(
onClick = {
if(newPwd.length>=4 || newPwd.isEmpty()) { confirmed=!confirmed }
else { Toast.makeText(context, R.string.require_4_digit_password, Toast.LENGTH_SHORT).show() }
},
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = if(confirmed) colorScheme.primary else colorScheme.error,
contentColor = if(confirmed) colorScheme.onPrimary else colorScheme.onError
)
) {
Text(text = stringResource(if(confirmed) R.string.cancel else R.string.confirm))
}
Spacer(Modifier.padding(vertical = 3.dp))
if(VERSION.SDK_INT >= 26) {
Button(
onClick = {
val resetSuccess = dpm.resetPasswordWithToken(receiver,newPwd,tokenByteArray,resetPwdFlag)
if(resetSuccess) { Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show(); newPwd=""}
else{ Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() }
confirmed=false
useToken = true
confirmDialog = true
focusMgr.clearFocus()
},
colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError),
enabled = confirmed && (context.isDeviceOwner || context.isProfileOwner),
enabled = tokenByteArray.size >=32 && password.length !in 1..3 && (context.isDeviceOwner || context.isProfileOwner),
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.reset_password_with_token))
}
}
Button(
onClick = {
val resetSuccess = dpm.resetPassword(newPwd,resetPwdFlag)
if(resetSuccess) { Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show(); newPwd="" }
else{ Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() }
confirmed=false
},
enabled = confirmed,
colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError),
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.reset_password_deprecated))
if(VERSION.SDK_INT <= 30) {
Button(
onClick = {
useToken = false
confirmDialog = true
focusMgr.clearFocus()
},
colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError),
enabled = password.length !in 1..3,
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.reset_password))
}
}
Spacer(Modifier.padding(vertical = 30.dp))
}
if(confirmDialog) {
var confirmPassword by remember { mutableStateOf("") }
AlertDialog(
onDismissRequest = { confirmDialog = false },
title = { Text(stringResource(R.string.reset_password)) },
text = {
val dialogFocusMgr = LocalFocusManager.current
OutlinedTextField(
value = confirmPassword,
onValueChange = { confirmPassword = it },
label = { Text(stringResource(R.string.confirm_password)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { dialogFocusMgr.clearFocus() }),
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
},
confirmButton = {
TextButton(
onClick = {
var resetFlag = 0
flags.forEach { resetFlag += it }
val success = if(VERSION.SDK_INT >= 26 && useToken) {
dpm.resetPasswordWithToken(receiver, password, tokenByteArray, resetFlag)
} else {
dpm.resetPassword(password, resetFlag)
}
Toast.makeText(context, if(success) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show()
password = ""
confirmDialog = false
},
colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error),
enabled = confirmPassword == password
) {
Text(stringResource(R.string.confirm))
}
},
dismissButton = {
TextButton(onClick = { confirmDialog = false }) {
Text(stringResource(R.string.cancel))
}
}
)
}
}
@SuppressLint("NewApi")

View File

@@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
@@ -110,7 +111,7 @@ private fun Home(navCtrl: NavHostController,scrollState: ScrollState) {
Text(
text = stringResource(R.string.user_manager),
style = typography.headlineLarge,
modifier = Modifier.padding(top = 8.dp, bottom = 5.dp)
modifier = Modifier.padding(top = 8.dp, bottom = 5.dp).offset(x = (-8).dp)
)
SubPageItem(R.string.user_info, "", R.drawable.person_fill0) { navCtrl.navigate("UserInfo") }
if(context.isDeviceOwner) {

View File

@@ -501,17 +501,19 @@
<string name="password_failed_attempts_is">Başarısız şifre denemeleri: %1$s</string>
<string name="is_using_unified_password">Birleşik şifre: %1$s</string>
<string name="reset_password_token">Şifre sıfırlama jetonu</string>
<string name="token">Token</string> <!--TODO-->
<string name="token_must_longer_than_32_byte">The token must be longer than 32-byte</string> <!--TODO-->
<string name="token_already_activated">Jeton zaten etkinleştirildi</string>
<string name="clear">Temizle</string>
<string name="set">Ayarla</string>
<string name="please_set_a_token">Lütfen bir jeton ayarlayın</string>
<string name="activate_token_not_required_when_no_password">Şifre ayarlanmadığında jeton otomatik olarak etkinleştirilecektir.</string>
<string name="reset_password">Şifreyi sıfırla</string>
<string name="confirm_password">Confirm password</string> <!--TODO-->
<string name="do_not_ask_credentials_on_boot">Başlangıçta kimlik bilgilerini sorma</string>
<string name="reset_password_require_entry">Giriş gerektir</string>
<string name="require_4_digit_password">En az 4 haneli şifre gerektir</string>
<string name="reset_password_with_token">Jeton ile şifreyi sıfırla</string>
<string name="reset_password_deprecated">Şifreyi sıfırla</string>
<string name="required_password_complexity">Gereken şifre karmaşıklığı</string>
<string name="require_set_new_password">Yeni şifre ayarlanmasını iste</string>
<string name="disable_keyguard_features">Kilit ekranı özellikleri</string>

View File

@@ -473,7 +473,7 @@
<!--Password&Keyguard-->
<string name="password_and_keyguard">密码与锁屏</string>
<string name="password_info">密码信息</string>
<string name="reset_pwd_desc">留空以清除密码纯数字将使用PIN码</string>
<string name="reset_pwd_desc">留空以清除密码</string>
<string name="max_pwd_fail">最大密码错误次数</string>
<string name="max_pwd_fail_desc">达到该限制会恢复出厂设置0为无限制</string>
<string name="max_pwd_fail_textfield">错误次数</string>
@@ -493,17 +493,19 @@
<string name="password_failed_attempts_is">密码已错误次数:%1$s</string>
<string name="is_using_unified_password">个人与工作应用密码一致:%1$s</string>
<string name="reset_password_token">密码重置令牌</string>
<string name="token">令牌</string>
<string name="token_must_longer_than_32_byte">令牌必须大于32字节</string>
<string name="token_already_activated">令牌已经激活</string>
<string name="clear">清除</string>
<string name="set">设置</string>
<string name="please_set_a_token">请先设置令牌</string>
<string name="activate_token_not_required_when_no_password">没有密码时会自动激活令牌</string>
<string name="reset_password">重置密码</string>
<string name="confirm_password">确认密码</string>
<string name="do_not_ask_credentials_on_boot">启动(boot)时不要求密码</string>
<string name="reset_password_require_entry">不允许其他设备管理员重置密码直至用户输入一次密码</string>
<string name="require_4_digit_password">需要4位密码</string>
<string name="reset_password_with_token">使用令牌重置密码</string>
<string name="reset_password_deprecated">重置密码(弃用)</string>
<string name="required_password_complexity">密码复杂度要求</string>
<string name="require_set_new_password">要求设置新密码</string>
<string name="disable_keyguard_features">锁屏功能</string>

View File

@@ -507,17 +507,19 @@
<string name="password_failed_attempts_is">Password failed attempts: %1$s</string>
<string name="is_using_unified_password">Unified password: %1$s</string>
<string name="reset_password_token">Reset password token</string>
<string name="token">Token</string>
<string name="token_must_longer_than_32_byte">The token must be longer than 32-byte</string>
<string name="token_already_activated">Token already activated</string>
<string name="clear">Clear</string>
<string name="set">Set</string>
<string name="please_set_a_token">Please set a token</string>
<string name="activate_token_not_required_when_no_password">Token will be automatically activated if no password is set. </string>
<string name="reset_password">Reset password</string>
<string name="confirm_password">Confirm password</string>
<string name="do_not_ask_credentials_on_boot">Do not ask credentials on boot</string>
<string name="reset_password_require_entry">Require entry</string>
<string name="require_4_digit_password">Require at least 4 digit password</string>
<string name="reset_password_with_token">Reset password with token</string>
<string name="reset_password_deprecated">Reset password</string>
<string name="required_password_complexity">Required password complexity</string>
<string name="require_set_new_password">Request to set a new password</string>
<string name="disable_keyguard_features">Keyguard features</string>