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 + +