我正在重制的 Android 许可插件有 logcat 错误:java.lang.ClassNotFoundException,仅在发布版本而非开发版本上

发布于 2025-01-14 13:41:01 字数 23584 浏览 1 评论 0原文

所以我一直在努力让这个插件工作: https://github.com/Unity-Technologies/GooglePlayLicenseVerification

真的,我很惊讶这是一个很大的挑战,因为这被认为只是数百万 Android 应用程序/游戏必须具备的基本功能。

我尝试了一些方法来让它工作,但不确定到底发生了什么,为什么它不起作用,所以我决定也许我应该尝试用 android studio 重新制作这个插件。所以我可以重建它。

我已经到了可以让一个测试插件工作的地步,它只在控制台中打印一些东西,但它只有在我进行统一开发构建时才有效。当我进行发布构建时,出现 logcat 错误 java.lang.ClassNotFoundException (下面我放置了完整的错误消息) 在开发模式下,许可插件不会给出任何 logcat 错误,但它也不起作用。我认为它不起作用的原因可能与发布模式给我一个错误的原因有关。

以下是有关我如何做事的一些重要信息。 我通过在 android studio 中构建 .aar 文件来制作插件。我将其复制到: \Assets\Plugins\Android\libs 文件夹。 它的名字是:unity-release.aar 当我构建时,我制作构建应用程序包(google play),因为这是我的最终目标。但如果我构建一个 apk,我也会遇到同样的问题。 我尝试将 Unity Android 播放器设置(例如最小 api)与构建 gradle 相匹配。

这是我的许可插件的 java 文件:


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;
}

这是 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> 

这是我的项目 build.gadle 文件:

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
}

这是我的模块 gradle.build 文件:


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'
}

这是使用该插件的 unity 文件: 另一个类调用 Init() 方法,然后调用 verifyLicense() 方法。

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 exponent\n{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);
    }
}

在发布版本中构建时出现完整的 logcat 错误。使用 Android 设备监视器

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 

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:
\Assets\Plugins\Android\libs 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(
quot;Please input a valid LVL public key in the inspector to generate its modulus and exponent\n{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" +
                         
quot;Package name: {m_PackageName}\n" +
                         
quot;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" +
                         
quot;Package name: {m_PackageName}\n" +
                         
quot;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 += 
quot;Response code: {m_ResponseCode_Received}\n" +
                   
quot;Package name: {m_PackageName_Received}\n" +
                   
quot;Received nonce: 0x{m_Nonce_Received:X}\n" +
                   
quot;Version code: {m_VersionCode_Received}\n" +
                   
quot;User ID: {m_UserID_Received}\n" +
                   
quot;Timestamp: {m_Timestamp_Received}\n" +
                   
quot;Max Retry: {m_MaxRetry_Received}\n" +
                   
quot;License Validity: {m_LicenceValidityTimestamp_Received}\n" +
                   
quot;Grace Period: {m_GracePeriodTimestamp_Received}\n" +
                   
quot;Update Since: {m_UpdateTimestamp_Received}\n" +
                   
quot;Main OBB URL: {m_FileURL1_Received.Substring(0, Mathf.Min(m_FileURL1_Received.Length,50)) + "..."}\n" +
                   
quot;Main OBB Name: {m_FileName1_Received}\n" +
                   
quot;Main OBB Size: {m_FileSize1_Received}\n" +
                   
quot;Patch OBB URL: {m_FileURL2_Received.Substring(0, Mathf.Min(m_FileURL2_Received.Length,50)) + "..."}\n" +
                   
quot;Patch OBB Name: {m_FileName2_Received}\n" +
                   
quot;Patch OBB Size: {m_FileSize2_Received}\n" +
                   
quot;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 

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

肤浅与狂妄 2025-01-21 13:41:02

非常感谢 Jaimin 在评论中帮助我,我找到了问题的解决方案。
我所要做的就是进入统一项目设置、播放器、发布设置并取消勾选发布时缩小。整个插件在修复后直接工作。

我不确定这是否是最干净的解决方案。最好更改 proguard-rules.pro 文件,以便以不破坏代码的方式进行缩小。就信息而言,缩小会更改代码标识符名称,使它们更短并占用更少的空间。对于我的项目,统一播放器设置似乎覆盖了有关缩小 gradle 构建文件中的位置的设置:

 buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

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:

 buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
九厘米的零° 2025-01-21 13:41:02

我是付费“Google Play License Check”插件的开发者。我多年来一直在使用 GooglePlayLicenseVerification 存储库,因此我决定创建自己的干净且简单的 API。使用我的插件,您只需调用以下命令即可检查 Google Play 许可证:

GooglePlayLicense.Check(license => {
    if (license.status == LicenseStatus.NOT_LICENSED) {
        Debug.Log("YOU ARE A PIRATE, SHAME ON YOU!");
    }
});

执行许可证检查后,插件将缓存响应并将其存储在 PlayerPrefs 中。要访问缓存的许可证:

var cachedLicense = GooglePlayLicense.cachedLicense;
if (cachedLicense != null) {
    Debug.Log($"License is valid: {cachedLicense.isLicenseValid}. Grace period: {cachedLicense.gracePeriodTimestamp}.");
}

要为您的应用程序添加额外的保护,您可以提供自定义安全存储,而不是使用 PlayerPrefs:

GooglePlayLicense.SetPersistentStorageImplementation(MyAwesomeSecureStorage.SaveString, MyAwesomeSecureStorage.LoadString);

我的插件可在资源商店中找到。请随时在此处或我的插件的常见问题解答<中提出更多问题/a>.

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:

GooglePlayLicense.Check(license => {
    if (license.status == LicenseStatus.NOT_LICENSED) {
        Debug.Log("YOU ARE A PIRATE, SHAME ON YOU!");
    }
});

After performing the license check, the plugin will cache the response and store it in the PlayerPrefs. To access the cached license:

var cachedLicense = GooglePlayLicense.cachedLicense;
if (cachedLicense != null) {
    Debug.Log(
quot;License is valid: {cachedLicense.isLicenseValid}. Grace period: {cachedLicense.gracePeriodTimestamp}.");
}

To add extra protection to your app, you can provide your custom secure storage instead of using PlayerPrefs:

GooglePlayLicense.SetPersistentStorageImplementation(MyAwesomeSecureStorage.SaveString, MyAwesomeSecureStorage.LoadString);

My plugin is available on the Asset Store. Please feel free to ask more questions here or in my plugin's FAQ.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文