25

We have an app that handles a custom URL scheme (vstream://). When someone comes to a web page that has some vstream:// content, we need to redirect them to the store if they don't have our app installed.

In iOS, we do this:

setTimeout(function() {
  window.location =
    "itms://itunes.apple.com/us/app/kaon-v-stream/id378890806?mt=8&uo=4";
}, 25);

window.location = "vstream:view?code=...stuff...";

If the window.location assignment fails, the timeout jumps over the App Store before the dialog box comes up. (I found this technique here: Is it possible to register a http+domain-based URL Scheme for iPhone apps, like YouTube and Maps? .)

Unfortunately, this trick is not working in Android. We detect the device server side and wrote this instead of the itms: line:

"market://details?id=com.kaon.android.vstream";

Trouble is, whereas iOS throws an error when you go to an unhandled url scheme, Android goes to a generated page. Therefore, the timeout never gets a chance to run.

Is there some way on a web page to explicitly test for whether a custom URL scheme is handled, or can someone suggest a hack like this one that will work in Android? (Of course, I suppose I need a hack that's going to work no matter what browser they are using, which is probably a tall order...)

UPDATE: The approaches below do not work in Jelly Bean on a Nexus 7. The new Chrome browser does not go to a generated page (so the iFrame is not needed), but there does not appear to be any way to know whether the URL scheme was handled. If it was, the timeout fires anyway. If it wasn't handled the timeout fires. If I use an onload handler and an iframe, the onload handler never fires (whether the app is installed or not). I'll update if I ever figure out how to know whether the scheme was handled...

I've removed my "Solved" on my previous solution, since it doesn't work any more.

UPDATE 2: I have a good cross-platform solution now that works on iOS, Android 4.1 with Chrome, and Android pre-Chrome. See below...

Update 3: Google broke everything again with intents. Check out the VERY nice solution I've accepted by amit_saxena down there someplace /

Community
  • 1
  • 1
Joshua Smith
  • 3,689
  • 4
  • 32
  • 45
  • Sounds like you want to read about how Intents work in Android: http://developer.android.com/guide/topics/intents/intents-filters.html – CrackerJack9 Aug 29 '11 at 14:13
  • That isn't a very helpful comment. Our intents work just fine. If our app is installed on the device, the web page launches our app. The question is how to detect whether our app has been installed from the web page, so we know whether to send them to the app or to the store. – Joshua Smith Aug 29 '11 at 15:57
  • If your app (and subsequently your Intent) is not installed/registered, a request with a protocol of `vstream` will not go anywhere. So just wrap your timeout with another timeout, since itms: would not be registered on Android (afaik) either. – CrackerJack9 Aug 29 '11 at 15:59
  • I'll update the question to show clearly what we tried on Android. – Joshua Smith Aug 29 '11 at 16:42
  • okay thanks, I think I'm missing some piece of this – CrackerJack9 Aug 29 '11 at 16:53
  • so you own the website, and want some code on it to redirect an Android's browser to the market if they don't have your app to handle vstream protocol? – CrackerJack9 Aug 29 '11 at 17:06

7 Answers7

18

UPDATE: Google broke this. See the new accepted answer instead.

The key, it turns out, is the document.webkitHidden property. When you set window.location to a custom URL scheme and it opens, the browser keeps running, but that property goes to false. So you can test it to determine whether the custom URL scheme was handled.

Here's a sample, which you can view live

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Starting App...</title>
<script>

var URL = "kaonkaon://product.html#malvern;6";
var MARKET = "market://details?id=com.kaon.android.lepton.kaon3d";
var ITUNES = "itms://itunes.apple.com/us/app/kaon-interactive-3d-product/id525051513?mt=8&uo=4";
var QR = "http://goo.gl/gz07g"; // this should be a shortened link back to this page

function onLoad() {

    if (navigator.userAgent.match(/Android/)) {

        if (navigator.userAgent.match(/Chrome/)) {

            // Jelly Bean with Chrome browser
            setTimeout(function() {
                if (!document.webkitHidden)
                    window.location = MARKET;
            }, 1000);

            window.location = URL;

        } else {

            // Older Android browser
            var iframe = document.createElement("iframe");
            iframe.style.border = "none";
            iframe.style.width = "1px";
            iframe.style.height = "1px";
            var t = setTimeout(function() {
                window.location = MARKET;
            }, 1000);
            iframe.onload = function () { clearTimeout(t) };
            iframe.src = URL;
            document.body.appendChild(iframe);

        }

     } else if (navigator.userAgent.match(/iPhone|iPad|iPod/)) {

         // IOS
         setTimeout(function() {
             if (!document.webkitHidden)
                 window.location = ITUNES;
         }, 25);

         window.location = URL;

     } else {

         // Not mobile
         var img = document.createElement("img");
         img.src = "https://chart.googleapis.com/chart?chs=300x300&cht=qr&chl="+encodeURIComponent(QR);
         document.body.appendChild(img);
     }
}
</script>
  </head>
  <body onload="onLoad()">
  </body>
</html>
Michael Kohne
  • 11,888
  • 3
  • 47
  • 79
Joshua Smith
  • 3,689
  • 4
  • 32
  • 45
  • In case anyone else runs into this in the next couple weeks: I was testing the above solution on iOS Chrome, where it currently fails; I burned some time trying to fix it there but it turns out that it's a known bug that's been fixed in the next release version. http://code.google.com/p/chromium/issues/detail?id=157028 – Dave Jan 17 '13 at 22:25
  • Dave, just to be clear, do you mean the Chrome browser on iOS? – Joshua Smith Jan 18 '13 at 20:00
  • that's right. Great little browser. It's just a standard iOS web view with some clever UI around it, so most things that work in Mobile Safari should work fine in iOS Chrome, but this particular thing is (just for now) different. – Dave Jan 22 '13 at 17:57
  • 1
    Just tried this on Android 4.2.2 and Chrome 25 and in the case where the app is not installed, it just ends up on a Chrome error page (ERR_UNKNOWN_URL_SCHEME). It almost seems like this bug has resurfaced - https://code.google.com/p/chromium/issues/detail?id=157028 except on Android instead of iOS. – Marc Novakowski Mar 26 '13 at 18:19
  • FYI, filed a Chrome bug for this (on Android) - https://code.google.com/p/chromium/issues/detail?id=224097 - hopefully they fix it like they fixed it for iOS – Marc Novakowski Mar 26 '13 at 22:36
  • Here's an update - heard back from Google and apparently for Chrome 25 and higher, they recommend using a new "intent:" URL syntax vs. iframes. See https://developers.google.com/chrome/mobile/docs/intents. I've confirmed that this method works on Chrome 25+. – Marc Novakowski May 03 '13 at 16:47
  • I suppose it's good that this is supported; however, the documentation implies that it has to be done via a user gesture. If that's true, then having a page take to you to an app automatically (onload) is not going to work. Can you confirm if that's true, Marc? (Apple ensures the user wants the app to launch by prompting the user, which strikes me as a better approach.) – Joshua Smith May 03 '13 at 20:15
  • 1
    I was able to get it to work automatically by putting the "intent:" URL in a hidden tag and calling click() on it in the document onload handler. – Marc Novakowski May 27 '13 at 16:14
  • a lame question, what does this `document.webkitHidden` plays a role here..any light on this will be helpful, thnx :) – amit karsale Oct 28 '13 at 05:49
  • If the intent worked, and the app started, then document.webkitHidden goes true. If it didn't work, then document.webkitHidden stays false. So you can test that to see if the intent worked. However, you should look through all the answers here, because it seems like google has changed everything a bunch of times since I started this thread a hundred years ago. And my solution might not work any more. The one with "intent" hrefs, below, looks particularly interesting. – Joshua Smith Oct 28 '13 at 15:40
  • @jesmith i followed the same... but if the app is not installed, it is not redirecting to MARKET URL. Please reply if you have any remedy – Sankar M Dec 16 '14 at 10:18
  • It seems to me that the `intent:` syntax requires user gesture and Chrome for Android is caching that user gesture. To reproduce: **1)** Create a page with `intent:` hyperlink. Tap on it in Chrome for Android. The app or Google Play Store should open. **2)** Modified the page to click on the link via Javascript. **3)** Reload the page. Result of 1) should happen. **4)** close the webpage tab in Chrome. **5)** open a new tab and go to the web page of 2) (with auto click via Javascript). Webpage not available error appears. _(As of Chrome version 40.0.2214.89)_ – cychoi Feb 03 '15 at 19:58
