diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CreateMessageActivityTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CreateMessageActivityTest.kt index 0517ddd42f..2d383b2740 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CreateMessageActivityTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CreateMessageActivityTest.kt @@ -69,9 +69,9 @@ import org.hamcrest.Matcher import org.hamcrest.Matchers.`is` import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.containsString +import org.hamcrest.Matchers.emptyString import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.hasItem -import org.hamcrest.Matchers.isEmptyString import org.hamcrest.Matchers.not import org.junit.Assert import org.junit.BeforeClass @@ -123,7 +123,7 @@ class CreateMessageActivityTest : BaseCreateMessageActivityTest() { activeActivityRule?.launch(intent) registerAllIdlingResources() onView(withId(R.id.editTextRecipientTo)) - .check(matches(withText(isEmptyString()))) + .check(matches(withText(`is`(emptyString())))) onView(withId(R.id.menuActionSend)) .check(matches(isDisplayed())) .perform(click()) @@ -146,7 +146,7 @@ class CreateMessageActivityTest : BaseCreateMessageActivityTest() { .perform(typeText(TestConstants.RECIPIENT_WITH_PUBLIC_KEY_ON_ATTESTER)) onView(withId(R.id.editTextEmailSubject)) .perform(scrollTo(), click(), typeText("subject"), clearText()) - .check(matches(withText(isEmptyString()))) + .check(matches(withText(`is`(emptyString())))) onView(withId(R.id.menuActionSend)) .check(matches(isDisplayed())) .perform(click()) @@ -175,7 +175,7 @@ class CreateMessageActivityTest : BaseCreateMessageActivityTest() { .perform(scrollTo(), click(), typeText(EMAIL_SUBJECT)) onView(withId(R.id.editTextEmailMessage)) .perform(scrollTo()) - .check(matches(withText(isEmptyString()))) + .check(matches(withText(`is`(emptyString())))) onView(withId(R.id.menuActionSend)) .check(matches(isDisplayed())) .perform(click()) @@ -250,14 +250,14 @@ class CreateMessageActivityTest : BaseCreateMessageActivityTest() { .check(matches(isDisplayed())) onView(withId(R.id.editTextFrom)) .perform(scrollTo()) - .check(matches(withText(not(isEmptyString())))) + .check(matches(withText(not(`is`(emptyString()))))) onView(withId(R.id.layoutTo)) .perform(scrollTo()) onView(withId(R.id.editTextRecipientTo)) - .check(matches(withText(isEmptyString()))) + .check(matches(withText(`is`(emptyString())))) onView(withId(R.id.editTextEmailSubject)) .perform(scrollTo()) - .check(matches(withText(isEmptyString()))) + .check(matches(withText(`is`(emptyString())))) } @Test @@ -715,7 +715,7 @@ class CreateMessageActivityTest : BaseCreateMessageActivityTest() { private fun checkIsDisplayedEncryptedAttributes() { onView(withId(R.id.underToolbarTextTextView)) - .check(doesNotExist()) + .check(matches(not(isDisplayed()))) onView(withId(R.id.appBarLayout)) .check( matches( diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CreateMessageFragmentPasswordProtectedTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CreateMessageFragmentPasswordProtectedTest.kt index 93c2718681..80bf499f00 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CreateMessageFragmentPasswordProtectedTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CreateMessageFragmentPasswordProtectedTest.kt @@ -6,6 +6,7 @@ package com.flowcrypt.email.ui.activity import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu import androidx.test.espresso.action.ViewActions.clearText import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.closeSoftKeyboard @@ -107,4 +108,17 @@ class CreateMessageFragmentPasswordProtectedTest : BaseCreateMessageActivityTest onView(withId(R.id.btnSetWebPortalPassword)) .check(matches(not(isDisplayed()))) } + + @Test + fun testHideWebPortalPasswordButtonWhenUseStandardMsgType() { + testShowWebPortalPasswordButton() + + openActionBarOverflowOrOptionsMenu(getTargetContext()) + onView(withText(R.string.switch_to_standard_email)) + .check(matches(isDisplayed())) + .perform(click()) + + onView(withId(R.id.btnSetWebPortalPassword)) + .check(matches(not(isDisplayed()))) + } } diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/fragment/ProvidePasswordToProtectMsgFragmentTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/fragment/ProvidePasswordToProtectMsgFragmentTest.kt new file mode 100644 index 0000000000..393a0517df --- /dev/null +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/fragment/ProvidePasswordToProtectMsgFragmentTest.kt @@ -0,0 +1,126 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.ui.activity.fragment + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.closeSoftKeyboard +import androidx.test.espresso.action.ViewActions.typeText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.hasTextColor +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.isEnabled +import androidx.test.espresso.matcher.ViewMatchers.isNotChecked +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.rules.activityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import com.flowcrypt.email.R +import com.flowcrypt.email.base.BaseTest +import com.flowcrypt.email.rules.AddAccountToDatabaseRule +import com.flowcrypt.email.rules.ClearAppSettingsRule +import com.flowcrypt.email.rules.RetryRule +import com.flowcrypt.email.rules.ScreenshotTestRule +import com.flowcrypt.email.ui.activity.settings.SettingsActivity +import com.flowcrypt.email.util.TestGeneralUtil +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.not +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +/** + * @author Denis Bondarenko + * Date: 1/22/22 + * Time: 6:32 PM + * E-mail: DenBond7@gmail.com + */ +@MediumTest +@RunWith(AndroidJUnit4::class) +class ProvidePasswordToProtectMsgFragmentTest : BaseTest() { + override val activityScenarioRule = activityScenarioRule( + TestGeneralUtil.genIntentForNavigationComponent( + uri = "flowcrypt://email.flowcrypt.com/compose/web-portal-password" + ) + ) + private val accountRule = AddAccountToDatabaseRule() + + @get:Rule + var ruleChain: TestRule = RuleChain + .outerRule(RetryRule.DEFAULT) + .around(ClearAppSettingsRule()) + .around(accountRule) + .around(activityScenarioRule) + .around(ScreenshotTestRule()) + + @Test + fun testPasswordStrength() { + onView(withId(R.id.btSetPassword)) + .check(matches(not(isEnabled()))) + + //type one uppercase + checkConditionItemState(R.id.checkedTVOneUppercase, false) + onView(withId(R.id.eTPassphrase)) + .perform( + typeText("A"), + closeSoftKeyboard() + ) + checkConditionItemState(R.id.checkedTVOneUppercase, true) + + //type one lowercase + checkConditionItemState(R.id.checkedTVOneLowercase, false) + onView(withId(R.id.eTPassphrase)) + .perform( + typeText("a"), + closeSoftKeyboard() + ) + checkConditionItemState(R.id.checkedTVOneLowercase, true) + + //type one number + checkConditionItemState(R.id.checkedTVOneNumber, false) + onView(withId(R.id.eTPassphrase)) + .perform( + typeText("1"), + closeSoftKeyboard() + ) + checkConditionItemState(R.id.checkedTVOneNumber, true) + + //type one special character + checkConditionItemState(R.id.checkedTVOneSpecialCharacter, false) + onView(withId(R.id.eTPassphrase)) + .perform( + typeText("@"), + closeSoftKeyboard() + ) + checkConditionItemState(R.id.checkedTVOneSpecialCharacter, true) + + //type one special character + checkConditionItemState(R.id.checkedTVMinLength, false) + onView(withId(R.id.eTPassphrase)) + .perform( + typeText("more than 8 symbols"), + closeSoftKeyboard() + ) + checkConditionItemState(R.id.checkedTVMinLength, true) + + //check that button is enabled + onView(withId(R.id.btSetPassword)) + .check(matches(isEnabled())) + } + + private fun checkConditionItemState(id: Int, isChecked: Boolean) { + onView(withId(id)) + .check( + matches( + allOf( + if (isChecked) isChecked() else isNotChecked(), + hasTextColor(if (isChecked) R.color.colorPrimary else R.color.orange), + ) + ) + ) + } +} diff --git a/FlowCrypt/src/devTest/res/navigation/create_msg_graph.xml b/FlowCrypt/src/devTest/res/navigation/create_msg_graph.xml new file mode 100644 index 0000000000..11a7c4e8b7 --- /dev/null +++ b/FlowCrypt/src/devTest/res/navigation/create_msg_graph.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ComposeMsgViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ComposeMsgViewModel.kt index 678e144285..90ea2fea25 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ComposeMsgViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ComposeMsgViewModel.kt @@ -137,6 +137,7 @@ class ComposeMsgViewModel(isCandidateToEncrypt: Boolean, application: Applicatio data class Recipient( val recipientType: Message.RecipientType, - val recipientWithPubKeys: RecipientWithPubKeys + val recipientWithPubKeys: RecipientWithPubKeys, + val creationTime: Long = System.currentTimeMillis() ) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/WebPortalPasswordStrengthViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/WebPortalPasswordStrengthViewModel.kt new file mode 100644 index 0000000000..e22b95e54b --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/WebPortalPasswordStrengthViewModel.kt @@ -0,0 +1,64 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.jetpack.viewmodel + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.flowcrypt.email.util.coroutines.runners.ControlledRunner +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.* + +/** + * This [ViewModel] implementation can be used to check the web portal strength + * + * @author Denis Bondarenko + * Date: 1/21/22 + * Time: 08:15 AM + * E-mail: DenBond7@gmail.com + */ +class WebPortalPasswordStrengthViewModel(application: Application) : + BaseAndroidViewModel(application) { + private val controlledRunnerForChecking = ControlledRunner>() + + private val pwdStrengthResultMutableStateFlow: MutableStateFlow> = + MutableStateFlow(emptyList()) + val pwdStrengthResultStateFlow: StateFlow> = + pwdStrengthResultMutableStateFlow.asStateFlow() + + fun check(password: CharSequence) { + viewModelScope.launch { + pwdStrengthResultMutableStateFlow.value = controlledRunnerForChecking.cancelPreviousThenRun { + return@cancelPreviousThenRun checkInternal(password) + } + } + } + + private suspend fun checkInternal(password: CharSequence): List = + withContext(Dispatchers.Default) { + return@withContext Requirement.values().map { + RequirementItem(it, it.regex.containsMatchIn(password)) + } + } + + data class RequirementItem( + val requirement: Requirement, + val isMatching: Boolean + ) + + enum class Requirement constructor(val regex: Regex) { + MIN_LENGTH(".{8,}".toRegex(RegexOption.MULTILINE)), + ONE_UPPERCASE("\\p{Lu}".toRegex(RegexOption.MULTILINE)), + ONE_LOWERCASE("\\p{Ll}".toRegex(RegexOption.MULTILINE)), + ONE_NUMBER("\\d".toRegex(RegexOption.MULTILINE)), + ONE_SPECIAL_CHARACTER("[&\"#-'_%-/@,;:!*()]".toRegex(RegexOption.MULTILINE)); + } +} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/model/KeysStorage.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/model/KeysStorage.kt index 2f9549dd11..bcc255dede 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/model/KeysStorage.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/model/KeysStorage.kt @@ -44,5 +44,7 @@ interface KeysStorage { fun hasEmptyPassphrase(): Boolean + fun hasPassphrase(passphrase: Passphrase): Boolean + fun getFingerprintsWithEmptyPassphrase(): List } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/KeysStorageImpl.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/KeysStorageImpl.kt index 9bb327c317..cb0dd9882a 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/KeysStorageImpl.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/KeysStorageImpl.kt @@ -229,6 +229,10 @@ class KeysStorageImpl private constructor(context: Context) : KeysStorage { return passPhraseMap.values.any { it.passphrase.isEmpty } } + override fun hasPassphrase(passphrase: Passphrase): Boolean { + return passPhraseMap.values.any { it.passphrase == passphrase } + } + override fun getFingerprintsWithEmptyPassphrase(): List { return passPhraseMap.filter { it.value.passphrase.isEmpty }.map { it.key } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt index 4d213b28f6..27f25713a0 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt @@ -12,7 +12,6 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle -import android.text.SpannableStringBuilder import android.text.format.Formatter import android.util.Log import android.view.ContextMenu @@ -69,6 +68,7 @@ import com.flowcrypt.email.extensions.showKeyboard import com.flowcrypt.email.extensions.showNeedPassphraseDialog import com.flowcrypt.email.extensions.toast import com.flowcrypt.email.extensions.visible +import com.flowcrypt.email.extensions.visibleOrGone import com.flowcrypt.email.jetpack.viewmodel.AccountAliasesViewModel import com.flowcrypt.email.jetpack.viewmodel.ComposeMsgViewModel import com.flowcrypt.email.jetpack.viewmodel.RecipientsViewModel @@ -82,13 +82,11 @@ import com.flowcrypt.email.ui.activity.fragment.base.BaseSyncFragment import com.flowcrypt.email.ui.activity.fragment.dialog.ChoosePublicKeyDialogFragment import com.flowcrypt.email.ui.activity.fragment.dialog.FixNeedPassphraseIssueDialogFragment import com.flowcrypt.email.ui.activity.fragment.dialog.NoPgpFoundDialogFragment -import com.flowcrypt.email.ui.activity.fragment.dialog.ProvidePasswordToProtectMsgDialogFragment import com.flowcrypt.email.ui.adapter.FromAddressesAdapter import com.flowcrypt.email.ui.adapter.RecipientAdapter import com.flowcrypt.email.ui.widget.CustomChipSpanChipCreator import com.flowcrypt.email.ui.widget.PGPContactChipSpan import com.flowcrypt.email.ui.widget.PgpContactsNachoTextView -import com.flowcrypt.email.ui.widget.SingleCharacterSpanChipTokenizer import com.flowcrypt.email.util.FileAndDirectoryUtils import com.flowcrypt.email.util.GeneralUtil import com.flowcrypt.email.util.UIUtil @@ -98,10 +96,12 @@ import com.google.android.material.snackbar.Snackbar import com.hootsuite.nachos.NachoTextView import com.hootsuite.nachos.chip.Chip import com.hootsuite.nachos.terminator.ChipTerminatorHandler +import com.hootsuite.nachos.tokenizer.SpanChipTokenizer import com.hootsuite.nachos.validator.ChipifyingNachoValidator import org.apache.commons.io.FileUtils import org.bouncycastle.openpgp.PGPSecretKeyRing import org.pgpainless.key.OpenPgpV4Fingerprint +import org.pgpainless.util.Passphrase import java.io.File import java.io.IOException import java.util.regex.Pattern @@ -168,7 +168,6 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) - setupComposeMsgViewModel() subscribeToSetWebPortalPassword() initExtras(activity?.intent) } @@ -182,9 +181,10 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - updateActionBarTitle() initNonEncryptedHintView() + updateActionBar() initViews() + setupComposeMsgViewModel() setupAccountAliasesViewModel() setupPrivateKeysViewModel() setupRecipientsViewModel() @@ -195,6 +195,11 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, } } + override fun onDestroyView() { + super.onDestroyView() + appBarLayout?.removeView(nonEncryptedHintView) + } + override fun onDestroy() { super.onDestroy() if (!isMsgSentToQueue) { @@ -882,9 +887,7 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, list: List? ) { view ?: return - val builder = SpannableStringBuilder(view.text) - - val pgpContactChipSpans = builder.getSpans(0, view.length(), PGPContactChipSpan::class.java) + val pgpContactChipSpans = view.text.getSpans(0, view.length(), PGPContactChipSpan::class.java) if (pgpContactChipSpans.isNotEmpty()) { for (recipientWithPubKeys in list ?: emptyList()) { @@ -917,7 +920,7 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, ' ', ChipTerminatorHandler .BEHAVIOR_CHIPIFY_TO_TERMINATOR ) - pgpContactsNachoTextView?.chipTokenizer = SingleCharacterSpanChipTokenizer( + pgpContactsNachoTextView?.chipTokenizer = SpanChipTokenizer( requireContext(), CustomChipSpanChipCreator(requireContext()), PGPContactChipSpan::class.java ) @@ -986,7 +989,7 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, binding?.btnSetWebPortalPassword?.setOnClickListener { navController?.navigate( CreateMessageFragmentDirections - .actionCreateMessageFragmentToProvidePasswordToProtectMsgDialogFragment( + .actionCreateMessageFragmentToProvidePasswordToProtectMsgFragment( composeMsgViewModel.webPortalPasswordStateFlow.value.toString() ) ) @@ -1557,7 +1560,6 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, progressBar?.invisible() it.data?.let { list -> composeMsgViewModel.replaceRecipients(recipientType, list) - updateChips(nachoTextView, list) } baseActivity.countingIdlingResource.decrementSafely() } @@ -1625,12 +1627,14 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, appBarLayout?.setBackgroundColor( UIUtil.getColor(requireContext(), R.color.colorPrimary) ) - appBarLayout?.removeView(nonEncryptedHintView) + nonEncryptedHintView?.gone() } MessageEncryptionType.STANDARD -> { appBarLayout?.setBackgroundColor(UIUtil.getColor(requireContext(), R.color.red)) - appBarLayout?.addView(nonEncryptedHintView) + nonEncryptedHintView?.visible() + binding?.btnSetWebPortalPassword?.gone() + composeMsgViewModel.setWebPortalPassword() } } @@ -1645,17 +1649,35 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, val hasRecipientsWithoutPgp = recipients.any { recipient -> !recipient.recipientWithPubKeys.hasAtLeastOnePubKey() } if (hasRecipientsWithoutPgp) { - if (binding?.btnSetWebPortalPassword?.visibility == View.GONE) { - composeMsgViewModel.setWebPortalPassword() - } binding?.btnSetWebPortalPassword?.visible() } else { binding?.btnSetWebPortalPassword?.gone() + composeMsgViewModel.setWebPortalPassword() } } } } + lifecycleScope.launchWhenStarted { + composeMsgViewModel.recipientsToStateFlow.collect { recipients -> + updateChips(binding?.editTextRecipientTo, recipients.map { it.recipientWithPubKeys }) + } + } + + lifecycleScope.launchWhenStarted { + composeMsgViewModel.recipientsCcStateFlow.collect { recipients -> + binding?.layoutCc?.visibleOrGone(recipients.isNotEmpty()) + updateChips(binding?.editTextRecipientCc, recipients.map { it.recipientWithPubKeys }) + } + } + + lifecycleScope.launchWhenStarted { + composeMsgViewModel.recipientsBccStateFlow.collect { recipients -> + binding?.layoutBcc?.visibleOrGone(recipients.isNotEmpty()) + updateChips(binding?.editTextRecipientBcc, recipients.map { it.recipientWithPubKeys }) + } + } + lifecycleScope.launchWhenStarted { composeMsgViewModel.webPortalPasswordStateFlow.collect { webPortalPassword -> if (isPasswordProtectedFunctionalityEnabled()) { @@ -1696,13 +1718,15 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, textView?.setText(R.string.this_message_will_not_be_encrypted) } - private fun updateActionBarTitle() { + private fun updateActionBar() { when (args.messageType) { MessageType.NEW -> supportActionBar?.setTitle(R.string.compose) MessageType.REPLY -> supportActionBar?.setTitle(R.string.reply) MessageType.REPLY_ALL -> supportActionBar?.setTitle(R.string.reply_all) MessageType.FORWARD -> supportActionBar?.setTitle(R.string.forward) } + + appBarLayout?.addView(nonEncryptedHintView) } /** @@ -1761,6 +1785,35 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, if (hasUnusableRecipient()) { return false } + + if (isPasswordProtectedFunctionalityEnabled()) { + val password = composeMsgViewModel.webPortalPasswordStateFlow.value + if (password.isNotEmpty()) { + val keysStorage = KeysStorageImpl.getInstance(requireContext()) + if (keysStorage.hasPassphrase(Passphrase(password.toString().toCharArray()))) { + navController?.navigate( + CreateMessageFragmentDirections.actionGlobalInfoDialogFragment( + dialogTitle = getString(R.string.warning), + dialogMsg = getString(R.string.warning_use_private_key_pass_phrase_as_password) + ) + ) + return false + } + + if (binding?.editTextEmailSubject?.text.toString() == password.toString()) { + navController?.navigate( + CreateMessageFragmentDirections.actionGlobalInfoDialogFragment( + dialogTitle = getString(R.string.warning), + dialogMsg = getString( + R.string.warning_use_subject_as_password, + getString(R.string.app_name) + ) + ) + ) + return false + } + } + } } if (binding?.editTextEmailSubject?.text?.isEmpty() == true) { showInfoSnackbar( @@ -1829,10 +1882,10 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, private fun subscribeToSetWebPortalPassword() { setFragmentResultListener( - ProvidePasswordToProtectMsgDialogFragment.REQUEST_KEY_PASSWORD + ProvidePasswordToProtectMsgFragment.REQUEST_KEY_PASSWORD ) { _, bundle -> val password = - bundle.getCharSequence(ProvidePasswordToProtectMsgDialogFragment.KEY_PASSWORD) ?: "" + bundle.getCharSequence(ProvidePasswordToProtectMsgFragment.KEY_PASSWORD) ?: "" composeMsgViewModel.setWebPortalPassword(password) } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ProvidePasswordToProtectMsgFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ProvidePasswordToProtectMsgFragment.kt new file mode 100644 index 0000000000..7bc3ec2b4b --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ProvidePasswordToProtectMsgFragment.kt @@ -0,0 +1,156 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.ui.activity.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.widget.CheckedTextView +import androidx.core.graphics.BlendModeColorFilterCompat +import androidx.core.graphics.BlendModeCompat +import androidx.core.os.bundleOf +import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.setFragmentResult +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.navArgs +import com.flowcrypt.email.R +import com.flowcrypt.email.databinding.FragmentProvidePasswordToProtectMsgBinding +import com.flowcrypt.email.extensions.hideKeyboard +import com.flowcrypt.email.extensions.navController +import com.flowcrypt.email.extensions.toast +import com.flowcrypt.email.jetpack.viewmodel.WebPortalPasswordStrengthViewModel +import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment +import com.flowcrypt.email.util.UIUtil + +class ProvidePasswordToProtectMsgFragment : BaseFragment() { + private var binding: FragmentProvidePasswordToProtectMsgBinding? = null + private val args by navArgs() + private val webPortalPasswordStrengthViewModel: WebPortalPasswordStrengthViewModel by viewModels() + + override val contentResourceId: Int = R.layout.fragment_provide_password_to_protect_msg + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View? { + binding = FragmentProvidePasswordToProtectMsgBinding.inflate(inflater, container, false) + return binding?.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + supportActionBar?.setTitle(R.string.web_portal_password) + initViews() + initWebPortalPasswordStrengthViewModel() + } + + private fun initViews() { + binding?.eTPassphrase?.addTextChangedListener { editable -> + webPortalPasswordStrengthViewModel.check(editable.toString()) + } + + binding?.eTPassphrase?.setOnEditorActionListener { v, actionId, _ -> + return@setOnEditorActionListener when (actionId) { + EditorInfo.IME_ACTION_DONE -> { + checkAndMoveOn() + v.hideKeyboard() + true + } + else -> false + } + } + + binding?.btSetPassword?.setOnClickListener { + checkAndMoveOn() + } + + binding?.eTPassphrase?.setText(args.defaultPassword) + + binding?.pBarPassphraseQuality?.progressDrawable?.colorFilter = + BlendModeColorFilterCompat.createBlendModeColorFilterCompat( + UIUtil.getColor(requireContext(), R.color.colorPrimary), + BlendModeCompat.SRC_IN + ) + } + + private fun checkAndMoveOn() { + if (binding?.eTPassphrase?.text?.isEmpty() == true) { + toast(getString(R.string.password_cannot_be_empty)) + } else { + navController?.navigateUp() + setFragmentResult( + REQUEST_KEY_PASSWORD, + bundleOf(KEY_PASSWORD to (binding?.eTPassphrase?.text ?: "")) + ) + } + } + + private fun initWebPortalPasswordStrengthViewModel() { + lifecycleScope.launchWhenStarted { + webPortalPasswordStrengthViewModel.pwdStrengthResultStateFlow.collect { + updateStrengthViews(it) + } + } + } + + private fun updateStrengthViews(list: List) { + list.forEach { + when (it.requirement) { + WebPortalPasswordStrengthViewModel.Requirement.MIN_LENGTH -> { + updateRequirementItem(binding?.checkedTVMinLength, it) + } + WebPortalPasswordStrengthViewModel.Requirement.ONE_LOWERCASE -> { + updateRequirementItem(binding?.checkedTVOneLowercase, it) + } + WebPortalPasswordStrengthViewModel.Requirement.ONE_UPPERCASE -> { + updateRequirementItem(binding?.checkedTVOneUppercase, it) + } + WebPortalPasswordStrengthViewModel.Requirement.ONE_NUMBER -> { + updateRequirementItem(binding?.checkedTVOneNumber, it) + } + WebPortalPasswordStrengthViewModel.Requirement.ONE_SPECIAL_CHARACTER -> { + updateRequirementItem(binding?.checkedTVOneSpecialCharacter, it) + } + } + } + + binding?.btSetPassword?.apply { + isEnabled = list.all { it.isMatching } + background?.colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat( + UIUtil.getColor( + requireContext(), if (isEnabled) R.color.colorPrimary else R.color.silver + ), BlendModeCompat.MODULATE + ) + } + + binding?.pBarPassphraseQuality?.apply { + val progress = list.count { it.isMatching } + this.progress = progress + } + } + + private fun updateRequirementItem( + view: CheckedTextView?, + it: WebPortalPasswordStrengthViewModel.RequirementItem + ) { + view?.apply { + isChecked = it.isMatching + setTextColor( + UIUtil.getColor( + requireContext(), + if (it.isMatching) R.color.colorPrimary else R.color.orange + ) + ) + } + } + + companion object { + const val REQUEST_KEY_PASSWORD = "REQUEST_KEY_PASSWORD" + const val KEY_PASSWORD = "KEY_PASSWORD" + } +} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/ProvidePasswordToProtectMsgDialogFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/ProvidePasswordToProtectMsgDialogFragment.kt deleted file mode 100644 index bbe3ddd5fd..0000000000 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/ProvidePasswordToProtectMsgDialogFragment.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 - */ - -package com.flowcrypt.email.ui.activity.fragment.dialog - -import android.app.AlertDialog -import android.app.Dialog -import android.os.Bundle -import android.view.LayoutInflater -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import androidx.core.os.bundleOf -import androidx.core.widget.addTextChangedListener -import androidx.fragment.app.setFragmentResult -import androidx.navigation.fragment.navArgs -import com.flowcrypt.email.R -import com.flowcrypt.email.databinding.FragmentProvidePasswordToProtectMsgBinding -import com.flowcrypt.email.extensions.hideKeyboard -import com.flowcrypt.email.extensions.navController -import com.flowcrypt.email.extensions.toast - -class ProvidePasswordToProtectMsgDialogFragment : BaseDialogFragment() { - private var binding: FragmentProvidePasswordToProtectMsgBinding? = null - private val args by navArgs() - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val builder = AlertDialog.Builder(activity) - binding = FragmentProvidePasswordToProtectMsgBinding.inflate( - LayoutInflater.from(requireContext()), - if ((view != null) and (view is ViewGroup)) view as ViewGroup? else null, - false - ) - - binding?.eTPassphrase?.addTextChangedListener { editable -> - //check typed password will be added a bit later(soon) - } - - binding?.eTPassphrase?.setOnEditorActionListener { v, actionId, _ -> - return@setOnEditorActionListener when (actionId) { - EditorInfo.IME_ACTION_DONE -> { - checkAndMoveOn() - v.hideKeyboard() - true - } - else -> false - } - } - - binding?.btSetPassphrase?.setOnClickListener { - checkAndMoveOn() - } - - binding?.eTPassphrase?.setText(args.defaultPassword) - - builder.setView(binding?.root) - builder.setTitle(null) - return builder.create() - } - - private fun checkAndMoveOn() { - if (binding?.eTPassphrase?.text?.isEmpty() == true) { - toast(getString(R.string.password_cannot_be_empty)) - } else { - navController?.navigateUp() - setFragmentResult( - REQUEST_KEY_PASSWORD, - bundleOf(KEY_PASSWORD to (binding?.eTPassphrase?.text ?: "")) - ) - } - } - - companion object { - const val REQUEST_KEY_PASSWORD = "REQUEST_KEY_PASSWORD" - const val KEY_PASSWORD = "KEY_PASSWORD" - } -} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/widget/PgpContactsNachoTextView.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/widget/PgpContactsNachoTextView.kt index 8285775952..d00d7de5c8 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/widget/PgpContactsNachoTextView.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/widget/PgpContactsNachoTextView.kt @@ -148,12 +148,12 @@ class PgpContactsNachoTextView(context: Context, attrs: AttributeSet) : if (chipTokenizer != null) { val stringBuilder = StringBuilder() - val chips = Arrays.asList(*chipTokenizer!!.findAllChips(start, end, editable)) + val chips = listOf(*chipTokenizer!!.findAllChips(start, end, editable)) for (i in chips.indices) { val chip = chips[i] stringBuilder.append(chip.text) if (i != chips.size - 1) { - stringBuilder.append(SingleCharacterSpanChipTokenizer.CHIP_SEPARATOR_WHITESPACE) + stringBuilder.append(CHIP_SEPARATOR_WHITESPACE) } } @@ -290,4 +290,8 @@ class PgpContactsNachoTextView(context: Context, attrs: AttributeSet) : return layout.getLineForOffset(offset) } } + + companion object { + const val CHIP_SEPARATOR_WHITESPACE = ' ' + } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/widget/SingleCharacterSpanChipTokenizer.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/widget/SingleCharacterSpanChipTokenizer.kt deleted file mode 100644 index 91a2bbbe88..0000000000 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/widget/SingleCharacterSpanChipTokenizer.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 - */ - -package com.flowcrypt.email.ui.widget - -import android.content.Context - -import com.hootsuite.nachos.NachoTextView -import com.hootsuite.nachos.chip.Chip -import com.hootsuite.nachos.chip.ChipCreator -import com.hootsuite.nachos.tokenizer.SpanChipTokenizer - -/** - * Define a custom chip separator in [NachoTextView] - * - * @author DenBond7 - * Date: 19.05.2017 - * Time: 14:14 - * E-mail: DenBond7@gmail.com - */ -class SingleCharacterSpanChipTokenizer -@JvmOverloads constructor( - context: Context, - chipCreator: ChipCreator, - chipClass: Class, - private val symbol: Char = CHIP_SEPARATOR_WHITESPACE -) : SpanChipTokenizer( - context, - chipCreator, chipClass -) { - override fun findTokenStart(text: CharSequence, cursor: Int): Int { - var i = cursor - - while (i > 0 && text[i - 1] != symbol) { - i-- - } - while (i < cursor && text[i] == symbol) { - i++ - } - - return i - } - - override fun findTokenEnd(text: CharSequence, cursor: Int): Int { - var i = cursor - val len = text.length - - while (i < len) { - if (text[i] == symbol) { - return i - } else { - i++ - } - } - - return len - } - - companion object { - const val CHIP_SEPARATOR_WHITESPACE = ' ' - } -} diff --git a/FlowCrypt/src/main/res/drawable/ic_baseline_check_green_24dp.xml b/FlowCrypt/src/main/res/drawable/ic_baseline_check_green_24dp.xml new file mode 100644 index 0000000000..12877ac05d --- /dev/null +++ b/FlowCrypt/src/main/res/drawable/ic_baseline_check_green_24dp.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/FlowCrypt/src/main/res/drawable/ic_baseline_remove_orange_24dp.xml b/FlowCrypt/src/main/res/drawable/ic_baseline_remove_orange_24dp.xml new file mode 100644 index 0000000000..5ddc868b1f --- /dev/null +++ b/FlowCrypt/src/main/res/drawable/ic_baseline_remove_orange_24dp.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/FlowCrypt/src/main/res/drawable/selector_list_item_condition.xml b/FlowCrypt/src/main/res/drawable/selector_list_item_condition.xml new file mode 100644 index 0000000000..096125620f --- /dev/null +++ b/FlowCrypt/src/main/res/drawable/selector_list_item_condition.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/FlowCrypt/src/main/res/layout/fragment_provide_password_to_protect_msg.xml b/FlowCrypt/src/main/res/layout/fragment_provide_password_to_protect_msg.xml index d517013183..e12354bf40 100644 --- a/FlowCrypt/src/main/res/layout/fragment_provide_password_to_protect_msg.xml +++ b/FlowCrypt/src/main/res/layout/fragment_provide_password_to_protect_msg.xml @@ -4,7 +4,6 @@ --> @@ -49,29 +48,74 @@ style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="@dimen/default_margin_content_small" + android:max="5" android:progress="0" /> - - + + + + + + + + + + + +