📜 ⬆️ ⬇️

We write the plugin for Unity correctly. Part 2: Android



In the previous part, we looked at the main problems of writing native plugins on Unity for iOS and Android, as well as methods for solving them for iOS. In this article, I will describe the basic recipes for solving these problems for Android, trying to maintain a similar interaction structure and the order of their consideration.

Libraries for Android in Unity can be represented as Jar (compiled java code only), Aar (compiled java code along with resources and a manifest), and source codes. It is desirable to store in the source code only the code specific for this project with minimal functionality, and this is not necessarily and not very convenient. The best option is to have a separate gradle project (you can directly in the repository with the main Unity project), in which you can place not only the library code, but also unit tests, and a test Android project with an Activity for quickly assembling and checking the library functionality. And in the gradle script build of this project, you can immediately add a task that will copy the compiled Aar to Assets:
')
/* gradle.properties */ deployAarPath=../Assets/Plugins/Android /* build.gradle */ task clearLibraryAar(type: Delete) { delete fileTree("${deployAarPath}") { include 'my-plugin-**.aar' } } task deployLibraryAar(type: Copy, dependsOn: clearLibraryAar) { from('build/outputs/aar/') into("${deployAarPath}") include('my-plugin-release.aar') rename('my-plugin-release.aar', 'my-plugin-' + android.defaultConfig.versionName + '.aar') doLast { fileTree("${deployAarPath}"){ include { it.file.name ==~ "^my-plugin-([0-9.]+).aar.meta\$" }}.each { f -> f.renameTo(file("${deployAarPath}/my-plugin-" + android.defaultConfig.versionName + ".aar.meta")) } } } tasks.whenTaskAdded { task -> if (task.name == 'bundleRelease') { task.finalizedBy 'deployLibraryAar' } } 

Here my-plugin is the name of the library project; deployAarPath - the path by which the compiled file is copied, can be any.

It is also undesirable to use Jar now, because Unity has long learned to support Aar, and it provides more features: in addition to the code, you can include resources and your AndroidManifest.xml, which will merge with the main one during the gradle build. The library files themselves do not need to be added to Assets / Plugins / Android. The rule is the same as for iOS: if you are writing a third-party library, put everything in a subfolder inside your specific folder with code and native code for iOS - it will be easier to update or delete packages later. In other cases, you can store where you want, in the Unity import settings you can specify whether to include the file in the Android assembly or not.

