Confused about handling errors and exceptions in RxJava - android

I'm just starting to learn RxJava and I've read and watched quite a few tutorials, but some things are not clicking just yet. I've dived in from the deep end and started by modifying one of my app's API calls to return an Observable. Currently the API call is used by an AsyncTaskLoader which returns cached data from a local database, then does the call, merges the data and returns it again. This pattern sounded like a perfect subject for my RxJava experiments, but starting small, I want to return an observable from my API call.
This is my original call:
public static ArrayList<Stuff> getStuffForId(String id)
throws IOException, UserNotAuthenticatedException {
Profile profile = getProfile();
HashMap<String,ArrayList<Stuff>> map = profile.getStuff();
if (map == null) {
throw new IOException("error processing - map cannot be null");
}
return map.get(id);
}
private static Profile getProfile()
throws IOException, UserNotAuthenticatedException {
// <.. getting url, auth tokens and other stuff to prepare the Request ..>
Response response = sHttpClient.newCall(request).execute();
if (response.code() == ERR_AUTH_REJECTED) {
throw new UserNotAuthenticatedException(response.body().string());
}
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
Gson gson = new Gson();
String result = response.body().string();
response.body().close();
return gson.fromJson(result, Profile.class);
}
And in RxJava world I'm thinking something along the lines of:
public static Observable<ArrayList<Stuff>> getStuffForId(String id) {
return getProfile().map(
Profile::getStuff).map(
map -> {
if (map == null) {
Observable.error(new IOException("error processing - map cannot be null"));
}
return map.get(id);
});
}
private static Observable<Profile> getProfile() {
return Observable.fromCallable(() -> {
// <.. getting url, auth tokens and other stuff to prepare the Request ..>
Response response = sHttpClient.newCall(request).execute();
if (response.code() == ERR_AUTH_REJECTED) {
throw new UserNotAuthenticatedException(response.body().string(),
authToken);
}
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
Gson gson = new Gson();
String result = response.body().string();
response.body().close();
return gson.fromJson(result, Profile.class);
});
}
Does this look anything like what one would expect? I'm still not sure of the difference between fromCallable() and defer() and where you use one or another. Also not sure where do the exceptions go once thrown inside the fromCallable() method - will they automagically end up in onError of my subscriber or do I need to handle them in my getStuffForId method? And finally, Android Studio is warning me that return map.get(id) might throw a nullPointer. Is that just because the IDE doesn't understand that Observable.error will terminate the execution, or is it that I don't understand what will happen on Observable.error?

1) Difference b/w fromCallable() and defer()?
Well defer() does not create the Observable until some subscriber subscribes and it creates new Obervable each time the user subscribes. Refer this link for why you would want to use defer().
2) Where do exceptions go once thrown inside the fromCallable() method?
Exceptions are caught inside the Observer and then passed as a Throwable object to Subscriber's onError() method.
3) Android Studio is warning me that return map.get(id) might throw a nullPointer.
Its because when it is actually null you are not returning anything in the if statement. The code will run beyond if statement, thus causing nullPointerException. Observable.error()); returns an Observable and it doesn't throw anything, in order to do that you have to explicitly throw a RuntimeException.
4) Does this look anything like what one would expect.
Apart from the above error, there is nothing wrong but you can search online for the rxJava patterns to code better structurally.

Does this look anything like what one would expect
Yes, but please have a look at libraries, which will do the plumbing for you: https://github.com/square/retrofit
If you throw any exception in fromCallable you would use defer and return Observable.error(new Exception("error"))
Difference fromCallable / defer
Both methods are factories for creating observables. From callable asks you to return a value as in string etc. Defer like you to return a observable of something. You would use fromCallable if you would like to wrap some non-observable-method-call which returns a certain type. Furthermore fromCallable will handle exceptions for your and pass them down the chain. In contrast you would use defer if you would like to handle your own exception/ observable. You may return a observable which emits a value and then finishes or not finishes. 'fromCallable' will alsways finish with a value(s) and onComplete or onError. With defer you may produce a observable, which will never end as in Observable.never().
will they automagically end up in onError
Yes, exception will be caught and passed along as onError. You may handle the error with a operator right away in the chain or you may provide a onError handle (overload) on subscription.
return map.get(id) might throw a nullPointer
If you are using RxJava1.x you may encounter null values in the stream, because passing values with onNext(null) are valid. Therefor you would need a null check, due to the possibility of a NPE. Instead of the null-check you could filter-out null values with filter-operator as in:
Observable.just("1", "2", null, "3")
.filter(s -> s != null)
.map(s -> s.getBytes())
.subscribe(bytes -> {
//...
});
In this case you will get a warning about a possible NPE at s.getBytes(). But due to the filter-operation you can be sure, that s is never null.

Related

How to make multiple request and responses from all the requests in Retrofit, RxJava, Android

