Skip to main content

Android sharing and deep linking with Facebook and Google Plus

Introduction

    Social integration in an app is a feature that not only provides a more user-friendly experience but also provides a free promotional method. However, many app's fail at properly supporting this feature. To illustrate what I mean, imagine scrolling through your Facebook feed. You see that a friend of yours liked an article and the title seems enticing. You click the link and the app opens up on your device. All ready to read, you patiently wait for the app to finish loading. The app finishes loading only to leave you confused as it doesn't display the article you selected! Instead, it shows a home screen or a loading page or even another article! Infuriated at this horrible experience you're unlikely to use the app again. :(

    Good news! This tragic scenario of user disbandment can be avoided altogether with just a few simply lines of code. Better news! I'm going to provide that code for a basic and common use case. The following tutorial demonstrates how to integrate sharing to Facebook and Google Plus, and deep linking into your app from another app, such as,  Facebook, Google Plus, and Google Search.

Here's a look at our basic UI:



    In regards to this article, Sharing can be defined as: sending information from one App to be hosted in another; And Deep Linking can be defined as: selecting a link in one App and opening/displaying in another. 

 Getting Started / UI Setup

    To avoid making this tutorial too verbose, I emitted certain sections of code but kept the relevant ones. Okay, let's have a look at the project's structure (I'm using Android Studio 1.2.2):



And here's the project's app module Gradle file:

      apply plugin: 'com.android.application'

      android {
          compileSdkVersion 23
          buildToolsVersion "22.0.1"

          defaultConfig {
              applicationId "com.chrynan.sampleshareproject"
              minSdkVersion 16
              targetSdkVersion 23
              versionCode 1
              versionName "1.0"
          }
          buildTypes {
              release {
                  minifyEnabled false
                  proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
              }
          }
      }

      dependencies {
          compile fileTree(dir: 'libs', include: ['*.jar'])
          compile 'com.android.support:appcompat-v7:23.1.1'
          compile 'com.android.support:support-v4:23.1.1'
          compile 'com.android.support:design:22.2.1'
          compile 'com.google.android.gms:play-services:7.8.0'
          compile 'com.android.support:palette-v7:21.0.0'
          compile 'com.facebook.android:facebook-android-sdk:4.1.0'
          compile 'com.flipboard:bottomsheet-core:1.5.0'
          compile 'com.flipboard:bottomsheet-commons:1.5.0'
          compile 'com.joanzapata.iconify:android-iconify-fontawesome:2.1.0'
     }

    
    
The manifest.xml file:


    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.chrynan.sampleshareproject">

    <application android:allowBackup="true" android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher" android:theme="@style/AppTheme">
        <activity
            android:name=".activities.MainActivity"
            android:label="@string/app_name"
            android:noHistory="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".activities.DeepLinkActivity"
            android:label="@string/app_name"
            android:configChanges="orientation|screenSize"
            android:parentActivityName=".activities.MainActivity">
            <!-- Parent activity meta-data to support 4.0 and lower -->
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value=".activities.MainActivity" />
            <!-- Intent Filters for deep linking -->
            <!-- Retrieves Android Link -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <!-- Allows the activity to be accessible from a web browser (ex: displaying in search results) -->
                <category android:name="android.intent.category.BROWSABLE" />
                <!-- Other apps use the following info to send a user to your app -->
                <!-- Ex: When a user selects a post from your app shared on Facebook, Facebook can send the user to your app with
                     the following URI pattern: sampleshareproject://deeplink/story?id=STORY_ID -->
                <!-- You could choose to use "story" as the host and allow the path to represent which story to load -->
                <data android:scheme="sampleshareproject"
                    android:host="deeplink"
                    android:pathPrefix="/story"/>
            </intent-filter>
            <!-- Retrieves Web Link -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <!-- For Google App Indexing (app appears in search results), you will need corresponding information on the web.
                     Meaning you must have a website for your app which is also indexed by Google for this feature to work -->
                <!-- You would place your website URL like so -->
                <data android:scheme="http"
                    android:host="sampleshareproject.com"
                    android:pathPrefix="/story"/>
            </intent-filter>
        </activity>
    </application>

</manifest>


    The most important part, in the above code, is the IntentFilters. They provide the URI mapping the app responds to. This allows other apps to send our app information and for our app to delegate that information to the appropriate Activity. As you can see, from the above code, we have two Activities: MainActivity and DeepLinkActivity (where the information gets sent to from another app). Code will only be shown for the DeepLinkActivity since that's relevant for the tutorial.

    Now, let's set up the user-interface files. We have two layout resource files we need to worry about: bottom_sheet.xml which displays the Facebook and Google Plus buttons for sharing information and deep_link_activity.xml for our Activity's view. To actually display the bottom sheet, I'm using Flipboard's Bottom Sheet library within the DeepLinkActivity class. The icons used can be any icon you want, just put them in your drawable folder and reference them in the bottom_sheet.xml file. One place to get icons is Flat Icon.

bottom_sheet.xml:

    <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="200dp"
    android:background="@color/white">
    <!-- Hardcoded the height to prevent the bottom sheet from taking up the whole screen -->

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:text="Share With..."
        android:textSize="18sp"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <LinearLayout
            android:id="@+id/facebook_container"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="vertical"
            android:clickable="true"
            android:foreground="?android:attr/selectableItemBackground">

            <ImageView
                android:id="@+id/facebook_icon"
                android:layout_width="48dp"
                android:layout_height="48dp"
                android:layout_gravity="center_horizontal"
                android:src="@drawable/facebook"/>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp"
                android:layout_gravity="center_horizontal"
                android:text="Facebook"/>

        </LinearLayout>

        <LinearLayout
            android:id="@+id/google_plus_container"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="vertical"
            android:clickable="true"
            android:foreground="?android:attr/selectableItemBackground">

            <ImageView
                android:id="@+id/google_plus_icon"
                android:layout_width="48dp"
                android:layout_height="48dp"
                android:layout_gravity="center_horizontal"
                android:src="@drawable/google_plus"/>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp"
                android:layout_gravity="center_horizontal"
                android:text="Google Plus" />

        </LinearLayout>

    </LinearLayout>

</LinearLayout>


deep_link_activity.xml:


    <?xml version="1.0" encoding="utf-8"?>
<com.flipboard.bottomsheet.BottomSheetLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/bottom_sheet"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- The bottom sheet view library used can only have a single immediate descendant view -->
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.design.widget.AppBarLayout
            android:layout_height="wrap_content"
            android:layout_width="match_parent"
            android:id="@+id/app_bar_layout">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_height="?attr/actionBarSize"
                android:layout_width="match_parent" />

        </android.support.design.widget.AppBarLayout>

        <!-- Place Activity UI here -->

    </LinearLayout>

</com.flipboard.bottomsheet.BottomSheetLayout>


Sharing and Linking

    Now that we got the user-interface and application setup code out of the way, we can can focus on the code that gets and shares the information. The information can be anything but for the purpose of this tutorial I'm going to create a custom Story object. A Story object can be sent to our DeepLinkActivity as a serialized extra in an Intent or its ID field can be passed in the Intent either from internally within the app or externally from another application. So, here's the Story object:



    /**
 * Created by chRyNaN on 12/8/2015. Example class of an object that can be shared and viewed within the app.
 */
public class Story implements Serializable {
    private long id;
    private String title;
    private String description;
    private Date date;
    private String content;
    private String imageURL;

    public Story(){
        this.id = Long.parseLong(UUID.randomUUID().toString());
        this.date = new Date();
    }
    
    //getters and setters emitted for brevity

}


Lo and behold the DeepLinkActivity:



    /**
 * Created by chRyNaN on 12/8/2015. This will be the Activity that displays the custom Story object.
 * This Activity can be loaded internally from within the app where the Story will be passed in as a
 * serialized object through the intent or from another application through deep linking.
 */
public class DeepLinkActivity extends AppCompatActivity {
    private static final String TAG = DeepLinkActivity.class.getSimpleName();

    public static final String IN_APP_STORY_FIELD = "Story";
    public static final String IN_APP_STORY_ID_FIELD = "id";

    //For App Index API (For content to appear in and be linked to search results);
    //requires related website content to also be indexed for this feature to properly work
    public static final Uri INDEX_APP_URI =
            Uri.parse("android-app://com.chrynan.sampleshareproject/sampleshareproject/deeplink/story");
    public static final Uri INDEX_WEB_URI = Uri.parse("http://sampleshareproject.com/story");
    private GoogleApiClient client;
    private Action viewAction;

    private BottomSheetLayout bottomSheet;
    private View bottomSheetView;
    private Toolbar toolbar;

    private Story story;

    @Override
    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.deep_link_activity);

        toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        bottomSheet = (BottomSheetLayout) findViewById(R.id.bottom_sheet);
        bottomSheetView = LayoutInflater.from(this).inflate(R.layout.bottom_sheet, bottomSheet, false);
        View facebookButton = bottomSheetView.findViewById(R.id.facebook_container);
        facebookButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ShareUtils.shareStoryToFacebook(DeepLinkActivity.this, story);
            }
        });
        View googleButton = bottomSheetView.findViewById(R.id.google_plus_container);
        googleButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ShareUtils.shareStoryToGooglePlus(DeepLinkActivity.this, story);
            }
        });

        //Handle and get the Story from the Intent
        onNewIntent(getIntent());

    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu){
        getMenuInflater().inflate(R.menu.deep_link_activity, menu);
        menu.findItem(R.id.action_share).setIcon(android.R.drawable.ic_menu_share);
        ActionBar actionBar = getSupportActionBar();
        if(actionBar != null){
            actionBar.setDisplayHomeAsUpEnabled(true);
        }
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            // Respond to the action bar's Up/Home button
            case android.R.id.home:
                try {
                    //ghost error occurring when pressing the up button on the nav bar
                    //causes the app to close but not crash, so try this
                    Intent intent = NavUtils.getParentActivityIntent(this);
                    intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                    NavUtils.navigateUpTo(this, intent);
                    finish();
                } catch (Exception e) {
                    e.printStackTrace();
                    NavUtils.navigateUpFromSameTask(this);
                }
                return true;
            case R.id.action_share:
                //Show the bottom sheet to allow the user to choose where to share from
                bottomSheet.showWithSheetView(bottomSheetView);
                return true;
        }
        return false;
    }

    @Override
    public void onStart(){
        super.onStart();
        if(client != null){
            client.connect();
        }
        startIndexing();
    }

    @Override
    public void onStop(){
        stopIndexing();
        super.onStop();
    }

    protected void onNewIntent(Intent intent) {
        String action = intent.getAction();
        Uri data = intent.getData();
        story = (Story) intent.getSerializableExtra(IN_APP_STORY_FIELD);
        long storyId = intent.getLongExtra(IN_APP_STORY_ID_FIELD, -1);
        if(story == null && storyId == -1){
            //Intent came from outside our app so get the id from the path field parameter
            if(data != null) {
                storyId = Long.parseLong(data.getQueryParameters(IN_APP_STORY_ID_FIELD).get(0));
                //TODO load the story from your server
            }
        }else if(story == null){
            //TODO load the story from your server
        }
        //TODO now that we have the story we should display it and the UI
    }

    //Method for Google App Indexing
    protected void startIndexing(){
        if(story != null){
            viewAction = Action.newAction(Action.TYPE_VIEW, story.getTitle(), INDEX_WEB_URI, INDEX_APP_URI);
            AppIndex.AppIndexApi.start(client, viewAction);
        }
    }

    //Method for Google App Indexing
    protected void stopIndexing(){
        AppIndex.AppIndexApi.end(client, viewAction);
        if(client != null){
            client.disconnect();
        }
    }

}


    In the code above, when the Activity is created (onCreate), after setting up the UI objects, we get the Intent that was used to start the Activity and we pass it into the onNewIntent method. The onNewIntent method extracts the information that we need from the Intent in order to get the Story object. It's all pretty self explanatory. However, you may have noticed the use of the ShareUtils class. This class contains static methods where I delegate the actual sharing work to. The most important methods, within this class, in regards to this tutorial, are the shareStoryToFacebook and the shareStoryToGooglePlus methods. And here's that class:


    /**
 * Created by chRyNaN on 12/8/2015. A simple Util class that handles sharing content to Facebook and Google Plus.
 */
