Firebase for Android: Dynamic Links and Invites

Firebase for Android: Dynamic Links and Invites

The primary purpose of dynamic links is to allow app developers to generate a URL that, when clicked, will either take the user to a specific location in an app or, if the app is not yet installed, load the Google Play store page for the app so that it can be installed and launched. A website might, for example, include a “Download our App in the Google Play Store” button which uses a dynamic link. Alternatively, dynamic links can be used to share references to content within the app. An app might, for example, allow users to recommend to other users particular content within the app. When the user follows the link, the app will open and use the dynamic link to present the screen and content that needs to be displayed. Dynamic links are also cross-platform and can be implemented on Android and iOS apps, or used exclusively for web based content.

There is an alternative in mobile linking to Firebase Dynamic Links. Another approach has name "Branch Links" and you can read comparison here.

Dynamic Links have a very simple process flow:

  • The user begins by clicking the Dynamic Link
  • If the the needs of the Dynamic Link target are satisfied (this is, the application being installed) then the user is navigated to the target location
  • Otherwise, if the application requires install in order to navigate to the Dynamic Link target, the the user is taken to the point of install for the application. Once the application has been installed, the user is navigated to the target location of the Dynamic Link
android_fb_dynamic_links_flow.png

In this tutorial we’ll first look at Dynamic Links, and how you can create and process them. We’ll then go into how you can expand on these using Firebase Invites, which give you an intelligent way to share your app with the contacts in your user’s address book over either email or SMS. When the user opts to share your app, Google will give them a set of intelligently sorted contacts – suggestions for who to share with based on the contacts that the user communicates with frequently.

To use Dynamic Links, first create a new project and use the assistant to connect it to a project and add Dynamic Links to it. If you're using the latest version of Android Studio (version 2.2 or later), it's recommend using the Firebase Assistant to connect your app to Firebase. The Firebase Assistant can connect your existing project or create a new one for you and automatically install any necessary gradle dependencies.

To open the Firebase Assistant in Android Studio:

  1. Click Tools > Firebase to open the Assistant window.
  2. Click Dynamic Links to expand one, then click the provided tutorial link (Add a dynamic link).
  3. Click the Connect to Firebase button to connect to Firebase and add the necessary code to your app.
  4. Click the Add Dynamic Links to your app.

Once you’ve done this and created a project on the Firebase Console, you can use it to create Dynamic Links for you. There are a number of methods to create links, including a REST API, and a builder in Android Studio, but we’ll look at the console first.

In the console, select the Grow > Dynamic Links tab on the left. Then click the New Dynamic Link button, and you’ll be presented with the steps to create a link.

  • Deep link URL – http://en.proft.me/?status=100
  • Dynamic Link name - Check status
  • iOS behavior – Open the deep link URL in a browser
  • Android behavior – Open the deep link in your Android app (select the app from the menu).
  • UTM parameters – None
  • Social media tags – None

Once the link has been configured, click on the Create Dynamic Link button to generate the link. After the link has been generated, hover the mouse pointer over the link row, click on the menu dots when they appear and select Link details from the menu.

When using dynamic links in scenarios such as this it is important to use the short form of the URL. In this example, the user could easily reverse engineer the long form URL to increase the number of bonus points awarded.

Copy the short dynamic link to the clipboard before moving to the next step.

Then, if I am using the dynamic link on the desktop, instead of mobile, there’s a web site it can go to. When the mobile user clicks on it, they’ll get the mobile app with a custom activity that can read the context from the link.

So now let’s take a look at opening an activity from the app, and getting details about the context.

First of all, in the app, you’ll need to edit the AndroidManifest.xml to include an intent filter that handles URLs matching those from the incoming link. The main activity has an existing intent filter for launching it, so be sure to add a second one containing the details of the incoming URL that you want to be browsable. Here’s the complete AndroidManifest.xml file with both intent filters.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="proft.me.sandbox">

    <uses-permission android:name="android.permission.INTERNET" />

   <application
       android:allowBackup="true"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:roundIcon="@mipmap/ic_launcher_round"
       android:supportsRtl="true"
       android:theme="@style/AppTheme">
       <activity android:name=".MainActivity">
           <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="en.proft.com"
                   android:scheme="https"/>
               <data
                   android:host="en.proft.com"
                   android:scheme="http"/>
           </intent-filter>
       </activity>
   </application>
