Merge pull request #5631 from k9mail/convert_RecipientPresenter_to_kotlin

Convert RecipientPresenter to Kotlin
This commit is contained in:
cketti 2021-09-02 17:22:30 +02:00 committed by GitHub
commit 4f290ba8f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 1492 additions and 1730 deletions

View file

@ -58,6 +58,7 @@ dependencies {
testImplementation "org.robolectric:robolectric:${versions.robolectric}"
testImplementation "androidx.test:core:${versions.androidxTestCore}"
testImplementation "junit:junit:${versions.junit}"
testImplementation "org.jetbrains.kotlin:kotlin-test:${versions.kotlin}"
testImplementation "com.google.truth:truth:${versions.truth}"
testImplementation "org.mockito:mockito-core:${versions.mockito}"
testImplementation "org.mockito.kotlin:mockito-kotlin:${versions.mockitoKotlin}"

View file

@ -1,543 +0,0 @@
package com.fsck.k9.activity.compose;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import android.app.PendingIntent;
import androidx.loader.app.LoaderManager;
import androidx.interpolator.view.animation.FastOutLinearInInterpolator;
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator;
import android.text.TextWatcher;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnFocusChangeListener;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewAnimator;
import com.fsck.k9.FontSizes;
import com.fsck.k9.ui.R;
import com.fsck.k9.activity.MessageCompose;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.view.RecipientSelectView;
import com.fsck.k9.view.RecipientSelectView.Recipient;
import com.fsck.k9.view.RecipientSelectView.TokenListener;
import com.fsck.k9.view.ToolableViewAnimator;
import static com.fsck.k9.FontSizes.FONT_10SP;
import static com.fsck.k9.FontSizes.FONT_12SP;
import static com.fsck.k9.FontSizes.FONT_16SP;
import static com.fsck.k9.FontSizes.FONT_20SP;
import static com.fsck.k9.FontSizes.FONT_DEFAULT;
import static com.fsck.k9.FontSizes.LARGE;
import static com.fsck.k9.FontSizes.MEDIUM;
import static com.fsck.k9.FontSizes.SMALL;
public class RecipientMvpView implements OnFocusChangeListener, OnClickListener {
private static final int VIEW_INDEX_HIDDEN = -1;
private static final int VIEW_INDEX_BCC_EXPANDER_VISIBLE = 0;
private static final int VIEW_INDEX_BCC_EXPANDER_HIDDEN = 1;
private static final FastOutLinearInInterpolator CRYPTO_ICON_OUT_ANIMATOR = new FastOutLinearInInterpolator();
private static final int CRYPTO_ICON_OUT_DURATION = 195;
private static final LinearOutSlowInInterpolator CRYPTO_ICON_IN_ANIMATOR = new LinearOutSlowInInterpolator();
private static final int CRYPTO_ICON_IN_DURATION = 225;
private final MessageCompose activity;
private final View ccWrapper;
private final View ccDivider;
private final View bccWrapper;
private final View bccDivider;
private final RecipientSelectView toView;
private final RecipientSelectView ccView;
private final RecipientSelectView bccView;
private final ToolableViewAnimator cryptoStatusView;
private final ViewAnimator recipientExpanderContainer;
private final ToolableViewAnimator cryptoSpecialModeIndicator;
private final Set<TextWatcher> textWatchers = new HashSet<>();
private RecipientPresenter presenter;
public RecipientMvpView(MessageCompose activity) {
this.activity = activity;
toView = activity.findViewById(R.id.to);
ccView = activity.findViewById(R.id.cc);
bccView = activity.findViewById(R.id.bcc);
ccWrapper = activity.findViewById(R.id.cc_wrapper);
ccDivider = activity.findViewById(R.id.cc_divider);
bccWrapper = activity.findViewById(R.id.bcc_wrapper);
bccDivider = activity.findViewById(R.id.bcc_divider);
recipientExpanderContainer = activity.findViewById(R.id.recipient_expander_container);
cryptoStatusView = activity.findViewById(R.id.crypto_status);
cryptoStatusView.setOnClickListener(this);
cryptoSpecialModeIndicator = activity.findViewById(R.id.crypto_special_mode);
cryptoSpecialModeIndicator.setOnClickListener(this);
toView.setOnFocusChangeListener(this);
ccView.setOnFocusChangeListener(this);
bccView.setOnFocusChangeListener(this);
View recipientExpander = activity.findViewById(R.id.recipient_expander);
recipientExpander.setOnClickListener(this);
View toLabel = activity.findViewById(R.id.to_label);
View ccLabel = activity.findViewById(R.id.cc_label);
View bccLabel = activity.findViewById(R.id.bcc_label);
toLabel.setOnClickListener(this);
ccLabel.setOnClickListener(this);
bccLabel.setOnClickListener(this);
}
public void setPresenter(final RecipientPresenter presenter) {
this.presenter = presenter;
if (presenter == null) {
toView.setTokenListener(null);
ccView.setTokenListener(null);
bccView.setTokenListener(null);
return;
}
toView.setTokenListener(new TokenListener<Recipient>() {
@Override
public void onTokenAdded(Recipient recipient) {
presenter.onToTokenAdded();
}
@Override
public void onTokenRemoved(Recipient recipient) {
presenter.onToTokenRemoved();
}
@Override
public void onTokenChanged(Recipient recipient) {
presenter.onToTokenChanged();
}
@Override
public void onTokenIgnored(Recipient token) {
// Do nothing
}
});
ccView.setTokenListener(new TokenListener<Recipient>() {
@Override
public void onTokenAdded(Recipient recipient) {
presenter.onCcTokenAdded();
}
@Override
public void onTokenRemoved(Recipient recipient) {
presenter.onCcTokenRemoved();
}
@Override
public void onTokenChanged(Recipient recipient) {
presenter.onCcTokenChanged();
}
@Override
public void onTokenIgnored(Recipient token) {
// Do nothing
}
});
bccView.setTokenListener(new TokenListener<Recipient>() {
@Override
public void onTokenAdded(Recipient recipient) {
presenter.onBccTokenAdded();
}
@Override
public void onTokenRemoved(Recipient recipient) {
presenter.onBccTokenRemoved();
}
@Override
public void onTokenChanged(Recipient recipient) {
presenter.onBccTokenChanged();
}
@Override
public void onTokenIgnored(Recipient token) {
// Do nothing
}
});
}
public void addTextChangedListener(TextWatcher textWatcher) {
textWatchers.add(textWatcher);
toView.addTextChangedListener(textWatcher);
ccView.addTextChangedListener(textWatcher);
bccView.addTextChangedListener(textWatcher);
}
private void removeAllTextChangedListeners(TextView view) {
for (TextWatcher textWatcher : textWatchers) {
view.removeTextChangedListener(textWatcher);
}
}
private void addAllTextChangedListeners(TextView view) {
for (TextWatcher textWatcher : textWatchers) {
view.addTextChangedListener(textWatcher);
}
}
public void setRecipientTokensShowCryptoEnabled(boolean isEnabled) {
toView.setShowCryptoEnabled(isEnabled);
ccView.setShowCryptoEnabled(isEnabled);
bccView.setShowCryptoEnabled(isEnabled);
}
public void setCryptoProvider(String openPgpProvider) {
// TODO move "show advanced" into settings, or somewhere?
toView.setCryptoProvider(openPgpProvider, false);
ccView.setCryptoProvider(openPgpProvider, false);
bccView.setCryptoProvider(openPgpProvider, false);
}
public void requestFocusOnToField() {
toView.requestFocus();
}
public void requestFocusOnCcField() {
ccView.requestFocus();
}
public void requestFocusOnBccField() {
bccView.requestFocus();
}
public void setFontSizes(FontSizes fontSizes, int fontSize) {
int tokenTextSize = getTokenTextSize(fontSize);
toView.setTokenTextSize(tokenTextSize);
ccView.setTokenTextSize(tokenTextSize);
bccView.setTokenTextSize(tokenTextSize);
fontSizes.setViewTextSize(toView, fontSize);
fontSizes.setViewTextSize(ccView, fontSize);
fontSizes.setViewTextSize(bccView, fontSize);
}
private int getTokenTextSize(int fontSize) {
switch (fontSize) {
case FONT_10SP: return FONT_10SP;
case FONT_12SP: return FONT_12SP;
case SMALL: return SMALL;
case FONT_16SP: return 15;
case MEDIUM: return FONT_16SP;
case FONT_20SP: return MEDIUM;
case LARGE: return FONT_20SP;
default: return FONT_DEFAULT;
}
}
public void addRecipients(RecipientType recipientType, Recipient... recipients) {
switch (recipientType) {
case TO: {
toView.addRecipients(recipients);
break;
}
case CC: {
ccView.addRecipients(recipients);
break;
}
case BCC: {
bccView.addRecipients(recipients);
break;
}
}
}
public void silentlyAddBccAddresses(Recipient... recipients) {
removeAllTextChangedListeners(bccView);
bccView.addRecipients(recipients);
addAllTextChangedListeners(bccView);
}
public void silentlyRemoveBccAddresses(Address[] addressesToRemove) {
if (addressesToRemove.length == 0) {
return;
}
List<Recipient> bccRecipients = new ArrayList<>(getBccRecipients());
for (Recipient recipient : bccRecipients) {
removeAllTextChangedListeners(bccView);
for (Address address : addressesToRemove) {
if (recipient.address.equals(address)) {
bccView.removeObjectSync(recipient);
}
}
addAllTextChangedListeners(bccView);
}
}
public void setCcVisibility(boolean visible) {
ccWrapper.setVisibility(visible ? View.VISIBLE : View.GONE);
ccDivider.setVisibility(visible ? View.VISIBLE : View.GONE);
}
public void setBccVisibility(boolean visible) {
bccWrapper.setVisibility(visible ? View.VISIBLE : View.GONE);
bccDivider.setVisibility(visible ? View.VISIBLE : View.GONE);
}
public void setRecipientExpanderVisibility(boolean visible) {
int childToDisplay = visible ? VIEW_INDEX_BCC_EXPANDER_VISIBLE : VIEW_INDEX_BCC_EXPANDER_HIDDEN;
if (recipientExpanderContainer.getDisplayedChild() != childToDisplay) {
recipientExpanderContainer.setDisplayedChild(childToDisplay);
}
}
public boolean isCcVisible() {
return ccWrapper.getVisibility() == View.VISIBLE;
}
public boolean isBccVisible() {
return bccWrapper.getVisibility() == View.VISIBLE;
}
public void showNoRecipientsError() {
toView.setError(toView.getContext().getString(R.string.message_compose_error_no_recipients));
}
public List<Address> getToAddresses() {
return Arrays.asList(toView.getAddresses());
}
public List<Address> getCcAddresses() {
return Arrays.asList(ccView.getAddresses());
}
public List<Address> getBccAddresses() {
return Arrays.asList(bccView.getAddresses());
}
public List<Recipient> getToRecipients() {
return toView.getObjects();
}
public List<Recipient> getCcRecipients() {
return ccView.getObjects();
}
public List<Recipient> getBccRecipients() {
return bccView.getObjects();
}
public boolean recipientToHasUncompletedText() {
return toView.hasUncompletedText();
}
public boolean recipientCcHasUncompletedText() {
return ccView.hasUncompletedText();
}
public boolean recipientBccHasUncompletedText() {
return bccView.hasUncompletedText();
}
public boolean recipientToTryPerformCompletion() {
return toView.tryPerformCompletion();
}
public boolean recipientCcTryPerformCompletion() {
return ccView.tryPerformCompletion();
}
public boolean recipientBccTryPerformCompletion() {
return bccView.tryPerformCompletion();
}
public void showToUncompletedError() {
toView.setError(toView.getContext().getString(R.string.compose_error_incomplete_recipient));
}
public void showCcUncompletedError() {
ccView.setError(ccView.getContext().getString(R.string.compose_error_incomplete_recipient));
}
public void showBccUncompletedError() {
bccView.setError(bccView.getContext().getString(R.string.compose_error_incomplete_recipient));
}
public void showCryptoSpecialMode(CryptoSpecialModeDisplayType cryptoSpecialModeDisplayType) {
boolean shouldBeHidden = cryptoSpecialModeDisplayType.childIdToDisplay == VIEW_INDEX_HIDDEN;
if (shouldBeHidden) {
cryptoSpecialModeIndicator.setVisibility(View.GONE);
return;
}
cryptoSpecialModeIndicator.setVisibility(View.VISIBLE);
cryptoSpecialModeIndicator.setDisplayedChildId(cryptoSpecialModeDisplayType.childIdToDisplay);
activity.invalidateOptionsMenu();
}
public void showCryptoStatus(CryptoStatusDisplayType cryptoStatusDisplayType) {
boolean shouldBeHidden = cryptoStatusDisplayType.childIdToDisplay == VIEW_INDEX_HIDDEN;
if (shouldBeHidden) {
cryptoStatusView.animate()
.translationXBy(100.0f)
.alpha(0.0f)
.setDuration(CRYPTO_ICON_OUT_DURATION)
.setInterpolator(CRYPTO_ICON_OUT_ANIMATOR)
.start();
return;
}
cryptoStatusView.setVisibility(View.VISIBLE);
cryptoStatusView.setDisplayedChildId(cryptoStatusDisplayType.childIdToDisplay);
cryptoStatusView.animate()
.translationX(0.0f)
.alpha(1.0f)
.setDuration(CRYPTO_ICON_IN_DURATION)
.setInterpolator(CRYPTO_ICON_IN_ANIMATOR)
.start();
}
public void showContactPicker(int requestCode) {
activity.showContactPicker(requestCode);
}
public void showErrorIsSignOnly() {
Toast.makeText(activity, R.string.error_sign_only_no_encryption, Toast.LENGTH_LONG).show();
}
public void showErrorContactNoAddress() {
Toast.makeText(activity, R.string.error_contact_address_not_found, Toast.LENGTH_LONG).show();
}
public void showErrorOpenPgpRetrieveStatus() {
Toast.makeText(activity, R.string.error_recipient_crypto_retrieve, Toast.LENGTH_LONG).show();
}
public void showErrorOpenPgpIncompatible() {
Toast.makeText(activity, R.string.error_crypto_provider_incompatible, Toast.LENGTH_LONG).show();
}
public void showErrorOpenPgpConnection() {
Toast.makeText(activity, R.string.error_crypto_provider_connect, Toast.LENGTH_LONG).show();
}
public void showErrorOpenPgpUserInteractionRequired() {
Toast.makeText(activity, R.string.error_crypto_provider_ui_required, Toast.LENGTH_LONG).show();
}
public void showErrorNoKeyConfigured() {
Toast.makeText(activity, R.string.compose_error_no_key_configured, Toast.LENGTH_LONG).show();
}
public void showErrorInlineAttach() {
Toast.makeText(activity, R.string.error_crypto_inline_attach, Toast.LENGTH_LONG).show();
}
@Override
public void onFocusChange(View view, boolean hasFocus) {
if (!hasFocus) {
return;
}
int id = view.getId();
if (id == R.id.to) {
presenter.onToFocused();
} else if (id == R.id.cc) {
presenter.onCcFocused();
} else if (id == R.id.bcc) {
presenter.onBccFocused();
}
}
@Override
public void onClick(View view) {
int id = view.getId();
if (id == R.id.to_label) {
presenter.onClickToLabel();
} else if (id == R.id.cc_label) {
presenter.onClickCcLabel();
} else if (id == R.id.bcc_label) {
presenter.onClickBccLabel();
} else if (id == R.id.recipient_expander) {
presenter.onClickRecipientExpander();
} else if (id == R.id.crypto_status) {
presenter.onClickCryptoStatus();
} else if (id == R.id.crypto_special_mode) {
presenter.onClickCryptoSpecialModeIndicator();
}
}
public void showOpenPgpInlineDialog(boolean firstTime) {
PgpInlineDialog dialog = PgpInlineDialog.newInstance(firstTime, R.id.crypto_special_mode);
dialog.show(activity.getSupportFragmentManager(), "openpgp_inline");
}
public void showOpenPgpSignOnlyDialog(boolean firstTime) {
PgpSignOnlyDialog dialog = PgpSignOnlyDialog.newInstance(firstTime, R.id.crypto_special_mode);
dialog.show(activity.getSupportFragmentManager(), "openpgp_signonly");
}
public void showOpenPgpEnabledErrorDialog(final boolean isGotItDialog) {
PgpEnabledErrorDialog dialog = PgpEnabledErrorDialog.newInstance(isGotItDialog, R.id.crypto_status_anchor);
dialog.show(activity.getSupportFragmentManager(), "openpgp_error");
}
public void showOpenPgpEncryptExplanationDialog() {
PgpEncryptDescriptionDialog dialog = PgpEncryptDescriptionDialog.newInstance(R.id.crypto_status_anchor);
dialog.show(activity.getSupportFragmentManager(), "openpgp_description");
}
public void launchUserInteractionPendingIntent(PendingIntent pendingIntent, int requestCode) {
activity.launchUserInteractionPendingIntent(pendingIntent, requestCode);
}
public void setLoaderManager(LoaderManager loaderManager) {
toView.setLoaderManager(loaderManager);
ccView.setLoaderManager(loaderManager);
bccView.setLoaderManager(loaderManager);
}
public enum CryptoStatusDisplayType {
UNCONFIGURED(VIEW_INDEX_HIDDEN),
UNINITIALIZED(VIEW_INDEX_HIDDEN),
SIGN_ONLY(R.id.crypto_status_disabled),
UNAVAILABLE(VIEW_INDEX_HIDDEN),
ENABLED(R.id.crypto_status_enabled),
ENABLED_ERROR(R.id.crypto_status_error),
ENABLED_TRUSTED(R.id.crypto_status_trusted),
AVAILABLE(R.id.crypto_status_disabled),
ERROR(R.id.crypto_status_error);
final int childIdToDisplay;
CryptoStatusDisplayType(int childIdToDisplay) {
this.childIdToDisplay = childIdToDisplay;
}
}
public enum CryptoSpecialModeDisplayType {
NONE(VIEW_INDEX_HIDDEN),
PGP_INLINE(R.id.crypto_special_inline),
SIGN_ONLY(R.id.crypto_special_sign_only),
SIGN_ONLY_PGP_INLINE(R.id.crypto_special_sign_only_inline);
final int childIdToDisplay;
CryptoSpecialModeDisplayType(int childIdToDisplay) {
this.childIdToDisplay = childIdToDisplay;
}
}
}

View file

@ -0,0 +1,414 @@
package com.fsck.k9.activity.compose
import android.app.PendingIntent
import android.text.TextWatcher
import android.view.View
import android.widget.TextView
import android.widget.Toast
import android.widget.ViewAnimator
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
import androidx.loader.app.LoaderManager
import com.fsck.k9.FontSizes
import com.fsck.k9.activity.MessageCompose
import com.fsck.k9.mail.Address
import com.fsck.k9.mail.Message.RecipientType
import com.fsck.k9.ui.R
import com.fsck.k9.view.RecipientSelectView
import com.fsck.k9.view.RecipientSelectView.Recipient
import com.fsck.k9.view.ToolableViewAnimator
import java.lang.AssertionError
class RecipientMvpView(private val activity: MessageCompose) : View.OnFocusChangeListener, View.OnClickListener {
private val toView: RecipientSelectView = activity.findViewById(R.id.to)
private val ccView: RecipientSelectView = activity.findViewById(R.id.cc)
private val bccView: RecipientSelectView = activity.findViewById(R.id.bcc)
private val ccWrapper: View = activity.findViewById(R.id.cc_wrapper)
private val ccDivider: View = activity.findViewById(R.id.cc_divider)
private val bccWrapper: View = activity.findViewById(R.id.bcc_wrapper)
private val bccDivider: View = activity.findViewById(R.id.bcc_divider)
private val recipientExpanderContainer: ViewAnimator = activity.findViewById(R.id.recipient_expander_container)
private val cryptoStatusView: ToolableViewAnimator = activity.findViewById(R.id.crypto_status)
private val cryptoSpecialModeIndicator: ToolableViewAnimator = activity.findViewById(R.id.crypto_special_mode)
private val textWatchers: MutableSet<TextWatcher> = HashSet()
private lateinit var presenter: RecipientPresenter
init {
cryptoStatusView.setOnClickListener(this)
cryptoSpecialModeIndicator.setOnClickListener(this)
toView.onFocusChangeListener = this
ccView.onFocusChangeListener = this
bccView.onFocusChangeListener = this
activity.findViewById<View>(R.id.recipient_expander).setOnClickListener(this)
activity.findViewById<View>(R.id.to_label).setOnClickListener(this)
activity.findViewById<View>(R.id.cc_label).setOnClickListener(this)
activity.findViewById<View>(R.id.bcc_label).setOnClickListener(this)
}
val isCcVisible: Boolean
get() = ccWrapper.isVisible
val isBccVisible: Boolean
get() = bccWrapper.isVisible
val toAddresses: List<Address>
get() = toView.addresses.toList()
val ccAddresses: List<Address>
get() = ccView.addresses.toList()
val bccAddresses: List<Address>
get() = bccView.addresses.toList()
val toRecipients: List<Recipient>
get() = toView.objects
val ccRecipients: List<Recipient>
get() = ccView.objects
val bccRecipients: List<Recipient>
get() = bccView.objects
fun setPresenter(presenter: RecipientPresenter) {
this.presenter = presenter
toView.setTokenListener(object : RecipientSelectView.TokenListener<Recipient> {
override fun onTokenAdded(recipient: Recipient) = presenter.onToTokenAdded()
override fun onTokenRemoved(recipient: Recipient) = presenter.onToTokenRemoved()
override fun onTokenChanged(recipient: Recipient) = presenter.onToTokenChanged()
override fun onTokenIgnored(token: Recipient) = Unit
})
ccView.setTokenListener(object : RecipientSelectView.TokenListener<Recipient> {
override fun onTokenAdded(recipient: Recipient) = presenter.onCcTokenAdded()
override fun onTokenRemoved(recipient: Recipient) = presenter.onCcTokenRemoved()
override fun onTokenChanged(recipient: Recipient) = presenter.onCcTokenChanged()
override fun onTokenIgnored(token: Recipient) = Unit
})
bccView.setTokenListener(object : RecipientSelectView.TokenListener<Recipient> {
override fun onTokenAdded(recipient: Recipient) = presenter.onBccTokenAdded()
override fun onTokenRemoved(recipient: Recipient) = presenter.onBccTokenRemoved()
override fun onTokenChanged(recipient: Recipient) = presenter.onBccTokenChanged()
override fun onTokenIgnored(token: Recipient) = Unit
})
}
fun addTextChangedListener(textWatcher: TextWatcher) {
textWatchers.add(textWatcher)
toView.addTextChangedListener(textWatcher)
ccView.addTextChangedListener(textWatcher)
bccView.addTextChangedListener(textWatcher)
}
private fun removeAllTextChangedListeners(view: TextView) {
for (textWatcher in textWatchers) {
view.removeTextChangedListener(textWatcher)
}
}
private fun addAllTextChangedListeners(view: TextView) {
for (textWatcher in textWatchers) {
view.addTextChangedListener(textWatcher)
}
}
fun setRecipientTokensShowCryptoEnabled(isEnabled: Boolean) {
toView.setShowCryptoEnabled(isEnabled)
ccView.setShowCryptoEnabled(isEnabled)
bccView.setShowCryptoEnabled(isEnabled)
}
fun setCryptoProvider(openPgpProvider: String?) {
// TODO move "show advanced" into settings, or somewhere?
toView.setCryptoProvider(openPgpProvider, false)
ccView.setCryptoProvider(openPgpProvider, false)
bccView.setCryptoProvider(openPgpProvider, false)
}
fun requestFocusOnToField() {
toView.requestFocus()
}
fun requestFocusOnCcField() {
ccView.requestFocus()
}
fun requestFocusOnBccField() {
bccView.requestFocus()
}
fun setFontSizes(fontSizes: FontSizes, fontSize: Int) {
val tokenTextSize = getTokenTextSize(fontSize)
toView.setTokenTextSize(tokenTextSize)
ccView.setTokenTextSize(tokenTextSize)
bccView.setTokenTextSize(tokenTextSize)
fontSizes.setViewTextSize(toView, fontSize)
fontSizes.setViewTextSize(ccView, fontSize)
fontSizes.setViewTextSize(bccView, fontSize)
}
private fun getTokenTextSize(fontSize: Int): Int {
return when (fontSize) {
FontSizes.FONT_10SP -> FontSizes.FONT_10SP
FontSizes.FONT_12SP -> FontSizes.FONT_12SP
FontSizes.SMALL -> FontSizes.SMALL
FontSizes.FONT_16SP -> 15
FontSizes.MEDIUM -> FontSizes.FONT_16SP
FontSizes.FONT_20SP -> FontSizes.MEDIUM
FontSizes.LARGE -> FontSizes.FONT_20SP
else -> FontSizes.FONT_DEFAULT
}
}
fun addRecipients(recipientType: RecipientType, vararg recipients: Recipient) {
when (recipientType) {
RecipientType.TO -> toView.addRecipients(*recipients)
RecipientType.CC -> ccView.addRecipients(*recipients)
RecipientType.BCC -> bccView.addRecipients(*recipients)
else -> throw AssertionError("Unsupported type: $recipientType")
}
}
fun silentlyAddBccAddresses(vararg recipients: Recipient) {
removeAllTextChangedListeners(bccView)
bccView.addRecipients(*recipients)
addAllTextChangedListeners(bccView)
}
fun silentlyRemoveBccAddresses(addresses: Array<Address>) {
if (addresses.isEmpty()) return
val addressesToRemove = addresses.toSet()
for (recipient in bccRecipients.toList()) {
removeAllTextChangedListeners(bccView)
if (recipient.address in addressesToRemove) {
bccView.removeObjectSync(recipient)
}
addAllTextChangedListeners(bccView)
}
}
fun setCcVisibility(visible: Boolean) {
ccWrapper.isVisible = visible
ccDivider.isVisible = visible
}
fun setBccVisibility(visible: Boolean) {
bccWrapper.isVisible = visible
bccDivider.isVisible = visible
}
fun setRecipientExpanderVisibility(visible: Boolean) {
val childToDisplay = if (visible) VIEW_INDEX_BCC_EXPANDER_VISIBLE else VIEW_INDEX_BCC_EXPANDER_HIDDEN
if (recipientExpanderContainer.displayedChild != childToDisplay) {
recipientExpanderContainer.displayedChild = childToDisplay
}
}
fun showNoRecipientsError() {
toView.error = toView.context.getString(R.string.message_compose_error_no_recipients)
}
fun recipientToHasUncompletedText(): Boolean {
return toView.hasUncompletedText()
}
fun recipientCcHasUncompletedText(): Boolean {
return ccView.hasUncompletedText()
}
fun recipientBccHasUncompletedText(): Boolean {
return bccView.hasUncompletedText()
}
fun recipientToTryPerformCompletion(): Boolean {
return toView.tryPerformCompletion()
}
fun recipientCcTryPerformCompletion(): Boolean {
return ccView.tryPerformCompletion()
}
fun recipientBccTryPerformCompletion(): Boolean {
return bccView.tryPerformCompletion()
}
fun showToUncompletedError() {
toView.error = toView.context.getString(R.string.compose_error_incomplete_recipient)
}
fun showCcUncompletedError() {
ccView.error = ccView.context.getString(R.string.compose_error_incomplete_recipient)
}
fun showBccUncompletedError() {
bccView.error = bccView.context.getString(R.string.compose_error_incomplete_recipient)
}
fun showCryptoSpecialMode(cryptoSpecialModeDisplayType: CryptoSpecialModeDisplayType) {
val shouldBeHidden = cryptoSpecialModeDisplayType.childIdToDisplay == VIEW_INDEX_HIDDEN
if (shouldBeHidden) {
cryptoSpecialModeIndicator.isGone = true
return
}
cryptoSpecialModeIndicator.isVisible = true
cryptoSpecialModeIndicator.displayedChildId = cryptoSpecialModeDisplayType.childIdToDisplay
activity.invalidateOptionsMenu()
}
fun showCryptoStatus(cryptoStatusDisplayType: CryptoStatusDisplayType) {
val shouldBeHidden = cryptoStatusDisplayType.childIdToDisplay == VIEW_INDEX_HIDDEN
if (shouldBeHidden) {
cryptoStatusView.animate()
.translationXBy(100.0f)
.alpha(0.0f)
.setDuration(CRYPTO_ICON_OUT_DURATION.toLong())
.setInterpolator(CRYPTO_ICON_OUT_ANIMATOR)
.start()
return
}
cryptoStatusView.isVisible = true
cryptoStatusView.displayedChildId = cryptoStatusDisplayType.childIdToDisplay
cryptoStatusView.animate()
.translationX(0.0f)
.alpha(1.0f)
.setDuration(CRYPTO_ICON_IN_DURATION.toLong())
.setInterpolator(CRYPTO_ICON_IN_ANIMATOR)
.start()
}
fun showContactPicker(requestCode: Int) {
activity.showContactPicker(requestCode)
}
fun showErrorIsSignOnly() {
Toast.makeText(activity, R.string.error_sign_only_no_encryption, Toast.LENGTH_LONG).show()
}
fun showErrorContactNoAddress() {
Toast.makeText(activity, R.string.error_contact_address_not_found, Toast.LENGTH_LONG).show()
}
fun showErrorOpenPgpIncompatible() {
Toast.makeText(activity, R.string.error_crypto_provider_incompatible, Toast.LENGTH_LONG).show()
}
fun showErrorOpenPgpConnection() {
Toast.makeText(activity, R.string.error_crypto_provider_connect, Toast.LENGTH_LONG).show()
}
fun showErrorOpenPgpUserInteractionRequired() {
Toast.makeText(activity, R.string.error_crypto_provider_ui_required, Toast.LENGTH_LONG).show()
}
fun showErrorNoKeyConfigured() {
Toast.makeText(activity, R.string.compose_error_no_key_configured, Toast.LENGTH_LONG).show()
}
fun showErrorInlineAttach() {
Toast.makeText(activity, R.string.error_crypto_inline_attach, Toast.LENGTH_LONG).show()
}
override fun onFocusChange(view: View, hasFocus: Boolean) {
if (!hasFocus) return
when (view.id) {
R.id.to -> presenter.onToFocused()
R.id.cc -> presenter.onCcFocused()
R.id.bcc -> presenter.onBccFocused()
}
}
override fun onClick(view: View) {
when (view.id) {
R.id.to_label -> presenter.onClickToLabel()
R.id.cc_label -> presenter.onClickCcLabel()
R.id.bcc_label -> presenter.onClickBccLabel()
R.id.recipient_expander -> presenter.onClickRecipientExpander()
R.id.crypto_status -> presenter.onClickCryptoStatus()
R.id.crypto_special_mode -> presenter.onClickCryptoSpecialModeIndicator()
}
}
fun showOpenPgpInlineDialog(firstTime: Boolean) {
val dialog = PgpInlineDialog.newInstance(firstTime, R.id.crypto_special_mode)
dialog.show(activity.supportFragmentManager, "openpgp_inline")
}
fun showOpenPgpSignOnlyDialog(firstTime: Boolean) {
val dialog = PgpSignOnlyDialog.newInstance(firstTime, R.id.crypto_special_mode)
dialog.show(activity.supportFragmentManager, "openpgp_signonly")
}
fun showOpenPgpEnabledErrorDialog(isGotItDialog: Boolean) {
val dialog = PgpEnabledErrorDialog.newInstance(isGotItDialog, R.id.crypto_status_anchor)
dialog.show(activity.supportFragmentManager, "openpgp_error")
}
fun showOpenPgpEncryptExplanationDialog() {
val dialog = PgpEncryptDescriptionDialog.newInstance(R.id.crypto_status_anchor)
dialog.show(activity.supportFragmentManager, "openpgp_description")
}
fun launchUserInteractionPendingIntent(pendingIntent: PendingIntent?, requestCode: Int) {
activity.launchUserInteractionPendingIntent(pendingIntent, requestCode)
}
fun setLoaderManager(loaderManager: LoaderManager?) {
toView.setLoaderManager(loaderManager)
ccView.setLoaderManager(loaderManager)
bccView.setLoaderManager(loaderManager)
}
enum class CryptoStatusDisplayType(val childIdToDisplay: Int) {
UNCONFIGURED(VIEW_INDEX_HIDDEN),
UNINITIALIZED(VIEW_INDEX_HIDDEN),
SIGN_ONLY(R.id.crypto_status_disabled),
UNAVAILABLE(VIEW_INDEX_HIDDEN),
ENABLED(R.id.crypto_status_enabled),
ENABLED_ERROR(R.id.crypto_status_error),
ENABLED_TRUSTED(R.id.crypto_status_trusted),
AVAILABLE(R.id.crypto_status_disabled),
ERROR(R.id.crypto_status_error);
}
enum class CryptoSpecialModeDisplayType(val childIdToDisplay: Int) {
NONE(VIEW_INDEX_HIDDEN),
PGP_INLINE(R.id.crypto_special_inline),
SIGN_ONLY(R.id.crypto_special_sign_only),
SIGN_ONLY_PGP_INLINE(R.id.crypto_special_sign_only_inline);
}
companion object {
private const val VIEW_INDEX_HIDDEN = -1
private const val VIEW_INDEX_BCC_EXPANDER_VISIBLE = 0
private const val VIEW_INDEX_BCC_EXPANDER_HIDDEN = 1
private val CRYPTO_ICON_OUT_ANIMATOR = FastOutLinearInInterpolator()
private const val CRYPTO_ICON_OUT_DURATION = 195
private val CRYPTO_ICON_IN_ANIMATOR = LinearOutSlowInInterpolator()
private const val CRYPTO_ICON_IN_DURATION = 225
}
}

View file

@ -1,886 +0,0 @@
package com.fsck.k9.activity.compose;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import android.Manifest;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.loader.app.LoaderManager;
import android.view.Menu;
import com.fsck.k9.Account;
import com.fsck.k9.Identity;
import com.fsck.k9.K9;
import com.fsck.k9.activity.compose.ComposeCryptoStatus.AttachErrorState;
import com.fsck.k9.activity.compose.ComposeCryptoStatus.SendErrorState;
import com.fsck.k9.activity.compose.RecipientMvpView.CryptoStatusDisplayType;
import com.fsck.k9.autocrypt.AutocryptDraftStateHeader;
import com.fsck.k9.autocrypt.AutocryptDraftStateHeaderParser;
import com.fsck.k9.helper.Contacts;
import com.fsck.k9.helper.MailTo;
import com.fsck.k9.helper.ReplyToParser;
import com.fsck.k9.helper.ReplyToParser.ReplyToAddresses;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.message.AutocryptStatusInteractor;
import com.fsck.k9.message.AutocryptStatusInteractor.RecipientAutocryptStatus;
import com.fsck.k9.message.ComposePgpEnableByDefaultDecider;
import com.fsck.k9.message.ComposePgpInlineDecider;
import com.fsck.k9.message.MessageBuilder;
import com.fsck.k9.message.PgpMessageBuilder;
import com.fsck.k9.ui.R;
import com.fsck.k9.view.RecipientSelectView.Recipient;
import org.openintents.openpgp.OpenPgpApiManager;
import org.openintents.openpgp.OpenPgpApiManager.OpenPgpApiManagerCallback;
import org.openintents.openpgp.OpenPgpApiManager.OpenPgpProviderError;
import org.openintents.openpgp.OpenPgpApiManager.OpenPgpProviderState;
import org.openintents.openpgp.util.OpenPgpApi;
import timber.log.Timber;
public class RecipientPresenter {
private static final String STATE_KEY_CC_SHOWN = "state:ccShown";
private static final String STATE_KEY_BCC_SHOWN = "state:bccShown";
private static final String STATE_KEY_LAST_FOCUSED_TYPE = "state:lastFocusedType";
private static final String STATE_KEY_CURRENT_CRYPTO_MODE = "state:currentCryptoMode";
private static final String STATE_KEY_CRYPTO_ENABLE_PGP_INLINE = "state:cryptoEnablePgpInline";
private static final int CONTACT_PICKER_TO = 1;
private static final int CONTACT_PICKER_CC = 2;
private static final int CONTACT_PICKER_BCC = 3;
private static final int OPENPGP_USER_INTERACTION = 4;
private static final int REQUEST_CODE_AUTOCRYPT = 5;
private static final int PGP_DIALOG_DISPLAY_THRESHOLD = 2;
// transient state, which is either obtained during construction and initialization, or cached
private final Context context;
private final RecipientMvpView recipientMvpView;
private final ComposePgpEnableByDefaultDecider composePgpEnableByDefaultDecider;
private final ComposePgpInlineDecider composePgpInlineDecider;
private final AutocryptStatusInteractor autocryptStatusInteractor;
private final OpenPgpApiManager openPgpApiManager;
private final AutocryptDraftStateHeaderParser draftStateHeaderParser;
private ReplyToParser replyToParser;
private Account account;
private Address[] alwaysBccAddresses;
private Boolean hasContactPicker;
@Nullable
private ComposeCryptoStatus cachedCryptoStatus;
// persistent state, saved during onSaveInstanceState
private RecipientType lastFocusedType = RecipientType.TO;
private CryptoMode currentCryptoMode = CryptoMode.NO_CHOICE;
private boolean cryptoEnablePgpInline = false;
private boolean isReplyToEncryptedMessage = false;
public RecipientPresenter(Context context, LoaderManager loaderManager,
OpenPgpApiManager openPgpApiManager, RecipientMvpView recipientMvpView, Account account,
ComposePgpInlineDecider composePgpInlineDecider,
ComposePgpEnableByDefaultDecider composePgpEnableByDefaultDecider,
AutocryptStatusInteractor autocryptStatusInteractor,
ReplyToParser replyToParser, AutocryptDraftStateHeaderParser draftStateHeaderParser) {
this.recipientMvpView = recipientMvpView;
this.context = context;
this.autocryptStatusInteractor = autocryptStatusInteractor;
this.composePgpInlineDecider = composePgpInlineDecider;
this.composePgpEnableByDefaultDecider = composePgpEnableByDefaultDecider;
this.replyToParser = replyToParser;
this.openPgpApiManager = openPgpApiManager;
this.draftStateHeaderParser = draftStateHeaderParser;
recipientMvpView.setPresenter(this);
recipientMvpView.setLoaderManager(loaderManager);
onSwitchAccount(account);
}
public List<Address> getToAddresses() {
return recipientMvpView.getToAddresses();
}
public List<Address> getCcAddresses() {
return recipientMvpView.getCcAddresses();
}
public List<Address> getBccAddresses() {
return recipientMvpView.getBccAddresses();
}
private List<Recipient> getAllRecipients() {
ArrayList<Recipient> result = new ArrayList<>();
result.addAll(recipientMvpView.getToRecipients());
result.addAll(recipientMvpView.getCcRecipients());
result.addAll(recipientMvpView.getBccRecipients());
return result;
}
public boolean checkRecipientsOkForSending() {
recipientMvpView.recipientToTryPerformCompletion();
recipientMvpView.recipientCcTryPerformCompletion();
recipientMvpView.recipientBccTryPerformCompletion();
if (recipientMvpView.recipientToHasUncompletedText()) {
recipientMvpView.showToUncompletedError();
return true;
}
if (recipientMvpView.recipientCcHasUncompletedText()) {
recipientMvpView.showCcUncompletedError();
return true;
}
if (recipientMvpView.recipientBccHasUncompletedText()) {
recipientMvpView.showBccUncompletedError();
return true;
}
if (getToAddresses().isEmpty() && getCcAddresses().isEmpty() && getBccAddresses().isEmpty()) {
recipientMvpView.showNoRecipientsError();
return true;
}
return false;
}
public void initFromReplyToMessage(Message message, boolean isReplyAll) {
ReplyToAddresses replyToAddresses = isReplyAll ?
replyToParser.getRecipientsToReplyAllTo(message, account) :
replyToParser.getRecipientsToReplyTo(message, account);
addToAddresses(replyToAddresses.to);
addCcAddresses(replyToAddresses.cc);
boolean shouldSendAsPgpInline = composePgpInlineDecider.shouldReplyInline(message);
if (shouldSendAsPgpInline) {
cryptoEnablePgpInline = true;
}
isReplyToEncryptedMessage = composePgpEnableByDefaultDecider.shouldEncryptByDefault(message);
}
public void initFromTrustIdAction(String trustId) {
addToAddresses(Address.parse(trustId));
currentCryptoMode = CryptoMode.CHOICE_ENABLED;
}
public void initFromMailto(MailTo mailTo) {
addToAddresses(mailTo.getTo());
addCcAddresses(mailTo.getCc());
addBccAddresses(mailTo.getBcc());
}
public void initFromSendOrViewIntent(Intent intent) {
String[] extraEmail = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
String[] extraCc = intent.getStringArrayExtra(Intent.EXTRA_CC);
String[] extraBcc = intent.getStringArrayExtra(Intent.EXTRA_BCC);
if (extraEmail != null) {
addToAddresses(addressFromStringArray(extraEmail));
}
if (extraCc != null) {
addCcAddresses(addressFromStringArray(extraCc));
}
if (extraBcc != null) {
addBccAddresses(addressFromStringArray(extraBcc));
}
}
public void onRestoreInstanceState(Bundle savedInstanceState) {
recipientMvpView.setCcVisibility(savedInstanceState.getBoolean(STATE_KEY_CC_SHOWN));
recipientMvpView.setBccVisibility(savedInstanceState.getBoolean(STATE_KEY_BCC_SHOWN));
lastFocusedType = RecipientType.valueOf(savedInstanceState.getString(STATE_KEY_LAST_FOCUSED_TYPE));
currentCryptoMode = CryptoMode.valueOf(savedInstanceState.getString(STATE_KEY_CURRENT_CRYPTO_MODE));
cryptoEnablePgpInline = savedInstanceState.getBoolean(STATE_KEY_CRYPTO_ENABLE_PGP_INLINE);
updateRecipientExpanderVisibility();
}
public void onSaveInstanceState(Bundle outState) {
outState.putBoolean(STATE_KEY_CC_SHOWN, recipientMvpView.isCcVisible());
outState.putBoolean(STATE_KEY_BCC_SHOWN, recipientMvpView.isBccVisible());
outState.putString(STATE_KEY_LAST_FOCUSED_TYPE, lastFocusedType.toString());
outState.putString(STATE_KEY_CURRENT_CRYPTO_MODE, currentCryptoMode.toString());
outState.putBoolean(STATE_KEY_CRYPTO_ENABLE_PGP_INLINE, cryptoEnablePgpInline);
}
public void initFromDraftMessage(Message message) {
initRecipientsFromDraftMessage(message);
String[] draftStateHeader = message.getHeader(AutocryptDraftStateHeader.AUTOCRYPT_DRAFT_STATE_HEADER);
if (draftStateHeader.length == 1) {
initEncryptionStateFromDraftStateHeader(draftStateHeader[0]);
} else {
initPgpInlineFromDraftMessage(message);
}
}
private void initEncryptionStateFromDraftStateHeader(String headerValue) {
AutocryptDraftStateHeader autocryptDraftStateHeader =
draftStateHeaderParser.parseAutocryptDraftStateHeader(headerValue);
if (autocryptDraftStateHeader != null) {
initEncryptionStateFromDraftStateHeader(autocryptDraftStateHeader);
}
}
private void initRecipientsFromDraftMessage(Message message) {
addToAddresses(message.getRecipients(RecipientType.TO));
Address[] ccRecipients = message.getRecipients(RecipientType.CC);
addCcAddresses(ccRecipients);
Address[] bccRecipients = message.getRecipients(RecipientType.BCC);
addBccAddresses(bccRecipients);
}
private void initEncryptionStateFromDraftStateHeader(AutocryptDraftStateHeader draftState) {
cryptoEnablePgpInline = draftState.isPgpInline();
isReplyToEncryptedMessage = draftState.isReply();
if (!draftState.isByChoice()) {
// TODO if it's not by choice, we're going with our defaults. should we do something here if those differ?
return;
}
if (draftState.isSignOnly()) {
currentCryptoMode = CryptoMode.SIGN_ONLY;
} else {
currentCryptoMode = draftState.isEncrypt() ? CryptoMode.CHOICE_ENABLED : CryptoMode.CHOICE_DISABLED;
}
}
private void initPgpInlineFromDraftMessage(Message message) {
cryptoEnablePgpInline = message.isSet(Flag.X_DRAFT_OPENPGP_INLINE);
}
private void addToAddresses(Address... toAddresses) {
addRecipientsFromAddresses(RecipientType.TO, toAddresses);
}
private void addCcAddresses(Address... ccAddresses) {
if (ccAddresses.length > 0) {
addRecipientsFromAddresses(RecipientType.CC, ccAddresses);
recipientMvpView.setCcVisibility(true);
updateRecipientExpanderVisibility();
}
}
public void addBccAddresses(Address... bccRecipients) {
if (bccRecipients.length > 0) {
addRecipientsFromAddresses(RecipientType.BCC, bccRecipients);
recipientMvpView.setBccVisibility(true);
updateRecipientExpanderVisibility();
}
}
public void addAlwaysBcc() {
alwaysBccAddresses = Address.parse(account.getAlwaysBcc());
new RecipientLoader(context, account.getOpenPgpProvider(), alwaysBccAddresses) {
@Override
public void deliverResult(List<Recipient> result) {
Recipient[] recipientArray = result.toArray(new Recipient[result.size()]);
recipientMvpView.silentlyAddBccAddresses(recipientArray);
stopLoading();
abandon();
}
}.startLoading();
}
private void removeAlwaysBcc() {
if (alwaysBccAddresses != null) {
recipientMvpView.silentlyRemoveBccAddresses(alwaysBccAddresses);
}
}
public void onPrepareOptionsMenu(Menu menu) {
ComposeCryptoStatus currentCryptoStatus = getCurrentCachedCryptoStatus();
boolean isCryptoConfigured = currentCryptoStatus != null && currentCryptoStatus.isProviderStateOk();
if (isCryptoConfigured) {
boolean isEncrypting = currentCryptoStatus.isEncryptionEnabled();
menu.findItem(R.id.openpgp_encrypt_enable).setVisible(!isEncrypting);
menu.findItem(R.id.openpgp_encrypt_disable).setVisible(isEncrypting);
boolean showSignOnly = !account.isOpenPgpHideSignOnly();
boolean isSignOnly = currentCryptoStatus.isSignOnly();
menu.findItem(R.id.openpgp_sign_only).setVisible(showSignOnly && !isSignOnly);
menu.findItem(R.id.openpgp_sign_only_disable).setVisible(showSignOnly && isSignOnly);
boolean pgpInlineModeEnabled = currentCryptoStatus.isPgpInlineModeEnabled();
boolean showPgpInlineEnable = (isEncrypting || isSignOnly) && !pgpInlineModeEnabled;
menu.findItem(R.id.openpgp_inline_enable).setVisible(showPgpInlineEnable);
menu.findItem(R.id.openpgp_inline_disable).setVisible(pgpInlineModeEnabled);
} else {
menu.findItem(R.id.openpgp_inline_enable).setVisible(false);
menu.findItem(R.id.openpgp_inline_disable).setVisible(false);
menu.findItem(R.id.openpgp_encrypt_enable).setVisible(false);
menu.findItem(R.id.openpgp_encrypt_disable).setVisible(false);
menu.findItem(R.id.openpgp_sign_only).setVisible(false);
menu.findItem(R.id.openpgp_sign_only_disable).setVisible(false);
}
menu.findItem(R.id.add_from_contacts).setVisible(hasContactPicker() && hasContactPermission());
}
public void onSwitchAccount(Account account) {
this.account = account;
if (account.isAlwaysShowCcBcc()) {
recipientMvpView.setCcVisibility(true);
recipientMvpView.setBccVisibility(true);
updateRecipientExpanderVisibility();
}
removeAlwaysBcc();
addAlwaysBcc();
String openPgpProvider = account.getOpenPgpProvider();
recipientMvpView.setCryptoProvider(openPgpProvider);
openPgpApiManager.setOpenPgpProvider(openPgpProvider, openPgpCallback);
}
@SuppressWarnings("UnusedParameters")
public void onSwitchIdentity(Identity identity) {
// TODO decide what actually to do on identity switch?
asyncUpdateCryptoStatus();
/*
if (mIdentityChanged) {
mBccWrapper.setVisibility(View.VISIBLE);
}
mBccView.setText("");
mBccView.addAddress(new Address(mAccount.getAlwaysBcc(), ""));
*/
}
private static Address[] addressFromStringArray(String[] addresses) {
return addressFromStringArray(Arrays.asList(addresses));
}
private static Address[] addressFromStringArray(List<String> addresses) {
ArrayList<Address> result = new ArrayList<>(addresses.size());
for (String addressStr : addresses) {
Collections.addAll(result, Address.parseUnencoded(addressStr));
}
return result.toArray(new Address[result.size()]);
}
void onClickToLabel() {
recipientMvpView.requestFocusOnToField();
}
void onClickCcLabel() {
recipientMvpView.requestFocusOnCcField();
}
void onClickBccLabel() {
recipientMvpView.requestFocusOnBccField();
}
void onClickRecipientExpander() {
recipientMvpView.setCcVisibility(true);
recipientMvpView.setBccVisibility(true);
updateRecipientExpanderVisibility();
}
private void hideEmptyExtendedRecipientFields() {
if (recipientMvpView.getCcAddresses().isEmpty()) {
recipientMvpView.setCcVisibility(false);
if (lastFocusedType == RecipientType.CC) {
lastFocusedType = RecipientType.TO;
}
}
if (recipientMvpView.getBccAddresses().isEmpty()) {
recipientMvpView.setBccVisibility(false);
if (lastFocusedType == RecipientType.BCC) {
lastFocusedType = RecipientType.TO;
}
}
updateRecipientExpanderVisibility();
}
private void updateRecipientExpanderVisibility() {
boolean notBothAreVisible = !(recipientMvpView.isCcVisible() && recipientMvpView.isBccVisible());
recipientMvpView.setRecipientExpanderVisibility(notBothAreVisible);
}
public void asyncUpdateCryptoStatus() {
cachedCryptoStatus = null;
OpenPgpProviderState openPgpProviderState = openPgpApiManager.getOpenPgpProviderState();
Long accountCryptoKey = account.getOpenPgpKey();
if (accountCryptoKey == Account.NO_OPENPGP_KEY) {
accountCryptoKey = null;
}
final ComposeCryptoStatus composeCryptoStatus = new ComposeCryptoStatus(
openPgpProviderState,
accountCryptoKey,
getAllRecipients(),
cryptoEnablePgpInline,
account.getAutocryptPreferEncryptMutual(),
isReplyToEncryptedMessage,
account.isOpenPgpEncryptAllDrafts(),
account.isOpenPgpEncryptSubject(),
currentCryptoMode);
if (openPgpProviderState != OpenPgpProviderState.OK) {
cachedCryptoStatus = composeCryptoStatus;
redrawCachedCryptoStatusIcon();
return;
}
final String[] recipientAddresses = composeCryptoStatus.getRecipientAddressesAsArray();
new AsyncTask<Void,Void,RecipientAutocryptStatus>() {
@Override
protected RecipientAutocryptStatus doInBackground(Void... voids) {
OpenPgpApi openPgpApi = openPgpApiManager.getOpenPgpApi();
if (openPgpApi == null) {
return null;
}
return autocryptStatusInteractor.retrieveCryptoProviderRecipientStatus(openPgpApi, recipientAddresses);
}
@Override
protected void onPostExecute(RecipientAutocryptStatus recipientAutocryptStatus) {
if (recipientAutocryptStatus != null) {
cachedCryptoStatus = composeCryptoStatus.withRecipientAutocryptStatus(recipientAutocryptStatus);
} else {
cachedCryptoStatus = composeCryptoStatus;
}
redrawCachedCryptoStatusIcon();
}
}.execute();
}
private void redrawCachedCryptoStatusIcon() {
if (cachedCryptoStatus == null) {
throw new IllegalStateException("must have cached crypto status to redraw it!");
}
recipientMvpView.setRecipientTokensShowCryptoEnabled(cachedCryptoStatus.isEncryptionEnabled());
CryptoStatusDisplayType cryptoStatusDisplayType = cachedCryptoStatus.getDisplayType();
recipientMvpView.showCryptoStatus(cryptoStatusDisplayType);
recipientMvpView.showCryptoSpecialMode(cachedCryptoStatus.getSpecialModeDisplayType());
}
@Nullable
public ComposeCryptoStatus getCurrentCachedCryptoStatus() {
return cachedCryptoStatus;
}
public boolean isForceTextMessageFormat() {
return cryptoEnablePgpInline;
}
void onToTokenAdded() {
asyncUpdateCryptoStatus();
}
void onToTokenRemoved() {
asyncUpdateCryptoStatus();
}
void onToTokenChanged() {
asyncUpdateCryptoStatus();
}
void onCcTokenAdded() {
asyncUpdateCryptoStatus();
}
void onCcTokenRemoved() {
asyncUpdateCryptoStatus();
}
void onCcTokenChanged() {
asyncUpdateCryptoStatus();
}
void onBccTokenAdded() {
asyncUpdateCryptoStatus();
}
void onBccTokenRemoved() {
asyncUpdateCryptoStatus();
}
void onBccTokenChanged() {
asyncUpdateCryptoStatus();
}
public void onCryptoModeChanged(CryptoMode cryptoMode) {
currentCryptoMode = cryptoMode;
asyncUpdateCryptoStatus();
}
public void onCryptoPgpInlineChanged(boolean enablePgpInline) {
cryptoEnablePgpInline = enablePgpInline;
asyncUpdateCryptoStatus();
}
private void addRecipientsFromAddresses(final RecipientType recipientType, final Address... addresses) {
new RecipientLoader(context, account.getOpenPgpProvider(), addresses) {
@Override
public void deliverResult(List<Recipient> result) {
Recipient[] recipientArray = result.toArray(new Recipient[result.size()]);
recipientMvpView.addRecipients(recipientType, recipientArray);
stopLoading();
abandon();
}
}.startLoading();
}
private void addRecipientFromContactUri(final RecipientType recipientType, final Uri uri) {
new RecipientLoader(context, account.getOpenPgpProvider(), uri, false) {
@Override
public void deliverResult(List<Recipient> result) {
// TODO handle multiple available mail addresses for a contact?
if (result.isEmpty()) {
recipientMvpView.showErrorContactNoAddress();
return;
}
Recipient recipient = result.get(0);
recipientMvpView.addRecipients(recipientType, recipient);
stopLoading();
abandon();
}
}.startLoading();
}
void onToFocused() {
lastFocusedType = RecipientType.TO;
}
void onCcFocused() {
lastFocusedType = RecipientType.CC;
}
void onBccFocused() {
lastFocusedType = RecipientType.BCC;
}
public void onMenuAddFromContacts() {
int requestCode = recipientTypeToRequestCode(lastFocusedType);
recipientMvpView.showContactPicker(requestCode);
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case CONTACT_PICKER_TO:
case CONTACT_PICKER_CC:
case CONTACT_PICKER_BCC:
if (resultCode != Activity.RESULT_OK || data == null) {
return;
}
RecipientType recipientType = recipientTypeFromRequestCode(requestCode);
addRecipientFromContactUri(recipientType, data.getData());
break;
case OPENPGP_USER_INTERACTION:
openPgpApiManager.onUserInteractionResult();
break;
case REQUEST_CODE_AUTOCRYPT:
asyncUpdateCryptoStatus();
break;
}
}
private static int recipientTypeToRequestCode(RecipientType type) {
switch (type) {
case TO: {
return CONTACT_PICKER_TO;
}
case CC: {
return CONTACT_PICKER_CC;
}
case BCC: {
return CONTACT_PICKER_BCC;
}
}
throw new AssertionError("Unhandled case: " + type);
}
private static RecipientType recipientTypeFromRequestCode(int type) {
switch (type) {
case CONTACT_PICKER_TO: {
return RecipientType.TO;
}
case CONTACT_PICKER_CC: {
return RecipientType.CC;
}
case CONTACT_PICKER_BCC: {
return RecipientType.BCC;
}
}
throw new AssertionError("Unhandled case: " + type);
}
public void onNonRecipientFieldFocused() {
if (!account.isAlwaysShowCcBcc()) {
hideEmptyExtendedRecipientFields();
}
}
void onClickCryptoStatus() {
switch (openPgpApiManager.getOpenPgpProviderState()) {
case UNCONFIGURED:
Timber.e("click on crypto status while unconfigured - this should not really happen?!");
return;
case OK:
toggleEncryptionState(false);
return;
case UI_REQUIRED:
// TODO show openpgp settings
PendingIntent pendingIntent = openPgpApiManager.getUserInteractionPendingIntent();
recipientMvpView.launchUserInteractionPendingIntent(pendingIntent, OPENPGP_USER_INTERACTION);
break;
case UNINITIALIZED:
case ERROR:
openPgpApiManager.refreshConnection();
}
}
private void toggleEncryptionState(boolean showGotIt) {
ComposeCryptoStatus currentCryptoStatus = getCurrentCachedCryptoStatus();
if (currentCryptoStatus == null) {
Timber.e("click on crypto status while crypto status not available - should not really happen?!");
return;
}
if (currentCryptoStatus.isEncryptionEnabled() && !currentCryptoStatus.allRecipientsCanEncrypt()) {
recipientMvpView.showOpenPgpEnabledErrorDialog(false);
return;
}
if (currentCryptoMode == CryptoMode.SIGN_ONLY) {
recipientMvpView.showErrorIsSignOnly();
return;
}
boolean isEncryptOnNoChoice = currentCryptoStatus.canEncryptAndIsMutualDefault() ||
currentCryptoStatus.isReplyToEncrypted();
if (currentCryptoMode == CryptoMode.NO_CHOICE) {
if (currentCryptoStatus.hasAutocryptPendingIntent()) {
recipientMvpView.launchUserInteractionPendingIntent(
currentCryptoStatus.getAutocryptPendingIntent(), REQUEST_CODE_AUTOCRYPT);
} else if (isEncryptOnNoChoice) {
// TODO warning dialog if we override, especially from reply!
onCryptoModeChanged(CryptoMode.CHOICE_DISABLED);
} else {
onCryptoModeChanged(CryptoMode.CHOICE_ENABLED);
if (showGotIt) {
recipientMvpView.showOpenPgpEncryptExplanationDialog();
}
}
} else if (currentCryptoMode == CryptoMode.CHOICE_DISABLED && !isEncryptOnNoChoice) {
onCryptoModeChanged(CryptoMode.CHOICE_ENABLED);
} else {
onCryptoModeChanged(CryptoMode.NO_CHOICE);
}
}
/**
* Does the device actually have a Contacts application suitable for
* picking a contact. As hard as it is to believe, some vendors ship
* without it.
*
* @return True, if the device supports picking contacts. False, otherwise.
*/
private boolean hasContactPicker() {
if (hasContactPicker == null) {
Contacts contacts = Contacts.getInstance(context);
PackageManager packageManager = context.getPackageManager();
List<ResolveInfo> resolveInfoList = packageManager.queryIntentActivities(contacts.contactPickerIntent(), 0);
hasContactPicker = !resolveInfoList.isEmpty();
}
return hasContactPicker;
}
private boolean hasContactPermission() {
return ContextCompat.checkSelfPermission(context,
Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED;
}
public void showPgpSendError(SendErrorState sendErrorState) {
switch (sendErrorState) {
case ENABLED_ERROR:
recipientMvpView.showOpenPgpEnabledErrorDialog(false);
break;
case PROVIDER_ERROR:
recipientMvpView.showErrorOpenPgpConnection();
break;
case KEY_CONFIG_ERROR:
recipientMvpView.showErrorNoKeyConfigured();
break;
default:
throw new AssertionError("not all error states handled, this is a bug!");
}
}
void showPgpAttachError(AttachErrorState attachErrorState) {
switch (attachErrorState) {
case IS_INLINE:
recipientMvpView.showErrorInlineAttach();
break;
default:
throw new AssertionError("not all error states handled, this is a bug!");
}
}
public void builderSetProperties(MessageBuilder messageBuilder) {
if (messageBuilder instanceof PgpMessageBuilder) {
throw new IllegalArgumentException("PpgMessageBuilder must be called with ComposeCryptoStatus argument!");
}
messageBuilder.setTo(getToAddresses());
messageBuilder.setCc(getCcAddresses());
messageBuilder.setBcc(getBccAddresses());
}
public void builderSetProperties(PgpMessageBuilder pgpMessageBuilder, ComposeCryptoStatus cryptoStatus) {
pgpMessageBuilder.setTo(getToAddresses());
pgpMessageBuilder.setCc(getCcAddresses());
pgpMessageBuilder.setBcc(getBccAddresses());
pgpMessageBuilder.setOpenPgpApi(openPgpApiManager.getOpenPgpApi());
pgpMessageBuilder.setCryptoStatus(cryptoStatus);
}
public void onMenuSetPgpInline(boolean enablePgpInline) {
onCryptoPgpInlineChanged(enablePgpInline);
if (enablePgpInline) {
boolean shouldShowPgpInlineDialog = checkAndIncrementPgpInlineDialogCounter();
if (shouldShowPgpInlineDialog) {
recipientMvpView.showOpenPgpInlineDialog(true);
}
}
}
public void onMenuSetSignOnly(boolean enableSignOnly) {
if (enableSignOnly) {
onCryptoModeChanged(CryptoMode.SIGN_ONLY);
boolean shouldShowPgpSignOnlyDialog = checkAndIncrementPgpSignOnlyDialogCounter();
if (shouldShowPgpSignOnlyDialog) {
recipientMvpView.showOpenPgpSignOnlyDialog(true);
}
} else {
onCryptoModeChanged(CryptoMode.NO_CHOICE);
}
}
public void onMenuToggleEncryption() {
toggleEncryptionState(true);
}
public void onCryptoPgpClickDisable() {
onCryptoModeChanged(CryptoMode.CHOICE_DISABLED);
}
public void onCryptoPgpSignOnlyDisabled() {
onCryptoPgpInlineChanged(false);
onCryptoModeChanged(CryptoMode.NO_CHOICE);
}
private boolean checkAndIncrementPgpInlineDialogCounter() {
int pgpInlineDialogCounter = K9.getPgpInlineDialogCounter();
if (pgpInlineDialogCounter < PGP_DIALOG_DISPLAY_THRESHOLD) {
K9.setPgpInlineDialogCounter(pgpInlineDialogCounter + 1);
K9.saveSettingsAsync();
return true;
}
return false;
}
private boolean checkAndIncrementPgpSignOnlyDialogCounter() {
int pgpSignOnlyDialogCounter = K9.getPgpSignOnlyDialogCounter();
if (pgpSignOnlyDialogCounter < PGP_DIALOG_DISPLAY_THRESHOLD) {
K9.setPgpSignOnlyDialogCounter(pgpSignOnlyDialogCounter + 1);
K9.saveSettingsAsync();
return true;
}
return false;
}
void onClickCryptoSpecialModeIndicator() {
if (currentCryptoMode == CryptoMode.SIGN_ONLY) {
recipientMvpView.showOpenPgpSignOnlyDialog(false);
} else if (cryptoEnablePgpInline) {
recipientMvpView.showOpenPgpInlineDialog(false);
} else {
throw new IllegalStateException("This icon should not be clickable while no special mode is active!");
}
}
public boolean shouldSaveRemotely() {
// TODO more appropriate logic?
return cachedCryptoStatus == null || !cachedCryptoStatus.isEncryptionEnabled();
}
private final OpenPgpApiManagerCallback openPgpCallback = new OpenPgpApiManagerCallback() {
@Override
public void onOpenPgpProviderStatusChanged() {
if (openPgpApiManager.getOpenPgpProviderState() == OpenPgpProviderState.UI_REQUIRED) {
recipientMvpView.showErrorOpenPgpUserInteractionRequired();
}
asyncUpdateCryptoStatus();
}
@Override
public void onOpenPgpProviderError(OpenPgpProviderError error) {
switch (error) {
case ConnectionLost:
openPgpApiManager.refreshConnection();
break;
case VersionIncompatible:
recipientMvpView.showErrorOpenPgpIncompatible();
break;
case ConnectionFailed:
default:
recipientMvpView.showErrorOpenPgpConnection();
break;
}
}
};
public enum CryptoMode {
SIGN_ONLY,
NO_CHOICE,
CHOICE_DISABLED,
CHOICE_ENABLED,
}
}

View file

@ -0,0 +1,770 @@
package com.fsck.k9.activity.compose
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.AsyncTask
import android.os.Bundle
import android.view.Menu
import androidx.core.content.ContextCompat
import androidx.loader.app.LoaderManager
import com.fsck.k9.Account
import com.fsck.k9.Identity
import com.fsck.k9.K9
import com.fsck.k9.activity.compose.ComposeCryptoStatus.AttachErrorState
import com.fsck.k9.activity.compose.ComposeCryptoStatus.SendErrorState
import com.fsck.k9.autocrypt.AutocryptDraftStateHeader
import com.fsck.k9.autocrypt.AutocryptDraftStateHeaderParser
import com.fsck.k9.helper.Contacts
import com.fsck.k9.helper.MailTo
import com.fsck.k9.helper.ReplyToParser
import com.fsck.k9.mail.Address
import com.fsck.k9.mail.Flag
import com.fsck.k9.mail.Message
import com.fsck.k9.mail.Message.RecipientType
import com.fsck.k9.message.AutocryptStatusInteractor
import com.fsck.k9.message.AutocryptStatusInteractor.RecipientAutocryptStatus
import com.fsck.k9.message.ComposePgpEnableByDefaultDecider
import com.fsck.k9.message.ComposePgpInlineDecider
import com.fsck.k9.message.MessageBuilder
import com.fsck.k9.message.PgpMessageBuilder
import com.fsck.k9.ui.R
import com.fsck.k9.view.RecipientSelectView.Recipient
import org.openintents.openpgp.OpenPgpApiManager
import org.openintents.openpgp.OpenPgpApiManager.OpenPgpApiManagerCallback
import org.openintents.openpgp.OpenPgpApiManager.OpenPgpProviderError
import org.openintents.openpgp.OpenPgpApiManager.OpenPgpProviderState
import timber.log.Timber
private const val STATE_KEY_CC_SHOWN = "state:ccShown"
private const val STATE_KEY_BCC_SHOWN = "state:bccShown"
private const val STATE_KEY_LAST_FOCUSED_TYPE = "state:lastFocusedType"
private const val STATE_KEY_CURRENT_CRYPTO_MODE = "state:currentCryptoMode"
private const val STATE_KEY_CRYPTO_ENABLE_PGP_INLINE = "state:cryptoEnablePgpInline"
private const val CONTACT_PICKER_TO = 1
private const val CONTACT_PICKER_CC = 2
private const val CONTACT_PICKER_BCC = 3
private const val OPENPGP_USER_INTERACTION = 4
private const val REQUEST_CODE_AUTOCRYPT = 5
private const val PGP_DIALOG_DISPLAY_THRESHOLD = 2
class RecipientPresenter(
private val context: Context,
loaderManager: LoaderManager,
private val openPgpApiManager: OpenPgpApiManager,
private val recipientMvpView: RecipientMvpView,
account: Account,
private val composePgpInlineDecider: ComposePgpInlineDecider,
private val composePgpEnableByDefaultDecider: ComposePgpEnableByDefaultDecider,
private val autocryptStatusInteractor: AutocryptStatusInteractor,
private val replyToParser: ReplyToParser,
private val draftStateHeaderParser: AutocryptDraftStateHeaderParser
) {
private lateinit var account: Account
private var alwaysBccAddresses: Array<Address>? = null
private var hasContactPicker: Boolean? = null
private var isReplyToEncryptedMessage = false
private var lastFocusedType = RecipientType.TO
private var currentCryptoMode = CryptoMode.NO_CHOICE
var isForceTextMessageFormat = false
private set
var currentCachedCryptoStatus: ComposeCryptoStatus? = null
private set
val toAddresses: List<Address>
get() = recipientMvpView.toAddresses
val ccAddresses: List<Address>
get() = recipientMvpView.ccAddresses
val bccAddresses: List<Address>
get() = recipientMvpView.bccAddresses
private val allRecipients: List<Recipient>
get() = with(recipientMvpView) { toRecipients + ccRecipients + bccRecipients }
init {
recipientMvpView.setPresenter(this)
recipientMvpView.setLoaderManager(loaderManager)
onSwitchAccount(account)
}
fun checkRecipientsOkForSending(): Boolean {
recipientMvpView.recipientToTryPerformCompletion()
recipientMvpView.recipientCcTryPerformCompletion()
recipientMvpView.recipientBccTryPerformCompletion()
if (recipientMvpView.recipientToHasUncompletedText()) {
recipientMvpView.showToUncompletedError()
return true
}
if (recipientMvpView.recipientCcHasUncompletedText()) {
recipientMvpView.showCcUncompletedError()
return true
}
if (recipientMvpView.recipientBccHasUncompletedText()) {
recipientMvpView.showBccUncompletedError()
return true
}
if (toAddresses.isEmpty() && ccAddresses.isEmpty() && bccAddresses.isEmpty()) {
recipientMvpView.showNoRecipientsError()
return true
}
return false
}
fun initFromReplyToMessage(message: Message?, isReplyAll: Boolean) {
val replyToAddresses = if (isReplyAll) {
replyToParser.getRecipientsToReplyAllTo(message, account)
} else {
replyToParser.getRecipientsToReplyTo(message, account)
}
addToAddresses(*replyToAddresses.to)
addCcAddresses(*replyToAddresses.cc)
val shouldSendAsPgpInline = composePgpInlineDecider.shouldReplyInline(message)
if (shouldSendAsPgpInline) {
isForceTextMessageFormat = true
}
isReplyToEncryptedMessage = composePgpEnableByDefaultDecider.shouldEncryptByDefault(message)
}
fun initFromTrustIdAction(trustId: String?) {
addToAddresses(*Address.parse(trustId))
currentCryptoMode = CryptoMode.CHOICE_ENABLED
}
fun initFromMailto(mailTo: MailTo) {
addToAddresses(*mailTo.to)
addCcAddresses(*mailTo.cc)
addBccAddresses(*mailTo.bcc)
}
fun initFromSendOrViewIntent(intent: Intent) {
val toAddresses = intent.getStringArrayExtra(Intent.EXTRA_EMAIL)?.toAddressArray()
val ccAddresses = intent.getStringArrayExtra(Intent.EXTRA_CC)?.toAddressArray()
val bccAddresses = intent.getStringArrayExtra(Intent.EXTRA_BCC)?.toAddressArray()
if (toAddresses != null) {
addToAddresses(*toAddresses)
}
if (ccAddresses != null) {
addCcAddresses(*ccAddresses)
}
if (bccAddresses != null) {
addBccAddresses(*bccAddresses)
}
}
fun onRestoreInstanceState(savedInstanceState: Bundle) {
recipientMvpView.setCcVisibility(savedInstanceState.getBoolean(STATE_KEY_CC_SHOWN))
recipientMvpView.setBccVisibility(savedInstanceState.getBoolean(STATE_KEY_BCC_SHOWN))
lastFocusedType = RecipientType.valueOf(savedInstanceState.getString(STATE_KEY_LAST_FOCUSED_TYPE)!!)
currentCryptoMode = CryptoMode.valueOf(savedInstanceState.getString(STATE_KEY_CURRENT_CRYPTO_MODE)!!)
isForceTextMessageFormat = savedInstanceState.getBoolean(STATE_KEY_CRYPTO_ENABLE_PGP_INLINE)
updateRecipientExpanderVisibility()
}
fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(STATE_KEY_CC_SHOWN, recipientMvpView.isCcVisible)
outState.putBoolean(STATE_KEY_BCC_SHOWN, recipientMvpView.isBccVisible)
outState.putString(STATE_KEY_LAST_FOCUSED_TYPE, lastFocusedType.toString())
outState.putString(STATE_KEY_CURRENT_CRYPTO_MODE, currentCryptoMode.toString())
outState.putBoolean(STATE_KEY_CRYPTO_ENABLE_PGP_INLINE, isForceTextMessageFormat)
}
fun initFromDraftMessage(message: Message) {
initRecipientsFromDraftMessage(message)
val draftStateHeader = message.getHeader(AutocryptDraftStateHeader.AUTOCRYPT_DRAFT_STATE_HEADER)
if (draftStateHeader.size == 1) {
initEncryptionStateFromDraftStateHeader(draftStateHeader.first())
} else {
initPgpInlineFromDraftMessage(message)
}
}
private fun initEncryptionStateFromDraftStateHeader(headerValue: String) {
val autocryptDraftStateHeader = draftStateHeaderParser.parseAutocryptDraftStateHeader(headerValue)
if (autocryptDraftStateHeader != null) {
initEncryptionStateFromDraftStateHeader(autocryptDraftStateHeader)
}
}
private fun initRecipientsFromDraftMessage(message: Message) {
addToAddresses(*message.getRecipients(RecipientType.TO))
addCcAddresses(*message.getRecipients(RecipientType.CC))
addBccAddresses(*message.getRecipients(RecipientType.BCC))
}
private fun initEncryptionStateFromDraftStateHeader(draftState: AutocryptDraftStateHeader) {
isForceTextMessageFormat = draftState.isPgpInline
isReplyToEncryptedMessage = draftState.isReply
if (!draftState.isByChoice) {
// TODO if it's not by choice, we're going with our defaults. should we do something here if those differ?
return
}
currentCryptoMode = when {
draftState.isSignOnly -> CryptoMode.SIGN_ONLY
draftState.isEncrypt -> CryptoMode.CHOICE_ENABLED
else -> CryptoMode.CHOICE_DISABLED
}
}
private fun initPgpInlineFromDraftMessage(message: Message) {
isForceTextMessageFormat = message.isSet(Flag.X_DRAFT_OPENPGP_INLINE)
}
private fun addToAddresses(vararg toAddresses: Address) {
addRecipientsFromAddresses(RecipientType.TO, *toAddresses)
}
private fun addCcAddresses(vararg ccAddresses: Address) {
if (ccAddresses.isNotEmpty()) {
addRecipientsFromAddresses(RecipientType.CC, *ccAddresses)
recipientMvpView.setCcVisibility(true)
updateRecipientExpanderVisibility()
}
}
private fun addBccAddresses(vararg bccRecipients: Address) {
if (bccRecipients.isNotEmpty()) {
addRecipientsFromAddresses(RecipientType.BCC, *bccRecipients)
recipientMvpView.setBccVisibility(true)
updateRecipientExpanderVisibility()
}
}
private fun addAlwaysBcc() {
val alwaysBccAddresses = Address.parse(account.alwaysBcc)
this.alwaysBccAddresses = alwaysBccAddresses
if (alwaysBccAddresses.isEmpty()) return
object : RecipientLoader(context, account.openPgpProvider, *alwaysBccAddresses) {
override fun deliverResult(result: List<Recipient>?) {
val recipientArray = result!!.toTypedArray()
recipientMvpView.silentlyAddBccAddresses(*recipientArray)
stopLoading()
abandon()
}
}.startLoading()
}
private fun removeAlwaysBcc() {
alwaysBccAddresses?.let { alwaysBccAddresses ->
recipientMvpView.silentlyRemoveBccAddresses(alwaysBccAddresses)
}
}
fun onPrepareOptionsMenu(menu: Menu) {
val currentCryptoStatus = currentCachedCryptoStatus
if (currentCryptoStatus != null && currentCryptoStatus.isProviderStateOk()) {
val isEncrypting = currentCryptoStatus.isEncryptionEnabled
menu.findItem(R.id.openpgp_encrypt_enable).isVisible = !isEncrypting
menu.findItem(R.id.openpgp_encrypt_disable).isVisible = isEncrypting
val showSignOnly = !account.isOpenPgpHideSignOnly
val isSignOnly = currentCryptoStatus.isSignOnly
menu.findItem(R.id.openpgp_sign_only).isVisible = showSignOnly && !isSignOnly
menu.findItem(R.id.openpgp_sign_only_disable).isVisible = showSignOnly && isSignOnly
val pgpInlineModeEnabled = currentCryptoStatus.isPgpInlineModeEnabled
val showPgpInlineEnable = (isEncrypting || isSignOnly) && !pgpInlineModeEnabled
menu.findItem(R.id.openpgp_inline_enable).isVisible = showPgpInlineEnable
menu.findItem(R.id.openpgp_inline_disable).isVisible = pgpInlineModeEnabled
} else {
menu.findItem(R.id.openpgp_inline_enable).isVisible = false
menu.findItem(R.id.openpgp_inline_disable).isVisible = false
menu.findItem(R.id.openpgp_encrypt_enable).isVisible = false
menu.findItem(R.id.openpgp_encrypt_disable).isVisible = false
menu.findItem(R.id.openpgp_sign_only).isVisible = false
menu.findItem(R.id.openpgp_sign_only_disable).isVisible = false
}
menu.findItem(R.id.add_from_contacts).isVisible = hasContactPermission() && hasContactPicker()
}
fun onSwitchAccount(account: Account) {
this.account = account
if (account.isAlwaysShowCcBcc) {
recipientMvpView.setCcVisibility(true)
recipientMvpView.setBccVisibility(true)
updateRecipientExpanderVisibility()
}
removeAlwaysBcc()
addAlwaysBcc()
val openPgpProvider = account.openPgpProvider
recipientMvpView.setCryptoProvider(openPgpProvider)
openPgpApiManager.setOpenPgpProvider(openPgpProvider, openPgpCallback)
}
fun onSwitchIdentity(identity: Identity) {
// TODO decide what actually to do on identity switch?
asyncUpdateCryptoStatus()
}
fun onClickToLabel() {
recipientMvpView.requestFocusOnToField()
}
fun onClickCcLabel() {
recipientMvpView.requestFocusOnCcField()
}
fun onClickBccLabel() {
recipientMvpView.requestFocusOnBccField()
}
fun onClickRecipientExpander() {
recipientMvpView.setCcVisibility(true)
recipientMvpView.setBccVisibility(true)
updateRecipientExpanderVisibility()
}
private fun hideEmptyExtendedRecipientFields() {
if (recipientMvpView.ccAddresses.isEmpty()) {
recipientMvpView.setCcVisibility(false)
if (lastFocusedType == RecipientType.CC) {
lastFocusedType = RecipientType.TO
}
}
if (recipientMvpView.bccAddresses.isEmpty()) {
recipientMvpView.setBccVisibility(false)
if (lastFocusedType == RecipientType.BCC) {
lastFocusedType = RecipientType.TO
}
}
updateRecipientExpanderVisibility()
}
private fun updateRecipientExpanderVisibility() {
val notBothAreVisible = !(recipientMvpView.isCcVisible && recipientMvpView.isBccVisible)
recipientMvpView.setRecipientExpanderVisibility(notBothAreVisible)
}
fun asyncUpdateCryptoStatus() {
currentCachedCryptoStatus = null
val openPgpProviderState = openPgpApiManager.openPgpProviderState
var accountCryptoKey: Long? = account.openPgpKey
if (accountCryptoKey == Account.NO_OPENPGP_KEY) {
accountCryptoKey = null
}
val composeCryptoStatus = ComposeCryptoStatus(
openPgpProviderState = openPgpProviderState,
openPgpKeyId = accountCryptoKey,
recipientAddresses = allRecipients,
isPgpInlineModeEnabled = isForceTextMessageFormat,
isSenderPreferEncryptMutual = account.autocryptPreferEncryptMutual,
isReplyToEncrypted = isReplyToEncryptedMessage,
isEncryptAllDrafts = account.isOpenPgpEncryptAllDrafts,
isEncryptSubject = account.isOpenPgpEncryptSubject,
cryptoMode = currentCryptoMode
)
if (openPgpProviderState != OpenPgpProviderState.OK) {
currentCachedCryptoStatus = composeCryptoStatus
redrawCachedCryptoStatusIcon()
return
}
val recipientAddresses = composeCryptoStatus.recipientAddressesAsArray
object : AsyncTask<Void?, Void?, RecipientAutocryptStatus?>() {
override fun doInBackground(vararg params: Void?): RecipientAutocryptStatus? {
val openPgpApi = openPgpApiManager.openPgpApi ?: return null
return autocryptStatusInteractor.retrieveCryptoProviderRecipientStatus(openPgpApi, recipientAddresses)
}
override fun onPostExecute(recipientAutocryptStatus: RecipientAutocryptStatus?) {
currentCachedCryptoStatus = if (recipientAutocryptStatus != null) {
composeCryptoStatus.withRecipientAutocryptStatus(recipientAutocryptStatus)
} else {
composeCryptoStatus
}
redrawCachedCryptoStatusIcon()
}
}.execute()
}
private fun redrawCachedCryptoStatusIcon() {
val cryptoStatus = checkNotNull(currentCachedCryptoStatus) { "must have cached crypto status to redraw it!" }
recipientMvpView.setRecipientTokensShowCryptoEnabled(cryptoStatus.isEncryptionEnabled)
recipientMvpView.showCryptoStatus(cryptoStatus.displayType)
recipientMvpView.showCryptoSpecialMode(cryptoStatus.specialModeDisplayType)
}
fun onToTokenAdded() {
asyncUpdateCryptoStatus()
}
fun onToTokenRemoved() {
asyncUpdateCryptoStatus()
}
fun onToTokenChanged() {
asyncUpdateCryptoStatus()
}
fun onCcTokenAdded() {
asyncUpdateCryptoStatus()
}
fun onCcTokenRemoved() {
asyncUpdateCryptoStatus()
}
fun onCcTokenChanged() {
asyncUpdateCryptoStatus()
}
fun onBccTokenAdded() {
asyncUpdateCryptoStatus()
}
fun onBccTokenRemoved() {
asyncUpdateCryptoStatus()
}
fun onBccTokenChanged() {
asyncUpdateCryptoStatus()
}
fun onCryptoModeChanged(cryptoMode: CryptoMode) {
currentCryptoMode = cryptoMode
asyncUpdateCryptoStatus()
}
fun onCryptoPgpInlineChanged(enablePgpInline: Boolean) {
isForceTextMessageFormat = enablePgpInline
asyncUpdateCryptoStatus()
}
private fun addRecipientsFromAddresses(recipientType: RecipientType, vararg addresses: Address) {
object : RecipientLoader(context, account.openPgpProvider, *addresses) {
override fun deliverResult(result: List<Recipient>?) {
val recipientArray = result!!.toTypedArray()
recipientMvpView.addRecipients(recipientType, *recipientArray)
stopLoading()
abandon()
}
}.startLoading()
}
private fun addRecipientFromContactUri(recipientType: RecipientType, uri: Uri?) {
object : RecipientLoader(context, account.openPgpProvider, uri, false) {
override fun deliverResult(result: List<Recipient>?) {
// TODO handle multiple available mail addresses for a contact?
if (result!!.isEmpty()) {
recipientMvpView.showErrorContactNoAddress()
return
}
val recipient = result[0]
recipientMvpView.addRecipients(recipientType, recipient)
stopLoading()
abandon()
}
}.startLoading()
}
fun onToFocused() {
lastFocusedType = RecipientType.TO
}
fun onCcFocused() {
lastFocusedType = RecipientType.CC
}
fun onBccFocused() {
lastFocusedType = RecipientType.BCC
}
fun onMenuAddFromContacts() {
val requestCode = lastFocusedType.toRequestCode()
recipientMvpView.showContactPicker(requestCode)
}
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
CONTACT_PICKER_TO, CONTACT_PICKER_CC, CONTACT_PICKER_BCC -> {
if (resultCode != Activity.RESULT_OK || data == null) return
val recipientType = requestCode.toRecipientType()
addRecipientFromContactUri(recipientType, data.data)
}
OPENPGP_USER_INTERACTION -> {
openPgpApiManager.onUserInteractionResult()
}
REQUEST_CODE_AUTOCRYPT -> {
asyncUpdateCryptoStatus()
}
}
}
fun onNonRecipientFieldFocused() {
if (!account.isAlwaysShowCcBcc) {
hideEmptyExtendedRecipientFields()
}
}
fun onClickCryptoStatus() {
when (openPgpApiManager.openPgpProviderState) {
OpenPgpProviderState.UNCONFIGURED -> {
Timber.e("click on crypto status while unconfigured - this should not really happen?!")
}
OpenPgpProviderState.OK -> {
toggleEncryptionState(false)
}
OpenPgpProviderState.UI_REQUIRED -> {
// TODO show openpgp settings
val pendingIntent = openPgpApiManager.userInteractionPendingIntent
recipientMvpView.launchUserInteractionPendingIntent(pendingIntent, OPENPGP_USER_INTERACTION)
}
OpenPgpProviderState.UNINITIALIZED, OpenPgpProviderState.ERROR -> {
openPgpApiManager.refreshConnection()
}
}
}
private fun toggleEncryptionState(showGotIt: Boolean) {
val currentCryptoStatus = currentCachedCryptoStatus
if (currentCryptoStatus == null) {
Timber.e("click on crypto status while crypto status not available - should not really happen?!")
return
}
if (currentCryptoStatus.isEncryptionEnabled && !currentCryptoStatus.allRecipientsCanEncrypt()) {
recipientMvpView.showOpenPgpEnabledErrorDialog(false)
return
}
if (currentCryptoMode == CryptoMode.SIGN_ONLY) {
recipientMvpView.showErrorIsSignOnly()
return
}
val isEncryptOnNoChoice = currentCryptoStatus.canEncryptAndIsMutualDefault() ||
currentCryptoStatus.isReplyToEncrypted
if (currentCryptoMode == CryptoMode.NO_CHOICE) {
if (currentCryptoStatus.hasAutocryptPendingIntent()) {
recipientMvpView.launchUserInteractionPendingIntent(
currentCryptoStatus.autocryptPendingIntent, REQUEST_CODE_AUTOCRYPT
)
} else if (isEncryptOnNoChoice) {
// TODO warning dialog if we override, especially from reply!
onCryptoModeChanged(CryptoMode.CHOICE_DISABLED)
} else {
onCryptoModeChanged(CryptoMode.CHOICE_ENABLED)
if (showGotIt) {
recipientMvpView.showOpenPgpEncryptExplanationDialog()
}
}
} else if (currentCryptoMode == CryptoMode.CHOICE_DISABLED && !isEncryptOnNoChoice) {
onCryptoModeChanged(CryptoMode.CHOICE_ENABLED)
} else {
onCryptoModeChanged(CryptoMode.NO_CHOICE)
}
}
/**
* Does the device actually have a Contacts application suitable for picking a contact.
* As hard as it is to believe, some vendors ship without it.
*/
private fun hasContactPicker(): Boolean {
return hasContactPicker ?: isContactPickerAvailable().also { hasContactPicker = it }
}
private fun isContactPickerAvailable(): Boolean {
val contacts = Contacts.getInstance(context)
val resolveInfoList = context.packageManager.queryIntentActivities(contacts.contactPickerIntent(), 0)
return resolveInfoList.isNotEmpty()
}
private fun hasContactPermission(): Boolean {
val permissionState = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
return permissionState == PackageManager.PERMISSION_GRANTED
}
fun showPgpSendError(sendErrorState: SendErrorState) {
when (sendErrorState) {
SendErrorState.ENABLED_ERROR -> recipientMvpView.showOpenPgpEnabledErrorDialog(false)
SendErrorState.PROVIDER_ERROR -> recipientMvpView.showErrorOpenPgpConnection()
SendErrorState.KEY_CONFIG_ERROR -> recipientMvpView.showErrorNoKeyConfigured()
else -> throw AssertionError("not all error states handled, this is a bug!")
}
}
fun showPgpAttachError(attachErrorState: AttachErrorState) {
when (attachErrorState) {
AttachErrorState.IS_INLINE -> recipientMvpView.showErrorInlineAttach()
else -> throw AssertionError("not all error states handled, this is a bug!")
}
}
fun builderSetProperties(messageBuilder: MessageBuilder) {
require(messageBuilder !is PgpMessageBuilder) {
"PpgMessageBuilder must be called with ComposeCryptoStatus argument!"
}
messageBuilder.setTo(toAddresses)
messageBuilder.setCc(ccAddresses)
messageBuilder.setBcc(bccAddresses)
}
fun builderSetProperties(pgpMessageBuilder: PgpMessageBuilder, cryptoStatus: ComposeCryptoStatus) {
pgpMessageBuilder.setTo(toAddresses)
pgpMessageBuilder.setCc(ccAddresses)
pgpMessageBuilder.setBcc(bccAddresses)
pgpMessageBuilder.setOpenPgpApi(openPgpApiManager.openPgpApi)
pgpMessageBuilder.setCryptoStatus(cryptoStatus)
}
fun onMenuSetPgpInline(enablePgpInline: Boolean) {
onCryptoPgpInlineChanged(enablePgpInline)
if (enablePgpInline) {
val shouldShowPgpInlineDialog = checkAndIncrementPgpInlineDialogCounter()
if (shouldShowPgpInlineDialog) {
recipientMvpView.showOpenPgpInlineDialog(true)
}
}
}
fun onMenuSetSignOnly(enableSignOnly: Boolean) {
if (enableSignOnly) {
onCryptoModeChanged(CryptoMode.SIGN_ONLY)
val shouldShowPgpSignOnlyDialog = checkAndIncrementPgpSignOnlyDialogCounter()
if (shouldShowPgpSignOnlyDialog) {
recipientMvpView.showOpenPgpSignOnlyDialog(true)
}
} else {
onCryptoModeChanged(CryptoMode.NO_CHOICE)
}
}
fun onMenuToggleEncryption() {
toggleEncryptionState(true)
}
fun onCryptoPgpClickDisable() {
onCryptoModeChanged(CryptoMode.CHOICE_DISABLED)
}
fun onCryptoPgpSignOnlyDisabled() {
onCryptoPgpInlineChanged(false)
onCryptoModeChanged(CryptoMode.NO_CHOICE)
}
private fun checkAndIncrementPgpInlineDialogCounter(): Boolean {
val pgpInlineDialogCounter = K9.pgpInlineDialogCounter
if (pgpInlineDialogCounter < PGP_DIALOG_DISPLAY_THRESHOLD) {
K9.pgpInlineDialogCounter = pgpInlineDialogCounter + 1
K9.saveSettingsAsync()
return true
}
return false
}
private fun checkAndIncrementPgpSignOnlyDialogCounter(): Boolean {
val pgpSignOnlyDialogCounter = K9.pgpSignOnlyDialogCounter
if (pgpSignOnlyDialogCounter < PGP_DIALOG_DISPLAY_THRESHOLD) {
K9.pgpSignOnlyDialogCounter = pgpSignOnlyDialogCounter + 1
K9.saveSettingsAsync()
return true
}
return false
}
fun onClickCryptoSpecialModeIndicator() {
when {
currentCryptoMode == CryptoMode.SIGN_ONLY -> {
recipientMvpView.showOpenPgpSignOnlyDialog(false)
}
isForceTextMessageFormat -> {
recipientMvpView.showOpenPgpInlineDialog(false)
}
else -> {
error("This icon should not be clickable while no special mode is active!")
}
}
}
private val openPgpCallback = object : OpenPgpApiManagerCallback {
override fun onOpenPgpProviderStatusChanged() {
if (openPgpApiManager.openPgpProviderState == OpenPgpProviderState.UI_REQUIRED) {
recipientMvpView.showErrorOpenPgpUserInteractionRequired()
}
asyncUpdateCryptoStatus()
}
override fun onOpenPgpProviderError(error: OpenPgpProviderError) {
when (error) {
OpenPgpProviderError.ConnectionLost -> openPgpApiManager.refreshConnection()
OpenPgpProviderError.VersionIncompatible -> recipientMvpView.showErrorOpenPgpIncompatible()
OpenPgpProviderError.ConnectionFailed -> recipientMvpView.showErrorOpenPgpConnection()
else -> recipientMvpView.showErrorOpenPgpConnection()
}
}
}
private fun Array<String>.toAddressArray(): Array<Address> {
return flatMap { addressString ->
Address.parseUnencoded(addressString).toList()
}.toTypedArray()
}
private fun RecipientType.toRequestCode(): Int = when (this) {
RecipientType.TO -> CONTACT_PICKER_TO
RecipientType.CC -> CONTACT_PICKER_CC
RecipientType.BCC -> CONTACT_PICKER_BCC
else -> throw AssertionError("Unhandled case: $this")
}
private fun Int.toRecipientType(): RecipientType = when (this) {
CONTACT_PICKER_TO -> RecipientType.TO
CONTACT_PICKER_CC -> RecipientType.CC
CONTACT_PICKER_BCC -> RecipientType.BCC
else -> throw AssertionError("Unhandled case: $this")
}
enum class CryptoMode {
SIGN_ONLY, NO_CHOICE, CHOICE_DISABLED, CHOICE_ENABLED
}
}

