Overview
Previously, we created wrappers around
mailto and
tel HTML links. Today, we will see how to integrate those wrappers into our Android app to respond to user clicking the
mailto and
tel links.
Introduction
By inspecting WebView class, you can find that there’s no direct way to subscribe to events of user clicking a link or navigating to another area of the website. The only way available is through implementing a custom WebViewClient. The WebViewClient class allows you to take control of various aspects of WebView like page loading, scale changing, error handling, and many others. This class is very useful so you have to inspect it yourself. Here we will focus only on handling page loading event.
mailto and tel Code Listing
For your reference, here’s the full code listing for mailto and tel web links,
- public abstract class WebLink {
-
-
-
- public abstract string Prefix { get; }
-
-
-
-
- public abstract void ClearFields();
-
-
-
-
- public virtual void ReadLink(string link) {
- if (link == null)
- throw new ArgumentNullException("link");
-
- if (link.ToLower().StartsWith(Prefix.ToLower()) == false)
- throw new FormatException("Invalid link.");
- }
-
-
-
-
- public virtual string GenerateLink(bool includePrefix) {
- var str = string.Empty;
-
- if (includePrefix)
- str += Prefix;
-
- return str;
- }
-
-
-
-
- protected string ExcludePrefix(string link) {
- link = link.Trim();
- if (link.ToLower().StartsWith(Prefix.ToLower()))
- link = link.Substring(Prefix.Length).Trim();
- return link;
- }
-
- public override string ToString() {
- return GenerateLink(true);
- }
- }
-
-
- public class MailWebLink : WebLink {
- #region Prefix
- protected static string LinkPrefix { get { return "mailto:"; } }
- public override string Prefix => LinkPrefix;
- #endregion
-
- #region Delimiters
- protected static readonly char[] MailDelimiters = new char[] { '?' };
- protected static readonly char[] RecipientDelimiters = new char[] { ',', ';' };
- protected static readonly char[] ParamDelimiters = new char[] { '&' };
- protected static readonly char[] ParamValueDelimiters = new char[] { '=' };
- #endregion
-
- #region Field Names
- protected static readonly string ToField = "to";
- protected static readonly string CcField = "cc";
- protected static readonly string BccField = "bcc";
- protected static readonly string SubjectField = "subject";
- protected static readonly string BodyField = "body";
- #endregion
-
-
- #region Fields
- public string[] To { get; set; }
- public string[] Cc { get; set; }
- public string[] Bcc { get; set; }
- public string Subject { get; set; }
- public string Body { get; set; }
- #endregion
-
- public MailWebLink() {
-
- }
- public MailWebLink(string link) {
- ReadLink(link);
- }
-
- public static bool CanHandle(string link) {
- return link.ToLower().Trim().StartsWith(LinkPrefix);
- }
-
- #region Link Loading
- public override void ClearFields() {
- To = Cc = Bcc = null;
- Subject = Body = null;
- }
-
- public override void ReadLink(string link) {
- base.ReadLink(link);
-
- try {
- ClearFields();
-
-
- link = ExcludePrefix(link);
-
-
- string tmpVal = null;
- int idx = -1;
-
- idx = link.IndexOfAny(MailDelimiters);
-
- if (idx > -1)
- tmpVal = link.Substring(0, idx);
- else
- tmpVal = link;
-
- this.To = LoadRecipients(tmpVal).ToArray();
-
- if (idx == -1)
- return;
-
- link = link.Substring(idx + 1);
-
-
- var parameters = GetParameters(link, true);
- foreach (var par in parameters) {
- if (par.Key == ToField)
- this.To = LoadRecipients(par.Value).ToArray();
- else if (par.Key == CcField)
- this.Cc = LoadRecipients(par.Value).ToArray();
- else if (par.Key == BccField)
- this.Bcc = LoadRecipients(par.Value).ToArray();
- else if (par.Key == SubjectField)
- this.Subject = par.Value;
- else if (par.Key == BodyField)
- this.Body = par.Value;
- }
- } catch {
- throw new FormatException();
- }
- }
-
-
-
-
- protected virtual IEnumerable<string> LoadRecipients(string val) {
- var items = val.Split(RecipientDelimiters, StringSplitOptions.RemoveEmptyEntries);
- return items.Select(s => s.Trim().ToLower()).Distinct();
- }
-
-
-
-
-
- protected virtual IEnumerable<KeyValuePair<string, string>> GetParameters(string val, bool skipEmpty = true) {
- var items = val.Split(ParamDelimiters, StringSplitOptions.RemoveEmptyEntries);
-
- foreach (var itm in items) {
- string key = string.Empty;
- string value = string.Empty;
-
- var delimiterIdx = itm.IndexOfAny(ParamValueDelimiters);
- if (delimiterIdx == -1)
- continue;
-
- key = itm.Substring(0, delimiterIdx).ToLower();
- value = itm.Substring(delimiterIdx + 1);
- value = UnscapeParamValue(value);
-
- if (key.Length == 0)
- continue;
-
- if (skipEmpty && value.Length == 0)
- continue;
-
- yield return new KeyValuePair<string, string> (key, value);
- }
- }
- #endregion
-
- #region Link Generation
-
- public virtual string GetLink() { return GenerateLink(true); }
-
- public override string GenerateLink(bool includePrefix) {
- string str = base.GenerateLink(includePrefix);
-
- if (this.To != null && this.To.Length > 0) {
- str += GetRecipientString(this.To);
- }
-
- str += MailDelimiters.First();
-
- if (this.Cc != null && this.Cc.Length > 0) {
- str += GetParameterString(CcField, GetRecipientString(this.Cc), false);
- str += ParamDelimiters.First();
- }
-
- if (this.Bcc != null && this.Bcc.Length > 0) {
- str += GetParameterString(BccField, GetRecipientString(this.Bcc), false);
- str += ParamDelimiters.First();
- }
-
- if (this.Subject != null && this.Subject.Length > 0) {
- str += GetParameterString(SubjectField, this.Subject, true);
- str += ParamDelimiters.First();
- }
-
- if (this.Body != null && this.Body.Length > 0) {
- str += GetParameterString(BodyField, this.Body, true);
- str += ParamDelimiters.First();
- }
-
- str = str.TrimEnd(MailDelimiters.Concat(ParamDelimiters).ToArray());
-
- return str;
- }
-
-
-
-
- protected virtual string GetRecipientString(string[] recipients) {
- return string.Join(RecipientDelimiters.First().ToString(), recipients);
- }
-
-
-
-
-
- protected virtual string GetParameterString(string key, string value, bool escapeValue) {
- return string.Format("{0}{1}{2}",
- key,
- ParamValueDelimiters.First(),
- escapeValue ? EscapeParamValue(value) : value);
- }
-
- #endregion
-
- #region Helpers
- protected static readonly Dictionary<string, string> CustomUnescapeCharacters =
- new Dictionary<string, string>() { { "+", " " } };
-
- private static string EscapeParamValue(string value) {
- return Uri.EscapeDataString(value);
- }
-
- private static string UnscapeParamValue(string value) {
- foreach (var customChar in CustomUnescapeCharacters) {
- if (value.Contains(customChar.Key))
- value = value.Replace(customChar.Key, customChar.Value);
- }
-
- return Uri.UnescapeDataString(value);
- }
- #endregion
- }
-
- public class TelephoneWebLink : WebLink {
- #region Prefix
- protected static string LinkPrefix { get { return "tel:"; } }
- public override string Prefix => LinkPrefix;
- #endregion
-
- #region Delimiters
- protected static readonly char ExtensionDelimiter = 'p';
- #endregion
-
- #region Fields
- public string Number { get; set; }
- public string Extension { get; set; }
- #endregion
-
-
- public TelephoneWebLink() {
-
- }
- public TelephoneWebLink(string link) {
- ReadLink(link);
- }
-
- public static bool CanHandle(string link) {
- return link.ToLower().Trim().StartsWith(LinkPrefix);
- }
-
- public override void ClearFields() {
- Number = null;
- Extension = null;
- }
-
- public override void ReadLink(string link) {
- base.ReadLink(link);
-
- try {
- ClearFields();
-
-
- link = ExcludePrefix(link).Trim();
-
- Number = string.Empty;
- Extension = string.Empty;
-
- bool foundExtension = false;
- int idx = 0;
- foreach (var c in link) {
- if (idx == 0 && c == '+')
- Number += "+";
- if (c == ExtensionDelimiter)
- foundExtension = true;
- else if (char.IsDigit(c)) {
- if (foundExtension == false)
- Number += c.ToString();
- else
- Extension += c.ToString();
- }
- idx++;
- }
-
- } catch {
- throw new FormatException();
- }
- }
-
- public override string GenerateLink(bool includePrefix) {
- var str = base.GenerateLink(includePrefix);
-
- if (Number != null)
- str += Number.ToString();
-
- if (Extension != null && Extension.Length > 0)
- str += ExtensionDelimiter.ToString() + Extension;
-
- return str;
- }
- }
Client Implementation
Start by laying out your custom implementation of WebViewClient,
- public class CustomWebViewClient : WebViewClient {
- public event EventHandler<WebViewEventArgs> PageStarted;
- public event EventHandler<WebViewEventArgs> PageFinished;
- public event EventHandler<WebLinkEventArgs> MailRequested;
- public event EventHandler<WebLinkEventArgs> TelephoneRequested;
-
-
-
-
- public override bool ShouldOverrideUrlLoading(WebView view, string url) {
- if (HandleCustomUrl(url))
- return true;
-
- view.LoadUrl(url);
- return true;
- }
-
- #region Custom URL Handling
- protected virtual bool HandleCustomUrl(string url) {
- try {
- if (MailWebLink.CanHandle(url)) {
- OnMailRequested(url);
- return true;
- }
-
- if (TelephoneWebLink.CanHandle(url)) {
- OnTelephoneRequested(url);
- return true;
- }
-
- return false;
- } catch (FormatException) {
- return false;
- }
- }
-
- private void OnMailRequested(string url) {
- if (MailRequested != null)
- MailRequested(this, new WebLinkEventArgs(url, new MailWebLink(url)));
- }
-
- private void OnTelephoneRequested(string url) {
- if (TelephoneRequested != null)
- TelephoneRequested(this, new WebLinkEventArgs(url, new TelephoneWebLink(url)));
- }
- #endregion
-
-
- #region Page Loading
- public override void OnPageStarted(WebView view, string url, Bitmap favicon) {
- base.OnPageStarted(view, url, favicon);
-
- if (PageStarted != null)
- PageStarted(this, new WebViewEventArgs(url.ToLower()));
- }
-
- public override void OnPageFinished(WebView view, string url) {
- base.OnPageFinished(view, url);
- if (PageFinished != null)
- PageFinished(this, new WebViewEventArgs(url.ToLower()));
- }
- #endregion
- }
A few things to mention here,
- ShouldOverrideUrlLoading would return true if you want to handle the loading events of the requested URL.
- As an object-oriented approach, we created some events to notify the host activity of the various events happen inside the WebView. We also implemented two versions of EventArgs to hold the state data for the events. The code for the EventArgs is displayed next.
- Before notifying the user of mailto and tel, we ensured first that they have the right format by calling CanHandle of both classes.
State Data
Now we would implement the EventArgs classes that would be passed to the host activity. The implementation is very straightforward.
- public class WebViewEventArgs : EventArgs{
- public string Url { get; set; }
-
- public WebViewEventArgs() { }
- public WebViewEventArgs(string url) {
- this.Url = url;
- }
- }
-
-
- public class WebLinkEventArgs : WebViewEventArgs {
- public WebLink WebLink { get; set; }
-
- public WebLinkEventArgs() { }
- public WebLinkEventArgs(string url, WebLink link) : base(url) {
- WebLink = link;
- }
- }
Linking the Client
Now link the client to the control by calling WebView.SetWebViewClient in activity’s OnCreate,
- protected WebView WebView { get; set; }
- protected CustomWebViewClient WebViewClient { get; set; }
-
- protected override void OnCreate(Bundle savedInstanceState) {
- base.OnCreate(savedInstanceState);
-
- this.WebView = this.FindViewById<WebView>(Resource.Id.WebView_View);
-
- WebViewClient = new CustomWebViewClient();
- WebViewClient.MailRequested += WebViewClient_MailRequested;
- WebViewClient.TelephoneRequested += WebViewClient_TelephoneRequested;
-
- this.WebView.SetWebViewClient(WebViewClient);
- }
-
- private void WebViewClient_MailRequested(object sender, WebLinkEventArgs e) {
- var lnk = e.WebLink as MailWebLink;
- IntentHelper.MailTo(this, lnk);
- }
-
- private void WebViewClient_TelephoneRequested(object sender, WebLinkEventArgs e) {
- var lnk = e.WebLink as TelephoneWebLink;
- if (lnk.Number.Length == 0)
- return;
-
- try {
- IntentHelper.PhoneCall(this, lnk.Number);
- } catch (Java.Lang.SecurityException ex) {
-
- }
In the previous code, we handled MailRequested and TelephoneRequested events and passed the data received to the IntentHelper class that we are going to create next.
Notice that Java.Lang.SecurityException will be thrown if the application is trying to make a phone call while not permitted. This should be handled to avoid app crashes.
Sending Emails
The code for sending an email is fairly easy. Next code will request the Android OS to open the mail app and display the relevant information. The OS might ask the user to select an app to handle this request.
- public static partial class IntentHelper {
- public static void MailTo(Context ctx, MailWebLink link, string activityTitle = null) {
- MailTo(ctx, link.To, link.Cc, link.Bcc, link.Subject, link.Body, activityTitle);
- }
-
- public static void MailTo(Context ctx,
- string[] to,
- string[] cc,
- string[] bcc,
- string subject,
- string body,
- string activityTitle = null) {
-
- Intent email = new Intent(Intent.ActionSend);
- email.SetType("message/rfc822");
-
- if (to != null)
- email.PutExtra(Intent.ExtraEmail, to );
-
- if (cc != null)
- email.PutExtra(Intent.ExtraCc, cc);
-
- if (bcc != null)
- email.PutExtra(Intent.ExtraBcc, bcc);
-
- if (subject != null)
- email.PutExtra(Intent.ExtraSubject, subject);
-
- if (body != null)
- email.PutExtra(Intent.ExtraText, body);
-
- if (activityTitle == null)
- activityTitle = Application.Context.Resources.GetString(Resource.String.text_send);
-
- ctx.StartActivity(Intent.CreateChooser(email, activityTitle));
- }
- }
Making Phone Calls
While sending an email is easy, calling a number is easier,
- public static partial class IntentHelper {
- public static void PhoneCall(Context ctx, TelephoneWebLink lnk) {
- PhoneCall(ctx, lnk.Number);
- }
-
- public static void PhoneCall(Context ctx, string number) {
- Intent intent = new Intent(Intent.ActionCall);
- intent.SetData(Android.Net.Uri.Parse("tel: " + number));
- ctx.StartActivity(intent);
- }
- }
Conclusion
The WebViewClient opens the possibility of handling many aspects and behavior of WebView. An idea, which we will see in a future post, is handling the browsing history and allowing the user to go back and forth. If you have any feedback, comments, or code updates please let me know.