How to gracefully fall back to website when Deep Link can't be handled by app
Asked Answered
L

7

14

The situation:

  1. You have an extensive mobile website, m.somewhere.com
  2. On Google Play you have an Android App that duplicates the key features of m.somewhere.com, but not all of them.
  3. Your Client/Employer/Investor has asked you to implement deep-linking for those urls that can be handled by the app.

TL;DR - how do you implement this?

My Approach So Far:

First instinct: match only certain urls and launch for them. Problem: paucity of expression in the AndroidManifest intent-filter prevents this (e.g. http://weiyang.wordpress.ncsu.edu/2013/04/11/a-limitation-in-intent-filter-of-android-application/).

As a subset of the problem, suppose the server at m.somewhere.com knows that any url that ends in a number goes to a certain page on the site, and the marketing guys are constantly futzing with the seo, so e.g.

I want to launch the app for:

http://m.somewhere.com/find/abc-12345
https://m.somewhere.com/shop/xyz-45678928492

But not for

http://m.somewhere.com/find/abc-12345-xyz
https://m.somewhere.com/about-us

no combination of path, pathPrefix, or pathPattern will handle this.

Best practice on stackoverflow (Match URIs with <data> like http://example.com/something in AndroidManifest) seems to be to catch everything, and then handle the situation when you get to onCreate() and realize you shouldn't have handled this particular url:

Android Manifest:

...
<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:scheme="http"
          android:host="m.somewhere.com"
          android:pathPattern=".*"/>
</intent-filter>
...    

Activity onCreate():

Intent i = getIntent()
String action = i.getAction();
Uri uri = i.getData();
if (Intent.ACTION_VIEW.equals(action) && cantHandleUrl(uri)) {
    // TODO - fallback to browser.
}

I have programmed something similar to the above that is working, but it leads to a very bad end-user experience:

  1. While browsing m.somewhere.com, there is a hiccup on every url click while the app is launched and then falls back.
  2. There is a nasty habit for a Chooser screen to popup for each and every link click on m.somewhere.com, asking the user which they would like to use (and the Android App is listed along with the browsers, but clicking on the Android App just launches the chooser screen again). If I'm not careful I get in an infinite relaunch loop for my app (if the user selects "Always"), and even if I am careful, it appears to the user that their "Always" selection is being ignored.

What can be done?

(EDIT: Displaying the site in a WebView in the app for unhandled pages is NOT an option).

Longley answered 22/1, 2015 at 18:50 Comment(3)
Where you able to find a solution to this?Peak
@DanielFalabella no, though the app in question is no longer relevant, so I'm not pressing for an answer any more.Longley
We got it working with Universal Links (only for IOS 9) -- incase anyone is interested: blog.hokolinks.com/…Peak
H
13

Late answer, but for future readers: if you're supporting a minimum of API level 15 then there's a more direct (less hacky) way of falling back to a browser for URLs you realize you don't want to handle, without resorting to disabling/re-enabling URL catching components.

nbarraille's answer is creative and possibly your only option if you need to support APIs lower than 15, but if you don't then you can make use of Intent.makeMainSelectorActivity() to directly launch the user's default browser, allowing you to bypass Android's ResolverActivity app selection dialog.

Don't do this

So instead of re-broadcasting the URL Intent the typical way like this:

// The URL your Activity intercepted
String data = "example.com/someurl"
Intent webIntent = new Intent(Intent.ACTION_VIEW, data);
webIntent.addCategory(Intent.CATEGORY_BROWSABLE);
startActivity(webIntent);

Do this

You would broadcast this Intent instead:

Intent defaultBrowser = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_BROWSER);
defaultBrowser.setData(data);
startActivity(defaultBrowser);

This will tell Android to load the browser app and data URL. This should bypass the chooser dialog even if they have more than one browser app installed. And without the chooser dialog you don't have to worry about the app falling into an infinite loop of intercepting/re-broadcasting the same Intent.

Caveat

You have to be okay with opening the URL (the one you didn't want to handle) in the user's browser. If you wanted to give other non-browser apps a chance to open the link as well, this solution wouldn't work since there is no chooser dialog.

Pitfalls

As far as I can tell, the only quirk from using this solution is that when the user clicks one of your deep links, they'll get to choose to open in your app or their browser, etc. When they choose your app and your internal app logic realizes it's a URL it doesn't want to intercept, the user gets shown the browser right away. So they choose your app but get shown the browser instead.

NOTE: when I say "broadcast" in this answer, I mean the general term, not the actual Android system feature.

Harlow answered 29/1, 2018 at 22:39 Comment(3)
Absolutely beautiful. This should definitely be the accepted answer. It is simple and works predictably. It's great for when you have app linking enabled but don't support certain links.Wench
In Android 13 this is crashing the app.Jezabella
In Android 13, the app throws an exception: android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] dat=https://... sel=act=android.intent.action.MAIN cat=[android.intent.category.APP_BROWSER]} }. See issuetracker.google.com/issues/243678703. In my opinion, we can indeed use a try-catch block to catch the exception and then proceed with opening the webview.Trichosis
A
5

There is a somewhat hacky way of doing this:

  • In the manifest, create an intent-filter for m.somewhere.com, to open a specific deeplink handler activity.
  • In that Activity, figure out if your app supports that URL or not.
  • If it does, just open whatever activity
  • If it doesn't, send a non-resolved ACTION_VIEW intent to be opened by your browser. The problem here, is that your app will also catch this intent, and this will create an infinite loop if your app is selected as the default handler for that URL. The solution is to use PackageManager.setComponentEnabledSetting() to disable your deeplink handler Activity before you send that intent, and re-enable it after.

Some example code:

public class DeepLinkHandlerActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Uri uri = intent.getData();
        Intent intent = makeInternallySupportedIntent(uri);
        if (intent == null) {
            final PackageManager pm = getPackageManager();
            final ComponentName component = new ComponentName(context, DeepLinkHandlerActivity.class);
            pm.setComponentEnabledSetting(component, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);

            Intent webIntent = new Intent(Intent.ACTION_VIEW);
            webIntent.setData(uri);
            context.startActivity(webIntent);

            AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {

                @Override
                protected Void doInBackground(Void[] params) {
                    SystemClock.sleep(2000);
                    pm.setComponentEnabledSetting(component, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
                    return null;
                    }
                };
             task.execute();
        } else {
            startActivity(intent);
        }
        finish();
    }
}

