Handling ‘mailto’ And ‘tel’ Links Inside Android WebView

Today, we will see how to integrate those wrappers into our Android app to respond to a user clicking mailto and tel links.

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,
  1. public abstract class WebLink {  
  2.   /// <summary>  
  3.   /// Link prefix. Examples are: 'mailto:' and 'tel:'  
  4.   /// </summary>  
  5.   public abstract string Prefix { get; }  
  6.   
  7.   /// <summary>  
  8.   /// Clears instance fields.  
  9.   /// </summary>  
  10.   public abstract void ClearFields();  
  11.   
  12.   /// <summary>  
  13.   /// Loads link input into relevant fields.  
  14.   /// </summary>  
  15.   public virtual void ReadLink(string link) {  
  16.     if (link == null)  
  17.       throw new ArgumentNullException("link");  
  18.   
  19.     if (link.ToLower().StartsWith(Prefix.ToLower()) == false)  
  20.       throw new FormatException("Invalid link.");  
  21.   }  
  22.   
  23.   /// <summary>  
  24.   /// Generates link from instance fields.  
  25.   /// </summary>  
  26.   public virtual string GenerateLink(bool includePrefix) {  
  27.     var str = string.Empty;  
  28.   
  29.     if (includePrefix)  
  30.       str += Prefix;  
  31.   
  32.     return str;  
  33.   }  
  34.   
  35.   /// <summary>  
  36.   /// Can be used to exclude prefix from a link string.  
  37.   /// </summary>  
  38.   protected string ExcludePrefix(string link) {  
  39.     link = link.Trim();  
  40.     if (link.ToLower().StartsWith(Prefix.ToLower()))  
  41.       link = link.Substring(Prefix.Length).Trim();  
  42.     return link;  
  43.   }  
  44.   
  45.   public override string ToString() {  
  46.     return GenerateLink(true);  
  47.   }  
  48. }  
  49.   
  50.   
  51. public class MailWebLink : WebLink {  
  52.   #region Prefix  
  53.   protected static string LinkPrefix { get { return "mailto:"; } }  
  54.   public override string Prefix => LinkPrefix;  
  55.   #endregion  
  56.   
  57.   #region Delimiters  
  58.   protected static readonly char[] MailDelimiters = new char[] { '?' };  
  59.   protected static readonly char[] RecipientDelimiters = new char[] { ','';' };  
  60.   protected static readonly char[] ParamDelimiters = new char[] { '&' };  
  61.   protected static readonly char[] ParamValueDelimiters = new char[] { '=' };  
  62.   #endregion  
  63.  
  64.   #region Field Names  
  65.   protected static readonly string ToField = "to";  
  66.   protected static readonly string CcField = "cc";  
  67.   protected static readonly string BccField = "bcc";  
  68.   protected static readonly string SubjectField = "subject";  
  69.   protected static readonly string BodyField = "body";  
  70.   #endregion  
  71.  
  72.  
  73.   #region Fields  
  74.   public string[] To { getset; }  
  75.   public string[] Cc { getset; }  
  76.   public string[] Bcc { getset; }  
  77.   public string Subject { getset; }  
  78.   public string Body { getset; }  
  79.   #endregion  
  80.   
  81.   public MailWebLink() {  
  82.   
  83.   }  
  84.   public MailWebLink(string link) {  
  85.     ReadLink(link);  
  86.   }  
  87.   
  88.   public static bool CanHandle(string link) {  
  89.     return link.ToLower().Trim().StartsWith(LinkPrefix);  
  90.   }  
  91.  
  92.   #region Link Loading  
  93.   public override void ClearFields() {  
  94.     To = Cc = Bcc = null;  
  95.     Subject = Body = null;  
  96.   }  
  97.   
  98.   public override void ReadLink(string link) {  
  99.     base.ReadLink(link);  
  100.   
  101.     try {  
  102.       ClearFields();  
  103.   
  104.       // Exclude prefix if necessary  
  105.       link = ExcludePrefix(link);  
  106.   
  107.       // Get mail 'To' Field  
  108.       string tmpVal = null;  
  109.       int idx = -1;  
  110.   
  111.       idx = link.IndexOfAny(MailDelimiters);  
  112.   
  113.       if (idx > -1)  
  114.         tmpVal = link.Substring(0, idx);  
  115.       else  
  116.         tmpVal = link;  
  117.   
  118.       this.To = LoadRecipients(tmpVal).ToArray();  
  119.   
  120.       if (idx == -1)  
  121.         return;  
  122.   
  123.       link = link.Substring(idx + 1);  
  124.   
  125.       // Handle rest of fields  
  126.       var parameters = GetParameters(link, true);  
  127.       foreach (var par in parameters) {  
  128.         if (par.Key == ToField) // overrides the above code  
  129.           this.To = LoadRecipients(par.Value).ToArray();  
  130.         else if (par.Key == CcField)  
  131.           this.Cc = LoadRecipients(par.Value).ToArray();  
  132.         else if (par.Key == BccField)  
  133.           this.Bcc = LoadRecipients(par.Value).ToArray();  
  134.         else if (par.Key == SubjectField)  
  135.           this.Subject = par.Value;  
  136.         else if (par.Key == BodyField)  
  137.           this.Body = par.Value;  
  138.       }  
  139.     } catch {  
  140.       throw new FormatException();  
  141.     }  
  142.   }  
  143.   
  144.   /// <summary>  
  145.   /// Splits a mail string into a list of mail addresses.  
  146.   /// </summary>  
  147.   protected virtual IEnumerable<string> LoadRecipients(string val) {  
  148.     var items = val.Split(RecipientDelimiters, StringSplitOptions.RemoveEmptyEntries);  
  149.     return items.Select(s => s.Trim().ToLower()).Distinct();  
  150.   }  
  151.   
  152.   /// <summary>  
  153.   /// Splits a parameter string into a list of parameters (kay and value)  
  154.   /// </summary>  
  155.   /// <param name="skipEmpty">Whether to skip empty parameters.</param>  
  156.   protected virtual IEnumerable<KeyValuePair<stringstring>> GetParameters(string val, bool skipEmpty = true) {  
  157.     var items = val.Split(ParamDelimiters, StringSplitOptions.RemoveEmptyEntries);  
  158.   
  159.     foreach (var itm in items) {  
  160.       string key = string.Empty;  
  161.       string value = string.Empty;  
  162.   
  163.       var delimiterIdx = itm.IndexOfAny(ParamValueDelimiters);  
  164.       if (delimiterIdx == -1)  
  165.         continue;  
  166.   
  167.       key = itm.Substring(0, delimiterIdx).ToLower();  
  168.       value = itm.Substring(delimiterIdx + 1);  
  169.       value = UnscapeParamValue(value);  
  170.   
  171.       if (key.Length == 0)  
  172.         continue;  
  173.   
  174.       if (skipEmpty && value.Length == 0)  
  175.         continue;  
  176.   
  177.       yield return new KeyValuePair<stringstring> (key, value);  
  178.     }  
  179.   }  
  180.   #endregion  
  181.  
  182.   #region Link Generation  
  183.   
  184.   public virtual string GetLink() { return GenerateLink(true); }  
  185.   
  186.   public override string GenerateLink(bool includePrefix) {  
  187.     string str = base.GenerateLink(includePrefix);  
  188.   
  189.     if (this.To != null && this.To.Length > 0) {  
  190.       str += GetRecipientString(this.To);  
  191.     }  
  192.   
  193.     str += MailDelimiters.First();  
  194.   
  195.     if (this.Cc != null && this.Cc.Length > 0) {  
  196.       str += GetParameterString(CcField, GetRecipientString(this.Cc), false);  
  197.       str += ParamDelimiters.First();  
  198.     }  
  199.   
  200.     if (this.Bcc != null && this.Bcc.Length > 0) {  
  201.       str += GetParameterString(BccField, GetRecipientString(this.Bcc), false);  
  202.       str += ParamDelimiters.First();  
  203.     }  
  204.   
  205.     if (this.Subject != null && this.Subject.Length > 0) {  
  206.       str += GetParameterString(SubjectField, this.Subject, true);  
  207.       str += ParamDelimiters.First();  
  208.     }  
  209.   
  210.     if (this.Body != null && this.Body.Length > 0) {  
  211.       str += GetParameterString(BodyField, this.Body, true);  
  212.       str += ParamDelimiters.First();  
  213.     }  
  214.   
  215.     str = str.TrimEnd(MailDelimiters.Concat(ParamDelimiters).ToArray());  
  216.   
  217.     return str;  
  218.   }  
  219.   
  220.   /// <summary>  
  221.   /// Joins a list of mail addresses into a string  
  222.   /// </summary>  
  223.   protected virtual string GetRecipientString(string[] recipients) {  
  224.     return string.Join(RecipientDelimiters.First().ToString(), recipients);  
  225.   }  
  226.   
  227.   /// <summary>  
  228.   /// Joins a parameter (key and value) into a string  
  229.   /// </summary>  
  230.   /// <param name="escapeValue">Whether to escape value.</param>  
  231.   protected virtual string GetParameterString(string key, string value, bool escapeValue) {  
  232.     return string.Format("{0}{1}{2}",  
  233.       key,  
  234.       ParamValueDelimiters.First(),  
  235.       escapeValue ? EscapeParamValue(value) : value);  
  236.   }  
  237.  
  238.   #endregion  
  239.  
  240.   #region Helpers  
  241.   protected static readonly Dictionary<stringstring> CustomUnescapeCharacters =  
  242.     new Dictionary<stringstring>() { { "+"" " } };  
  243.    
  244.   private static string EscapeParamValue(string value) {  
  245.     return Uri.EscapeDataString(value);  
  246.   }  
  247.   
  248.   private static string UnscapeParamValue(string value) {  
  249.     foreach (var customChar in CustomUnescapeCharacters) {  
  250.       if (value.Contains(customChar.Key))  
  251.         value = value.Replace(customChar.Key, customChar.Value);  
  252.     }  
  253.   
  254.     return Uri.UnescapeDataString(value);  
  255.   }  
  256.   #endregion  
  257. }  
  258.   
  259. public class TelephoneWebLink : WebLink {  
  260.   #region Prefix  
  261.   protected static string LinkPrefix { get { return "tel:"; } }  
  262.   public override string Prefix => LinkPrefix;  
  263.   #endregion  
  264.  
  265.   #region Delimiters  
  266.   protected static readonly char ExtensionDelimiter = 'p';  
  267.   #endregion  
  268.  
  269.   #region Fields  
  270.   public string Number { getset; }  
  271.   public string Extension { getset; }  
  272.   #endregion  
  273.   
  274.   
  275.   public TelephoneWebLink() {  
  276.   
  277.   }  
  278.   public TelephoneWebLink(string link) {  
  279.     ReadLink(link);  
  280.   }  
  281.   
  282.   public static bool CanHandle(string link) {  
  283.     return link.ToLower().Trim().StartsWith(LinkPrefix);  
  284.   }  
  285.   
  286.   public override void ClearFields() {  
  287.     Number = null;  
  288.     Extension = null;  
  289.   }  
  290.   
  291.   public override void ReadLink(string link) {  
  292.     base.ReadLink(link);  
  293.   
  294.     try {  
  295.       ClearFields();  
  296.   
  297.       // Exclude prefix if necessary  
  298.       link = ExcludePrefix(link).Trim();  
  299.   
  300.       Number = string.Empty;  
  301.       Extension = string.Empty;  
  302.   
  303.       bool foundExtension = false;  
  304.       int idx = 0;  
  305.       foreach (var c in link) {  
  306.         if (idx == 0 && c == '+')  
  307.           Number += "+";  
  308.         if (c == ExtensionDelimiter)  
  309.           foundExtension = true;  
  310.         else if (char.IsDigit(c)) {  
  311.           if (foundExtension == false)  
  312.             Number += c.ToString();  
  313.           else  
  314.             Extension += c.ToString();  
  315.         }  
  316.         idx++;  
  317.       }  
  318.   
  319.     } catch {  
  320.       throw new FormatException();  
  321.     }  
  322.   }  
  323.   
  324.   public override string GenerateLink(bool includePrefix) {  
  325.     var str = base.GenerateLink(includePrefix);  
  326.   
  327.     if (Number != null)  
  328.       str += Number.ToString();  
  329.   
  330.     if (Extension != null && Extension.Length > 0)  
  331.       str += ExtensionDelimiter.ToString() + Extension;  
  332.   
  333.     return str;  
  334.   }  
  335. }  