View file

@ -1,301 +0,0 @@
package com.fsck.k9.activity.compose;
import java.util.Arrays;
import java.util.List;
import android.content.Context;
import androidx.loader.app.LoaderManager;
import com.fsck.k9.Account;
import com.fsck.k9.DI;
import com.fsck.k9.K9RobolectricTest;
import com.fsck.k9.activity.compose.RecipientMvpView.CryptoSpecialModeDisplayType;
import com.fsck.k9.activity.compose.RecipientMvpView.CryptoStatusDisplayType;
import com.fsck.k9.activity.compose.RecipientPresenter.CryptoMode;
import com.fsck.k9.autocrypt.AutocryptDraftStateHeaderParser;
import com.fsck.k9.helper.ReplyToParser;
import com.fsck.k9.helper.ReplyToParser.ReplyToAddresses;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.message.AutocryptStatusInteractor;
import com.fsck.k9.message.AutocryptStatusInteractor.RecipientAutocryptStatus;
import com.fsck.k9.message.AutocryptStatusInteractor.RecipientAutocryptStatusType;
import com.fsck.k9.message.ComposePgpEnableByDefaultDecider;
import com.fsck.k9.message.ComposePgpInlineDecider;
import com.fsck.k9.view.RecipientSelectView.Recipient;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.openintents.openpgp.OpenPgpApiManager;
import org.openintents.openpgp.OpenPgpApiManager.OpenPgpApiManagerCallback;
import org.openintents.openpgp.OpenPgpApiManager.OpenPgpProviderState;
import org.openintents.openpgp.util.OpenPgpApi;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.LooperMode;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SuppressWarnings("ConstantConditions")
@LooperMode(LooperMode.Mode.LEGACY)
public class RecipientPresenterTest extends K9RobolectricTest {
private static final ReplyToAddresses TO_ADDRESSES = new ReplyToAddresses(Address.parse("to@example.org"));
private static final List<Address> ALL_TO_ADDRESSES = Arrays.asList(Address.parse("allTo@example.org"));
private static final List<Address> ALL_CC_ADDRESSES = Arrays.asList(Address.parse("allCc@example.org"));
private static final String CRYPTO_PROVIDER = "crypto_provider";
private static final long CRYPTO_KEY_ID = 123L;
private RecipientPresenter recipientPresenter;
private ReplyToParser replyToParser;
private ComposePgpInlineDecider composePgpInlineDecider;
private ComposePgpEnableByDefaultDecider composePgpEnableByDefaultDecider;
private Account account;
private RecipientMvpView recipientMvpView;
private AutocryptStatusInteractor autocryptStatusInteractor;
private RecipientAutocryptStatus noRecipientsAutocryptResult;
private OpenPgpApiManager openPgpApiManager;
private OpenPgpApiManagerCallback openPgpApiManagerCallback;
@Before
public void setUp() throws Exception {
Context context = RuntimeEnvironment.application;
Robolectric.getBackgroundThreadScheduler().pause();
recipientMvpView = mock(RecipientMvpView.class);
openPgpApiManager = mock(OpenPgpApiManager.class);
account = mock(Account.class);
composePgpInlineDecider = mock(ComposePgpInlineDecider.class);
composePgpEnableByDefaultDecider = mock(ComposePgpEnableByDefaultDecider.class);
autocryptStatusInteractor = mock(AutocryptStatusInteractor.class);
replyToParser = mock(ReplyToParser.class);
LoaderManager loaderManager = mock(LoaderManager.class);
when(openPgpApiManager.getOpenPgpProviderState()).thenReturn(OpenPgpProviderState.UNCONFIGURED);
recipientPresenter = new RecipientPresenter(
context, loaderManager, openPgpApiManager, recipientMvpView, account, composePgpInlineDecider,
composePgpEnableByDefaultDecider, autocryptStatusInteractor, replyToParser,
DI.get(AutocryptDraftStateHeaderParser.class)
);
ArgumentCaptor<OpenPgpApiManagerCallback> callbackCaptor = ArgumentCaptor.forClass(OpenPgpApiManagerCallback.class);
verify(openPgpApiManager).setOpenPgpProvider(isNull(String.class), callbackCaptor.capture());
openPgpApiManagerCallback = callbackCaptor.getValue();
noRecipientsAutocryptResult = new RecipientAutocryptStatus(RecipientAutocryptStatusType.NO_RECIPIENTS, null);
}
@Test
@Ignore("It looks like the support version of AsyncTaskLoader handles background tasks differently")
public void testInitFromReplyToMessage() throws Exception {
Message message = mock(Message.class);
when(replyToParser.getRecipientsToReplyTo(message, account)).thenReturn(TO_ADDRESSES);
recipientPresenter.initFromReplyToMessage(message, false);
runBackgroundTask();
Recipient toRecipient = new Recipient(TO_ADDRESSES.to[0]);
verify(recipientMvpView).addRecipients(eq(RecipientType.TO), eq(toRecipient));
}
@Test
@Ignore("It looks like the support version of AsyncTaskLoader handles background tasks differently")
public void testInitFromReplyToAllMessage() throws Exception {
Message message = mock(Message.class);
when(replyToParser.getRecipientsToReplyTo(message, account)).thenReturn(TO_ADDRESSES);
ReplyToAddresses replyToAddresses = new ReplyToAddresses(ALL_TO_ADDRESSES, ALL_CC_ADDRESSES);
when(replyToParser.getRecipientsToReplyAllTo(message, account)).thenReturn(replyToAddresses);
recipientPresenter.initFromReplyToMessage(message, true);
// one for To, one for Cc
runBackgroundTask();
runBackgroundTask();
verify(recipientMvpView).addRecipients(eq(RecipientType.TO), any(Recipient.class));
verify(recipientMvpView).addRecipients(eq(RecipientType.CC), any(Recipient.class));
}
@Test
public void initFromReplyToMessage_shouldCallComposePgpInlineDecider() throws Exception {
Message message = mock(Message.class);
when(replyToParser.getRecipientsToReplyTo(message, account)).thenReturn(TO_ADDRESSES);
recipientPresenter.initFromReplyToMessage(message, false);
verify(composePgpInlineDecider).shouldReplyInline(message);
}
@Test
public void getCurrentCryptoStatus_withoutCryptoProvider() throws Exception {
when(openPgpApiManager.getOpenPgpProviderState()).thenReturn(OpenPgpProviderState.UNCONFIGURED);
recipientPresenter.asyncUpdateCryptoStatus();
ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus();
assertEquals(CryptoStatusDisplayType.UNCONFIGURED, status.getDisplayType());
assertEquals(CryptoSpecialModeDisplayType.NONE, status.getSpecialModeDisplayType());
assertNull(status.getAttachErrorStateOrNull());
assertFalse(status.isProviderStateOk());
assertFalse(status.isOpenPgpConfigured());
}
@Test
public void getCurrentCryptoStatus_withCryptoProvider() throws Exception {
setupCryptoProvider(noRecipientsAutocryptResult);
ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus();
assertEquals(CryptoStatusDisplayType.UNAVAILABLE, status.getDisplayType());
assertTrue(status.isProviderStateOk());
assertTrue(status.isOpenPgpConfigured());
}
@Test
public void getCurrentCryptoStatus_withOpportunistic() throws Exception {
RecipientAutocryptStatus recipientAutocryptStatus = new RecipientAutocryptStatus(
RecipientAutocryptStatusType.AVAILABLE_UNCONFIRMED, null);
setupCryptoProvider(recipientAutocryptStatus);
ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus();
assertEquals(CryptoStatusDisplayType.AVAILABLE, status.getDisplayType());
assertTrue(status.isProviderStateOk());
assertTrue(status.isOpenPgpConfigured());
}
@Test
public void getCurrentCryptoStatus_withOpportunistic__confirmed() throws Exception {
RecipientAutocryptStatus recipientAutocryptStatus = new RecipientAutocryptStatus(
RecipientAutocryptStatusType.AVAILABLE_CONFIRMED, null);
setupCryptoProvider(recipientAutocryptStatus);
ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus();
assertEquals(CryptoStatusDisplayType.AVAILABLE, status.getDisplayType());
assertTrue(status.isProviderStateOk());
assertTrue(status.isOpenPgpConfigured());
}
@Test
public void getCurrentCryptoStatus_withOpportunistic__missingKeys() throws Exception {
RecipientAutocryptStatus recipientAutocryptStatus = new RecipientAutocryptStatus(
RecipientAutocryptStatusType.UNAVAILABLE, null);
setupCryptoProvider(recipientAutocryptStatus);
ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus();
assertEquals(CryptoStatusDisplayType.UNAVAILABLE, status.getDisplayType());
assertTrue(status.isProviderStateOk());
assertTrue(status.isOpenPgpConfigured());
}
@Test
public void getCurrentCryptoStatus_withOpportunistic__privateMissingKeys() throws Exception {
RecipientAutocryptStatus recipientAutocryptStatus = new RecipientAutocryptStatus(
RecipientAutocryptStatusType.UNAVAILABLE, null);
setupCryptoProvider(recipientAutocryptStatus);
recipientPresenter.onCryptoModeChanged(CryptoMode.CHOICE_ENABLED);
runBackgroundTask();
ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus();
assertEquals(CryptoStatusDisplayType.ENABLED_ERROR, status.getDisplayType());
assertTrue(status.isProviderStateOk());
assertTrue(status.isOpenPgpConfigured());
}
@Test
public void getCurrentCryptoStatus_withModeDisabled() throws Exception {
RecipientAutocryptStatus recipientAutocryptStatus = new RecipientAutocryptStatus(
RecipientAutocryptStatusType.AVAILABLE_UNCONFIRMED, null);
setupCryptoProvider(recipientAutocryptStatus);
recipientPresenter.onCryptoModeChanged(CryptoMode.CHOICE_DISABLED);
runBackgroundTask();
ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus();
assertEquals(CryptoStatusDisplayType.AVAILABLE, status.getDisplayType());
assertTrue(status.isProviderStateOk());
assertTrue(status.isOpenPgpConfigured());
}
@Test
public void getCurrentCryptoStatus_withModePrivate() throws Exception {
RecipientAutocryptStatus recipientAutocryptStatus = new RecipientAutocryptStatus(
RecipientAutocryptStatusType.AVAILABLE_UNCONFIRMED, null);
setupCryptoProvider(recipientAutocryptStatus);
recipientPresenter.onCryptoModeChanged(CryptoMode.CHOICE_ENABLED);
runBackgroundTask();
ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus();
assertEquals(CryptoStatusDisplayType.ENABLED, status.getDisplayType());
assertTrue(status.isProviderStateOk());
assertTrue(status.isOpenPgpConfigured());
}
@Test
public void getCurrentCryptoStatus_withModeSignOnly() throws Exception {
setupCryptoProvider(noRecipientsAutocryptResult);
recipientPresenter.onMenuSetSignOnly(true);
runBackgroundTask();
ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus();
assertEquals(CryptoStatusDisplayType.SIGN_ONLY, status.getDisplayType());
assertTrue(status.isProviderStateOk());
assertTrue(status.isSigningEnabled());
assertTrue(status.isSignOnly());
}
@Test
public void getCurrentCryptoStatus_withModeInline() throws Exception {
setupCryptoProvider(noRecipientsAutocryptResult);
recipientPresenter.onMenuSetPgpInline(true);
runBackgroundTask();
ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus();
assertEquals(CryptoStatusDisplayType.UNAVAILABLE, status.getDisplayType());
assertTrue(status.isProviderStateOk());
assertTrue(status.isPgpInlineModeEnabled());
}
private void runBackgroundTask() {
boolean taskRun = Robolectric.getBackgroundThreadScheduler().runOneTask();
assertTrue(taskRun);
}
private void setupCryptoProvider(RecipientAutocryptStatus autocryptStatusResult) throws Exception {
Account account = mock(Account.class);
OpenPgpApi openPgpApi = mock(OpenPgpApi.class);
when(account.getOpenPgpProvider()).thenReturn(CRYPTO_PROVIDER);
when(account.isOpenPgpProviderConfigured()).thenReturn(true);
when(account.getOpenPgpKey()).thenReturn(CRYPTO_KEY_ID);
recipientPresenter.onSwitchAccount(account);
when(openPgpApiManager.getOpenPgpProviderState()).thenReturn(OpenPgpProviderState.OK);
when(openPgpApiManager.getOpenPgpApi()).thenReturn(openPgpApi);
when(autocryptStatusInteractor.retrieveCryptoProviderRecipientStatus(
any(OpenPgpApi.class), any(String[].class))).thenReturn(autocryptStatusResult);
openPgpApiManagerCallback.onOpenPgpProviderStatusChanged();
runBackgroundTask();
}
}