15

Below is a working code snippet for most of the android browsers:

<script type="text/javascript">
    var custom = "myapp://custom_url";
    var alt = "http://mywebsite.com/alternate/content";
    var g_intent = "intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;end";
    var timer;
    var heartbeat;
    var iframe_timer;

    function clearTimers() {
        clearTimeout(timer);
        clearTimeout(heartbeat);
        clearTimeout(iframe_timer);
    }

    function intervalHeartbeat() {
        if (document.webkitHidden || document.hidden) {
            clearTimers();
        }
    }

    function tryIframeApproach() {
        var iframe = document.createElement("iframe");
        iframe.style.border = "none";
        iframe.style.width = "1px";
        iframe.style.height = "1px";
        iframe.onload = function () {
            document.location = alt;
        };
        iframe.src = custom;
        document.body.appendChild(iframe);
    }

    function tryWebkitApproach() {
        document.location = custom;
        timer = setTimeout(function () {
            document.location = alt;
        }, 2500);
    }

    function useIntent() {
        document.location = g_intent;
    }

    function launch_app_or_alt_url(el) {
        heartbeat = setInterval(intervalHeartbeat, 200);
        if (navigator.userAgent.match(/Chrome/)) {
            useIntent();
        } else if (navigator.userAgent.match(/Firefox/)) {
            tryWebkitApproach();
            iframe_timer = setTimeout(function () {
                tryIframeApproach();
            }, 1500);
        } else {
            tryIframeApproach();
        }
    }

    $(".source_url").click(function (event) {
        launch_app_or_alt_url($(this));
        event.preventDefault();
    });
