Tag Archives: JAVA

Using Android WebView to display a webpage on top of the Unity App view

Hello and welcome to my tutorial on how to show a WebView on top of your Android Unity App, while still allowing the user to interact with your Unity UI.

You can watch the video of this tutorial at https://youtu.be/r1hLo5C50wE.

This tutorial assumes a reasonable knowledge of Unity, C#, Android Studio and Java. The source code for this tutorial can be found at https://github.com/cwgtech/androidwebview.

The plan is to extend the plugin created in my previous tutorials by adding a method that will create an android layout containing an Android WebView object and a blank TextView. We’ll adjust the height of the TextView to create space at the top of the layout that will allow the user to still see and interact with a portion of the Unity viewspace. We’ll add this layout to our App’s content view which will place it on top of the Unity view.

We’ll also add a method to remove this layout from the content view, returning the full screen to Unity.

Get started by loading up the previous version of this project in Unity and the MyPlugin project in Android Studio. If you don’t have it, you can download it from https://github.com/cwgtech/AndroidActivityResult.

Using Android Studio, open the MyPlugin java source and add the following variable declarations above the first method definition:

private LinearLayout webLayout;
private TextView webTextView;
private WebView webView;

We’re going to use these vars to store references to the objects we create when the webview is displayed. This will allow the plugin to close and deallocate those objects when the webview is closed.

Add the following method to the body of the plugin:

public void showWebView(final String webURL, final int pixelSpace)
{
    mainActivity.runOnUiThread(new Runnable() {
    @Override
    public void run() {
        Log.i(LOGTAG,"Want to open webview for " + webURL);
        if (webTextView==null)
            webTextView = new TextView(mainActivity);
        webTextView.setText("");
        if (webLayout==null)
            webLayout = new LinearLayout(mainActivity);
        webLayout.setOrientation(LinearLayout.VERTICAL);
        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
        LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
        mainActivity.addContentView(webLayout,layoutParams);
        if (webView==null)
            webView = new WebView(mainActivity);
        webView.setWebViewClient(new WebViewClient());
        layoutParams.weight = 1.0f;
        webView.setLayoutParams(layoutParams);
        webView.loadUrl(webURL);
        webLayout.addView(webTextView);
        webLayout.addView(webView);
        if (pixelSpace>0)
            webTextView.setHeight(pixelSpace);
        }
    });
}

The method showWebView takes two parameters, the URL of the webpage you want to display, and the number of screen pixels the layout needs to reserve for the Unity UI. This version assumes that the Unity UI is at the top of the screen and pushes the WebView down, you’ll need to modify the order the views are added to the layout if this is not what you want.

First, we create the TextView and set its contents to an empty string.

Next we create the LinearLayout and set its orientation to vertical, and its layout so that it will fill its parent object, and then add it to the ContentView for our activity.

Lastly, we create the actual WebView and we assign the WebViewClient to be a default WebViewClient. This tells our WebView how to handle links, and with the default client, the links will be opened in our WebView. Without this, when the user clicks on a link, Android will pop-up a chooser asking the user what app they want to send the link to.

We also set the weight of the WebView to 1, which means the layout system will give our WebView as much space as it can. Finally, we tell the WebView to load the URL passed to this method.

The TextView and WebView are added to the LinearLayout object, with the order they are added determining the order they will appear on screen, and the height of the TextView is set to the number of screen pixels we want to push the WebView down by.

We need to add one more method that will allow our Unity app to remove the Layout when it’s no longer needed. Add the following code:

public void closeWebView(final ShareImageCallback callback)
{
    mainActivity.runOnUiThread(new Runnable() {
        @Override
        public void run() {
            if (webLayout!=null)
            {
                webLayout.removeAllViews();
                webLayout.setVisibility(View.GONE);
                webLayout = null;
                webView = null;
                webTextView = null;
                callback.onShareComplete(1);
            }
            else
                callback.onShareComplete(0);
        }
    });
}

This method is going to reuse the ShareImage callback interface. We could create a new interface just for this method, but there is no harm in using an existing interface that can do the same job, which is to let Unity know when we’ve closed the layout.

To remove the layout, first remove all it’s child views, then set it’s visibility state to GONE. This will cause it to remove itself from its parent and mark it for garbage collection. Setting the vars that hold the references to our views to NULL will also allow the garbage collection system to free the memory used by them.

