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 URLSpan
s again.
You may also wish to keep tabs on this issue,
which is tracking a requested change to URLSpan
to handle this
scenario better.