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