Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): authenticate first when enabling security settings #991

Merged
merged 6 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ repositories {
dependencies {
// Testing
testImplementation("junit:junit:4.13.2")
testImplementation("org.json:json:20231013")
testImplementation("org.json:json:20240303")
androidTestImplementation("androidx.test:core")
implementation("androidx.test.ext:junit-ktx:1.1.5")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
Expand Down
14 changes: 10 additions & 4 deletions app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,10 @@ 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.biometricPrompt
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
Expand Down Expand Up @@ -1231,18 +1234,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener,
changeStatusBarState(isLayout(EMULATOR))

/** Biometric stuff for users without accounts **/
val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false)
val noAccounts = settingsManager.getBoolean(
getString(R.string.skip_startup_account_select_key),
false
) || accounts.count() <= 1

if (isLayout(PHONE) && authEnabled && noAccounts) {
if (isLayout(PHONE) && isAuthEnabled(this) && noAccounts) {
if (deviceHasPasswordPinLock(this)) {
startBiometricAuthentication(this, R.string.biometric_authentication_title, false)

BiometricAuthenticator.promptInfo?.let { promt ->
BiometricAuthenticator.biometricPrompt?.authenticate(promt)
promptInfo?.let { prompt ->
biometricPrompt?.authenticate(prompt)
}

// hide background while authenticating, Sorry moms & dads 🙏
Expand Down Expand Up @@ -1827,6 +1829,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener,
binding?.navHostFragment?.isInvisible = false
}

override fun onAuthenticationError() {
finish()
}

private var backPressedCallback: OnBackPressedCallback? = null

private fun attachBackPressedCallback() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.BiometricAuthenticator
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts
import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex
Expand All @@ -48,24 +51,23 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
)

val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_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 (isLayout(PHONE) && authEnabled) {
if (isLayout(PHONE) && isAuthEnabled(this)) {
if (deviceHasPasswordPinLock(this)) {
startBiometricAuthentication(
this,
R.string.biometric_authentication_title,
false
)

BiometricAuthenticator.promptInfo?.let { promt ->
BiometricAuthenticator.biometricPrompt?.authenticate(promt)
promptInfo?.let { prompt ->
biometricPrompt?.authenticate(prompt)
}
}
}
Expand Down Expand Up @@ -189,4 +191,8 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
override fun onAuthenticationSuccess() {
Log.i(BiometricAuthenticator.TAG,"Authentication successful in AccountSelectActivity")
}

override fun onAuthenticationError() {
finish()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
Expand All @@ -30,6 +31,7 @@ import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
Expand All @@ -38,13 +40,20 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setTool
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.BackupUtils
import com.lagradost.cloudstream3.utils.BiometricAuthenticator
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.authCallback
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.setImage

class SettingsAccount : PreferenceFragmentCompat() {
class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.BiometricAuthCallback {
companion object {
/** Used by nginx plugin too */
fun showLoginInfo(
Expand Down Expand Up @@ -252,6 +261,31 @@ class SettingsAccount : PreferenceFragmentCompat() {
}
}

private fun updateAuthPreference(enabled: Boolean) {
val biometricKey = getString(R.string.biometric_key)

PreferenceManager.getDefaultSharedPreferences(context ?: requireContext()).edit()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove requireContext

.putBoolean(biometricKey, enabled).apply()
findPreference<SwitchPreferenceCompat>(biometricKey)?.isChecked = enabled
}

override fun onAuthenticationError() {
updateAuthPreference(!isAuthEnabled(context ?: requireContext()))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove requireContext

}

override fun onAuthenticationSuccess() {
if (isAuthEnabled(context?: return)) {
updateAuthPreference(true)
BackupUtils.backup(activity)
activity?.showBottomDialogText(
getString(R.string.biometric_setting),
getString(R.string.biometric_warning).html()
) { onDialogDismissedEvent }
} else {
updateAuthPreference(false)
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpToolbar(R.string.category_account)
Expand All @@ -262,23 +296,27 @@ class SettingsAccount : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
hideKeyboard()
setPreferencesFromResource(R.xml.settings_account, rootKey)
// hide preference on tvs and emulators
if (!isLayout(PHONE)) {
getPref(R.string.biometric_key)?.isEnabled = false
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getPref(R.string.biometric_key)?.isEnabled = isLayout(PHONE)

}

getPref(R.string.biometric_key)?.setOnPreferenceClickListener {
val authEnabled = PreferenceManager.getDefaultSharedPreferences(
context ?: return@setOnPreferenceClickListener false
)
.getBoolean(getString(R.string.biometric_key), false)

if (authEnabled) {
BackupUtils.backup(activity)
val title = activity?.getString(R.string.biometric_setting)
val warning = activity?.getString(R.string.biometric_warning)
activity?.showBottomDialogText(
title as String,
warning.html()
) { onDialogDismissedEvent }
val ctx = context ?: return@setOnPreferenceClickListener false

if (deviceHasPasswordPinLock(ctx)) {
startBiometricAuthentication(
activity?: requireActivity(),
R.string.biometric_authentication_title,
false
)
promptInfo?.let {
authCallback = this
biometricPrompt?.authenticate(it)
}
}
true

false
}

val syncApis =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@ import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getString
import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R

object BiometricAuthenticator {

const val TAG = "cs3Auth"
private const val MAX_FAILED_ATTEMPTS = 3
private var failedAttempts = 0
const val TAG = "cs3Auth"

private var biometricManager: BiometricManager? = null
var biometricPrompt: BiometricPrompt? = null
var promptInfo: BiometricPrompt.PromptInfo? = null

var authCallback: BiometricAuthCallback? = null // listen to authentication success

private fun initializeBiometrics(activity: Activity) {
Expand All @@ -37,20 +37,12 @@ object BiometricAuthenticator {
activity as FragmentActivity,
executor,
object : BiometricPrompt.AuthenticationCallback() {

override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
showToast("$errString")
Log.e(TAG, "$errorCode")
failedAttempts++

if (failedAttempts >= MAX_FAILED_ATTEMPTS) {
failedAttempts = 0
activity.finish()
} else {
failedAttempts = 0
activity.finish()
}
authCallback?.onAuthenticationError()
//activity.finish()
}

override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
Expand Down Expand Up @@ -89,7 +81,6 @@ object BiometricAuthenticator {
.setDescription(description)
.setAllowedAuthenticators(authFlag)
.build()

} else {
// for apis < 30
promptInfo = BiometricPrompt.PromptInfo.Builder()
Expand All @@ -98,7 +89,6 @@ object BiometricAuthenticator {
.setDeviceCredentialAllowed(true)
.build()
}

} else {
// fallback for A12+ when both fingerprint & Face unlock is absent but PIN is set
promptInfo = BiometricPrompt.PromptInfo.Builder()
Expand All @@ -114,7 +104,6 @@ object BiometricAuthenticator {
var result = false

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {

when (biometricManager?.canAuthenticate(
DEVICE_CREDENTIAL or BIOMETRIC_STRONG or BIOMETRIC_WEAK
)) {
Expand All @@ -126,7 +115,6 @@ object BiometricAuthenticator {
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> result = true
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> result = false
}

} else {
@Suppress("DEPRECATION")
when (biometricManager?.canAuthenticate()) {
Expand All @@ -153,12 +141,11 @@ object BiometricAuthenticator {
// function to start authentication in any fragment or activity
fun startBiometricAuthentication(activity: Activity, title: Int, setDeviceCred: Boolean) {
initializeBiometrics(activity)

authCallback = activity as? BiometricAuthCallback
if (isBiometricHardWareAvailable()) {
authCallback = activity as? BiometricAuthCallback
authenticationDialog(activity, title, setDeviceCred)
promptInfo?.let { biometricPrompt?.authenticate(it) }

} else {
if (deviceHasPasswordPinLock(activity)) {
authCallback = activity as? BiometricAuthCallback
Expand All @@ -171,7 +158,15 @@ object BiometricAuthenticator {
}
}

fun isAuthEnabled(ctx: Context):Boolean {
return ctx.let {
PreferenceManager.getDefaultSharedPreferences(ctx)
.getBoolean(getString(ctx, R.string.biometric_key), false)
}
}

interface BiometricAuthCallback {
fun onAuthenticationSuccess()
fun onAuthenticationError()
}
}
4 changes: 2 additions & 2 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@
<string name="search">Search</string>
<string name="library">Library</string>
<string name="category_account">Accounts and Security</string>
<string name="category_updates">Updates and backup</string>
<string name="category_updates">Updates and Backup</string>
<string name="settings_info">Info</string>
<string name="advanced_search">Advanced Search</string>
<string name="advanced_search_des">Gives you the search results separated by provider</string>
Expand Down Expand Up @@ -611,7 +611,7 @@
<string name="tracks">Tracks</string>
<string name="audio_tracks">Audio tracks</string>
<string name="video_tracks">Video tracks</string>
<string name="apply_on_restart">Apply on Restart</string>
<string name="apply_on_restart">Restart the app to see changes.</string>
<string name="restart">Restart</string>
<string name="stop">Stop</string>
<string name="safe_mode_title">Safe mode on</string>
Expand Down
8 changes: 2 additions & 6 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}

dependencies {
classpath("com.android.tools.build:gradle:8.2.1")
classpath("com.android.tools.build:gradle:8.2.2")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22")
classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10")

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle.kts files
}
}

Expand All @@ -23,7 +19,7 @@ allprojects {
}

plugins {
id("com.google.devtools.ksp") version "1.9.22-1.0.16" apply false
id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false
}

tasks.register<Delete>("clean") {
Expand Down