From 38902a5cd9bf56dbae5778a31974a07dceea4f05 Mon Sep 17 00:00:00 2001 From: M66B Date: Sat, 22 Oct 2022 20:44:42 +0200 Subject: [PATCH] Refactoring --- .../eu/faircode/email/AdapterMessage.java | 4 +- .../eu/faircode/email/ProtectedContent.java | 321 ++++++++++++++++++ .../java/eu/faircode/email/StyleHelper.java | 272 +-------------- 3 files changed, 330 insertions(+), 267 deletions(-) create mode 100644 app/src/main/java/eu/faircode/email/ProtectedContent.java diff --git a/app/src/main/java/eu/faircode/email/AdapterMessage.java b/app/src/main/java/eu/faircode/email/AdapterMessage.java index f0636ac810..4601854f92 100644 --- a/app/src/main/java/eu/faircode/email/AdapterMessage.java +++ b/app/src/main/java/eu/faircode/email/AdapterMessage.java @@ -5853,11 +5853,11 @@ public class AdapterMessage extends RecyclerView.Adapter. + + Copyright 2018-2022 by Marcel Bokhorst (M66B) +*/ + +import android.app.Dialog; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.text.Editable; +import android.text.Spannable; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.text.method.ArrowKeyMovementMethod; +import android.text.style.URLSpan; +import android.util.Base64; +import android.view.GestureDetector; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; + +import com.google.android.material.textfield.TextInputLayout; + +import org.jsoup.nodes.Element; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; + +public class ProtectedContent { + static final int MAX_PROTECTED_TEXT = 1500; + private static final int DECRYPT_ITERATIONS = 120000; + private static final int DECRYPT_KEYLEN = 256; + private static final String DECRYPT_DERIVATION = "PBKDF2WithHmacSHA512"; + private static final int DECRYPT_TAGLEN = 128; + private static final String DECRYPT_TRANSFORMATION = "AES/GCM/NoPadding"; + private static final String DECRYPT_URL = "https://fairemail.net/decrypt/"; + + static Uri toUri(Context context, String html, String password) + throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + if (html.length() > MAX_PROTECTED_TEXT) + throw new IllegalArgumentException(context.getString(R.string.title_style_protect_size)); + + SecureRandom random = new SecureRandom(); + + byte[] salt = new byte[16]; // 128 bits + random.nextBytes(salt); + + byte[] iv = new byte[12]; // 96 bites + random.nextBytes(iv); + + // Iterations = 120,000; Keylength = 256 bits = 32 bytes + PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, DECRYPT_ITERATIONS, DECRYPT_KEYLEN); + + SecretKeyFactory skf = SecretKeyFactory.getInstance(DECRYPT_DERIVATION); + SecretKey key = skf.generateSecret(spec); + + // Authentication tag length = 128 bits = 16 bytes + GCMParameterSpec parameterSpec = new GCMParameterSpec(DECRYPT_TAGLEN, iv); + + final Cipher cipher = Cipher.getInstance(DECRYPT_TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec); + + byte[] cipherText = cipher.doFinal(html.getBytes(StandardCharsets.UTF_8)); + + ByteBuffer out = ByteBuffer.allocate(1 + salt.length + iv.length + cipherText.length); + out.put((byte) 1); // version + out.put(salt); + out.put(iv); + out.put(cipherText); + + String fragment = Base64.encodeToString(out.array(), Base64.URL_SAFE | Base64.NO_WRAP); + return Uri.parse(DECRYPT_URL + "#" + fragment); + } + + static String fromUri(Context context, Uri uri, String password) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + byte[] msg = Base64.decode(uri.getFragment(), Base64.URL_SAFE | Base64.NO_WRAP); + + int version = msg[0]; + if (version > 1) + throw new IllegalArgumentException("Please update the app"); + + byte[] salt = new byte[16]; // 128 bits + System.arraycopy(msg, 1, salt, 0, salt.length); + + byte[] iv = new byte[12]; // 96 bites + System.arraycopy(msg, 1 + salt.length, iv, 0, iv.length); + + byte[] encrypted = new byte[msg.length - 1 - salt.length - iv.length]; + System.arraycopy(msg, 1 + salt.length + iv.length, encrypted, 0, encrypted.length); + + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DECRYPT_DERIVATION); + KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, DECRYPT_ITERATIONS, DECRYPT_KEYLEN); + SecretKey secret = keyFactory.generateSecret(keySpec); + Cipher cipher = Cipher.getInstance(DECRYPT_TRANSFORMATION); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + cipher.init(Cipher.DECRYPT_MODE, secret, ivSpec); + byte[] decrypted = cipher.doFinal(encrypted); + + return new String(decrypted, StandardCharsets.UTF_8); + } + + static boolean isProtectedContent(Uri uri) { + return uri.toString().startsWith(DECRYPT_URL); + } + + public static final class FragmentDialogDecrypt extends FragmentDialogBase { + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Bundle args = getArguments(); + + final Context context = getContext(); + final View view = LayoutInflater.from(context).inflate(R.layout.dialog_decrypt, null); + final TextInputLayout tilPassword = view.findViewById(R.id.tilPassword); + final Button btnDecrypt = view.findViewById(R.id.btnDecrypt); + final TextView tvContent = view.findViewById(R.id.tvContent); + final TextView tvError = view.findViewById(R.id.tvError); + final TextView tvErrorDetail = view.findViewById(R.id.tvErrorDetail); + + tilPassword.setVisibility(View.VISIBLE); + btnDecrypt.setVisibility(View.VISIBLE); + tvContent.setVisibility(View.GONE); + tvError.setVisibility(View.GONE); + tvErrorDetail.setVisibility(View.GONE); + + String password = args.getString("password"); + tilPassword.getEditText().setText(password); + btnDecrypt.setEnabled(!TextUtils.isEmpty(password)); + + tilPassword.getEditText().addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Do nothing + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Do nothing + } + + @Override + public void afterTextChanged(Editable s) { + String password = s.toString(); + btnDecrypt.setEnabled(!TextUtils.isEmpty(password)); + args.putString("password", password); + } + }); + + btnDecrypt.setEnabled(false); + + tvContent.setMovementMethod(new ArrowKeyMovementMethod() { + private GestureDetector gestureDetector = new GestureDetector(context, + new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapUp(MotionEvent event) { + return onClick(event); + } + + private boolean onClick(MotionEvent event) { + Spannable buffer = (Spannable) tvContent.getText(); + int off = Helper.getOffset(tvContent, buffer, event); + + URLSpan[] link = buffer.getSpans(off, off, URLSpan.class); + if (link.length > 0) { + String url = link[0].getURL(); + Uri uri = Uri.parse(url); + + int start = buffer.getSpanStart(link[0]); + int end = buffer.getSpanEnd(link[0]); + String title = (start < 0 || end < 0 || end <= start + ? null : buffer.subSequence(start, end).toString()); + if (url.equals(title)) + title = null; + + Bundle args = new Bundle(); + args.putParcelable("uri", uri); + args.putString("title", title); + + FragmentDialogOpenLink fragment = new FragmentDialogOpenLink(); + fragment.setArguments(args); + fragment.show(getParentFragmentManager(), "open:link"); + + return true; + } + + return false; + } + }); + + @Override + public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { + return gestureDetector.onTouchEvent(event); + } + }); + + btnDecrypt.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new SimpleTask() { + @Override + protected void onPreExecute(Bundle args) { + tilPassword.setEnabled(false); + btnDecrypt.setEnabled(false); + tvError.setVisibility(View.GONE); + tvErrorDetail.setVisibility(View.GONE); + } + + @Override + protected void onPostExecute(Bundle args) { + tilPassword.setEnabled(true); + btnDecrypt.setEnabled(true); + } + + @Override + protected Spanned onExecute(Context context, Bundle args) throws Throwable { + Uri uri = args.getParcelable("uri"); + String password = args.getString("password"); + + String html = ProtectedContent.fromUri(context, uri, password); + + return HtmlHelper.fromHtml(html, new HtmlHelper.ImageGetterEx() { + @Override + public Drawable getDrawable(Element element) { + return ImageHelper.decodeImage(context, + -1, element, true, 0, 1.0f, tvContent); + } + }, null, context); + } + + @Override + protected void onExecuted(Bundle args, Spanned content) { + tilPassword.setVisibility(View.GONE); + btnDecrypt.setVisibility(View.GONE); + tvContent.setText(content); + tvContent.setVisibility(View.VISIBLE); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + tvError.setText(ex.getMessage()); + tvErrorDetail.setText(ex.toString()); + tvError.setVisibility(View.VISIBLE); + tvErrorDetail.setVisibility(View.VISIBLE); + } + }.execute(FragmentDialogDecrypt.this, args, "decypt"); + } + }); + + tilPassword.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + btnDecrypt.performClick(); + return true; + } else + return false; + } + }); + + if (!TextUtils.isEmpty(password)) + btnDecrypt.post(new Runnable() { + @Override + public void run() { + btnDecrypt.performClick(); + } + }); + + Dialog dialog = new AlertDialog.Builder(context) + .setView(view) + .setNegativeButton(android.R.string.cancel, null) + .create(); + + dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + + return dialog; + } + } +} diff --git a/app/src/main/java/eu/faircode/email/StyleHelper.java b/app/src/main/java/eu/faircode/email/StyleHelper.java index 3d8f574a35..cd85e841ae 100644 --- a/app/src/main/java/eu/faircode/email/StyleHelper.java +++ b/app/src/main/java/eu/faircode/email/StyleHelper.java @@ -19,7 +19,6 @@ package eu.faircode.email; Copyright 2018-2022 by Marcel Bokhorst (M66B) */ -import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -41,7 +40,6 @@ import android.text.Spanned; import android.text.TextPaint; import android.text.TextUtils; import android.text.TextWatcher; -import android.text.method.ArrowKeyMovementMethod; import android.text.style.AlignmentSpan; import android.text.style.BackgroundColorSpan; import android.text.style.BulletSpan; @@ -57,22 +55,17 @@ import android.text.style.URLSpan; import android.text.style.UnderlineSpan; import android.util.Base64; import android.util.Pair; -import android.view.GestureDetector; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MenuItem; -import android.view.MotionEvent; import android.view.SubMenu; import android.view.View; import android.view.WindowManager; -import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.PopupMenu; import androidx.core.content.ContextCompat; @@ -89,32 +82,13 @@ import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import java.io.ByteArrayOutputStream; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.security.SecureRandom; -import java.security.spec.KeySpec; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; -import javax.crypto.Cipher; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.GCMParameterSpec; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.PBEKeySpec; - public class StyleHelper { - private static final int MAX_PROTECTED_TEXT = 1500; - private static final int DECRYPT_ITERATIONS = 120000; - private static final int DECRYPT_KEYLEN = 256; - private static final String DECRYPT_DERIVATION = "PBKDF2WithHmacSHA512"; - private static final int DECRYPT_TAGLEN = 128; - private static final String DECRYPT_TRANSFORMATION = "AES/GCM/NoPadding"; - private static final String DECRYPT_URL = "https://fairemail.net/decrypt/"; - private static final List> CLEAR_STYLES = Collections.unmodifiableList(Arrays.asList( StyleSpan.class, UnderlineSpan.class, @@ -598,11 +572,11 @@ public class StyleHelper { } boolean toolong = false; - if (end - start > MAX_PROTECTED_TEXT) { + if (end - start > ProtectedContent.MAX_PROTECTED_TEXT) { toolong = true; } else { String html = getProtectedContent((Spanned) edit.subSequence(start, end)); - if (html.length() > MAX_PROTECTED_TEXT) + if (html.length() > ProtectedContent.MAX_PROTECTED_TEXT) toolong = true; } if (toolong) { @@ -637,52 +611,18 @@ public class StyleHelper { args.putInt("start", start); args.putInt("end", end); - new SimpleTask() { + new SimpleTask() { @Override - protected String onExecute(Context context, Bundle args) throws Throwable { + protected Uri onExecute(Context context, Bundle args) throws Throwable { Spanned text = (Spanned) args.getCharSequence("text"); String password = args.getString("password"); String html = getProtectedContent(text); - if (html.length() > MAX_PROTECTED_TEXT) - throw new IllegalArgumentException(context.getString(R.string.title_style_protect_size)); - - SecureRandom random = new SecureRandom(); - - byte[] salt = new byte[16]; // 128 bits - random.nextBytes(salt); - - byte[] iv = new byte[12]; // 96 bites - random.nextBytes(iv); - - // Iterations = 120,000; Keylength = 256 bits = 32 bytes - PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, DECRYPT_ITERATIONS, DECRYPT_KEYLEN); - - SecretKeyFactory skf = SecretKeyFactory.getInstance(DECRYPT_DERIVATION); - SecretKey key = skf.generateSecret(spec); - - // Authentication tag length = 128 bits = 16 bytes - GCMParameterSpec parameterSpec = new GCMParameterSpec(DECRYPT_TAGLEN, iv); - - final Cipher cipher = Cipher.getInstance(DECRYPT_TRANSFORMATION); - cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec); - - byte[] cipherText = cipher.doFinal(html.getBytes(StandardCharsets.UTF_8)); - - ByteBuffer out = ByteBuffer.allocate(1 + salt.length + iv.length + cipherText.length); - out.put((byte) 1); // version - out.put(salt); - out.put(iv); - out.put(cipherText); - - String fragment = Base64.encodeToString(out.array(), Base64.URL_SAFE | Base64.NO_WRAP); - String url = DECRYPT_URL + "#" + fragment; - - return url; + return ProtectedContent.toUri(context, html, password); } @Override - protected void onExecuted(Bundle args, String url) { + protected void onExecuted(Bundle args, Uri uri) { if (etBody.getSelectionStart() != start || etBody.getSelectionEnd() != end) return; @@ -690,7 +630,7 @@ public class StyleHelper { String title = context.getString(R.string.title_decrypt); edit.delete(start, end); edit.insert(start, title); - edit.setSpan(new URLSpan(url), start, start + title.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + edit.setSpan(new URLSpan(uri.toString()), start, start + title.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); etBody.setSelection(start + title.length()); } @@ -1478,203 +1418,5 @@ public class StyleHelper { } } - public static boolean isProtectedContent(Uri uri) { - return uri.toString().startsWith(DECRYPT_URL); - } - - public static final class FragmentDialogDecrypt extends FragmentDialogBase { - @NonNull - @Override - public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - Bundle args = getArguments(); - - final Context context = getContext(); - final View view = LayoutInflater.from(context).inflate(R.layout.dialog_decrypt, null); - final TextInputLayout tilPassword = view.findViewById(R.id.tilPassword); - final Button btnDecrypt = view.findViewById(R.id.btnDecrypt); - final TextView tvContent = view.findViewById(R.id.tvContent); - final TextView tvError = view.findViewById(R.id.tvError); - final TextView tvErrorDetail = view.findViewById(R.id.tvErrorDetail); - - tilPassword.setVisibility(View.VISIBLE); - btnDecrypt.setVisibility(View.VISIBLE); - tvContent.setVisibility(View.GONE); - tvError.setVisibility(View.GONE); - tvErrorDetail.setVisibility(View.GONE); - - String password = args.getString("password"); - tilPassword.getEditText().setText(password); - btnDecrypt.setEnabled(!TextUtils.isEmpty(password)); - - tilPassword.getEditText().addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // Do nothing - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - // Do nothing - } - - @Override - public void afterTextChanged(Editable s) { - String password = s.toString(); - btnDecrypt.setEnabled(!TextUtils.isEmpty(password)); - args.putString("password", password); - } - }); - - btnDecrypt.setEnabled(false); - - tvContent.setMovementMethod(new ArrowKeyMovementMethod() { - private GestureDetector gestureDetector = new GestureDetector(context, - new GestureDetector.SimpleOnGestureListener() { - @Override - public boolean onSingleTapUp(MotionEvent event) { - return onClick(event); - } - - private boolean onClick(MotionEvent event) { - Spannable buffer = (Spannable) tvContent.getText(); - int off = Helper.getOffset(tvContent, buffer, event); - - URLSpan[] link = buffer.getSpans(off, off, URLSpan.class); - if (link.length > 0) { - String url = link[0].getURL(); - Uri uri = Uri.parse(url); - - int start = buffer.getSpanStart(link[0]); - int end = buffer.getSpanEnd(link[0]); - String title = (start < 0 || end < 0 || end <= start - ? null : buffer.subSequence(start, end).toString()); - if (url.equals(title)) - title = null; - - Bundle args = new Bundle(); - args.putParcelable("uri", uri); - args.putString("title", title); - - FragmentDialogOpenLink fragment = new FragmentDialogOpenLink(); - fragment.setArguments(args); - fragment.show(getParentFragmentManager(), "open:link"); - - return true; - } - - return false; - } - }); - - @Override - public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { - return gestureDetector.onTouchEvent(event); - } - }); - - btnDecrypt.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - new SimpleTask() { - @Override - protected void onPreExecute(Bundle args) { - tilPassword.setEnabled(false); - btnDecrypt.setEnabled(false); - tvError.setVisibility(View.GONE); - tvErrorDetail.setVisibility(View.GONE); - } - - @Override - protected void onPostExecute(Bundle args) { - tilPassword.setEnabled(true); - btnDecrypt.setEnabled(true); - } - - @Override - protected Spanned onExecute(Context context, Bundle args) throws Throwable { - Uri uri = args.getParcelable("uri"); - String password = args.getString("password"); - - byte[] msg = Base64.decode(uri.getFragment(), Base64.URL_SAFE | Base64.NO_WRAP); - - int version = msg[0]; - - byte[] salt = new byte[16]; // 128 bits - System.arraycopy(msg, 1, salt, 0, salt.length); - - byte[] iv = new byte[12]; // 96 bites - System.arraycopy(msg, 1 + salt.length, iv, 0, iv.length); - - byte[] encrypted = new byte[msg.length - 1 - salt.length - iv.length]; - System.arraycopy(msg, 1 + salt.length + iv.length, encrypted, 0, encrypted.length); - - SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DECRYPT_DERIVATION); - KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, DECRYPT_ITERATIONS, DECRYPT_KEYLEN); - SecretKey secret = keyFactory.generateSecret(keySpec); - Cipher cipher = Cipher.getInstance(DECRYPT_TRANSFORMATION); - IvParameterSpec ivSpec = new IvParameterSpec(iv); - cipher.init(Cipher.DECRYPT_MODE, secret, ivSpec); - byte[] decrypted = cipher.doFinal(encrypted); - - String html = new String(decrypted, StandardCharsets.UTF_8); - - return HtmlHelper.fromHtml(html, new HtmlHelper.ImageGetterEx() { - @Override - public Drawable getDrawable(Element element) { - return ImageHelper.decodeImage(context, - -1, element, true, 0, 1.0f, tvContent); - } - }, null, context); - } - - @Override - protected void onExecuted(Bundle args, Spanned content) { - tilPassword.setVisibility(View.GONE); - btnDecrypt.setVisibility(View.GONE); - tvContent.setText(content); - tvContent.setVisibility(View.VISIBLE); - } - - @Override - protected void onException(Bundle args, Throwable ex) { - tvError.setText(ex.getMessage()); - tvErrorDetail.setText(ex.toString()); - tvError.setVisibility(View.VISIBLE); - tvErrorDetail.setVisibility(View.VISIBLE); - } - }.execute(FragmentDialogDecrypt.this, args, "decypt"); - } - }); - - tilPassword.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (actionId == EditorInfo.IME_ACTION_DONE) { - btnDecrypt.performClick(); - return true; - } else - return false; - } - }); - - if (!TextUtils.isEmpty(password)) - btnDecrypt.post(new Runnable() { - @Override - public void run() { - btnDecrypt.performClick(); - } - }); - - Dialog dialog = new AlertDialog.Builder(context) - .setView(view) - .setNegativeButton(android.R.string.cancel, null) - .create(); - - dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); - - return dialog; - } - } - //TextUtils.dumpSpans(text, new LogPrinter(android.util.Log.INFO, "FairEmail"), "afterTextChanged "); }