Using a child activity to wait for onActivityResult with Unity3D

Welcome to my tutorial on how to extend our Unity plugin to get a callback from onActivityResult without overriding the standard Unity Player Activity.

We will add a child activity to our plugin that will be launched when required, and will wait for a call to startActivityForResult, and pass that back to our C# callback function.  Normally, we’d do this by extending the UnityPlayerActivity class, but that means we won’t play nice with any other plugins or extensions that want to do the same thing, and we must make sure our Android project imports the correct version of the UnityPlayer each time we upgrade.

Once again this tutorial assumes a reasonable familiarity with Unity, Java, Android programming and Android Studio.

Start by loading up our previous project in Unity and the MyPlugin project in Android Studio.  All of the changes we’re going to make this time will be entirely to the Java code.

Right click on the Unity tab in the Project View and select New/Activity/Empty Activity.

Call the Activity “OnResultCallback”, deselect ‘backwards compatibility’ and make sure the package name matches the package name you’ve been using.  For me, that is ‘com.cwgtech.unity’. Click Finish.

If you forget to uncheck the backwards compatibility box, you’re new activity will extend AppCompatActivity.  You need to change that to Activity.

Add the following four lines:

public static final String LOGTAG = MyPlugin.LOGTAG + “_OnResult”;
public static MyPlugin.ShareImageCallback shareImageCallback;
String caption;
Uri imageUri;

You’ll get an error on MyPlugin.LOGTAG, so you’ll need to switch back to the MyPlugin class and change the LOGTAG definition from private to protected.  We’re going to use this modified LOGTAG to identify the Log entries from this child activity, while the static callback variable will hold a pointer to the C# callback our main plugin receives.

Switch back to our new activity, and add the following method before the onCreate method:

void myFinish(int result)
{
    if (shareImageCallback!=null)
        shareImageCallback.onShareComplete(result);
    shareImageCallback = null;
    finish();
}

We’ll use this to exit our activity, calling the callback method if it exists, and then clearing it after use.  Now modify the default onCreate method. Remove the line

setContentView(R.layout.activity_on_result_callback);

This new activity will not have a content view, so we’ll not need to set it.

Add the following lines:

Log.i(LOGTAG, "onCreateBundle");
Intent intent = getIntent();
if (intent != null) {
    caption = intent.getStringExtra(Intent.EXTRA_TEXT);
    imageUri = (Uri)intent.getExtras().get(Intent.EXTRA_STREAM);
    Log.i(LOGTAG, "Uri: " + imageUri);
}
if (intent==null || imageUri==null)
{
    myFinish(1);
    return;
}

This will get the intent passed to our activity and grab the caption and imageUri that were included in the intent.  If there is no intent or image, then just exit the activity, as we’ve nothing to do and we’ve been called incorrectly.

Now to call the share intent and wait for a result.  Add the following:

try
{
    Intent shareIntent = new Intent(Intent.ACTION_SEND);
    shareIntent.setDataAndType(intent.getData(),intent.getType());
    shareIntent.putExtra(Intent.EXTRA_STREAM,imageUri);
    if (caption!=null)
        shareIntent.putExtra(Intent.EXTRA_TEXT,caption);
    startActivityForResult(Intent.createChooser(shareIntent,"Share with..."),1);
}
catch (Exception e)
{
    e.printStackTrace();
    Log.i(LOGTAG,"error: " + e.getLocalizedMessage());
    myFinish(2);
}

We copy forward the data from the incoming intent to a new intent, which we then pass on to the chooser and wait for a result.  Wrap the whole thing in a try/catch so any errors will be flagged and the app will not crash.

Add a new method that will override the default onActivityResult method and pass the resultCode back to our callback.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    Log.i(LOGTAG,"onActivityResult: " + requestCode + ", " + resultCode + ", " + data);
    myFinish(resultCode);
}

The result code will be -1 or 0 depending on the share activity, so we’ll pass that back to our C# method.  Note that earlier we sent a 1 or 2 depending on the error condition, so our C# code could check for that and give the user more information.

That’s the Java code for our child activity completed, but we’ve a few things to clean up in the manifest and we can also remove the layout that was auto-created.

Expand the res folder in the project hierarchy and right click on the ‘layout’ folder.  Select delete and confirm the deletion. This will remove the folder and the un-needed layout file.

Now expand the manifest folder and double click the AndroidManifest.xml file.  After the name=”.OnResultCallback” but before the closing > add the following line:

android:theme="@android:style/Theme.Translucent.NoTitleBar"

This will cause our child activity to effectively have no display.  If we use the NoDisplay theme, then we run into a problem on Android 6 and higher.  A NoDisplay theme expects an activity to call finish before it’s resumed, and in our case that means we don’t get the OnResultCallback as our activity has been terminated.

Switch back to the MyPlugin java source where we will modify the plugin to use our new child activity.

Go to the section in shareImage where we prepare the shareIntent.  We’re going to replace the call to startActivity with the following three lines:

shareIntent.setClass(mainActivity,OnResultCallback.class);
OnResultCallback.shareImageCallBack = callback;
mainActivity.startActivity(shareIntent);

Make sure you remove the line:

mainActivity.startActivity(Intent.createChooser(shareIntent,"Share with..."));

And you can also remove any references to the ‘result’ variable, as we will no longer use it, including the line:

callback.onShareComplete(result);

As our new child activity will call the callback hook when the OnActivityResult is triggered.

That’s all the modifications completed, so build the plugin by clicking on the green play button, assuming you’ve still got the copyPlugin task indicated in the dropdown.  When gradle completes, you can switch back to Unity and build the project using the new plugin.

I’m going to run the apk on the emulator as before, but now when I tap the share button, I’ll still get the share dialog, however the result pop-up alert will not occur until I’ve finished interacting with the share dialog.

And that’s it done.  We’ve now got a child activity that will send our image to the share system, and wait for it to finish and return a value, which we then forward to our Unity App.  You can use this technique for any intent you need a result from. You’ll need to add custom code that either uses the requestCode passed when the activity is started to decide how to handle the passed intent, or create other child activities that just handle your specific case, whether that is a photo-picker or a QR code scan request.

You can download the source code for this plugin from https://github.com/cwgtech/AndroidActivityResult, and watch the video of this tutorial at https://youtu.be/HrhYWBqxkn8

Please feel free to post any comments or questions.

Leave a Reply