Skip to content

Commit

Permalink
Added some UI and logic. Refactored code.| #1251
Browse files Browse the repository at this point in the history
  • Loading branch information
DenBond7 committed Jun 2, 2021
1 parent 8266be3 commit d9269e0
Show file tree
Hide file tree
Showing 15 changed files with 443 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact [email protected]
* Contributors: DenBond7
*/

package com.flowcrypt.email.jetpack.viewmodel

import android.app.Application
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap
import com.flowcrypt.email.api.retrofit.response.base.Result
import com.flowcrypt.email.extensions.org.bouncycastle.openpgp.toPgpKeyDetails
import com.flowcrypt.email.security.KeysStorageImpl
import com.flowcrypt.email.security.model.PgpKeyDetails

/**
* @author Denis Bondarenko
* Date: 6/1/21
* Time: 9:59 AM
* E-mail: [email protected]
*/
class KeysWithEmptyPassphraseViewModel(application: Application) : AccountViewModel(application) {
private val keysStorage: KeysStorageImpl = KeysStorageImpl.getInstance(getApplication())

val keysWithEmptyPassphrasesLiveData: MediatorLiveData<Result<List<PgpKeyDetails>>> =
MediatorLiveData()

private val pgpKeyDetailsLiveData: LiveData<Result<List<PgpKeyDetails>>> =
keysStorage.secretKeyRingsLiveData.switchMap { list ->
liveData {
emit(Result.loading())
emit(Result.success(list
.map { it.toPgpKeyDetails() }
.filter { keysStorage.getPassphraseByFingerprint(it.fingerprint)?.isEmpty == true })
)
}
}

private val afterPassphraseUpdatedKeyDetailsLiveData: LiveData<Result<List<PgpKeyDetails>>> =
keysStorage.passphrasesUpdatesLiveData.switchMap {
liveData {
emit(Result.loading())
emit(
Result.success(keysStorage.getPgpKeyDetailsList()
.filter { keysStorage.getPassphraseByFingerprint(it.fingerprint)?.isEmpty == true })
)
}
}

init {
keysWithEmptyPassphrasesLiveData.addSource(pgpKeyDetailsLiveData) {
keysWithEmptyPassphrasesLiveData.value = it
}
keysWithEmptyPassphrasesLiveData.addSource(afterPassphraseUpdatedKeyDetailsLiveData) {
keysWithEmptyPassphrasesLiveData.value = it
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import com.flowcrypt.email.api.retrofit.node.NodeRepository
import com.flowcrypt.email.api.retrofit.node.PgpApiRepository
import com.flowcrypt.email.api.retrofit.request.node.ParseDecryptMsgRequest
import com.flowcrypt.email.api.retrofit.response.base.Result
import com.flowcrypt.email.api.retrofit.response.model.node.DecryptErrorDetails
import com.flowcrypt.email.api.retrofit.response.model.node.DecryptErrorMsgBlock
import com.flowcrypt.email.api.retrofit.response.model.node.PublicKeyMsgBlock
import com.flowcrypt.email.api.retrofit.response.node.ParseDecryptedMsgResult
import com.flowcrypt.email.database.FlowCryptRoomDatabase
Expand Down Expand Up @@ -93,6 +95,8 @@ class MsgDetailsViewModel(
private var currentPercentage = 0
private var lastUpdateTime = System.currentTimeMillis()

val passphraseNeededLiveData: MutableLiveData<Boolean> = MutableLiveData()

val freshMsgLiveData: LiveData<MessageEntity?> = roomDatabase.msgDao().getMsgLiveData(
account = messageEntity.email,
folder = messageEntity.folder,
Expand Down Expand Up @@ -393,6 +397,14 @@ class MsgDetailsViewModel(
val contactEntity = roomDatabase.contactsDao().getContactByEmailSuspend(pgpContact.email)
block.existingPgpContact = contactEntity?.toPgpContact()
}

if (block is DecryptErrorMsgBlock) {
if (block.error?.details?.type == DecryptErrorDetails.Type.NEED_PASSPHRASE
&& keysStorage.hasEmptyPassphrase()
) {
passphraseNeededLiveData.postValue(true)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class PgpKeyDetailsViewModel(val fingerprint: String?, application: Application)

fun forgetPassphrase() {
fingerprint?.let {
keysStorage.putPassPhraseToCache(
keysStorage.putPassphraseToCache(
fingerprint = it,
passphrase = Passphrase.emptyPassphrase(),
validUntil = Instant.now(),
Expand All @@ -86,7 +86,7 @@ class PgpKeyDetailsViewModel(val fingerprint: String?, application: Application)

fun updatePassphrase(passphrase: Passphrase) {
fingerprint?.let {
keysStorage.putPassPhraseToCache(
keysStorage.putPassphraseToCache(
fingerprint = it,
passphrase = passphrase,
validUntil = KeysStorageImpl.calculateLifeTimeForPassphrase(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ class PrivateKeysViewModel(application: Application) : BaseNodeApiViewModel(appl

if (isAdded) {
if (keyDetails.passphraseType == KeyEntity.PassphraseType.RAM) {
keysStorage.putPassPhraseToCache(
keysStorage.putPassphraseToCache(
fingerprint = fingerprint,
passphrase = Passphrase(keyDetails.tempPassphrase),
validUntil = KeysStorageImpl.calculateLifeTimeForPassphrase(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ interface KeysStorage {

fun getSecretKeyRingProtector(): SecretKeyRingProtector

fun updatePassPhrasesCache()
fun updatePassphrasesCache()

fun putPassPhraseToCache(
fun putPassphraseToCache(
fingerprint: String,
passphrase: Passphrase,
validUntil: Instant,
passphraseType: KeyEntity.PassphraseType
)

fun hasEmptyPassphrase(): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ class KeysStorageImpl private constructor(context: Context) : KeysStorage {
}
}

override fun updatePassPhrasesCache() {
override fun updatePassphrasesCache() {
for (key in getRawKeys()) {
if (key.passphraseType == KeyEntity.PassphraseType.RAM) {
val entry = passPhraseMap[key.fingerprint] ?: continue
Expand All @@ -185,7 +185,7 @@ class KeysStorageImpl private constructor(context: Context) : KeysStorage {
}
}

override fun putPassPhraseToCache(
override fun putPassphraseToCache(
fingerprint: String,
passphrase: Passphrase,
validUntil: Instant,
Expand All @@ -200,6 +200,10 @@ class KeysStorageImpl private constructor(context: Context) : KeysStorage {
passphrasesUpdatesLiveData.postValue(System.currentTimeMillis())
}

override fun hasEmptyPassphrase(): Boolean {
return passPhraseMap.values.any { it.passphrase.isEmpty }
}

private fun preparePassphrasesMap(keyEntityList: List<KeyEntity>) {
val existedIdList = passPhraseMap.keys
val refreshedIdList = keyEntityList.map { it.fingerprint }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class PassPhrasesInRAMService : BaseLifecycleService() {

lifecycleScope.launch {
repeatableActionFlow.collect {
keysStorage.updatePassPhrasesCache()
keysStorage.updatePassphrasesCache()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import com.flowcrypt.email.ui.activity.base.BaseSyncActivity
import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment
import com.flowcrypt.email.ui.activity.fragment.base.ProgressBehaviour
import com.flowcrypt.email.ui.activity.fragment.dialog.ChoosePublicKeyDialogFragment
import com.flowcrypt.email.ui.activity.fragment.dialog.FixEmptyPassphraseDialogFragment
import com.flowcrypt.email.ui.activity.fragment.dialog.TwoWayDialogFragment
import com.flowcrypt.email.ui.adapter.AttachmentsRecyclerViewAdapter
import com.flowcrypt.email.ui.adapter.MsgDetailsRecyclerViewAdapter
Expand Down Expand Up @@ -475,6 +476,15 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
fragment.show(parentFragmentManager, ChoosePublicKeyDialogFragment::class.java.simpleName)
}

private fun showNeedPassphraseDialog() {
val fragment = FixEmptyPassphraseDialogFragment.newInstance()
fragment.setTargetFragment(
this@MessageDetailsFragment,
REQUEST_CODE_SHOW_FIX_EMPTY_PASSPHRASE_DIALOG
)
fragment.show(parentFragmentManager, FixEmptyPassphraseDialogFragment::class.java.simpleName)
}

/**
* Send a template message with a sender public key.
*
Expand Down Expand Up @@ -1227,6 +1237,7 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
observerIncomingMessageInfoLiveData()
observeAttsLiveData()
observerMsgStatesLiveData()
observerPassphraseNeededLiveData()
}

private fun observeFreshMsgLiveData() {
Expand Down Expand Up @@ -1357,6 +1368,14 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
})
}

private fun observerPassphraseNeededLiveData() {
msgDetailsViewModel.passphraseNeededLiveData.observe(viewLifecycleOwner, { isPassphraseNeeded ->
if (isPassphraseNeeded) {
showNeedPassphraseDialog()
}
})
}

private fun messageNotAvailableInFolder(showToast: Boolean = true) {
msgDetailsViewModel.deleteMsg()
if (showToast) {
Expand All @@ -1369,6 +1388,7 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi
private const val REQUEST_CODE_START_IMPORT_KEY_ACTIVITY = 101
private const val REQUEST_CODE_SHOW_DIALOG_WITH_SEND_KEY_OPTION = 102
private const val REQUEST_CODE_DELETE_MESSAGE_DIALOG = 103
private const val REQUEST_CODE_SHOW_FIX_EMPTY_PASSPHRASE_DIALOG = 104
private const val CONTENT_MAX_ALLOWED_LENGTH = 50000
private const val MAX_ALLOWED_RECEPIENTS_IN_HEADER_VALUE = 10
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact [email protected]
* Contributors: DenBond7
*/

package com.flowcrypt.email.ui.activity.fragment.dialog

import android.annotation.SuppressLint
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.flowcrypt.email.R
import com.flowcrypt.email.api.retrofit.response.base.Result
import com.flowcrypt.email.extensions.decrementSafely
import com.flowcrypt.email.extensions.gone
import com.flowcrypt.email.extensions.incrementSafely
import com.flowcrypt.email.extensions.visible
import com.flowcrypt.email.jetpack.viewmodel.KeysWithEmptyPassphraseViewModel
import com.flowcrypt.email.ui.adapter.PrvKeysRecyclerViewAdapter
import com.flowcrypt.email.ui.adapter.recyclerview.itemdecoration.MarginItemDecoration
import com.google.android.gms.common.util.CollectionUtils

/**
* @author Denis Bondarenko
* Date: 5/28/21
* Time: 2:50 PM
* E-mail: [email protected]
*/
class FixEmptyPassphraseDialogFragment : BaseDialogFragment() {
private var rVKeys: RecyclerView? = null
private var tVStatusMessage: TextView? = null
private var pBLoading: View? = null
private var gCheckPassphrase: View? = null
private val prvKeysRecyclerViewAdapter = PrvKeysRecyclerViewAdapter()

private val keysWithEmptyPassphraseViewModel: KeysWithEmptyPassphraseViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
isCancelable = false
setupKeysWithEmptyPassphraseLiveData()
}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val view = LayoutInflater.from(context).inflate(
R.layout.fragment_fix_empty_passphrase,
if ((view != null) and (view is ViewGroup)) view as ViewGroup? else null, false
)

gCheckPassphrase = view.findViewById(R.id.gCheckPassphrase)
tVStatusMessage = view.findViewById(R.id.tVStatusMessage)
pBLoading = view.findViewById(R.id.pBLoading)
rVKeys = view.findViewById(R.id.rVKeys)

rVKeys?.apply {
layoutManager = LinearLayoutManager(context)
addItemDecoration(
MarginItemDecoration(
marginBottom = resources.getDimensionPixelSize(R.dimen.default_margin_content_small)
)
)
adapter = prvKeysRecyclerViewAdapter
}

val builder = AlertDialog.Builder(requireContext()).apply {
setView(view)
setNegativeButton(R.string.cancel) { _, _ -> }
}

return builder.create()
}

@SuppressLint("FragmentLiveDataObserve")
private fun setupKeysWithEmptyPassphraseLiveData() {
keysWithEmptyPassphraseViewModel.keysWithEmptyPassphrasesLiveData.observe(this, {
when (it.status) {
Result.Status.LOADING -> {
baseActivity?.countingIdlingResource?.incrementSafely()
pBLoading?.visible()
}

Result.Status.SUCCESS -> {
pBLoading?.gone()
val keyDetailsList = it.data ?: emptyList()
if (CollectionUtils.isEmpty(keyDetailsList)) {
tVStatusMessage?.text = getString(R.string.no_pub_keys)
} else {
gCheckPassphrase?.visible()
tVStatusMessage?.text = requireContext().resources.getQuantityString(
R.plurals.please_provide_passphrase_for_following_keys, keyDetailsList.size
)

prvKeysRecyclerViewAdapter.submitList(keyDetailsList)
}
baseActivity?.countingIdlingResource?.decrementSafely()
}

Result.Status.EXCEPTION -> {
pBLoading?.gone()
tVStatusMessage?.visible()
tVStatusMessage?.text = it.exception?.message
baseActivity?.countingIdlingResource?.decrementSafely()
}
}
})
}

companion object {
fun newInstance(): FixEmptyPassphraseDialogFragment {
return FixEmptyPassphraseDialogFragment()
}
}
}
Loading

0 comments on commit d9269e0

Please sign in to comment.