Customizing individual share services in a ShareActionProvider on Android

In theory, sharing on Android is simple. Because of the system of Intents, you can prepare your shareable content, like an image, or text, and then easily show a list of installed apps which are able to handle this type of content. For example, if you’re sharing a photo, apps like Instagram, Twitter and the Gallery app will be available in the list. No additional coding is required for each sharing service!

  1. Intent i=new Intent(android.content.Intent.ACTION_SEND);
  2. i.setType("text/plain");
  3. i.putExtra(android.content.Intent.EXTRA_SUBJECT,"My cool app");
  4. i.putExtra(android.content.Intent.EXTRA_TEXT, "Here’s some content to share");
  5. startActivity(Intent.createChooser(i,"Share"));

But what if you want to customise the content depending on the chosen service? For example, if the user chooses Twitter you might want to shorten the text you’re sharing. Facebook on Android also has a long-running bug that means it won’t share text/plain content shared via an Intent… it only works for links.

Perhaps we’d like to use some different logic when our user clicks Facebook, for example using the Facebook Android SDK we could invoke an Open Graph sharing dialog.

Unfortunately, this is not easy. If you’ve followed the Android tutorial on adding an easy share action to the action bar then you’ll have a ShareActionProvider which creates a share button and dropdown for your action bar.

sap

The documentation is rather contradictory about whether you can customise ShareActionProvider’s behaviour. There’s a promising looking listener called setOnShareTargetSelectedListener, described here:

Sets a listener to be notified when a share target has been selected. The listener can optionally decide to handle the selection and not rely on the default behaviour which is to launch the activity.

So we might think to check the type of the intent in the listener, and run some custom behaviour for certain share types.

However the documentation goes on to say that

"Modifying the intent is not permitted and any changes to the latter will be ignored. You should not handle the intent here. This callback aims to notify the client that a sharing is being performed, so the client can update the UI if necessary."

The return result is ignored. Always return false for consistency.

It turns out if we try to add some custom code in setOnShareTargetSelectedListener, the custom code is run, but the standard share intent is also launched 🙁

Luckily since Android is open source, we can dig around in the source code to find out what’s going on.

Here’s the source code for the ShareActionProvider class in the v7 support library on Github.

Notice the class at the bottom ShareActivityChooserModelPolicy, which calls the listener, but then returns false regardless. Returning true from this method would allow us to handle the intent, without invoking the default behaviour.

  1. private class ShareActivityChooserModelPolicy implements OnChooseActivityListener {
  2. @Override
  3. public boolean onChooseActivity(ActivityChooserModel host, Intent intent) {
  4. if (mOnShareTargetSelectedListener != null) {
  5. mOnShareTargetSelectedListener.onShareTargetSelected(ShareActionProvider.this,intent);
  6. }
  7. return false;
  8. }
  9. }

We can’t easily subclass ShareActionProvider to override this behaviour, but what we can do is make a complete copy of the class and implement our own custom behaviour!

Copy the entire source file into your app, changing the package declaration at the top, and optionally the class name, for example to RDShareActionProvider.

Implement a new listener

  1.  
  2. private OnShareListener mOnShareListener; //also need to add getter and setter
  3.  
  4. public interface OnShareListener {
  5. /**
  6. * Called when a share target has been selected. The client can
  7. * decide whether to perform some action before the sharing is
  8. * actually performed OR handle the action itself.
  9.  
* * @param source The source of the notification. * @param intent The intent for launching the chosen share target. * @return Return true if you have handled the intent. */ public boolean willHandleShareTarget(RDShareActionProvider source, Intent intent); }

Time to re-implement the ShareActivityChooserModelPolicy using our new more powerful callback.

  1.  
  2. private class ShareActivityChooserModelPolicy implements OnChooseActivityListener {
  3. @Override
  4. public boolean onChooseActivity(ActivityChooserModel host, Intent intent) {
  5. if (mOnShareListener != null) {
  6. boolean result = mOnShareListener.willHandleShareTarget(
  7. RDShareActionProvider.this, intent);
  8. return result;
  9. }
  10. return false;
  11. }
  12. }
  13.  

We're in the home straight! Now we need to change the reference in the menu XML to our new class name.

  1. <item
  2. android:id="@+id/action_share"
  3. android:title="@string/menu_share"
  4. android:icon="@drawable/ic_action_share"
  5. myapp:showAsAction="always"
  6. myapp:actionProviderClass="com.myapp.RDShareActionProvider"

Finally we can implement our listener. We can check the package name of the intent, each sharer will have a different one, depending on the app. For Facebook, it’s com.facebook.katana.

  1.  
  2. mShareActionProvider.setOnShareListener(new OnShareListener() {
  3. @Override
  4. public boolean willHandleShareTarget(RDShareActionProvider source, Intent intent) {
  5. if (intent.getComponent().getPackageName().equalsIgnoreCase("com.facebook.katana")) {
  6. //just showing a toast for now
  7. //we could also manually dispatch an intent, based on the original intent
  8. Toast.makeText(self, "Hey, you're trying to share to Facebook", Toast.LENGTH_LONG).show();
  9. return true;
  10. } else {
  11. return false; //default behaviour.
  12. }
  13. }
  14. });
  15.  

Finally, we have control over individual sharers!

Matt Mayer

Matt Mayer is a founder at ReignDesign. Matt is from the UK and was based in Shanghai for ten years. He is now living in Bangkok, Thailand.

