diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableMultipleKeysWithPassphraseInDatabaseFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableMultipleKeysWithPassphraseInDatabaseFlowTest.kt new file mode 100644 index 0000000000..7b10db46d8 --- /dev/null +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableMultipleKeysWithPassphraseInDatabaseFlowTest.kt @@ -0,0 +1,71 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.ui + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.hasTextColor +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import com.flowcrypt.email.R +import com.flowcrypt.email.TestConstants +import com.flowcrypt.email.extensions.kotlin.asInternetAddress +import com.flowcrypt.email.junit.annotations.FlowCryptTestSettings +import com.flowcrypt.email.rules.AddPrivateKeyToDatabaseRule +import com.flowcrypt.email.rules.ClearAppSettingsRule +import com.flowcrypt.email.rules.GrantPermissionRuleChooser +import com.flowcrypt.email.rules.RetryRule +import com.flowcrypt.email.rules.ScreenshotTestRule +import com.flowcrypt.email.ui.base.BaseComposeScreenNoKeyAvailableTest +import org.hamcrest.CoreMatchers.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 Denys Bondarenko + */ +@MediumTest +@RunWith(AndroidJUnit4::class) +@FlowCryptTestSettings(useCommonIdling = false) +class ComposeScreenNoKeyAvailableMultipleKeysWithPassphraseInDatabaseFlowTest : + BaseComposeScreenNoKeyAvailableTest() { + private val addPrivateKeyToDatabaseRuleFirst = AddPrivateKeyToDatabaseRule( + keyPath = "pgp/key_testing@flowcrypt.test_keyA_strong.asc" + ) + + private val addPrivateKeyToDatabaseRuleSecond = AddPrivateKeyToDatabaseRule( + keyPath = "pgp/key_testing@flowcrypt.test_keyC_strong.asc" + ) + + @get:Rule + var ruleChain: TestRule = RuleChain + .outerRule(RetryRule.DEFAULT) + .around(ClearAppSettingsRule()) + .around(GrantPermissionRuleChooser.grant(android.Manifest.permission.POST_NOTIFICATIONS)) + .around(addAccountToDatabaseRule) + .around(addPrivateKeyToDatabaseRuleFirst) + .around(addPrivateKeyToDatabaseRuleSecond) + .around(activeActivityRule) + .around(ScreenshotTestRule()) + + @Test + fun testAddEmailToExistingKey() { + doTestAddEmailToExistingKey { + waitForObjectWithText(getResString(android.R.string.ok), 2000) + + onView(withId(R.id.buttonOk)) + .check(matches(isDisplayed())) + .perform(click()) + } + } +} diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableMultipleKeysWithPassphraseInRamFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableMultipleKeysWithPassphraseInRamFlowTest.kt new file mode 100644 index 0000000000..4980405d86 --- /dev/null +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableMultipleKeysWithPassphraseInRamFlowTest.kt @@ -0,0 +1,84 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.ui + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.clearText +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.pressImeActionButton +import androidx.test.espresso.action.ViewActions.replaceText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import com.flowcrypt.email.R +import com.flowcrypt.email.TestConstants +import com.flowcrypt.email.database.entity.KeyEntity +import com.flowcrypt.email.junit.annotations.FlowCryptTestSettings +import com.flowcrypt.email.rules.AddPrivateKeyToDatabaseRule +import com.flowcrypt.email.rules.ClearAppSettingsRule +import com.flowcrypt.email.rules.GrantPermissionRuleChooser +import com.flowcrypt.email.rules.RetryRule +import com.flowcrypt.email.rules.ScreenshotTestRule +import com.flowcrypt.email.ui.base.BaseComposeScreenNoKeyAvailableTest +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +/** + * @author Denys Bondarenko + */ +@MediumTest +@RunWith(AndroidJUnit4::class) +@FlowCryptTestSettings(useCommonIdling = false) +class ComposeScreenNoKeyAvailableMultipleKeysWithPassphraseInRamFlowTest : + BaseComposeScreenNoKeyAvailableTest() { + private val addPrivateKeyToDatabaseRuleFirst = AddPrivateKeyToDatabaseRule( + keyPath = "pgp/key_testing@flowcrypt.test_keyA_strong.asc", + passphraseType = KeyEntity.PassphraseType.RAM + ) + + private val addPrivateKeyToDatabaseRuleSecond = AddPrivateKeyToDatabaseRule( + keyPath = "pgp/key_testing@flowcrypt.test_keyC_strong.asc", + passphraseType = KeyEntity.PassphraseType.RAM + ) + + @get:Rule + var ruleChain: TestRule = RuleChain + .outerRule(RetryRule.DEFAULT) + .around(ClearAppSettingsRule()) + .around(GrantPermissionRuleChooser.grant(android.Manifest.permission.POST_NOTIFICATIONS)) + .around(addAccountToDatabaseRule) + .around(addPrivateKeyToDatabaseRuleFirst) + .around(addPrivateKeyToDatabaseRuleSecond) + .around(activeActivityRule) + .around(ScreenshotTestRule()) + + @Test + fun testAddEmailToExistingKey() { + doTestAddEmailToExistingKey { + waitForObjectWithText(getResString(android.R.string.ok), 2000) + + onView(withId(R.id.buttonOk)) + .check(matches(isDisplayed())) + .perform(click()) + + waitForObjectWithText(getResString(R.string.provide_passphrase), 2000) + + onView(withId(R.id.eTKeyPassword)) + .inRoot(RootMatchers.isDialog()) + .perform( + clearText(), + replaceText(TestConstants.DEFAULT_STRONG_PASSWORD), + pressImeActionButton() + ) + } + } +} diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableSingleKeyWithPassphraseInDatabaseFlowTest.kt similarity index 74% rename from FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableFlowTest.kt rename to FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableSingleKeyWithPassphraseInDatabaseFlowTest.kt index a029aa13bc..d084afe6e8 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableSingleKeyWithPassphraseInDatabaseFlowTest.kt @@ -23,24 +23,17 @@ import com.flowcrypt.email.extensions.kotlin.asInternetAddress import com.flowcrypt.email.junit.annotations.FlowCryptTestSettings import com.flowcrypt.email.rules.AddPrivateKeyToDatabaseRule import com.flowcrypt.email.rules.ClearAppSettingsRule -import com.flowcrypt.email.rules.FlowCryptMockWebServerRule import com.flowcrypt.email.rules.GrantPermissionRuleChooser import com.flowcrypt.email.rules.RetryRule import com.flowcrypt.email.rules.ScreenshotTestRule -import com.flowcrypt.email.ui.base.BaseComposeScreenTest +import com.flowcrypt.email.ui.base.BaseComposeScreenNoKeyAvailableTest import com.flowcrypt.email.util.PrivateKeysManager -import com.flowcrypt.email.util.TestGeneralUtil -import okhttp3.mockwebserver.Dispatcher -import okhttp3.mockwebserver.MockResponse -import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.CoreMatchers.not -import org.junit.ClassRule import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain import org.junit.rules.TestRule import org.junit.runner.RunWith -import java.net.HttpURLConnection /** * @author Denys Bondarenko @@ -48,7 +41,7 @@ import java.net.HttpURLConnection @MediumTest @RunWith(AndroidJUnit4::class) @FlowCryptTestSettings(useCommonIdling = false) -class ComposeScreenNoKeyAvailableFlowTest : BaseComposeScreenTest() { +class ComposeScreenNoKeyAvailableSingleKeyWithPassphraseInDatabaseFlowTest : BaseComposeScreenNoKeyAvailableTest() { private val addPrivateKeyToDatabaseRule = AddPrivateKeyToDatabaseRule( keyPath = "pgp/denbond7@flowcrypt.test_prv_strong_primary.asc" ) @@ -116,28 +109,10 @@ class ComposeScreenNoKeyAvailableFlowTest : BaseComposeScreenTest() { .check(matches(not(hasTextColor(R.color.gray)))) } - companion object { - @get:ClassRule - @JvmStatic - val mockWebServerRule = FlowCryptMockWebServerRule(TestConstants.MOCK_WEB_SERVER_PORT, - object : Dispatcher() { - override fun dispatch(request: RecordedRequest): MockResponse { - if (request.path?.startsWith("/attester/pub", ignoreCase = true) == true) { - val lastSegment = request.requestUrl?.pathSegments?.lastOrNull() - - when { - TestConstants.RECIPIENT_WITH_PUBLIC_KEY_ON_ATTESTER.equals( - lastSegment, true - ) -> { - return MockResponse() - .setResponseCode(HttpURLConnection.HTTP_OK) - .setBody(TestGeneralUtil.readResourceAsString("3.txt")) - } - } - } - - return MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) - } - }) + @Test + fun testAddEmailToExistingSingleKeyPassphraseInDatabase() { + doTestAddEmailToExistingKey { + //no more additional actions + } } } diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableSingleKeyWithPassphraseInRamFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableSingleKeyWithPassphraseInRamFlowTest.kt new file mode 100644 index 0000000000..94dbae9113 --- /dev/null +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ComposeScreenNoKeyAvailableSingleKeyWithPassphraseInRamFlowTest.kt @@ -0,0 +1,71 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.ui + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.replaceText +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist +import androidx.test.espresso.matcher.RootMatchers +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import com.flowcrypt.email.R +import com.flowcrypt.email.TestConstants +import com.flowcrypt.email.database.entity.KeyEntity +import com.flowcrypt.email.junit.annotations.FlowCryptTestSettings +import com.flowcrypt.email.rules.AddPrivateKeyToDatabaseRule +import com.flowcrypt.email.rules.ClearAppSettingsRule +import com.flowcrypt.email.rules.GrantPermissionRuleChooser +import com.flowcrypt.email.rules.RetryRule +import com.flowcrypt.email.rules.ScreenshotTestRule +import com.flowcrypt.email.ui.base.BaseComposeScreenNoKeyAvailableTest +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +/** + * @author Denys Bondarenko + */ +@MediumTest +@RunWith(AndroidJUnit4::class) +@FlowCryptTestSettings(useCommonIdling = false) +class ComposeScreenNoKeyAvailableSingleKeyWithPassphraseInRamFlowTest : BaseComposeScreenNoKeyAvailableTest() { + private val addPrivateKeyToDatabaseRule = AddPrivateKeyToDatabaseRule( + keyPath = "pgp/key_testing@flowcrypt.test_keyA_strong.asc", + passphraseType = KeyEntity.PassphraseType.RAM + ) + + @get:Rule + var ruleChain: TestRule = RuleChain + .outerRule(RetryRule.DEFAULT) + .around(ClearAppSettingsRule()) + .around(GrantPermissionRuleChooser.grant(android.Manifest.permission.POST_NOTIFICATIONS)) + .around(addAccountToDatabaseRule) + .around(addPrivateKeyToDatabaseRule) + .around(activeActivityRule) + .around(ScreenshotTestRule()) + + @Test + fun testAddEmailToExistingKey() { + doTestAddEmailToExistingKey { + onView(withId(R.id.buttonOk)) + .check(doesNotExist()) + + waitForObjectWithText(getResString(R.string.provide_passphrase), 2000) + + onView(withId(R.id.eTKeyPassword)) + .inRoot(RootMatchers.isDialog()) + .perform( + ViewActions.clearText(), + replaceText(TestConstants.DEFAULT_STRONG_PASSWORD), + ViewActions.pressImeActionButton() + ) + } + } +} diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsFlowTest.kt index b650223156..8d067e7228 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MessageDetailsFlowTest.kt @@ -355,6 +355,7 @@ class MessageDetailsFlowTest : BaseMessageDetailsFlowTest() { } @Test + @Ignore("Should be fixed before the next release") fun testMissingKeyErrorChooseFromFewPubKeys() { val msgInfo = getMsgInfo( path = "messages/info/encrypted_msg_info_text_with_missing_key.json", diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseComposeScreenNoKeyAvailableTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseComposeScreenNoKeyAvailableTest.kt new file mode 100644 index 0000000000..fe36703ce1 --- /dev/null +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseComposeScreenNoKeyAvailableTest.kt @@ -0,0 +1,94 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.ui.base + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.hasTextColor +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import com.flowcrypt.email.R +import com.flowcrypt.email.TestConstants +import com.flowcrypt.email.extensions.kotlin.asInternetAddress +import com.flowcrypt.email.rules.FlowCryptMockWebServerRule +import com.flowcrypt.email.util.TestGeneralUtil +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.RecordedRequest +import org.hamcrest.CoreMatchers +import org.junit.ClassRule +import java.net.HttpURLConnection + +/** + * @author Denys Bondarenko + */ +abstract class BaseComposeScreenNoKeyAvailableTest : BaseComposeScreenTest() { + + protected fun doTestAddEmailToExistingKey(action: () -> Unit) { + activeActivityRule?.launch(intent) + registerAllIdlingResources() + fillInAllFields( + to = setOf( + requireNotNull(TestConstants.RECIPIENT_WITH_PUBLIC_KEY_ON_ATTESTER.asInternetAddress()) + ) + ) + + //check that editTextFrom has gray text color. It means a sender doesn't have a private key + onView(withId(R.id.editTextFrom)) + .check(matches(isDisplayed())) + .check(matches(hasTextColor(R.color.gray))) + + onView(withId(R.id.menuActionSend)) + .check(matches(isDisplayed())) + .perform(click()) + + isDialogWithTextDisplayed( + decorView, + getResString(R.string.no_key_available, addAccountToDatabaseRule.account.email) + ) + + onView(withText(R.string.add_email_to_existing_key)) + .check(matches(isDisplayed())) + .perform(click()) + + action.invoke() + + Thread.sleep(2000) + + //check that editTextFrom doesn't have gray text color. It means a sender has a private key. + onView(withId(R.id.editTextFrom)) + .check(matches(isDisplayed())) + .check(matches(CoreMatchers.not(hasTextColor(R.color.gray)))) + } + + companion object { + @get:ClassRule + @JvmStatic + val mockWebServerRule = FlowCryptMockWebServerRule( + TestConstants.MOCK_WEB_SERVER_PORT, + object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + if (request.path?.startsWith("/attester/pub", ignoreCase = true) == true) { + val lastSegment = request.requestUrl?.pathSegments?.lastOrNull() + + when { + TestConstants.RECIPIENT_WITH_PUBLIC_KEY_ON_ATTESTER.equals( + lastSegment, true + ) -> { + return MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setBody(TestGeneralUtil.readResourceAsString("3.txt")) + } + } + } + + return MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) + } + }) + } +} \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/FragmentExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/FragmentExt.kt index b6627592dc..983e506922 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/FragmentExt.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/FragmentExt.kt @@ -177,10 +177,12 @@ fun androidx.fragment.app.Fragment.setFragmentResultListenerForInfoDialog( fun androidx.fragment.app.Fragment.showNeedPassphraseDialog( requestKey: String, fingerprints: List, + requestCode: Int = Int.MIN_VALUE, logicType: Long = FixNeedPassphraseIssueDialogFragment.LogicType.AT_LEAST_ONE ) { showNeedPassphraseDialog( requestKey = requestKey, + requestCode = requestCode, navController = navController, fingerprints = fingerprints, logicType = logicType diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/EditPrivateKeyViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/EditPrivateKeyViewModel.kt new file mode 100644 index 0000000000..26450f3ea9 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/EditPrivateKeyViewModel.kt @@ -0,0 +1,91 @@ +/* + * © 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.viewModelScope +import com.flowcrypt.email.api.retrofit.response.base.Result +import com.flowcrypt.email.database.entity.RecipientEntity +import com.flowcrypt.email.extensions.org.bouncycastle.openpgp.toPgpKeyRingDetails +import com.flowcrypt.email.security.KeyStoreCryptoManager +import com.flowcrypt.email.security.KeysStorageImpl +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 org.pgpainless.PGPainless +import org.pgpainless.key.util.UserId + +/** + * @author Denys Bondarenko + */ +class EditPrivateKeyViewModel(val fingerprint: String, application: Application) : + AccountViewModel(application) { + + private val controlledRunnerForEditingPrivateKey = ControlledRunner>() + private val editPrivateKeyMutableStateFlow: MutableStateFlow> = + MutableStateFlow(Result.loading()) + val editPrivateKeyStateFlow: StateFlow> = + editPrivateKeyMutableStateFlow.asStateFlow() + + fun addUserId(userId: UserId) { + viewModelScope.launch { + editPrivateKeyMutableStateFlow.value = Result.loading() + editPrivateKeyMutableStateFlow.value = + controlledRunnerForEditingPrivateKey.cancelPreviousThenRun { + return@cancelPreviousThenRun addUserIdInternal(userId) + } + } + } + + private suspend fun addUserIdInternal(userId: UserId): Result = + withContext(Dispatchers.IO) { + try { + val keyStore = KeysStorageImpl.getInstance(getApplication()) + val pgpSecretKeyRing = keyStore.getPGPSecretKeyRingByFingerprint(fingerprint) + ?: throw IllegalStateException("Private key with fingerprint = $fingerprint not found") + + val modifiedPgpSecretKeyRing = PGPainless.modifyKeyRing(pgpSecretKeyRing) + .addUserId(userId, keyStore.getSecretKeyRingProtector()).done() + + val account = + keyStore.getActiveAccount() ?: throw IllegalStateException("Account is not defined") + val entity = + roomDatabase.keysDao().getKeyByAccountAndFingerprint(account.email, fingerprint) + ?: throw IllegalStateException("Private key with fingerprint = $fingerprint not found") + + val pgpKeyRingDetails = modifiedPgpSecretKeyRing.toPgpKeyRingDetails() + val encryptedPrvKey = + KeyStoreCryptoManager.encryptSuspend(pgpKeyRingDetails.privateKey).toByteArray() + roomDatabase.keysDao().updateSuspend(entity.copy(privateKey = encryptedPrvKey)) + + //update contacts and pub keys + val email = userId.email.lowercase() + var cachedRecipientWithPubKeys = roomDatabase.recipientDao() + .getRecipientWithPubKeysByEmailSuspend(email) + + if (cachedRecipientWithPubKeys == null) { + roomDatabase.recipientDao().insertSuspend(RecipientEntity(email = email)) + cachedRecipientWithPubKeys = + roomDatabase.recipientDao().getRecipientWithPubKeysByEmailSuspend(email) + ?: return@withContext Result.success(true) + } + + if (cachedRecipientWithPubKeys.publicKeys.none { + it.fingerprint == pgpKeyRingDetails.fingerprint + }) { + roomDatabase.pubKeyDao() + .insertWithReplaceSuspend(pgpKeyRingDetails.toPublicKeyEntity(email)) + } + return@withContext Result.success(true) + } catch (e: Exception) { + return@withContext Result.exception(e) + } + } +} \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/PrivateKeysViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/PrivateKeysViewModel.kt index abdc3a5a86..9b9a0e9854 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/PrivateKeysViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/PrivateKeysViewModel.kt @@ -100,6 +100,8 @@ class PrivateKeysViewModel(application: Application) : AccountViewModel(applicat } } + fun getActiveAccount(): AccountEntity? = keysStorage.getActiveAccount() + @ExperimentalCoroutinesApi val secretKeyRingsInfoStateFlow: StateFlow?>> = keysStorage.secretKeyRingsLiveData.asFlow().flatMapLatest { 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 1a10df4495..9c4dc4264b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/model/KeysStorage.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/model/KeysStorage.kt @@ -6,6 +6,7 @@ package com.flowcrypt.email.model import androidx.annotation.Keep +import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.KeyEntity import com.flowcrypt.email.security.model.PgpKeyRingDetails import kotlinx.coroutines.flow.Flow @@ -56,4 +57,6 @@ interface KeysStorage { fun getFirstUsableForEncryptionPGPSecretKeyRing(user: String): PGPSecretKeyRing? fun getPassPhrasesUpdatesFlow(): Flow + + fun getActiveAccount(): AccountEntity? } 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 deffe52c94..336b610547 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/KeysStorageImpl.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/KeysStorageImpl.kt @@ -12,6 +12,7 @@ import androidx.lifecycle.asFlow import androidx.lifecycle.liveData import androidx.lifecycle.switchMap import com.flowcrypt.email.database.FlowCryptRoomDatabase +import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.KeyEntity import com.flowcrypt.email.extensions.org.bouncycastle.openpgp.toPgpKeyRingDetails import com.flowcrypt.email.extensions.org.pgpainless.key.info.usableForEncryption @@ -280,6 +281,8 @@ class KeysStorageImpl private constructor(context: Context) : KeysStorage { override fun getPassPhrasesUpdatesFlow(): Flow = passphrasesUpdatesLiveData.asFlow() + override fun getActiveAccount(): AccountEntity? = pureActiveAccountLiveData.value + private fun preparePassphrasesMap(keyEntityList: List) { val existedIdList = passPhraseMap.keys val refreshedIdList = keyEntityList.map { it.fingerprint } 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 b8b142e645..0ebc557dc6 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 @@ -64,11 +64,13 @@ import com.flowcrypt.email.extensions.exceptionMsg import com.flowcrypt.email.extensions.gone import com.flowcrypt.email.extensions.hideKeyboard import com.flowcrypt.email.extensions.incrementSafely +import com.flowcrypt.email.extensions.kotlin.isValidEmail import com.flowcrypt.email.extensions.launchAndRepeatWithViewLifecycle import com.flowcrypt.email.extensions.navController import com.flowcrypt.email.extensions.org.bouncycastle.openpgp.toPgpKeyRingDetails import com.flowcrypt.email.extensions.showActionDialogFragment import com.flowcrypt.email.extensions.showChoosePublicKeyDialogFragment +import com.flowcrypt.email.extensions.showDialogFragment import com.flowcrypt.email.extensions.showInfoDialog import com.flowcrypt.email.extensions.showKeyboard import com.flowcrypt.email.extensions.showNeedPassphraseDialog @@ -90,8 +92,13 @@ import com.flowcrypt.email.security.model.PgpKeyRingDetails import com.flowcrypt.email.security.pgp.PgpDecryptAndOrVerify import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment import com.flowcrypt.email.ui.activity.fragment.dialog.ActionsDialogFragment +import com.flowcrypt.email.ui.activity.fragment.dialog.AddNewUserIdToPrivateKeyDialogFragment +import com.flowcrypt.email.ui.activity.fragment.dialog.AddNewUserIdToPrivateKeyDialogFragmentArgs +import com.flowcrypt.email.ui.activity.fragment.dialog.ChoosePrivateKeyDialogFragment +import com.flowcrypt.email.ui.activity.fragment.dialog.ChoosePrivateKeyDialogFragmentArgs import com.flowcrypt.email.ui.activity.fragment.dialog.ChoosePublicKeyDialogFragment import com.flowcrypt.email.ui.activity.fragment.dialog.CreateOutgoingMessageDialogFragment +import com.flowcrypt.email.ui.activity.fragment.dialog.FixNeedPassphraseIssueDialogFragment import com.flowcrypt.email.ui.activity.fragment.dialog.NoPgpFoundDialogFragment import com.flowcrypt.email.ui.adapter.AttachmentsRecyclerViewAdapter import com.flowcrypt.email.ui.adapter.AutoCompleteResultRecyclerViewAdapter @@ -332,6 +339,8 @@ class CreateMessageFragment : BaseFragment(), subscribeToActionsDialogFragment() subscribeToImportingAdditionalPrivateKeys() subscribeToChoosePublicKeyDialogFragment() + subscribeToChoosePrivateKeysDialogFragment() + subscribeToAddNewUserIdToPrivateKeyDialogFragment() subscribeToCreateOutgoingMessageDialogFragment() val isEncryptedMode = @@ -1585,8 +1594,31 @@ class CreateMessageFragment : BaseFragment(), } private fun subscribeToFixNeedPassphraseIssueDialogFragment() { - setFragmentResultListener(REQUEST_KEY_FIX_MISSING_PASSPHRASE) { _, _ -> - sendMsg() + setFragmentResultListener(REQUEST_KEY_FIX_MISSING_PASSPHRASE) { _, bundle -> + val requestCode = bundle.getInt( + FixNeedPassphraseIssueDialogFragment.KEY_REQUEST_CODE, Int.MIN_VALUE + ) + + when (requestCode) { + REQUEST_CODE_FIX_MISSING_PASSPHRASE_FOR_PRIVATE_KEY_BY_SENDER_EMAIL -> sendMsg() + REQUEST_CODE_FIX_MISSING_PASSPHRASE_FOR_PRIVATE_KEY_BY_FINGERPRINT -> { + val predefinedFingerprints = bundle.getStringArray( + FixNeedPassphraseIssueDialogFragment.KEY_PREDEFINED_FINGERPRINTS + ) ?: emptyArray() + + val fingerprintsOfUnlockedKeys = bundle.getStringArray( + FixNeedPassphraseIssueDialogFragment.KEY_RESULT + ) ?: emptyArray() + + if (predefinedFingerprints.contentEquals(fingerprintsOfUnlockedKeys)) { + addNewUserIdToPrivateKey(predefinedFingerprints.first()) + } else { + toast(R.string.error_occurred_please_try_again) + } + } + + else -> toast(R.string.unknown_error) + } } } @@ -1675,7 +1707,17 @@ class CreateMessageFragment : BaseFragment(), } RESULT_CODE_ADD_USER_ID_TO_EXISTING_PRIVATE_KEY -> { - //todo-denbond7 need to add realization in a separate PR + showDialogFragment(navController) { + return@showDialogFragment object : NavDirections { + override val actionId = R.id.choose_private_key_dialog_graph + override val arguments = ChoosePrivateKeyDialogFragmentArgs( + requestKey = REQUEST_KEY_CHOOSE_PRIVATE_KEYS, + choiceMode = ListView.CHOICE_MODE_SINGLE, + title = getString(R.string.please_choose_key_you_would_like_to_modify), + returnResultImmediatelyIfSingle = true + ).toBundle() + } + } } } } @@ -1707,6 +1749,41 @@ class CreateMessageFragment : BaseFragment(), } } + private fun subscribeToChoosePrivateKeysDialogFragment() { + setFragmentResultListener(REQUEST_KEY_CHOOSE_PRIVATE_KEYS) { _, bundle -> + val fingerprints = bundle.getStringArray(ChoosePrivateKeyDialogFragment.KEY_RESULT) + ?: return@setFragmentResultListener + + if (fingerprints.isEmpty()) { + toast(R.string.please_select_key) + } else { + val fingerprint = fingerprints.first() + + val passphrase = + KeysStorageImpl.getInstance(requireContext()).getPassphraseByFingerprint(fingerprint) + + if (passphrase == null || passphrase.isEmpty) { + showNeedPassphraseDialog( + requestKey = REQUEST_KEY_FIX_MISSING_PASSPHRASE, + requestCode = REQUEST_CODE_FIX_MISSING_PASSPHRASE_FOR_PRIVATE_KEY_BY_FINGERPRINT, + fingerprints = listOf(fingerprint), + logicType = FixNeedPassphraseIssueDialogFragment.LogicType.ALL + ) + } else { + addNewUserIdToPrivateKey(fingerprint) + } + } + } + } + + private fun subscribeToAddNewUserIdToPrivateKeyDialogFragment() { + setFragmentResultListener(REQUEST_KEY_ADD_NEW_USER_ID_TO_PRIVATE_KEY) { _, bundle -> + val fingerprint = bundle.getString(AddNewUserIdToPrivateKeyDialogFragment.KEY_RESULT) + ?: return@setFragmentResultListener + toast(getString(R.string.key_was_updated, fingerprint), Toast.LENGTH_LONG) + } + } + private fun subscribeToCreateOutgoingMessageDialogFragment() { setFragmentResultListener(REQUEST_KEY_CREATE_OUTGOING_MESSAGE) { _, bundle -> val result: Result<*>? = @@ -1836,6 +1913,7 @@ class CreateMessageFragment : BaseFragment(), if (passphrase?.isEmpty == true) { showNeedPassphraseDialog( requestKey = REQUEST_KEY_FIX_MISSING_PASSPHRASE, + requestCode = REQUEST_CODE_FIX_MISSING_PASSPHRASE_FOR_PRIVATE_KEY_BY_SENDER_EMAIL, fingerprints = listOf(fingerprint) ) return @@ -1856,24 +1934,49 @@ class CreateMessageFragment : BaseFragment(), } private fun fixNoKeyAvailableIssue(text: String) { - showActionDialogFragment( - navController, - requestKey = REQUEST_KEY_FIX_NO_PRIVATE_KEY_AVAILABLE, - dialogTitle = text, - isCancelable = true, - items = listOf( - DialogItem( - iconResourceId = R.drawable.ic_import_user_public_key, - title = getString(R.string.import_private_key), - id = RESULT_CODE_IMPORT_PRIVATE_KEY - ), - DialogItem( - iconResourceId = R.drawable.ic_edit_key_add_user_id, - title = getString(R.string.add_email_to_existing_key), - id = RESULT_CODE_ADD_USER_ID_TO_EXISTING_PRIVATE_KEY + if (account?.clientConfiguration?.usesKeyManager() == true) { + toast(getString(R.string.no_prv_keys_ask_admin)) + } else { + showActionDialogFragment( + navController, + requestKey = REQUEST_KEY_FIX_NO_PRIVATE_KEY_AVAILABLE, + dialogTitle = text, + isCancelable = true, + items = listOf( + DialogItem( + iconResourceId = R.drawable.ic_import_user_public_key, + title = getString(R.string.import_private_key), + id = RESULT_CODE_IMPORT_PRIVATE_KEY + ), + DialogItem( + iconResourceId = R.drawable.ic_edit_key_add_user_id, + title = getString(R.string.add_email_to_existing_key), + id = RESULT_CODE_ADD_USER_ID_TO_EXISTING_PRIVATE_KEY + ) ) ) - ) + } + } + + private fun addNewUserIdToPrivateKey(fingerprint: String) { + val email = fromAddressesAdapter?.getItem( + binding?.spinnerFrom?.selectedItemPosition ?: Spinner.INVALID_POSITION + ) ?: return + + if (email.isValidEmail()) { + showDialogFragment(navController) { + return@showDialogFragment object : NavDirections { + override val actionId = R.id.add_new_userid_to_private_key_dialog_graph + override val arguments = AddNewUserIdToPrivateKeyDialogFragmentArgs( + requestKey = REQUEST_KEY_ADD_NEW_USER_ID_TO_PRIVATE_KEY, + fingerprint = fingerprint, + userId = email, + ).toBundle() + } + } + } else { + toast(R.string.error_email_is_not_valid) + } } companion object { @@ -1922,7 +2025,20 @@ class CreateMessageFragment : BaseFragment(), CreateMessageFragment::class.java ) + private val REQUEST_KEY_CHOOSE_PRIVATE_KEYS = GeneralUtil.generateUniqueExtraKey( + "REQUEST_KEY_CHOOSE_PRIVATE_KEYS", + CreateMessageFragment::class.java + ) + + private val REQUEST_KEY_ADD_NEW_USER_ID_TO_PRIVATE_KEY = GeneralUtil.generateUniqueExtraKey( + "REQUEST_KEY_ADD_NEW_USER_ID_TO_PRIVATE_KEY", + CreateMessageFragment::class.java + ) + private const val RESULT_CODE_IMPORT_PRIVATE_KEY = 1 private const val RESULT_CODE_ADD_USER_ID_TO_EXISTING_PRIVATE_KEY = 2 + + private const val REQUEST_CODE_FIX_MISSING_PASSPHRASE_FOR_PRIVATE_KEY_BY_SENDER_EMAIL = 1 + private const val REQUEST_CODE_FIX_MISSING_PASSPHRASE_FOR_PRIVATE_KEY_BY_FINGERPRINT = 2 } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/RecipientDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/RecipientDetailsFragment.kt index fef97c19b0..3d66a035da 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/RecipientDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/RecipientDetailsFragment.kt @@ -78,7 +78,7 @@ class RecipientDetailsFragment : BaseFragment() recipientDetailsViewModel.recipientPubKeysFlow.collect { it ?: return@collect if (it.isEmpty()) { - showEmptyView(resourcesId = R.drawable.ic_no_result_grey_24dp) + showEmptyView(imageResourcesId = R.drawable.ic_no_result_grey_24dp) } else { pubKeysRecyclerViewAdapter.submitList(it) showContent() diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/ListProgressBehaviour.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/ListProgressBehaviour.kt index 6364d135cc..a4ad76b785 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/ListProgressBehaviour.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/ListProgressBehaviour.kt @@ -34,7 +34,7 @@ interface ListProgressBehaviour : ProgressBehaviour { super.showStatus(msg, resourcesId) } - fun showEmptyView(msg: String? = null, resourcesId: Int = 0) { + fun showEmptyView(msg: String? = null, imageResourcesId: Int = 0) { contentView?.visibility = View.GONE goneStatusView() goneProgressView() @@ -44,9 +44,9 @@ interface ListProgressBehaviour : ProgressBehaviour { tVEmpty?.text = it } - if (resourcesId > 0) { + if (imageResourcesId > 0) { val iVEmptyImg = emptyView?.findViewById(R.id.iVEmptyImg) - iVEmptyImg?.setImageResource(resourcesId) + iVEmptyImg?.setImageResource(imageResourcesId) } emptyView?.visibility = View.VISIBLE diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/AddNewUserIdToPrivateKeyDialogFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/AddNewUserIdToPrivateKeyDialogFragment.kt new file mode 100644 index 0000000000..ba92c24df7 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/AddNewUserIdToPrivateKeyDialogFragment.kt @@ -0,0 +1,132 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.ui.activity.fragment.dialog + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.core.os.bundleOf +import androidx.fragment.app.setFragmentResult +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModel +import androidx.navigation.fragment.navArgs +import com.flowcrypt.email.R +import com.flowcrypt.email.api.retrofit.response.base.Result +import com.flowcrypt.email.databinding.FragmentCommonProcessingBinding +import com.flowcrypt.email.extensions.exceptionMsg +import com.flowcrypt.email.extensions.gone +import com.flowcrypt.email.extensions.launchAndRepeatWithLifecycle +import com.flowcrypt.email.extensions.navController +import com.flowcrypt.email.extensions.visible +import com.flowcrypt.email.jetpack.lifecycle.CustomAndroidViewModelFactory +import com.flowcrypt.email.jetpack.viewmodel.EditPrivateKeyViewModel +import com.flowcrypt.email.ui.activity.fragment.base.ProgressBehaviour +import com.flowcrypt.email.util.GeneralUtil +import org.pgpainless.key.util.UserId + +/** + * @author Denys Bondarenko + */ +class AddNewUserIdToPrivateKeyDialogFragment : BaseDialogFragment(), ProgressBehaviour { + private var binding: FragmentCommonProcessingBinding? = null + private val args by navArgs() + private val editPrivateKeyViewModel: EditPrivateKeyViewModel by viewModels { + object : CustomAndroidViewModelFactory(requireActivity().application) { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return EditPrivateKeyViewModel(args.fingerprint, requireActivity().application) as T + } + } + } + + override val progressView: View? + get() = binding?.layoutProgress?.root + override val contentView: View? + get() = null + override val statusView: View? + get() = binding?.layoutStatus?.root + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + isCancelable = false + collectEditPrivateKeyStateFlow() + modifyKey() + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = FragmentCommonProcessingBinding.inflate( + LayoutInflater.from(requireContext()), + if ((view != null) and (view is ViewGroup)) view as ViewGroup? else null, + false + ) + + val builder = AlertDialog.Builder(requireContext()).apply { + setView(binding?.root) + //just need to describe a button + setPositiveButton(R.string.retry) { _, _ -> } + + setNegativeButton(R.string.cancel) { _, _ -> + navController?.navigateUp() + } + } + + return builder.create() + } + + override fun onStart() { + super.onStart() + getButton(AlertDialog.BUTTON_POSITIVE)?.apply { + setOnClickListener { + modifyKey() + gone() + } + //we hide the button at the start up. + gone() + } + } + + private fun modifyKey() { + editPrivateKeyViewModel.addUserId(UserId.onlyEmail(args.userId)) + } + + private fun collectEditPrivateKeyStateFlow() { + launchAndRepeatWithLifecycle { + editPrivateKeyViewModel.editPrivateKeyStateFlow.collect { + when (it.status) { + Result.Status.LOADING -> { + showProgress(getString(R.string.processing_please_wait)) + } + + Result.Status.SUCCESS -> { + navController?.navigateUp() + if (it.data == true) { + setFragmentResult( + args.requestKey, + bundleOf(KEY_RESULT to args.fingerprint) + ) + } + } + + Result.Status.EXCEPTION, Result.Status.ERROR -> { + showStatus(msg = it.exceptionMsg) + getButton(AlertDialog.BUTTON_POSITIVE)?.apply { visible() } + } + + else -> {} + } + } + } + } + + companion object { + val KEY_RESULT = GeneralUtil.generateUniqueExtraKey( + "KEY_RESULT", AddNewUserIdToPrivateKeyDialogFragment::class.java + ) + } +} \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/BaseDialogFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/BaseDialogFragment.kt index 069e3c3e5c..65df641988 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/BaseDialogFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/BaseDialogFragment.kt @@ -8,7 +8,9 @@ package com.flowcrypt.email.ui.activity.fragment.dialog import android.text.method.LinkMovementMethod import android.text.util.Linkify import android.view.View +import android.widget.Button import android.widget.TextView +import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import com.flowcrypt.email.util.IdlingCountListener import java.util.concurrent.atomic.AtomicInteger @@ -47,4 +49,8 @@ abstract class BaseDialogFragment : DialogFragment(), IdlingCountListener { } } } + + protected fun getButton(whichButton: Int): Button? { + return (dialog as? AlertDialog)?.getButton(whichButton) + } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/ChoosePrivateKeyDialogFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/ChoosePrivateKeyDialogFragment.kt new file mode 100644 index 0000000000..ab1b06b1e9 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/ChoosePrivateKeyDialogFragment.kt @@ -0,0 +1,147 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.ui.activity.fragment.dialog + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.core.os.bundleOf +import androidx.fragment.app.setFragmentResult +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.navArgs +import com.flowcrypt.email.R +import com.flowcrypt.email.api.retrofit.response.base.Result +import com.flowcrypt.email.databinding.FragmentChoosePrivateKeyBinding +import com.flowcrypt.email.extensions.exceptionMsg +import com.flowcrypt.email.extensions.navController +import com.flowcrypt.email.extensions.toast +import com.flowcrypt.email.jetpack.viewmodel.PrivateKeysViewModel +import com.flowcrypt.email.security.model.PgpKeyRingDetails +import com.flowcrypt.email.ui.activity.fragment.base.ListProgressBehaviour +import com.flowcrypt.email.ui.adapter.PrivateKeysArrayAdapter +import com.flowcrypt.email.util.GeneralUtil + +/** + * This dialog can be used to pick imported private keys. + * + * @author Denys Bondarenko + */ +class ChoosePrivateKeyDialogFragment : BaseDialogFragment(), ListProgressBehaviour { + private var binding: FragmentChoosePrivateKeyBinding? = null + private val args by navArgs() + private val privateKeysViewModel: PrivateKeysViewModel by viewModels() + + override val emptyView: View? + get() = binding?.layoutEmpty?.root + override val progressView: View? + get() = binding?.layoutProgress?.root + override val contentView: View? + get() = binding?.groupContent + override val statusView: View? + get() = binding?.layoutStatus?.root + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setupPrivateKeysViewModel() + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = FragmentChoosePrivateKeyBinding.inflate( + LayoutInflater.from(requireContext()), + if ((view != null) and (view is ViewGroup)) view as ViewGroup? else null, + false + ) + + return AlertDialog.Builder(requireContext()).apply { + setTitle(null) + binding?.textViewMessage?.text = args.title + binding?.buttonOk?.setOnClickListener { sendResult() } + setView(binding?.root) + }.create() + } + + private fun setupPrivateKeysViewModel() { + privateKeysViewModel.parseKeysResultLiveData.observe(this) { + when (it.status) { + Result.Status.LOADING -> { + showProgress() + } + + Result.Status.SUCCESS -> { + val pgpKeyDetailsList = it.data ?: emptyList() + + if (pgpKeyDetailsList.isEmpty()) { + showEmptyView( + msg = getString( + R.string.no_key_available, + privateKeysViewModel.getActiveAccount()?.email ?: "" + ), + imageResourcesId = R.drawable.ic_no_result_grey_24dp + ) + } else { + binding?.listViewKeys?.choiceMode = args.choiceMode + binding?.listViewKeys?.adapter = PrivateKeysArrayAdapter( + requireContext(), + pgpKeyDetailsList, + args.choiceMode + ) + binding?.listViewKeys?.setItemChecked(0, true) + + if (pgpKeyDetailsList.size == 1 && args.returnResultImmediatelyIfSingle) { + sendResult() + } else { + showContent() + } + } + } + + Result.Status.EXCEPTION -> { + binding?.textViewMessage?.text = it.exceptionMsg + showContent() + } + + else -> {} + } + } + } + + private fun sendResult() { + val selectedKeys = mutableListOf() + val checkedItemPositions = binding?.listViewKeys?.checkedItemPositions + val pgpKeyDetailsList = privateKeysViewModel.parseKeysResultLiveData.value?.data ?: emptyList() + if (checkedItemPositions != null) { + for (i in 0 until checkedItemPositions.size()) { + val key = checkedItemPositions.keyAt(i) + if (checkedItemPositions.get(key)) { + selectedKeys.add(pgpKeyDetailsList[key]) + } + } + } + + if (selectedKeys.isEmpty()) { + toast(R.string.please_select_key) + } else { + navController?.navigateUp() + sendResult(selectedKeys.map { it.fingerprint }) + } + } + + private fun sendResult(fingerprints: List) { + setFragmentResult( + args.requestKey, + bundleOf(KEY_RESULT to fingerprints.toTypedArray()) + ) + } + + companion object { + val KEY_RESULT = GeneralUtil.generateUniqueExtraKey( + "KEY_RESULT", ChoosePrivateKeyDialogFragment::class.java + ) + } +} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/FixNeedPassphraseIssueDialogFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/FixNeedPassphraseIssueDialogFragment.kt index c86f736532..6d44d25653 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/FixNeedPassphraseIssueDialogFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/FixNeedPassphraseIssueDialogFragment.kt @@ -223,14 +223,14 @@ class FixNeedPassphraseIssueDialogFragment : BaseDialogFragment() { binding?.pBCheckPassphrase?.invisible() val checkResults = it.data ?: emptyList() var isWrongPassphraseExceptionFound = false - var countOfMatchedPassphrases = 0 + val unlockedKeysFingerprints = mutableListOf() for (checkResult in checkResults) { if (checkResult.e is WrongPassPhraseException) { isWrongPassphraseExceptionFound = true } else { val passphraseType = checkResult.pgpKeyRingDetails.passphraseType ?: continue val rawPassphrase = checkResult.pgpKeyRingDetails.tempPassphrase ?: continue - countOfMatchedPassphrases++ + unlockedKeysFingerprints.add(checkResult.pgpKeyRingDetails.fingerprint) val passphrase = Passphrase(rawPassphrase) context?.let { nonNullContext -> val keysStorage = KeysStorageImpl.getInstance(nonNullContext) @@ -245,14 +245,18 @@ class FixNeedPassphraseIssueDialogFragment : BaseDialogFragment() { } when { - countOfMatchedPassphrases > 0 -> { + unlockedKeysFingerprints.size > 0 -> { when (args.logicType) { ALL -> { - if (countOfMatchedPassphrases == checkResults.size) { + if (unlockedKeysFingerprints.size == checkResults.size) { navController?.navigateUp() setFragmentResult( args.requestKey, - bundleOf(KEY_RESULT to 1, KEY_REQUEST_CODE to args.requestCode) + bundleOf( + KEY_RESULT to unlockedKeysFingerprints.toTypedArray(), + KEY_PREDEFINED_FINGERPRINTS to args.fingerprints, + KEY_REQUEST_CODE to args.requestCode + ) ) } } @@ -261,7 +265,11 @@ class FixNeedPassphraseIssueDialogFragment : BaseDialogFragment() { navController?.navigateUp() setFragmentResult( args.requestKey, - bundleOf(KEY_RESULT to 1, KEY_REQUEST_CODE to args.requestCode) + bundleOf( + KEY_RESULT to unlockedKeysFingerprints.toTypedArray(), + KEY_PREDEFINED_FINGERPRINTS to args.fingerprints, + KEY_REQUEST_CODE to args.requestCode + ) ) } } @@ -337,5 +345,9 @@ class FixNeedPassphraseIssueDialogFragment : BaseDialogFragment() { val KEY_REQUEST_CODE = GeneralUtil.generateUniqueExtraKey( "KEY_REQUEST_CODE", FixNeedPassphraseIssueDialogFragment::class.java ) + + val KEY_PREDEFINED_FINGERPRINTS = GeneralUtil.generateUniqueExtraKey( + "KEY_PREDEFINED_FINGERPRINTS", FixNeedPassphraseIssueDialogFragment::class.java + ) } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/PrivateKeysArrayAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/PrivateKeysArrayAdapter.kt new file mode 100644 index 0000000000..cf0c564de5 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/PrivateKeysArrayAdapter.kt @@ -0,0 +1,75 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.ui.adapter + +import android.content.Context +import android.graphics.Color +import android.text.Spanned +import android.text.style.ForegroundColorSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.ListView +import android.widget.TextView +import androidx.core.text.toSpannable +import com.flowcrypt.email.R +import com.flowcrypt.email.security.model.PgpKeyRingDetails + +/** + * @author Denys Bondarenko + */ +class PrivateKeysArrayAdapter(context: Context, keys: List, choiceMode: Int) : + ArrayAdapter(context, R.layout.private_key_item_radio_button, keys) { + private val inflater: LayoutInflater = LayoutInflater.from(context) + private val layoutId = if (choiceMode == ListView.CHOICE_MODE_MULTIPLE) { + R.layout.private_key_item_checkbox + } else { + R.layout.private_key_item_radio_button + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + var view = convertView + val item = getItem(position) + + val viewHolder: ViewHolder + if (view == null) { + viewHolder = ViewHolder() + view = inflater.inflate(layoutId, parent, false) + viewHolder.textViewEmail = view.findViewById(R.id.textViewEmail) + viewHolder.textViewFingerprint = view.findViewById(R.id.textViewFingerprint) + view.tag = viewHolder + } else { + viewHolder = view.tag as ViewHolder + } + + updateView(item, viewHolder) + + return requireNotNull(view) + } + + private fun updateView(pgpKeyRingDetails: PgpKeyRingDetails?, viewHolder: ViewHolder) { + val userIds = pgpKeyRingDetails?.users?.joinToString(separator = ",\n") ?: "" + viewHolder.textViewEmail?.text = userIds + if (userIds.contains("\n")) { + val spannable = viewHolder.textViewEmail?.text?.toSpannable() + spannable?.setSpan( + ForegroundColorSpan(Color.GRAY), + userIds.indexOfFirst { it == '\n' }, + userIds.length, + Spanned.SPAN_INCLUSIVE_INCLUSIVE + ) + viewHolder.textViewEmail?.text = spannable + } + + viewHolder.textViewFingerprint?.text = pgpKeyRingDetails?.fingerprint + } + + private class ViewHolder { + var textViewEmail: TextView? = null + var textViewFingerprint: TextView? = null + } +} diff --git a/FlowCrypt/src/main/res/layout/fragment_choose_private_key.xml b/FlowCrypt/src/main/res/layout/fragment_choose_private_key.xml new file mode 100644 index 0000000000..891ea668bc --- /dev/null +++ b/FlowCrypt/src/main/res/layout/fragment_choose_private_key.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + +