Billing API v3 IabHelper NullPointerException - android

edit 4/15: Catching nullpointer in IabHelper appears to have stopped this problem. I am no longer seeing the exceptions being thrown, I'm going to accept this as an answer.
edit 4/04: A little bit of a deeper dive. There are try catch blocks that handle RemoteExceptions and JSONExceptions for the queryPurchases method, but no NullPointerException handling. What I am going to try is include NullPointer Exception handling so IabHelper looks like this when trying to querySkuDetails:
catch (NullPointerException e) {
throw new IabException(IABHELPER_UNKNOWN_ERROR, "NullPointer while refreshing inventory.", e);
}
I just filed a bug on this:
https://code.google.com/p/marketbilling/issues/detail?id=114
edit 3/25: well, still receiving this crash... now it happens while trying to get a context at line 3 of the following excerpt from IabHelper:
int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
logDebug("Querying owned items, item type: " + itemType);
logDebug("Package name: " + mContext.getPackageName());
This is frustrating because in my manifest I always use the full path name of my app for "name".
Example "com.myappname.blah.ClassName"
I've also tried passing this, MyClass.this, getApplicationContext() to mHelper. However they all produce the same NullPointer results randomly from devices in the wild. I also tried name=".MyClass" in the manifest. This is what it looks like currently:
mHelper = new IabHelper(MyClass.this, myKey);
edit 3/18/13: I am still receiving exceptions, even with the new IabHelper version deployed on 3/17.
I am starting to see a pattern here, that the crashes are all when trying to get a context when executing mContext.getPackageName(). I'm curious why this works on all of my test devices, and I can't reproduce this crash, and only seems to be on a small number of devices.
Here is the new crash:
java.lang.NullPointerException
at com.myapp.util.IabHelper.queryPurchases(SourceFile:836)
at com.myapp.util.IabHelper.queryInventory(SourceFile:558)
at com.myapp.util.IabHelper.queryInventory(SourceFile:522)
at com.myapp.util.IabHelper$2.run(SourceFile:617)
at java.lang.Thread.run(Thread.java:1019)
Caused by IabHelper...
line 836: logDebug("Package name: " + mContext.getPackageName());
edit 3/17/13: I see that there have been many bug fixes published over the past several months, I will try the latest code available here and see if this resolves the problem:
https://code.google.com/p/marketbilling/source/browse/v3/src/com/example/android/trivialdrivesample/util
In one of my apps, I am using the billing API and the boilerplate code included with it.
I am using the latest version of billing API available via the SDK manager as of 3/16/2013.
In my activity, I query the inventory using the following:
final List<String> skuList = new ArrayList<String>();
skuList.add("sku1");
skuList.add("sku2");
skuList.add("sku3");
if (skuList != null) {
if (skuList.size() > 0) {
try {
mHelper.queryInventoryAsync(true, skuList, mGotInventoryListener);
} catch (Exception e) {
ACRA.getErrorReporter().handleException(e);
}
}
}
I am receiving multiple NullPointerException reports in the wild from the IabHelper class for the following devices. I can't reproduce the issue and can't find any information regarding these crashes, and is the reason why I am posting this question.
I have countless other checks for nulls and try/catch blocks in the "developer facing" part of the billing API, including within onQueryInventoryFinished, so I know this exception is not being thrown from "my code" (because I'm not capturing crashes from any of my app's classes), but instead is being thrown from within the IabHelper itself. I have not modified the IabHelper other than this recommended fix: https://stackoverflow.com/a/14737699
Crash #1 Galaxy Nexus
java.lang.NullPointerException
at com.myapp.util.IabHelper.querySkuDetails(SourceFile:802)
at com.myapp.util.IabHelper.queryInventory(SourceFile:471)
at com.myapp.util.IabHelper$2.run(SourceFile:521)
at java.lang.Thread.run(Thread.java:856)
Caused by IabHelper...
line 802: Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(), ITEM_TYPE_INAPP, querySkus);
Crash #2 Samsung GT-S5570L
java.lang.NullPointerException
at com.myapp.util.IabHelper.queryPurchases(SourceFile:735)
at com.myapp.util.IabHelper.queryInventory(SourceFile:465)
at com.myapp.util.IabHelper$2.run(SourceFile:521)
at java.lang.Thread.run(Thread.java:1019)
Caused by IabHelper...
line 735: Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(), ITEM_TYPE_INAPP, continueToken);

edit 4/15: Catching nullpointer in IabHelper appears to have stopped this problem. I am no longer seeing the exceptions being thrown, I'm going to accept this as an answer.
edit 4/04: A little bit of a deeper dive. There are try catch blocks that handle RemoteExceptions and JSONExceptions for the queryPurchases method, but no NullPointerException handling. What I am going to try is include NullPointer Exception handling so IabHelper looks like this when trying to querySkuDetails:
catch (NullPointerException e) {
throw new IabException(IABHELPER_UNKNOWN_ERROR, "NullPointer while refreshing inventory.", e);
}
I just filed a bug on this:
https://code.google.com/p/marketbilling/issues/detail?id=114
Change
if (querySkuDetails) {
r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
if (r != BILLING_RESPONSE_RESULT_OK) {
throw new IabException(r, "Error refreshing inventory (querying prices of items).");
}
}
to
if (querySkuDetails) {
try {
r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
if (r != BILLING_RESPONSE_RESULT_OK) {
throw new IabException(r, "Error refreshing inventory (querying prices of items).");
}
} catch (NullPointerException e) {
throw new IabException(IABHELPER_UNKNOWN_ERROR, "NPE while refreshing inventory.", e);
}
}
Change
if (querySkuDetails) {
r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreSubsSkus);
if (r != BILLING_RESPONSE_RESULT_OK) {
throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
}
}
to
if (querySkuDetails) {
try {
r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreSubsSkus);
if (r != BILLING_RESPONSE_RESULT_OK) {
throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
}
} catch (NullPointerException e) {
throw new IabException(IABHELPER_UNKNOWN_ERROR, "NPE while refreshing inventory.", e);
}
}