public class ShareUtils {
    private static final String TAG = ShareUtils.class.getSimpleName();
    public static final String DEEP_LINK_URL = "sampleshareproject://deeplink/story";
    public static final int GOOGLE_PLUS_SHARE_LINK_REQUEST_CODE = 0;
    public static final int GOOGLE_PLUS_SHARE_MEDIA_REQUEST_CODE = 1;

    public static void shareStoryToFacebook(Activity activity, Story story){
        //Sharing with the Open Graph API requires you to publish your app for review with Facebook
        //For this tutorial, I'll just simply share a link to our content
        String url = DEEP_LINK_URL + "?id=" + story.getId();
        shareLinkToFacebook(activity, url, story.getTitle(), story.getDescription(), story.getImageURL());
        Toast.makeText(activity, "Shared story to Facebook.", Toast.LENGTH_SHORT).show();
    }

    public static void shareStoryToGooglePlus(Activity activity, Story story){
        //Doesn't handle logging in but in order to use this feature you must be logged in to Google Plus
        PlusShare.Builder builder = new PlusShare.Builder(activity);
        builder.addCallToAction("READ", Uri.parse(DEEP_LINK_URL), String.valueOf(story.getId()));
        builder.setContentUrl(Uri.parse(DEEP_LINK_URL + "?id=" + story.getId()));
        builder.setContentDeepLinkId(String.valueOf(story.getId()), story.getTitle(), story.getDescription(),
                Uri.parse(story.getImageURL()));
        builder.setText("Read the story!");
        activity.startActivityForResult(builder.getIntent(), GOOGLE_PLUS_SHARE_LINK_REQUEST_CODE);
        Toast.makeText(activity, "Shared story to Google Plus.", Toast.LENGTH_SHORT).show();
    }