</script>

You need to add source_url class to the anchor tag.

I have blogged more about it here:

http://aawaara.com/post/88310470252/smallest-piece-of-code-thats-going-to-change-the

amit_saxena
  • 7,450
  • 5
  • 49
  • 64
  • 1
    Brilliant! I'm not sure when I'll get a chance, but I'm basically going to replace everything I've done so far with this snippet. – Joshua Smith Jun 10 '14 at 20:27
  • 1
    This works. I was also able to pass a parameter into my app, using any browser I tested. – Paranoid Android Oct 30 '14 at 16:27
  • Just a question, what happens on Chrome < 25? – Paranoid Android Oct 30 '14 at 16:42
  • Don't exactly remember, but I believe the iframe approach will work on those. You can identify the user agent and use the iframe approach for chrome < 25. Since it's significantly old version, so I think the usage will be very small. If you really need it, implement what I described above. – amit_saxena Oct 30 '14 at 23:44
  • Do you have a working example? I noticed you are using jquery, added that and tried a few examples with little luck. – Shane Nov 28 '14 at 19:21
  • @Shane - the blog post at the bottom has a working gist which I use in my apps. – amit_saxena Nov 28 '14 at 21:50
  • @amit_saxena yes I had seen this, I was able to create a more simplified intent that works without your code base, but for some reason does not seem to tie into your script, to cover other scenarios. Do you have the actual html you used to get yours working? – Shane Nov 28 '14 at 22:35
  • Intents work only in later versions of chrome. Which part isn't working for you? If you can pass on a hosted HTML (or anything else that I can take a look at), I can try and help debug. This is working js snippet that I use in production (and intent is a separate thing that you need to define in your android app manifest). This js snippet simply falls back to intent approach for chrome, and uses other approaches for other browsers. – amit_saxena Nov 28 '14 at 22:58
  • @amit_saxena How do we can pass querystring on Intent. here is my question http://stackoverflow.com/questions/27503820/android-custom-url-scheme-with-query-string-values – Sankar M Jan 16 '15 at 05:21
