compose: improve handling if crypto provider is not available

This commit is contained in:
Vincent Breitmoser 2016-02-05 23:49:22 +01:00
parent 7b0c94f6bb
commit eac65df806
8 changed files with 225 additions and 71 deletions

View file

@ -109,8 +109,11 @@ import org.htmlcleaner.CleanerProperties;
import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.SimpleHtmlSerializer;
import org.htmlcleaner.TagNode;
import org.openintents.openpgp.IOpenPgpService2;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpServiceConnection;
import org.openintents.openpgp.util.OpenPgpServiceConnection.OnBound;
@SuppressWarnings("deprecation")
public class MessageCompose extends K9Activity implements OnClickListener,
@ -695,7 +698,17 @@ public class MessageCompose extends K9Activity implements OnClickListener,
// attachKeyCheckBox = (CheckBox) findViewById(R.id.cb_attach_key);
// attachKeyCheckBox.setEnabled(mAccount.getCryptoKey() != 0);
mOpenPgpServiceConnection = new OpenPgpServiceConnection(this, mOpenPgpProvider);
mOpenPgpServiceConnection = new OpenPgpServiceConnection(this, mOpenPgpProvider, new OnBound() {
@Override
public void onBound(IOpenPgpService2 service) {
recipientPresenter.onCryptoProviderBound();
}
@Override
public void onError(Exception e) {
recipientPresenter.onCryptoProviderError(e);
}
});
mOpenPgpServiceConnection.bindToService();
updateMessageFormat();
@ -1320,16 +1333,17 @@ public class MessageCompose extends K9Activity implements OnClickListener,
return;
}
if (resultCode != RESULT_OK) {
return;
}
if (data == null) {
if ((requestCode & REQUEST_MASK_RECIPIENT_PRESENTER) == REQUEST_MASK_RECIPIENT_PRESENTER) {
requestCode ^= REQUEST_MASK_RECIPIENT_PRESENTER;
recipientPresenter.onActivityResult(resultCode, requestCode, data);
return;
}
if ((requestCode & REQUEST_MASK_RECIPIENT_PRESENTER) == REQUEST_MASK_RECIPIENT_PRESENTER) {
requestCode ^= REQUEST_MASK_RECIPIENT_PRESENTER;
recipientPresenter.onActivityResult(requestCode, data);
if (resultCode != RESULT_OK) {
return;
}
if (data == null) {
return;
}
@ -3015,4 +3029,12 @@ public class MessageCompose extends K9Activity implements OnClickListener,
}
}
public void launchUserInteractionPendingIntent(PendingIntent pendingIntent, int requestCode) {
requestCode |= REQUEST_MASK_RECIPIENT_PRESENTER;
try {
startIntentSenderForResult(pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0);
} catch (SendIntentException e) {
e.printStackTrace();
}
}
}

View file

