Merge pull request #2220 from k9mail/no-provider

Handle unconfigured OpenPGP provider in MessageView
This commit is contained in:
cketti 2017-02-20 00:43:18 +01:00 committed by GitHub
commit e70295efec
15 changed files with 425 additions and 12 deletions

View file

@ -97,6 +97,12 @@
android:configChanges="locale"
android:label="@string/welcome_message_title"/>
<activity
android:name=".activity.setup.OpenPgpAppSelectDialog"
android:configChanges="locale"
android:theme="@style/Theme.K9.Transparent"
/>
<activity
android:name=".activity.setup.FontSizeSettings"
android:configChanges="locale"

View file

@ -126,7 +126,11 @@ public class MessageLoaderHelper {
public void asyncRestartMessageCryptoProcessing() {
cancelAndClearCryptoOperation();
cancelAndClearDecodeLoader();
startOrResumeCryptoOperation();
if (K9.isOpenPgpProviderConfigured()) {
startOrResumeCryptoOperation();
} else {
startOrResumeDecodeMessage();
}
}
/** Cancels all loading processes, prevents future callbacks, and destroys all loading state. */
@ -261,7 +265,8 @@ public class MessageLoaderHelper {
RetainFragment<MessageCryptoHelper> retainCryptoHelperFragment = getMessageCryptoHelperRetainFragment(true);
if (retainCryptoHelperFragment.hasData()) {
messageCryptoHelper = retainCryptoHelperFragment.getData();
} else {
}
if (messageCryptoHelper == null || messageCryptoHelper.isConfiguredForOutdatedCryptoProvider()) {
messageCryptoHelper = new MessageCryptoHelper(context);
retainCryptoHelperFragment.setData(messageCryptoHelper);
}

View file

@ -0,0 +1,276 @@
package com.fsck.k9.activity.setup;
import java.util.ArrayList;
import java.util.List;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;
import android.widget.TextView;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.preferences.StorageEditor;
import com.fsck.k9.ui.dialog.ApgDeprecationWarningDialog;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpAppPreference;
public class OpenPgpAppSelectDialog extends Activity {
private static final String OPENKEYCHAIN_PACKAGE = "org.sufficientlysecure.keychain";
private static final String APG_PROVIDER_PLACEHOLDER = "apg-placeholder";
private static final String PACKAGE_NAME_APG = "org.thialfihar.android.apg";
public static final String FRAG_OPENPGP_SELECT = "openpgp_select";
public static final String FRAG_APG_DEPRECATE = "apg_deprecate";
private static final String MARKET_INTENT_URI_BASE = "market://details?id=%s";
private static final Intent MARKET_INTENT = new Intent(Intent.ACTION_VIEW, Uri.parse(
String.format(MARKET_INTENT_URI_BASE, OPENKEYCHAIN_PACKAGE)));
private static final ArrayList<String> PROVIDER_BLACKLIST = new ArrayList<>();
static {
// Unfortunately, the current released version of APG includes a broken version of the API
PROVIDER_BLACKLIST.add(PACKAGE_NAME_APG);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme(K9.getK9Theme() == K9.Theme.LIGHT ?
R.style.Theme_K9_Dialog_Translucent_Light : R.style.Theme_K9_Dialog_Translucent_Dark);
if (savedInstanceState == null) {
showOpenPgpSelectDialogFragment();
}
}
private void showOpenPgpSelectDialogFragment() {
OpenPgpAppSelectFragment fragment = new OpenPgpAppSelectFragment();
fragment.show(getFragmentManager(), FRAG_OPENPGP_SELECT);
}
private void showApgDeprecationDialogFragment() {
ApgDeprecationDialogFragment fragment = new ApgDeprecationDialogFragment();
fragment.show(getFragmentManager(), FRAG_APG_DEPRECATE);
}
public static class OpenPgpAppSelectFragment extends DialogFragment {
private ArrayList<OpenPgpProviderEntry> openPgpProviderList = new ArrayList<>();
private String selectedPackage;
private void populateAppList() {
openPgpProviderList.clear();
Context context = getActivity();
OpenPgpProviderEntry noneEntry = new OpenPgpProviderEntry("",
context.getString(R.string.openpgp_list_preference_none),
getResources().getDrawable(R.drawable.ic_action_cancel_launchersize));
openPgpProviderList.add(0, noneEntry);
if (OpenPgpAppPreference.isApgInstalled(getActivity())) {
Drawable icon = getResources().getDrawable(R.drawable.ic_apg_small);
openPgpProviderList.add(new OpenPgpProviderEntry(
APG_PROVIDER_PLACEHOLDER, getString(R.string.apg), icon));
}
// search for OpenPGP providers...
Intent intent = new Intent(OpenPgpApi.SERVICE_INTENT_2);
List<ResolveInfo> resInfo = getActivity().getPackageManager().queryIntentServices(intent, 0);
boolean hasNonBlacklistedChoices = false;
if (resInfo != null) {
for (ResolveInfo resolveInfo : resInfo) {
if (resolveInfo.serviceInfo == null) {
continue;
}
String packageName = resolveInfo.serviceInfo.packageName;
String simpleName = String.valueOf(resolveInfo.serviceInfo.loadLabel(context.getPackageManager()));
Drawable icon = resolveInfo.serviceInfo.loadIcon(context.getPackageManager());
if (!PROVIDER_BLACKLIST.contains(packageName)) {
openPgpProviderList.add(new OpenPgpProviderEntry(packageName, simpleName, icon));
hasNonBlacklistedChoices = true;
}
}
}
if (!hasNonBlacklistedChoices) {
// add install links if provider list is empty
resInfo = context.getPackageManager().queryIntentActivities(MARKET_INTENT, 0);
for (ResolveInfo resolveInfo : resInfo) {
Intent marketIntent = new Intent(MARKET_INTENT);
marketIntent.setPackage(resolveInfo.activityInfo.packageName);
Drawable icon = resolveInfo.activityInfo.loadIcon(context.getPackageManager());
String marketName = String.valueOf(resolveInfo.activityInfo.applicationInfo
.loadLabel(context.getPackageManager()));
String simpleName = String.format(context.getString(R.string
.openpgp_install_openkeychain_via), marketName);
openPgpProviderList.add(new OpenPgpProviderEntry(OPENKEYCHAIN_PACKAGE, simpleName,
icon, marketIntent));
}
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
selectedPackage = K9.getOpenPgpProvider();
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.account_settings_crypto_app);
// do again, maybe an app has now been installed
populateAppList();
// Init ArrayAdapter with OpenPGP Providers
ListAdapter adapter = new ArrayAdapter<OpenPgpProviderEntry>(getActivity(),
android.R.layout.select_dialog_singlechoice, android.R.id.text1, openPgpProviderList) {
public View getView(int position, View convertView, ViewGroup parent) {
// User super class to create the View
View v = super.getView(position, convertView, parent);
TextView tv = (TextView) v.findViewById(android.R.id.text1);
// Put the image on the TextView
tv.setCompoundDrawablesWithIntrinsicBounds(openPgpProviderList.get(position).icon, null,
null, null);
// Add margin between image and text (support various screen densities)
int dp10 = (int) (10 * getContext().getResources().getDisplayMetrics().density + 0.5f);
tv.setCompoundDrawablePadding(dp10);
return v;
}
};
builder.setSingleChoiceItems(adapter, getIndexOfProviderList(selectedPackage),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
OpenPgpProviderEntry entry = openPgpProviderList.get(which);
if (entry.intent != null) {
/*
* Intents are called as activity
*
* Current approach is to assume the user installed the app.
* If he does not, the selected package is not valid.
*
* However applications should always consider this could happen,
* as the user might remove the currently used OpenPGP app.
*/
getActivity().startActivity(entry.intent);
return;
}
selectedPackage = entry.packageName;
dialog.dismiss();
}
});
return builder.create();
}
private int getIndexOfProviderList(String packageName) {
for (OpenPgpProviderEntry app : openPgpProviderList) {
if (app.packageName.equals(packageName)) {
return openPgpProviderList.indexOf(app);
}
}
// default is "none"
return 0;
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
((OpenPgpAppSelectDialog) getActivity()).onSelectProvider(selectedPackage);
}
}
public static class ApgDeprecationDialogFragment extends DialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new ApgDeprecationWarningDialog(getActivity());
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
((OpenPgpAppSelectDialog) getActivity()).onDismissApgDialog();
}
}
public void onSelectProvider(String selectedPackage) {
if (APG_PROVIDER_PLACEHOLDER.equals(selectedPackage)) {
showApgDeprecationDialogFragment();
return;
}
persistOpenPgpProviderSetting(selectedPackage);
finish();
}
private void persistOpenPgpProviderSetting(String selectedPackage) {
K9.setOpenPgpProvider(selectedPackage);
StorageEditor editor = Preferences.getPreferences(this).getStorage().edit();
K9.save(editor);
editor.commit();
}
public void onDismissApgDialog() {
showOpenPgpSelectDialogFragment();
}
private static class OpenPgpProviderEntry {
private String packageName;
private String simpleName;
private Drawable icon;
private Intent intent;
OpenPgpProviderEntry(String packageName, String simpleName, Drawable icon) {
this.packageName = packageName;
this.simpleName = simpleName;
this.icon = icon;
}
OpenPgpProviderEntry(String packageName, String simpleName, Drawable icon, Intent intent) {
this(packageName, simpleName, icon);
this.intent = intent;
}
@Override
public String toString() {
return simpleName;
}
}
}