Hope that helps.

Note: It looks like you need to delay the re-enabling by a couple of seconds for this to work.

Note 2: For a better experience, using a Transparent theme for your activity will make it look like your app didn't even open.

Note 3: If for some reason your app crashes or gets killed before the component re-registers, you're loosing deep link support forever (or until next update/reinstall), so I would also do the component re-enabling in App.onCreate() just in case.

Adria answered 22/1, 2016 at 23:53 Comment(1)
Awesome solution. Thanks!Relique
B
1

URX provides a free tool (urxlinks.js) that automatically redirects mobile web users into an app if the app is installed. The documentation is available here: http://developers.urx.com/deeplinks/urx-links.html#using-urx-links-js

Bovine answered 26/1, 2015 at 22:47 Comment(0)
C
0

If two apps are using same scheme then the chooser screen will be popped as android wont know which app the link is intended for. Using custom scheme for your app might solve this issue. But still you can't be sure no one else will use that scheme.

Calumnious answered 28/1, 2015 at 16:58 Comment(0)
Y
0

It sounds like you're trying to treat your mobile app and mobile website as extensions of the same experience. That's good practice, generally speaking, but at this point the two are simply not at parity. At least until they reach parity I would not recommend automatically pushing the end user into your mobile app because users who are deliberately using the mobile site in order to find the content your app is missing will find this incredibly frustrating.

Instead, it might make sense to use a smart banner to encourage users on the mobile website pages that do have an in-app equivalent to open the app instead. Those banners would be your deeplinks. You could create them yourself or integrate a tool like Branch ( https://branch.io/universal-app-banner/ ) that handles deep linking and smart banners both.

That last part of your question has to do with where to place the deep links. One advantage to using smart banners instead of redirects is that you can embed them into the appropriate templates on your CMS instead of needing to rely on url detection.

Good luck!

Yestreen answered 28/9, 2015 at 17:11 Comment(0)
F
0

This was my solution to your second problem. PackageManager.queryIntentActivities() will give you the list of apps/activities that would appear in the chooser. Iterate through the list (which should at least include the browser) and find an activity whose package name doesn't match the current app, and set the intent class name to it, then launch an Activity with that intent and call finish();

public Intent getNotMeIntent(Uri uri) {
    Intent intent = new Intent(Intent.ACTION_VIEW, uri);

    PackageManager manager = context.getPackageManager();
    List<ResolveInfo> infos = manager.queryIntentActivities(intent, 0);
    for (int i = 0; i < infos.size(); i++) {
        ResolveInfo info = infos.get(i);
        // Find a handler for this url that isn't us
        if (!info.activityInfo.packageName.equals(context.getPackageName())) {
            intent.setComponent(null);
            intent.setClassName(info.activityInfo.packageName, info.activityInfo.name);
            return intent;
        }
    }

    // They have no browser
    return null;
}

The Transparent theme (mentioned above) should be a good solution for the first problem.

Fda answered 18/1, 2017 at 22:57 Comment(0)
N
0

In destination activity in onCreate set this code for Kotlin:

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   handleIntent(intent)
}

‌private fun handleIntent(intent: Intent?) {
   val appLinkAction: String? = intent?.action
   val appLinkData: Uri? = intent?.data
   showDeepLinkData(appLinkAction, appLinkData)
}

private fun showDeepLinkData(appLinkAction: String?, appLinkData: Uri?) {
   if (Intent.ACTION_VIEW == appLinkAction && appLinkData != null) {
       val promotionCode = appLinkData.getQueryParameter("exampleQueryString")
       Log.e("TAG", "Uri is: $appLinkData")
   }
}
Nyaya answered 1/6, 2021 at 6:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.