From baab4cba361695a669857fe3a18663e64837b65a Mon Sep 17 00:00:00 2001 From: M66B Date: Sun, 23 Feb 2020 12:02:42 +0100 Subject: [PATCH] Added signature editor --- app/src/main/AndroidManifest.xml | 6 + .../eu/faircode/email/ActivitySignature.java | 232 ++++++++++++++++++ .../eu/faircode/email/FragmentIdentity.java | 105 ++------ .../main/res/layout/activity_signature.xml | 58 +++++ app/src/main/res/layout/fragment_identity.xml | 33 +-- app/src/main/res/menu/action_signature.xml | 17 ++ .../main/res/menu/action_signature_style.xml | 27 ++ app/src/main/res/values/strings.xml | 5 +- 8 files changed, 366 insertions(+), 117 deletions(-) create mode 100644 app/src/main/java/eu/faircode/email/ActivitySignature.java create mode 100644 app/src/main/res/layout/activity_signature.xml create mode 100644 app/src/main/res/menu/action_signature.xml create mode 100644 app/src/main/res/menu/action_signature_style.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cc29b586ea..0159c99a75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -94,6 +94,12 @@ + + diff --git a/app/src/main/java/eu/faircode/email/ActivitySignature.java b/app/src/main/java/eu/faircode/email/ActivitySignature.java new file mode 100644 index 0000000000..ae8b312763 --- /dev/null +++ b/app/src/main/java/eu/faircode/email/ActivitySignature.java @@ -0,0 +1,232 @@ +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-2020 by Marcel Bokhorst (M66B) +*/ + +import android.content.ClipboardManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.text.Html; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ImageSpan; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; + +import com.google.android.material.bottomnavigation.BottomNavigationView; + +import java.io.FileNotFoundException; +import java.io.InputStream; + +public class ActivitySignature extends ActivityBase { + private EditTextCompose etText; + private BottomNavigationView style_bar; + private BottomNavigationView bottom_navigation; + + private static final int REQUEST_IMAGE = 1; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getSupportActionBar().setSubtitle(getString(R.string.title_edit_signature)); + setContentView(R.layout.activity_signature); + + etText = findViewById(R.id.etText); + style_bar = findViewById(R.id.style_bar); + bottom_navigation = findViewById(R.id.bottom_navigation); + + etText.setSelectionListener(new EditTextCompose.ISelection() { + @Override + public void onSelected(boolean selection) { + style_bar.setVisibility(selection ? View.VISIBLE : View.GONE); + } + }); + + style_bar.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() { + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem item) { + return onActionStyle(item.getItemId()); + } + }); + + bottom_navigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() { + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case R.id.action_insert_image: + insertImage(); + return true; + case R.id.action_delete: + delete(); + return true; + case R.id.action_save: + save(); + return true; + default: + return false; + } + } + }); + + style_bar.setVisibility(View.GONE); + + setResult(RESULT_CANCELED, new Intent()); + + load(); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + load(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + try { + switch (requestCode) { + case REQUEST_IMAGE: + if (resultCode == RESULT_OK && data != null) + onImageSelected(data.getData()); + break; + } + } catch (Throwable ex) { + Log.e(ex); + } + } + + private void load() { + String html = getIntent().getStringExtra("html"); + if (html == null) + etText.setText(null); + else + etText.setText(HtmlHelper.fromHtml(html, new Html.ImageGetter() { + @Override + public Drawable getDrawable(String source) { + return getDrawableByUri(Uri.parse(source)); + } + }, null)); + } + + private void delete() { + Intent result = new Intent(); + result.putExtra("html", (String) null); + setResult(RESULT_OK, result); + finish(); + } + + private void save() { + etText.clearComposingText(); + String html = HtmlHelper.toHtml(etText.getText()); + Intent result = new Intent(); + result.putExtra("html", html); + setResult(RESULT_OK, result); + finish(); + } + + private void insertImage() { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("image/*"); + Helper.openAdvanced(intent); + startActivityForResult(intent, REQUEST_IMAGE); + } + + private boolean onActionStyle(int action) { + Log.i("Style action=" + action); + + if (action == R.id.menu_link) { + Uri uri = null; + + ClipboardManager cbm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + if (cbm != null && cbm.hasPrimaryClip()) { + String link = cbm.getPrimaryClip().getItemAt(0).coerceToText(this).toString(); + uri = Uri.parse(link); + if (uri.getScheme() == null) + uri = null; + } + + View view = LayoutInflater.from(this).inflate(R.layout.dialog_insert_link, null); + EditText etLink = view.findViewById(R.id.etLink); + TextView tvInsecure = view.findViewById(R.id.tvInsecure); + + etLink.setText(uri == null ? "https://" : uri.toString()); + tvInsecure.setVisibility(View.GONE); + + new AlertDialog.Builder(this) + .setView(view) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String link = etLink.getText().toString(); + StyleHelper.apply(R.id.menu_link, etText, link); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + + return true; + } else + return StyleHelper.apply(action, etText); + } + + private void onImageSelected(Uri uri) { + getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + + int start = etText.getSelectionStart(); + SpannableStringBuilder ssb = new SpannableStringBuilder(etText.getText()); + ssb.insert(start, " "); + ImageSpan is = new ImageSpan(getDrawableByUri(uri), uri.toString(), ImageSpan.ALIGN_BASELINE); + ssb.setSpan(is, start, start + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + etText.setText(ssb); + } + + private Drawable getDrawableByUri(Uri uri) { + Drawable d; + try { + Log.i("Loading image source=" + uri); + InputStream inputStream = getContentResolver().openInputStream(uri); + d = Drawable.createFromStream(inputStream, uri.toString()); + } catch (FileNotFoundException ex) { + Log.w(ex); + d = getResources().getDrawable(R.drawable.baseline_broken_image_24); + } + + int w = Helper.dp2pixels(this, d.getIntrinsicWidth()); + int h = Helper.dp2pixels(this, d.getIntrinsicHeight()); + + d.setBounds(0, 0, w, h); + return d; + } +} diff --git a/app/src/main/java/eu/faircode/email/FragmentIdentity.java b/app/src/main/java/eu/faircode/email/FragmentIdentity.java index 26a89d686e..3b4a15e0be 100644 --- a/app/src/main/java/eu/faircode/email/FragmentIdentity.java +++ b/app/src/main/java/eu/faircode/email/FragmentIdentity.java @@ -21,17 +21,14 @@ package eu.faircode.email; import android.accounts.Account; import android.accounts.AccountManager; -import android.app.Dialog; import android.app.NotificationManager; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.graphics.Color; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.text.Editable; -import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.TextWatcher; import android.text.method.LinkMovementMethod; @@ -57,7 +54,6 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; import androidx.constraintlayout.widget.Group; import androidx.lifecycle.Lifecycle; @@ -86,8 +82,7 @@ public class FragmentIdentity extends FragmentBase { private EditText etDisplay; private ViewButtonColor btnColor; private TextView tvColorPro; - private EditText etSignature; - private Button btnHtml; + private Button btnSignature; private Button btnAdvanced; private Spinner spProvider; @@ -135,12 +130,13 @@ public class FragmentIdentity extends FragmentBase { private int auth = EmailService.AUTH_TYPE_PASSWORD; private String provider = null; private String certificate = null; + private String signature = null; private boolean saving = false; private static final int REQUEST_COLOR = 1; private static final int REQUEST_SAVE = 2; private static final int REQUEST_DELETE = 3; - private static final int REQUEST_HTML = 4; + private static final int REQUEST_SIGNATURE = 4; @Override public void onCreate(Bundle savedInstanceState) { @@ -172,8 +168,7 @@ public class FragmentIdentity extends FragmentBase { etDisplay = view.findViewById(R.id.etDisplay); btnColor = view.findViewById(R.id.btnColor); tvColorPro = view.findViewById(R.id.tvColorPro); - etSignature = view.findViewById(R.id.etSignature); - btnHtml = view.findViewById(R.id.btnHtml); + btnSignature = view.findViewById(R.id.btnSignature); btnAdvanced = view.findViewById(R.id.btnAdvanced); spProvider = view.findViewById(R.id.spProvider); @@ -337,36 +332,12 @@ public class FragmentIdentity extends FragmentBase { Helper.linkPro(tvColorPro); - etSignature.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - @Override - public void afterTextChanged(Editable editable) { - SpannableStringBuilder ssb = new SpannableStringBuilder(editable); - Helper.clearComposingText(ssb); - if (TextUtils.isEmpty(editable.toString())) - etSignature.setTag(null); - else - etSignature.setTag(HtmlHelper.toHtml(ssb)); - } - }); - - btnHtml.setOnClickListener(new View.OnClickListener() { + btnSignature.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - Bundle args = new Bundle(); - args.putString("html", (String) etSignature.getTag()); - - FragmentDialogHtml fragment = new FragmentDialogHtml(); - fragment.setArguments(args); - fragment.setTargetFragment(FragmentIdentity.this, REQUEST_HTML); - fragment.show(getParentFragmentManager(), "identity:html"); + Intent intent = new Intent(getContext(), ActivitySignature.class); + intent.putExtra("html", signature); + startActivityForResult(intent, REQUEST_SIGNATURE); } }); @@ -589,8 +560,6 @@ public class FragmentIdentity extends FragmentBase { name = hint.toString(); } - etSignature.clearComposingText(); - Bundle args = new Bundle(); args.putLong("id", id); args.putString("name", name); @@ -614,7 +583,7 @@ public class FragmentIdentity extends FragmentBase { args.putString("realm", etRealm.getText().toString()); args.putString("fingerprint", cbTrust.isChecked() ? (String) cbTrust.getTag() : null); args.putBoolean("use_ip", cbUseIp.isChecked()); - args.putString("signature", (String) etSignature.getTag()); + args.putString("signature", signature); args.putBoolean("synchronize", cbSynchronize.isChecked()); args.putBoolean("primary", cbPrimary.isChecked()); @@ -1006,7 +975,7 @@ public class FragmentIdentity extends FragmentBase { outState.putInt("fair:advanced", grpAdvanced.getVisibility()); outState.putInt("fair:auth", auth); outState.putString("fair:authprovider", provider); - outState.putString("fair:html", (String) etSignature.getTag()); + outState.putString("fair:html", signature); super.onSaveInstanceState(outState); } @@ -1033,9 +1002,7 @@ public class FragmentIdentity extends FragmentBase { etDisplay.setText(identity == null ? null : identity.display); btnColor.setColor(identity == null ? null : identity.color); - String signature = (identity == null ? null : identity.signature); - etSignature.setText(TextUtils.isEmpty(signature) ? null : HtmlHelper.fromHtml(signature)); - etSignature.setTag(signature); + signature = (identity == null ? null : identity.signature); etHost.setText(identity == null ? null : identity.host); rgEncryption.check(identity != null && identity.starttls ? R.id.radio_starttls : R.id.radio_ssl); @@ -1092,7 +1059,7 @@ public class FragmentIdentity extends FragmentBase { grpAdvanced.setVisibility(savedInstanceState.getInt("fair:advanced")); auth = savedInstanceState.getInt("fair:auth"); provider = savedInstanceState.getString("fair:authprovider"); - etSignature.setTag(savedInstanceState.getString("fair:html")); + signature = savedInstanceState.getString("fair:html"); } Helper.setViewsEnabled(view, true); @@ -1257,9 +1224,9 @@ public class FragmentIdentity extends FragmentBase { if (resultCode == RESULT_OK) onDelete(); break; - case REQUEST_HTML: - if (resultCode == RESULT_OK && data != null) - onHtml(data.getBundleExtra("args")); + case REQUEST_SIGNATURE: + if (resultCode == RESULT_OK) + onHtml(data.getExtras()); break; } } catch (Throwable ex) { @@ -1302,46 +1269,6 @@ public class FragmentIdentity extends FragmentBase { } private void onHtml(Bundle args) { - String html = args.getString("html"); - etSignature.setText(HtmlHelper.fromHtml(html)); - etSignature.setTag(html); - } - - public static class FragmentDialogHtml extends FragmentDialogBase { - private EditText etHtml; - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - outState.putString("fair:html", etHtml.getText().toString()); - super.onSaveInstanceState(outState); - } - - @NonNull - @Override - public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - String html; - if (savedInstanceState == null) - html = getArguments().getString("html"); - else - html = savedInstanceState.getString("fair:html"); - - View dview = LayoutInflater.from(getContext()).inflate(R.layout.dialog_signature, null); - etHtml = dview.findViewById(R.id.etHtml); - etHtml.setText(html); - - return new AlertDialog.Builder(getContext()) - .setTitle(R.string.title_edit_html) - .setView(dview) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - String html = etHtml.getText().toString(); - getArguments().putString("html", html); - sendResult(RESULT_OK); - } - }) - .setNegativeButton(android.R.string.cancel, null) - .create(); - } + signature = args.getString("html"); } } diff --git a/app/src/main/res/layout/activity_signature.xml b/app/src/main/res/layout/activity_signature.xml new file mode 100644 index 0000000000..43eee26224 --- /dev/null +++ b/app/src/main/res/layout/activity_signature.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_identity.xml b/app/src/main/res/layout/fragment_identity.xml index 96291dc37c..3166af283c 100644 --- a/app/src/main/res/layout/fragment_identity.xml +++ b/app/src/main/res/layout/fragment_identity.xml @@ -149,39 +149,18 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tvColorHint" /> - - - -