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 e665c3bc..a23ef725 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -14,7 +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 bde62cde..fa6cae18 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
@@ -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)
@@ -1200,7 +1206,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
R.id.result_search_Button,
R.id.result_episodes_show_button,
)
-
+
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
if (exceptionButtons.contains(newFocus?.id)) return@addOnGlobalFocusChangeListener
centerView(newFocus)
@@ -1220,6 +1226,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 {
@@ -1759,6 +1784,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/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt
index e0b13ba6..08c8588b 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt
@@ -440,9 +440,9 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
interceptor = interceptor
).isSuccessful
} else {
- val statusResponse = status?.let { setStatus ->
+ val statusResponse = this.status?.let { setStatus ->
val newStatus =
- SimklListStatusType.values()
+ SimklListStatusType.entries
.firstOrNull { it.value == setStatus }?.originalName
?: SimklListStatusType.Watching.originalName!!
@@ -479,9 +479,14 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
).isSuccessful
} ?: true
+ // You cannot rate if you are planning to watch it.
+ val shouldRate =
+ score != null && status != SimklListStatusType.Planning.value
+ val realScore = if (shouldRate) score else null
+
val historyResponse =
// Only post if there are episodes or score to upload
- if (addEpisodes != null || score != null) {
+ if (addEpisodes != null || shouldRate) {
app.post(
"${this.url}/sync/history",
json = StatusRequest(
@@ -492,8 +497,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
ids,
addEpisodes?.first,
addEpisodes?.second,
- score,
- score?.let { time },
+ realScore,
+ realScore?.let { time },
)
), movies = emptyList()
),
@@ -827,7 +832,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
if (foundItem != null) {
return SimklSyncStatus(
- status = foundItem.status?.let { SyncWatchType.fromInternalId(SimklListStatusType.fromString(it)?.value) }
+ status = foundItem.status?.let {
+ SyncWatchType.fromInternalId(
+ SimklListStatusType.fromString(
+ it
+ )?.value
+ )
+ }
?: return null,
score = foundItem.user_rating,
watchedEpisodes = foundItem.watched_episodes_count,
@@ -839,7 +850,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
)
} else {
return SimklSyncStatus(
- status = SyncWatchType.fromInternalId(SimklListStatusType.None.value) ,
+ status = SyncWatchType.fromInternalId(SimklListStatusType.None.value),
score = 0,
watchedEpisodes = 0,
maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.total_episodes,
@@ -859,11 +870,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
val builder = SimklScoreBuilder.Builder()
.apiUrl(this.mainUrl)
.score(status.score, simklStatus?.oldScore)
- .status(status.status.internalId, (status as? SimklSyncStatus)?.oldStatus?.let { oldStatus ->
- SimklListStatusType.values().firstOrNull {
- it.originalName == oldStatus
- }?.value
- })
+ .status(
+ status.status.internalId,
+ (status as? SimklSyncStatus)?.oldStatus?.let { oldStatus ->
+ SimklListStatusType.entries.firstOrNull {
+ it.originalName == oldStatus
+ }?.value
+ })
.interceptor(interceptor)
.ids(MediaObject.Ids.fromMap(parsedId))
@@ -996,7 +1009,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
val list = getSyncListSmart() ?: return null
val baseMap =
- SimklListStatusType.values()
+ SimklListStatusType.entries
.filter { it.value >= 0 && it.value != SimklListStatusType.ReWatching.value }
.associate {
it.stringRes to emptyList()
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/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt
index e7e50b9d..76066c2e 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt
@@ -681,6 +681,7 @@ open class ResultFragmentPhone : FullScreenPlayer() {
resultMetaYear.setText(d.yearText)
resultMetaDuration.setText(d.durationText)
resultMetaRating.setText(d.ratingText)
+ resultMetaStatus.setText(d.onGoingText)
resultMetaContentRating.setText(d.contentRatingText)
resultCastText.setText(d.actorsText)
resultNextAiring.setText(d.nextAiringEpisode)
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/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt
index 37c71134..8dedd896 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt
@@ -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
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 76142f72..6e925d15 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt
@@ -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)
//}
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 9afe497e..325740df 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
@@ -750,4 +750,17 @@
Movie
Unfavorite
Search
+
+ 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 8c2b298d..0a693769 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -383,6 +383,16 @@
- @color/colorPrimaryCoolBlue
+
+