Linkify and autoLink Need a Custom URLSpan

Many of you use android:autoLink in TextView widgets to convert URLs, email addresses, phone numbers, and the like into clickable items. Each launches an activity to handle the request, via an ACTION_VIEW Intent on a suitable Uri.

Under the covers, this is handled via Linkify, and many of you use Linkify directly, as it gives you a bit more control over what gets converted into links and how.

The problem is in the class that implements the links themselves: URLSpan.

URLSpan is a CharacterStyle, used to format text in a TextView. As with other styles, like ForegroundColorSpan, it applies a visual effect to a range of characters inside of a CharSequence, specifically something that implements the Spanned interface. As the name suggests, URLSpan is used for formatting links to URLs.

URLSpan is also a ClickableSpan, one that can be clicked upon by the user to perform some action. The URLSpan is called with onClick(), much like an OnClickListener does.

The problem lies in its onClick() implementation: it just blindly calls startActivity() on the Intent that it creates.

Nowadays, startActivity() is a riskier operation than it had been before. It is very possible that there will be no activity to handle a particular URL structure, particularly on Android 4.3+ devices for users running in a restricted profile. But URLSpan does not catch the ActivityNotFoundException that could be thrown by startActivity(), and there is no standard place where you could introduce your own try/catch block to handle places where onClick() fails.

The solution is to create your own span, one that handles the ActivityNotFoundException in a manner that fits your application, and apply it to the SpannedString that comes out of the autoLink/Linkify process.

private static class DefensiveURLSpan extends URLSpan {
  public DefensiveURLSpan(String url) {
    super(url);
  }

  @Override
  public void onClick(View widget) {
    try {
      android.util.Log.d(getClass().getSimpleName(), "Got here!");
      super.onClick(widget);
    }
    catch (ActivityNotFoundException e) {
      // do something useful here
    }
  }
}

You can apply that by finding all the current URLSpan instances and replacing them with your own class:

private void fixTextView(TextView tv) {
  SpannableString current=(SpannableString)tv.getText();
  URLSpan[] spans=
      current.getSpans(0, current.length(), URLSpan.class);

  for (URLSpan span : spans) {
    int start=current.getSpanStart(span);
    int end=current.getSpanEnd(span);

    current.removeSpan(span);
    current.setSpan(new DefensiveURLSpan(span.getURL()), start, end,
                    0);
  }
}

Note that you do not want to call setText() on the TextView, thinking that you would be replacing the text with the modified version. You are modifying the TextView’s text in place in this fixTextView() method, and therefore setText() is not necessary. Worse, if you are using android:autoLink, setText() would cause Android go back through and add URLSpans again.

You may also wish to keep tabs on this issue, which is tracking a requested change to URLSpan to handle this scenario better.