diff --git a/app/src/main/java/eu/faircode/email/FragmentAnswer.java b/app/src/main/java/eu/faircode/email/FragmentAnswer.java index 5ea676530a..913bb85365 100644 --- a/app/src/main/java/eu/faircode/email/FragmentAnswer.java +++ b/app/src/main/java/eu/faircode/email/FragmentAnswer.java @@ -526,8 +526,9 @@ public class FragmentAnswer extends FragmentBase { String link = args.getString("link"); int start = args.getInt("start"); int end = args.getInt("end"); + String title = args.getString("title"); etText.setSelection(start, end); - StyleHelper.apply(R.id.menu_link, getViewLifecycleOwner(), null, etText, link); + StyleHelper.apply(R.id.menu_link, getViewLifecycleOwner(), null, etText, link, title); } private void onDelete() { @@ -568,23 +569,8 @@ public class FragmentAnswer extends FragmentBase { Log.i("Style action=" + action); if (action == R.id.menu_link) { - Uri uri = null; - - ClipboardManager cbm = Helper.getSystemService(getContext(), ClipboardManager.class); - if (cbm != null && cbm.hasPrimaryClip()) { - String link = cbm.getPrimaryClip().getItemAt(0).coerceToText(getContext()).toString(); - uri = Uri.parse(link); - if (uri.getScheme() == null) - uri = null; - } - - Bundle args = new Bundle(); - args.putParcelable("uri", uri); - args.putInt("start", etText.getSelectionStart()); - args.putInt("end", etText.getSelectionEnd()); - FragmentDialogInsertLink fragment = new FragmentDialogInsertLink(); - fragment.setArguments(args); + fragment.setArguments(FragmentDialogInsertLink.getArguments(etText)); fragment.setTargetFragment(this, REQUEST_LINK); fragment.show(getParentFragmentManager(), "answer:link"); diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index 1136bd476e..6aa6ee4166 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -229,6 +229,7 @@ import biweekly.Biweekly; import biweekly.ICalendar; import biweekly.component.VEvent; import biweekly.property.Organizer; +import io.noties.markwon.core.spans.LinkSpan; public class FragmentCompose extends FragmentBase { private enum State {NONE, LOADING, LOADED} @@ -2576,53 +2577,8 @@ public class FragmentCompose extends FragmentBase { } private void onActionLink() { - Uri uri = null; - - if (etBody.hasSelection()) { - int start = etBody.getSelectionStart(); - URLSpan[] spans = etBody.getText().getSpans(start, start, URLSpan.class); - if (spans.length > 0) { - String url = spans[0].getURL(); - if (url != null) { - uri = Uri.parse(url); - if (uri.getScheme() == null) - uri = null; - } - } - } - - if (uri == null) - try { - ClipboardManager cbm = Helper.getSystemService(getContext(), ClipboardManager.class); - if (cbm != null && cbm.hasPrimaryClip()) { - String link = cbm.getPrimaryClip().getItemAt(0).coerceToText(getContext()).toString(); - uri = Uri.parse(link); - if (uri.getScheme() == null) - uri = null; - } - } catch (Throwable ex) { - Log.w(ex); - /* - java.lang.SecurityException: Permission Denial: opening provider org.chromium.chrome.browser.util.ChromeFileProvider from ProcessRecord{43c6094 11175:eu.faircode.email/u0a73} (pid=11175, uid=10073) that is not exported from uid 10080 - at android.os.Parcel.readException(Parcel.java:1692) - at android.os.Parcel.readException(Parcel.java:1645) - at android.app.ActivityManagerProxy.getContentProvider(ActivityManagerNative.java:4214) - at android.app.ActivityThread.acquireProvider(ActivityThread.java:5584) - at android.app.ContextImpl$ApplicationContentResolver.acquireUnstableProvider(ContextImpl.java:2239) - at android.content.ContentResolver.acquireUnstableProvider(ContentResolver.java:1520) - at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:1133) - at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:1093) - at android.content.ClipData$Item.coerceToText(ClipData.java:340) - */ - } - - Bundle args = new Bundle(); - args.putParcelable("uri", uri); - args.putInt("start", etBody.getSelectionStart()); - args.putInt("end", etBody.getSelectionEnd()); - FragmentDialogInsertLink fragment = new FragmentDialogInsertLink(); - fragment.setArguments(args); + fragment.setArguments(FragmentDialogInsertLink.getArguments(etBody)); fragment.setTargetFragment(this, REQUEST_LINK); fragment.show(getParentFragmentManager(), "compose:link"); } @@ -4162,8 +4118,9 @@ public class FragmentCompose extends FragmentBase { String link = args.getString("link"); int start = args.getInt("start"); int end = args.getInt("end"); + String title = args.getString("title"); etBody.setSelection(start, end); - StyleHelper.apply(R.id.menu_link, getViewLifecycleOwner(), null, etBody, link); + StyleHelper.apply(R.id.menu_link, getViewLifecycleOwner(), null, etBody, link, title); } private void onActionDiscardConfirmed() { diff --git a/app/src/main/java/eu/faircode/email/FragmentDialogInsertLink.java b/app/src/main/java/eu/faircode/email/FragmentDialogInsertLink.java index caa6a817f1..4ad65ec0f5 100644 --- a/app/src/main/java/eu/faircode/email/FragmentDialogInsertLink.java +++ b/app/src/main/java/eu/faircode/email/FragmentDialogInsertLink.java @@ -22,37 +22,66 @@ package eu.faircode.email; import static android.app.Activity.RESULT_OK; import android.app.Dialog; +import android.content.ClipboardManager; +import android.content.Context; import android.content.DialogInterface; import android.net.Uri; import android.os.Bundle; import android.text.Editable; +import android.text.TextUtils; import android.text.TextWatcher; +import android.text.style.URLSpan; import android.view.LayoutInflater; import android.view.View; +import android.widget.Button; import android.widget.EditText; +import android.widget.ProgressBar; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +import javax.net.ssl.HttpsURLConnection; + public class FragmentDialogInsertLink extends FragmentDialogBase { private EditText etLink; + private EditText etTitle; + + private static final int METADATA_CONNECT_TIMEOUT = 10 * 1000; // milliseconds + private static final int METADATA_READ_TIMEOUT = 15 * 1000; // milliseconds @Override public void onSaveInstanceState(@NonNull Bundle outState) { outState.putString("fair:link", etLink == null ? null : etLink.getText().toString()); + outState.putString("fair:text", etTitle == null ? null : etTitle.getText().toString()); super.onSaveInstanceState(outState); } @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - Uri uri = getArguments().getParcelable("uri"); + Bundle args = getArguments(); + Uri uri = args.getParcelable("uri"); + String title = args.getString("title"); - View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_insert_link, null); + final Context context = getContext(); + View view = LayoutInflater.from(context).inflate(R.layout.dialog_insert_link, null); etLink = view.findViewById(R.id.etLink); final TextView tvInsecure = view.findViewById(R.id.tvInsecure); + etTitle = view.findViewById(R.id.etTitle); + final Button btnMetadata = view.findViewById(R.id.btnMetadata); + final ProgressBar pbWait = view.findViewById(R.id.pbWait); etLink.addTextChangedListener(new TextWatcher() { @Override @@ -65,27 +94,116 @@ public class FragmentDialogInsertLink extends FragmentDialogBase { @Override public void afterTextChanged(Editable editable) { - if (tvInsecure == null) + if (tvInsecure == null || btnMetadata == null) return; Uri uri = Uri.parse(editable.toString()); - tvInsecure.setVisibility(!uri.isOpaque() && - "http".equals(uri.getScheme()) ? View.VISIBLE : View.GONE); + tvInsecure.setVisibility(UriHelper.isSecure(uri) ? View.GONE : View.VISIBLE); + btnMetadata.setEnabled(UriHelper.isHyperLink(uri)); } }); - if (savedInstanceState == null) - etLink.setText(uri == null ? "https://" : uri.toString()); - else + btnMetadata.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Bundle args = new Bundle(); + args.putString("url", etLink.getText().toString()); + + new SimpleTask() { + @Override + protected void onPreExecute(Bundle args) { + btnMetadata.setEnabled(false); + pbWait.setVisibility(View.VISIBLE); + } + + @Override + protected void onPostExecute(Bundle args) { + btnMetadata.setEnabled(true); + pbWait.setVisibility(View.GONE); + } + + @Override + protected String onExecute(Context context, Bundle args) throws Throwable { + String url = args.getString("url"); + URL base = new URL(url); + + HttpURLConnection connection = (HttpURLConnection) base.openConnection(); + connection.setRequestMethod("GET"); + connection.setReadTimeout(METADATA_READ_TIMEOUT); + connection.setConnectTimeout(METADATA_CONNECT_TIMEOUT); + connection.setInstanceFollowRedirects(true); + connection.setRequestProperty("User-Agent", WebViewEx.getUserAgent(context)); + connection.connect(); + + try { + int status = connection.getResponseCode(); + if (status != HttpURLConnection.HTTP_OK) { + String responseText = Helper.readStream(connection.getInputStream()); + throw new IOException("HTTP " + status + ": " + responseText); + } + + // ... + // <meta name="description" content="... + // <meta property="og:title" content="... + // <meta property="twitter:title" content="... + Document doc = JsoupEx.parse(connection.getInputStream(), StandardCharsets.UTF_8.name(), url); + + Element title = doc.select("title").first(); + if (title != null && !TextUtils.isEmpty(title.text())) + return title.text(); + + Element description = doc.select("meta[name=description]").first(); + if (description != null && !TextUtils.isEmpty(description.attr("content"))) + return description.attr("content"); + + Element ogTitle = doc.select("meta[property=og:title]").first(); + if (ogTitle != null && !TextUtils.isEmpty(ogTitle.attr("content"))) + return ogTitle.attr("content"); + + Element twitterTitle = doc.select("meta[property=twitter:title]").first(); + if (twitterTitle != null && !TextUtils.isEmpty(twitterTitle.attr("content"))) + return twitterTitle.attr("content"); + + return null; + } finally { + connection.disconnect(); + } + } + + @Override + protected void onExecuted(Bundle args, String text) { + if (TextUtils.isEmpty(text)) + etTitle.setText(null); + else + etTitle.setText(text.replaceAll("\\s+", " ")); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Log.unexpectedError(getParentFragmentManager(), ex, !(ex instanceof IOException)); + } + }.execute(FragmentDialogInsertLink.this, args, "link:meta"); + } + }); + + if (savedInstanceState == null) { + String link = (uri == null ? "https://" : uri.toString()); + etLink.setText(link); + etTitle.setText(link.equals(title) ? null : title); + } else { etLink.setText(savedInstanceState.getString("fair:link")); + etTitle.setText(savedInstanceState.getString("fair:text")); + } - return new AlertDialog.Builder(getContext()) + pbWait.setVisibility(View.GONE); + + return new AlertDialog.Builder(context) .setView(view) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - String link = etLink.getText().toString(); - getArguments().putString("link", link); + args.putString("link", etLink.getText().toString()); + args.putString("title", etTitle.getText().toString()); sendResult(RESULT_OK); } }) @@ -98,4 +216,59 @@ public class FragmentDialogInsertLink extends FragmentDialogBase { }) .create(); } + + static Bundle getArguments(EditText etBody) { + Uri uri = null; + + int start = etBody.getSelectionStart(); + int end = etBody.getSelectionEnd(); + + URLSpan[] spans = etBody.getText().getSpans(start, start, URLSpan.class); + if (spans != null && spans.length > 0) { + start = etBody.getText().getSpanStart(spans[0]); + end = etBody.getText().getSpanEnd(spans[0]); + + String url = spans[0].getURL(); + if (url != null) { + uri = Uri.parse(url); + if (uri.getScheme() == null) + uri = null; + } + } + + if (uri == null) + try { + ClipboardManager cbm = Helper.getSystemService(etBody.getContext(), ClipboardManager.class); + if (cbm != null && cbm.hasPrimaryClip()) { + String link = cbm.getPrimaryClip().getItemAt(0).coerceToText(etBody.getContext()).toString(); + uri = Uri.parse(link); + if (uri.getScheme() == null) + uri = null; + } + } catch (Throwable ex) { + Log.w(ex); + /* + java.lang.SecurityException: Permission Denial: opening provider org.chromium.chrome.browser.util.ChromeFileProvider from ProcessRecord{43c6094 11175:eu.faircode.email/u0a73} (pid=11175, uid=10073) that is not exported from uid 10080 + at android.os.Parcel.readException(Parcel.java:1692) + at android.os.Parcel.readException(Parcel.java:1645) + at android.app.ActivityManagerProxy.getContentProvider(ActivityManagerNative.java:4214) + at android.app.ActivityThread.acquireProvider(ActivityThread.java:5584) + at android.app.ContextImpl$ApplicationContentResolver.acquireUnstableProvider(ContextImpl.java:2239) + at android.content.ContentResolver.acquireUnstableProvider(ContentResolver.java:1520) + at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:1133) + at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:1093) + at android.content.ClipData$Item.coerceToText(ClipData.java:340) + */ + } + + String title = (start >= 0 && end > start ? etBody.getText().subSequence(start, end).toString() : ""); + + Bundle args = new Bundle(); + args.putParcelable("uri", uri); + args.putInt("start", start); + args.putInt("end", end); + args.putString("title", title); + + return args; + } } diff --git a/app/src/main/java/eu/faircode/email/StyleHelper.java b/app/src/main/java/eu/faircode/email/StyleHelper.java index 777908a252..7f7533e3df 100644 --- a/app/src/main/java/eu/faircode/email/StyleHelper.java +++ b/app/src/main/java/eu/faircode/email/StyleHelper.java @@ -66,10 +66,8 @@ import com.flask.colorpicker.builder.ColorPickerDialogBuilder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Locale; -import java.util.Map; public class StyleHelper { private static final List<Class> CLEAR_STYLES = Collections.unmodifiableList(Arrays.asList( @@ -715,38 +713,26 @@ public class StyleHelper { Log.breadcrumb("style", "action", "link"); String url = (String) args[0]; + String title = (String) args[1]; - List<CharacterStyle> spans = new ArrayList<>(); - Map<CharacterStyle, Pair<Integer, Integer>> ranges = new HashMap<>(); - Map<CharacterStyle, Integer> flags = new HashMap<>(); - for (CharacterStyle span : edit.getSpans(start, end, CharacterStyle.class)) { - if (!(span instanceof URLSpan)) { - spans.add(span); - ranges.put(span, new Pair<>(edit.getSpanStart(span), edit.getSpanEnd(span))); - flags.put(span, edit.getSpanFlags(span)); - } - edit.removeSpan(span); - } + if (TextUtils.isEmpty(url)) + return false; + if (TextUtils.isEmpty(title)) + title = url; - if (url != null) { - int e = end; - if (start == end) { - etBody.getText().insert(start, url); - e += url.length(); - } + URLSpan[] spans = edit.getSpans(start, end, URLSpan.class); + for (URLSpan span : spans) + edit.removeSpan(span); - edit.setSpan(new URLSpan(url), start, e, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } + if (start == end) + edit.insert(start, title); + else if (!title.equals(edit.subSequence(start, end).toString())) + edit.replace(start, end, title); - // Restore other spans - for (CharacterStyle span : spans) - edit.setSpan(span, - ranges.get(span).first, - ranges.get(span).second, - flags.get(span)); + edit.setSpan(new URLSpan(url), start, start + title.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); etBody.setText(edit); - etBody.setSelection(end, end); + etBody.setSelection(start + title.length()); return true; } else if (action == R.id.menu_clear) { diff --git a/app/src/main/res/layout/dialog_insert_link.xml b/app/src/main/res/layout/dialog_insert_link.xml index b032d871a9..0a35f6d99b 100644 --- a/app/src/main/res/layout/dialog_insert_link.xml +++ b/app/src/main/res/layout/dialog_insert_link.xml @@ -1,47 +1,119 @@ <?xml version="1.0" encoding="utf-8"?> -<eu.faircode.email.ConstraintLayoutEx xmlns:android="http://schemas.android.com/apk/res/android" +<eu.faircode.email.ScrollViewEx xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="24dp"> - - <eu.faircode.email.FixedTextView - android:id="@+id/tvInsertLink" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:drawableStart="@drawable/twotone_insert_link_45_24" - android:drawablePadding="6dp" - android:labelFor="@+id/etLink" - android:text="@string/title_style_link" - android:textAppearance="@style/TextAppearance.AppCompat.Large" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <eu.faircode.email.EditTextPlain - android:id="@+id/etLink" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginTop="24dp" - android:imeOptions="actionDone" - android:inputType="textUri|textMultiLine" - android:text="https://email.faircode.eu/" - android:textAppearance="@style/TextAppearance.AppCompat.Small" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/tvInsertLink"> - - <requestFocus /> - </eu.faircode.email.EditTextPlain> - - <eu.faircode.email.FixedTextView - android:id="@+id/tvInsecure" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="12dp" - android:text="@string/title_insecure_link" - android:textAppearance="@style/TextAppearance.AppCompat.Small" - android:textColor="?attr/colorWarning" - android:textStyle="bold" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/etLink" /> -</eu.faircode.email.ConstraintLayoutEx> \ No newline at end of file + android:padding="24dp" + android:scrollbarStyle="outsideOverlay"> + + <eu.faircode.email.ConstraintLayoutEx + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <eu.faircode.email.FixedTextView + android:id="@+id/tvInsertLink" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:drawableStart="@drawable/twotone_insert_link_45_24" + android:drawablePadding="6dp" + android:labelFor="@+id/etLink" + android:text="@string/title_style_link" + android:textAppearance="@style/TextAppearance.AppCompat.Large" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <eu.faircode.email.FixedTextView + android:id="@+id/tvUrl" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:labelFor="@+id/etLink" + android:text="@string/title_style_link_address" + android:textAppearance="@style/TextAppearance.AppCompat.Medium" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/tvInsertLink" /> + + <eu.faircode.email.EditTextPlain + android:id="@+id/etLink" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:imeOptions="actionDone" + android:inputType="textUri|textMultiLine" + android:textAppearance="@style/TextAppearance.AppCompat.Small" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/tvUrl"> + + <requestFocus /> + </eu.faircode.email.EditTextPlain> + + <eu.faircode.email.FixedTextView + android:id="@+id/tvInsecure" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:text="@string/title_insecure_link" + android:textAppearance="@style/TextAppearance.AppCompat.Small" + android:textColor="?attr/colorWarning" + android:textStyle="bold" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/etLink" /> + + <eu.faircode.email.FixedTextView + android:id="@+id/tvTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:labelFor="@+id/etLink" + android:text="@string/title_style_link_title" + android:textAppearance="@style/TextAppearance.AppCompat.Medium" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/tvInsecure" /> + + <eu.faircode.email.EditTextPlain + android:id="@+id/etTitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:imeOptions="actionDone" + android:inputType="text" + android:textAppearance="@style/TextAppearance.AppCompat.Small" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/tvTitle" /> + + <Button + android:id="@+id/btnMetadata" + style="?android:attr/buttonStyleSmall" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:drawableEnd="@drawable/twotone_text_snippet_24" + android:drawablePadding="6dp" + android:text="@string/title_style_link_metadata" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/etTitle" /> + + <eu.faircode.email.ContentLoadingProgressBar + android:id="@+id/pbWait" + style="@style/Base.Widget.AppCompat.ProgressBar" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_marginStart="12dp" + android:indeterminate="true" + app:layout_constraintBottom_toBottomOf="@id/btnMetadata" + app:layout_constraintStart_toEndOf="@id/btnMetadata" + app:layout_constraintTop_toTopOf="@id/btnMetadata" /> + + <eu.faircode.email.FixedTextView + android:id="@+id/tvMetadataRemark" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:labelFor="@+id/etLink" + android:text="@string/title_style_link_metadata_remark" + android:textAppearance="@style/TextAppearance.AppCompat.Small" + android:textStyle="italic" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/btnMetadata" /> + </eu.faircode.email.ConstraintLayoutEx> +</eu.faircode.email.ScrollViewEx> \ 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 bf77bb7392..00903bc77e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1379,6 +1379,10 @@ <string name="title_style_code" translatable="false">Code</string> <string name="title_style_clear">Clear formatting</string> <string name="title_style_link">Insert link</string> + <string name="title_style_link_address">Address</string> + <string name="title_style_link_title">Title</string> + <string name="title_style_link_metadata">Fetch title</string> + <string name="title_style_link_metadata_remark">This will fetch the title at the entered address</string> <string name="title_add_image">Add image</string> <string name="title_add_image_inline">Insert</string>