Building cross platform apps with Unity 3D is powerful and straightforward and this is one of the main reasons why Unity is popular among game developers and especially within the indie community. Its wide adoption on platforms like Android created the need for building plugins for libraries used in Android apps in order for developers to be able to take advantage of functionality provided. However, creating such plugins for Android is not well documented and there is no deep understanding on what happens behind the scenes.

In this article, we will try to explain the architecture of Android apps created with Unity and how to use WindowManager and different types of Windows to overcome issues created by Unity 5.6 release, that affected most of ad plugins out there and spiked several complains by publishers.

Unity’s view hierarchy in Android apps

When you build an app for an Android platform in Unity, in the generated apk you will see 3 different activities under the com.unity3d.player package UnityPlayerActivity, UnityPlayerProxyActivity and UnityPlayerNativeActivity. From those activities, both UnityPlayerProxyActivity and UnityPlayerNativeActivity are deprecated and the suggested way to go nowadays is UnityPlayerActivity.

As we can see from the AndroidManifest of the exported apk’s , UnityPlayerActivity becomes the entry point of the application, unless if requested otherwise. When UnityPlayerActivity is launched, it creates a UnityPlayer which is nothing more than a FrameLayout that holds the SurfaceView where-with the help of OpenGL-the actual game or app created in Unity will be drawn later on .

public class UnityPlayerActivity extends Activity {



protected UnityPlayer mUnityPlayer; ... protected void onCreate(Bundle bundle) {

requestWindowFeature(1);

super.onCreate(bundle);

getWindow().setFormat(2);

this.mUnityPlayer = new UnityPlayer(this);

setContentView(this.mUnityPlayer);

this.mUnityPlayer.requestFocus();

}

}

Below is an oversimplified version of the implementation of UnityPlayer.

public UnityPlayer(ContextWrapper contextWrapper) {

super(contextWrapper);

...

this.mSurfaceView = new SurfaceView((Context)contextWrapper);

this.mSurfaceView.getHolder().setFormat(2);

...

this.mSurfaceView.setFocusable(true);

this.mSurfaceView.setFocusableInTouchMode(true); // added on Unity 5.6

// removed in patch 5.6.1p4

this.mSurfaceView.setZOrderOnTop(true); addView(mSurfaceView);

}

As we can see above, when UnityPlayer is created, it creates a SurfaceView and then adds that view to itself.

When you create an Activity on Android and you do not explicitly call setContentView with a layout and do not apply any specific theme or request any special window feature, this is how the view hierarchy of the app will look like in its simplified version:

Activity without setContentView being called

You can read more on how the view hierarchy of an app is built in one of my previous articles here. When setContentView is called, the layout passed is attached to the FrameLayout with id content. In Unity’s case that layout is UnityPlayer and its child SurfaceView.

UnityPlayerActivity after setContentView is called

UnityPlayerActivity, aside from creating and setting UnityPlayer as the content view of the window, also dispatches events to UnityPlayer.

public class UnityPlayerActivity extends Activity { ... public boolean onTouchEvent(MotionEvent motionEvent) {

return this.mUnityPlayer.injectEvent(motionEvent);

}

}

This is a quick overview on how Android apps are structured when exported from Unity. SurfaceView is the core element in this structure and the place where the actual game created with Unity will be rendered.

Android Unity plugins

Android Unity plugins are created to allow access on features like OS calls or third party libraries available on Android, to Unity projects. There are different ways to create such plugins.

One approach is to extend and subclass UnityPlayerActivity and create your own PluginActivity. You then have to make PluginActivity the main entry point of the application. This overcomplicates the process when several plugins follow the same approach.

The second option is to create a class with a public static interface to allow initialization of the plugin and expose its features to Unity. Several plugins especially-in the ad space-follow this approach.

You can read a really good article on building Unity Android plugins here.

Android Unity plugins and Ad Networks

Most Ad Network plugins create an overlay in Unity apps to render their ads. We also work this way at Pollfish. Pollfish is a survey monetization network, delivering surveys instead of ads through mobile apps. The Pollfish SDK creates an overlay on top of the calling activity where the survey questionnaire is rendered. This SDK runs to date on more than 420M devices, worldwide. The Pollfish Unity plugin follows the second approach mentioned above, that is using a public static interface to allow initialization and create the overlay.

Pollfish SDK creates an overlay with a questionnaire on top of Unity’s view

In my presentation at Droidcon London last year I elaborated on the different ways of creating overlays in Android apps and how we eventually decided at Pollfish to create them by calling addContentView function.

addContentView(mPollfishOverlay);

This call has the following result in the view hierarchy of the Unity app:

This is how the majority of ad networks implement this overlay creation. If you want to learn more on how we came to this approach for creating the overlay, use the link below to view the presentation:

Android Unity plugins and Ad Networks — The issue with Unity 5.6

When Unity 5.6 was released, most of the ad network plugins stopped working properly. Developers started reporting that they were not able to see ads in their apps anymore. They could see that ads were loaded but the ads themselves were not visible.

You can see some of the reports and discussions in the following links: link1, link2, link3, link4, link5.

Initially, the official response from the Unity team was that adding views to an Activity was never guaranteed to work and developers should create a new Window-like a Dialog-if they would like to render something on top of Unity’s view.

Since that didn’t make much sense, we investigated what was different in Unity 5.6 and found this line:

public UnityPlayer(ContextWrapper contextWrapper) {

...

this.mSurfaceView.setZOrderOnTop(true);

}

