feat(security): add biometric fingerprint sensor / face unlock authentication (#826)

This commit is contained in:
IndusAryan 2024-03-08 06:15:20 +05:30 committed by GitHub
parent f0f4ec87bc
commit e3999d6e9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 306 additions and 31 deletions

View File

@ -212,6 +212,7 @@ dependencies {
implementation("androidx.palette:palette-ktx:1.0.0") // Palette For Images -> Colors
implementation("androidx.tvprovider:tvprovider:1.0.0")
implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures
implementation ("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication
implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview
// Extensions & Other Libs

View File

@ -14,7 +14,7 @@
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <!-- Used for Android TV watch next -->
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!-- Required for getting arbitrary Aniyomi packages -->

View File

@ -19,6 +19,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IdRes
@ -28,6 +29,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.marginStart
import androidx.fragment.app.FragmentActivity
@ -112,6 +114,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
@ -131,11 +134,15 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
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.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
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
@ -166,7 +173,6 @@ import kotlin.math.absoluteValue
import kotlin.reflect.KClass
import kotlin.system.exitProcess
//https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898
//https://wiki.videolan.org/Android_Player_Intents/
@ -285,7 +291,7 @@ var app = Requests(responseParser = object : ResponseParser {
defaultHeaders = mapOf("user-agent" to USER_AGENT)
}
class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAuthenticator.BiometricAuthCallback {
companion object {
const val TAG = "MAINACT"
const val ANIMATED_OUTLINE: Boolean = false
@ -1171,7 +1177,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
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)
@ -1183,14 +1189,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
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)
@ -1204,6 +1208,25 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
changeStatusBarState(isEmulatorSettings())
/** 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 (isTruePhone() && authEnabled && noAccounts) {
if (deviceHasPasswordPinLock(this)) {
startBiometricAuthentication(this, R.string.biometric_authentication_title, false)
BiometricAuthenticator.promptInfo?.let {
BiometricAuthenticator.biometricPrompt?.authenticate(it)
}
// hide background while authenticating, Sorry moms & dads 🙏
binding?.navHostFragment?.isInvisible = true
} else {
showToast(R.string.phone_not_secured, LENGTH_LONG)
}
}
// Automatically enable jsdelivr if cant connect to raw.githubusercontent.com
if (this.getKey<Boolean>(getString(R.string.jsdelivr_proxy_key)) == null && isNetworkAvailable()) {
main {
@ -1743,6 +1766,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
)
}
/** Biometric stuff **/
override fun onAuthenticationSuccess() {
// make background (nav host fragment) visible again
binding?.navHostFragment?.isInvisible = false
}
private var backPressedCallback: OnBackPressedCallback? = null
private fun attachBackPressedCallback() {

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,13 +19,17 @@ 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
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
class AccountSelectActivity : AppCompatActivity() {
class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.BiometricAuthCallback {
lateinit var viewModel: AccountViewModel
@ -41,13 +47,27 @@ class AccountSelectActivity : AppCompatActivity() {
)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val skipStartup = settingsManager.getBoolean(
getString(R.string.skip_startup_account_select_key),
false
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 (isTruePhone() && authEnabled) {
if (deviceHasPasswordPinLock(this)) {
startBiometricAuthentication(this, R.string.biometric_authentication_title, false)
BiometricAuthenticator.promptInfo?.let {
BiometricAuthenticator.biometricPrompt?.authenticate(it)
}
}
} else {
showToast(R.string.phone_not_secured, Toast.LENGTH_LONG)
}
}
// Don't show account selection if there is only
// one account that exists
if (!isEditingFromMainActivity && skipStartup) {
@ -158,6 +178,8 @@ class AccountSelectActivity : AppCompatActivity() {
} else 6
}
}
askBiometricAuth()
}
private fun navigateToMainActivity() {
@ -165,4 +187,8 @@ class AccountSelectActivity : AppCompatActivity() {
startActivity(mainIntent)
finish() // Finish the account selection activity
}
override fun onAuthenticationSuccess() {
Log.i(BiometricAuthenticator.TAG,"Authentication successful in AccountSelectActivity")
}
}

View File

@ -13,6 +13,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceFragmentCompat
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.AccountManagmentBinding
@ -32,7 +33,10 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSet
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
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.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
@ -256,6 +260,19 @@ class SettingsAccount : PreferenceFragmentCompat() {
hideKeyboard()
setPreferencesFromResource(R.xml.settings_account, rootKey)
getPref(R.string.biometric_key)?.setOnPreferenceClickListener {
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 }
true
}
val syncApis =
listOf(
R.string.mal_key to malApi,

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

@ -66,6 +66,7 @@ object BackupUtils {
OPEN_SUBTITLES_USER_KEY,
"nginx_user", // Nginx user key
"biometric_key" // can lock down users if backup is shared on a incompatible device
)
/** false if blacklisted key */

View File

@ -0,0 +1,177 @@
package com.lagradost.cloudstream3.utils
import android.app.Activity
import android.app.KeyguardManager
import android.content.Context
import android.os.Build
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
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.fragment.app.FragmentActivity
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
object BiometricAuthenticator {
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) {
val executor = ContextCompat.getMainExecutor(activity)
biometricManager = BiometricManager.from(activity)
biometricPrompt = BiometricPrompt(
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()
}
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
failedAttempts = 0
authCallback?.onAuthenticationSuccess()
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
failedAttempts++
if (failedAttempts >= MAX_FAILED_ATTEMPTS) {
failedAttempts = 0
activity.finish()
}
}
})
}
@Suppress("DEPRECATION")
// authentication dialog prompt builder
private fun authenticationDialog(
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) {
val authFlag = DEVICE_CREDENTIAL or BIOMETRIC_WEAK or BIOMETRIC_STRONG
promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(activity.getString(title))
.setDescription(description)
.setAllowedAuthenticators(authFlag)
.build()
} else {
// for apis < 30
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
promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(activity.getString(title))
.setDescription(description)
.setDeviceCredentialAllowed(true)
.build()
}
}
private fun isBiometricHardWareAvailable(): Boolean {
// authentication occurs only when this is true and device is truly capable
var result = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
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
}
}
return result
}
// checks if device is secured i.e has at least some type of lock
fun deviceHasPasswordPinLock(context: Context?): Boolean {
val keyMgr =
context?.getSystemService(AppCompatActivity.KEYGUARD_SERVICE) as? KeyguardManager
return keyMgr?.isKeyguardSecure ?: false
}
// function to start authentication in any fragment or activity
fun startBiometricAuthentication(activity: Activity, title: Int, setDeviceCred: Boolean) {
initializeBiometrics(activity)
if (isBiometricHardWareAvailable()) {
authCallback = activity as? BiometricAuthCallback
authenticationDialog(activity, title, setDeviceCred)
promptInfo?.let { biometricPrompt?.authenticate(it) }
} else {
if (deviceHasPasswordPinLock(activity)) {
authCallback = activity as? BiometricAuthCallback
authenticationDialog(activity, R.string.password_pin_authentication_title, true)
promptInfo?.let { biometricPrompt?.authenticate(it) }
} else {
showToast(R.string.biometric_unsupported)
}
}
}
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

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tint="?attr/white"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<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.39s-4.66,1.97 -4.66,4.39c0,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.94s3.08,1.32 3.08,2.94c0,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