View file

@ -0,0 +1,307 @@
package com.fsck.k9.activity.compose
import androidx.test.core.app.ApplicationProvider
import com.fsck.k9.Account
import com.fsck.k9.K9RobolectricTest
import com.fsck.k9.activity.compose.RecipientMvpView.CryptoSpecialModeDisplayType
import com.fsck.k9.activity.compose.RecipientMvpView.CryptoStatusDisplayType
import com.fsck.k9.activity.compose.RecipientPresenter.CryptoMode
import com.fsck.k9.autocrypt.AutocryptDraftStateHeaderParser
import com.fsck.k9.helper.ReplyToParser
import com.fsck.k9.helper.ReplyToParser.ReplyToAddresses
import com.fsck.k9.mail.Address
import com.fsck.k9.mail.Message
import com.fsck.k9.mail.Message.RecipientType
import com.fsck.k9.message.AutocryptStatusInteractor
import com.fsck.k9.message.AutocryptStatusInteractor.RecipientAutocryptStatus
import com.fsck.k9.message.AutocryptStatusInteractor.RecipientAutocryptStatusType
import com.fsck.k9.message.ComposePgpEnableByDefaultDecider
import com.fsck.k9.message.ComposePgpInlineDecider
import com.fsck.k9.view.RecipientSelectView.Recipient
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertNotNull
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.koin.test.inject
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.verify
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.stubbing
import org.openintents.openpgp.OpenPgpApiManager
import org.openintents.openpgp.OpenPgpApiManager.OpenPgpApiManagerCallback
import org.openintents.openpgp.OpenPgpApiManager.OpenPgpProviderState
import org.openintents.openpgp.util.OpenPgpApi
import org.robolectric.Robolectric
import org.robolectric.annotation.LooperMode
private val TO_ADDRESS = Address("to@domain.example")
private val CC_ADDRESS = Address("cc@domain.example")
private const val CRYPTO_PROVIDER = "crypto_provider"
private const val CRYPTO_KEY_ID = 123L
@LooperMode(LooperMode.Mode.LEGACY)
class RecipientPresenterTest : K9RobolectricTest() {
private val openPgpApiManager = mock<OpenPgpApiManager> {
on { openPgpProviderState } doReturn OpenPgpProviderState.UNCONFIGURED
on { setOpenPgpProvider(any(), any()) } doAnswer { invocation ->
openPgpApiManagerCallback = invocation.getArgument(1)
}
}
private val recipientMvpView = mock<RecipientMvpView>()
private val account = mock<Account>()
private val composePgpInlineDecider = mock<ComposePgpInlineDecider>()
private val composePgpEnableByDefaultDecider = mock<ComposePgpEnableByDefaultDecider>()
private val autocryptStatusInteractor = mock<AutocryptStatusInteractor>()
private val replyToParser = mock<ReplyToParser>()
private val autocryptDraftStateHeaderParser: AutocryptDraftStateHeaderParser by inject()
private lateinit var recipientPresenter: RecipientPresenter
private val noRecipientsAutocryptResult = RecipientAutocryptStatus(RecipientAutocryptStatusType.NO_RECIPIENTS, null)
private var openPgpApiManagerCallback: OpenPgpApiManagerCallback? = null
@Before
fun setUp() {
Robolectric.getBackgroundThreadScheduler().pause()
recipientPresenter = RecipientPresenter(
ApplicationProvider.getApplicationContext(),
mock(),
openPgpApiManager,
recipientMvpView,
account,
composePgpInlineDecider,
composePgpEnableByDefaultDecider,
autocryptStatusInteractor,
replyToParser,
autocryptDraftStateHeaderParser
)
}
@Test
@Ignore("It looks like the support version of AsyncTaskLoader handles background tasks differently")
fun testInitFromReplyToMessage() {
val message = mock<Message>()
stubbing(replyToParser) {
on { getRecipientsToReplyTo(message, account) } doReturn ReplyToAddresses(arrayOf(TO_ADDRESS))
}
recipientPresenter.initFromReplyToMessage(message, false)
runBackgroundTask()
verify(recipientMvpView).addRecipients(eq(RecipientType.TO), eq(Recipient(TO_ADDRESS)))
}
@Test
@Ignore("It looks like the support version of AsyncTaskLoader handles background tasks differently")
fun testInitFromReplyToAllMessage() {
val message = mock<Message>()
val replyToAddresses = ReplyToAddresses(listOf(TO_ADDRESS), listOf(CC_ADDRESS))
stubbing(replyToParser) {
on { getRecipientsToReplyAllTo(message, account) } doReturn replyToAddresses
}
recipientPresenter.initFromReplyToMessage(message, true)
runBackgroundTask()
runBackgroundTask()
verify(recipientMvpView).addRecipients(eq(RecipientType.TO), eq(Recipient(TO_ADDRESS)))
verify(recipientMvpView).addRecipients(eq(RecipientType.CC), eq(Recipient(CC_ADDRESS)))
}
@Test
fun initFromReplyToMessage_shouldCallComposePgpInlineDecider() {
val message = mock<Message>()
stubbing(replyToParser) {
on { getRecipientsToReplyTo(message, account) } doReturn ReplyToAddresses(arrayOf(TO_ADDRESS))
}
recipientPresenter.initFromReplyToMessage(message, false)
verify(composePgpInlineDecider).shouldReplyInline(message)
}
@Test
fun getCurrentCryptoStatus_withoutCryptoProvider() {
stubbing(openPgpApiManager) {
on { openPgpProviderState } doReturn OpenPgpProviderState.UNCONFIGURED
}
recipientPresenter.asyncUpdateCryptoStatus()
assertNotNull(recipientPresenter.currentCachedCryptoStatus) { status ->
assertThat(status.displayType).isEqualTo(CryptoStatusDisplayType.UNCONFIGURED)
assertThat(status.specialModeDisplayType).isEqualTo(CryptoSpecialModeDisplayType.NONE)
assertThat(status.attachErrorStateOrNull).isNull()
assertThat(status.isProviderStateOk()).isFalse()
assertThat(status.isOpenPgpConfigured).isFalse()
}
}
@Test
fun getCurrentCryptoStatus_withCryptoProvider() {
setupCryptoProvider(noRecipientsAutocryptResult)
assertNotNull(recipientPresenter.currentCachedCryptoStatus) { status ->
assertThat(status.displayType).isEqualTo(CryptoStatusDisplayType.UNAVAILABLE)
assertThat(status.isProviderStateOk()).isTrue()
assertThat(status.isOpenPgpConfigured).isTrue()
}
}
@Test
fun getCurrentCryptoStatus_withOpportunistic() {
val recipientAutocryptStatus = RecipientAutocryptStatus(
RecipientAutocryptStatusType.AVAILABLE_UNCONFIRMED, null
)
setupCryptoProvider(recipientAutocryptStatus)
assertNotNull(recipientPresenter.currentCachedCryptoStatus) { status ->
assertThat(status.displayType).isEqualTo(CryptoStatusDisplayType.AVAILABLE)
assertThat(status.isProviderStateOk()).isTrue()
assertThat(status.isOpenPgpConfigured).isTrue()
}
}
@Test
fun getCurrentCryptoStatus_withOpportunistic__confirmed() {
val recipientAutocryptStatus = RecipientAutocryptStatus(
RecipientAutocryptStatusType.AVAILABLE_CONFIRMED, null
)
setupCryptoProvider(recipientAutocryptStatus)
assertNotNull(recipientPresenter.currentCachedCryptoStatus) { status ->
assertThat(status.displayType).isEqualTo(CryptoStatusDisplayType.AVAILABLE)
assertThat(status.isProviderStateOk()).isTrue()
assertThat(status.isOpenPgpConfigured).isTrue()
}
}
@Test
fun getCurrentCryptoStatus_withOpportunistic__missingKeys() {
val recipientAutocryptStatus = RecipientAutocryptStatus(
RecipientAutocryptStatusType.UNAVAILABLE, null
)
setupCryptoProvider(recipientAutocryptStatus)
assertNotNull(recipientPresenter.currentCachedCryptoStatus) { status ->
assertThat(status.displayType).isEqualTo(CryptoStatusDisplayType.UNAVAILABLE)
assertThat(status.isProviderStateOk()).isTrue()
assertThat(status.isOpenPgpConfigured).isTrue()
}
}
@Test
fun getCurrentCryptoStatus_withOpportunistic__privateMissingKeys() {
val recipientAutocryptStatus = RecipientAutocryptStatus(
RecipientAutocryptStatusType.UNAVAILABLE, null
)
setupCryptoProvider(recipientAutocryptStatus)
recipientPresenter.onCryptoModeChanged(CryptoMode.CHOICE_ENABLED)
runBackgroundTask()
assertNotNull(recipientPresenter.currentCachedCryptoStatus) { status ->
assertThat(status.displayType).isEqualTo(CryptoStatusDisplayType.ENABLED_ERROR)
assertThat(status.isProviderStateOk()).isTrue()
assertThat(status.isOpenPgpConfigured).isTrue()
}
}
@Test
fun getCurrentCryptoStatus_withModeDisabled() {
val recipientAutocryptStatus = RecipientAutocryptStatus(
RecipientAutocryptStatusType.AVAILABLE_UNCONFIRMED, null
)
setupCryptoProvider(recipientAutocryptStatus)
recipientPresenter.onCryptoModeChanged(CryptoMode.CHOICE_DISABLED)
runBackgroundTask()
assertNotNull(recipientPresenter.currentCachedCryptoStatus) { status ->
assertThat(status.displayType).isEqualTo(CryptoStatusDisplayType.AVAILABLE)
assertThat(status.isProviderStateOk()).isTrue()
assertThat(status.isOpenPgpConfigured).isTrue()
}
}
@Test
fun getCurrentCryptoStatus_withModePrivate() {
val recipientAutocryptStatus = RecipientAutocryptStatus(
RecipientAutocryptStatusType.AVAILABLE_UNCONFIRMED, null
)
setupCryptoProvider(recipientAutocryptStatus)
recipientPresenter.onCryptoModeChanged(CryptoMode.CHOICE_ENABLED)
runBackgroundTask()
assertNotNull(recipientPresenter.currentCachedCryptoStatus) { status ->
assertThat(status.displayType).isEqualTo(CryptoStatusDisplayType.ENABLED)
assertThat(status.isProviderStateOk()).isTrue()
assertThat(status.isOpenPgpConfigured).isTrue()
}
}
@Test
fun getCurrentCryptoStatus_withModeSignOnly() {
setupCryptoProvider(noRecipientsAutocryptResult)
recipientPresenter.onMenuSetSignOnly(true)
runBackgroundTask()
assertNotNull(recipientPresenter.currentCachedCryptoStatus) { status ->
assertThat(status.displayType).isEqualTo(CryptoStatusDisplayType.SIGN_ONLY)
assertThat(status.isProviderStateOk()).isTrue()
assertThat(status.isOpenPgpConfigured).isTrue()
assertThat(status.isSignOnly).isTrue()
}
}
@Test
fun getCurrentCryptoStatus_withModeInline() {
setupCryptoProvider(noRecipientsAutocryptResult)
recipientPresenter.onMenuSetPgpInline(true)
runBackgroundTask()
assertNotNull(recipientPresenter.currentCachedCryptoStatus) { status ->
assertThat(status.displayType).isEqualTo(CryptoStatusDisplayType.UNAVAILABLE)
assertThat(status.isProviderStateOk()).isTrue()
assertThat(status.isPgpInlineModeEnabled).isTrue()
}
}
private fun runBackgroundTask() {
assertThat(Robolectric.getBackgroundThreadScheduler().runOneTask()).isTrue()
}
private fun setupCryptoProvider(autocryptStatusResult: RecipientAutocryptStatus) {
stubbing(account) {
on { openPgpProvider } doReturn CRYPTO_PROVIDER
on { isOpenPgpProviderConfigured } doReturn true
on { openPgpKey } doReturn CRYPTO_KEY_ID
}
recipientPresenter.onSwitchAccount(account)
val openPgpApiMock = mock<OpenPgpApi>()
stubbing(autocryptStatusInteractor) {
on { retrieveCryptoProviderRecipientStatus(eq(openPgpApiMock), any()) } doReturn autocryptStatusResult
}
stubbing(openPgpApiManager) {
on { openPgpApi } doReturn openPgpApiMock
on { openPgpProviderState } doReturn OpenPgpProviderState.OK
}
openPgpApiManagerCallback!!.onOpenPgpProviderStatusChanged()
runBackgroundTask()
}
}