The Linkify Problem: The Detection and the Mitigation

As was covered yesterday, a number of HTC devices are going to have the behavior of suppressing the default chooser that would appear when we have multiple activities that could all handle the same Intent structure, for a number of common Intent structures, such as viewing Web pages.

How can we tell, at runtime, that we are running on such a device?

The best answer that I have come up with involves PackageManager, resolveActivity(), and a check for a specific response:

PackageManager mgr=getPackageManager();
Intent test=new Intent(Intent.ACTION_VIEW, Uri.parse(""));

if ("".equals(mgr.resolveActivity(view, PackageManager.MATCH_DEFAULT_ONLY).activityInfo.packageName)) {
  // you are affected

Here, we create a test Intent that is one affected by this Linkify fix, then use PackageManager to see who will handle it. If it comes up as, then we know that we are on an affected device. If it comes up null (no browser???) or something else, then we are behaving normally.

The package is the one that adds in this particular fix, which is why in various forums you will find recipes for removing this app from rooted devices as a way for users to get past the limitation.

Apps that expect users to be able to reach them via a chooser (e.g., Web browsers, mapping apps, streaming media players) could use this detection mechanism to let the users of affected devices know of the limitations and direct them to the “App Associations” portion of the HTC-customized Settings app, so they can make your app be the one to handle these requests.

Apps that wish to display choosers (or the equivalent) in spite of this limitation (e.g., for Web pages that have nothing to do with Linkify) could use this detection mechanism to adjust their behavior, going through some code needed for these HTC devices and not needed for others. So, if we are on a device known to block the default chooser in some scenarios via, what can we do about it?

There are two pieces to the puzzle of “do about it” – we have to know when we want to start an activity, and then we have to do something other than the ordinary default call to startActivity() that will no longer give the user options for how to proceed.

Sometimes, we are the ones calling startActivity() where we are used to a system-default chooser appearing.

Sometimes, while we are not the ones calling startActivity(), we could do so easily enough. An example here is with WebView. It may be that for links we do not necessary want to keep in the WebView, we would allow the default WebView processing for those links occur, to display the user’s choice of Web browser. But, we could always handle that ourselves inside of shouldOverrideUrlLoading() of a WebViewClient, so that is not too hard.

The trickiest situation, ironically, is the one that spawned this whole fiasco: Linkify. If we are using Linkify directly, or if we are using it indirectly via things like android:autoLink in a TextView, not only are we not calling startActivity() ourselves, but we do not even get control at the point in time of clicking a link to do something differently ourselves.

That is, unless we get tricky.

What Linkify does is examine text, search for particular patterns, then and convert those patterns into URLSpan objects inside of a Spannable. URLSpan just uses an ordinary startActivity() call, but we could replace those URLSpan objects with something else where we could specify what to do when the links are clicked. After all, URLSpan is simply a ClickableSpan, where we can override onClick(). And, we can use getSpans() to find the existing URLSpan objects, use removeSpan() to get rid of them, and use setSpan() to replace them with our own URLSpan alternative. TextView and android:autoLink can get more complicated, as we have to wait until TextView uses Linkify to change the text in the TextView before making our own changes to the Linkify output, or we could just skip android:autoLink and use Linkify ourselves.

Given that we can be the ones to make the startActivity() call, how do we give the user options where would not?

The simplest solution is to use createChooser() on the Intent class, passing in the ACTION_VIEW Intent that we ordinarily would start ourselves. createChooser() will return another Intent that we actually use with startActivity(), one that will always display a chooser if it is necessary (i.e., if there is more than one option for the Intent). This works, even on HtcLinkifyDispatcher-affected devices. And, in cases where you feel comfortable that patents are not an issue, it is fairly simple to add to your code.

If you are less comfortable with that, you may have to get more creative. After all, it is up to you and your qualified legal counsel to determine for yourselves what constitutes “display a pop-up menu of the linked actions” based upon “a user interface enabling the selection of a detected structure and a linked action”, as the terms are used in the patent that is behind all of these workarounds.

Android 4.1 (Jelly Bean), for example, slightly changes the chooser, to show a set of icons in a row or grid for the chooser, instead of a classic vertically-oriented menu-ish list. Whether this change was introduced based upon this patent, we may never know. But, you could consider similar sorts of light UI changes to the chooser structure:

  • Use a GridView of icons

  • Use a HorizontalScrollView of buttons in a horizontal LinearLayout, for a “swipe to find the app you want” motif

  • Use a Spinner, with some likely default choice, forcing the user to take an additional step to “display a pop-up menu of the linked actions”

  • Use a quick-actions pop-up bubble

  • And so on

Or, you could try to break the pattern entirely:

  • Upon clicking the link, raise a Notification that, in turn, displays some sort of chooser

  • Upon clicking the link, add a dynamic ShareActionProvider to your action bar that will display the options for the last-clicked-upon link

These sort of options more dramatically increase the separation between “user interface enabling the selection of a detected structure” and the “pop-up menu of the linked actions”, albeit at the cost of usability. And, again, it is up to you and qualified legal counsel to determine whether such steps as this are sufficient for your needs.

Many people complain about Android fragmentation, thinking of mundane things like screen sizes and densities. However, it is changes like HtcLinkifyDispatcher that represent the real fragmentation. Sometimes, we as developers simply see the fragmentation and assume the issue is the result of “pointed-haired bosses” at a device manufacturer, or some such. And, most likely, that is the case from time to time, as any firm the size of major device manufacturer will undoubtedly have some bosses whose hair is decidedly pointy.

But the HtcLinkifyDispatcher situation is one where the fragmentation is being triggered by external forces – effectively, by “pointy-haired bosses” from outside the Android ecosystem. Regardless of the origin of the fragmentation, though, we as app developers must do as we have always done: find these things, make sure we are aware of them, and cobble together workarounds as needed.