</manifest>

Now in MainActivity in your onCreate you can set up your activity to parse the incoming Dynamic Link in the scenario where the app is launched using one. This is achieved using FirebaseDynamicLinks.getInstance(). On this you can add a success listener and failure listener. The success listener takes PendingDynamicLinkData as a parameter, and when this is not null, your link details are available in its getLink() method. The onFailure listener will catch failures, passing exception details that you can log. So, for example, here’s how to catch the dynamic link details and render them in the MainActivity.

public class MainActivity extends AppCompatActivity {
    String TAG = "TAG";

    TextView tvText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tvText = (TextView) findViewById(R.id.tvText);

        FirebaseDynamicLinks.getInstance()
            .getDynamicLink(getIntent())
            .addOnSuccessListener(this, new OnSuccessListener<PendingDynamicLinkData>() {
                @Override
                public void onSuccess(PendingDynamicLinkData pendingDynamicLinkData) {
                    // Get deep link from result (may be null if no link is found)
                    Uri deepLink = null;
                    if (pendingDynamicLinkData != null) {
                        deepLink = pendingDynamicLinkData.getLink();
                    }
                    if (deepLink != null) {
                        tvText.setText(deepLink.toString());
                    } else {
                        Log.d(TAG, "getDynamicLink: no link found");
                    }
                }
            })
            .addOnFailureListener(this, new OnFailureListener() {
                @Override
                public void onFailure(@NonNull Exception e) {
                    Log.d(TAG, "getDynamicLink:onFailure", e);
                }
            });
    }
}

Run the app, and you should see the 'TextView' in the empty activity.

Now touch on the link, and your app will open. You may be asked how to open the app and be given a number of options. If so, select 'Google Play services' as the method to open it. When it opens the details of the Dynamic Link will be shown in the TextView.

Typing the URL into the browser can be time consuming and prone to error (particularly when using a long form URL). A useful trick to avoid having to type the URL is to use the adb tool. On the device or emulator, open Chrome once again and select the search bar so that the keyboard appears ready to receive input and remove any previous URL within the search field. Open a command-prompt or terminal window on the development system and run the following command where 'dynamic url here' is replaced by the dynamic link URL copied from the Firebase console:

adb shell input text "dynamic url here"

When the command is executed, the text will appear within the Chrome search bar. Tapping the search button on the virtual keyboard will then load the URL and launch the app.

android_fb_dynamic_link1.png
android_fb_dynamic_link2.png

In this case we caught all URLs in the MainActivity by specifying it in the intent filter. Should we decide to, we could set up separate activities for different URLs, but in the case where you could have very many dynamic links (for example, if the users create their own as you’ll see shortly), it’s best to have a single activity to handle them all, and then based on the parameters passed into it, you could pick different activities to load.

Creating a Dynamic Link within the App

The most useful context for sharing Dynamic Links, is when a user with your app can pick content or an activity and then share that with other users. In order to do this they should be able to create Dynamic Links on-the-fly, and Firebase offers an API to do that.

Dynamic links are available in both long and short form. A short URL, which can be useful for obfuscating the content of the link, is simply a long URL that has been processed by a URL shortener.

Let’s explore that next. In your app, add a new Activity called MainActivity. Give this activity a simple UI with two Button and a TextView. Here’s an example:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center_horizontal"
    android:paddingTop="20dp">

    <TextView
        android:id="@+id/tvLink"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Link will be here!"
        android:textAppearance="@style/TextAppearance.AppCompat.Headline" />

    <Button
        android:id="@+id/btnGet"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="buildLink"
        android:text="Get link" />

    <Button
        android:id="@+id/btnShare"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="shareLink"
        android:text="Share link" />

</LinearLayout>

When the user clicks the button we want to generate a dynamic link that can then be shared with others. The API supports this in two different steps. The first is to create the link itself, and the second is to create a shortened link.

To create a dynamic link, you first need the Uri that you want to link to. In this case, I’m just hard-coding a simple one, but if this activity was rendering data from a database, you’d probably want to parameterize an identifier on that database. You’ll also need the link domain. This is visible in the Firebase Console when you look in the dynamic links section. So, for example, my link domain is s3tzp.app.goo.gl.

Now that you have the destination link and the link domain, you can use a FirebaseDynamicLinks.getInstance() to create a dynamic link like this:

