Add Biometric Authentication (#16)

This allows using the biometric authentication to authenticate as parent user.

Co-authored-by: Marcel Voigt <ycram@mailbox.org>
Reviewed-on: https://codeberg.org/timelimit/opentimelimit-android/pulls/16
Co-Authored-By: ycram <ycram@noreply.codeberg.org>
Co-Committed-By: ycram <ycram@noreply.codeberg.org>
This commit is contained in:
ycram 2021-02-12 18:32:54 +01:00 committed by jonas-l
parent 921ada1156
commit 1df18a2a76
21 changed files with 843 additions and 23 deletions

9
CONTRIBUTORS.md Normal file
View file

@ -0,0 +1,9 @@
# TimeLimit Contributors
- [Jonas Lochmann](https://codeberg.org/jonas-l)
- Author and maintainer
- [Marcel Voigt](https://codeberg.org/ycram)
- Biometric Authentication

View file

@ -1,5 +1,6 @@
/* /*
* Open TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann * Open TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
* Copyright <C> 2020 Marcel Voigt
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -103,6 +104,8 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'
implementation 'androidx.biometric:biometric:1.1.0'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

View file

@ -1,5 +1,6 @@
/* /*
* Open TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * Open TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* Copyright <C> 2020 Marcel Voigt
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -127,6 +128,9 @@ data class User(
val allowSelfLimitAdding: Boolean val allowSelfLimitAdding: Boolean
get() = flags and UserFlags.ALLOW_SELF_LIMIT_ADD == UserFlags.ALLOW_SELF_LIMIT_ADD get() = flags and UserFlags.ALLOW_SELF_LIMIT_ADD == UserFlags.ALLOW_SELF_LIMIT_ADD
val biometricAuthEnabled
get() = flags and UserFlags.BIOMETRIC_AUTH_ENABLED == UserFlags.BIOMETRIC_AUTH_ENABLED
override fun serialize(writer: JsonWriter) { override fun serialize(writer: JsonWriter) {
writer.beginObject() writer.beginObject()
@ -175,5 +179,6 @@ class UserTypeConverter {
object UserFlags { object UserFlags {
const val RESTRICT_VIEWING_TO_PARENTS = 1L const val RESTRICT_VIEWING_TO_PARENTS = 1L
const val ALLOW_SELF_LIMIT_ADD = 2L const val ALLOW_SELF_LIMIT_ADD = 2L
const val ALL_FLAGS = RESTRICT_VIEWING_TO_PARENTS or ALLOW_SELF_LIMIT_ADD const val BIOMETRIC_AUTH_ENABLED = 4L
const val ALL_FLAGS = RESTRICT_VIEWING_TO_PARENTS or ALLOW_SELF_LIMIT_ADD or BIOMETRIC_AUTH_ENABLED
} }

View file

@ -1,5 +1,6 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
* Copyright <C> 2020 Marcel Voigt
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -36,6 +37,7 @@ import io.timelimit.android.sync.actions.ChildSignInAction
import io.timelimit.android.sync.actions.apply.ApplyActionUtil import io.timelimit.android.sync.actions.apply.ApplyActionUtil
import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.AuthenticatedUser import io.timelimit.android.ui.main.AuthenticatedUser
import io.timelimit.android.ui.main.AuthenticationMethod
import io.timelimit.android.ui.manage.parent.key.ScannedKey import io.timelimit.android.ui.manage.parent.key.ScannedKey
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@ -79,6 +81,7 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
} }
private val isCheckingPassword = MutableLiveData<Boolean>().apply { value = false } private val isCheckingPassword = MutableLiveData<Boolean>().apply { value = false }
private val wasPasswordWrong = MutableLiveData<Boolean>().apply { value = false } private val wasPasswordWrong = MutableLiveData<Boolean>().apply { value = false }
var biometricPromptDismissed = false
private val isLoginDone = MutableLiveData<Boolean>().apply { value = false } private val isLoginDone = MutableLiveData<Boolean>().apply { value = false }
private val loginLock = Mutex() private val loginLock = Mutex()
@ -94,21 +97,21 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
val loginScreen = isCheckingPassword.switchMap { isCheckingPassword -> val loginScreen = isCheckingPassword.switchMap { isCheckingPassword ->
wasPasswordWrong.map { wasPasswordWrong -> wasPasswordWrong.map { wasPasswordWrong ->
ParentUserLogin( ParentUserLogin(
isCheckingPassword = isCheckingPassword, isCheckingPassword = isCheckingPassword,
wasPasswordWrong = wasPasswordWrong wasPasswordWrong = wasPasswordWrong,
biometricAuthEnabled = selectedUser.biometricAuthEnabled,
userName = selectedUser.name
) as LoginDialogStatus ) as LoginDialogStatus
} }
} }
AllowUserLoginStatusUtil.calculateLive(logic, selectedUser.id).switchMap { status -> AllowUserLoginStatusUtil.calculateLive(logic, selectedUser.id).switchMap { status ->
if (status is AllowUserLoginStatus.Allow) { if (status is AllowUserLoginStatus.ForbidByCategory) {
loginScreen
} else if (status is AllowUserLoginStatus.ForbidByCategory) {
liveDataFromValue( liveDataFromValue(
ParentUserLoginBlockedByCategory( ParentUserLoginBlockedByCategory(
categoryTitle = status.categoryTitle, categoryTitle = status.categoryTitle,
reason = status.blockingReason reason = status.blockingReason
) as LoginDialogStatus ) as LoginDialogStatus
) )
} else { } else {
loginScreen loginScreen
@ -146,6 +149,7 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
} }
fun startSignIn(user: User) { fun startSignIn(user: User) {
biometricPromptDismissed = false
selectedUserId.value = user.id selectedUserId.value = user.id
} }
@ -170,8 +174,10 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
if (shouldSignIn) { if (shouldSignIn) {
model.setAuthenticatedUser(AuthenticatedUser( model.setAuthenticatedUser(AuthenticatedUser(
userId = user.id, userId = user.id,
passwordHash = user.password passwordHash = user.password,
isPasswordDisabled = emptyPasswordValid,
authenticatedBy = AuthenticationMethod.Password
)) ))
isLoginDone.value = true isLoginDone.value = true
@ -228,8 +234,10 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
if (shouldSignIn) { if (shouldSignIn) {
model.setAuthenticatedUser(AuthenticatedUser( model.setAuthenticatedUser(AuthenticatedUser(
userId = user.id, userId = user.id,
passwordHash = user.password passwordHash = user.password,
isPasswordDisabled = Threads.crypto.executeAndWait { PasswordHashing.validateSync("", user.password) },
authenticatedBy = AuthenticationMethod.KeyCode
)) ))
isLoginDone.value = true isLoginDone.value = true
@ -268,8 +276,10 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
} }
val authenticatedUser = AuthenticatedUser( val authenticatedUser = AuthenticatedUser(
userId = userEntry.id, userId = userEntry.id,
passwordHash = userEntry.password passwordHash = userEntry.password,
isPasswordDisabled = Threads.crypto.executeAndWait { PasswordHashing.validateSync("", userEntry.password) },
authenticatedBy = AuthenticationMethod.Password
) )
val allowLoginStatus = Threads.database.executeAndWait { val allowLoginStatus = Threads.database.executeAndWait {
@ -337,6 +347,37 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
} }
} }
fun performBiometricLogin(activityViewModel: ActivityViewModel) {
runAsync {
loginLock.withLock {
selectedUser.waitForNullableValue()?.loginRelatedData?.user?.let { user: User ->
if (!user.biometricAuthEnabled) {
return@runAsync
}
val allowLoginStatus = Threads.database.executeAndWait {
AllowUserLoginStatusUtil.calculateSync(
logic = logic,
userId = user.id
)
}
if (allowLoginStatus !is AllowUserLoginStatus.Allow) {
Toast.makeText(getApplication(), formatAllowLoginStatusError(allowLoginStatus, getApplication()), Toast.LENGTH_SHORT).show()
return@runAsync
}
activityViewModel.setAuthenticatedUser(AuthenticatedUser(
userId = user.id,
passwordHash = user.password,
isPasswordDisabled = Threads.crypto.executeAndWait { PasswordHashing.validateSync("", user.password) },
authenticatedBy = AuthenticationMethod.Biometric
))
isLoginDone.value = true
}
}
}
}
fun resetPasswordWrong() { fun resetPasswordWrong() {
if (wasPasswordWrong.value == true) { if (wasPasswordWrong.value == true) {
wasPasswordWrong.value = false wasPasswordWrong.value = false
@ -358,8 +399,10 @@ sealed class LoginDialogStatus
data class UserListLoginDialogStatus(val usersToShow: List<User>): LoginDialogStatus() data class UserListLoginDialogStatus(val usersToShow: List<User>): LoginDialogStatus()
data class ParentUserLoginBlockedByCategory(val categoryTitle: String, val reason: BlockingReason): LoginDialogStatus() data class ParentUserLoginBlockedByCategory(val categoryTitle: String, val reason: BlockingReason): LoginDialogStatus()
data class ParentUserLogin( data class ParentUserLogin(
val isCheckingPassword: Boolean, val isCheckingPassword: Boolean,
val wasPasswordWrong: Boolean val wasPasswordWrong: Boolean,
val biometricAuthEnabled: Boolean,
val userName: String
): LoginDialogStatus() ): LoginDialogStatus()
object LoginDialogDone: LoginDialogStatus() object LoginDialogDone: LoginDialogStatus()
data class CanNotSignInChildHasNoPassword(val childName: String): LoginDialogStatus() data class CanNotSignInChildHasNoPassword(val childName: String): LoginDialogStatus()
@ -367,4 +410,4 @@ object ChildAlreadyDeviceUser: LoginDialogStatus()
data class ChildUserLogin( data class ChildUserLogin(
val isCheckingPassword: Boolean, val isCheckingPassword: Boolean,
val wasPasswordWrong: Boolean val wasPasswordWrong: Boolean
): LoginDialogStatus() ): LoginDialogStatus()

View file

@ -1,5 +1,6 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* Copyright <C> 2020 Marcel Voigt
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -24,6 +25,8 @@ import android.view.ViewGroup
import android.view.WindowManager import android.view.WindowManager
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
@ -159,6 +162,10 @@ class NewLoginFragment: DialogFragment() {
} }
password.setOnEnterListenr { go() } password.setOnEnterListenr { go() }
biometricAuthButton.setOnClickListener {
tryBiometricLogin()
}
} }
binding.childPassword.apply { binding.childPassword.apply {
@ -192,9 +199,13 @@ class NewLoginFragment: DialogFragment() {
binding.switcher.setInAnimation(context!!, R.anim.wizard_open_step_in) binding.switcher.setInAnimation(context!!, R.anim.wizard_open_step_in)
binding.switcher.setOutAnimation(context!!, R.anim.wizard_open_step_out) binding.switcher.setOutAnimation(context!!, R.anim.wizard_open_step_out)
binding.switcher.displayedChild = PARENT_AUTH binding.switcher.displayedChild = PARENT_AUTH
if (status.biometricAuthEnabled && !model.biometricPromptDismissed) {
tryBiometricLogin()
}
} }
binding.enterPassword.password.isEnabled = !status.isCheckingPassword binding.enterPassword.password.isEnabled = !status.isCheckingPassword
binding.enterPassword.biometricAuthEnabled = status.biometricAuthEnabled
if (!binding.enterPassword.showCustomKeyboard) { if (!binding.enterPassword.showCustomKeyboard) {
binding.enterPassword.password.requestFocus() binding.enterPassword.password.requestFocus()
@ -272,4 +283,46 @@ class NewLoginFragment: DialogFragment() {
fun tryCodeLogin(code: ScannedKey) { fun tryCodeLogin(code: ScannedKey) {
model.tryCodeLogin(code, getActivityViewModel(activity!!)) model.tryCodeLogin(code, getActivityViewModel(activity!!))
} }
private fun tryBiometricLogin() {
model.biometricPromptDismissed = false
model.status.value?.let { status ->
if (status is ParentUserLogin) {
BiometricPrompt(this, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
model.biometricPromptDismissed = true
Toast.makeText(
context,
getString(R.string.biometric_auth_failed, status.userName) + "\n" +
getString(R.string.biometric_auth_failed_reason, errString),
Toast.LENGTH_LONG
).show()
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
model.biometricPromptDismissed = true
model.performBiometricLogin(getActivityViewModel(requireActivity()))
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
model.biometricPromptDismissed = true
Toast.makeText(context, getString(R.string.biometric_auth_failed, status.userName), Toast.LENGTH_LONG)
.show()
}
}).authenticate(
BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.biometric_login_prompt_title))
.setSubtitle(status.userName)
.setDescription(getString(R.string.biometric_login_prompt_description, status.userName))
.setNegativeButtonText(getString(R.string.generic_cancel))
.setConfirmationRequired(false)
.build()
)
}
}
}
} }

View file

@ -1,5 +1,6 @@
/* /*
* Open TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * Open TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* Copyright <C> 2020 Marcel Voigt
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -150,7 +151,12 @@ class ActivityViewModel(application: Application): AndroidViewModel(application)
} }
} }
data class AuthenticatedUser ( data class AuthenticatedUser(
val userId: String, val userId: String,
val passwordHash: String val passwordHash: String,
val isPasswordDisabled: Boolean,
val authenticatedBy: AuthenticationMethod
) )
enum class AuthenticationMethod {
Password, KeyCode, Biometric
}

View file

@ -1,5 +1,6 @@
/* /*
* Open TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann * Open TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* Copyright <C> 2020 Marcel Voigt
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -40,6 +41,7 @@ import io.timelimit.android.ui.manage.child.advanced.timezone.UserTimezoneView
import io.timelimit.android.ui.manage.parent.delete.DeleteParentView import io.timelimit.android.ui.manage.parent.delete.DeleteParentView
import io.timelimit.android.ui.manage.parent.key.ManageUserKeyView import io.timelimit.android.ui.manage.parent.key.ManageUserKeyView
import io.timelimit.android.ui.manage.parent.limitlogin.ParentLimitLoginView import io.timelimit.android.ui.manage.parent.limitlogin.ParentLimitLoginView
import io.timelimit.android.ui.manage.parent.password.biometric.ManageUserBiometricAuthView
class ManageParentFragment : Fragment(), FragmentWithCustomTitle { class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder } private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
@ -119,6 +121,14 @@ class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
fragmentManager = parentFragmentManager fragmentManager = parentFragmentManager
) )
ManageUserBiometricAuthView.bind(
view = binding.biometricAuth,
user = parentUser,
auth = activity.getActivityViewModel(),
fragmentManager = parentFragmentManager,
fragment = this
)
binding.handlers = object: ManageParentFragmentHandlers { binding.handlers = object: ManageParentFragmentHandlers {
override fun onChangePasswordClicked() { override fun onChangePasswordClicked() {
navigation.safeNavigate( navigation.safeNavigate(

View file

@ -0,0 +1,35 @@
/*
* TimeLimit Copyright <C> 2020 Marcel Voigt
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.manage.parent.password.biometric
import io.timelimit.android.R
class DisableBiometricAuthDeniedPasswordRequiredDialog :
ManageBiometricAuthDialog(DisableBiometricAuthDeniedPasswordRequiredDialog::class.java.simpleName) {
override val titleText by lazy { getString(R.string.biometric_manage_disable_password_required_dialog_title) }
override val messageText by lazy { getString(R.string.biometric_manage_disable_password_required_dialog_text) }
override val positiveButtonText by lazy { getString(R.string.generic_logout) }
override val negativeButtonText by lazy { getString(R.string.generic_cancel) }
override fun onPositiveButtonClicked() {
auth.logOut()
super.onPositiveButtonClicked()
}
companion object {
fun newInstance() = DisableBiometricAuthDeniedPasswordRequiredDialog()
}
}

View file

@ -0,0 +1,103 @@
/*
* TimeLimit Copyright <C> 2020 Marcel Voigt
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.manage.parent.password.biometric
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.biometric.BiometricPrompt
import io.timelimit.android.R
import io.timelimit.android.data.model.UserFlags
import io.timelimit.android.sync.actions.UpdateUserFlagsAction
class EnableBiometricAuthConfirmDialog :
ManageBiometricAuthDialog(EnableBiometricAuthConfirmDialog::class.java.simpleName) {
override val titleText by lazy { getString(R.string.biometric_manage_enable_dialog_title) }
override val messageText by lazy { getString(R.string.biometric_manage_enable_dialog_text, userName) }
override val positiveButtonText by lazy { getString(R.string.generic_enable) }
override val negativeButtonText by lazy { getString(R.string.generic_cancel) }
private lateinit var biometricPrompt: BiometricPrompt
private val userName by lazy { requireArguments().getString(ARG_USER_NAME) }
override fun onPositiveButtonClicked() {
showBiometricPrompt()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
biometricPrompt = BiometricPrompt(requireActivity(), object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Toast.makeText(
context,
getString(R.string.biometric_auth_failed, userName) + "\n" +
getString(R.string.biometric_auth_failed_reason, errString),
Toast.LENGTH_LONG
).show()
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
if (setUserFlag()) dismiss()
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Toast.makeText(
context,
getString(R.string.biometric_auth_failed, userName),
Toast.LENGTH_LONG
).show()
}
})
}
private fun showBiometricPrompt() {
biometricPrompt.authenticate(
BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.biometric_enable_prompt_title))
.setSubtitle(userName)
.setDescription(getString(R.string.biometric_enable_prompt_description, userName))
.setNegativeButtonText(getString(R.string.generic_cancel))
.setConfirmationRequired(false)
.build()
)
}
private fun setUserFlag(): Boolean {
val userId = requireArguments().getString(ARG_USER_ID) ?: return false
return auth.tryDispatchParentAction(
UpdateUserFlagsAction(
userId = userId,
modifiedBits = UserFlags.BIOMETRIC_AUTH_ENABLED,
newValues = UserFlags.BIOMETRIC_AUTH_ENABLED
)
)
}
companion object {
private const val ARG_USER_ID = "userId"
private const val ARG_USER_NAME = "userName"
fun newInstance(userId: String, userName: String) = EnableBiometricAuthConfirmDialog().apply {
arguments = Bundle().apply {
putString(ARG_USER_ID, userId)
putString(ARG_USER_NAME, userName)
}
}
}
}

View file

@ -0,0 +1,51 @@
/*
* TimeLimit Copyright <C> 2020 Marcel Voigt
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.manage.parent.password.biometric
import android.content.Intent
import android.os.Build
import android.provider.Settings
import io.timelimit.android.R
class EnableBiometricAuthDeniedNoCredentialsDialog :
ManageBiometricAuthDialog(EnableBiometricAuthDeniedNoCredentialsDialog::class.java.simpleName) {
override val titleText by lazy { getString(R.string.biometric_manage_no_credentials_dialog_title) }
override val messageText by lazy { getString(R.string.biometric_manage_no_credentials_dialog_text) }
override val positiveButtonText by lazy { getString(R.string.biometric_manage_no_credentials_dialog_action) }
override val negativeButtonText by lazy { getString(R.string.generic_cancel) }
override fun onPositiveButtonClicked() {
super.onPositiveButtonClicked()
@Suppress("DEPRECATION")
startActivity(
Intent(
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->
Settings.ACTION_BIOMETRIC_ENROLL
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ->
Settings.ACTION_FINGERPRINT_ENROLL
else ->
Settings.ACTION_SECURITY_SETTINGS
}
)
)
}
companion object {
fun newInstance() = EnableBiometricAuthDeniedNoCredentialsDialog()
}
}

View file

@ -0,0 +1,44 @@
/*
* TimeLimit Copyright <C> 2020 Marcel Voigt
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.manage.parent.password.biometric
import android.os.Bundle
import io.timelimit.android.R
class ManageBiometricAuthDeniedNotOwnerDialog :
ManageBiometricAuthDialog(ManageBiometricAuthDeniedNotOwnerDialog::class.java.simpleName) {
override val titleText by lazy { getString(R.string.biometric_manage_not_owner_dialog_title) }
override val messageText by lazy {
getString(R.string.biometric_manage_not_owner_dialog_text, requireArguments().getString(ARG_USER_NAME))
}
override val positiveButtonText by lazy { getString(R.string.generic_logout) }
override val negativeButtonText by lazy { getString(R.string.generic_cancel) }
override fun onPositiveButtonClicked() {
auth.logOut()
super.onPositiveButtonClicked()
}
companion object {
private const val ARG_USER_NAME = "userName"
fun newInstance(userName: String) = ManageBiometricAuthDeniedNotOwnerDialog().apply {
arguments = Bundle().apply {
putString(ARG_USER_NAME, userName)
}
}
}
}

View file

@ -0,0 +1,73 @@
/*
* TimeLimit Copyright <C> 2020 Marcel Voigt
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.manage.parent.password.biometric
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import io.timelimit.android.databinding.ManageUserBiometricAuthDialogBinding
import io.timelimit.android.extensions.showSafe
import io.timelimit.android.ui.main.getActivityViewModel
interface ManageBiometricAuthDialogHandler {
fun onPositiveButtonClicked()
fun onNegativeButtonClicked()
}
abstract class ManageBiometricAuthDialog(private val dialogTag: String) :
BottomSheetDialogFragment(), ManageBiometricAuthDialogHandler {
protected abstract val titleText: String
protected abstract val messageText: String
protected abstract val positiveButtonText: String
protected abstract val negativeButtonText: String
protected val auth by lazy { getActivityViewModel(requireActivity()) }
protected lateinit var binding: ManageUserBiometricAuthDialogBinding
var handleCancelAsNegativeButton = true
override fun onPositiveButtonClicked() = dismiss()
override fun onNegativeButtonClicked() = dismiss()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = ManageUserBiometricAuthDialogBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.title = titleText
binding.text = messageText
binding.positiveButtonText = positiveButtonText
binding.negativeButtonText = negativeButtonText
binding.handler = this
}
override fun onCancel(dialog: DialogInterface) {
if (handleCancelAsNegativeButton) onNegativeButtonClicked()
super.onCancel(dialog)
}
fun show(fragmentManager: FragmentManager) {
showSafe(fragmentManager, dialogTag)
}
}

View file

@ -0,0 +1,104 @@
/*
* TimeLimit Copyright <C> 2020 Marcel Voigt
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.manage.parent.password.biometric
import androidx.biometric.BiometricManager
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LiveData
import io.timelimit.android.R
import io.timelimit.android.data.model.User
import io.timelimit.android.data.model.UserFlags
import io.timelimit.android.databinding.ManageUserBiometricAuthViewBinding
import io.timelimit.android.sync.actions.UpdateUserFlagsAction
import io.timelimit.android.ui.extension.bindHelpDialog
import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.AuthenticationMethod
object ManageUserBiometricAuthView {
fun bind(
view: ManageUserBiometricAuthViewBinding,
user: LiveData<User?>,
auth: ActivityViewModel,
fragmentManager: FragmentManager,
fragment: Fragment
) {
user.observe(view.lifecycleOwner ?: fragment.viewLifecycleOwner) {
if (it != null) {
view.userName = it.name
view.biometricAuthEnabled = it.biometricAuthEnabled
view.errorText = when (BiometricManager.from(fragment.requireContext()).canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) {
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> fragment.getString(R.string.biometric_manage_error_no_hw)
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE, BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED ->
fragment.getString(R.string.biometric_manage_error_hw_not_available)
//BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> "" //Handled later by a dialog if necessary
//BiometricManager.BIOMETRIC_SUCCESS -> ""
else -> ""
}
}
}
fun toggleUserFlag() {
user.value?.let { user ->
auth.tryDispatchParentAction(
UpdateUserFlagsAction(
userId = user.id,
modifiedBits = UserFlags.BIOMETRIC_AUTH_ENABLED,
newValues = if (!view.biometricAuthEnabled) UserFlags.BIOMETRIC_AUTH_ENABLED else 0
)
)
}
}
view.toggleBiometricAuthSwitch.setOnCheckedChangeListener { v, isChecked ->
// Checked state of the switch view shall always reflect the currently active setting:
// when it's the same there's nothing to do (just updating the UI from user data);
// when it differs (changed via UI) just reset the UI state, which is updated whenever the user's flag actually has changed.
if (isChecked == view.biometricAuthEnabled)
return@setOnCheckedChangeListener
else
v.isChecked = view.biometricAuthEnabled
if (auth.requestAuthenticationOrReturnTrue()) {
@Suppress("NAME_SHADOWING") val user = user.value ?: return@setOnCheckedChangeListener
val authenticatedUser = auth.authenticatedUser.value ?: return@setOnCheckedChangeListener
if (authenticatedUser.id != user.id) {
ManageBiometricAuthDeniedNotOwnerDialog.newInstance(userName = user.name).show(fragmentManager)
} else if (!view.biometricAuthEnabled) {
when (BiometricManager.from(fragment.requireContext()).canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) {
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED ->
EnableBiometricAuthDeniedNoCredentialsDialog.newInstance().show(fragmentManager)
else ->
EnableBiometricAuthConfirmDialog.newInstance(userId = user.id, userName = user.name).show(fragmentManager)
}
} else {
if (auth.getAuthenticatedUser()?.authenticatedBy == AuthenticationMethod.Password || auth.getAuthenticatedUser()?.isPasswordDisabled == true) {
toggleUserFlag()
} else {
DisableBiometricAuthDeniedPasswordRequiredDialog.newInstance().show(fragmentManager)
}
}
}
}
view.titleView.bindHelpDialog(
titleRes = R.string.biometric_manage_title,
textRes = R.string.biometric_manage_info,
fragmentManager = fragmentManager
)
}
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M17.81,4.47c-0.08,0 -0.16,-0.02 -0.23,-0.06C15.66,3.42 14,3 12.01,3c-1.98,0 -3.86,0.47 -5.57,1.41 -0.24,0.13 -0.54,0.04 -0.68,-0.2 -0.13,-0.24 -0.04,-0.55 0.2,-0.68C7.82,2.52 9.86,2 12.01,2c2.13,0 3.99,0.47 6.03,1.52 0.25,0.13 0.34,0.43 0.21,0.67 -0.09,0.18 -0.26,0.28 -0.44,0.28zM3.5,9.72c-0.1,0 -0.2,-0.03 -0.29,-0.09 -0.23,-0.16 -0.28,-0.47 -0.12,-0.7 0.99,-1.4 2.25,-2.5 3.75,-3.27C9.98,4.04 14,4.03 17.15,5.65c1.5,0.77 2.76,1.86 3.75,3.25 0.16,0.22 0.11,0.54 -0.12,0.7 -0.23,0.16 -0.54,0.11 -0.7,-0.12 -0.9,-1.26 -2.04,-2.25 -3.39,-2.94 -2.87,-1.47 -6.54,-1.47 -9.4,0.01 -1.36,0.7 -2.5,1.7 -3.4,2.96 -0.08,0.14 -0.23,0.21 -0.39,0.21zM9.75,21.79c-0.13,0 -0.26,-0.05 -0.35,-0.15 -0.87,-0.87 -1.34,-1.43 -2.01,-2.64 -0.69,-1.23 -1.05,-2.73 -1.05,-4.34 0,-2.97 2.54,-5.39 5.66,-5.39s5.66,2.42 5.66,5.39c0,0.28 -0.22,0.5 -0.5,0.5s-0.5,-0.22 -0.5,-0.5c0,-2.42 -2.09,-4.39 -4.66,-4.39 -2.57,0 -4.66,1.97 -4.66,4.39 0,1.44 0.32,2.77 0.93,3.85 0.64,1.15 1.08,1.64 1.85,2.42 0.19,0.2 0.19,0.51 0,0.71 -0.11,0.1 -0.24,0.15 -0.37,0.15zM16.92,19.94c-1.19,0 -2.24,-0.3 -3.1,-0.89 -1.49,-1.01 -2.38,-2.65 -2.38,-4.39 0,-0.28 0.22,-0.5 0.5,-0.5s0.5,0.22 0.5,0.5c0,1.41 0.72,2.74 1.94,3.56 0.71,0.48 1.54,0.71 2.54,0.71 0.24,0 0.64,-0.03 1.04,-0.1 0.27,-0.05 0.53,0.13 0.58,0.41 0.05,0.27 -0.13,0.53 -0.41,0.58 -0.57,0.11 -1.07,0.12 -1.21,0.12zM14.91,22c-0.04,0 -0.09,-0.01 -0.13,-0.02 -1.59,-0.44 -2.63,-1.03 -3.72,-2.1 -1.4,-1.39 -2.17,-3.24 -2.17,-5.22 0,-1.62 1.38,-2.94 3.08,-2.94 1.7,0 3.08,1.32 3.08,2.94 0,1.07 0.93,1.94 2.08,1.94s2.08,-0.87 2.08,-1.94c0,-3.77 -3.25,-6.83 -7.25,-6.83 -2.84,0 -5.44,1.58 -6.61,4.03 -0.39,0.81 -0.59,1.76 -0.59,2.8 0,0.78 0.07,2.01 0.67,3.61 0.1,0.26 -0.03,0.55 -0.29,0.64 -0.26,0.1 -0.55,-0.04 -0.64,-0.29 -0.49,-1.31 -0.73,-2.61 -0.73,-3.96 0,-1.2 0.23,-2.29 0.68,-3.24 1.33,-2.79 4.28,-4.6 7.51,-4.6 4.55,0 8.25,3.51 8.25,7.83 0,1.62 -1.38,2.94 -3.08,2.94s-3.08,-1.32 -3.08,-2.94c0,-1.07 -0.93,-1.94 -2.08,-1.94s-2.08,0.87 -2.08,1.94c0,1.71 0.66,3.31 1.87,4.51 0.95,0.94 1.86,1.46 3.27,1.85 0.27,0.07 0.42,0.35 0.35,0.61 -0.05,0.23 -0.26,0.38 -0.47,0.38z"/>
</vector>

View file

@ -1,5 +1,6 @@
<!-- <!--
Open TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann Open TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
Copyright <C> 2020 Marcel Voigt
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@ -74,6 +75,9 @@
<include android:id="@+id/user_key" <include android:id="@+id/user_key"
layout="@layout/manage_user_key_view" /> layout="@layout/manage_user_key_view" />
<include android:id="@+id/biometric_auth"
layout="@layout/manage_user_biometric_auth_view" />
<include android:id="@+id/timezone" <include android:id="@+id/timezone"
layout="@layout/user_timezone_view" /> layout="@layout/user_timezone_view" />

View file

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2020 Marcel Voigt
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="title"
type="String" />
<variable
name="text"
type="String" />
<variable
name="positiveButtonText"
type="String" />
<variable
name="negativeButtonText"
type="String" />
<variable
name="handler"
type="io.timelimit.android.ui.manage.parent.password.biometric.ManageBiometricAuthDialogHandler" />
<import type="android.view.View" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="8dp"
android:paddingTop="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{title}"
android:textAppearance="?android:textAppearanceLarge"
tools:text="@string/biometric_manage_enable_dialog_title" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{text}"
android:textAppearance="?android:textAppearanceMedium"
tools:text="@string/biometric_manage_enable_dialog_text" />
<LinearLayout
style="?buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal">
<Button
style="?buttonBarNegativeButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> handler.onNegativeButtonClicked()}"
android:text="@{negativeButtonText}"
android:visibility="@{negativeButtonText.isEmpty() ? View.GONE : View.VISIBLE}"
tools:text="Cancel" />
<Button
style="?buttonBarPositiveButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> handler.onPositiveButtonClicked()}"
android:text="@{positiveButtonText}"
android:visibility="@{positiveButtonText.isEmpty() ? View.GONE : View.VISIBLE}"
tools:text="Activate" />
</LinearLayout>
</LinearLayout>
</layout>

View file

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2020 Marcel Voigt
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="userName"
type="String" />
<variable
name="biometricAuthEnabled"
type="boolean" />
<variable
name="errorText"
type="String" />
<import type="android.view.View" />
</data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/title_view"
android:text="@string/biometric_manage_title"
android:textAppearance="?android:textAppearanceLarge"
android:background="?selectableItemBackground"
app:drawableEndCompat="@drawable/ic_info_outline_black_24dp"
app:drawableTint="?colorOnSurface"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:text="@{biometricAuthEnabled ? @string/biometric_manage_description_enabled(userName) : @string/biometric_manage_description_disabled(userName)}"
tools:text="@string/biometric_manage_description_disabled"
android:textAppearance="?android:textAppearanceMedium"
android:visibility="@{errorText == null || errorText.isEmpty() ? View.VISIBLE : View.GONE}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:text="@{errorText}"
android:textAppearance="?android:textAppearanceMedium"
android:visibility="@{errorText == null || errorText.isEmpty() ? View.GONE : View.VISIBLE}"
app:drawableStartCompat="@android:drawable/ic_dialog_alert"
app:drawableTint="?android:attr/textColorSecondary"
android:drawablePadding="8dp"
tools:text="@string/biometric_manage_error_hw_not_available"
tools:visibility="visible"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/toggle_biometric_auth_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/biometric_manage_switch_text"
android:checked="@{biometricAuthEnabled}"
android:enabled="@{errorText == null || errorText.isEmpty()}" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</layout>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
Copyright <C> 2020 Marcel Voigt
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License. the Free Software Foundation version 3 of the License.
@ -18,6 +19,9 @@
<variable <variable
name="showCustomKeyboard" name="showCustomKeyboard"
type="boolean" /> type="boolean" />
<variable
name="biometricAuthEnabled"
type="boolean" />
<import type="android.view.View" /> <import type="android.view.View" />
</data> </data>
@ -56,6 +60,16 @@
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" /> android:layout_height="48dp" />
<ImageButton
android:id="@+id/biometric_auth_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?selectableItemBackground"
android:contentDescription="@string/biometric_login_button_description"
android:src="@drawable/ic_fingerprint_24dp"
android:tint="?colorOnBackground"
android:visibility="@{biometricAuthEnabled ? View.VISIBLE : View.GONE}" />
</LinearLayout> </LinearLayout>
<io.timelimit.android.ui.view.KeyboardView <io.timelimit.android.ui.view.KeyboardView

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
Open TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann Open TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
Copyright <C> 2020 Marcel Voigt
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@ -29,6 +30,8 @@
<string name="generic_no">Nein</string> <string name="generic_no">Nein</string>
<string name="generic_yes">Ja</string> <string name="generic_yes">Ja</string>
<string name="generic_skip">Überspringen</string> <string name="generic_skip">Überspringen</string>
<string name="generic_login">Anmelden</string>
<string name="generic_logout">Abmelden</string>
<string name="generic_swipe_to_dismiss">Sie können diesen Hinweis entfernen, indem Sie ihn zur Seite wischen</string> <string name="generic_swipe_to_dismiss">Sie können diesen Hinweis entfernen, indem Sie ihn zur Seite wischen</string>
@ -1077,4 +1080,24 @@
<string name="task_review_last_grant">Diese Aufgabe wurde zuletzt bestätigt am %s</string> <string name="task_review_last_grant">Diese Aufgabe wurde zuletzt bestätigt am %s</string>
<string name="dummy_app_unassigned_system_image_app">nicht zugeordnete Apps von der Systempartition</string> <string name="dummy_app_unassigned_system_image_app">nicht zugeordnete Apps von der Systempartition</string>
<string name="biometric_title">Biometrische Authentifizierung</string>
<string name="biometric_title_enable">Biometrische Authentifizierung aktivieren</string>
<string name="biometric_title_disable">Biometrische Authentifizierung deaktivieren</string>
<string name="biometric_enable_prompt_description">Bestätigen Sie die Verwendung Ihrer biometrischen Anmeldemerkmale, um diese für die Anmeldung als %s zuzulassen.</string>
<string name="biometric_login_prompt_description">Bestätigen Sie Ihre biometrischen Anmeldemerkmale, um sich als %s anzumelden.</string>
<string name="biometric_login_button_description">Biometrische Anmeldemerkmale erfassen</string>
<string name="biometric_auth_failed">Authentifizierung als %s mit biometrischen Anmeldemerkmalen fehlgeschlagen.</string>
<string name="biometric_auth_failed_reason">Grund: %s</string>
<string name="biometric_manage_description_enabled">%s kann sich mit den im Gerät hinterlegten biometrischen Merkmalen anmelden.</string>
<string name="biometric_manage_description_disabled">Biometrische Authententifizierung als %s ist nicht aktiv.</string>
<string name="biometric_manage_info">Erlaubt es sich an ein (Eltern-)Konto mit biometrischen Anmeldemerkmalen, die auf diesem Gerät registriert sind, anzumelden, z.B. einen Fingerabdruck scannen statt das Passwort einzugeben.</string>
<string name="biometric_manage_enable_dialog_text">Hiermit werden ALLE auf dem Gerät hinterlegten biometrischen Merkmale akzeptiert. Jede Person mit registrierten biometrischen Merkmalen könnte sich als %s anmelden, auch Kindkontonutzer oder in TimeLimit nicht erfasste Personen.</string>
<string name="biometric_manage_disable_password_required_dialog_text">Um zu verhindern, dass Sie sich selbst aus diesem Konto aussperren, ist es erforderlich sich mit dem Passwort dieses Kontos anzumelden, um die biometrische Authentifizierung zu deaktivieren.</string>
<string name="biometric_manage_not_owner_dialog_text">Es ist nicht erlaubt die Einstellungen zur biometrischen Authentifizierung für andere Nutzer zu ändern. Bitte melden Sie sich als %s an, um Zugriff zu bekommen.</string>
<string name="biometric_manage_no_credentials_dialog_text">Keine biometrischen Merkmale registriert. Es müssen erst biometrische Merkmale für dieses Gerät hinterlegt werden, um diese Funktion nutzen zu können.</string>
<string name="biometric_manage_no_credentials_dialog_action_default">Einstellungen öffnen</string>
<string name="biometric_manage_no_credentials_dialog_action_v28">Anmeldemerkmale erfassen</string>
<string name="biometric_manage_error_no_hw">Kein biometrisches Erfassungsgerät erkannt.</string>
<string name="biometric_manage_error_hw_not_available">Das Gerät zur biometrischen Merkmalserfassung ist gerade nicht verfügbar.</string>
</resources> </resources>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Open TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
Copyright <C> Marcel Voigt
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<resources>
<string name="biometric_manage_no_credentials_dialog_action" translatable="false">@string/biometric_manage_no_credentials_dialog_action_v28</string>
</resources>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
Open TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann Open TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
Copyright <C> 2020 Marcel Voigt
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@ -29,6 +30,8 @@
<string name="generic_no">No</string> <string name="generic_no">No</string>
<string name="generic_yes">Yes</string> <string name="generic_yes">Yes</string>
<string name="generic_skip">Skip</string> <string name="generic_skip">Skip</string>
<string name="generic_login">Login</string>
<string name="generic_logout">Logout</string>
<string name="generic_swipe_to_dismiss">Swipe to the side to remove this message</string> <string name="generic_swipe_to_dismiss">Swipe to the side to remove this message</string>
@ -1019,7 +1022,7 @@
<string name="setup_terms_title">Welcome to TimeLimit</string> <string name="setup_terms_title">Welcome to TimeLimit</string>
<string name="about_terms_title">Legal</string> <string name="about_terms_title">Legal</string>
<string name="terms_text" translatable="false"> <string name="terms_text" translatable="false">
Open TimeLimit Copyright &#169; 2019 - 2021 Jonas Lochmann Open TimeLimit Copyright &#169; 2019 - 2021 Jonas Lochmann and the TimeLimit contributors
\n\nThis program is free software: you can redistribute it and/or modify \n\nThis program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@ -1126,4 +1129,33 @@
<string name="task_review_last_grant">This task was confirmed last time at %s</string> <string name="task_review_last_grant">This task was confirmed last time at %s</string>
<string name="dummy_app_unassigned_system_image_app">not assigned Apps from the system image</string> <string name="dummy_app_unassigned_system_image_app">not assigned Apps from the system image</string>
<string name="biometric_title">Biometric Authentication</string>
<string name="biometric_title_enable">Enable Biometric Authentication</string>
<string name="biometric_title_disable">Disable Biometric Authentication</string>
<string name="biometric_enable_prompt_title" translatable="false">@string/biometric_title_enable</string>
<string name="biometric_enable_prompt_description">Authenticate using your biometric credentials to allow them to be used to login as %s.</string>
<string name="biometric_login_prompt_title" translatable="false">@string/generic_login</string>
<string name="biometric_login_prompt_description">Authenticate using your biometric credentials to login as %s.</string>
<string name="biometric_login_button_description">Prompt for biometric credentials</string>
<string name="biometric_auth_failed">Failed to authenticate as %s using biometric credentials.</string>
<string name="biometric_auth_failed_reason">Reason: %s</string>
<string name="biometric_manage_title" translatable="false">@string/biometric_title</string>
<string name="biometric_manage_description_enabled">%s can login using the biometric credentials registered on the device.</string>
<string name="biometric_manage_description_disabled">%s has no biometric authentication enabled.</string>
<string name="biometric_manage_switch_text" translatable="false">@string/biometric_title_enable</string>
<string name="biometric_manage_info">Allows a (parent) user to login using the configured biometric credentials of this device, e.g. scanning a fingerprint instead of typing the password.</string>
<string name="biometric_manage_enable_dialog_title" translatable="false">@string/biometric_title_enable</string>
<string name="biometric_manage_enable_dialog_text">This enables ALL biometric credentials enrolled on this device to be accepted. Everyone who has biometric credentials registered could login as %s, even child account users or persons not managed by TimeLimit.</string>
<string name="biometric_manage_disable_password_required_dialog_title" translatable="false">@string/biometric_title_disable</string>
<string name="biometric_manage_disable_password_required_dialog_text">To avoid locking you out of your account please login using your password to allow disabling biometric authentication for this account.</string>
<string name="biometric_manage_not_owner_dialog_title" translatable="false">@string/biometric_title</string>
<string name="biometric_manage_not_owner_dialog_text">It is not permitted to change the biometric authentication settings of other users. Please login as %s to get access.</string>
<string name="biometric_manage_no_credentials_dialog_title" translatable="false">@string/biometric_title</string>
<string name="biometric_manage_no_credentials_dialog_text">No biometric credentials enrolled. First configure any biometric credentials for this device to use this feature.</string>
<string name="biometric_manage_no_credentials_dialog_action" translatable="false">@string/biometric_manage_no_credentials_dialog_action_default</string>
<string name="biometric_manage_no_credentials_dialog_action_default">Open Settings</string>
<string name="biometric_manage_no_credentials_dialog_action_v28">Enroll Credentials</string>
<string name="biometric_manage_error_no_hw">No biometric hardware detected.</string>
<string name="biometric_manage_error_hw_not_available">The biometric hardware is somehow not available at the moment.</string>
</resources> </resources>