From 2b3e119445ebd34e796528672f99451513b56cc9 Mon Sep 17 00:00:00 2001 From: M66B Date: Wed, 4 Dec 2019 11:54:02 +0100 Subject: [PATCH] Added certificate selection --- .../eu/faircode/email/AdapterCertificate.java | 186 ++++++++++++++++++ .../eu/faircode/email/DaoCertificate.java | 6 +- .../eu/faircode/email/EntityCertificate.java | 10 + .../eu/faircode/email/FragmentCompose.java | 88 +++++++-- .../eu/faircode/email/FragmentMessages.java | 3 +- .../main/res/layout/dialog_certificate.xml | 29 +++ app/src/main/res/layout/item_certificate.xml | 36 ++++ app/src/main/res/values/strings.xml | 1 + 8 files changed, 336 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/eu/faircode/email/AdapterCertificate.java create mode 100644 app/src/main/res/layout/dialog_certificate.xml create mode 100644 app/src/main/res/layout/item_certificate.xml diff --git a/app/src/main/java/eu/faircode/email/AdapterCertificate.java b/app/src/main/java/eu/faircode/email/AdapterCertificate.java new file mode 100644 index 0000000000..62d6e932e4 --- /dev/null +++ b/app/src/main/java/eu/faircode/email/AdapterCertificate.java @@ -0,0 +1,186 @@ +package eu.faircode.email; + +/* + This file is part of FairEmail. + + FairEmail is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + FairEmail is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FairEmail. If not, see . + + Copyright 2018-2019 by Marcel Bokhorst (M66B) +*/ + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.LifecycleOwner; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListUpdateCallback; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +public class AdapterCertificate extends RecyclerView.Adapter { + private ICertificate intf; + private Context context; + private LifecycleOwner owner; + private LayoutInflater inflater; + + private List items = new ArrayList<>(); + + public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + private View view; + private TextView tvEmail; + private TextView tvSubject; + + ViewHolder(View itemView) { + super(itemView); + + view = itemView.findViewById(R.id.clItem); + tvEmail = itemView.findViewById(R.id.tvEmail); + tvSubject = itemView.findViewById(R.id.tvSubject); + } + + @Override + public void onClick(View v) { + int pos = getAdapterPosition(); + if (pos == RecyclerView.NO_POSITION) + return; + + EntityCertificate certificate = items.get(pos); + intf.onSelected(certificate); + } + + private void wire() { + view.setOnClickListener(this); + } + + private void unwire() { + view.setOnClickListener(null); + } + + private void bindTo(EntityCertificate certificate) { + tvEmail.setText(certificate.email); + tvSubject.setText(certificate.subject); + } + } + + AdapterCertificate(Fragment parentFragment, ICertificate intf) { + this.intf = intf; + this.context = parentFragment.getContext(); + this.owner = parentFragment.getViewLifecycleOwner(); + this.inflater = LayoutInflater.from(parentFragment.getContext()); + + setHasStableIds(true); + } + + public void set(@NonNull List certificates) { + Log.i("Set certificates=" + certificates.size()); + + DiffUtil.DiffResult diff = DiffUtil.calculateDiff(new DiffCallback(items, certificates), false); + + items = certificates; + + diff.dispatchUpdatesTo(new ListUpdateCallback() { + @Override + public void onInserted(int position, int count) { + Log.i("Inserted @" + position + " #" + count); + } + + @Override + public void onRemoved(int position, int count) { + Log.i("Removed @" + position + " #" + count); + } + + @Override + public void onMoved(int fromPosition, int toPosition) { + Log.i("Moved " + fromPosition + ">" + toPosition); + } + + @Override + public void onChanged(int position, int count, Object payload) { + Log.i("Changed @" + position + " #" + count); + } + }); + diff.dispatchUpdatesTo(this); + } + + private class DiffCallback extends DiffUtil.Callback { + private List prev = new ArrayList<>(); + private List next = new ArrayList<>(); + + DiffCallback(List prev, List next) { + this.prev.addAll(prev); + this.next.addAll(next); + } + + @Override + public int getOldListSize() { + return prev.size(); + } + + @Override + public int getNewListSize() { + return next.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + EntityCertificate c1 = prev.get(oldItemPosition); + EntityCertificate c2 = next.get(newItemPosition); + return c1.id.equals(c2.id); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + EntityCertificate c1 = prev.get(oldItemPosition); + EntityCertificate c2 = next.get(newItemPosition); + return c1.equals(c2); + } + } + + @Override + public long getItemId(int position) { + return items.get(position).id; + } + + @Override + public int getItemCount() { + return items.size(); + } + + @Override + @NonNull + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ViewHolder(inflater.inflate(R.layout.item_certificate, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + holder.unwire(); + + EntityCertificate certificate = items.get(position); + holder.bindTo(certificate); + + holder.wire(); + } + + interface ICertificate { + void onSelected(EntityCertificate certificate); + } +} diff --git a/app/src/main/java/eu/faircode/email/DaoCertificate.java b/app/src/main/java/eu/faircode/email/DaoCertificate.java index 75947b5f29..d9a54cd226 100644 --- a/app/src/main/java/eu/faircode/email/DaoCertificate.java +++ b/app/src/main/java/eu/faircode/email/DaoCertificate.java @@ -29,9 +29,13 @@ import java.util.List; @Dao public interface DaoCertificate { @Query("SELECT * FROM certificate" + - " ORDER BY email DESC") + " ORDER BY email") LiveData> liveCertificates(); + @Query("SELECT * FROM certificate" + + " WHERE id = :id") + EntityCertificate getCertificate(long id); + @Query("SELECT * FROM certificate" + " WHERE fingerprint = :fingerprint" + " AND email = :email") diff --git a/app/src/main/java/eu/faircode/email/EntityCertificate.java b/app/src/main/java/eu/faircode/email/EntityCertificate.java index 0d2862f7e9..bfe7abde84 100644 --- a/app/src/main/java/eu/faircode/email/EntityCertificate.java +++ b/app/src/main/java/eu/faircode/email/EntityCertificate.java @@ -19,6 +19,8 @@ package eu.faircode.email; Copyright 2018-2019 by Marcel Bokhorst (M66B) */ +import android.util.Base64; + import androidx.annotation.NonNull; import androidx.room.Entity; import androidx.room.Index; @@ -48,6 +50,14 @@ public class EntityCertificate { @NonNull public String data; + void setEncoded(byte[] encoded) { + this.data = Base64.encodeToString(encoded, Base64.NO_WRAP); + } + + byte[] getEncoded() { + return Base64.decode(this.data, Base64.NO_WRAP); + } + @Override public boolean equals(Object obj) { if (obj instanceof EntityCertificate) { diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index ea95cf69f4..84b6d8dfa7 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -60,7 +60,6 @@ import android.text.Spanned; import android.text.TextUtils; import android.text.style.ImageSpan; import android.text.style.QuoteSpan; -import android.util.Base64; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.Menu; @@ -78,6 +77,7 @@ import android.widget.EditText; import android.widget.FilterQueryProvider; import android.widget.ImageButton; import android.widget.MultiAutoCompleteTextView; +import android.widget.ProgressBar; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; @@ -255,6 +255,7 @@ public class FragmentCompose extends FragmentBase { private static final int REQUEST_LINK = 12; private static final int REQUEST_DISCARD = 13; private static final int REQUEST_SEND = 14; + private static final int REQUEST_SELECT_CERTIFICATE = 15; @Override public void onCreate(Bundle savedInstanceState) { @@ -1247,7 +1248,18 @@ public class FragmentCompose extends FragmentBase { @Override public void run() { try { - onSmime(draft, alias); + Bundle args = new Bundle(); + args.putLong("id", draft.id); + args.putInt("type", draft.encrypt); + args.putString("alias", alias); + + if (EntityMessage.SMIME_SIGNENCRYPT.equals(draft.encrypt)) { + FragmentDialogCertificate fragment = new FragmentDialogCertificate(); + fragment.setArguments(args); + fragment.setTargetFragment(FragmentCompose.this, REQUEST_SELECT_CERTIFICATE); + fragment.show(getParentFragmentManager(), "compose:certificate"); + } else + onSmime(args); } catch (Throwable ex) { Log.e(ex); } @@ -1367,6 +1379,10 @@ public class FragmentCompose extends FragmentBase { if (resultCode == RESULT_OK) onActionSend(); break; + case REQUEST_SELECT_CERTIFICATE: + if (resultCode == RESULT_OK && data != null) + onSmime(data.getBundleExtra("args")); + break; } } catch (Throwable ex) { Log.e(ex); @@ -1844,18 +1860,14 @@ public class FragmentCompose extends FragmentBase { }.execute(this, args, "compose:pgp"); } - private void onSmime(EntityMessage draft, String alias) { - Bundle args = new Bundle(); - args.putLong("id", draft.id); - args.putInt("type", draft.encrypt); - args.putString("alias", alias); - + private void onSmime(Bundle args) { new SimpleTask() { @Override protected Void onExecute(Context context, Bundle args) throws Throwable { long id = args.getLong("id"); int type = args.getInt("type"); String alias = args.getString("alias"); + long cid = args.getLong("certificate", -1); DB db = DB.getInstance(context); @@ -1866,6 +1878,9 @@ public class FragmentCompose extends FragmentBase { EntityIdentity identity = db.identity().getIdentity(draft.identity); if (identity == null) throw new IllegalArgumentException(getString(R.string.title_from_missing)); + EntityCertificate certificate = db.certificate().getCertificate(cid); + if (certificate == null && EntityMessage.SMIME_SIGNENCRYPT.equals(type)) + throw new IllegalArgumentException("Certificate missing"); // Get/clean attachments List attachments = db.attachment().getAttachments(id); @@ -1974,19 +1989,8 @@ public class FragmentCompose extends FragmentBase { return null; } - // Get recipient - if (draft.to == null || draft.to.length != 1) - throw new IllegalArgumentException(getString(R.string.title_to_missing)); - String to = ((InternetAddress) draft.to[0]).getAddress(); - - // Get public key - List c = db.certificate().getCertificateByEmail(to); - if (c == null || c.size() == 0) - throw new IllegalArgumentException("Certificate not found"); - - byte[] encoded = Base64.decode(c.get(0).data, Base64.NO_WRAP); X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509") - .generateCertificate(new ByteArrayInputStream(encoded)); + .generateCertificate(new ByteArrayInputStream(certificate.getEncoded())); // Build signature BodyPart bpSignature = new MimeBodyPart(); @@ -3933,6 +3937,50 @@ public class FragmentCompose extends FragmentBase { } } + public static class FragmentDialogCertificate extends FragmentDialogBase { + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + View dview = LayoutInflater.from(getContext()).inflate(R.layout.dialog_certificate, null); + final RecyclerView rvCertificate = dview.findViewById(R.id.rvCertificate); + final ProgressBar pbWait = dview.findViewById(R.id.pbWait); + + final Dialog dialog = new AlertDialog.Builder(getContext()) + .setTitle(R.string.title_select_certificate) + .setView(dview) + .setNegativeButton(android.R.string.cancel, null).create(); + + rvCertificate.setHasFixedSize(false); + LinearLayoutManager llm = new LinearLayoutManager(getContext()); + rvCertificate.setLayoutManager(llm); + + final AdapterCertificate adapter = new AdapterCertificate(this, new AdapterCertificate.ICertificate() { + @Override + public void onSelected(EntityCertificate certificate) { + dialog.dismiss(); + getArguments().putLong("certificate", certificate.id); + sendResult(RESULT_OK); + } + }); + rvCertificate.setAdapter(adapter); + + rvCertificate.setVisibility(View.GONE); + pbWait.setVisibility(View.VISIBLE); + + DB db = DB.getInstance(getContext()); + db.certificate().liveCertificates().observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(List certificates) { + pbWait.setVisibility(View.GONE); + rvCertificate.setVisibility(View.VISIBLE); + adapter.set(certificates); + } + }); + + return dialog; + } + } + public static class FragmentDialogSend extends FragmentDialogBase { @NonNull @Override diff --git a/app/src/main/java/eu/faircode/email/FragmentMessages.java b/app/src/main/java/eu/faircode/email/FragmentMessages.java index a2973f1c8a..c47d175432 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessages.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java @@ -52,7 +52,6 @@ import android.security.KeyChain; import android.security.KeyChainAliasCallback; import android.text.TextUtils; import android.text.format.DateUtils; -import android.util.Base64; import android.util.LongSparseArray; import android.util.Pair; import android.util.TypedValue; @@ -4398,7 +4397,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. record.fingerprint = fingerprint; record.email = email; record.subject = cert.getSubjectX500Principal().getName(X500Principal.RFC2253); - record.data = Base64.encodeToString(cert.getEncoded(), Base64.NO_WRAP); + record.setEncoded(cert.getEncoded()); record.id = db.certificate().insertCertificate(record); } } diff --git a/app/src/main/res/layout/dialog_certificate.xml b/app/src/main/res/layout/dialog_certificate.xml new file mode 100644 index 0000000000..ff77d6a8e4 --- /dev/null +++ b/app/src/main/res/layout/dialog_certificate.xml @@ -0,0 +1,29 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_certificate.xml b/app/src/main/res/layout/item_certificate.xml new file mode 100644 index 0000000000..5890491ae5 --- /dev/null +++ b/app/src/main/res/layout/item_certificate.xml @@ -0,0 +1,36 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1cdf10e74b..1a0df4b7b7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -673,6 +673,7 @@ Insert template Edit as plain text Edit as reformatted text + Select certificate Plain text only Request delivery/read receipt Most providers and email clients ignore receipt requests