14 comments

  1. Hi,
    I also want to customize the onShareTargetSelected return value to true to use updated intent in my app, But I am not able to customize it.Your post partially helped me. Can you please provide me the working code of this post so that I can do the same in my app.

  2. Hi,
    Great job, but i can’t get it to work.

    How do you initialize mShareActionProvider?? I’m trying to do this:
    MenuItem item = menu.findItem(R.id.menu_item_share);
    // Fetch and store ShareActionProvider
    mShareActionProvider = (RDShareActionProvider) MenuItemCompat.getActionProvider(item);
    But it gives me null pointer exception.
    Thanks for your help¡¡

  3. Thanks a lot. Got it working. One problem thoug. Returning true in onChooseActivity() will cause the history file to not be updated. Do you know of a fix for this?

  4. @Daniel

    You need to add:

    public void setOnShareListener(OnShareListener listener) {
    mOnShareListener = listener;
    setActivityChooserPolicyIfNeeded();
    }

    This is skipped in the tutorial…

  5. In setActivityChooserPolicyIfNeeded() you have to change:
    if (mOnShareTargetSelectedListener == null) {
    return;
    }
    by:
    if (mOnShareListener == null) {
    return;
    }

  6. So the problem with this is that the ActivityChooserModel interface import cannot be found which is referenced in the source of ShareActionProvider several times, including in the method we would like to customise. The default import is android.widget.ActivityChooserModel.OnChooseActivityListener

    Any ideas on how we can get the ShareActionProvider to compile if it can’t find this class (or com.android.internal.R either)?

  7. Thanks for the great post and comments. They really helped me forward with my ShareActionProvider problems.

    @Dhruva: I struggled with the history issue, too, and ended up making complete copies of ActivityChooserModel.java and ActivityChooserView.java files. In ActivityChooserModel class there is chooseActivity method that returns null if onChooseActivity method has returned true earlier. And it does that before updating the history file, so I guess that is why the history does not get updated at all. I moved the lines that update the history a bit upwards, inside if clause, like this:

    public Intent chooseActivity(int index) {

    HistoricalRecord historicalRecord = new HistoricalRecord(chosenName, System.currentTimeMillis(), DEFAULT_HISTORICAL_RECORD_WEIGHT);
    addHisoricalRecord(historicalRecord);

    if (handled) {
    return null;
    }

    I’m a bit freshman in creating Android apps, so I’m not sure if this is the right solution, but at least I got it working is most of the cases. Some weirdness remained, for some reason the list is not always updated correctly. Perhaps something to do with the sorting. But I can live with that for now.

    Of course, now that I have both those classes in my own control, I could call the history methods directly e.g. from myShareActionProvider. Something like this:

    ComponentName chosenName = new ComponentName(
    intent.getComponent().getPackageName(),
    intent.getComponent().getClassName());

    HistoricalRecord historicalRecord = new HistoricalRecord(chosenName,
    System.currentTimeMillis(), DEFAULT_HISTORICAL_RECORD_WEIGHT);
    host.addHisoricalRecord(historicalRecord); // Yes, it really is “Hisorical”

  8. Hello everybody,
    I tried to implement this since it sounds incredible to me that there’s no standard way to customize sharing data.

    I think I did everything good but still, when I click on the share button, nothing happens. Just nothing :-/

    This is my onCreateOptionsMenu:

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
    if (getIntent().getBundleExtra(“bundle”).getBoolean(“success”, false))
    getMenuInflater().inflate(R.menu.share, menu);

    MenuItem shareItem = menu.findItem(R.id.btn_share);

    mShareActionProvider = (CustomShareActionProvider) MenuItemCompat.getActionProvider(shareItem);

    mShareActionProvider.setOnShareListener(new CustomShareActionProvider.OnShareListener() {
    @Override
    public boolean willHandleShareTarget(CustomShareActionProvider source, Intent intent) {
    if (intent.getComponent().getPackageName().equalsIgnoreCase(“com.facebook.katana”)) {
    //just showing a toast for now
    //we could also manually dispatch an intent, based on the original intent
    Toast.makeText(BookingResultActivity.this, “Hey, you’re trying to share to Facebook”, Toast.LENGTH_LONG).show();
    return true;
    } else {
    return false; //default behaviour.
    }
    }
    });
    return true;
    }

    CustomShareActionProvider is of course my version of ShareActionProvider, with the changes explained in this post.

    I see the button on the ActionBar, I can see debugging that mShareActionProvider gets properly initialized. What I don’t understand is: what should trigger the OnShareListener? What should make the sharing popup appear? Should I explicitly call setShareIntent? If so, with what Intent?

    In other words: why – in your opinion – does this not work?

  9. I managed to make it work by adding, under the lines I pasted before, the following:

    Intent mShareIntent = new Intent();
    mShareIntent.setAction(Intent.ACTION_SEND);
    mShareIntent.setType(“text/plain”);
    mShareIntent.putExtra(Intent.EXTRA_TEXT, “From me to you, this text is new.”);

    if (mShareActionProvider != null) {
    mShareActionProvider.setShareIntent(mShareIntent);
    }

    Now it works but the only intent I get to be shared is the one I explicitly create, a text/plain. Should I put extra content in the same intent inside the willHandleShareTarget method in case I get to know the content is going to be shared on a specific app?

    Thank you, sorry for bothering-

Leave a Reply

Your email address will not be published. Required fields are marked *

Share this post