View file

@ -1,5 +1,6 @@
package com.fsck.k9.activity.setup;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
@ -37,7 +38,6 @@ import com.fsck.k9.preferences.CheckBoxListPreference;
import com.fsck.k9.preferences.Storage;
import com.fsck.k9.preferences.StorageEditor;
import com.fsck.k9.preferences.TimePickerPreference;
import com.fsck.k9.service.MailService;
import com.fsck.k9.ui.dialog.ApgDeprecationWarningDialog;
import org.openintents.openpgp.util.OpenPgpAppPreference;

View file

@ -159,5 +159,6 @@ public final class CryptoResultAnnotation {
OPENPGP_ENCRYPTED_BUT_INCOMPLETE,
SIGNED_BUT_UNSUPPORTED,
ENCRYPTED_BUT_UNSUPPORTED,
OPENPGP_ENCRYPTED_NO_PROVIDER,
}
}

View file

@ -62,6 +62,7 @@ public class MessageCryptoHelper {
private final Context context;
private final String openPgpProviderPackage;
private final Object callbackLock = new Object();
private final Deque<CryptoPart> partsToDecryptOrVerify = new ArrayDeque<>();
@ -92,6 +93,12 @@ public class MessageCryptoHelper {
if (!K9.isOpenPgpProviderConfigured()) {
throw new IllegalStateException("MessageCryptoHelper must only be called with a openpgp provider!");
}
openPgpProviderPackage = K9.getOpenPgpProvider();
}
public boolean isConfiguredForOutdatedCryptoProvider() {
return !openPgpProviderPackage.equals(K9.getOpenPgpProvider());
}
public void asyncStartOrResumeProcessingMessage(LocalMessage message, MessageCryptoCallback callback,
@ -207,7 +214,6 @@ public class MessageCryptoHelper {
}
private void connectToCryptoProviderService() {
String openPgpProviderPackage = K9.getOpenPgpProvider();
openPgpServiceConnection = new OpenPgpServiceConnection(context, openPgpProviderPackage,
new OnBound() {
@Override

View file

@ -12,6 +12,7 @@ import com.fsck.k9.crypto.MessageDecryptVerifier;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mailstore.CryptoResultAnnotation;
import com.fsck.k9.mailstore.CryptoResultAnnotation.CryptoError;
public class MessageCryptoSplitter {
@ -19,20 +20,21 @@ public class MessageCryptoSplitter {
@Nullable
public static CryptoMessageParts split(@NonNull Message message, @Nullable MessageCryptoAnnotations annotations) {
if (annotations == null) {
ArrayList<Part> extraParts = new ArrayList<>();
Part primaryPart = MessageDecryptVerifier.findPrimaryEncryptedOrSignedPart(message, extraParts);
if (primaryPart == null) {
return null;
}
ArrayList<Part> extraParts = new ArrayList<>();
Part primaryPart = MessageDecryptVerifier.findPrimaryEncryptedOrSignedPart(message, extraParts);
if (!annotations.has(primaryPart)) {
return null;
if (annotations == null) {
CryptoResultAnnotation rootPartAnnotation =
CryptoResultAnnotation.createErrorAnnotation(CryptoError.OPENPGP_ENCRYPTED_NO_PROVIDER, null);
return new CryptoMessageParts(primaryPart, rootPartAnnotation, extraParts);
}
CryptoResultAnnotation rootPartAnnotation = annotations.get(primaryPart);
Part rootPart;
if (rootPartAnnotation.hasReplacementData()) {
if (rootPartAnnotation != null && rootPartAnnotation.hasReplacementData()) {
rootPart = rootPartAnnotation.getReplacementData();
} else {
rootPart = primaryPart;
@ -43,11 +45,12 @@ public class MessageCryptoSplitter {
public static class CryptoMessageParts {
public final Part contentPart;
@Nullable
public final CryptoResultAnnotation contentCryptoAnnotation;
public final List<Part> extraParts;
CryptoMessageParts(Part contentPart, CryptoResultAnnotation contentCryptoAnnotation, List<Part> extraParts) {
CryptoMessageParts(Part contentPart, @Nullable CryptoResultAnnotation contentCryptoAnnotation, List<Part> extraParts) {
this.contentPart = contentPart;
this.contentCryptoAnnotation = contentCryptoAnnotation;
this.extraParts = Collections.unmodifiableList(extraParts);

View file

@ -36,6 +36,7 @@ public class MessageCryptoPresenter implements OnCryptoClickListener {
// transient state
private CryptoResultAnnotation cryptoResultAnnotation;
private boolean reloadOnResumeWithoutRecreateFlag;
public MessageCryptoPresenter(Bundle savedInstanceState, MessageCryptoMvpView messageCryptoMvpView) {
@ -50,6 +51,13 @@ public class MessageCryptoPresenter implements OnCryptoClickListener {
outState.putBoolean("overrideCryptoWarning", overrideCryptoWarning);
}
public void onResume() {
if (reloadOnResumeWithoutRecreateFlag) {
reloadOnResumeWithoutRecreateFlag = false;
messageCryptoMvpView.restartMessageCryptoProcessing();
}
}
public boolean maybeHandleShowMessage(MessageTopView messageView, Account account, MessageViewInfo messageViewInfo) {
this.cryptoResultAnnotation = messageViewInfo.cryptoResultAnnotation;
@ -116,6 +124,11 @@ public class MessageCryptoPresenter implements OnCryptoClickListener {
break;
}
case ENCRYPTED_NO_PROVIDER: {
messageView.showCryptoProviderNotConfigured(messageViewInfo);
break;
}
case INCOMPLETE_SIGNED:
case UNSUPPORTED_SIGNED:
default: {
@ -232,6 +245,11 @@ public class MessageCryptoPresenter implements OnCryptoClickListener {
}
}
public void onClickConfigureProvider() {
reloadOnResumeWithoutRecreateFlag = true;
messageCryptoMvpView.showCryptoConfigDialog();
}
public interface MessageCryptoMvpView {
void redisplayMessage();
void restartMessageCryptoProcessing();
@ -240,5 +258,6 @@ public class MessageCryptoPresenter implements OnCryptoClickListener {
int flagsMask, int flagValues, int extraFlags) throws IntentSender.SendIntentException;
void showCryptoInfoDialog(MessageCryptoDisplayStatus displayStatus);
void showCryptoConfigDialog();
}
}

View file

@ -189,6 +189,21 @@ public class MessageTopView extends LinearLayout {
displayViewOnLoadFinished(false);
}
public void showCryptoProviderNotConfigured(final MessageViewInfo messageViewInfo) {
resetAndPrepareMessageView(messageViewInfo);
View view = mInflater.inflate(R.layout.message_content_crypto_no_provider, containerView, false);
view.findViewById(R.id.crypto_settings).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
messageCryptoPresenter.onClickConfigureProvider();
}
});
containerView.addView(view);
displayViewOnLoadFinished(false);
}
private void setCryptoProviderIcon(Drawable openPgpApiProviderIcon, View view) {
ImageView cryptoProviderIcon = (ImageView) view.findViewById(R.id.crypto_error_icon);
if (openPgpApiProviderIcon != null) {

View file

@ -46,6 +46,7 @@ import com.fsck.k9.mail.Flag;
import com.fsck.k9.mailstore.AttachmentViewInfo;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.mailstore.MessageViewInfo;
import com.fsck.k9.activity.setup.OpenPgpAppSelectDialog;
import com.fsck.k9.ui.messageview.CryptoInfoDialog.OnClickShowCryptoKeyListener;
import com.fsck.k9.ui.messageview.MessageCryptoPresenter.MessageCryptoMvpView;
import com.fsck.k9.view.MessageCryptoDisplayStatus;
@ -134,6 +135,13 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
mInitialized = true;
}
@Override
public void onResume() {
super.onResume();
messageCryptoPresenter.onResume();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
@ -388,6 +396,11 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
startActivityForResult(intent, activity);
}
private void startOpenPgpChooserActivity() {
Intent i = new Intent(getActivity(), OpenPgpAppSelectDialog.class);
startActivity(i);
}
public void onPendingIntentResult(int requestCode, int resultCode, Intent data) {
if ((requestCode & REQUEST_MASK_LOADER_HELPER) == REQUEST_MASK_LOADER_HELPER) {
requestCode ^= REQUEST_MASK_LOADER_HELPER;
@ -688,6 +701,11 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
mMessageView.setToLoadingState();
messageLoaderHelper.asyncRestartMessageCryptoProcessing();
}
@Override
public void showCryptoConfigDialog() {
startOpenPgpChooserActivity();
}
};
@Override

View file

@ -136,6 +136,12 @@ public enum MessageCryptoDisplayStatus {
R.string.crypto_msg_signed_unencrypted, R.string.crypto_msg_sign_incomplete
),
ENCRYPTED_NO_PROVIDER (
R.attr.openpgp_red,
R.drawable.status_lock_error,
R.string.crypto_msg_unsupported_encrypted
),
UNSUPPORTED_ENCRYPTED (
R.attr.openpgp_red,
R.drawable.status_lock_error,
@ -214,6 +220,9 @@ public enum MessageCryptoDisplayStatus {
case OPENPGP_ENCRYPTED_API_ERROR:
return ENCRYPTED_ERROR;
case OPENPGP_ENCRYPTED_NO_PROVIDER:
return ENCRYPTED_NO_PROVIDER;
}
throw new IllegalStateException("Unhandled case!");
}

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:id="@+id/crypto_error_title"
android:textAppearance="?android:textAppearanceMedium"
android:text="@string/crypto_no_provider_title"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="30dp"
android:layout_marginBottom="30dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:text="@string/crypto_no_provider_message"
/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:id="@+id/crypto_settings"
android:text="@string/crypto_no_provider_button"
/>
</LinearLayout>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.K9.Transparent" parent="@android:style/Theme.Translucent.NoTitleBar.Fullscreen" />
</resources>

View file

@ -1163,6 +1163,7 @@ Please submit bug reports, contribute new features and ask questions at
<string name="crypto_msg_encrypted_error">Message is encrypted, but there was a decryption error.</string>
<string name="crypto_msg_signed_error">Message is signed, but the signature could not be verified.</string>
<string name="crypto_msg_encrypted_no_provider">Message is encrypted, but no crypto app is configured.</string>
<string name="crypto_msg_unsupported_encrypted">Message is encrypted, but in an unsupported format.</string>
<string name="crypto_msg_unsupported_signed">Message is signed, but in an unsupported format.</string>
<string name="crypto_msg_incomplete_encrypted">Message is encrypted, but must be fully downloaded for decryption.</string>
@ -1217,6 +1218,9 @@ Please submit bug reports, contribute new features and ask questions at
<string name="apg_deprecated_ok">Got it!</string>
<string name="apg">APG</string>
<string name="no_crypto_provider_see_global">No OpenPGP app configured, see global settings!</string>
<string name="crypto_no_provider_title">This email is encrypted</string>
<string name="crypto_no_provider_message">This email has been encrypted with OpenPGP.\nTo read it, you need to install and configure a compatible OpenPGP App.</string>
<string name="crypto_no_provider_button">Choose OpenPGP App</string>
<string name="mail_list_widget_text">K-9 Message List</string>
<string name="mail_list_widget_loading">Loading messages…</string>

View file

@ -157,6 +157,10 @@
<item name="android:windowNoTitle">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowAnimationStyle">@android:style/Animation.Translucent</item>
<item name="tintColorBulletPointPositive">#77aa22</item>
<item name="tintColorBulletPointNegative">#dd2222</item>
<item name="tintColorBulletPointNeutral">#bbb</item>
</style>
<style name="Theme.K9.Dialog.Translucent.Light" parent="@android:style/Theme.Holo.Light.Dialog">
@ -166,6 +170,11 @@
<item name="android:windowNoTitle">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowAnimationStyle">@android:style/Animation.Translucent</item>
<item name="tintColorBulletPointPositive">#77aa22</item>
<item name="tintColorBulletPointNegative">#dd2222</item>
<item name="tintColorBulletPointNeutral">#bbb</item>
</style>
<style name="Theme.K9.Transparent" parent="@android:style/Theme.NoDisplay" />
</resources>