Let's try to organize interaction between Java and Unity code without using GameObject in the same way as examples for iOS, by implementing our UnitySendMessage and the ability to transfer callbacks from C #. For this we need AndroidJavaProxy - C # classes used as implementations of Java interfaces. Class names leave the same as in the previous article. If desired, their code can be combined with the code from the first part for a multiplatform implementation.

 /* MessageHandler.cs */ using UnityEngine; public static class MessageHandler { //     Java Interface,    private class JavaMessageHandler : AndroidJavaProxy { private JavaMessageHandler() : base("com.myplugin.JavaMessageHandler") {} public void onMessage(string message, string data) { //      MessageRouter.RouteMessage(message, data); } } //        Unity Engine   [RuntimeInitializeOnLoadMethod] private static void Initialize() { #if !UNITY_EDITOR //   JavaMessageHandler    new AndroidJavaClass("com.myplugin.UnityBridge").CallStatic("registerMessageHandler", new JavaMessageHandler()); #endif } } 

On the Java side, we define an interface for receiving messages and a class that will register and then delegate calls to the above-described JavaMessageHandler. Along the way, we will solve the problem of redirecting threads. Since, unlike iOS, on Android, Unity creates its own stream having a loop circle, you can create android.os.Handler during initialization and transfer execution to it.

 /* com.myplugin.JavaMessageHandler */ package com.myplugin; //  ,    public interface JavaMessageHandler { void onMessage(String message, String data); } /* com.myplugin.UnityBridge */ package com.myplugin; import android.os.Handler; public final class UnityBridge { //    C#   private static JavaMessageHandler javaMessageHandler; //    Unity  private static Handler unityMainThreadHandler; public static void registerMessageHandler(JavaMessageHandler handler) { javaMessageHandler = handler; if(unityMainThreadHandler == null) { //         Unity, //         , //    Handler unityMainThreadHandler = new Handler(); } } //     Unity ,    public static void runOnUnityThread(Runnable runnable) { if(unityMainThreadHandler != null && runnable != null) { unityMainThreadHandler.post(runnable); } } //  - ,      Unity public static void SendMessageToUnity(final String message, final String data) { runOnUnityThread(new Runnable() { @Override public void run() { if(javaMessageHandler != null) { javaMessageHandler.onMessage(message, data); } } }); } } 

Now let's add the ability to call Java functions with callbacks using the same AndroidJavaProxy.

 /* MonoJavaCallback.cs */ using System; using UnityEngine; public static class MonoJavaCallback { //  ,    Java //      Action private class AndroidCallbackHandler<T> : AndroidJavaProxy { private readonly Action<T> _resultHandler; public AndroidCallbackHandler(Action<T> resultHandler) : base("com.myplugin.CallbackJsonHandler") { _resultHandler = resultHandler; } //     JSONObject //       , //        public void onHandleResult(AndroidJavaObject result) { if(_resultHandler != null) { //  json    var resultJson = result == null ? null : result.Call<string>("toString"); //      C#  _resultHandler.Invoke(Newtonsoft.Json.JsonConvert.DeserializeObject<T>(resultJson)); } } } //         C#  public static AndroidJavaProxy ActionToJavaObject<T>(Action<T> action) { return new AndroidCallbackHandler<T>(action); } } 

On the Java side, we declare a callback interface, which we will later use in all exported functions with a callback:

 /* CallbackJsonHandler.java */ package com.myplugin; import org.json.JSONObject; public interface CallbackJsonHandler { void onHandleResult(JSONObject result); } 

I used Json as the callback argument, as well as in the first part, because it eliminates the need to describe the interfaces and AndroidJavaProxy for each set of different types of arguments needed in the project. Perhaps your project is more suitable string or array. I give an example of use with the description of a test serializable class as a type for a callback.

 /* Example.cs */ public class Example { public class ResultData { public bool Success; public string ValueStr; public int ValueInt; } public static void GetSomeData(string key, Action<ResultData> completionHandler) { new AndroidJavaClass("com.myplugin.Example").CallStatic("getSomeDataWithCallback", key, MonoJavaCallback.ActionToJavaObject<ResultData>(completionHandler)); } } /* Example.java */ package com.myplugin; import org.json.JSONException; import org.json.JSONObject; public class Example { public static void getSomeDataWithCallback(String key, CallbackJsonHandler callback) { //     -   background  new Thread(new Runnable() { @Override public void run() { doSomeStuffWithKey(key); //     Unity  UnityBridge.runOnUnityThread(new Runnable() { @Override public void run() { try { callback.OnHandleResult(new JSONObject().put("Success", true).put("ValueStr", someResult).put( "ValueInt", 42)); } catch (JSONException e) { e.printStackTrace(); } } }); }); } } 

A typical problem when writing plugins for Android for Unity is to catch the life cycles of the gaming Activity, as well as onActivityResult and launching Application. Typically, this is suggested to inherit from UnityPlayerActivity and override the class at the launch activity in the manifest. You can do the same for Application. But in this article we are writing a plugin. There may be several such plug-ins in large projects, inheritance will not help. It is necessary to integrate as transparently as possible without the need for modifications to the main classes of the game. ActivityLifecycleCallbacks and ContentProvider will come to the rescue.

 public class InitProvider extends ContentProvider { @Override public boolean onCreate() { Context context = getContext(); if (context != null && context instanceof Application) { // ActivityLifecycleListener —    Application.ActivityLifecycleCallbacks ((Application) context).registerActivityLifecycleCallbacks(new ActivityLifecycleListener(context)); } return false; } //     } 

Do not forget to register the InitProvider in the manifest (Aar libraries, not mostly):

 <provider android:name=".InitProvider" android:authorities="${applicationId}.InitProvider" android:enabled="true" android:exported="false" android:initOrder="200" /> 

It uses the fact that Application at the start creates all the declared Content Provider. And even if it does not provide any data that a normal Content Provider should return, in the onCreate method you can do something that is usually done at the start of the Application, for example, register our ActivityLifecycleCallbacks. And it will already receive onActivityCreated, onActivityStarted, onActivityResumed, onActivityPaused, onActivityStopped, onActivitySaveInstanceState, and onActivityDestroyed events. True, events will come from all activations, but to determine the main one and react only to it does not cost anything:

 private boolean isLaunchActivity(Activity activity) { Intent launchIntent = activity.getPackageManager().getLaunchIntentForPackage(activity.getPackageName()); return launchIntent != null && launchIntent.getComponent() != null && activity.getClass().getName().equals(launchIntent.getComponent().getClassName()); } 

Also in the manifest was specified the variable $ {applicationId}, which when building gradle will be replaced with the packageName of the application.

The only thing missing is onActivityResult, which is usually required to return the result from showing the native screen on top of the game. Unfortunately, this call cannot be received directly. But you can create a new Activity, which will show the desired Activity, then get the result from it, return it to us and finish. The main thing is to exclude it from the history and make it transparent, specifying the subject in the manifesto, so that when you open it, the white screen does not flash:

 <activity android:name=".ProxyActivity" android:excludeFromRecents="true" android:exported="false" android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen" android:theme="@android:style/Theme.Translucent.NoTitleBar" /> 

Thus, you can implement the necessary functionality without resorting to modifying the basic Unity Java classes, and carefully pack the manifest with code and resources into the Aar library. But what to do with dependency packages from the maven repositories that are required by our plugin? Unity generates a gradle project in which all the java project libraries are added to the libs of the exported project and are connected locally. There should be no duplicates. Other dependencies will not be included automatically. Putting dependencies next to compiled Aar is not always a good idea: most often other Unity plugins also need these dependencies. And if they put their version in unitypackage too, there will be a conflict of versions, gradle when building will swear at the duplicate of classes. Also, dependencies depend on other packages, and manually composing this dependency chain by extorting everything you need from the maven repository is not a simple task.

To look for duplicates in the project is also tiring. I want an automated solution that itself downloads the necessary libraries of the required versions into the project, removing duplicates. And there is such a solution . This package can be downloaded independently, and also it comes with Google Play Services and Firebase. The idea is that in the Unity project we create xml files with a list of dependencies required by plugins with syntax similar to the definition in build.gradle (with minimum versions):

 <dependencies> <iosPods> </iosPods> <androidPackages> <androidPackage spec="com.android.support:appcompat-v7:23+"> <androidSdkPackageIds> <androidSdkPackageId>extra-google-m2repository</androidSdkPackageId> <androidSdkPackageId>extra-android-m2repository</androidSdkPackageId> </androidSdkPackageIds> </androidPackage> <androidPackage spec="com.android.support:cardview-v7:23+"> <androidSdkPackageIds> <androidSdkPackageId>extra-google-m2repository</androidSdkPackageId> <androidSdkPackageId>extra-android-m2repository</androidSdkPackageId> </androidSdkPackageIds> </androidPackage> <androidPackage spec="com.android.support:design:23+"> <androidSdkPackageIds> <androidSdkPackageId>extra-google-m2repository</androidSdkPackageId> <androidSdkPackageId>extra-android-m2repository</androidSdkPackageId> </androidSdkPackageIds> </androidPackage> <androidPackage spec="com.android.support:recyclerview-v7:23+"> <androidSdkPackageIds> <androidSdkPackageId>extra-google-m2repository</androidSdkPackageId> <androidSdkPackageId>extra-android-m2repository</androidSdkPackageId> </androidSdkPackageIds> </androidPackage> </androidPackages> </dependencies> 

Next, after installing or changing dependencies in the project, select in the Unity menu of the editor Assets → Play Services Resolver → Android Resolver → Resolve and voila! The utility will scan the xml ads, create a dependency graph and download all the necessary dependency packages of the required versions from the maven repositories in Assets / Plugins / Android. And she notes in a special file the downloaded one and next time replaces it with new versions, and she will not touch those files that we put. There is also a settings window where you can turn on automatic resolution of dependencies, in order not to click Resolve via the menu, and many other options. It requires Android Sdk installed on the computer along with Unity and the target target selected is Android. In the same file, you can write CocoaPods dependencies for iOS builds, and in the settings set Unity to generate xcworkspace with the included dependencies for the main Xcode project.

Unity relatively recently began to fully support the gradle collector for Android, and ADT announced it as legacy. Now it is possible to create a template for the gradle configuration of the exported project, full support for Aar and variables in manifests, merging manifests. But third-party sdk plugins have not yet managed to adapt to these changes and do not use the features that the editor provides. Therefore, my advice, it is better to modify the imported library under modern realities: remove the dependencies and declare them via xml for the Unity Jar Resolver, compile all the java code and resources in Aar. Otherwise, each subsequent integration will break the previous ones and take more and more time.

Source: https://habr.com/ru/post/353444/


All Articles