rewrite and improve biometric authenticator

This commit is contained in:
IndusAryan 2024-02-19 00:15:16 +05:30
parent 0f80d9950c
commit b75441eb3a
6 changed files with 160 additions and 112 deletions

View file

@ -113,6 +113,7 @@ import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.SearchFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTruePhone
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
@ -133,9 +134,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.BackupUtils.backup
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
import com.lagradost.cloudstream3.utils.BiometricAuthenticator
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.TAG
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isTruePhone
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getKey
@ -1176,7 +1176,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false)
setContentView(newLocalBinding.root)
if(isTrueTvSettings() && ANIMATED_OUTLINE) {
if (isTrueTvSettings() && ANIMATED_OUTLINE) {
TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener {
TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)
@ -1188,14 +1188,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
newLocalBinding.focusOutline.isVisible = false
}
if(isTrueTvSettings()) {
if (isTrueTvSettings()) {
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
centerView(newFocus)
}
}
ActivityMainBinding.bind(newLocalBinding.root) // this may crash
} else {
val newLocalBinding = ActivityMainBinding.inflate(layoutInflater, null, false)
@ -1213,12 +1211,20 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_enabled_key), false)
val noAccounts = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false) || accounts.count() <= 1
if (isTruePhone() && authEnabled && noAccounts ) {
if (isTruePhone() && authEnabled && noAccounts) {
if (deviceHasPasswordPinLock(this)) {
startBiometricAuthentication(this,
R.string.biometric_authentication_title,
false
)
BiometricAuthenticator.initializeBiometrics(this@MainActivity, this)
BiometricAuthenticator.checkBiometricAvailability()
BiometricAuthenticator.biometricPrompt.authenticate(promptInfo)
binding?.navHostFragment?.isInvisible = true // hide background while authenticating
BiometricAuthenticator.biometricPrompt.authenticate(BiometricAuthenticator.promptInfo)
// hide background while authenticating, Sorry moms & dads 🙏
binding?.navHostFragment?.isInvisible = true
} else {
showToast(R.string.phone_not_secured, Toast.LENGTH_LONG)
}
}
// Automatically enable jsdelivr if cant connect to raw.githubusercontent.com
@ -1760,7 +1766,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
)
}
override fun onAuthenticationSuccess() { /** Biometric stuff **/
/** Biometric stuff **/
override fun onAuthenticationSuccess() {
// make background (nav host fragment) visible again
binding?.navHostFragment?.isInvisible = false
}

View file

@ -3,6 +3,8 @@ package com.lagradost.cloudstream3.ui.account
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
@ -17,8 +19,11 @@ import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_EDIT_ACCOUNT
import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_SELECT_ACCOUNT
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTruePhone
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.BiometricAuthenticator
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts
import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex
import com.lagradost.cloudstream3.utils.DataStoreHelper.setAccount
@ -43,19 +48,24 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_enabled_key), false)
val skipStartup = settingsManager.getBoolean(
getString(R.string.skip_startup_account_select_key),
false
val skipStartup = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false
) || accounts.count() <= 1
viewModel = ViewModelProvider(this)[AccountViewModel::class.java]
fun askBiometricAuth() {
if (BiometricAuthenticator.isTruePhone() && authEnabled) {
BiometricAuthenticator.initializeBiometrics(this@AccountSelectActivity, this)
BiometricAuthenticator.checkBiometricAvailability()
BiometricAuthenticator.biometricPrompt.authenticate(BiometricAuthenticator.promptInfo)
if (isTruePhone() && authEnabled) {
if (deviceHasPasswordPinLock(this)) {
startBiometricAuthentication(
this,
R.string.biometric_authentication_title,
false
)
BiometricAuthenticator.biometricPrompt.authenticate(BiometricAuthenticator.promptInfo)
}
} else {
showToast(R.string.phone_not_secured, Toast.LENGTH_LONG)
}
}
@ -178,7 +188,8 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
startActivity(mainIntent)
finish() // Finish the account selection activity
}
override fun onAuthenticationSuccess() {
//ask github.com/IndusAryan if confusion occurs
Log.i(BiometricAuthenticator.TAG,"Authentication successful in AccountSelectActivity")
}
}

View file