You are probably using async operations. The current IabHelper is not safe in case you use the ...async methods. The problem is that in any moment an async operation is running dispose can be called on the main thread. In this case you will get NullPointerExceptions and IllegalStateExceptions.
Here is the patch fixing it:
Index: src/com/evotegra/aCoDriver/iabUtil/IabHelper.java
===================================================================
--- src/com/evotegra/aCoDriver/iabUtil/IabHelper.java (revision 1162)
+++ src/com/evotegra/aCoDriver/iabUtil/IabHelper.java (working copy)
## -86,7 +86,10 ##
// Is an asynchronous operation in progress?
// (only one at a time can be in progress)
- boolean mAsyncInProgress = false;
+ volatile boolean mAsyncInProgress = false;
+
+ // is set to true if dispose is called while a thread is running. Allows graceful shutdown
+ volatile boolean mDisposeRequested = false;
// (for logging/debugging)
// if mAsyncInProgress == true, what asynchronous operation is in progress?
## -285,6 +288,12 ##
* disposed of, it can't be used again.
*/
public void dispose() {
+ // do not dispose while an async Thread is running. Will cause all kinds of exceptions.
+ // In this case dispose must be called from thread after setting mAsyncInProgress to true
+ if (mAsyncInProgress) {
+ mDisposeRequested = true;
+ return;
+ }
logDebug("Disposing.");
mSetupDone = false;
if (mServiceConn != null) {
## -827,6 +836,7 ##
logDebug("Ending async operation: " + mAsyncOperation);
mAsyncOperation = "";
mAsyncInProgress = false;
+ if (mDisposeRequested) IabHelper.this.dispose();
}
Or download the patch here.
http://code.google.com/p/marketbilling/issues/detail?id=139&thanks=139&ts=1375614409

Slightly modify the beginning of the queryPurchases method to look like this:
int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
// Query purchases
//logDebug("Querying owned items, item type: " + itemType);
//logDebug("Package name: " + mContext.getPackageName());
boolean verificationFailed = false;
String continueToken = null;
do {
// logDebug("Calling getPurchases with continuation token: " + continueToken);
if(mDisposed || mService==null) return IABHELPER_UNKNOWN_ERROR;
Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
itemType, continueToken);
Thanks to sebastie for pointing out the cause of this.

tmanthey patch also requires
mDisposeRequested = false;
after the disposing takes place

If you're getting this error on the emulator, it may be a very simple thing which happens in more than a half cases.
Check that you're using Google API SDK and not regular SDK!!!

The IabHelper is obsolete and has been replaced by the BillingClient.
See https://developer.android.com/google/play/billing/billing_library.html

Related

Android New relic track non Network error

are there any way to track handled errors on New Relic?
Documentation says that we can track with
NewRelic.noticeNetworkFailure(...)
however I've tried to track errors which aren't from any network call and with a fake URL but I got this:
'java.lang.String com.newrelic.agent.android.api.common.TransactionData.getUrl()' on a null object reference
Other platforms like JAVA have this
NewRelic.noticeError(e);
but the android platform does not have a method to notice a simple error.
do you know how we can send handled errors?
At the end, there is way to track an error however it won't appear in you crashList.
The notice methods that New Relic has allow you to notice error. With a method like the below one you can send a notice that will appear in your Network --> error page
public static void reportLoggedException(String message, Throwable tr) {
long time = new Date().getTime();
try {
if (message.isEmpty()) {
message = "none";
}
NewRelic.noticeNetworkFailure("http://" + URLEncoder.encode(message, "utf-8")+".com", "GET", time, time, new Exception(tr));
} catch (Exception e) {
e.printStackTrace();
}
}

Sony QX100 return 403 error for supported methods

I am trying to set the Exposure mode and Focus mode for my QX100 device. Each time I make the API call I get a 403 error. However, these two methods setExposureMode and setFocusMode are supported by the QX100 as it clearly states in the API docs. In addition, I can set the focus mode through Playmemories. This same problem also occurs with setBeepMode, which is also supported. Any ideas about why this could be occurring?
There are some supported methods that are working, such as actTakePicture and setPostviewImageSize
An example call:
public JSONObject setFocusMode() throws IOException {
String service = "camera";
try {
JSONObject requestJson =
new JSONObject().put("method", "setFocusMode").put("params", new JSONArray().put("MF")) //
.put("id", id()).put("version", "1.0");
String url = findActionListUrl(service) + "/" + service;
log("Request: " + requestJson.toString());
JSONObject responseJson = SimpleHttpClient.httpPost(url, requestJson, null);
log("Response: " + responseJson.toString());
return responseJson;
} catch (JSONException e) {
throw new IOException(e);
}
}
Is your QX100 updated to the latest firmware? On old one, most of APIs are restricted.
Or they may be temporary disabled. You can use getAvailableApiList to know that.

Android Google+ integration - repeated UserRecoverableAuthException

We have contacted Google about this and we are on chat
The issue seems to be fixed for devices except Samsung phones.
I'm adding a Google+ sign in option to an app per the official instructions. Once the user has selected their account I would like my server to retrieve their Google+ profile info and update their profile on our site to match.
The first part - having the user select a Google account locally - seems to work just fine. When I try to request a token for the selected account, the Google auth dialog displays with the appropriate parameters; however, when I authorize the app using that dialog and re-request the token, GoogleAuthUtil.getToken(...) again throws a UserRecoverableAuthException (NeedPermission, not GooglePlayServicesAvailabilityException) and I get the same dialog asking me to approve!
This behavior is present on a Samsung S3 running Android 4.1.1 (with 3 Google accounts) and an Acer A100 running 4.0.3. It is NOT present on an HTC Glacier running 2.3.4. Instead, the HTC Glacier gives me a valid auth code. All devices have the latest iteration of Google Play Services installed and are using different Google+ accounts.
Anyone seen this before? Where can I start with debugging?
Here's the complete code - is anything obviously awry?
public class MyGooglePlusClient {
private static final String LOG_TAG = "GPlus";
private static final String SCOPES_LOGIN = Scopes.PLUS_LOGIN + " " + Scopes.PLUS_PROFILE;
private static final String ACTIVITIES_LOGIN = "http://schemas.google.com/AddActivity";
private static MyGooglePlusClient myGPlus = null;
private BaseActivity mRequestingActivity = null;
private String mSelectedAccount = null;
/**
* Get the GPlus singleton
* #return GPlus
*/
public synchronized static MyGooglePlusClient getInstance() {
if (myGPlus == null)
myGPlus = new MyGooglePlusClient();
return myGPlus;
}
public boolean login(BaseActivity requester) {
Log.w(LOG_TAG, "Starting login...");
if (mRequestingActivity != null) {
Log.w(LOG_TAG, "Login attempt already in progress.");
return false; // Cannot launch a new request; already in progress
}
mRequestingActivity = requester;
if (mSelectedAccount == null) {
Intent intent = AccountPicker.newChooseAccountIntent(null, null, new String[]{GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE}, false,
null, GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE, null, null);
mRequestingActivity.startActivityForResult(intent, BaseActivity.REQUEST_GPLUS_SELECT);
}
return true;
}
public void loginCallback(String accountName) {
mSelectedAccount = accountName;
authorizeCallback();
}
public void logout() {
Log.w(LOG_TAG, "Logging out...");
mSelectedAccount = null;
}
public void authorizeCallback() {
Log.w(LOG_TAG, "User authorized");
AsyncTask<Void, Void, String> task = new AsyncTask<Void, Void, String>() {
#Override
protected String doInBackground(Void... params) {
String token = null;
try {
Bundle b = new Bundle();
b.putString(GoogleAuthUtil.KEY_REQUEST_VISIBLE_ACTIVITIES, ACTIVITIES_LOGIN);
token = GoogleAuthUtil.getToken(mRequestingActivity,
mSelectedAccount,
"oauth2:server:client_id:"+Constants.GOOGLE_PLUS_SERVER_OAUTH_CLIENT
+":api_scope:" + SCOPES_LOGIN,
b);
} catch (IOException transientEx) {
// Network or server error, try later
Log.w(LOG_TAG, transientEx.toString());
onCompletedLoginAttempt(false);
} catch (GooglePlayServicesAvailabilityException e) {
Log.w(LOG_TAG, "Google Play services not available.");
Intent recover = e.getIntent();
mRequestingActivity.startActivityForResult(recover, BaseActivity.REQUEST_GPLUS_AUTHORIZE);
} catch (UserRecoverableAuthException e) {
// Recover (with e.getIntent())
Log.w(LOG_TAG, "User must approve "+e.toString());
Intent recover = e.getIntent();
mRequestingActivity.startActivityForResult(recover, BaseActivity.REQUEST_GPLUS_AUTHORIZE);
} catch (GoogleAuthException authEx) {
// The call is not ever expected to succeed
Log.w(LOG_TAG, authEx.toString());
onCompletedLoginAttempt(false);
}
Log.w(LOG_TAG, "Finished with task; token is "+token);
if (token != null) {
authorizeCallback(token);
}
return token;
}
};
task.execute();
}
public void authorizeCallback(String token) {
Log.w(LOG_TAG, "Token obtained: "+token);
// <snipped - do some more stuff involving connecting to the server and resetting the state locally>
}
public void onCompletedLoginAttempt(boolean success) {
Log.w(LOG_TAG, "Login attempt "+(success ? "succeeded" : "failed"));
mRequestingActivity.hideProgressDialog();
mRequestingActivity = null;
}
}
I've had this issue for a while and came up with a proper solution.
String token = GoogleAuthUtil.getToken(this, accountName, scopeString, appActivities);
This line will either return the one time token or will trigger the UserRecoverableAuthException.
On the Google Plus Sign In guide, it says to open the proper recovery activity.
startActivityForResult(e.getIntent(), RECOVERABLE_REQUEST_CODE);
When the activity returns with the result, it will come back with few extras in the intent and that is where the new token resides :
#Override
protected void onActivityResult(int requestCode, int responseCode, Intent intent) {
if (requestCode == RECOVERABLE_REQUEST_CODE && responseCode == RESULT_OK) {
Bundle extra = intent.getExtras();
String oneTimeToken = extra.getString("authtoken");
}
}
With the new oneTimeToken given from the extra, you can submit to the server to connect properly.
I hope this helps!
Its too late to reply but it may help to people having same concern in future.
They have mentioned in the tutorial that it will always throw UserRecoverableAuthException
when you invoke GoogleAuthUtil.getToken() for the first time. Second time it will succeed.
catch (UserRecoverableAuthException e) {
// Requesting an authorization code will always throw
// UserRecoverableAuthException on the first call to GoogleAuthUtil.getToken
// because the user must consent to offline access to their data. After
// consent is granted control is returned to your activity in onActivityResult
// and the second call to GoogleAuthUtil.getToken will succeed.
startActivityForResult(e.getIntent(), AUTH_CODE_REQUEST_CODE);
return;
}
i used below code to get access code from google.
execute this new GetAuthTokenFromGoogle().execute(); once from public void onConnected(Bundle connectionHint) and once from protected void onActivityResult(int requestCode, int responseCode, Intent intent)
private class GetAuthTokenFromGoogle extends AsyncTask<Void, Integer, Void>{
#Override
protected void onPreExecute()
{
}
#Override
protected Void doInBackground(Void... params) {
// TODO Auto-generated method stub
try {
accessCode = GoogleAuthUtil.getToken(mContext, Plus.AccountApi.getAccountName(mGoogleApiClient), SCOPE);
new ValidateTokenWithPhoneOmega().execute();
Log.d("Token -- ", accessCode);
} catch (IOException transientEx) {
// network or server error, the call is expected to succeed if you try again later.
// Don't attempt to call again immediately - the request is likely to
// fail, you'll hit quotas or back-off.
return null;
} catch (UserRecoverableAuthException e) {
// Recover
startActivityForResult(e.getIntent(), RC_ACCESS_CODE);
e.printStackTrace();
} catch (GoogleAuthException authEx) {
// Failure. The call is not expected to ever succeed so it should not be
// retried.
authEx.printStackTrace();
return null;
} catch (Exception e) {
throw new RuntimeException(e);
}
return null;
}
#Override
protected void onPostExecute(Void result)
{
}
}
I have got around this issue by using a web based login. I open a url like this
String url = "https://accounts.google.com/o/oauth2/auth?scope=" + Scopes.PLUS_LOGIN + "&client_id=" + webLoginClientId + "&response_type=code&access_type=offline&approval_prompt=force&redirect_uri=" + redirect;
The redirect url then handles the response and returns to my app.
In terms of my findings on using the Google Play Services, I've found:
HTC One is 3.1.59 (736673-30) - not working
Galaxy Note is 3.1.59 (736673-36) - not working
Nexus S is 3.1.59 (736673-34) - works
And I'd like to be involved in the chat that is occurring, however I don't have a high enough reputation to do so.
I've experienced the same issue recently - it appears to be device-specific (I had it happen every time on one S3, but on another S3 running the same OS it didn't happen, even with the same account). My hunch is that it's a bug in a client app, either the G+ app or the Google Play Services app. I managed to solve the issue on one of my devices by factory resetting it (a Motorola Defy), then reinstalling the Google Play Services app, but that's a completely useless solution to tell to users.
Edit (6th Aug 2013): This seems to have been fixed for me without any changes to my code.
The first potential issue I can see is that you are calling GoogleAuthUtil.getToken() after you get the onConnected() callback. This is a problem because requesting an authorization code for your server using GoogleAuthUtil.getToken() will always show a consent screen to your users. So you should only get an authorization code for new users and, to avoid showing new users two consent screens, you must fetch an authorization code and exchange it on your server before resolving any connection failures from PlusClient.
Secondly, make sure you actually need both a PlusClient and an authorization code for your servers. You only need to get a PlusClient and an authorization code if you are intending to make calls to the Google APIs from both the Android client and your server. As explained in this answer.
These issues would only result in two consent dialogs being displayed (which is clearly not an endless loop) - are you seeing more than two consent dialogs?
I had a similar problem where an apparent auth loop kept creating {read: spamming} these "Signing In..." and Permission request dialogs while also giving out the discussed exception repeatedly.
The problem appears in some slightly-modified example code that I (and other like me, I suspect) "cargo-culted" from AndroidHive. The solution that worked for me was ensuring that only one background token-retrieval task runs at the background at any given time.
To make my code easier to follow, here's the auth flow in my app (that is almost identical to the example code on AndoidHive): Activity -> onConnected(...) -> getProfileInformation() -> getOneTimeToken().
Here's where getOneTimeToken() is called:
private void getProfileInformation() {
try {
if (Plus.PeopleApi.getCurrentPerson(mGoogleApiClient) != null) {
Person currentPerson = Plus.PeopleApi
.getCurrentPerson(mGoogleApiClient);
String personName = currentPerson.getDisplayName();
String personPhotoUrl = currentPerson.getImage().getUrl();
String personGooglePlusProfile = currentPerson.getUrl();
String email = Plus.AccountApi.getAccountName(mGoogleApiClient);
getOneTimeToken(); // <-------
...
Here's my getOneTimeToken():
private void getOneTimeToken(){
if (task==null){
task = new AsyncTask<Void, Void, String>() {
#Override
protected String doInBackground(Void... params) {
LogHelper.log('d',LOGTAG, "Executing background task....");
Bundle appActivities = new Bundle();
appActivities.putString(
GoogleAuthUtil.KEY_REQUEST_VISIBLE_ACTIVITIES,
ACTIVITIES_LOGIN);
String scopes = "oauth2:server" +
":client_id:" + SERVER_CLIENT_ID +
":api_scope:" + SCOPES_LOGIN;
String token = null;
try {
token = GoogleAuthUtil.getToken(
ActivityPlus.this,
Plus.AccountApi.getAccountName(mGoogleApiClient),
scopes,
appActivities
);
} catch (IOException transientEx) {
/* Original comment removed*/
LogHelper.log('e',LOGTAG, transientEx.toString());
} catch (UserRecoverableAuthException e) {
/* Original comment removed*/
LogHelper.log('e',LOGTAG, e.toString());
startActivityForResult(e.getIntent(), AUTH_CODE_REQUEST);
} catch (GoogleAuthException authEx) {
/* Original comment removed*/
LogHelper.log('e',LOGTAG, authEx.toString());
} catch (IllegalStateException stateEx){
LogHelper.log('e',LOGTAG, stateEx.toString());
}
LogHelper.log('d',LOGTAG, "Background task finishing....");
return token;
}
#Override
protected void onPostExecute(String token) {
LogHelper.log('i',LOGTAG, "Access token retrieved: " + token);
}
};
}
LogHelper.log('d',LOGTAG, "Task setup successful.");
if(task.getStatus() != AsyncTask.Status.RUNNING){
task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); //double safety!
} else
LogHelper.log('d',LOGTAG,
"Attempted to restart task while it is running!");
}
Please note that I have a {probably redundant} double-safety against the task executing multiple times:
if(task .getStatus() != AsyncTask.Status.RUNNING){...} - ensures that the task isn't running before attempting to execute it.
task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);- makes sure that copies of this task are "synchronized" (i.e. a queue is in place such that only one task of this type can executed at a given time).
P.S.
Minor clarification: LogHelper.log('e',...) is equivalent to Log.e(...) etc.
you should startactiviy in UI thread
try {
....
} catch (IOException transientEx) {
....
} catch (final UserRecoverableAuthException e) {
....
runOnUiThread(new Runnable() {
public void run() {
startActivityForResult(e1.getIntent(), AUTH_CODE_REQUEST);
}
});
}
Had the same bug with infinite loop of permission request. For me it was because time on my phone was shifted. When I check detect time automatically this bug disappeared. Hope this helps!

Android in app purchase: Signature verification failed

I have tried for several days to solve this problem, using the Dungeons demo code that comes with the SDK. I've tried to Google for an answer but can't find one.
In the Dungeons demo, I passed my public key from the dev console.
Signed the apk and uploaded to console without publish.
Testing for both android.test.purchased & product list created on console with published for subscription (The main feature I want for my app).
But still I get an error of Signature verification failed and then the signature does not match data. How can I solve this?
public static ArrayList<VerifiedPurchase> verifyPurchase(String signedData, String signature)
{
if (signedData == null) {
Log.e(TAG, "data is null");
return null;
}
if (Consts.DEBUG) {
Log.i(TAG, "signedData: " + signedData);
}
boolean verified = false;
if (!TextUtils.isEmpty(signature)) {
String base64EncodedPublicKey = "MIIBIjA....AQAB";
PublicKey key = Security.generatePublicKey(base64EncodedPublicKey);
verified = Security.verify(key, signedData, signature);
if (!verified) {
Log.w(TAG, "signature does not match data.");
return null;
}
}
}
public static boolean verify(PublicKey publicKey, String signedData, String signature)
{
if (Consts.DEBUG) {
Log.i(TAG, "signature: " + signature);
}
Signature sig;
try {
sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
if (!sig.verify(Base64.decode(signature))) {
Log.e(TAG, "Signature verification failed.");
return false;
}
return true;
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "NoSuchAlgorithmException.");
} catch (InvalidKeyException e) {
Log.e(TAG, "Invalid key specification.");
} catch (SignatureException e) {
Log.e(TAG, "Signature exception.");
} catch (Base64DecoderException e) {
Log.e(TAG, "Base64 decoding failed.");
}
return false;
}
This problem is still going on in the current Google billing version. Basically the android.test.purchased is broken; After you buy android.test.purchased the verifyPurchase function in Security.java will always fail and the QueryInventoryFinishedListener will stop at the line if (result.isFailure()); this is because the android.test.purchased item always fails the TextUtils.isEmpty(signature) check in Security.java as it is not a real item and has no signature returned by the server.
My advice (from lack of any other solution) is to NEVER use "android.test.purchased". There are various code tweaks on the net but none of them work 100%.
If you have used the android.test.purchased then one way to get rid of the error is to do the following:-
Edit Security.java and change the "return false" line in the verifyPurchase to "return true" - this is temporary, we'll be putting it back in a minute.
In your QueryInventoryFinishedListener, after the "if (result.isFailure()) {...}" lines add the following to consume and get rid of your never ending android.test.purchased item:
if (inventory.hasPurchase(SKU_ANDROID_TEST_PURCHASE_GOOD)) {
mHelper.consumeAsync(inventory.getPurchase(SKU_ANDROID_TEST_PURCHASE_GOOD),null);
}
Run your app so the consunmeAsync happens, this gets rid of the "android.test.purchased" item on the server.
Remove the consumeAsync code (or comment it out).
Back in the Security.java, change the "return true" back to "return false".
Your QueryInventoryFinishedListener will no longer error on the verify, everything is back to "normal" (if you can call it that). Remember - don't bother using android.test.purchased again as it will just cause this error again... it's broke! The only real way to test your purchasing it to upload an APK, wait for it to appear, and then test it (the same APK) on your device with logging enabled.
Yes, the problem still occurs.
After I bought android.test.purchased I start getting the error on quering the inventory.
It is possible to fix your phone by just clearing data of Google Play Store application and running Google Play one time.
When you clear data of Google Play it forgets that you bought android.test.purchased
Please check that base64EncodedPublicKey and the one from the Play Developer Console are equal.
Once you re-upload the APK in the Developer Console, the public key may change, if so update your base64EncodedPublicKey.
You can skip the verifying process for those "android.test.*" product ids. If you are using the sample code from the TrivialDrive example, open IabHelper.java, find the following line code, change it from
if (Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) { ... }
into
boolean verifySignature = !sku.startsWith("android.test."); // or inplace the condition in the following line
if (verifySignature && !Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) { ... }
It's harmless, even if you forgot to rollback the code. So, you can continue to test the further workflow step.
Based on GMTDev's answer, this is what I do in order to fix the testing issues when consuming products in the simplest possible way. In Security.java, replace the verifyPurchase() method with this:
public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
TextUtils.isEmpty(signature)) {
Log.e(TAG, "Purchase verification failed: missing data.");
return BuildConfig.DEBUG; // Line modified by Cristian. Original line was: return false;
}
PublicKey key = Security.generatePublicKey(base64PublicKey);
return Security.verify(key, signedData, signature);
}
I only modified one line (see comment), and this way you can keep the code like that for debugging and still publish your release versions safely.
The error is caused because of the wrong license key. Maybe the license key is probably from your another app.
The solution is to use the proper license key from :
Play console > App > Development Tools > Licensing & in-app billing
What worked for me, while using In-app Billing v3 and the included utility classes, was consuming the test purchase within the returned onActivityResult call.
No changes to IabHelper, Security, or any of the In-app Billing util classes are needed to avoid this for future test purchases.
If you have already tried purchasing the test product and are now stuck on the purchase signature verification failed error, which you likely are since you are looking up answers for this error, then you should:
make the changes that GMTDev recommended
run the app to ensure that it consumes the product
remove/undo GMTDev's changes
implement the code below within onActivityResult.
Not only does this allow for the purchase testing process to be fluid but this should also avoid any conflicting issues with iab returning the " Item Already Owned " error when attempting to repurchase the test product.
If this is being called from within a fragment and your fragment's onActivityResult isn't being called then be sure to call YourFragmentName.onActivityResult(requestCode, resultCode, data) from your parent ActivityFragment if necessary. This is explained in more detail in Calling startIntentSenderForResult from Fragment (Android Billing v3).
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_PURCHASE) {
//this ensures that the mHelper.flagEndAsync() gets called
//prior to starting a new async request.
mHelper.handleActivityResult(requestCode, resultCode, data);
//get needed data from Intent extra to recreate product object
int responseCode = data.getIntExtra("RESPONSE_CODE", 0);
String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE");
// Strip out getActivity() if not being used within a fragment
if (resultCode == getActivity().RESULT_OK) {
try {
JSONObject jo = new JSONObject(purchaseData);
String sku = jo.getString("productId");
//only auto consume the android.test.purchased product
if (sku.equals("android.test.purchased")) {
//build the purchase object from the response data
Purchase purchase = new Purchase("inapp", purchaseData, dataSignature);
//consume android.test.purchased
mHelper.consumeAsync(purchase,null);
}
} catch (JSONException je) {
//failed to parse the purchase data
je.printStackTrace();
} catch (IllegalStateException ise) {
//most likely either disposed, not setup, or
//another billing async process is already running
ise.printStackTrace();
} catch (Exception e) {
//unexpected error
e.printStackTrace();
}
}
}
}
It will only remove the purchase if it's sku is "android.test.purchased" so it should be safe to use.
This Solution worked for me. I changed the new verifyPurchase method in purchase class with old one.
Signature verification fails only for the default test product.
A quick fix :
Goto IabHelper class.
Invert the if conditions of Security.verifyPurchase.
Thats it!
Remember to revert the changes when test product is replaced by actual product
Ran into the same issue (signature verification, and getting rid of the test purchase) today (Oct 30, 2018).
The signature issue is probably being caused by the fact that these test sku's are not really part of your app, and are thus do not have your app's signature. I did open a ticket with Google, but not sure if they can fix this. The workaround, as others pointed out, is to replace the code
if (verifyValidSignature(purchase.getOriginalJson(), purchase.getSignature())) {
with
if (verifyValidSignature(purchase.getOriginalJson(), purchase.getSignature()) ||
(purchase.getSku().startsWith("android.test.")) ) {
Regarding "how to get rid of the purchase of android.test.purchased SKU", I found that a simple reboot of the device, followed by waiting for a minute or so and/or re-starting your app a couple of times fixed it for me (i.e. I didn't have to 'consume' the purchase by code). I am guessing that the wait is needed so that the Play store completes syncing with Google's servers. (Not sure if this will continue to work this way in the future, but if it works for you now, this might help you move forward.)
Check this answer:
Is the primary account on your test device the same as your Google
Play developer account?
If not you won't get signatures on the android.test.* static responses
unless the app has been published on Play before.
See the table at
http://developer.android.com/guide/market/billing/billing_testing.html#static-responses-table
for the full set of conditions.
And it's comment:
I don't think the static ids return signature anymore. See
https://groups.google.com/d/topic/android-developers/PCbCJdOl480/discussion
Also, previously the sample code (used by many big apps) from Google Play Billing Library allowed an empty signature. That's why it's static purchases worked there.
But it was a security hole, so when it was published, Google submitted an update.
I have the same problem and follow #Deadolus said based on https://www.gaffga.de/implementing-in-app-billing-for-android/
The key point is we need to make the SKU is consumable even the inventory query result is failed. Below is the sample how i did that.
IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
Log.d(TAG, "Query inventory finished.");
// Have we been disposed of in the meantime? If so, quit.
if (mHelper == null) return;
// Is it a failure?
if (result.isFailure()) {
try {
Purchase purchase = new Purchase("inapp", "{\"packageName\":\"PACKAGE_NAME\","+
"\"orderId\":\"transactionId.android.test.purchased\","+
"\"productId\":\"android.test.purchased\",\"developerPayload\":\"\",\"purchaseTime\":0,"+
"\"purchaseState\":0,\"purchaseToken\":\"inapp:PACKAGE_NAME :android.test.purchased\"}",
"");
} catch (JSONException e) {
e.printStackTrace();
}
mHelper.consumeAsync(purchase, null);
complain("Failed to query inventory: " + result);
return;
}
Log.d(TAG, "Query inventory was successful.");
/*
* Check for items we own. Notice that for each purchase, we check
* the developer payload to see if it's correct! See
* verifyDeveloperPayload().
*/
}
};
Replace PACKAGE_NAME in the code above with the package name of your app.
This is what worked for me:
Call BillingClient.querySkuDetailsAsync to query if item if available
Wait for SkuDetailsResponseListener.onSkuDetailsResponse
Wait another 500ms
Start purchase using BillingClient.launchBillingFlow...
The step 3 shouldn't be necessary because when I received onSkuDetailsResponse it should be OK but it isn't, had to wait a little bit. After that purchase works, no more "Item not available error". This is how I tested it:
clear my app data
clear Google Play data
run app
purchase android.test.purchased
try to purchase my items (it fails with item not available)
use my solution above, it works
For Cordova and Hybrid apps you need to use this.iap.subscribe(this.productId) method to subscription InAppPurchase.
Following are the code working fine for me:
getProdutIAP() {
this.navCtrl.push('subscribeDialogPage');
this.iap
.getProducts(['productID1']).then((products: any) => {
this.buy(products);
})
.catch((err) => {
console.log(JSON.stringify(err));
alert('Finished Purchase' + JSON.stringify(err));
console.log(err);
});
}
buy(products: any) {
// this.getProdutIAP();
// alert(products[0].productId);
this.iap.subscribe(products[0].productId).then((buydata: any) => {
alert('buy Purchase' + JSON.stringify(buydata));
// this.sub();
}).catch((err) => {
// this.navCtrl.push('subscribeDialogPage');
alert('buyError' + JSON.stringify(err));
});
}
sub() {
this.platform.ready().then(() => {
this.iap
.subscribe(this.productId)
.then((data) => {
console.log('subscribe Purchase' + JSON.stringify(data));
alert('subscribe Purchase' + JSON.stringify(data));
this.getReceipt();
}).catch((err) => {
this.getReceipt();
alert('subscribeError' + JSON.stringify(err));
console.log(err);
});
})
}

Android In-App-Billing RESTORE_TRANSACTIONS with empty JSON!

Hey folks,
I am slightly modified the dungeons example for the Android In-App-Billing SDK article. I am having trouble with the RESTORE_TRANSACTIONS request. I first make a legitimate purchase and that goes fine, I get a call to onPurchaseStateChange with no issue. However, when I try to use RESTORE_TRANSACTIONS request, I was expecting to get a constructed verified list of purchases, but when I trace it, the JSON returned is verified just fine, but contains no transactions! Within Security.java in the Dungeons example you see this code in the verifyPurchase method:
if (Consts.DEBUG) {
Log.d(TAG, "Parsing JSON object");
}
JSONObject jObject;
JSONArray jTransactionsArray = null;
int numTransactions = 0;
long nonce = 0L;
try {
jObject = new JSONObject(signedData);
// The nonce might be null if the user backed out of the buy page.
nonce = jObject.optLong("nonce");
jTransactionsArray = jObject.optJSONArray("orders");
if (jTransactionsArray != null) {
numTransactions = jTransactionsArray.length();
}
if (Consts.DEBUG) {
Log.d(TAG, "JSON Array has " + numTransactions + " transactions");
}
} catch (JSONException e) {
if (Consts.DEBUG) {
Log.d(TAG, e.getMessage());
}
return null;
}
Am I misunderstanding the purpose of the RESTORE_TRANSACTIONS? Isn't it supposed to return a verified list of purchases just like REQUEST_PURCHASE through the onPurchaseStateChange?
Nevermind, I have to look much closer. The RESTORE_TRANSACTIONS is meant to be used with MANAGED product ids. I was purchasing an UNMANAGED item.
http://developer.android.com/guide/market/billing/billing_admin.html#billing-purchase-type

Categories

Resources