@ -65,14 +65,23 @@ public class ComposeCryptoStatus {
return CryptoStatusDisplayType.OPPORTUNISTIC_NOKEY;
case SIGN_ONLY:
return CryptoStatusDisplayType.SIGN_ONLY;
default:
case DISABLE:
return CryptoStatusDisplayType.DISABLED;
case ERROR:
return CryptoStatusDisplayType.ERROR;
default:
case UNINITIALIZED:
return CryptoStatusDisplayType.UNINITIALIZED;
}
}
public boolean isPgpErrorState() {
return cryptoMode == CryptoMode.ERROR;
}
public boolean shouldUsePgpMessageBuilder() {
return cryptoMode != CryptoMode.DISABLE;
return cryptoMode == CryptoMode.PRIVATE || cryptoMode == CryptoMode.OPPORTUNISTIC
|| cryptoMode == CryptoMode.SIGN_ONLY;
}
public boolean isEncryptionEnabled() {
@ -129,7 +138,7 @@ public class ComposeCryptoStatus {
ArrayList<String> keyReferences = new ArrayList<>();
boolean allKeysAvailable = true;
boolean allKeysVerified = true;
boolean allKeysVerified = !recipients.isEmpty();
for (Recipient recipient : recipients) {
RecipientCryptoStatus cryptoStatus = recipient.getCryptoStatus();
if (cryptoStatus.isAvailable()) {

View file

@ -255,9 +255,14 @@ public class RecipientLoader extends AsyncTaskLoader<List<Recipient>> {
List<String> recipientList = new ArrayList<>(recipientMap.keySet());
String[] recipientAddresses = recipientList.toArray(new String[recipientList.size()]);
Cursor cursor;
Uri queryUri = Uri.parse("content://" + cryptoProvider + ".provider.exported/email_status");
Cursor cursor = getContext().getContentResolver().query(queryUri, PROJECTION_CRYPTO_STATUS, null,
try {
cursor = getContext().getContentResolver().query(queryUri, PROJECTION_CRYPTO_STATUS, null,
recipientAddresses, null);
} catch (SecurityException e) {
return;
}
initializeCryptoStatusForAllRecipients(recipientMap);

View file

@ -6,6 +6,7 @@ import java.util.List;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.app.PendingIntent;
import android.text.TextWatcher;
import android.view.View;
import android.view.View.OnClickListener;
@ -25,11 +26,13 @@ import com.fsck.k9.view.RecipientSelectView.TokenListener;
public class RecipientMvpView implements OnFocusChangeListener, OnClickListener {
private static final int VIEW_INDEX_HIDDEN = -1;
private static final int VIEW_INDEX_CRYPTO_STATUS_DISABLED = 0;
private static final int VIEW_INDEX_CRYPTO_STATUS_NO_KEY = 1;
private static final int VIEW_INDEX_CRYPTO_STATUS_UNTRUSTED = 2;
private static final int VIEW_INDEX_CRYPTO_STATUS_TRUSTED = 3;
private static final int VIEW_INDEX_CRYPTO_STATUS_SIGN_ONLY = 4;
private static final int VIEW_INDEX_CRYPTO_STATUS_ERROR = 1;
private static final int VIEW_INDEX_CRYPTO_STATUS_NO_KEY = 2;
private static final int VIEW_INDEX_CRYPTO_STATUS_UNTRUSTED = 3;
private static final int VIEW_INDEX_CRYPTO_STATUS_TRUSTED = 4;
private static final int VIEW_INDEX_CRYPTO_STATUS_SIGN_ONLY = 5;
private static final int VIEW_INDEX_BCC_EXPANDER_VISIBLE = 0;
private static final int VIEW_INDEX_BCC_EXPANDER_HIDDEN = 1;
@ -223,20 +226,6 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener
return bccView.getObjects();
}
public void hideCryptoStatus() {
if (cryptoStatusView.getVisibility() == View.GONE) {
return;
}
cryptoStatusView.animate().translationX(100).setDuration(300).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
cryptoStatusView.setVisibility(View.GONE);
}
}).start();
}
public boolean recipientToHasUncompletedText() {
return toView.hasUncompletedText();
}
@ -261,17 +250,16 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener
bccView.setError(bccView.getContext().getString(R.string.compose_error_incomplete_recipient));
}
public void showMissingSignKeyError() {
Toast.makeText(activity, R.string.compose_error_no_signing_key, Toast.LENGTH_LONG).show();
public void showCryptoStatus(final CryptoStatusDisplayType cryptoStatusDisplayType) {
boolean shouldBeHidden = cryptoStatusDisplayType.childToDisplay == VIEW_INDEX_HIDDEN;
if (shouldBeHidden) {
hideCryptoStatus();
return;
}
public void showPrivateAndIncompleteError() {
Toast.makeText(activity, R.string.compose_error_private_missing_keys, Toast.LENGTH_LONG).show();
}
public void showCryptoStatus(final CryptoStatusDisplayType childToDisplay) {
if (cryptoStatusView.getVisibility() == View.VISIBLE) {
switchCryptoStatus(childToDisplay);
boolean alreadyVisible = cryptoStatusView.getVisibility() == View.VISIBLE;
if (alreadyVisible) {
switchCryptoStatus(cryptoStatusDisplayType);
return;
}
@ -280,7 +268,22 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener
cryptoStatusView.animate().translationX(0).setDuration(300).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
switchCryptoStatus(childToDisplay);
switchCryptoStatus(cryptoStatusDisplayType);
}
}).start();
}
private void hideCryptoStatus() {
if (cryptoStatusView.getVisibility() == View.GONE) {
return;
}
cryptoStatusView.animate().translationX(100).setDuration(300).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
cryptoStatusView.setVisibility(View.GONE);
}
}).start();
}
@ -297,8 +300,23 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener
}
public void showErrorContactNoAddress() {
String errorMessage = activity.getString(R.string.error_contact_address_not_found);
Toast.makeText(activity, errorMessage, Toast.LENGTH_LONG).show();
Toast.makeText(activity, R.string.error_contact_address_not_found, 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 showErrorMissingSignKey() {
Toast.makeText(activity, R.string.compose_error_no_signing_key, Toast.LENGTH_LONG).show();
}
public void showErrorPrivateButMissingKeys() {
Toast.makeText(activity, R.string.compose_error_private_missing_keys, Toast.LENGTH_LONG).show();
}
@Override
@ -342,7 +360,16 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener
dialog.show(activity.getFragmentManager(), "crypto_settings");
}
public void checkOpenPgpServicePermission(RecipientPresenter recipientPresenter) {
activity.getOpenPgpApi().checkPermissionPing(recipientPresenter);
}
public void launchUserInteractionPendingIntent(PendingIntent pendingIntent, int requestCode) {
activity.launchUserInteractionPendingIntent(pendingIntent, requestCode);
}
public enum CryptoStatusDisplayType {
UNINITIALIZED(VIEW_INDEX_HIDDEN),
DISABLED(VIEW_INDEX_CRYPTO_STATUS_DISABLED),
SIGN_ONLY(VIEW_INDEX_CRYPTO_STATUS_SIGN_ONLY),
OPPORTUNISTIC_NOKEY(VIEW_INDEX_CRYPTO_STATUS_NO_KEY),
@ -350,7 +377,8 @@ public class RecipientMvpView implements OnFocusChangeListener, OnClickListener
OPPORTUNISTIC_TRUSTED(VIEW_INDEX_CRYPTO_STATUS_TRUSTED),
PRIVATE_NOKEY(VIEW_INDEX_CRYPTO_STATUS_NO_KEY),
PRIVATE_UNTRUSTED(VIEW_INDEX_CRYPTO_STATUS_UNTRUSTED),
PRIVATE_TRUSTED(VIEW_INDEX_CRYPTO_STATUS_TRUSTED);
PRIVATE_TRUSTED(VIEW_INDEX_CRYPTO_STATUS_TRUSTED),
ERROR(VIEW_INDEX_CRYPTO_STATUS_ERROR);
final int childToDisplay;

View file

@ -6,17 +6,22 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
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.Bundle;
import android.util.Log;
import android.view.Menu;
import com.fsck.k9.Account;
import com.fsck.k9.Identity;
import com.fsck.k9.K9;
import com.fsck.k9.R;
import com.fsck.k9.activity.MessageCompose;
import com.fsck.k9.activity.compose.ComposeCryptoStatus.ComposeCryptoStatusBuilder;
import com.fsck.k9.helper.Contacts;
import com.fsck.k9.helper.MailTo;
@ -27,15 +32,18 @@ import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.view.RecipientSelectView.Recipient;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpApi.PermissionPingCallback;
public class RecipientPresenter {
public class RecipientPresenter implements PermissionPingCallback {
private static final String STATE_KEY_CC_SHOWN = "state:ccShown";
private static final String STATE_KEY_BCC_SHOWN = "state:bccShown";
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 final Context context;
@ -44,8 +52,9 @@ public class RecipientPresenter {
private String cryptoProvider;
private RecipientType lastFocusedType = RecipientType.TO;
private Boolean hasContactPicker;
private CryptoMode currentCryptoMode = CryptoMode.OPPORTUNISTIC;
private CryptoMode currentCryptoMode = CryptoMode.UNINITIALIZED;
private ComposeCryptoStatus currentCryptoStatus;
private PendingIntent pendingUserInteractionIntent;
public RecipientPresenter(Context context, RecipientMvpView recipientMvpView, Account account) {
@ -301,8 +310,6 @@ public class RecipientPresenter {
}
public void updateCryptoStatus() {
List<Recipient> recipients = getAllRecipients();
ComposeCryptoStatusBuilder builder = new ComposeCryptoStatusBuilder()
.setCryptoMode(currentCryptoMode)
.setRecipients(getAllRecipients());
@ -315,13 +322,6 @@ public class RecipientPresenter {
}
currentCryptoStatus = builder.build();
boolean hasNoCryptoProviderOrRecipients = cryptoProvider == null || recipients.isEmpty();
if (hasNoCryptoProviderOrRecipients) {
recipientMvpView.hideCryptoStatus();
return;
}
recipientMvpView.showCryptoStatus(currentCryptoStatus.getCryptoStatusDisplayType());
}
@ -438,13 +438,21 @@ public class RecipientPresenter {
recipientMvpView.showContactPicker(requestCode);
}
public void onActivityResult(int requestCode, Intent data) {
if (requestCode != CONTACT_PICKER_TO && requestCode != CONTACT_PICKER_CC && requestCode != CONTACT_PICKER_BCC) {
public void onActivityResult(int resultCode, int requestCode, 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:
recipientMvpView.checkOpenPgpServicePermission(this);
break;
}
}
private static int recipientTypeToRequestCode(RecipientType type) {
@ -482,6 +490,16 @@ public class RecipientPresenter {
}
public void onClickCryptoStatus() {
if (currentCryptoMode == CryptoMode.ERROR) {
if (pendingUserInteractionIntent != null) {
recipientMvpView
.launchUserInteractionPendingIntent(pendingUserInteractionIntent, OPENPGP_USER_INTERACTION);
pendingUserInteractionIntent = null;
} else {
recipientMvpView.checkOpenPgpServicePermission(this);
}
return;
}
recipientMvpView.showCryptoDialog(currentCryptoMode);
}
@ -510,22 +528,67 @@ public class RecipientPresenter {
}
ComposeCryptoStatus cryptoStatus = getCurrentCryptoStatus();
if (cryptoStatus.isPgpErrorState()) {
// TODO: be more specific about this error
recipientMvpView.showErrorOpenPgpConnection();
return false;
}
if (cryptoStatus.isMissingSignKey()) {
recipientMvpView.showMissingSignKeyError();
recipientMvpView.showErrorMissingSignKey();
return false;
}
if (cryptoStatus.isPrivateAndIncomplete()) {
recipientMvpView.showPrivateAndIncompleteError();
recipientMvpView.showErrorPrivateButMissingKeys();
return false;
}
return true;
}
public void onCryptoProviderBound() {
recipientMvpView.checkOpenPgpServicePermission(this);
}
public void onCryptoProviderError(Exception e) {
// TODO handle error case better
recipientMvpView.showErrorOpenPgpConnection();
currentCryptoMode = CryptoMode.ERROR;
Log.e(K9.LOG_TAG, "error connecting to crypto provider!", e);
updateCryptoStatus();
}
@Override
public void onPgpPermissionCheckResult(Intent result) {
int resultCode = result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR);
switch (resultCode) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
if (currentCryptoMode == CryptoMode.UNINITIALIZED || currentCryptoMode == CryptoMode.ERROR) {
// TODO initialize to other values under some circumstances, e.g. if we reply to an encrypted e-mail
currentCryptoMode = CryptoMode.OPPORTUNISTIC;
}
break;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
recipientMvpView.showErrorOpenPgpUserInteractionRequired();
pendingUserInteractionIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
currentCryptoMode = CryptoMode.ERROR;
break;
case OpenPgpApi.RESULT_CODE_ERROR:
default:
recipientMvpView.showErrorOpenPgpConnection();
currentCryptoMode = CryptoMode.ERROR;
break;
}
updateCryptoStatus();
}
public enum CryptoMode {
PRIVATE,
OPPORTUNISTIC,
SIGN_ONLY,
UNINITIALIZED,
DISABLE,
SIGN_ONLY,
OPPORTUNISTIC,
PRIVATE,
ERROR,
}
}

View file

@ -62,6 +62,14 @@
android:tint="@color/openpgp_grey"
/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/status_lock_error"
android:tint="@color/openpgp_red"
/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -74,7 +82,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/status_lock_error"
android:src="@drawable/status_lock_opportunistic"
android:tint="@color/openpgp_orange"
/>

View file

@ -1133,11 +1133,15 @@ Please submit bug reports, contribute new features and ask questions at
<string name="address_type_mobile">Mobile</string>
<string name="warn_search_disabled">Search in message bodies is disabled in this development version!</string>
<string name="compose_error_no_draft_folder">No Drafts folder configured for this account!</string>
<string name="compose_error_no_signing_key">Signing requested, but no key selected!</string>
<string name="compose_error_private_missing_keys">Private mode enabled, but cannot encrypt to all recipients!</string>
<string name="compose_not_saving_unencrypted">Draft NOT saved without encryption!</string>
<string name="compose_error_pgp_error">Error with OpenPGP provider. Check your settings!</string>
<string name="compose_error_no_signing_key">No key configured for signing! Please check your settings.</string>
<string name="compose_error_private_missing_keys">Private mode is enabled, but some recipients do not have keys!</string>
<string name="crypto_mode_disabled">Never Sign or Encrypt.</string>
<string name="crypto_mode_sign_only">Always Sign, Never Encrypt.</string>
<string name="crypto_mode_opportunistic">Always Sign, Encrypt if possible.</string>
<string name="crypto_mode_private">Always Sign, Always Encrypt.</string>
<string name="error_crypto_provider_connect">Cannot connect to crypto provider, check your settings or click crypto icon to retry!</string>
<string name="error_crypto_provider_ui_required">Crypto provider access denied, click crypto icon to retry!</string>
</resources>

View file

@ -57,6 +57,8 @@ public class OpenPgpApi {
* PendingIntent RESULT_INTENT (if RESULT_CODE == RESULT_CODE_USER_INTERACTION_REQUIRED)
*/
public static final String ACTION_CHECK_PERMISSION = "org.openintents.openpgp.action.CHECK_PERMISSION";
/**
* DEPRECATED
* Same as ACTION_CLEARTEXT_SIGN
@ -203,7 +205,6 @@ public class OpenPgpApi {
*/
public static final String ACTION_GET_KEY = "org.openintents.openpgp.action.GET_KEY";
/* Intent extras */
public static final String EXTRA_API_VERSION = "api_version";
@ -418,4 +419,18 @@ public class OpenPgpApi {
}
}
public interface PermissionPingCallback {
void onPgpPermissionCheckResult(Intent result);
}
public void checkPermissionPing(final PermissionPingCallback permissionPingCallback) {
Intent intent = new Intent(OpenPgpApi.ACTION_CHECK_PERMISSION);
executeApiAsync(intent, null, null, new IOpenPgpCallback() {
@Override
public void onReturn(Intent result) {
permissionPingCallback.onPgpPermissionCheckResult(result);
}
});
}
}