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("http://commonsware.com"));
if ("com.htc.HtcLinkifyDispatcher".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 com.htc.HtcLinkifyDispatcher
,
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 com.htc.HtcLinkifyDispatcher
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 com.htc.HtcLinkifyDispatcher
, 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 com.htc.HtcLinkifyDispatcher
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 horizontalLinearLayout
, 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.