I have a scenario where I want to call same API for multiple devices and display result after completing all requests.
I am using retrofit 2.
I know little bit about RxJava. I thought zip operator will be suitable for this. So implemented as below.
API in ApiInterface :
#PUT(AppConstants.BASE_URL + AppConstants.PATH_SEPARATOR + "/user/endpoint")
Observable<ResponseBody> updateInfo(#Header("Authorization") String token, #Query("device_id") String deviceId, #Body JsonObject body);
Here is a method which calls API. It gets device id and its body in Map. This method calls API for every device id available in Map.
public void updateAllInfo(final HashMap<String, String> deviceIdMap, final ApiResponseListener listener) {
List<Observable<ResponseBody>> requests = new ArrayList<>();
ArrayList<String> reqIdList = new ArrayList<>();
for (Map.Entry<String, String> entry : map.entrySet()) {
String deviceId = entry.getKey();
String jsonBodyStr = entry.getValue();
Gson gson = new Gson();
JsonObject jsonBody = gson.fromJson(jsonBodyStr, JsonObject.class);
reqIdList.add(deviceId);
requests.add(apiInterface.updateSchedules("accessToken", deviceId, jsonBody));
}
Observable.zip(requests, new Function<Object[], List<ResponseBody>>() {
#Override
public List<ResponseBody> apply(Object[] objects) throws Exception {
Log.e("onSubscribe", "apply : " + objects.length);
List<ResponseBody> dataResponses = new ArrayList<>();
for (Object o : objects) {
dataResponses.add((ResponseBody) o);
}
return dataResponses;
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<List<ResponseBody>>() {
#Override
public void accept(List<ResponseBody> responseBodies) throws Exception {
Log.e("onSubscribe", "YOUR DATA IS HERE: " + responseBodies.size());
for (int i = 0; i < responseBodies.size(); i++) {
Log.e(TAG, "Response received for " + i + " is : " + responseBodies.get(i).string());
}
}
}, new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) throws Exception {
Log.e("onSubscribe", "Throwable: " + throwable);
}
});
}
I want to get the response (success / failure) for every device id. Means I need response and also id for which API is called.
Using zip operator, if any API is failed, failure is received in accept(Throwable throwable) method. If any API is failed, I think zip operator is not calling next API.
How can I get response (success or failure) for all request ?
Also need something to indicate the response is for which req / device id (Some mapping) to display result.
Is there any other operator I can use instead of zip ?
Any suggestion / help ?
I am a bit rusty in java, so I will write my answer in Kotlin, it should not be a problem for you to convert it yourself.
Create a helper class that will include the ResponseBody alongside with the deviceId:
data class IdentifiedResponseBody(
val deviceId: String,
val responseBody: ResponseBody?
)
Then:
// change the signature of your requests list to return IdentifiedResponseBody observables
val requests = mutableListOf<Observable<IdentifiedResponseBody>>()
...
// your stated API have updateInfo instead of updateSchedules, but I will assume they have the same signature
requests.add(
apiInterface.updateSchedules("accessToken", deviceId, jsonBody)
.map { responseBody ->
// map the added observable to return IdentifiedResponseBody
IdentifiedResponseBody(deviceId, responseBody)
}
.onErrorReturn { error ->
// return an item here instead of throwing error, so that the other observables will still execute
IdentifiedResponseBody(deviceId, null)
}
)
Finally, use merge instead of zip:
Observable.merge(requests)
.subscribeOn(Schedulers.io())
// it's a question if you want to observe these on main thread, depends on context of your application
.subscribe(
{ identifiedResponse ->
// here you get both the deviceId and the responseBody
Log.d("RESPNOSE", "deviceId=${identifiedResponse.deviceId}, body=${identifiedResponse.responseBody}")
if (responseBody == null || responseBody.hasError()) {
// request for this deviceId failed, handle it
}
},
{ error ->
Log.e("onSubscribe", "Throwable: " + error)
}
)
See merge: http://reactivex.io/documentation/operators/merge.html
See zip: http://reactivex.io/documentation/operators/zip.html
You should see the profound difference: zip combines your responses to one single item defined by your mapping function (i.e. list of responses in your case), while merge emits all responses individually, at the time they are returned. In case of zip here, the combined result is returned at the moment (and only) when all the requests have finished; you may not want this behavior, as if a single request would not return a response, you would not get any response at all.
UPDATE
The java equivalent should be as follow, but revise before trying out, as I am not sure if I converted everything correctly:
requests.add(
apiInterface.updateSchedules("accessToken", deviceId, jsonBody)
.map(new Function<ResponseBody, IdentifiedResponseBody>() {
#Override
public IdentifiedResponseBody apply(ResponseBody responseBody) throws Exception {
return new IdentifiedResponseBody(deviceId, responseBody);
}
})
.onErrorReturn(new Function<Throwable, IdentifiedResponseBody>() {
#Override
public IdentifiedResponseBody apply(Throwable throwable) throws Exception {
return new IdentifiedResponseBody(deviceId, null);
}
})
);
Observable.merge(requests)
.subscribeOn(Schedulers.io())
.subscribe(new Consumer<IdentifiedResponseBody>() {
#Override
public void accept(IdentifiedResponseBody identifiedResponseBody) throws Exception {
// same logic as from kotlin part
}
},
new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) throws Exception {
Log.e("onSubscribe", "Throwable: " + throwable);
}
});
UPDATE 2
In the comment you asked:
Is there any way from which I can get final callback for all requests
completed
That's the problem with using Observable instead of Single/Completable, it just does not finish unless you explicitly close the channel or an error is thrown. In ideal context, Observable should be used for streams that continuously emits some data, for example an open channel to Room DB, as there are no telling how many time the DB will change. I admit that in your case it seems to be difficult to apply something else than Observable. There is however a workaround:
Observable.merge(requests)
// emits only this much items, then close this channel
.take(requests.size.toLong())
// executed when the channel is closed or disposed
.doFinally {
// todo: finalCallback
}
.subscribeOn(Schedulers.io())
.subscribe(...)
The code is again in Kotlin, should not be hard to transform to java. Please check what Observable.take() does: http://reactivex.io/documentation/operators/take.html