14

This is the answer who will save you all !

https://developers.google.com/chrome/mobile/docs/intents

<a href="intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;end"> Take a QR code </a>

It will your url schestart me if the app is installed ortherwise it will start the market at the indicated package

Christopher-BZC
  • 155
  • 1
  • 7
6

@jesmith, this is a clean version that fixes double action on Android.

if (navigator.appVersion.indexOf('iPhone') > -1) {
  setTimeout(function noapp() { window.location="http://itunes.apple.com/app/id378890806?mt=8"; }, 25);
  window.location = 'vstream:';
}
else if (navigator.userAgent.indexOf('Android') > -1) {
  var iframe = document.createElement('iframe');
  iframe.style.visibility = 'hidden';
  iframe.src = 'vstream:';
  iframe.onload = function noapp() { window.location="market://details?id=com.kaon.android.vstream"; };
  document.body.appendChild(iframe);
}
vmus
  • 61
  • 1
  • 4
  • Interesting approach. So the onload handler for a frame gets called even when that frame is unable to load the specified content? – Joshua Smith Mar 21 '12 at 18:23
4

Solved! The trick is to open my app in an IFRAME, instead of setting the location:

setTimeout(function() {
  window.location =
    "market://details?id=com.kaon.android.vstream";
}, 1000);

document.write('<iframe style="border:none; width:1px; height:1px;" src="vstream:view?code='+code+'"></iframe>');

Notice that I increased the timeout to 1000, because Android actually does both actions in every case (not ideal, but not awful), and this larger timeout is needed to make sure that Market doesn't end up being the thing the user sees when I'm already installed.

(And yes, of course using document.write is so last-century, but I'm old school that way :)

Joshua Smith
  • 3,689
  • 4
  • 32
  • 45
  • easy way to fix it doing both options is getting the variable of the settimeout, and adding window.clearTimeout(num) to the onload event of that iframe. that way if the iframe loads, it runs the code to cancel the window.location timeout. – benpage Dec 07 '11 at 05:12
  • Would the onload of that iframe would even execute, since it is launching another app? – Joshua Smith Dec 09 '11 at 14:58
2

For some reasons, the final solution does not work for me on android (Is it just me?!!). The key is that iframe.onload function is NOT executed when your app is installed, and it IS executed when your app is NOT installed.

The solution becomes a little simpler actually. Here is the segment for "Older Android Browser" part:

    } else {

        // Older Android browser
        var iframe = document.createElement("iframe");
        iframe.style.border = "none";
        iframe.style.width = "1px";
        iframe.style.height = "1px";
        iframe.onload = function () { window.location = MARKET; };
        iframe.src = URL;
        document.body.appendChild(iframe);

    }
Jun Sun
  • 21
  • 1
  • 1
    When the app is not installed, the unit we tested on put up a generated page, that did fire its onload handler. If you don't have the onload handler in there, do you see a generated page? – Joshua Smith Oct 05 '12 at 22:38
  • That solved the problem for us as well. Iframe.onload never executes when the App IS installed. I guess because nothing will be loaded in the iframe itself when the app is launched, so there will never be a onload event fired. If the app is NOT installed the onload will trigger for the "site not found" message which is displayed inside the iframe in this case. We still have to check if this is intended behaviour or some strange bug in the android version we are testing on. – bjunix May 21 '14 at 11:50
1

Embed a http server in your app as a Service, listen to a local port(higher than 1024, ex: 8080), send a request from Browser to 127.0.0.1:8080. If your app installed(and service running), start it, if request fail, goto google play.

javamonk
  • 180
  • 1
  • 7
  • That's really clever. Except that the user would probably freak out when they see we have a service running in our app that is accepting connections. – Joshua Smith Jan 15 '15 at 15:48