feat: biometric | etc

This commit is contained in:
rebelonion 2024-05-25 10:08:11 -05:00
parent 7717974b9e
commit ce488ea536
7 changed files with 192 additions and 6 deletions

View file

@ -101,6 +101,8 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.webkit:webkit:1.11.0' implementation 'androidx.webkit:webkit:1.11.0'
implementation "com.anggrayudi:storage:1.5.5" implementation "com.anggrayudi:storage:1.5.5"
implementation "androidx.biometric:biometric:1.1.0"
// Glide // Glide
ext.glide_version = '4.16.0' ext.glide_version = '4.16.0'

View file

@ -0,0 +1,57 @@
package ani.dantotsu.others.calc
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import ani.dantotsu.R
import ani.dantotsu.util.Logger
object BiometricPromptUtils {
private const val TAG = "BiometricPromptUtils"
/**
* Create a BiometricPrompt instance
* @param activity: AppCompatActivity
* @param processSuccess: success callback
*/
fun createBiometricPrompt(
activity: AppCompatActivity,
processSuccess: (BiometricPrompt.AuthenticationResult) -> Unit
): BiometricPrompt {
val executor = ContextCompat.getMainExecutor(activity)
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errCode: Int, errString: CharSequence) {
super.onAuthenticationError(errCode, errString)
Logger.log("$TAG errCode is $errCode and errString is: $errString")
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Logger.log("$TAG User biometric rejected.")
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Log.d(TAG, "Authentication was successful")
processSuccess(result)
}
}
return BiometricPrompt(activity, executor, callback)
}
/**
* Create a BiometricPrompt.PromptInfo instance
* @param activity: AppCompatActivity
* @return BiometricPrompt.PromptInfo: instance
*/
fun createPromptInfo(activity: AppCompatActivity): BiometricPrompt.PromptInfo =
BiometricPrompt.PromptInfo.Builder().apply {
setTitle(activity.getString(R.string.bio_prompt_info_title))
setDescription(activity.getString(R.string.bio_prompt_info_desc))
setConfirmationRequired(false)
setNegativeButtonText(activity.getString(R.string.cancel))
}.build()
}

View file

