Intercepting Android’s action_send Intents

Background

The folks at Google get it.  Eating a sandwich isn’t good enough.  You need to make sure everyone you know KNOWS you ate a sandwich, what you thought of it, how much it costs, and any other critical cuisine-related details you’ve picked up in the last 28 seconds.  And because Google gets it, they designed a remarkably easy to use model for “sharing”.

Of course I don’t mean sharing your sandwich.  I’m talking about sharing in the context of social media.  With so many social media outlets and more coming out every day, attempting to find and learn an API and integrate each one into your Android app so users can share would be an impossibly cumbersome task.  The clean solution is this: Let the social media platforms worry about the integration.  All this requires is that NewSocialSiteX puts a bit of code in their own app that lets a user’s Android device know it is receptive to sharing, as well as how to handle the incoming data.  Of course this really just follows Google’s intent-based API model and likely has little to do with sandwiches.

This model buys developers some incredible functionality with very little work.  With a minimal amount of code, we can allow users to post to Facebook, Tweet, text, email gmail or any other mail they’d like, provided they have an app installed on their phone that has registered the action_send intent.

Problem

Well that’s all great, but what happens when I want to disallow SMS clients?  Or the Facebook app won’t allow my app to pre-populate a wall post?  This was the case I was faced with.  I needed my app to allow sharing of any kind, including Facebook.  Unfortunately, Facebook’s standard wall post functionality [after July 12th 2011] ignores the ‘message’ parameter to prevent apps from pre-populating wall post data.  Now we’re back to square one and looking at individually integrating each client just because one doesn’t work as we would like!  Fortunately, I found a better option that I feel is worth sharing.

Solution

What I wanted was a way to fire an action_send intent but intercept the users choice so I could then do what I wanted, and that is exactly what I managed.  The solution is relatively simple, but does assume you have some knowledge about using ListViews [here for help] and the ACTION_SEND intent [here for help].  Here is some code to bring some context, then we’ll examine each part.


package com.example.blog;

import java.util.List;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import com.facebook.android.Facebook;
import com.goldsgym.android.spotter.R;

public class ShareHelper {
	Context context;
	String subject;
	String body;
	Facebook facebook;
	public ShareHelper(Context context, String subject, String body) {
		this.context = context;
		this.subject = subject;
		this.body = body;
		facebook = null;
	}
	
	public Facebook share() {
	Intent sendIntent = new Intent(android.content.Intent.ACTION_SEND);
	sendIntent.setType("text/plain");
	List activities = context.getPackageManager().queryIntentActivities(sendIntent, 0);
	AlertDialog.Builder builder = new AlertDialog.Builder(context);
	builder.setTitle("Share with...");
	final ShareIntentListAdapter adapter = new ShareIntentListAdapter((Activity)context, R.layout.basiclistview, R.id.text1, activities.toArray());
	builder.setAdapter(adapter, new DialogInterface.OnClickListener() {
		@Override
		public void onClick(DialogInterface dialog, int which) {
			ResolveInfo info = (ResolveInfo) adapter.getItem(which);
			if(info.activityInfo.packageName.contains("facebook")) {
				new PostToFacebookDialog(context, body).show();
			} else {
				Intent intent = new Intent(android.content.Intent.ACTION_SEND);
				intent.setClassName(info.activityInfo.packageName, info.activityInfo.name);
				intent.setType("text/plain");
				intent.putExtra(Intent.EXTRA_SUBJECT, subject);
				intent.putExtra(Intent.EXTRA_TEXT, body);
				((Activity)context).startActivity(intent);
			}
		}
	});
	builder.create().show();
	return facebook;
	}
}

 

In the code above, you see my ShareHelper. This class has a single constructor that takes 3 arguments: the context of the activity where it is instantiated, the subject text we would like to share [eg subject line of an email] and the body text we would like to share [eg body of an email]. Past that we see the class’ single method, ‘share’. This method takes no parameters, but does almost all of the work. Let’s take a look.