DynamicLink.Builder builder = FirebaseDynamicLinks.getInstance()
    .createDynamicLink()
    .setDynamicLinkDomain(linkdomain)
    .setAndroidParameters(new DynamicLink.AndroidParameters.Builder()
        .setMinimumVersion(minVersion)
        .build())
    .setLink(destinationLink);
DynamicLink link = builder.buildDynamicLink();

The DynamicLink object that is created supports a getUri() method that you can use to determine the Uri of the returned Dynamic Link.

Now that you have this, you can then create a shortened link using the same API. This works asynchronously so you have to use it within a Task<> to create a ShortDynamicLink object. You simply pass it the Uri of the long link and ask it to build a short link from it.

Here’s the code:

Task<ShortDynamicLink> task = FirebaseDynamicLinks.getInstance()
   .createDynamicLink()
   .setLongLink(uri)
   .buildShortDynamicLink()
   .addOnCompleteListener(new OnCompleteListener<ShortDynamicLink>() {
       @Override
       public void onComplete(@NonNull Task<ShortDynamicLink> task) {
           if (task.isSuccessful()) {
               Uri shortLink = task.getResult().getShortLink();
               shareTextView.setText(shortLink.toString());
           } else {
               shareTextView.setText("Error retrieving link");
           }
       }
   });

For convenience, here’s the full code for the Share Activity showing how this is triggered on a button press:

public class MainActivity extends AppCompatActivity {
    String TAG = "TAG";
    TextView tvLink;
    Uri shortLink;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tvLink = (TextView) findViewById(R.id.tvLink);

        parseDeepLink();
    }

    private void parseDeepLink() {
        FirebaseDynamicLinks.getInstance()
                .getDynamicLink(getIntent())
                .addOnSuccessListener(this, new OnSuccessListener<PendingDynamicLinkData>() {
                    @Override
                    public void onSuccess(PendingDynamicLinkData pendingDynamicLinkData) {
                        // Get deep link from result (may be null if no link is found)
                        Uri deepLink = null;
                        if (pendingDynamicLinkData != null) {
                            deepLink = pendingDynamicLinkData.getLink();
                        }
                        if (deepLink != null) {
                            tvLink.setText(deepLink.toString());
                        } else {
                            Log.d(TAG, "getDynamicLink: no link found");
                        }
                    }
                })
                .addOnFailureListener(this, new OnFailureListener() {
                    @Override
                    public void onFailure(@NonNull Exception e) {
                        Log.d(TAG, "getDynamicLink:onFailure", e);
                    }
                });
    }

    public void buildLink(View v){
        Uri uri = getDynamicLink(Uri.parse("http://en.proft.me/?status=100"), 0);
        Task<ShortDynamicLink> task = FirebaseDynamicLinks.getInstance()
            .createDynamicLink()
            .setLongLink(uri)
            .buildShortDynamicLink()
            .addOnCompleteListener(new OnCompleteListener<ShortDynamicLink>() {
                @Override
                public void onComplete(@NonNull Task<ShortDynamicLink> task) {
                    if (task.isSuccessful()) {
                        shortLink = task.getResult().getShortLink();
                        Log.d(TAG, "onComplete: " + shortLink.toString());
                        tvLink.setText(shortLink.toString());
                    } else {
                        tvLink.setText("Error retrieving link");
                    }
                }
            });
    }

    public void shareLink(View v){
        try {
            URL url = new URL(URLDecoder.decode(shortLink.toString(), "UTF-8"));
            Log.i(TAG, "URL = " + url.toString());
            Intent intent = new Intent(Intent.ACTION_SEND);
            intent.setType("text/plain");
            intent.putExtra(Intent.EXTRA_SUBJECT, "Firebase Deep Link");
            intent.putExtra(Intent.EXTRA_TEXT, url.toString());
            startActivity(intent);
        } catch (Exception e) {
            Log.i(TAG, "Could not decode Uri: " + e.getLocalizedMessage());
        }
    }

    private Uri getDynamicLink(@NonNull Uri destinationLink, int minVersion){
        String linkdomain = "s3tzp.app.goo.gl";
        DynamicLink.Builder builder = FirebaseDynamicLinks.getInstance()
                .createDynamicLink()
                .setDynamicLinkDomain(linkdomain)
                .setAndroidParameters(new DynamicLink.AndroidParameters.Builder()
                        .setMinimumVersion(minVersion)
                        .build())
                .setLink(destinationLink);
        DynamicLink link = builder.buildDynamicLink();
        return link.getUri();
    }
}