@ -1,10 +1,14 @@
package ani.dantotsu.others.calc package ani.dantotsu.others.calc
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.view.MotionEvent
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -16,6 +20,8 @@ import ani.dantotsu.databinding.ActivityCalcBinding
import ani.dantotsu.getThemeColor import ani.dantotsu.getThemeColor
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.NumberConverter.Companion.toBinary import ani.dantotsu.util.NumberConverter.Companion.toBinary
@ -24,7 +30,13 @@ import ani.dantotsu.util.NumberConverter.Companion.toHex
class CalcActivity : AppCompatActivity() { class CalcActivity : AppCompatActivity() {
private lateinit var binding: ActivityCalcBinding private lateinit var binding: ActivityCalcBinding
private lateinit var code: String private lateinit var code: String
private val handler = Handler(Looper.getMainLooper())
private val runnable = Runnable {
success()
}
private val stack = CalcStack() private val stack = CalcStack()
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
@ -73,6 +85,29 @@ class CalcActivity : AppCompatActivity() {
binding.displayHex.text = "" binding.displayHex.text = ""
binding.display.text = "0" binding.display.text = "0"
} }
if (PrefManager.getVal(PrefName.OverridePassword, false)) {
buttonClear.setOnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
handler.postDelayed(runnable, 10000)
true
}
MotionEvent.ACTION_UP -> {
v.performClick()
handler.removeCallbacks(runnable)
true
}
MotionEvent.ACTION_CANCEL -> {
handler.removeCallbacks(runnable)
true
}
else -> false
}
}
}
buttonBackspace.setOnClickListener { buttonBackspace.setOnClickListener {
stack.remove() stack.remove()
updateDisplay() updateDisplay()
@ -81,6 +116,20 @@ class CalcActivity : AppCompatActivity() {
} }
} }
override fun onResume() {
super.onResume()
if (hasPermission) {
success()
}
if (PrefManager.getVal(PrefName.BiometricToken, "").isNotEmpty()) {
val bioMetricPrompt = BiometricPromptUtils.createBiometricPrompt(this) {
success()
}
val promptInfo = BiometricPromptUtils.createPromptInfo(this)
bioMetricPrompt.authenticate(promptInfo)
}
}
private fun success() { private fun success() {
hasPermission = true hasPermission = true
ContextCompat.startActivity( ContextCompat.startActivity(

View file

@ -8,9 +8,12 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.EditText import android.widget.EditText
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -21,6 +24,7 @@ import ani.dantotsu.databinding.DialogUserAgentBinding
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.others.calc.BiometricPromptUtils
import ani.dantotsu.restartApp import ani.dantotsu.restartApp
import ani.dantotsu.savePrefsToDownloads import ani.dantotsu.savePrefsToDownloads
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
@ -39,6 +43,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.UUID
class SettingsCommonActivity : AppCompatActivity() { class SettingsCommonActivity : AppCompatActivity() {
@ -183,14 +188,47 @@ class SettingsCommonActivity : AppCompatActivity() {
(dialog as AlertDialog).findViewById<EditText>(R.id.passwordInput) (dialog as AlertDialog).findViewById<EditText>(R.id.passwordInput)
val confirmPasswordInput = val confirmPasswordInput =
dialog.findViewById<EditText>(R.id.confirmPasswordInput) dialog.findViewById<EditText>(R.id.confirmPasswordInput)
val forgotPasswordCheckbox =
dialog.findViewById<CheckBox>(R.id.forgotPasswordCheckbox)
if (forgotPasswordCheckbox?.isChecked == true) {
PrefManager.setVal(PrefName.OverridePassword, true)
}
val password = passwordInput?.text.toString() val password = passwordInput?.text.toString()
val confirmPassword = confirmPasswordInput?.text.toString() val confirmPassword = confirmPasswordInput?.text.toString()
if (password == confirmPassword && password.isNotEmpty()) { if (password == confirmPassword && password.isNotEmpty()) {
PrefManager.setVal(PrefName.AppPassword, password) PrefManager.setVal(PrefName.AppPassword, password)
toast(R.string.success) if (dialog.findViewById<CheckBox>(R.id.biometricCheckbox)?.isChecked == true) {
dialog.dismiss() val canBiometricPrompt =
BiometricManager.from(applicationContext)
.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_WEAK
) == BiometricManager.BIOMETRIC_SUCCESS
if (canBiometricPrompt) {
val biometricPrompt =
BiometricPromptUtils.createBiometricPrompt(
this@SettingsCommonActivity
) { _ ->
val token = UUID.randomUUID().toString()
PrefManager.setVal(
PrefName.BiometricToken,
token
)
toast(R.string.success)
dialog.dismiss()
}
val promptInfo =
BiometricPromptUtils.createPromptInfo(
this@SettingsCommonActivity
)
biometricPrompt.authenticate(promptInfo)
}
} else {
PrefManager.setVal(PrefName.BiometricToken, "")
toast(R.string.success)
dialog.dismiss()
}
} else { } else {
toast(R.string.password_mismatch) toast(R.string.password_mismatch)
} }
@ -200,6 +238,8 @@ class SettingsCommonActivity : AppCompatActivity() {
} }
.setNeutralButton(R.string.remove) { dialog, _ -> .setNeutralButton(R.string.remove) { dialog, _ ->
PrefManager.setVal(PrefName.AppPassword, "") PrefManager.setVal(PrefName.AppPassword, "")
PrefManager.setVal(PrefName.BiometricToken, "")
PrefManager.setVal(PrefName.OverridePassword, false)
toast(R.string.success) toast(R.string.success)
dialog.dismiss() dialog.dismiss()
} }
@ -209,6 +249,19 @@ class SettingsCommonActivity : AppCompatActivity() {
passwordDialog.setOnShowListener { passwordDialog.setOnShowListener {
passwordDialog.findViewById<EditText>(R.id.passwordInput) passwordDialog.findViewById<EditText>(R.id.passwordInput)
?.requestFocus() ?.requestFocus()
val biometricCheckbox =
passwordDialog.findViewById<CheckBox>(R.id.biometricCheckbox)
val forgotPasswordCheckbox =
passwordDialog.findViewById<CheckBox>(R.id.forgotPasswordCheckbox)
val canAuthenticate =
BiometricManager.from(applicationContext).canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_WEAK
) == BiometricManager.BIOMETRIC_SUCCESS
biometricCheckbox?.isVisible = canAuthenticate
biometricCheckbox?.isChecked =
PrefManager.getVal(PrefName.BiometricToken, "").isNotEmpty()
forgotPasswordCheckbox?.isChecked =
PrefManager.getVal(PrefName.OverridePassword)
} }
passwordDialog.show() passwordDialog.show()
} }

View file

@ -202,4 +202,6 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
MALCodeChallenge(Pref(Location.Protected, String::class, "")), MALCodeChallenge(Pref(Location.Protected, String::class, "")),
MALToken(Pref(Location.Protected, MAL.ResponseToken::class, "")), MALToken(Pref(Location.Protected, MAL.ResponseToken::class, "")),
AppPassword(Pref(Location.Protected, String::class, "")), AppPassword(Pref(Location.Protected, String::class, "")),
BiometricToken(Pref(Location.Protected, String::class, "")),
OverridePassword(Pref(Location.Protected, Boolean::class, false)),
} }

View file

@ -10,16 +10,34 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/enter_password" android:hint="@string/enter_password"
android:maxLength="32" android:inputType="numberPassword"
android:inputType="numberPassword" /> android:maxLength="32" />
<EditText <EditText
android:id="@+id/confirmPasswordInput" android:id="@+id/confirmPasswordInput"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/confirm_password" android:hint="@string/confirm_password"
android:inputType="numberPassword" android:inputType="numberPassword"
android:maxLength="32" android:maxLength="32" />
android:layout_marginTop="16dp" />
<CheckBox
android:id="@+id/biometricCheckbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="@font/poppins"
android:text="@string/enable_biometric"
android:textSize="16sp" />
<CheckBox
android:id="@+id/forgotPasswordCheckbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="@font/poppins"
android:text="@string/enable_forgot_password"
android:textSize="16sp" />
</LinearLayout> </LinearLayout>

View file

@ -1024,4 +1024,9 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
<string name="app_lock_desc">Lock the app with a password\n( ͡° ͜ʖ ͡°)</string> <string name="app_lock_desc">Lock the app with a password\n( ͡° ͜ʖ ͡°)</string>
<string name="confirm_password">Confirm Password</string> <string name="confirm_password">Confirm Password</string>
<string name="password_mismatch">Passwords do not match or are empty!</string> <string name="password_mismatch">Passwords do not match or are empty!</string>
<string name="enable_biometric">Enable Biometric</string>
<string name="bio_prompt_info_title">Biometric Authentication</string>
<string name="bio_prompt_info_desc">Use your fingerprint or face to unlock the app</string>
<string name="enable_forgot_password">Enable Forgot Password (hold clear button for 10 seconds)</string>
</resources> </resources>