Lastly, trigger the supplied callback passing a 1 if the close happened as expected, or a 0 if the LinearLayout had already been closed.

That completes the modifications to the plugin, so you can go ahead and let Gradle build it and copy the updated AAR to the Plugin folder in the Unity project.

Switch back to Unity where we’ll modify the canvas object in the hierarchy view to include a new layer for our WebView, but first double click the script PluginTest to open it in Visual Studio.
Add the following two lines to the C# code, after the other public UI vars:

public RectTransform webPanel;
public RectTransform buttonStrip;

These will hold references to the UI objects we’ll create later. The webPanel is the root UI object that will contain all the objects that will be displayed when the WebView is on screen, and buttonStrip holds the title text, and the close button.

Now add the following methods that will call our Java methods, but only if we’re on an Android platform:

public void OpenWebView(string url, int pixelShift)
{
    if (Application.platform == RuntimePlatform.Android)
    {
        PluginInstance.Call("showWebView", new object[] { url, pixelShift });
    }
}

public void CloseWebView(System.Action<int> closeComplete)
{
    if (Application.platform == RuntimePlatform.Android)
    {
        PluginInstance.Call("closeWebView", new object[] { new ShareImageCallback(closeComplete) });
    }
    else
        closeComplete(0);
}

These methods are just wrappers for the Java code and pass the parameters directly to the plugin.
Next, add the method we’ll connect to a UI button that will figure out how much space to reserve at the top of the display and then pass that with the URL to our Java wrapper.

public void OpenWebViewTapped()
{
    Canvas parentCanvas = buttonStrip.GetComponentInParent<canvas>();
    int stripHeight = (int)(buttonStrip.rect.height * parentCanvas.scaleFactor + 0.5f);
    webPanel.gameObject.SetActive(true);
    OpenWebView("http://www.cwgtech.com", stripHeight);
}

We get a reference to the Canvas object that our buttonStrip belongs to, and then use it’s scaling factor along with the height of our ButtonStrip to calculate how many screen pixels we need to push the webview down by. Enable the WebPanel and pass the URL and height to our Java wrapper.

Add the following method:

public void CloseWebViewTapped()
{
    CloseWebView((int result) =>
    {
        webPanel.gameObject.SetActive(false);
    });
}

This method will be connected to the close button child of the buttonStrip. It simply calls our Java wrapper, using the inline function to hide the WebPanel object once the Android views have been cleaned up and removed.

Save the file and return to Unity. Wait a few seconds to let Unity recompile the C# code and then expand the Canvas object.

Right click on the Canvas object, and select UI, then Button and left click. This will create a button in the middle of the screen called Button (1). Rename it to browseButton and expand it. Change the default text on the child Text object to ‘Browse’.

Highlight the browseButton again click the + button on the On Click list of the button script. Now drag the Main Camera object into the reference holder. Click the function selector, click PluginTest and then OpenWebViewTapped.