@ -247,7 +247,7 @@
<string name="backup_failed_error_format">Error backing up %s</string>
<string name="search">Search</string>
<string name="library">Library</string>
<string name="category_account">Accounts</string>
<string name="category_account">Accounts and Security</string>
<string name="category_updates">Updates and backup</string>
<string name="settings_info">Info</string>
<string name="advanced_search">Advanced Search</string>
@ -745,4 +745,17 @@
<string name="rotate_video_desc">Display a toggle button for screen orientation</string>
<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">Lock with Biometrics</string>
<string name="biometric_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_setting_summary">Unlock the app with Fingerprint, Face ID, PIN, Pattern and Password.</string>
<string name="biometric_prompt_description">This window will close after few failed attempts. You\'ll have to restart the App.</string>
<string name="biometric_warning">Your CloudStream data has been backed up now, although probability of this rare case is very low but all
devices behave differently, in case you get locked down from accessing the app in worst case scenario,
Clear the app data wholly and restore the backup. Any inconvenience if arrived is deeply regretted.</string>
</resources>

View File

@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/ic_outline_account_circle_24"
android:key="@string/skip_startup_account_select_key"
android:title="@string/skip_startup_account_select_pref" />
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:icon="@drawable/mal_logo"
@ -22,17 +16,18 @@
<Preference
android:icon="@drawable/open_subtitles_icon"
android:key="@string/opensubtitles_key" />
<!-- <Preference-->
<!-- android:key="@string/nginx_key"-->
<!-- android:icon="@drawable/nginx" />-->
<!-- <Preference-->
<!-- android:title="@string/nginx_info_title"-->
<!-- android:icon="@drawable/nginx_question"-->
<!-- android:summary="@string/nginx_info_summary">-->
<!-- <intent-->
<!-- android:action="android.intent.action.VIEW"-->
<!-- android:data="https://www.sarlays.com/use-nginx-with-cloudstream/" />-->
<!-- </Preference>-->
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/ic_outline_account_circle_24"
android:key="@string/skip_startup_account_select_key"
android:title="@string/skip_startup_account_select_pref" />
<SwitchPreferenceCompat
android:key="@string/biometric_key"
android:defaultValue="false"
android:summary="@string/biometric_setting_summary"
android:icon="@drawable/ic_fingerprint"
android:title="@string/biometric_setting" />
</PreferenceScreen>