    public static void shareLinkToFacebook(Activity activity, String url, String title, String description, String imageURL){
        if(activity != null && url != null) { //Activity and url are mandatory
            ShareLinkContent.Builder builder = new ShareLinkContent.Builder();
            builder.setContentUrl(Uri.parse(url));
            if(title != null){
                builder.setContentTitle(title);
            }
            if(description != null){
                builder.setContentDescription(description);
            }
            if(imageURL != null){
                builder.setImageUrl(Uri.parse(imageURL));
            }
            //Opens either the native Facebook app or a web dialog and returns to the provided activity when finished
            ShareDialog.show(activity, builder.build());
        }else{
            Log.e(TAG, "activity and url parameters in the shareLinkToFacebook() method must not be null.");
        }
    }

    public static void sharePhotoToFacebook(Activity activity, Bitmap bitmap, String url){
        if(activity != null && bitmap != null){
            SharePhotoContent.Builder builder = new SharePhotoContent.Builder();
            builder.addPhoto(new SharePhoto.Builder().setBitmap(bitmap).build());
            if(url != null){
                builder.setContentUrl(Uri.parse(url));
            }
            //In order to share pictures, the user must have the native Facebook app installed, version 7 or higher
            if(ShareDialog.canShow(SharePhotoContent.class)){
                ShareDialog.show(activity, builder.build());
            }else{
                Toast.makeText(activity, "You must have the latest Facebook app installed to share photos.",
                        Toast.LENGTH_SHORT).show();
            }
        }else{
            Log.e(TAG, "activity and bitmap parameters in the sharePhotoToFacebook() method must not be null.");
        }
    }

    public static void shareVideoToFacebook(Activity activity, Uri videoUri, String title, String description, String contentUrl,
                                            Uri photoUri){
        if(activity != null && videoUri != null){
            ShareVideo video = new ShareVideo.Builder()
                    .setLocalUrl(videoUri).build();
            ShareVideoContent.Builder builder = new ShareVideoContent.Builder();
            builder.setVideo(video);
            if(title != null){
                builder.setContentTitle(title);
            }
            if(description != null){
                builder.setContentDescription(description);
            }
            if(contentUrl != null){
                builder.setContentUrl(Uri.parse(contentUrl));
            }
            if(photoUri != null){
                builder.setPreviewPhoto(new SharePhoto.Builder().setImageUrl(photoUri).build());
            }
            if(ShareDialog.canShow(ShareVideoContent.class)){
                ShareDialog.show(activity, builder.build());
            }else{
                Toast.makeText(activity, "You must have the latest Facebook app installed to share videos.",
                        Toast.LENGTH_SHORT).show();
            }
        }else{
            Log.e(TAG, "activity and videoUri parameters in the shareVideoToFacebook() method must not be null.");
        }
    }

    public static void shareVideoToFacebook(Activity activity, Uri videoUri, String title, String description, String contentUrl,
                                            Bitmap bitmap){
        if(activity != null && videoUri != null){
            ShareVideo video = new ShareVideo.Builder()
                    .setLocalUrl(videoUri).build();
            ShareVideoContent.Builder builder = new ShareVideoContent.Builder();
            builder.setVideo(video);
            if(title != null){
                builder.setContentTitle(title);
            }
            if(description != null){
                builder.setContentDescription(description);
            }
            if(contentUrl != null){
                builder.setContentUrl(Uri.parse(contentUrl));
            }
            if(bitmap != null){
                builder.setPreviewPhoto(new SharePhoto.Builder().setBitmap(bitmap).build());
            }
            if(ShareDialog.canShow(ShareVideoContent.class)){
                ShareDialog.show(activity, builder.build());
            }else{
                Toast.makeText(activity, "You must have the latest Facebook app installed to share videos.",
                        Toast.LENGTH_SHORT).show();
            }
        }else{
            Log.e(TAG, "activity and videoUri parameters in the shareVideoToFacebook() method must not be null.");
        }
    }

    public static void shareLinkToGooglePlus(Activity activity, String url, String text, String deepLinkURL, String callToAction,
                                             String callToActionURL){
        if(activity != null && url != null) {
            PlusShare.Builder builder = new PlusShare.Builder(activity);
            builder.setContentUrl(Uri.parse(url));
            if (callToAction != null && callToActionURL != null) {
                builder.addCallToAction(callToAction, null, callToActionURL);
            }
            if (deepLinkURL != null) {
                builder.setContentDeepLinkId(deepLinkURL);
            }
            if (text != null) {
                builder.setText(text);
            }
            activity.startActivityForResult(builder.getIntent(), GOOGLE_PLUS_SHARE_LINK_REQUEST_CODE);
        }else{
            Log.e(TAG, "activity and url parameters in the shareLinkToGooglePlus() method must not be null.");
        }
    }

    public static void shareMediaToGooglePlus(Activity activity, String text, Uri media, String mime){
        if(activity != null && media != null){
            PlusShare.Builder builder = new PlusShare.Builder(activity);
            builder.setStream(media);
            if(text != null){
                builder.setText(text);
            }
            if(mime != null){
                builder.setType(mime);
            }
            activity.startActivityForResult(builder.getIntent(), GOOGLE_PLUS_SHARE_MEDIA_REQUEST_CODE);
        }else{
            Log.e(TAG, "activity and media parameters in the shareMediaToGooglePlus() method must not be null.");
        }
    }

}



Conclusion

    See that wasn't so hard! Now your app can properly handle incoming links from other apps. As this was just a basic implementation to illustrate sharing and deep linking features, the code isn't completed (ex: actually showing the story) and isn't production ready. Nevertheless, the code provided can be a useful basis to any application! 

Comments

Popular posts from this blog

Face detection and live filters

Live video filters are becoming a popular trend fueled by Facebook (through their purchase of Msqrd) and Snapchat incorporating the features into their apps. These filters apply images or animations to your face using face tracking software. This technology has been around for awhile but is becoming increasingly more common due to the powerful CPU's that our mobile phones now have. Google provides an API that provides face tracking abilities through the Google Play Services library called Mobile Vision. I'm going to use their API to build a basic live filter app. The end result will look something like this:


    The bounding box wraps around the detected face and the sunglasses are the filter I chose (which is just a PNG image) which are drawn over the eyes. You could use any PNG image (with alpha for the background) you want, you will just have to adjust the layout according to where the image should be displayed. As you move your head, the box and sunglasses are redrawn…

Setting Up Connection Pooling With Elastic Beanstalk

Amazon's Elastic Beanstalk is a service which automatically scales your application when needed. It uses Amazon's Elastic Compute Cloud (EC2) instances as deployable containers which when your app requires more resources more containers will be deployed. This removes the need to manually configure your EC2 instance whenever you need more connections or resources and attempts to add simplicity to the maintenance aspect of your application. So, when you get more users of your app, your app will scale accordingly.

    Unfortunately, along with the ability to scale automatically, comes less control and configuration. Things you would normally have the ability to configure to your liking, such as your server, you no longer can. Amazon attempts to address this issue with configuration files. You can provide configuration files which can set up your server. These files are either written in JSON or the horrible format YAML. Though these files provide some level of control, you ca…

Android Guitar Tuner

Recently I created a guitar tuner application for Android that is written with pure Java (no C++ or NDK usage). The design was inspired by the Google Chrome team's guitar tuner web app using the WebAudio API. I wanted to code a version written natively for Android that didn't have to rely on a WebView, the WebAudio APIs, or server-side logic. Also, I wanted this application to be available to as many versions of Android as possible (whereas the WebAudio API may only be supported in more recent versions of WebView available only on newer flavors of Android). So, I coded it from scratch. I used a portion of the open source TarsosDSP project (their YIN algorithm) to help with the pitch detection.

    The application is available in the Google Play Store for Android: https://play.google.com/store/apps/details?id=com.chrynan.guitartuner. The project is completely open source and the code can be found on the GitHub repository: https://github.com/chRyNaN/Android-Guitar-Tuner. Fi…