@ -19,6 +19,7 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.MainSettingsBinding
import com.lagradost.cloudstream3.mvvm.logError
@ -155,6 +156,11 @@ class SettingsFragment : Fragment() {
return getLayoutInt() == 2
}
// phone exclusive
fun isTruePhone(): Boolean {
return !isTrueTvSettings() && !isTvSettings() && context?.isEmulatorSettings() != true
}
private fun Context.isAutoTv(): Boolean {
val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
// AFT = Fire TV

View file

@ -5,7 +5,7 @@ import android.app.KeyguardManager
import android.content.Context
import android.os.Build
import android.util.Log
import android.widget.Toast
import android.widget.Toast.LENGTH_SHORT
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
@ -13,139 +13,164 @@ import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
object BiometricAuthenticator {
private const val MAX_FAILED_ATTEMPTS = 3
private var failedAttempts = 0
const val TAG = "cs3Auth"
private lateinit var biometricManager: BiometricManager
lateinit var biometricPrompt: BiometricPrompt
lateinit var promptInfo: BiometricPrompt.PromptInfo
const val TAG = "cs3Auth"
private var authCallback: BiometricAuthCallback? = null
var authCallback: BiometricAuthCallback? = null // listen to authentication success
fun initializeBiometrics(activity: Activity, callback: BiometricAuthCallback) {
private fun initializeBiometrics(activity: Activity) {
val executor = ContextCompat.getMainExecutor(activity)
authCallback = callback // to listen success authentication
biometricManager = BiometricManager.from(activity)
biometricPrompt = BiometricPrompt(activity as AppCompatActivity, executor, object : BiometricPrompt.AuthenticationCallback() {
if (!::promptInfo.isInitialized) {
initBiometricPrompt(
activity as AppCompatActivity,
R.string.biometric_authentication_title,
false
)
}
biometricPrompt = BiometricPrompt(
activity as AppCompatActivity,
executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Log.d(TAG, "Authentication error: $errString")
showToast("$errString", LENGTH_SHORT)
Log.i(TAG, "$errorCode")
activity.finish()
failedAttempts++
if (failedAttempts >= MAX_FAILED_ATTEMPTS) {
failedAttempts = 0
activity.finish()
} else {
failedAttempts = 0
MainActivity().finish()
}
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Log.d(TAG, "Biometric succeeded.")
unblockMain() //to make background visible after authenticating
failedAttempts = 0
authCallback?.onAuthenticationSuccess()
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Log.d(TAG, "Authentication error")
showToast(R.string.biometric_failed, Toast.LENGTH_SHORT)
activity.finish()
failedAttempts++
if (failedAttempts >= MAX_FAILED_ATTEMPTS) {
failedAttempts = 0
activity.finish()
}
}
})
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
private fun initBiometricPrompt(
activity: Activity,
title: Int,
setDeviceCred: Boolean,
) {
val description = activity.getString(R.string.biometric_prompt_description)
if (setDeviceCred) {
// For API level > 30, Newer API setAllowedAuthenticators is used
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock CloudStream")
//.setSubtitle("Log in using your biometric credential")
//.setNegativeButtonText("Use account password")
.setAllowedAuthenticators(BIOMETRIC_WEAK or BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
.build()
} else if (context?.let { deviceHasPasswordPinLock(it) } == true) {
val authFlag = DEVICE_CREDENTIAL or BIOMETRIC_WEAK or BIOMETRIC_STRONG
promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(activity.getString(title))
.setDescription(description)
.setAllowedAuthenticators(authFlag)
.build()
} else {
@Suppress("DEPRECATION")
promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(activity.getString(title))
.setDescription(description)
.setDeviceCredentialAllowed(true)
.build()
}
} else {
// fallback for A12+ when both fingerprint & Face unlock is absent but PIN is set
@Suppress("DEPRECATION")
promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock CloudStream")
.setTitle(activity.getString(title))
.setDescription(description)
.setDeviceCredentialAllowed(true)
.build()
}
}
fun checkBiometricAvailability() {
private fun isBiometricHardWareAvailable(): Boolean {
var result = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Strong and device credential bundle cannot be checked at same time in API < A11 (R)
when (biometricManager.canAuthenticate(BIOMETRIC_WEAK or BIOMETRIC_STRONG or DEVICE_CREDENTIAL)) {
BiometricManager.BIOMETRIC_SUCCESS ->
Log.d(TAG, "App can authenticate.")
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE ->
Log.d(TAG, "No biometric sensor found.")
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE ->
Log.d(TAG, "Biometric authentication is currently unavailable.")
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED ->
showToast(R.string.biometric_not_enrolled, Toast.LENGTH_LONG)
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED ->
showToast(R.string.biometric_update_required, Toast.LENGTH_LONG)
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED ->
showToast(R.string.biometric_unsupported, Toast.LENGTH_SHORT)
BiometricManager.BIOMETRIC_STATUS_UNKNOWN ->
Log.e(TAG, "Unknown error encountered while authenticating fingerprint.")
when (biometricManager.canAuthenticate(
DEVICE_CREDENTIAL or BIOMETRIC_STRONG or BIOMETRIC_WEAK
)) {
BiometricManager.BIOMETRIC_SUCCESS -> result = true
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> result = false
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> result = false
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> result = false
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> result = true
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> result = true
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> result = false
}
} else {
@Suppress("DEPRECATION")
when (biometricManager.canAuthenticate()) {
BiometricManager.BIOMETRIC_SUCCESS -> result = true
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> result = false
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> result = false
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> result = false
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> result = true
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> result = true
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> result = false
}
}
else {
return result
}
when (biometricManager.canAuthenticate(BIOMETRIC_WEAK or BIOMETRIC_STRONG)) {
// only needed for Android 9 and below
fun deviceHasPasswordPinLock(context: Context?): Boolean {
val keyMgr =
context?.getSystemService(AppCompatActivity.KEYGUARD_SERVICE) as KeyguardManager
return keyMgr.isKeyguardSecure
}
BiometricManager.BIOMETRIC_SUCCESS ->
Log.d(TAG, "App can authenticate using biometrics.")
fun startBiometricAuthentication(activity: Activity, title: Int, setDeviceCred: Boolean) {
initializeBiometrics(activity)
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE ->
Log.d(TAG, "No biometric features available on this device.")
if (isBiometricHardWareAvailable()) {
authCallback = activity as? BiometricAuthCallback
initBiometricPrompt(activity, title, setDeviceCred)
biometricPrompt.authenticate(promptInfo)
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE ->
Log.e(TAG, "Biometric features are currently unavailable.")
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED ->
showToast(R.string.biometric_not_enrolled, Toast.LENGTH_LONG)
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED ->
showToast(R.string.biometric_update_required, Toast.LENGTH_LONG)
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED ->
showToast(R.string.biometric_unsupported, Toast.LENGTH_SHORT)
BiometricManager.BIOMETRIC_STATUS_UNKNOWN ->
Log.d(TAG, "Unknown error encountered while authenticating fingerprint.")
} else {
if (deviceHasPasswordPinLock(activity)) {
authCallback = activity as? BiometricAuthCallback
initBiometricPrompt(activity, R.string.password_pin_authentication_title, true)
biometricPrompt.authenticate(promptInfo)
} else {
showToast(R.string.biometric_unsupported, LENGTH_SHORT)
}
}
}
// yes, this feature is phone exclusive
fun isTruePhone(): Boolean {
return !isTrueTvSettings() && !isTvSettings() && context?.isEmulatorSettings() != true
}
fun unblockMain() {
authCallback?.onAuthenticationSuccess()
}
private fun deviceHasPasswordPinLock(con: Context): Boolean {
val keyManager = con.getSystemService(AppCompatActivity.KEYGUARD_SERVICE) as KeyguardManager
return keyManager.isKeyguardSecure
}
interface BiometricAuthCallback {
fun onAuthenticationSuccess()
}

View file

@ -532,7 +532,6 @@ object UIHelper {
WindowInsetsControllerCompat(window, View(this)).show(WindowInsetsCompat.Type.systemBars())
} else {*/ /** WINDOW COMPAT IS BUGGY DUE TO FU*KED UP PLAYER AND TRAILERS **/
Suppress("DEPRECATION")
window.decorView.systemUiVisibility =
(View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
//}

View file

@ -745,13 +745,13 @@
<string name="auto_rotate_video_desc">Enable automatic switching of screen orientation based on video orientation</string>
<string name="auto_rotate_video">Auto rotate</string>
<!-- For Biometrics -->
<string name="biometric_authentication_title">Unlock CloudStream</string>
<string name="biometric_setting">Fingerprint sensor authentication</string>
<string name="biometric_success">Fingerprint authentication succeeded</string>
<string name="biometric_failed">Fingerprint authentication failed</string>
<string name="biometric_prompt_title">Unlock CloudStream</string>
<string name="biometric_not_enrolled">Please add fingerprint or screen lock or disable setting</string>
<string name="biometric_update_required">Please update software and security patches.</string>
<string name="biometric_unsupported">Biometric authentication is not supported on this device</string>
<string name="biometric_enabled_key" translatable="false">biometric_key</string>
<string name="password_pin_authentication_title">Password/PIN Authentication</string>
<string name="biometric_unsupported">Biometric authentication is not supported on this device</string>
<string name="phone_not_secured">Please disable fingerprint authentication if device has no lock.</string>
<string name="biometric_prompt_description">This window will close after few failed attempts. You\'ll have to restart the App.</string>
</resources>