Responding to URLs

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.

Prerequisites

Understanding this chapter requires that you have read the chapter on Intent filters.

Manifest Modifications

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>

(from Introspection/URLHandler/app/src/main/AndroidManifest.xml)

Here, we have four <intent-filter> elements for our one activity:

What happens for the first two links varies based on browser.

The original Android “Browser” app, and Google Chrome, will do the following:

Firefox 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.

Creating a Custom URL

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.

Reacting to the Link

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")));
  }
}
(from Introspection/URLHandler/app/src/main/java/com/commonsware/android/urlhandler/URLHandler.java)

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:

  1. If it was launched via the launcher (e.g., the action is 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.
  2. If it was not launched via the launcher, it was launched from a Web link. If the 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.
  3. Otherwise, the 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:

Using 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).

Setting Up the IntentFilter

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>

(from Introspection/URLHandlerMNC/app/src/main/AndroidManifest.xml)

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.

Setting Up the JSON

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:

  1. The package_name, which will be your application ID, and
  2. The sha256_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.

Results

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/")));
  }
}
(from Introspection/URLHandlerMNC/app/src/main/java/com/commonsware/android/urlhandler/URLHandler.java)

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:

User Intervention

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:

URLHandlerMNC in Settings, Open by default Visible
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:

URLHandlerMNC in Settings, Open by default Screen
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”:

URLHandlerMNC in Settings, Open supported links Options
Figure 888: URLHandlerMNC in Settings, “Open supported links” Options

Testing Your Setup

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).