godotengine / godot-google-play-billing

Godot Android plugin for the Google Play Billing library

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

emitSignal with null parameter throws error

ricardoalcantara opened this issue · comments

Godot version:

3.2.2.stable.mono.official

OS/device including version:

Android with GLES3

Issue description:

I am using this lib to implement IAP in my game, after a while testing I face this error:

C#

07-22 20:03:00.296 10356 10356 D AndroidRuntime: >>>>>> START com.android.internal.os.RuntimeInit uid 10580 <<<<<<
07-22 20:03:00.555 10356 10356 D AndroidRuntime: Calling main entry org.chromium.components.crash.browser.CrashpadMain
07-22 20:03:00.775 10356 10356 D AndroidRuntime: Shutting down VM
07-22 20:03:00.836  9906  9932 I godot   : BillingResult: System.NullReferenceException: Object reference not set to an instance of an object
07-22 20:03:00.836  9906  9932 I godot   :   at Tetris.Core.GooglePlayBilling.BillingResult..ctor (Godot.Collections.Dictionary billingResult) [0x00009] in <cb1faa21092740ca8679fe1d0a4c59e8>:0
07-22 20:03:00.934  9906  9932 E AndroidRuntime: FATAL EXCEPTION: GLThread 10402
07-22 20:03:00.934  9906  9932 E AndroidRuntime: Process: org.godotengine.tetris, PID: 9906
07-22 20:03:00.934  9906  9932 E AndroidRuntime: java.lang.IllegalArgumentException: Invalid type for argument #0. Should be of type java.lang.Integer
07-22 20:03:00.934  9906  9932 E AndroidRuntime:        at org.godotengine.godot.plugin.GodotPlugin.emitSignal(GodotPlugin.java:300)
07-22 20:03:00.934  9906  9932 E AndroidRuntime:        at org.godotengine.godot.plugin.googleplaybilling.GodotGooglePlayBilling.purchase(GodotGooglePlayBilling.java:178)
07-22 20:03:00.934  9906  9932 E AndroidRuntime:        at org.godotengine.godot.GodotLib.touch(Native Method)
07-22 20:03:00.934  9906  9932 E AndroidRuntime:        at org.godotengine.godot.Godot$8.run(Godot.java:1023)
07-22 20:03:00.934  9906  9932 E AndroidRuntime:        at android.opengl.GLSurfaceView$GLThread.guardedRun(GLSurfaceView.java:1500)
07-22 20:03:00.934  9906  9932 E AndroidRuntime:        at android.opengl.GLSurfaceView$GLThread.run(GLSurfaceView.java:1270)
07-22 20:03:01.506  9906  9906 I AndroidRuntime: VM exiting with result code 0, cleanup skipped.
07-22 20:03:01.577 10268 10268 I AndroidRuntime: VM exiting with result code 0, cleanup skipped.

First I thought that it could be some C# issue, so I tested with GDScript and the same error happens:

GDScript

At: res://GDScript/google_play_billing.gdc:96:purchase() - Invalid get index 'status' (on base: 'Nil').
07-22 19:51:41.195  8879  8901 E AndroidRuntime: FATAL EXCEPTION: GLThread 10386
07-22 19:51:41.195  8879  8901 E AndroidRuntime: Process: org.godotengine.tetris, PID: 8879
07-22 19:51:41.195  8879  8901 E AndroidRuntime: java.lang.IllegalArgumentException: Invalid type for argument #0. Should be of type java.lang.Integer
07-22 19:51:41.195  8879  8901 E AndroidRuntime:        at org.godotengine.godot.plugin.GodotPlugin.emitSignal(GodotPlugin.java:300)
07-22 19:51:41.195  8879  8901 E AndroidRuntime:        at org.godotengine.godot.plugin.googleplaybilling.GodotGooglePlayBilling.purchase(GodotGooglePlayBilling.java:178)
07-22 19:51:41.195  8879  8901 E AndroidRuntime:        at org.godotengine.godot.GodotLib.touch(Native Method)
07-22 19:51:41.195  8879  8901 E AndroidRuntime:        at org.godotengine.godot.Godot$8.run(Godot.java:1023)
07-22 19:51:41.195  8879  8901 E AndroidRuntime:        at android.opengl.GLSurfaceView$GLThread.guardedRun(GLSurfaceView.java:1500)
07-22 19:51:41.195  8879  8901 E AndroidRuntime:        at android.opengl.GLSurfaceView$GLThread.run(GLSurfaceView.java:1270)
07-22 19:51:41.756  8879  8879 I AndroidRuntime: VM exiting with result code 0, cleanup skipped.

It happens when I try to purchase a SKU that I haven't queried before and it emitSignal with null parameter.

emitSignal("purchase_error", null, "You must query the sku details and wait for the result before purchasing!");

it's registered to send Interger

This signal is also used when I try to purchase:

emitSignal("purchase_error", billingResult.getResponseCode(), billingResult.getDebugMessage());

the getResponseCode can be:

