29

Any way to read a constant depending buildType ${deepLinkHost}?

debug -> deepLinkUri = http://link.debug/
staging -> deepLinkUri = http://link.staging/
release ->  deepLinkUri=  http://link/
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/navigation_home"
    app:startDestination="@id/fragment_home">

    <fragment
        android:id="@+id/fragment_home"
        android:name="..."
        tools:layout="@layout/fragment_home">
        <argument
            android:name="token"
            android:defaultValue="@null"
            app:argType="string"
            app:nullable="true" />
        <deepLink app:uri="${deepLinkUri}/?code={token}" />
</fragment>

This was manage before with manifestPlaceholders.deepLinkHost on build.gradle and deeplinks by activity in the AndroidManifest, but once google uses 1 Activity to N Fragments, how can we manage it with navigation components?

rafaelasguerra
  • 2,685
  • 5
  • 24
  • 56

5 Answers5

26

It looks like it's not currently supported out of the box, but there is a pretty simple workaround. Normally registering a deep link requires two steps:

  1. Add deepLink to the navigation graph. Since we can't specify any substitution or a @string resource as the uri, let's just define the host name as a variable: <deepLink app:uri="http://{deepLinkHost}/?code={token}" />. This link will match any host and pass deepLinkHost as a parameter.
  2. Register an intent-filter in the AndroidManifest.xml, so our activity actually reacts on the deep link. The recommended approach for the latest android studio is to add <nav-graph android:value="@navigation/nav_graph" /> to the manifest, so it generates necessary intent-filter automatically. It will, however, register our activity to accept links with any host, which is probably not what we want. So instead of doing this, let's go with the standard approach. Basically here we define the intent-filter manually instead of auto-generating it from navigation graph. So we can use manifest substitutions just as we would normally do.

Example

and

build.gradle

buildTypes {

        debug {
            manifestPlaceholders.deepLinkUri = "http://link.debug"
        }

        staging {
           manifestPlaceholders.deepLinkUri = "http://link.staging"
        }

        release {
           manifestPlaceholders.deepLinkUri = "http://link"
        }
    }

AndroidManifest.xml

 <activity
     android:name=".HomeActivity">
     <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:host="${deepLinkUri}"
            android:scheme="https" />
    </intent-filter>   
 </activity>

navigation_main.xml

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/navigation_home"
    app:startDestination="@id/fragment_home">

    <fragment
        android:id="@+id/fragment_home"
        android:name="..."
        tools:layout="@layout/fragment_home">
        <argument
            android:name="deepLinkUri"
            android:defaultValue="@null"
            app:argType="string"
            app:nullable="true" />
        <argument
            android:name="token"
            android:defaultValue="@null"
            app:argType="string"
            app:nullable="true" />
        <deepLink app:uri="{deepLinkUri}/identification?code={token}" />
</fragment>

    <fragment
        android:id="@+id/fragment_marketing"
        android:name="..."
        tools:layout="@layout/fragment_marketing">
        <argument
            android:name="deepLinkUri"
            android:defaultValue="@null"
            app:argType="string"
            app:nullable="true" />
        <argument
            android:name="id"
            app:argType="integer"
            app:nullable="true" />
        <deepLink app:uri="{deepLinkUri}/banner?id={id}" />
</fragment>

22.05.2020 Update: Starting with the gradle plugin version 3.6 the deep link uri cannot contain parameters in scheme and host parts, it will result in Improper use of wildcards and/or placeholders in deeplink URI host error.

Fortunately one can use a wildcard, so to make the approach work again just change the deepLink definition in your nav graph file:

<deepLink app:uri="{deepLinkUri}/banner?id={id}" /> to <deepLink app:uri=".*/banner?id={id}" />