In the share method, we first create a new action_send intent and set the type to text/plain. Next, we create a List. We make the call to the package manager to query for all Activities with the action_send intent registered. This call returs a list of ResolveInfo objects, each corresponding to an Activity on the device that claims to handle send actions.

Now this is when the magic happens. Rather than launching the action_send intent and letting it create its own dialog [one that we would have no control over], we will build our own with the AlertDialog.Builder. First we give it a title, then set its adapter to the list adapter we just created with the activities list as our data set [list adapter code posted at the end of the article, but there is nothing special about it].

The next important piece we need to look at is the OnClick listener we gave the Builder. To find which item the user clicked, we use the adapter.getItem(int which) method. This will return the object in that position of our original list, in this case, a ResolveInfo object corresponding to the selected Activity. For my case in particular, I only care about separating things into two groups: Facebook and not Facebook. To do this, I simply check if the selected Activity’s package name contains ‘facebook’. If it does not, I create a new action_send intent, set the class name to the selected activity, and launch it. However, if the package name DOES contain ‘facebook’, I instantiated my personal PostToFacebookDialog object which will create a basic Android dialog with post and cancel buttons, and will post to Facebook directly using the Graph API, thus circumventing the user’s installed Facebook app.

Closing Notes

This solution may seem Facebook specific, but it is actually quite powerful. Using this same idea, you can eliminate certain apps from the list, filter the content the user has chosen to share, or even reorder the list to give preference to apps you feel users prefer. I hope this post was clear, but I know all too well that reading someone else’s code can get a little mind bending. If you are feeling lost, please do not hesitate to post questions! I will do my best to help. Also, see screenshots of the custom Activity list dialog and Facebook post dialog below, as well as my list adapter code. Finally, be sure to check out posts, whitepapers, and other great content at http://blog.credera.com


package com.example.blog;
 
import android.app.Activity;
import android.content.pm.ResolveInfo;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
 
public class ShareIntentListAdapter extends ArrayAdapter
{
    Activity context;
    Object[] items;
    boolean[] arrows;
    int layoutId;
 
    public ShareIntentListAdapter(Activity context, int layoutId, Object[] items) {
        super(context, layoutId, items);
 
        this.context = context;
        this.items = items;
        this.layoutId = layoutId;
    }
 
    public View getView(int pos, View convertView, ViewGroup parent) {
        LayoutInflater inflater=context.getLayoutInflater();
        View row = inflater.inflate(layoutId, null);
        TextView label = (TextView) row.findViewById(R.id.text1);
        label.setText(((ResolveInfo)items[pos]).activityInfo.applicationInfo.loadLabel(context.getPackageManager()).toString());
        ImageView image = (ImageView) row.findViewById(R.id.logo);
        image.setImageDrawable(((ResolveInfo)items[pos]).activityInfo.applicationInfo.loadIcon(context.getPackageManager()));
        
        return(row);
    }
}
Advertisements