Handle all types of Retrofit errors properly with rxjava

I'm new to Rxjava with Retrofit and I'm asking for the best right way to handle all possible status in Retrofit using rxjava and rxbinding which includes:
No Internet connection.
Null response from server.
Success response.
Error response and show the error message like Username or password is incorrect.
Other errors like connection reset by peers.
I'm having exception subclass for each important failure response.
Exceptions are delivered as Observable.error(), while values are delivered through stream without any wrapping.
1) No internet - ConnectionException
2) Null - just NullPointerException
4) Check for "Bad Request" and throw IncorrectLoginPasswordException
5) any other error is just NetworkException
You can map errors with onErrorResumeNext() and map()
For example
Typical retrofit method that fetches data from web service:
public Observable<List<Bill>> getBills() {
return mainWebService.getBills()
.doOnNext(this::assertIsResponseSuccessful)
.onErrorResumeNext(transformIOExceptionIntoConnectionException());
}
Method that assures response is ok, throws appropriate exceptions otherwise
private void assertIsResponseSuccessful(Response response) {
if (!response.isSuccessful() || response.body() == null) {
int code = response.code();
switch (code) {
case 403:
throw new ForbiddenException();
case 500:
case 502:
throw new InternalServerError();
default:
throw new NetworkException(response.message(), response.code());
}
}
}
IOException means there is no network connection so I throw ConnectionException
private <T> Function<Throwable, Observable<T>> transformIOExceptionIntoConnectionException() {
// if error is IOException then transform it into ConnectionException
return t -> t instanceof IOException ? Observable.error(new ConnectionException(t.getMessage())) : Observable.error(
t);
}
For your login request create new method that will check if login/password are ok.
And at the end there is
subscribe(okResponse -> {}, error -> {
// handle error
});

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

android - slow starting new Activity

Second Activity in my project starts very slow, and I don't know how to fix it. How it works (everything's in onCreate()): firstly it does get-request to the page with json:
try {
DefaultHttpClient hc = new DefaultHttpClient();
ResponseHandler<String> res = new BasicResponseHandler();
HttpGet getMethod = new HttpGet(url);
String response = hc.execute(getMethod, res);
resStr = response.toString();
} catch (Exception e) {
System.out.println("Exp=" + e);
}
I know some methods from here are deprecated, but I don't know how to make it more optimized. As a result it returns a string with JSON array of about 32 Objects.
Then I fill 6 arrays to pass them to ArrayAdapter for filling ListView. Methods of getting different types of data looks like this:
public static String GetWantedType(String resStr, int num) {
String jsonvalues = "";
try {
JSONArray json_Array = new JSONArray(resStr);
JSONObject json_data = json_Array.getJSONObject(num);
jsonvalues = json_data.getString("wanted_type");
} catch (JSONException e) {
e.printStackTrace();
}
return jsonvalues;
}
Maybe I should have created json_Array one time outside filling arrays and just pass it to those methods as a JSONArray, not as a string - I don't know if it influence the speed.
In the ArrayAdapter everything seems to be all right, I'm using ViewHolder and my scrolling is excelent. Unlike starting Activity.
How to make it better and more optimized?
First, don't do all operations in onCreate() , prefer to do in onResume()
Second, all server call should be in background thread using Async and use result for displaying user.
If you don't want to call multiple times API for data during onResume() and onPause(), you can consume result of data in array or something and when onResume() call, you can check whether it has data, just load it, else fetch from server.
As Gaurav said the problem is that the network request is called on the main thread.
At the time you ask a network call your program say : OK STOP I WAIT THE RESPONSE.
So if you want to change this you can do various things.
For example you can use a Asynchronous Network call with a lib (loopj lib)
or you can simply open a Thread : do the network call.
With that your UI will not freeze
Are you doing the network call on the main thread? Please don't do it. Instead do the network operations on different thread.
For networking you can use a library retrofit.
It also makes it easy for you do the operations asynchronously, by using callbacks or using Observables from RxJava

Billing API v3 IabHelper NullPointerException

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

Categories

Resources