Client Implementation

 
Start by laying out your custom implementation of WebViewClient,
  1. public class CustomWebViewClient : WebViewClient {  
  2.   public event EventHandler<WebViewEventArgs> PageStarted;  
  3.   public event EventHandler<WebViewEventArgs> PageFinished;  
  4.   public event EventHandler<WebLinkEventArgs> MailRequested;  
  5.   public event EventHandler<WebLinkEventArgs> TelephoneRequested;  
  6.   
  7.   /// <summary>  
  8.   /// Give the host application a chance to take control when a URL is about to be loaded in the current WebView.  
  9.   /// </summary>  
  10.   public override bool ShouldOverrideUrlLoading(WebView view, string url) {  
  11.     if (HandleCustomUrl(url))  
  12.       return true;  
  13.   
  14.     view.LoadUrl(url);  
  15.     return true;  
  16.   }  
  17.  
  18.   #region Custom URL Handling  
  19.   protected virtual bool HandleCustomUrl(string url) {  
  20.     try {  
  21.       if (MailWebLink.CanHandle(url)) {  
  22.         OnMailRequested(url);  
  23.         return true;  
  24.       }  
  25.   
  26.       if (TelephoneWebLink.CanHandle(url)) {  
  27.         OnTelephoneRequested(url);  
  28.         return true;  
  29.       }  
  30.   
  31.       return false;  
  32.     } catch (FormatException) {  
  33.       return false;  
  34.     }  
  35.   }  
  36.   
  37.   private void OnMailRequested(string url) {  
  38.     if (MailRequested != null)  
  39.       MailRequested(thisnew WebLinkEventArgs(url, new MailWebLink(url)));  
  40.   }  
  41.   
  42.   private void OnTelephoneRequested(string url) {  
  43.     if (TelephoneRequested != null)  
  44.       TelephoneRequested(thisnew WebLinkEventArgs(url, new TelephoneWebLink(url)));  
  45.   }  
  46.   #endregion  
  47.  
  48.  
  49.   #region Page Loading   
  50.   public override void OnPageStarted(WebView view, string url, Bitmap favicon) {  
  51.     base.OnPageStarted(view, url, favicon);  
  52.   
  53.     if (PageStarted != null)  
  54.       PageStarted(thisnew WebViewEventArgs(url.ToLower()));  
  55.   }  
  56.   
  57.   public override void OnPageFinished(WebView view, string url) {  
  58.     base.OnPageFinished(view, url);  
  59.     if (PageFinished != null)  
  60.       PageFinished(thisnew WebViewEventArgs(url.ToLower()));  
  61.   }  
  62.   #endregion  
  63. }  
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.
  1. public class WebViewEventArgs : EventArgs{  
  2.   public string Url { getset; }  
  3.   
  4.   public WebViewEventArgs() { }  
  5.   public WebViewEventArgs(string url) {  
  6.     this.Url = url;  
  7.   }  
  8. }  
  9.   
  10.   
  11. public class WebLinkEventArgs : WebViewEventArgs {  
  12.   public WebLink WebLink { getset; }  
  13.   
  14.   public WebLinkEventArgs() { }  
  15.   public WebLinkEventArgs(string url, WebLink link) : base(url) {  
  16.     WebLink = link;  
  17.   }  
  18. }  

