Monthly Archives: January 2012

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);
    }
}

Tackling Android’s Camera Resolutions

Problem

Anyone who has played with Android knows that form factor is not to be taken lightly.  The platform is available on an ever growing number of devices with a wide variety of resolutions and pixel densities.  This can give developers grief, particularly if they are coming from iPhone dev, where, with few exceptions, the hardware is the same across all  users.  Recently, I was developing an app that made use of the devices camera.  Everything was going swimmingly until my app began crashing on my Nexus S, but not on my Galaxy Tab or the Evo.  After some digging, it became clear that I was trying to set a camera resolution that was not compatible with the Nexus S.

My first go-around seemed simple enough.  I had not hard-coded a resolution, but instead was polling the device for its display size, and using the width and height as my camera resolution [swapping the two if necessary to ensure a ‘portrait’ resolution].  However, there are apparently many phones who’s display resolution is not available as a camera resolution.  Fail.

My thoughts then began to trend toward just setting the highest resolution available as I am sure many do, but that didn’t seem good enough.  I can think of many times wherein a developer may want a SurfaceView that is a particular portion of the screen, and thus a compatible resolution that is close to that size.

Solution

Fortunately after a bit of tinkering, I came up with a solution I am pretty fond of.  Rather than picking the highest resolution and cramming it into a SurfaceView, why not find a resolution that most closely matches the SurfaceView we specified?  That is what I chose to do, using the following algorithm.

 
public int[] findCompatibleResolution(int width, int height) { 
    List<Size> sizes = camera.getParameters().getSupportedPreviewSizes();
    int[] resolution = { 0, 0 };
    int delta = Integer.MAX_VALUE;
    int temp = 0;
    for(Size size : sizes) {
        temp = Math.abs(size.width - width) + Math.abs(size.height - height);
        if(temp < delta) {
            delta = temp;
            resolution[0] = size.width;
            resolution[1] = size.height;
        }
    }
    return resolution;
}



This method takes width and height parameters and looks tries to find a resolution that varies from these the least.  It will then return an int array with the compatible width in [0] and compatible height in [1].

Hope you find this useful!  Please feel free to post questions and I will do my best to get back to them. Also, check out posts, whitepapers, and other great content at http://blog.credera.com