With setZOrderOnTop, UnityPlayer is requesting that the created SurfaceView is placed on top of the Window. This means that none of the contents of the window that this SurfaceView is in, will be visible on top of its surface.

In the implementation of SurfaceView we can see that when we call setZOrderOnTop(true) the window type of SurfaceView is set to TYPE_APPLICATION_PANEL. (If you want to read about the different available types of windows on android you can read more here)

public void setZOrderOnTop(boolean onTop) {

if (onTop) {

mWindowType = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; ...

}

}

As we can see from the WindowManager’s class implementation, TYPE_APPLICATION_PANEL is a window type that appears on top of its attached window.

From the Hierarchy Viewer, we could see that the Pollfish overlay was rendered as expected in the view hierarchy of the activity:

However, our overlay was not visible. We then checked the available Windows with Device Monitor. Bear in mind that you can do the same by using the dumpsys tool to get a list of the windows or just the current visible windows.

> adb shell dumpsys window visible-apps

As we can see below, we have 2 windows for our running app where the SurfaceView one is placed on top of our Activity’s window.

This SurfaceView was preventing anything else in the Activity’s window from being visible, thus disrupting the functionality of our overlay plugin.

The solution

Since this affected a lot of our publishers that upgraded to Unity 5.6 we needed to find a solution.

One “hacky” approach was to traverse the view tree in our activity, find UnityPlayer’s SurfaceView, remove it, setZOrderOnTop to false and add it back (since setting the Z order of the window needs to be done before the SurfaceView’s containing window is attached to the window manager, as described in the documentation). This approach has a lot of side effects, including a black glitch when removing and adding back the surface view and therefore we had to go another way.

The first and only ad network that actually provided a good workaround to to this issue with its updated Unity Plugin was AdMob. In AdMob’s approach, a new PopupWindow was created and ads are then rendered in that PopupWindow (this is following the suggested guidelines by Unity’s team for creating a Dialog to achieve something similar).

At Pollfish we followed a similar approach. We created a a relative layout and set it as the content view of a transparent PopupWindow, which we then showed full screen on top of the current activity. We placed our overlay in that relative layout.

RelativeLayout relativeLayout = = new RelativeLayout(UnityPlayer.currentActivity); //apply current window's flags to the new overlay

int visibilityFlags = UnityPlayer.currentActivity.getWindow().getAttributes().flags;

relativeLayout.setSystemUiVisibility(visibilityFlags);



PopupWindow mPopupWindow = new PopupWindow(relativeLayout, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); mPopupWindow.setTouchable(true);

mPopupWindow.setContentView(relativeLayout); mPopupWindow.showAtLocation(UnityPlayer.currentActivity.getWindow().getDecorView().getRootView(),

Gravity.TOP | Gravity.LEFT, 0, 0);



setPopUpWindowLayoutType(mPopupWindow,

WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);

Since the PopupWindow window layout type is by default set to TYPE_APPLICATION_PANEL, as we can see from its implementation, we set it to TYPE_APPLICATION_SUB_PANEL to avoid any conflicts on devices, since this type of windows are displayed on top their attached window and above any TYPE_APPLICATION_PANEL windows (which was the window type of Unity’s Surface View).

Our new PopupWindow is placed on top of other windows of the app and therefore we can render the Pollfish overlay.

The new PopupWindow that holds Pollfish overlay has its own view hierarchy, as shown below.

PopupWindow with Pollfish overlay

UnityPlayerActivity’s window view hierarchy remains untouched!

UnityPlayerActivity’s view hierarchy

Pollfish overlay is visible

Using a PopupWindow, or a Dialog in general, to show an overlay creates an overhead, especially in terms of handling touch events or how soft input is handled. Since we have created our own overlay that renders above Unity’s activity, we then need to pass touch events back to our activity when the Pollfish panel is closed.

View.OnTouchListener onTouchListener = new View.OnTouchListener() {

@Override

public boolean onTouch(View v, MotionEvent event) {

UnityPlayer.currentActivity.dispatchTouchEvent(event);

relativeLayout.dispatchTouchEvent(event);

return true;

}

};

mPopupWindow.setTouchInterceptor(onTouchListener);

mPopupWindow.setFocusable(false);

mPopupWindow.update();

Moreover, since Pollfish questionnaires may include open-ended questions that require the use of the keyboard, while the Pollfish survey panel is open, we need to request our PopupWindow to be focusable. Pay attention to the update call that actually allows this to happen.

mPopupWindow.setTouchInterceptor(null);

mPopupWindow.setFocusable(true);

mPopupWindow.update();

The approach we developed actually solved the issue introduced by Unity 5.6 update, and kept our partners happy.

In closing, creating overlay SDKs can be achieved with several different ways. Using addContentView is the best approach since this is the way provided by the framework. Playing around with different window and window types can also create the desired effect as we have seen above, but may have some limitations.

Unity’s patch

Unity, following complains by its own users, released the 5.6.1p4 patch a few days ago, where the Unity team had removed the setZOrderOnTop in UnityPlayer class that caused all the trouble, resulting in SurfaceView not being placed on top of the activity’s window anymore, allowing any views or layouts added to the activity to be visible as before.

You can see below that the new patch places the window of SurfaceView under the activity’s one and Ad networks can continue to use their initial approach for rendering overlays.

Since this article was written a week before the patch was live, I decided to publish it as it contains information that might be helpful to other engineers interested in understanding more about how WindowManager works on Android and how Unity apps are structured when built for Android.

P.S. If you liked this post and would like to see more, please hit on the little heart ❤