10 thoughts on “Intercepting Android’s action_send Intents

  1. Thanks heaps for this. Worked like a charm.

    Do you know why the list items in the dialog are not enabled though. That is, they don’t change background colour when pressed?

  2. mpt245 says:

    @d_j_briggs, glad you found this helpful!

    As for the list items not changing color, that is part of the ListView’s functionality. There could be a number of things preventing it from highlighting, but more than likely you have set a custom background on your ListView layout but did not handle the highlighting yourself [whether it is changing the background color or swapping the background image]. I recommend you take a look at a ListView tutorial and see the various ways to get this functionality.

    Let me know if you have any further questions!

  3. Thanks for this! It’s super helpful for customizing what’s shared based on the target, and for gathering analytics, and just what I was looking for. One typo in the sample code: when you initialize ShareIntentListAdapter from ShareHelper, you pass in 4 arguments, but the constructor takes 3. Looks like you should remove R.id.text1.

    • mpt245 says:

      Thanks for the typo catch! I did a bit of refactoring to simplify things for the sake of example..guess I wasn’t as careful as I would have liked 🙂

  4. User says:

    Great post

  5. spacedroid says:

    Awesome post, had an idea like this bouncing in my head all day, was glad such a helpful post on this.

  6. Hi,
    I’m trying to use this with an intent to show all apps, so the user has an option to select any app and it will recieve the package name from it, but I’m having issues on this line in the ShareIntentListAdapter:
    super(context, layoutId, items);

    Could you upload the whole source of the app as a zip so I can import it and use it better?
    Thanks

    • mpt245 says:

      Hi Quinn,

      Thanks for posting. Unfortunately I cannot post the whole source – I used this piece in a product built for a client. What sort of trouble are you having? I know a few comments have been posted regarding minor issues with the code that I posted; I made a few mistakes when removing client-identifying code. Sorry if that turns out to be the cause of your issues!

      If you post a stack trace or describe your issue, I may be able to help next time I get a spare moment. Also, don’t forget about great resources such as stackoverflow! Lots of good info to be found there.

      Thanks for reading.

      • Thanks for your quick reply!
        This is the stack trace:
        E/AndroidRuntime( 7750): FATAL EXCEPTION: main
        E/AndroidRuntime( 7750): java.lang.IllegalStateException: Could not execute method of the activity
        E/AndroidRuntime( 7750): at android.view.View$1.onClick(View.java:3599)
        E/AndroidRuntime( 7750): at android.view.View.performClick(View.java:4204)
        E/AndroidRuntime( 7750): at android.view.View$PerformClick.run(View.java:17355)
        E/AndroidRuntime( 7750): at android.os.Handler.handleCallback(Handler.java:725)
        E/AndroidRuntime( 7750): at android.os.Handler.dispatchMessage(Handler.java:92)
        E/AndroidRuntime( 7750): at android.os.Looper.loop(Looper.java:137)
        E/AndroidRuntime( 7750): at android.app.ActivityThread.main(ActivityThread.java:5230)
        E/AndroidRuntime( 7750): at java.lang.reflect.Method.invokeNative(Native Method)
        E/AndroidRuntime( 7750): at java.lang.reflect.Method.invoke(Method.java:525)
        E/AndroidRuntime( 7750): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:799)
        E/AndroidRuntime( 7750): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:566)
        E/AndroidRuntime( 7750): at dalvik.system.NativeStart.main(Native Method)
        E/AndroidRuntime( 7750): Caused by: java.lang.reflect.InvocationTargetException
        E/AndroidRuntime( 7750): at java.lang.reflect.Method.invokeNative(Native Method)
        E/AndroidRuntime( 7750): at java.lang.reflect.Method.invoke(Method.java:525)
        E/AndroidRuntime( 7750): at android.view.View$1.onClick(View.java:3594)
        E/AndroidRuntime( 7750): … 11 more
        E/AndroidRuntime( 7750): Caused by: java.lang.NullPointerException
        E/AndroidRuntime( 7750): at android.widget.ArrayAdapter.init(ArrayAdapter.java:310)
        E/AndroidRuntime( 7750): at android.widget.ArrayAdapter.(ArrayAdapter.java:128)
        E/AndroidRuntime( 7750): at com.quinny898.app.srsmanager.ShareIntentListAdapter.(ShareIntentListAdapter.java:20)
        E/AndroidRuntime( 7750): at com.quinny898.app.srsmanager.MainActivity.chooseApp(MainActivity.java:717)
        E/AndroidRuntime( 7750): at com.quinny898.app.srsmanager.MainActivity.choose_app(MainActivity.java:708)
        E/AndroidRuntime( 7750): … 14 more
        W/ActivityManager( 409): Force finishing activity com.quinny898.app.srsmanager/.MainActivity

        Line 20 is obviously the line as follows:
        super(context, layoutId, items);

        Thanks for your help

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: