So I have being trying to make this plugin work:
https://github.com/Unity-Technologies/GooglePlayLicenseVerification
Really I’m surprised of how much of challenge it’s being since this is suppose to be just a basic functionality that millions of android apps/games must have.
I tried tones of things to make it work wasn’t sure what was going on why it wasn’t working so I decided maybe I should try to remake the plugin with android studio. So I can rebuild it.
I got to the point where I can make a test plugin work that just prints something in the console but it only works when I make a unity development build. When I make a release build I get in logcat error java.lang.ClassNotFoundException (below i put the full error message)
In the development mode the licensing plugin doesn’t give any logcat errors but it doesn’t work either. I figured that probably the reason it doesn’t work is linked to the reason the release mode is giving me an error.
Here is some important info of how I’m doing things.
I’m making the plugin by building a .aar file in android studio. That I copy in to the:
AssetsPluginsAndroidlibs folder.
it’s name is : unity-release.aar
when I build I make Build App Bundle (google play) since that’s my end goal. but if i build an apk i have the same issue.
I tried to match the unity android player settings like minimum api with the build gradle.
So here is my java file for the licensing plugin:
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
public class ServiceBinder extends android.os.Binder implements ServiceConnection
{
private final Context mContext;
private static String PrintTag = "Licensing";
public ServiceBinder(Context context)
{
mContext = context;
}
private Runnable mDone = null;
private int mNonce;
public void create(int nonce, Runnable done)
{
if (mDone != null)
{
Log.i(PrintTag,"mDone != null");
destroy();
_arg0 = -1;
mDone.run();
}
mNonce = nonce;
mDone = done;
Intent serviceIntent = new Intent(SERVICE);
serviceIntent.setPackage("com.android.vending");
if (mContext.bindService(serviceIntent, this, Context.BIND_AUTO_CREATE)){
Log.i(PrintTag,"mContext.bindService(..)true");
return;
}
Log.i(PrintTag,"mContext.bindService(..)false");
mDone.run();
}
private void destroy()
{
mContext.unbindService(this);
}
private static final String SERVICE = "com.android.vending.licensing.ILicensingService";
public void onServiceConnected(ComponentName name, IBinder service) {
Log.i(PrintTag,"onServiceConnected called 0");
android.os.Parcel _data = android.os.Parcel.obtain();
_data.writeInterfaceToken(SERVICE);
_data.writeLong(mNonce);
_data.writeString(mContext.getPackageName());
_data.writeStrongBinder(this);
Log.i(PrintTag,"onServiceConnected called 1");
try {
Log.i(PrintTag,"service.transact called");
service.transact(1/*Stub.TRANSACTION_checkLicense*/, _data, null, IBinder.FLAG_ONEWAY);
}
catch (Exception ex)
{
ex.printStackTrace();
Log.i(PrintTag,"Exception called ex.printStackTrace();");
}
finally {
_data.recycle();
Log.i(PrintTag,"finally _data.recycle();");
}
}
private static final String LISTENER = "com.android.vending.licensing.ILicenseResultListener";
public boolean onTransact(int code, android.os.Parcel data,
android.os.Parcel reply, int flags)
throws android.os.RemoteException {
Log.i(PrintTag,"onTransact called");
switch (code) {
case INTERFACE_TRANSACTION: {
Log.i(PrintTag,"switch INTERFACE_TRANSACTION ");
reply.writeString(LISTENER);
return true;
}
case 1/*TRANSACTION_verifyLicense*/: {
Log.i(PrintTag,"switch 1 ");
data.enforceInterface(LISTENER);
_arg0 = data.readInt();
_arg1 = data.readString();
_arg2 = data.readString();
mDone.run();
destroy();
return true;
}
}
Log.i(PrintTag,"return super.onTransact(code, data, reply, flags)");
return super.onTransact(code, data, reply, flags);
}
public void onServiceDisconnected(ComponentName name) {
}
int _arg0;
String _arg1;
String _arg2;
}
Here is the AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.PlayStore.plugin.unity">
<application
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="com.android.vending.CHECK_LICENSE"/>
</manifest>
here is my project build.gadle file:
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.4"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
here is my module gradle.build file:
plugins {
id 'com.android.application'
}
android {
compileSdk 32
defaultConfig {
applicationId "com.unity3d.plugin.lvl"
minSdk 21
targetSdk 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
here is the unity file that uses the plugin:
another class calls the Init() method then the VerifyLicense() methode.
using System;
using UnityEngine;
using System.Collections.Generic;
using System.Security.Cryptography;
using UnityEngine.Networking;
using UnityEngine.UI;
using Random = System.Random;
public class CheckLVLButton : MonoBehaviour
{
public Text printText;
/*
* Use the public LVL key from the Android Market publishing section here.
*/
[SerializeField] [Tooltip("Insert LVL public key here")]
private string m_PublicKey_Base64 = string.Empty;
/*
* Consider storing the public key as RSAParameters.Modulus/.Exponent rather than Base64 to prevent the ASN1 parsing..
* These are printed to the logcat below.
*/
[SerializeField] [Tooltip("Filled automatically when you input a valid LVL public key above")]
private string m_PublicKey_Modulus_Base64 = string.Empty;
[SerializeField] [Tooltip("Filled automatically when you input a valid LVL public key above")]
private string m_PublicKey_Exponent_Base64 = string.Empty;
const string pluginName = "com.PlayStore.plugin.unity.ServiceBinder";
[SerializeField]
private Text resultsTextArea = default;
private RSAParameters m_PublicKey;
private Random _random;
private AndroidJavaObject m_Activity;
private AndroidJavaObject m_LVLCheck;
bool licenceConfirmed = false;
public void Init()
{
Debug.Log("hello are my in android?");
printText.text += "n-started app";
Debug.unityLogger.Log(BrandDisplay.LOG_TAG_LICENSING, "Init licensing---");
if (string.IsNullOrEmpty(m_PublicKey_Modulus_Base64) || string.IsNullOrEmpty(m_PublicKey_Exponent_Base64))
{
DisplayError("Please input a valid LVL public key in the inspector to generate its modulus and exponent");
return;
}
bool isRunningInAndroid = new AndroidJavaClass("android.os.Build").GetRawClass() != IntPtr.Zero;
if (isRunningInAndroid == false)
{
DisplayError("Please run this on an Android device!");
return;
}
_random = new Random();
m_PublicKey.Modulus = Convert.FromBase64String(m_PublicKey_Modulus_Base64);
m_PublicKey.Exponent = Convert.FromBase64String(m_PublicKey_Exponent_Base64);
m_Activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic<AndroidJavaObject>("currentActivity");
m_PackageName = m_Activity.Call<string>("getPackageName");
printText.text += "n-end started app";
Debug.unityLogger.Log(BrandDisplay.LOG_TAG_LICENSING, "end Init licensing---");
}
private void OnValidate()
{
if (string.IsNullOrEmpty(m_PublicKey_Base64) == false)
{
try
{
RSA.SimpleParseASN1(m_PublicKey_Base64, ref m_PublicKey.Modulus, ref m_PublicKey.Exponent);
}
catch (Exception e)
{
Debug.LogError($"Please input a valid LVL public key in the inspector to generate its modulus and exponentn{e.Message}");
return;
}
// The reason we keep the modulus and exponent is to avoid a costly call to SimpleParseASN1 at runtime
m_PublicKey_Modulus_Base64 = Convert.ToBase64String(m_PublicKey.Modulus);
m_PublicKey_Exponent_Base64 = Convert.ToBase64String(m_PublicKey.Exponent);
m_PublicKey_Base64 = string.Empty;
}
}
public bool VerifyLicense()
{
m_Nonce = _random.Next();
string results = "<b>Requesting LVL response...</b>n" +
$"Package name: {m_PackageName}n" +
$"Request nonce: 0x{m_Nonce:X}";
DisplayResults(results);
printText.text += "n-verifyLicense";
Debug.unityLogger.Log(BrandDisplay.LOG_TAG_LICENSING, "verifyLicense---");
m_LVLCheck = new AndroidJavaObject(pluginName, m_Activity);
m_LVLCheck.Call("create", m_Nonce, new AndroidJavaRunnable(Process));
Debug.unityLogger.Log(BrandDisplay.LOG_TAG_LICENSING, "EndverifyLicense---");
printText.text += "n-EndverifyLicense";
return licenceConfirmed;
}
private string m_PackageName;
private int m_Nonce;
private string m_ResponseCode_Received;
private string m_PackageName_Received;
private int m_Nonce_Received;
private int m_VersionCode_Received;
private string m_UserID_Received;
private string m_Timestamp_Received;
private int m_MaxRetry_Received;
private string m_LicenceValidityTimestamp_Received;
private string m_GracePeriodTimestamp_Received;
private string m_UpdateTimestamp_Received;
private string m_FileURL1_Received = string.Empty;
private string m_FileURL2_Received = string.Empty;
private string m_FileName1_Received;
private string m_FileName2_Received;
private int m_FileSize1_Received;
private int m_FileSize2_Received;
private string m_LicensingURL_Received = string.Empty;
private static Dictionary<string, string> DecodeExtras(string query)
{
Dictionary<string, string> result = new Dictionary<string, string>();
if (query.Length == 0)
return result;
string decoded = query;
int decodedLength = decoded.Length;
int namePos = 0;
bool first = true;
while (namePos <= decodedLength)
{
int valuePos = -1, valueEnd = -1;
for (int q = namePos; q < decodedLength; q++)
{
if (valuePos == -1 && decoded[q] == '=')
{
valuePos = q + 1;
}
else if (decoded[q] == '&')
{
valueEnd = q;
break;
}
}
if (first)
{
first = false;
if (decoded[namePos] == '?')
namePos++;
}
string name;
if (valuePos == -1)
{
name = string.Empty;
valuePos = namePos;
}
else
{
name = UnityWebRequest.UnEscapeURL(decoded.Substring(namePos, valuePos - namePos - 1));
}
if (valueEnd < 0)
{
namePos = -1;
valueEnd = decoded.Length;
}
else
{
namePos = valueEnd + 1;
}
string value = UnityWebRequest.UnEscapeURL(decoded.Substring(valuePos, valueEnd - valuePos));
result.Add(name, value);
if (namePos == -1)
break;
}
return result;
}
private Int64 ConvertEpochSecondsToTicks(Int64 secs)
{
DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
Int64 seconds_to_100ns_ticks = 10 * 1000;
Int64 max_seconds_allowed = (DateTime.MaxValue.Ticks - epoch.Ticks)
/ seconds_to_100ns_ticks;
if (secs < 0)
secs = 0;
if (secs > max_seconds_allowed)
secs = max_seconds_allowed;
return epoch.Ticks + secs * seconds_to_100ns_ticks;
}
private void Process()
{
string results = "<b>Requested LVL response</b>n" +
$"Package name: {m_PackageName}n" +
$"Request nonce: 0x{m_Nonce:X}n" +
"------------------------------------------n" +
"<b>Received LVL response</b>n";
printText.text += "n-process called";
Debug.Log("process called");
Debug.unityLogger.Log(BrandDisplay.LOG_TAG_LICENSING, "process called-----");
if (m_LVLCheck == null)
{
results += "m_LVLCheck is null!";
DisplayResults(results);
return;
}
int responseCode = m_LVLCheck.Get<int>("_arg0");
string message = m_LVLCheck.Get<string>("_arg1");
string signature = m_LVLCheck.Get<string>("_arg2");
m_LVLCheck.Dispose();
m_LVLCheck = null;
m_ResponseCode_Received = responseCode.ToString();
if (responseCode < 0 || string.IsNullOrEmpty(message) || string.IsNullOrEmpty(signature))
{
results += "Package name: <Failed>";
licenceConfirmed = false;
DisplayResults(results);
return;
}
byte[] message_bytes = System.Text.Encoding.UTF8.GetBytes(message);
byte[] signature_bytes = Convert.FromBase64String(signature);
RSACryptoServiceProvider csp = new RSACryptoServiceProvider();
csp.ImportParameters(m_PublicKey);
SHA1Managed sha1 = new SHA1Managed();
bool match = csp.VerifyHash(sha1.ComputeHash(message_bytes), CryptoConfig.MapNameToOID("SHA1"), signature_bytes);
if (!match)
{
results += "Response code: <Failed>" +
"Package name: <Invalid Signature>";
DisplayResults(results);
licenceConfirmed = false;
return;
}
int index = message.IndexOf(':');
string mainData, extraData;
if (-1 == index)
{
mainData = message;
extraData = string.Empty;
}
else
{
mainData = message.Substring(0, index);
extraData = index >= message.Length ? string.Empty : message.Substring(index + 1);
}
string[] vars = mainData.Split('|'); // response | nonce | package | version | userid | timestamp
if (String.Compare(vars[0], responseCode.ToString(), StringComparison.Ordinal) != 0)
{
results += "Response code: <Failed>" +
"Package name: <Invalid Mismatch>";
DisplayResults(results);
licenceConfirmed = false;
return;
}
m_ResponseCode_Received = vars[0];
m_Nonce_Received = Convert.ToInt32(vars[1]);
m_PackageName_Received = vars[2];
m_VersionCode_Received = Convert.ToInt32(vars[3]);
m_UserID_Received = vars[4];
Int64 ticks = ConvertEpochSecondsToTicks(Convert.ToInt64(vars[5]));
m_Timestamp_Received = new DateTime(ticks).ToLocalTime().ToString();
if (!string.IsNullOrEmpty(extraData))
{
Dictionary<string, string> extrasDecoded = DecodeExtras(extraData);
if (extrasDecoded.ContainsKey("GR"))
{
m_MaxRetry_Received = Convert.ToInt32(extrasDecoded["GR"]);
}
else
{
m_MaxRetry_Received = 0;
}
if (extrasDecoded.ContainsKey("VT"))
{
ticks = ConvertEpochSecondsToTicks(Convert.ToInt64(extrasDecoded["VT"]));
m_LicenceValidityTimestamp_Received = new DateTime(ticks).ToLocalTime().ToString();
}
else
{
m_LicenceValidityTimestamp_Received = null;
}
if (extrasDecoded.ContainsKey("GT"))
{
ticks = ConvertEpochSecondsToTicks(Convert.ToInt64(extrasDecoded["GT"]));
m_GracePeriodTimestamp_Received = new DateTime(ticks).ToLocalTime().ToString();
}
else
{
m_GracePeriodTimestamp_Received = null;
}
if (extrasDecoded.ContainsKey("UT"))
{
ticks = ConvertEpochSecondsToTicks(Convert.ToInt64(extrasDecoded["UT"]));
m_UpdateTimestamp_Received = new DateTime(ticks).ToLocalTime().ToString();
}
else
{
m_UpdateTimestamp_Received = null;
}
if (extrasDecoded.ContainsKey("FILE_URL1"))
{
m_FileURL1_Received = extrasDecoded["FILE_URL1"];
}
else
{
m_FileURL1_Received = "";
}
if (extrasDecoded.ContainsKey("FILE_URL2"))
{
m_FileURL2_Received = extrasDecoded["FILE_URL2"];
}
else
{
m_FileURL2_Received = "";
}
if (extrasDecoded.ContainsKey("FILE_NAME1"))
{
m_FileName1_Received = extrasDecoded["FILE_NAME1"];
}
else
{
m_FileName1_Received = null;
}
if (extrasDecoded.ContainsKey("FILE_NAME2"))
{
m_FileName2_Received = extrasDecoded["FILE_NAME2"];
}
else
{
m_FileName2_Received = null;
}
if (extrasDecoded.ContainsKey("FILE_SIZE1"))
{
m_FileSize1_Received = System.Convert.ToInt32(extrasDecoded["FILE_SIZE1"]);
}
else
{
m_FileSize1_Received = 0;
}
if (extrasDecoded.ContainsKey("FILE_SIZE2"))
{
m_FileSize2_Received = System.Convert.ToInt32(extrasDecoded["FILE_SIZE2"]);
}
else
{
m_FileSize2_Received = 0;
}
if (extrasDecoded.ContainsKey("LU"))
{
m_LicensingURL_Received = extrasDecoded["LU"];
}
else
{
m_LicensingURL_Received = "";
}
}
results += $"Response code: {m_ResponseCode_Received}n" +
$"Package name: {m_PackageName_Received}n" +
$"Received nonce: 0x{m_Nonce_Received:X}n" +
$"Version code: {m_VersionCode_Received}n" +
$"User ID: {m_UserID_Received}n" +
$"Timestamp: {m_Timestamp_Received}n" +
$"Max Retry: {m_MaxRetry_Received}n" +
$"License Validity: {m_LicenceValidityTimestamp_Received}n" +
$"Grace Period: {m_GracePeriodTimestamp_Received}n" +
$"Update Since: {m_UpdateTimestamp_Received}n" +
$"Main OBB URL: {m_FileURL1_Received.Substring(0, Mathf.Min(m_FileURL1_Received.Length,50)) + "..."}n" +
$"Main OBB Name: {m_FileName1_Received}n" +
$"Main OBB Size: {m_FileSize1_Received}n" +
$"Patch OBB URL: {m_FileURL2_Received.Substring(0, Mathf.Min(m_FileURL2_Received.Length,50)) + "..."}n" +
$"Patch OBB Name: {m_FileName2_Received}n" +
$"Patch OBB Size: {m_FileSize2_Received}n" +
$"Licensing URL: {m_LicensingURL_Received.Substring(0, Mathf.Min(m_LicensingURL_Received.Length,50)) + "..."}n";
DisplayResults(results);
licenceConfirmed = true;
printText.text += "n-process finished";
}
private void DisplayResults(string text)
{
Debug.Log(text);
resultsTextArea.text = text;
}
private void DisplayError(string text)
{
resultsTextArea.text = text;
Debug.LogError(text);
}
}
Full logcat error I get when building in release. using the Android Device Monitor
03-14 16:40:09.246: E/Unity(16433): AndroidJavaException: java.lang.ClassNotFoundException: com.PlayStore.plugin.unity.ServiceBinder
03-14 16:40:09.246: E/Unity(16433): java.lang.ClassNotFoundException: com.PlayStore.plugin.unity.ServiceBinder
03-14 16:40:09.246: E/Unity(16433): at java.lang.Class.classForName(Native Method)
03-14 16:40:09.246: E/Unity(16433): at java.lang.Class.forName(Class.java:454)
03-14 16:40:09.246: E/Unity(16433): at com.unity3d.player.UnityPlayer.nativeRender(Native Method)
03-14 16:40:09.246: E/Unity(16433): at com.unity3d.player.UnityPlayer.access$300(Unknown Source:0)
03-14 16:40:09.246: E/Unity(16433): at com.unity3d.player.UnityPlayer$e$1.handleMessage(Unknown Source:83)
03-14 16:40:09.246: E/Unity(16433): at android.os.Handler.dispatchMessage(Handler.java:103)
03-14 16:40:09.246: E/Unity(16433): at android.os.Looper.loop(Looper.java:225)
03-14 16:40:09.246: E/Unity(16433): at com.unity3d.player.UnityPlayer$e.run(Unknown Source:20)
03-14 16:40:09.246: E/Unity(16433): Caused by: java.lang.ClassNotFoundException: com.PlayStore.plugin.unity.ServiceBinder
03-14 16:40:09.246: E/Unity(16433): ... 8 more
03-14 16:40:09.246: E/Unity(16433): at UnityEngine.AndroidJNISafe.CheckException () [0x00096] in <18c3cbae8278498a88f31fc7b855af9e>:0
03-14 16:40:09.246: E/Unity(16433): at UnityEngine.AndroidJNISafe.FindClass (System.String name) [0x0000c] in <18c3cbae8278498a88f31fc7b855af9e>:0
03-14 16:40:09.246: E/Unity(16433): at UnityEngine.AndroidJavaObject._AndroidJavaObject (System.String className, System
2
Answers
So completely thx to Jaimin that helped me in the comment I found the fix to my issue. All I had to do was go to the unity project setting,player,publishing settings and untick minify on release. The whole plugin worked directly after the fix.
I'm not sure if this is the most clean solution. It's maybe better to change the proguard-rules.pro file so it minifies in a way as to not break the code. For the info, minifying changes the code identifiers names so they are shorter and take less space. For my project the unity player settings seem to override settings concerning minifying that where in the gradle build file:
I’m a developer of a paid ‘Google Play License Check‘ plugin. I struggled for years using the GooglePlayLicenseVerification repo, so I decided to create my own clean and simple API. Using my plugin, you can check the Google Play license by simply calling this:
After performing the license check, the plugin will cache the response and store it in the PlayerPrefs. To access the cached license:
To add extra protection to your app, you can provide your custom secure storage instead of using PlayerPrefs:
My plugin is available on the Asset Store. Please feel free to ask more questions here or in my plugin’s FAQ.