esentsov
  • 6,372
  • 21
  • 28
  • if I use the traditional way, I need to manage all the deeplinks on one activity. Once navigation component just recommend to have one single activity and N fragments @esentsov – rafaelasguerra Sep 15 '19 at 11:40
  • @rguerra the above approach makes navigation components deep linking work with traditional way to register links. So you don't need to handle links in the activity, the library will do it for you and will open the right screen – esentsov Sep 15 '19 at 15:22
  • yap, thanks. Unfortunately it works just for simple deeplinks. On my case, I'm using firebase dynamic links and I don't know how to manage long links https://stackoverflow.com/questions/57868086/navigation-component-seems-just-working-with-short-dynamic-link-firebase – rafaelasguerra Sep 20 '19 at 16:03
  • For guys read this answer: Inject build variables into the manifest https://developer.android.com/studio/build/manifest-build-variables – Weiyi Nov 27 '19 at 03:55
  • 2
    @esentsov ` – Weiyi Nov 27 '19 at 04:23
  • 2
    Thanks @li2, I fixed it in the answer – esentsov Nov 27 '19 at 15:11
  • @esentsov I followed your answer and found an issue. I only have one Activity (MainActivity) that hosts multiple fragments. If the MainActivity launch mode is standard, the app will open the deep-linked fragment but there will be multiple app instances opened. If the MainActivity launch mode is singleTask, there will be one app opened but the deep-linked fragment won't be navigated to. – Harvey Mar 05 '20 at 04:36
  • @HauLuu When you set launch mode to singleTask you also need to override `onNewIntent` in your activity and pass the intent to [NavController.handleDeepLink](https://developer.android.com/reference/androidx/navigation/NavController#handleDeepLink(android.content.Intent)) – esentsov Mar 05 '20 at 15:54
  • Oh, yes. I also figure that out. But I use the NavController.navigate(Uri) instead. – Harvey Mar 05 '20 at 17:34
  • I have `Improper use of wildcards and/or placeholders in deeplink URI host` errors because I forgot to **remove** `` from activity tag in manifest. Remember that folks! – wrozwad Mar 19 '20 at 16:46
  • And in `app:uri` argument in navigation `deepLink` tag you can use any name for your host name, eg. `{anyAwesomePlaceholderName}/identification?code={token}` – wrozwad Mar 19 '20 at 16:50
  • With nav components version 2.2.2 is not possible apparently. – Leandro Ocampo May 20 '20 at 11:27
  • 1
    @LeandroOcampo please see the updates answer, might be it solves the issue – esentsov May 22 '20 at 13:44
  • @esentsov thanks! Working on this feature, I noticed some weird behaviour in the library. It does not have anything to do with this question. In case you are interested: https://stackoverflow.com/questions/61951850/android-navigation-components-with-deep-link-onnewintent-called-multiple-times – Leandro Ocampo May 22 '20 at 13:47
  • @esentsov I have a URIs like `https://staging.example.com/invite/email` for staging and `https://example.com/invite/email` for production. For me `*/invite/email` not working. It thinks it cannot handle the deeplink. FYI, I don't have a nav-graph in the manifest. – Bhavin Desai Oct 05 '20 at 10:52
  • 1
    @BhavinDesai check you haven't missed the dot `.*/invite/email` – esentsov Oct 05 '20 at 15:58
  • 1
    @esentsov Saved my day! Actually, in the accepted answer, it is `*.` which should be corrected to `.*` – Bhavin Desai Oct 05 '20 at 17:47
  • 1
    @esentsov in my case when I use `.*` wildcard it just open the app and don't open the fragment...looks that the wildcard isn't working for some reason – Buntupana Nov 16 '20 at 11:28
  • 1
    just checked and with scheme set as `app` is not working as expected...if I change it to `https` it'll work...what could be the reason? – Buntupana Nov 16 '20 at 11:53
  • 1
    @Buntupana if you don't specify the scheme the navigation library sets it to `http[s]` automatically. Try to use something like this `app://.*/deep-link` – esentsov Nov 16 '20 at 16:33
  • what about `android:autoVerify="true"` with wildcarded host? Is that possible? I don't think so, or I have some issue with my app... – mtrakal May 12 '21 at 07:33
  • `android:autoVerify` works by reading the manifest file of your apk. So as long as you have proper url in the merged manifest (e.g. by e.g. using build-time substitutions as in this answer), you should be fine – esentsov May 12 '21 at 08:20
  • 1
    what about only schemes? In my scenario I've different schemes per flavor, I'm trying that approach but seems to not work – extmkv Jun 11 '21 at 10:54
  • I'm also looking for a solution that supports custom schemes for each flavor – G_comp Jun 14 '21 at 20:02
  • Wildcards are not supported in the scheme part. So the easiest way would be to add both `` and `` to the nav graph and define which one is handled by particular flavor by adding manifest substitution: `` – esentsov Jun 15 '21 at 05:44
  • This solution will open your app for any host. Probably not what you want. – d_r Sep 01 '22 at 20:34
  • It works for me ` ` ` ` `AGP` version 7.4.1 – Alis Abenov Feb 08 '23 at 12:22
  • Be careful because the 2.05.2020 Update doesn't work as well as defining the whole URL. I have one URL `https://example.com/feature/{param}` and another one `https://example.com/feature/subfeature` and while with exact URLs there are no issues, with the wildcard .* the second URL is mistaken for the first one. – galex Feb 25 '23 at 06:19
1

Update 2022-03-17:

https://issuetracker.google.com/issues/36994900#comment35

The fix will be in AGP 7.3.0-alpha08.

With this fix, AGP supports manifest placeholders in deep link URI's scheme, host, and path.

Example usage: <deepLink app:uri="${scheme}://${host}/${path}" />

Old info:

There is still no valid solution for it.

Please star these two issues for faster resolving (I don't think, that it will help, but maybe...):

https://issuetracker.google.com/issues/36994900

https://issuetracker.google.com/issues/110237825

mtrakal
  • 6,121
  • 2
  • 25
  • 35
1

2022-05-27 - SIMPLIFIED ANSWER

Solution 1:

It's possible to accomplish this by:

<deepLink app:uri="https://.*example.com/link" />

.* will support any prefix. If your env is not on prefix, move it to that place.

UPDATE: IMPORTANT NOTE: This will not work with AppLink


Solution 2:

NOT WORKING AS EXPECTED - Better solution would be using manifest placeholders:

build.gradle:

...
debug {
    manifestPlaceholders = [scheme: "https", host: "dev.example.com"]
}

and then:

<deepLink app:uri="${scheme}://${host}/link" />

It requires gradle 7.3.0-alpha08 or greater. It recognizes the deeplink but it's not properly handling to the right fragment (https://issuetracker.google.com/issues/36994900#comment48). So stick with solution 1 for now if you wish to use implicit deeplink.

william xyz
  • 710
  • 5
  • 19
1

After digging this thread and IssueTrackers, this configuration ended up working and the bonus in my use case was the that the deeplinks do not expose other links in other flavors if you check for Supported Links in the app's App Info in the settings

root build.gradle.kts:
classpath("com.android.tools.build:gradle:7.4.2")

app build.gradle.kts:

buildTypes {
        getByName("release") {
            signingConfig = signingConfigs.getByName("release")
            manifestPlaceholders["host"] = "your_prod_url.com"
        }
    
        getByName("uat") {
            signingConfig = signingConfigs.getByName("uat")
            manifestPlaceholders["host"] = "your_uat_url.com"
        }
}

AndroidManifest.xml:

<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:scheme="https" />
                <data android:scheme="http" />

                <data android:host="${host}" />

            </intent-filter>

Example (login) deeplinked navGraph:

 <fragment
        android:id="@+id/login"
        android:name="com.package.LoginFragment"
        android:label=" "

        tools:layout="@layout/fragment_login">

        <deepLink
            android:id="@+id/deepLinkLogin"
            app:uri="${host}/{lang}/login" />

    </fragment>

-1

Just simplely adding -

<deepLink app:uri=".*/{my_token}" />

Worked for me!!! Which also made me surprised that it didn't need any extra code.

Edit 1: But the above code also open others URL from others app with my app.It is GOOD if you really want it that people open their URL with your app. But if you don't like that then you can try the fix below. I fixed it with just simply add two deep-link for my two variation of my URL like -

<deepLink
    app:uri="http://www.stage.com/{token}" />

<deepLink
    app:uri="http://www.production.com/{token}" />
Gk Mohammad Emon
  • 6,084
  • 3
  • 42
  • 42