Now when you run the application and press the button in the MainActivity, you’ll be taken to the share activity. Press the button on that, and you should see something like in following figure.

android_fb_dynamic_link3.png

Next, let’s take a look at parsing the link in the MainActivity, and if it detects this set of parameters it would open the share activity instead.

if (deepLink != null) {
    Set<String> params = deepLink.getQueryParameterNames();
    if (params.contains("status")){
        Intent intent = new Intent(getApplicationContext() , StatusActivity.class);
        startActivity(intent);
    }
    contentText.setText(deepLink.toString());
} else {
   Log.d(TAG, "getDynamicLink: no link found");
}

So now, whenever a link containing the parameter shareactivity is found, that Activity will load, and can parse the parameter. Otherwise the MainActivity will handle the parameter. This technique allows you to pick the right activity for the right Dynamic link, and activate the activity accordingly.

Of course you also want your users to share with their contacts, and Firebase Invites has been built to use Dynamic Links to make that easy. Let’s look at that in the next section. You aren’t limited to using Invites, of course - technologies like Firebase Cloud Messaging make for easy and powerful avenues for sharing!

Using Firebase Invites

Firebase Invites is a simple intent-based API that gives you the User Interface and back end for sharing with friends via SMS or Email.

Let’s take a look at what it takes to add Firebase Invites to an app. You create an invite dialog using the AppInviteInvitation intent builder. This takes a number of parameters such as the Message that you’re sending to a user, the title for the dialog, the deep link URL to the application and a call to action. You use this to create an intent and start a new activity for that. Android will then present the user with a dialog containing suggested contacts as well as an alphabetically sorted list of email and SMS contacts. In the Activity result, the data returned contains an array of invitation IDs that you can use for tracking successful invites if you like – but using a Dynamic Link would be more effective because you have the built-in analytics.

Change btnShare button in MainActivity, and use this code to define its action:

public void shareLink(View v){
   Intent intent = new AppInviteInvitation.IntentBuilder("Try out my cool app!")
           .setMessage("Hey, this app is really cool. I thought you might like it!")
           .setDeepLink(Uri.parse("http://en.proft.me/?status=100"))
           .setCallToActionText("Let's do this!")
           .build();
   startActivityForResult(intent, REQUEST_INVITE);
}

When you send the invite it will be distributed based on the contact type – so email contacts will receive an email, and SMS contacts will receive an SMS. It’s all automated to the end user, so they don’t have to do any further action.

A number of methods are available for customizing the intent including the following:

  • setMessage(). Used to declare a default message to be included with the invitation. This default text can be modified by the user within the invitation dialog before the message is sent.
  • setDeepLink(). The deep link URL which is to be embedded into the dynamic link sent with the message. This can be used to pass information to the app when it is installed and launched by the message recipient.
  • setCustomImage(). Allows an image to be included within the message. The image can be up to 4000x4000 pixels in size, though 600x600 pixels is the recommended size.
  • setCallToActionText(). The text that is to appear on the Install button included within email messages. Text is limited to 32 characters.
  • setEmailHtmlContent(). Allows HTML content to be defined for email based invitations. The dynamic link URL should be substituted by the %%APPINVITE_LINK_PLACEHOLDER%% string. This method must be used in conjunction with the setEmailSubject() method. When using HTML content, the setMessage(), setCustomImage() and setCallToActionText() methods are redundant.
  • setEmailSubject(). Used to set the email subject line when using HTML content.
  • setGoogleAnalyticsTrackingId(). Allows a Google Analytics tracking ID to be specified to track the performance of the invitation.

Since the app invitation intent is launched using the startActivityForResult() method, clearly some code needs to be added to handle the result. This is handled in the usual way by implementing an onActivityResult() method.

When invitations have been sent each one is automatically assigned an invitation ID which can be extracted from the results data as shown in the following method:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode == INVITE_REQUEST) {
        if (resultCode == RESULT_OK) {
            String[] ids = AppInviteInvitation.getInvitationIds(resultCode, data);
            for (String id : ids) {
                Log.d(TAG, "id of sent invitation: " + id);
            }
        } else {
            // Failed to send invitations
        }
    }
}