// The request has reached the maximum timeout before Google Play responds.
SERVICE_TIMEOUT = -3,
// Requested feature is not supported by Play Store on the current device.
FEATURE_NOT_SUPPORTED = -2,
// Play Store service is not connected now - potentially transient state.
SERVICE_DISCONNECTED = -1,
// Success
OK = 0,
// User pressed back or canceled a dialog
USER_CANCELED = 1,
// Network connection is down
SERVICE_UNAVAILABLE = 2,
// Billing API version is not supported for the type requested
BILLING_UNAVAILABLE = 3,
// Requested product is not available for purchase
ITEM_UNAVAILABLE = 4,
// Invalid arguments provided to the API.
DEVELOPER_ERROR = 5,
// Fatal error during the API action
ERROR = 6,
// Failure to purchase since item is already owned
ITEM_ALREADY_OWNED = 7,
// Failure to consume since item is not owned
ITEM_NOT_OWNED = 8,

By google documentation: https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode

I can fix it and create a PR if it's confirmed a bug.
But what could be a good solution?

1 - Create another signal for that purpose
2 - Use the signal but create a different errorCode from those in the google documentation (e.g -4 or 9)
3 - public Dictionary purchase(String sku) could return another status with debug_message

I prefer the 3 option:

Dictionary returnValue = new Dictionary();
    returnValue.put("status", 1); // FAILED = 1
    returnValue.put("debug_message", "You must query the sku details and wait for the result before purchasing!");

Thanks for pointing out! I'll fix that to allow null there. That particular error is a special one, as it doesn't originate from the Billing Library.

You need to call querySkuDetails for an SKU before purchasing it.

Thanks for pointing out! I'll fix that to allow null there. That particular error is a special one, as it doesn't originate from the Billing Library.

You need to call querySkuDetails for an SKU before purchasing it.

Thanks for fixing it! I understand how this signal is emitting and I also know that I need to querySkuDetails before, I mentioned it on my issue.

But I believe that sending null on an ErrorCode parameter is kind of strange and not very clear of what kind of error is that.

I also think that if you already have a method that return some result why use a signal that have two different purpose if null is a internal error 'else' Google's error, and if later you need to signal two different internal errors how would I if it (with string comparison)? That's why I suggested to use the result status = 1 because it makes more sense to me that status = Godot status and responseCode = Google status

That's why I made that whole explanation for, I just wanted to point that out too.

@ricardoalcantara I don't see a problem with null. Remember that this is an error that can never happen in production (unless you publish an app without testing IAP once).

Never mind, I just wanted to help.

@ricardoalcantara you definitely helped (you pointed out this issue in the first place) :)

I just think introducing another special return format for one single error that can only occur in development makes error handling too complex. But I'd definitely implement something like your idea in case we add more errors that come from the plugin rather than the Billing Library.

@ricardoalcantara I'm starting to dislike making the error code nullable 😅 What do you think of printing a message and throwing an exception? This will crash the app but I think that's fine because it shouldn't happen in production and makes the developers aware that something is definitely wrong...

I'm starting to dislike making the error code nullable

I think the Engine doesn't support it.

What do you think of printing a message and throwing an exception? This will crash the app

I once saw Juan saying that the Engine should never crash and that they don't work with exceptions, I also think that crash the app for wrong flow not friendly.

I still think that change from emitting the signal to return the Dictionary a good solution.

	public Dictionary purchase(String sku) {
		if (!skuDetailsCache.containsKey(sku)) {
			Dictionary returnValue = new Dictionary();
			returnValue.put("status", 1); // FAILED = 1
			returnValue.put("debug_message", "You must query the sku details and wait for the result before purchasing!");
			return returnValue;
		}

		SkuDetails skuDetails = skuDetailsCache.get(sku);
		BillingFlowParams purchaseParams = BillingFlowParams.newBuilder()
												   .setSkuDetails(skuDetails)
												   .build();

		BillingResult result = billingClient.launchBillingFlow(getActivity(), purchaseParams);

		Dictionary returnValue = new Dictionary();
		if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
			returnValue.put("status", 0); // OK = 0
		} else {
			returnValue.put("status", 1); // FAILED = 1
			returnValue.put("response_code", result.getResponseCode());
			returnValue.put("debug_message", result.getDebugMessage());
		}

		return returnValue;
	}

I think the Engine doesn't support it.

It doesn't support statically typed nullables. Variables can be of any type.

I once saw Juan saying that the Engine should never crash and that they don't work with exceptions, I also think that crash the app for wrong flow not friendly.

I agree with Juan that the Engine should never crash at runtime, but I think this is an exception, as this can possibly only happen when testing IAP. If this happens in production, the developer never tested IAP once, which, in my opinion, is not a case we should take care of. This particular error we are talking about ("You must query the sku details and wait for the result before purchasing!") is clearly a developer mistake. It's like running a GLES3 game on a GLES2-only device, it will also just crash.
I'm also trying to avoid changing the current API for such a small problem so that people can upgrade seamlessly.

The absolutely best way would be an API which makes it impossible to produce that error, something like querySkuDetails returns an SKU object which has a purchase method. But unfortunately, this is not possible with GDScript+Java at the moment (we cannot instantiate GDScript classes as far as I know).

@m4gr3d do you have any ideas?

@timoschwarzer

I'm also trying to avoid changing the current API for such a small problem so that people can upgrade seamlessly.

My suggestion won't break anything and won't change the API, actually people who are using the way it is may not be using this signal in that specific situation since it never worked!

@ricardoalcantara actually you're right. I don't know why but I thought purchase was a void all the time (which would have required an API change).
Would you like to make a PR for that?

@timoschwarzer Sure, I will make that change and create a PR for that. Thanks.