Right click on the Canvas object, then click UI, then Panel. Rename the panel created to WebPanel. Click on the color gizmo and set the color to pink (#FFD4F7) and alpha to 255.

Right click on WebPanel and then click UI, Image. Click on the Rect Transform gizmo, and select top center, horizontal stretch while hold shift and alt (option on mac). This will move the image to the top of the screen and make it fill the view horizontally. Set the height to 50 and the color to black. Rename the Image to ButtonStrip.

Right click on ButtonStrip and then UI, Text. Click the Rect Transform gizmo and set stretch for vertical and horizontal, while holding shift & alt (option on mac). This will cause the Text object to fill the image area. Set the font size to 28, the color to White, and the text to ‘Web Page View’.

Click the checkboxes to center the text both horizontally and vertically.

Right click on the ButtonStrip again and click UI,Button. Click the Rect Transform gizmo and then top right anchor with shift & alt held (option on mac). Set the width and height of the button to 24. Expand the button object and set the default text of the Text child to ‘X’. Rename the button to ‘CloseButton’.

Just like we did for the BrowseButton, connect the OnClick event for the CloseButton to the CloseWebViewTapped method of PluginTest.

Highlight the Main Camera, and in the PluginTest script object, drag the WebPanel object to the Web Panel holder and the ButtonStrip object to the Button Strip holder. Highlight the WebPanel object in the hierarchy and disable it.

Save and run the scene. If you click the Browse button, you’ll see the WebPanel appear and the close button will hide it. There will be no actual webview as we’re not yet running on an Android platform. Stop execution of the player.

Click ‘File’ and then ‘Build Settings’. Click on ‘Player Settings’ and then ‘Other Settings’. Scroll down to the configuration area and change ‘Internet Access’ from ‘Auto’ to ‘Require’.

Now click ‘Build and Run’ to build the Android version and run it on your connected device. In my case, I’m running it on an emulator I started earlier. We need to tell Unity to include the Internet permission as Unity is unaware that our plugin is making calls to fetch content from the web and won’t add it by itself.

With the app running, tapping on the browse button will make the WebPanel appear, which is why we made it pink, and if you tap a link in the web content, you’ll see the webview follow the link. Tapping the close button will close the Android webView and also disable the WebPanel, allowing our app to behave as before.

I hope you found this tutorial useful. You can use this to show a help page, or information page directly in your Unity app that is either stored as a HTML file, or is downloaded from a website. You can add more controls to the Unity Canvas to allow you to navigate forwards and backwards, and maybe jump to a specific URL.

As always, you can follow me on Twitter @cwgtech, or check out my youtube channel at https://www.youtube.com/channel/UCdrrB0J4ovI4xQkqiK4HEiw. Please feel free to leave any comments or suggestions below, or let me know how you customized this technique for your own purpose. Subscribe to my youtube channel to get notified when I post a new tutorial.

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.

Unity 5.6 and Android overlays – Updated

Unity just released 5.6.2f1 which rolls back the change they made to their Android player.  While the method described below works, it’s no longer needed.  There was also some performance issues using the PopupWindow, so it’s better just to go back to using a view added to the Unity view.

I updated the Unity version I’ve been using to 5.6 when it came out of beta, but when I built my android app, I noticed a major problem – the ad banners no longer appeared!  So glad I made sure to test my app before updating in to GPlay 😀

Turns out 5.6 uses a feature in Android to make their view sit on top of all other views for that activity, and when my plugin would create a view to hold both my AdMob view and Amazon adView, it wouldn’t display them on top of the Unity view as before.

I tried a few different methods before seeing that the folks who maintain the AdMob plugin for Android on Unity had  updated their method to use a PopupWindow.  I did some research on this method, and got it to work for both AdMob and Amazon ads.

The one thing that had me stuck for a bit was that you cannot show a PopupWindow right after creating it in the application onCreate method, you need to wait for a bit.  This was also true when I created the PopupWindow during my plugin initialization.

I used the post() method on the rootView to get the delay I needed.

    if (mPopupWindow!=null)
    {
        activity.getWindow().getDecorView().getRootView().post(new Runnable() {
        public void run() {
            if (isDebug)
                Log.i(LOGTAG,"Showing POPUP window...");
            mPopupWindow.showAtLocation(activity.getWindow().getDecorView().getRootView(),
                            Gravity.TOP, 0, 0);
        }
    });
    }

At some point, I’ll update my plugin to allow the PopupWindow to be either at the top or bottom of the display, right now it’s hard-coded to appear at the top.

Running JAVA class from Mac OS CLI

I wrote a utility in Java that I want to eventually run as a cron job, which means I need to be able to specify the path that the code uses for its files.

There is a command line parameter that is passed to the java command that can be used to specify a particular directory, and then my java code can use that as the base path.

My command line to execute the java code is:

java -cp /Users/cwg/workspace/getInfo/bin -Duser.dir=/Users/cwg/workspace/getInfo com.cwgtech.getInfo

This will call the java code from any path, and the -Duser.dir sets the user variable to point to the path my data is stored in.

In the java code, I access the passed variable with the following code:

workingPath = System.getProperty("user.dir") + "/";

Then when I want to access a file or folder, I just pre-pend it with the workingPath String.

Node xml = loadXML(workingPath + XMLFILE);

Note: Don’t use ~ in the path for the java command, use the fully qualified path!

Also, I ended up using a couple of external Jars in my code, so I had to modify the -cp option to include the path to my lib folder as follows:

-cp /Users/cwg/workspace/getInfo/bin:/Users/cwg/workspace/getInfo/libs/*

The * at the end of the second path says to use all the jar (or zip) files in the specified folder when looking for classes.  The : is the separator if you need more than one path in -classpath (can be shortened to -cp)