You may have noticed that Android supports a market:
URL scheme.
Web pages can use such URLs so that, if they are viewed on an Android
device’s browser, the user can be transported to a Play Store
page, perhaps for a specific app or a list of apps for a publisher.
Fortunately, that mechanism is not limited to Android’s code —
you can get control for various other types of links as well. You do
this by adding certain entries to an activity’s <intent-filter>
for
an ACTION_VIEW
Intent
.
However, be forewarned that this capability is browser-specific. What works on the original Android “Browser” app and Google’s Chrome may not necessarily work on Firefox for Android or other browsers.
Understanding this chapter requires that you have read the chapter on
Intent
filters.
First, any <intent-filter>
designed to respond to browser links
will need to have a <category>
element with a name of
android.intent.category.BROWSABLE
. Just as the LAUNCHER
category
indicates an activity that should get an icon in the launcher, the
BROWSABLE
category indicates an activity that wishes to respond to
browser links.
You will then need to further refine which links you wish to respond
to, via a <data>
element. This lets you describe the URL and/or
MIME type that you wish to respond to. For example, here is the
AndroidManifest.xml
file from the
Introspection/URLHandler
sample project:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.commonsware.android.urlhandler" android:versionCode="1" android:versionName="1.0">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="19"/>
<supports-screens android:largeScreens="true" android:normalScreens="true" android:smallScreens="false"/>
<application android:icon="@drawable/ic_launcher" android:label="@string/app_name">
<activity android:name="URLHandler" android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:mimeType="application/pdf"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:host="www.this-so-does-not-exist.com" android:path="/something" android:scheme="http"/>
</intent-filter>
<intent-filter>
<action android:name="com.commonsware.android.MY_ACTION"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
</intent-filter>
</activity>
</application>
</manifest>
Here, we have four <intent-filter>
elements for our one activity:
LAUNCHABLE
categoryapplication/pdf
), and that we will respond to browser links
(BROWSABLE
category)"http"
) for a certain Web site (host of
"www.this-so-does-not-exist.com"
and path of /something
),
and that we will respond to
browser links (BROWSABLE
category)BROWSABLE
category) — we will examine this more closely in
the next section
What happens for the first two links varies based on browser.
The original Android “Browser” app, and Google Chrome, will do the following:
Notification
in the status bar),
the user will have URLHandler
as one of the options in the chooser
to view the PDF file.http://www.this-so-does-not-exist.com/something
will bring up a chooser showing all available Web browser, plus
URLHandler
, as expectedFirefox for Android will treat the PDF link the same way. However,
Firefox for Android does not check the URL for the second link to see
if there is anything else supporting ACTION_VIEW
for the URL, and so it
always loads up the Web page. You see this effect with the link to
Barcode Scanner as well — even though a device has Barcode Scanner installed,
Firefox never offers that as an option.
Responding to MIME types makes complete sense… if we implement something designed to handle such a MIME type.
Responding to certain schemes, hosts, paths, or file extensions is certainly usable, but other than perhaps the file extension approach, it makes your application a bit fragile. If the site changes domain names (even a sub-domain) or reorganizes its site with different URL structures, your code will break.
If the goal is simply for you to be able to trigger your own
application from your own Web pages, though, the safest approach is
to use an intent:
URL. These can be generated from an Intent
object by calling toUri(Intent.URI_INTENT_SCHEME)
on a
properly-configured Intent
, then calling toString()
on the
resulting Uri
.
For example, the intent:
URL for the fourth <intent-filter>
from
above is:
intent:#Intent;action=com.commonsware.android.MY_ACTION;end
This is not an official URL scheme, any more than market:
is, but
it works for Android devices. When the Android built-in Browser
encounters this URL, it will create an Intent
out of the
URL-serialized form and call startActivity()
on it, thereby
starting your activity. Chrome also supports this URL structure.
Firefox for Android does not, indicating instead that it cannot
recognize the URL.
Your activity can then examine the Intent
that launched it to
determine what to do. In particular, you will probably be interested
in the Uri
corresponding to the link — this is available via
the getData()
method. For example, here is the URLHandler
activity for this sample project:
package com.commonsware.android.urlhandler;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
public class URLHandler extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
TextView uri=(TextView)findViewById(R.id.uri);
if (Intent.ACTION_MAIN.equals(getIntent().getAction())) {
String intentUri=(new Intent("com.commonsware.android.MY_ACTION"))
.toUri(Intent.URI_INTENT_SCHEME)
.toString();
uri.setText(intentUri);
Log.w("URLHandler", intentUri);
}
else {
Uri data=getIntent().getData();
if (data==null) {
uri.setText("Got com.commonsware.android.MY_ACTION Intent");
}
else {
uri.setText(getIntent().getData().toString());
}
}
}
public void visitSample(View v) {
startActivity(new Intent(Intent.ACTION_VIEW,
Uri.parse("https://commonsware.com/sample")));
}
}
This activity’s layout has a TextView
(uri) for showing a Uri
and
a Button
to launch a page of links, found on the CommonsWare site
(https://commonsware.com/sample
). The Button
is wired to call
visitSample()
, which just calls startActivity()
using the
aforementioned URL to display it in the user’s chosen Web browser.
When the activity starts up, though, it first loads up the
TextView
. What goes in there depends on how the activity was
launched:
MAIN
),
then we display in the TextView
the intent:
URL shown in the
previous section, generated from an Intent
object designed to
trigger our fourth <intent-filter>
. This also gets dumped to
LogCat, and is how the author got this URL in the first place to put
on the sample Web page of links.Uri
from the launching Intent
is null
, though,
that means the activity was launched via the custom intent:
URL
(which only has an action string), so we put a message in the
TextView
to match.Uri
from the launching Intent
will have
something we can use to process the link request. For the PDF file,
it will be the local path to the downloaded PDF, so we can open it.
For the www.this-so-does-not-exist.com
URL, it will be the URL
itself, so we can process it our own way.Note that for the PDF case, clicking the PDF link in the Browser will
download the file in the background, with a Notification
indicating
when it is complete. Tapping on the entry in the notification drawer
will then trigger the URLHandler
activity.
Also, bear in mind that the device may have multiple handlers for
some URLs. For example, a device with a real PDF viewer will give the
user a choice of whether to launch the downloaded PDF in the real
view or URLHandler
.
We have had the ability to have activities with <intent-filter>
elements
that support custom schemes (e.g., myapp://
) since Android 1.0. The
benefit over using a custom scheme is that, if it is unique on the device,
an Intent
for that custom scheme will go straight to the desired activity.
However, this approach had a lot of flaws:
ACTION_VIEW
Intent
on the desired Uri
ACTION_VIEW
Intent
, and the app handling that custom scheme was not
installed, the request would simply failUsing an <intent-filter>
advertising support for some http
or
https
URL would improve the results for the latter two issues, as
many more apps would recognize the URL as being a URL, and usually
the fallback would be to have a browser open up on that URL. However,
now it is guaranteed that the scheme is not unique. Users would
initially get a chooser, to determine what activity should handle
the request. This can be confusing, particularly since the chooser
does not really indicate the scope of the choice (would I be saying
that XYZ app is now handling all Web links?).
Android 6.0 added an interesting solution for this. If you
use a <intent-filter>
for a domain that you control, you can publish
a bit of metadata, as a JSON file, on the Web server. Android can be
taught to sniff for that metadata and use it to validate that the app
was developed by the same person or group that runs the server for the
identified domain. In that case, Android will bypass the chooser
and go straight to the activity with the domain-specific <intent-filter>
.
The cited example would be Twitter doing this, so any link click on a
twitter.com
URL would bring up the Twitter app, not a Web browser.
Of course, these links are only so useful. They are fine for when a
link appears in an ordinary app. Web browsers, however, tend not to
actually see whether a URL they encounter is handled by some on-device
app. Android 6.0 does not change this behavior. So,
links on Web pages viewed in 2015 versions of Firefox will not honor your
desired <intent-filter>
regardless of whether you are using this
new app link system or not. Chrome’s behavior varies by version.
That being said, app links still have their uses (e.g., responding to links from social media posts).
Supporting an <intent-filter>
for some http
or https
URL has
been possible since Android 1.0. The only thing that is different is that
now you can add an android:autoVerify="true"
to the <intent-filter>
element, to tell Android that you would like it to verify the connection
between the app and the domain used in the <intent-filter>
, to skip the
chooser when URLs for that domain trigger your <intent-filter>
.
For example, the
Introspection/URLHandlerMNC
sample project is a revised version of the URLHandler
sample, one that
switches its http
<intent-filter>
to look for https://commonsware.com
URLs, and it incorporates android:autoVerify="true"
:
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="commonsware.com"
android:scheme="https" />
</intent-filter>
On pre-Marshmallow versions of Android, this attribute will be ignored, as it will not be recognized. But, on Android 6.0+, this attribute will be used to attempt to validate that your app was written by somebody who owns the specified domain.
The author of this book owns the commonsware.com
domain.
To actually run this project
and have the updated app linking work, you would need to switch this to
be some domain that you control.
Note that while android:autoVerify="true"
is written at the scope
of a single <intent-filter>
, it affects all activities and all
<intent-filter>
structures. All of them that use http
or
https
as the android:scheme
must support the app links protocol
described in this chapter. You cannot have some filters supporting
app links and others not — either they all support app links, or none
will.
When Android installs an app that has one or more <intent-filter>
elements with android:autoVerify="true"
, it will attempt to find
a JSON file on the identified server. Specifically, for the sample
app, Android will create a URL of the form:
https://commonsware.com/.well-known/assetlinks.json
In your app, commonsware.com
would be replaced with the
domain you have in your <intent-filter>
.
This URL is part of a proposed IETF standard that unfortunately does not appear to be formally documented.
Android 6.0+ will use HTTPS to retrieve your assetlinks.json
file, regardless of the scheme
that you use in the <intent-filter>
. Also, the JSON needs to be publicly accessible, without any forms of
authentication. And, the JSON needs to be served with a MIME type
of application/json
.
The JSON content itself is an array of JSON objects, one object per application ID that you publish as an app:
[
{
"relation"
:
[
"delegate_permission/common.handle_all_urls"
],
"target"
:
{
"namespace"
:
"android_app"
,
"package_name"
:
"com.commonsware.android.urlhandler"
,
"sha256_cert_fingerprints"
:
[
"A9:99:84:D8:...:60:5B:CB:E3"
]
}
}
]
(the sha256_cert_fingerprints
value is shown truncated for easier reading)
Here, the only two variable bits are:
package_name
, which will be your application ID, andsha256_cert_fingerprints
array, which will list the SHA256
hashes of your public signing keys, for whatever keystores you might
be using for this app (e.g., your debug keystore and your production
keystore)To get the SHA256 hash of your public signing key, you will need to use
the keytool
command from your Java SDK (Java 7 or higher required):
keytool -list -v -keystore ...
where ...
is the path to your keystore (e.g., ~/.android/debug.keystore
for your debug keystore on OS X and Linux).
You will need to provide the password to the keystore. For the debug
keystore, this is android
.
As part of the output, you will get the SHA256 hash:
Keystore type: JKS
Keystore provider: SUN
Your keystore contains 1 entry
Alias name: androiddebugkey
Creation date: Aug 7, 2011
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=Android Debug, O=Android, C=US
Issuer: CN=Android Debug, O=Android, C=US
Serial number: 4e3f2684
Valid from: Sun Aug 07 19:57:56 EDT 2011 until: Tue Jul 30 19:57:56 EDT 2041
Certificate fingerprints:
MD5: 98:84:0E:36:F0:B3:48:9C:CD:13:EB:C6:D8:7F:F3:B1
SHA1: E6:C5:81:EB:8A:F4:35:B0:04:84:3E:6E:C3:88:BD:B2:66:52:E7:09
SHA256: A9:99:84:D8:...:60:5B:CB:E3
Signature algorithm name: SHA1withRSA
Version: 3
*******************************************
*******************************************
(the SHA256 value is shown truncated for easier reading)
That long set of hex digits will need to go in the
sha256_cert_fingerprints
JSON array.
The rest of the JSON is fixed. Try not to introduce other JSON properties and
such into this file, as they may cause your file to fail validation.
However, you can have multiple JSON objects for multiple apps, each
providing the relation
and target
properties.
Our URLHandler
activity not only responds to http://misc.commonsware.com
URLs, but it uses one if the user taps the “view-sample” button:
package com.commonsware.android.urlhandler;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
public class URLHandler extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
if (Intent.ACTION_VIEW.equals(getIntent().getAction())) {
findViewById(R.id.visit).setEnabled(false);
}
}
public void visitSample(View v) {
startActivity(new Intent(Intent.ACTION_VIEW,
Uri.parse("https://commonsware.com/Android/")));
}
}
There, we launch an ACTION_VIEW
Intent
on a
http://commonsware.com/Android
URL via startActivity()
.
On a pre-Marshmallow device, this startActivity()
request will normally bring
up a chooser, offering the URLHandler
activity along with Web
browsers and potentially other apps.
On an Android 6.0+ device, in the normal case, if the server is configured properly with the above
JSON, and if the app was compiled by the author of this book, the chooser
is bypassed, and the user gets another instance of URLHandler
. The
“another instance” part can be controlled via Intent
flags
or manifest entries, as is covered in the chapter on tasks.
However, this is not assured:
Another thing that can change the behavior to return is if the user revokes the app link. Users can do this by going to the app’s screen in the Settings app and clicking the “Open by default” option:
Figure 886: URLHandlerMNC in Settings, “Open by default” Visible
If the user taps that entry, one section of the next screen is entitled “App links” and gives the user the option to toggle the app link behavior off:
Figure 887: URLHandlerMNC in Settings, “Open by default” Screen
Unfortunately, the labeling here does not seem to work properly. The “Ask every time” choice shown selected here actually bypasses the chooser. The available choices are “open in this app”, “ask every time”, and “don’t open in this app”:
Figure 888: URLHandlerMNC in Settings, “Open supported links” Options
You can confirm that other parties can see your assetlinks.json
file
by visiting the following URL:
https://digitalassetlinks.googleapis.com/v1/statements:list?
source.web.site=https://DDDDD&
relation=delegate_permission/common.handle_all_urls
(NOTE: the URL shown above is split across several lines for readability but should be all on one line when actually using the URL)
Replace DDDDD
with the domain name for your site, and you should
get a JSON document back that, among other things, contains
the details from your assetlinks.json
file:
{
"statements"
:
[
{
"source"
:
{
"web"
:
{
"site"
:
"https://commonsware.com."
}
},
"relation"
:
"delegate_permission/common.handle_all_urls"
,
"target"
:
{
"androidApp"
:
{
"packageName"
:
"com.commonsware.android.urlhandler"
,
"certificate"
:
{
"sha256Fingerprint"
:
"A9:99:84:D8:...:60:5B:CB:E3"
}
}
}
}
],
"maxAge"
:
"3213.779933024s"
}
(sha256Fingerprint
truncated for readability)
If you try visiting that URL, and there is no assetlinks.json
file available for that domain, you will get a JSON response back
containing a debugString
indicating the nature of the problem.
You can see if an Android device in your lab has successfully performed
the app link validation by running the adb shell dumpsys package domain-preferred-apps
command. This will list all of the apps that have app links, and your
app should appear among them, in a stanza like this one:
Package: com.commonsware.android.urlhandler
Domains: commonsware.com
Status: never
The status will reflect the user’s choice of how to handle your
app link inside of Settings (the never
shown above indicates that
the user decided to ignore your app link and have your app never
handle such URLs).