Linking the Client

 
Now link the client to the control by calling WebView.SetWebViewClient in activity’s OnCreate,
  1. protected WebView WebView { getset; }  
  2. protected CustomWebViewClient WebViewClient { getset; }  
  3.   
  4. protected override void OnCreate(Bundle savedInstanceState) {  
  5.   base.OnCreate(savedInstanceState);  
  6.   
  7.   this.WebView = this.FindViewById<WebView>(Resource.Id.WebView_View);  
  8.   
  9.   WebViewClient = new CustomWebViewClient();  
  10.   WebViewClient.MailRequested += WebViewClient_MailRequested;  
  11.   WebViewClient.TelephoneRequested += WebViewClient_TelephoneRequested;  
  12.   
  13.   this.WebView.SetWebViewClient(WebViewClient);  
  14. }  
  15.   
  16. private void WebViewClient_MailRequested(object sender, WebLinkEventArgs e) {  
  17.   var lnk = e.WebLink as MailWebLink;  
  18.   IntentHelper.MailTo(this, lnk);   
  19. }  
  20.   
  21. private void WebViewClient_TelephoneRequested(object sender, WebLinkEventArgs e) {  
  22.   var lnk = e.WebLink as TelephoneWebLink;  
  23.   if (lnk.Number.Length == 0)  
  24.     return;  
  25.   
  26.   try {  
  27.     IntentHelper.PhoneCall(this, lnk.Number);  
  28.   } catch (Java.Lang.SecurityException ex) {  
  29.     // App is not granted permmission for phone calls  
  30.   }  
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.
  1. public static partial class IntentHelper {  
  2.   public static void MailTo(Context ctx, MailWebLink link, string activityTitle = null) {  
  3.     MailTo(ctx, link.To, link.Cc, link.Bcc, link.Subject, link.Body, activityTitle);  
  4.   }  
  5.   
  6.   public static void MailTo(Context ctx,   
  7.     string[] to,  
  8.     string[] cc,   
  9.     string[] bcc,   
  10.     string subject,   
  11.     string body,  
  12.     string activityTitle = null) {  
  13.   
  14.     Intent email = new Intent(Intent.ActionSend);  
  15.     email.SetType("message/rfc822");  
  16.   
  17.     if (to != null)  
  18.      email.PutExtra(Intent.ExtraEmail,  to );  
  19.   
  20.     if (cc != null)  
  21.       email.PutExtra(Intent.ExtraCc, cc);  
  22.   
  23.     if (bcc != null)  
  24.       email.PutExtra(Intent.ExtraBcc, bcc);  
  25.   
  26.     if (subject != null)  
  27.       email.PutExtra(Intent.ExtraSubject, subject);  
  28.   
  29.     if (body != null)  
  30.       email.PutExtra(Intent.ExtraText, body);  
  31.   
  32.     if (activityTitle == null)  
  33.       activityTitle = Application.Context.Resources.GetString(Resource.String.text_send);  
  34.   
  35.     ctx.StartActivity(Intent.CreateChooser(email, activityTitle));  
  36.   }  
  37. }  

Making Phone Calls

 
While sending an email is easy, calling a number is easier,
  1. public static partial class IntentHelper {  
  2.   public static void PhoneCall(Context ctx, TelephoneWebLink lnk) {  
  3.     PhoneCall(ctx, lnk.Number);  
  4.   }  
  5.   
  6.   public static void PhoneCall(Context ctx, string number) {  
  7.     Intent intent = new Intent(Intent.ActionCall);  
  8.     intent.SetData(Android.Net.Uri.Parse("tel: " + number));  
  9.     ctx.StartActivity(intent);  
  10.   }  

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.