From ce488ea536aa7874a8a00fe6ed80a5710d928e1b Mon Sep 17 00:00:00 2001 From: rebelonion <87634197+rebelonion@users.noreply.github.com> Date: Sat, 25 May 2024 10:08:11 -0500 Subject: [PATCH] feat: biometric | etc --- app/build.gradle | 2 + .../others/calc/BiometricPromptUtils.kt | 57 +++++++++++++++++++ .../ani/dantotsu/others/calc/CalcActivity.kt | 49 ++++++++++++++++ .../settings/SettingsCommonActivity.kt | 57 ++++++++++++++++++- .../dantotsu/settings/saving/Preferences.kt | 2 + .../main/res/layout/dialog_set_password.xml | 26 +++++++-- app/src/main/res/values/strings.xml | 5 ++ 7 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/ani/dantotsu/others/calc/BiometricPromptUtils.kt diff --git a/app/build.gradle b/app/build.gradle index 811cab900e..fe30452e98 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -101,6 +101,8 @@ dependencies { implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.webkit:webkit:1.11.0' implementation "com.anggrayudi:storage:1.5.5" + implementation "androidx.biometric:biometric:1.1.0" + // Glide ext.glide_version = '4.16.0' diff --git a/app/src/main/java/ani/dantotsu/others/calc/BiometricPromptUtils.kt b/app/src/main/java/ani/dantotsu/others/calc/BiometricPromptUtils.kt new file mode 100644 index 0000000000..6e91ed0756 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/others/calc/BiometricPromptUtils.kt @@ -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() +} diff --git a/app/src/main/java/ani/dantotsu/others/calc/CalcActivity.kt b/app/src/main/java/ani/dantotsu/others/calc/CalcActivity.kt index 442ab39629..a5d762d190 100644 --- a/app/src/main/java/ani/dantotsu/others/calc/CalcActivity.kt +++ b/app/src/main/java/ani/dantotsu/others/calc/CalcActivity.kt @@ -1,10 +1,14 @@ package ani.dantotsu.others.calc +import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.text.Spannable import android.text.SpannableString import android.text.style.ForegroundColorSpan +import android.view.MotionEvent import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat @@ -16,6 +20,8 @@ import ani.dantotsu.databinding.ActivityCalcBinding import ani.dantotsu.getThemeColor import ani.dantotsu.initActivity import ani.dantotsu.navBarHeight +import ani.dantotsu.settings.saving.PrefManager +import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.statusBarHeight import ani.dantotsu.themes.ThemeManager import ani.dantotsu.util.NumberConverter.Companion.toBinary @@ -24,7 +30,13 @@ import ani.dantotsu.util.NumberConverter.Companion.toHex class CalcActivity : AppCompatActivity() { private lateinit var binding: ActivityCalcBinding private lateinit var code: String + private val handler = Handler(Looper.getMainLooper()) + private val runnable = Runnable { + success() + } private val stack = CalcStack() + + @SuppressLint("ClickableViewAccessibility") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ThemeManager(this).applyTheme() @@ -73,6 +85,29 @@ class CalcActivity : AppCompatActivity() { binding.displayHex.text = "" 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 { stack.remove() 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() { hasPermission = true ContextCompat.startActivity( diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt index bd590873dc..c8dc29d551 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt @@ -8,9 +8,12 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.widget.ArrayAdapter +import android.widget.CheckBox import android.widget.EditText import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricManager +import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.documentfile.provider.DocumentFile import androidx.recyclerview.widget.LinearLayoutManager @@ -21,6 +24,7 @@ import ani.dantotsu.databinding.DialogUserAgentBinding import ani.dantotsu.download.DownloadsManager import ani.dantotsu.initActivity import ani.dantotsu.navBarHeight +import ani.dantotsu.others.calc.BiometricPromptUtils import ani.dantotsu.restartApp import ani.dantotsu.savePrefsToDownloads import ani.dantotsu.settings.saving.PrefManager @@ -39,6 +43,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.util.UUID class SettingsCommonActivity : AppCompatActivity() { @@ -183,14 +188,47 @@ class SettingsCommonActivity : AppCompatActivity() { (dialog as AlertDialog).findViewById(R.id.passwordInput) val confirmPasswordInput = dialog.findViewById(R.id.confirmPasswordInput) + val forgotPasswordCheckbox = + dialog.findViewById(R.id.forgotPasswordCheckbox) + if (forgotPasswordCheckbox?.isChecked == true) { + PrefManager.setVal(PrefName.OverridePassword, true) + } val password = passwordInput?.text.toString() val confirmPassword = confirmPasswordInput?.text.toString() if (password == confirmPassword && password.isNotEmpty()) { PrefManager.setVal(PrefName.AppPassword, password) - toast(R.string.success) - dialog.dismiss() + if (dialog.findViewById(R.id.biometricCheckbox)?.isChecked == true) { + 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 { toast(R.string.password_mismatch) } @@ -200,6 +238,8 @@ class SettingsCommonActivity : AppCompatActivity() { } .setNeutralButton(R.string.remove) { dialog, _ -> PrefManager.setVal(PrefName.AppPassword, "") + PrefManager.setVal(PrefName.BiometricToken, "") + PrefManager.setVal(PrefName.OverridePassword, false) toast(R.string.success) dialog.dismiss() } @@ -209,6 +249,19 @@ class SettingsCommonActivity : AppCompatActivity() { passwordDialog.setOnShowListener { passwordDialog.findViewById(R.id.passwordInput) ?.requestFocus() + val biometricCheckbox = + passwordDialog.findViewById(R.id.biometricCheckbox) + val forgotPasswordCheckbox = + passwordDialog.findViewById(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() } diff --git a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt index 1d69ebad10..fe6db422b3 100644 --- a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt +++ b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt @@ -202,4 +202,6 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files MALCodeChallenge(Pref(Location.Protected, String::class, "")), MALToken(Pref(Location.Protected, MAL.ResponseToken::class, "")), AppPassword(Pref(Location.Protected, String::class, "")), + BiometricToken(Pref(Location.Protected, String::class, "")), + OverridePassword(Pref(Location.Protected, Boolean::class, false)), } \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_set_password.xml b/app/src/main/res/layout/dialog_set_password.xml index 303a1bfb28..e9bb281f98 100644 --- a/app/src/main/res/layout/dialog_set_password.xml +++ b/app/src/main/res/layout/dialog_set_password.xml @@ -10,16 +10,34 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/enter_password" - android:maxLength="32" - android:inputType="numberPassword" /> + android:inputType="numberPassword" + android:maxLength="32" /> + android:maxLength="32" /> + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 00ae43c267..74ea81eb1b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1024,4 +1024,9 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc Lock the app with a password\n( ͡° ͜ʖ ͡°) Confirm Password Passwords do not match or are empty! + Enable Biometric + Biometric Authentication + Use your fingerprint or face to unlock the app + Enable Forgot Password (hold clear button for 10 seconds) +