diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 83100db8..31e225de 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8c05dc43..a23ef725 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ + diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 759f99d4..80de223e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -319,6 +319,7 @@ object CommonActivity { "Banana" -> R.style.OverlayPrimaryColorBanana "Party" -> R.style.OverlayPrimaryColorParty "Pink" -> R.style.OverlayPrimaryColorPink + "Lavender" -> R.style.OverlayPrimaryColorLavender "Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 2f65c788..fff737f6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -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 @@ -284,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 @@ -1169,7 +1176,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) @@ -1181,14 +1188,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) @@ -1201,6 +1206,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(getString(R.string.jsdelivr_proxy_key)) == null && isNetworkAvailable()) { main { @@ -1740,6 +1764,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() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index eb575775..817d7db3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -49,11 +49,15 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) { } } +/** NOTE: Only one observer at a time per value */ fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) liveData.observe(this) { it?.let { t -> action(t) } } } +/** NOTE: Only one observer at a time per value */ fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) liveData.observe(this) { action(it) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt index 23071f59..ccaa38f0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt @@ -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,34 @@ 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) + } + } + + observe(viewModel.isAllowedLogin) { isAllowedLogin -> + if (isAllowedLogin) { + // We are allowed to continue to MainActivity + navigateToMainActivity() + } + } + // Don't show account selection if there is only // one account that exists if (!isEditingFromMainActivity && skipStartup) { @@ -55,12 +82,6 @@ class AccountSelectActivity : AppCompatActivity() { if (currentAccount?.lockPin != null) { CommonActivity.init(this) viewModel.handleAccountSelect(currentAccount, this, true) - observe(viewModel.isAllowedLogin) { isAllowedLogin -> - if (isAllowedLogin) { - // We are allowed to continue to MainActivity - navigateToMainActivity() - } - } } else { if (accounts.count() > 1) { showToast(this, getString( @@ -88,12 +109,6 @@ class AccountSelectActivity : AppCompatActivity() { // Handle the selected account accountSelectCallback = { viewModel.handleAccountSelect(it, this) - observe(viewModel.isAllowedLogin) { isAllowedLogin -> - if (isAllowedLogin) { - // We are allowed to continue to MainActivity - navigateToMainActivity() - } - } }, accountCreateCallback = { viewModel.handleAccountUpdate(it, this) }, accountEditCallback = { @@ -158,6 +173,8 @@ class AccountSelectActivity : AppCompatActivity() { } else 6 } } + + askBiometricAuth() } private fun navigateToMainActivity() { @@ -165,4 +182,8 @@ class AccountSelectActivity : AppCompatActivity() { startActivity(mainIntent) finish() // Finish the account selection activity } + + override fun onAuthenticationSuccess() { + Log.i(BiometricAuthenticator.TAG,"Authentication successful in AccountSelectActivity") + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index 8e9c8521..fa91d990 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -131,6 +131,18 @@ class LibraryFragment : Fragment() { super.onSaveInstanceState(outState) } + private fun updateRandom() { + val position = libraryViewModel.currentPage.value ?: 0 + val pages = (libraryViewModel.pages.value as? Resource.Success)?.value ?: return + if (toggleRandomButton) { + listLibraryItems.clear() + listLibraryItems.addAll(pages[position].items) + binding?.libraryRandom?.isVisible = listLibraryItems.isNotEmpty() + } else { + binding?.libraryRandom?.isGone = true + } + } + @SuppressLint("ResourceType", "CutPasteId") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -395,15 +407,7 @@ class LibraryFragment : Fragment() { binding?.viewpager?.setCurrentItem(page, false) } - observe(libraryViewModel.currentPage){ - if (toggleRandomButton) { - listLibraryItems.clear() - listLibraryItems.addAll(pages[it].items) - libraryRandom.isVisible = listLibraryItems.isNotEmpty() - } else { - libraryRandom.isGone = true - } - } + updateRandom() // Only stop loading after 300ms to hide the fade effect the viewpager produces when updating // Without this there would be a flashing effect: @@ -481,6 +485,7 @@ class LibraryFragment : Fragment() { } observe(libraryViewModel.currentPage) { position -> + updateRandom() val all = binding?.viewpager?.allViews?.toList() ?.filterIsInstance() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index aa5a3182..d04757da 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -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, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index e50131fe..db001ef5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -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 */ diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt new file mode 100644 index 00000000..de9b9963 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt @@ -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() + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index 49fa3618..f435e628 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -531,7 +531,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) //} diff --git a/app/src/main/res/drawable/ic_fingerprint.xml b/app/src/main/res/drawable/ic_fingerprint.xml new file mode 100644 index 00000000..5c96e5a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_fingerprint.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/library_icon.xml b/app/src/main/res/drawable/library_icon.xml new file mode 100644 index 00000000..f62dceac --- /dev/null +++ b/app/src/main/res/drawable/library_icon.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml index cb620bb8..830b004a 100644 --- a/app/src/main/res/menu/bottom_nav_menu.xml +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -10,7 +10,7 @@ android:title="@string/title_search" /> Banana Fiesta Dolor rosa + Lavanda Material You Material You (Secondary) @@ -235,6 +236,7 @@ Banana Party Pink + Lavender Monet Monet2 diff --git a/app/src/main/res/values-pl/array.xml b/app/src/main/res/values-pl/array.xml index 8384187f..9f76f423 100644 --- a/app/src/main/res/values-pl/array.xml +++ b/app/src/main/res/values-pl/array.xml @@ -221,6 +221,7 @@ Bananowy Łososiowy Świnko peppowy + Lawenda Material You Material You (drugorzędny) @@ -244,6 +245,7 @@ Banana Party Pink + Lavender Monet Monet2 diff --git a/app/src/main/res/values-tr/array.xml b/app/src/main/res/values-tr/array.xml index d14a3e2a..5c723f72 100644 --- a/app/src/main/res/values-tr/array.xml +++ b/app/src/main/res/values-tr/array.xml @@ -247,6 +247,7 @@ Muz Parti Pembe + Lavanta Material You Material You (İkincil) @@ -270,6 +271,7 @@ Banana Party Pink + Lavender Monet Monet2 diff --git a/app/src/main/res/values-vi/array.xml b/app/src/main/res/values-vi/array.xml index d32f37ce..aac94100 100644 --- a/app/src/main/res/values-vi/array.xml +++ b/app/src/main/res/values-vi/array.xml @@ -213,6 +213,7 @@ Vàng Hồng Hồng đậm + Hoa oải hương Material You Material You (Secondary) @@ -236,6 +237,7 @@ Banana Party Pink + Lavender Monet Monet2 diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index e38dd5c9..3be12510 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -284,6 +284,7 @@ Banana Party Pink Pain + Lavender Material You Material You (Secondary) @@ -307,6 +308,7 @@ Banana Party Pink + Lavender Monet Monet2 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index c2c84d0d..7c9ccebe 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -84,6 +84,7 @@ #CE8500 #F5BB00 #408cac + #6F55AF #48E484 #ea596e diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 52640246..05fa1a85 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -247,7 +247,7 @@ Error backing up %s Search Library - Accounts + Accounts and Security Updates and backup Info Advanced Search @@ -751,4 +751,17 @@ Display a toggle button for screen orientation Enable automatic switching of screen orientation based on video orientation Auto rotate + + + Unlock CloudStream + Lock with Biometrics + biometric_key + Password/PIN Authentication + Biometric authentication is not supported on this device + Please disable fingerprint authentication if device has no lock. + Unlock the app with Fingerprint, Face ID, PIN, Pattern and Password. + This window will close after few failed attempts. You\'ll have to restart the App. + 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. diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 2fa4eb41..b30d7397 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -383,6 +383,16 @@ @color/colorPrimaryCoolBlue + +