From 9d957d7cc77b328699165d7da2b8eddcad851565 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sat, 28 Oct 2023 15:15:21 -0600 Subject: [PATCH] Add support for profile locks --- .../cloudstream3/ui/WhoIsWatchingAdapter.kt | 8 +- .../cloudstream3/utils/DataStoreHelper.kt | 225 +++++++++++++++--- app/src/main/res/layout/lock_pin_dialog.xml | 25 ++ .../res/layout/who_is_watching_account.xml | 9 + .../layout/who_is_watching_account_edit.xml | 6 + app/src/main/res/values/strings.xml | 8 + 6 files changed, 242 insertions(+), 39 deletions(-) create mode 100644 app/src/main/res/layout/lock_pin_dialog.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WhoIsWatchingAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WhoIsWatchingAdapter.kt index 6b3090a9..c5c38dc0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WhoIsWatchingAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WhoIsWatchingAdapter.kt @@ -55,7 +55,6 @@ class WhoIsWatchingAdapter( editCallBack = editCallBack, ) - override fun onBindViewHolder(holder: WhoIsWatchingHolder, position: Int) = holder.bind(currentList.getOrNull(position)) @@ -70,10 +69,15 @@ class WhoIsWatchingAdapter( fun bind(card: DataStoreHelper.Account?) { when (binding) { is WhoIsWatchingAccountBinding -> binding.apply { - if(card == null) return@apply + if (card == null) return@apply outline.isVisible = card.keyIndex == DataStoreHelper.selectedKeyIndex profileText.text = card.name profileImageBackground.setImage(card.image) + + // Handle the lock indicator + val isLocked = card.lockPin != null + lockIcon.isVisible = isLocked + root.setOnClickListener { selectCallBack(card) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 78f801b6..1736c5a9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -3,9 +3,15 @@ package com.lagradost.cloudstream3.utils import android.content.Context import android.content.DialogInterface import android.text.Editable +import android.text.TextWatcher import android.view.LayoutInflater +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.TextView +import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone +import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import com.fasterxml.jackson.annotation.JsonProperty import com.google.android.material.bottomsheet.BottomSheetDialog @@ -19,6 +25,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.databinding.LockPinDialogBinding import com.lagradost.cloudstream3.databinding.WhoIsWatchingAccountEditBinding import com.lagradost.cloudstream3.databinding.WhoIsWatchingBinding import com.lagradost.cloudstream3.mvvm.logError @@ -27,7 +34,6 @@ import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WhoIsWatchingAdapter import com.lagradost.cloudstream3.ui.library.ListSorting -import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.result.VideoWatchState import com.lagradost.cloudstream3.ui.result.setImage @@ -136,6 +142,8 @@ object DataStoreHelper { val customImage: String? = null, @JsonProperty("defaultImageIndex") val defaultImageIndex: Int, + @JsonProperty("lockPin") + val lockPin: String? = null, ) { val image: UiImage get() = customImage?.let { UiImage.Image(it) } ?: UiImage.Drawable( @@ -230,36 +238,86 @@ object DataStoreHelper { binding.profilePic.setImage(account.image) binding.profilePic.setOnClickListener { - // rolls the image forwards once + // Roll the image forwards once currentEditAccount = currentEditAccount.copy(defaultImageIndex = (currentEditAccount.defaultImageIndex + 1) % profileImages.size) binding.profilePic.setImage(currentEditAccount.image) } binding.applyBtt.setOnClickListener { - val currentAccounts = accounts.toMutableList() - - val overrideIndex = - currentAccounts.indexOfFirst { it.keyIndex == currentEditAccount.keyIndex } - - // if an account is found that has the same keyIndex then override that one, if not then append it - if (overrideIndex != -1) { - currentAccounts[overrideIndex] = currentEditAccount + if (currentEditAccount.lockPin != null) { + // Ask for the current PIN + showPinInputDialog(context, currentEditAccount.lockPin, false) { pin -> + if (pin == null) return@showPinInputDialog + // PIN is correct, proceed to update the account + performAccountUpdate(currentEditAccount) + dialog.dismissSafe() + } } else { - currentAccounts.add(currentEditAccount) + // No lock PIN set, proceed to update the account + performAccountUpdate(currentEditAccount) + dialog.dismissSafe() } - - // Save the current homepage for new accounts - val currentHomePage = DataStoreHelper.currentHomePage - - // set the new default account as well as add the key for the new account - setAccount(currentEditAccount, false) - DataStoreHelper.currentHomePage = currentHomePage - - accounts = currentAccounts.toTypedArray() - - dialog.dismissSafe() } + + // Handle setting or changing the PIN + + if (currentEditAccount.keyIndex == getDefaultAccount(context).keyIndex) { + binding.lockProfileCheckbox.isVisible = false + if (currentEditAccount.lockPin != null) { + currentEditAccount = currentEditAccount.copy(lockPin = null) + } + } + + var canSetPin = true + + binding.lockProfileCheckbox.isChecked = currentEditAccount.lockPin != null + + binding.lockProfileCheckbox.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + if (canSetPin) { + showPinInputDialog(context, null, true) { pin -> + if (pin == null) { + binding.lockProfileCheckbox.isChecked = false + return@showPinInputDialog + } + + currentEditAccount = currentEditAccount.copy(lockPin = pin) + } + } + } else { + if (currentEditAccount.lockPin != null) { + // Ask for the current PIN + showPinInputDialog(context, currentEditAccount.lockPin, true) { pin -> + if (pin == null || pin != currentEditAccount.lockPin) { + canSetPin = false + binding.lockProfileCheckbox.isChecked = true + } else { + currentEditAccount = currentEditAccount.copy(lockPin = null) + } + } + } + } + } + + canSetPin = true + } + + private fun performAccountUpdate(account: Account) { + val currentAccounts = accounts.toMutableList() + + val overrideIndex = currentAccounts.indexOfFirst { it.keyIndex == account.keyIndex } + + if (overrideIndex != -1) { + currentAccounts[overrideIndex] = account + } else { + currentAccounts.add(account) + } + + val currentHomePage = this.currentHomePage + setAccount(account, false) + this.currentHomePage = currentHomePage + accounts = currentAccounts.toTypedArray() } private fun getDefaultAccount(context: Context): Account { @@ -273,9 +331,9 @@ object DataStoreHelper { } fun showWhoIsWatching(context: Context) { - val binding: WhoIsWatchingBinding = WhoIsWatchingBinding.inflate( - LayoutInflater.from(context) - ) + val binding: WhoIsWatchingBinding = WhoIsWatchingBinding.inflate(LayoutInflater.from(context)) + val builder = BottomSheetDialog(context) + builder.setContentView(binding.root) val showAccount = accounts.toMutableList().apply { val item = getDefaultAccount(context) @@ -283,22 +341,25 @@ object DataStoreHelper { add(0, item) } - val builder = - BottomSheetDialog(context) - builder.setContentView(binding.root) val accountName = context.getString(R.string.account) - binding.profilesRecyclerview.setLinearListLayout( - isHorizontal = true, - nextUp = FOCUS_SELF, - nextDown = FOCUS_SELF, - nextLeft = FOCUS_SELF, - nextRight = FOCUS_SELF - ) + binding.profilesRecyclerview.setLinearListLayout(isHorizontal = true) binding.profilesRecyclerview.adapter = WhoIsWatchingAdapter( selectCallBack = { account -> - setAccount(account, true) - builder.dismissSafe() + // Check if the selected account has a lock PIN set + if (account.lockPin != null) { + // Prompt for the lock pin + showPinInputDialog(context, account.lockPin, false) { pin -> + if (pin == null) return@showPinInputDialog + // Pin is correct, unlock the profile + setAccount(account, true) + builder.dismissSafe() + } + } else { + // No lock PIN set, directly set the account + setAccount(account, true) + builder.dismissSafe() + } }, addAccountCallback = { val currentAccounts = accounts @@ -334,6 +395,96 @@ object DataStoreHelper { builder.show() } + private fun showPinInputDialog( + context: Context, + currentPin: String?, + editAccount: Boolean, + callback: (String?) -> Unit + ) { + fun TextView.visibleWithText(@StringRes textRes: Int) { + visibility = View.VISIBLE + setText(textRes) + } + + fun View.isVisible() = visibility == View.VISIBLE + + val binding = LockPinDialogBinding.inflate(LayoutInflater.from(context)) + + binding.pinEditTextError.visibility = View.GONE + + val isPinSet = currentPin != null + val isNewPin = editAccount && !isPinSet + val isEditPin = editAccount && isPinSet + + val titleRes = if (isEditPin) R.string.enter_current_pin else R.string.enter_pin + + val dialog = AlertDialog.Builder(context, R.style.AlertDialogCustom) + .setView(binding.root) + .setTitle(titleRes) + .setNegativeButton(R.string.cancel) { _, _ -> + callback.invoke(null) + } + .setOnCancelListener { + callback.invoke(null) + } + .setOnDismissListener { + if (binding.pinEditTextError.isVisible()) { + callback.invoke(null) + } + } + .create() + + var isPinValid = false + + binding.pinEditText.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + val enteredPin = s.toString() + val isEnteredPinValid = enteredPin.length == 4 + + if (isEnteredPinValid) { + if (isPinSet) { + if (enteredPin != currentPin) { + binding.pinEditTextError.visibleWithText(R.string.pin_error_incorrect) + binding.pinEditText.text = null + isPinValid = false + } else { + binding.pinEditTextError.visibility = View.GONE + isPinValid = true + + callback.invoke(enteredPin) + dialog.dismiss() + } + } else { + binding.pinEditTextError.visibility = View.GONE + isPinValid = true + } + } else if (isNewPin) { + binding.pinEditTextError.visibleWithText(R.string.pin_error_length) + isPinValid = false + } + } + + override fun afterTextChanged(s: Editable?) {} + }) + + // Detect IME_ACTION_DONE + binding.pinEditText.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE && isPinValid) { + val enteredPin = binding.pinEditText.text.toString() + callback.invoke(enteredPin) + dialog.dismiss() + } + true + } + + // We don't want to accidentally have the dialog dismiss when clicking outside of it. + // That is what the cancel button is for. + dialog.setCanceledOnTouchOutside(false) + + dialog.show() + } data class PosDur( @JsonProperty("position") val position: Long, diff --git a/app/src/main/res/layout/lock_pin_dialog.xml b/app/src/main/res/layout/lock_pin_dialog.xml new file mode 100644 index 00000000..c69ff21a --- /dev/null +++ b/app/src/main/res/layout/lock_pin_dialog.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/app/src/main/res/layout/who_is_watching_account.xml b/app/src/main/res/layout/who_is_watching_account.xml index 4970d004..8152ed27 100644 --- a/app/src/main/res/layout/who_is_watching_account.xml +++ b/app/src/main/res/layout/who_is_watching_account.xml @@ -35,6 +35,15 @@ android:background="@drawable/outline_card" android:visibility="gone" /> + + + + tv_no_focus_tag + + + Enter PIN + Enter Current PIN + Lock Profile + PIN + Incorrect PIN. Please try again. + PIN must be 4 characters