Move purchases to google billing library
This commit is contained in:
parent
0808d1a3b8
commit
222b9e410b
12 changed files with 701 additions and 1509 deletions
|
@ -395,6 +395,7 @@ dependencies {
|
|||
implementation 'org.immutables:gson:2.5.0'
|
||||
implementation 'com.vividsolutions:jts-core:1.14.0'
|
||||
implementation 'com.google.openlocationcode:openlocationcode:1.0.4'
|
||||
implementation 'com.android.billingclient:billing:2.0.3'
|
||||
// turn off for now
|
||||
//implementation 'com.atilika.kuromoji:kuromoji-ipadic:0.9.0'
|
||||
implementation 'com.squareup.picasso:picasso:2.71828'
|
||||
|
|
|
@ -54,17 +54,6 @@ public class OsmandInAppPurchaseActivity extends AppCompatActivity implements In
|
|||
deinitInAppPurchaseHelper();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
// Pass on the activity result to the helper for handling
|
||||
if (purchaseHelper == null || !purchaseHelper.onActivityResultHandled(requestCode, resultCode, data)) {
|
||||
// not handled, so handle it ourselves (here's where you'd
|
||||
// perform any handling of activity results not related to in-app
|
||||
// billing...
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
private void initInAppPurchaseHelper() {
|
||||
deinitInAppPurchaseHelper();
|
||||
|
||||
|
|
|
@ -2,15 +2,22 @@ package net.osmand.plus.inapp;
|
|||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode;
|
||||
import com.android.billingclient.api.BillingClient.SkuType;
|
||||
import com.android.billingclient.api.BillingResult;
|
||||
import com.android.billingclient.api.Purchase;
|
||||
import com.android.billingclient.api.SkuDetails;
|
||||
import com.android.billingclient.api.SkuDetailsResponseListener;
|
||||
|
||||
import net.osmand.AndroidNetworkUtils;
|
||||
import net.osmand.AndroidNetworkUtils.OnRequestResultListener;
|
||||
import net.osmand.PlatformUtil;
|
||||
import net.osmand.plus.OsmandApplication;
|
||||
import net.osmand.plus.OsmandSettings;
|
||||
import net.osmand.plus.OsmandSettings.OsmandPreference;
|
||||
|
@ -21,13 +28,8 @@ import net.osmand.plus.inapp.InAppPurchases.InAppPurchase.PurchaseState;
|
|||
import net.osmand.plus.inapp.InAppPurchases.InAppPurchaseLiveUpdatesOldSubscription;
|
||||
import net.osmand.plus.inapp.InAppPurchases.InAppSubscription;
|
||||
import net.osmand.plus.inapp.InAppPurchases.InAppSubscriptionList;
|
||||
import net.osmand.plus.inapp.util.IabHelper;
|
||||
import net.osmand.plus.inapp.util.IabHelper.OnIabPurchaseFinishedListener;
|
||||
import net.osmand.plus.inapp.util.IabHelper.QueryInventoryFinishedListener;
|
||||
import net.osmand.plus.inapp.util.IabResult;
|
||||
import net.osmand.plus.inapp.util.Inventory;
|
||||
import net.osmand.plus.inapp.util.Purchase;
|
||||
import net.osmand.plus.inapp.util.SkuDetails;
|
||||
import net.osmand.plus.inapp.util.BillingManager;
|
||||
import net.osmand.plus.inapp.util.BillingManager.BillingUpdatesListener;
|
||||
import net.osmand.plus.liveupdates.CountrySelectionFragment;
|
||||
import net.osmand.plus.liveupdates.CountrySelectionFragment.CountryItem;
|
||||
import net.osmand.util.Algorithms;
|
||||
|
@ -46,11 +48,9 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static net.osmand.plus.inapp.util.IabHelper.IABHELPER_USER_CANCELLED;
|
||||
import static net.osmand.plus.inapp.util.IabHelper.ITEM_TYPE_SUBS;
|
||||
|
||||
public class InAppPurchaseHelper {
|
||||
// Debug tag, for logging
|
||||
private static final org.apache.commons.logging.Log LOG = PlatformUtil.getLog(InAppPurchaseHelper.class);
|
||||
private static final String TAG = InAppPurchaseHelper.class.getSimpleName();
|
||||
private boolean mDebugLog = true;
|
||||
|
||||
|
@ -64,7 +64,9 @@ public class InAppPurchaseHelper {
|
|||
private static final int RC_REQUEST = 10001;
|
||||
|
||||
// The helper object
|
||||
private IabHelper mHelper;
|
||||
private BillingManager billingManager;
|
||||
private List<SkuDetails> skuDetailsList;
|
||||
|
||||
private boolean isDeveloperVersion;
|
||||
private String token = "";
|
||||
private InAppPurchaseTaskType activeTask;
|
||||
|
@ -201,10 +203,6 @@ public class InAppPurchaseHelper {
|
|||
|
||||
// Create the helper, passing it our context and the public key to verify signatures with
|
||||
logDebug("Creating InAppPurchaseHelper.");
|
||||
mHelper = new IabHelper(ctx, BASE64_ENCODED_PUBLIC_KEY);
|
||||
|
||||
// enable debug logging (for a production application, you should set this to false).
|
||||
mHelper.enableDebugLogging(false);
|
||||
|
||||
// Start setup. This is asynchronous and the specified listener
|
||||
// will be called once setup completes.
|
||||
|
@ -212,26 +210,101 @@ public class InAppPurchaseHelper {
|
|||
try {
|
||||
processingTask = true;
|
||||
activeTask = taskType;
|
||||
mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
|
||||
public void onIabSetupFinished(IabResult result) {
|
||||
billingManager = new BillingManager(ctx, BASE64_ENCODED_PUBLIC_KEY, new BillingUpdatesListener() {
|
||||
|
||||
@Override
|
||||
public void onBillingClientSetupFinished() {
|
||||
logDebug("Setup finished.");
|
||||
|
||||
if (!result.isSuccess()) {
|
||||
if (!billingManager.isIsServiceConnected()) {
|
||||
// Oh noes, there was a problem.
|
||||
//complain("Problem setting up in-app billing: " + result);
|
||||
notifyError(taskType, result.getMessage());
|
||||
notifyError(taskType, billingManager.getBillingClientResponseMessage());
|
||||
stop(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Have we been disposed of in the meantime? If so, quit.
|
||||
if (mHelper == null) {
|
||||
if (billingManager == null) {
|
||||
stop(true);
|
||||
return;
|
||||
}
|
||||
|
||||
processingTask = !runnable.run(InAppPurchaseHelper.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConsumeFinished(String token, BillingResult billingResult) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPurchasesUpdated(List<Purchase> purchases) {
|
||||
|
||||
// Have we been disposed of in the meantime? If so, quit.
|
||||
if (billingManager == null) {
|
||||
stop(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeTask == InAppPurchaseTaskType.REQUEST_INVENTORY) {
|
||||
List<String> skuInApps = new ArrayList<>();
|
||||
for (InAppPurchase purchase : getInAppPurchases().getAllInAppPurchases(false)) {
|
||||
skuInApps.add(purchase.getSku());
|
||||
}
|
||||
billingManager.querySkuDetailsAsync(SkuType.INAPP, skuInApps, new SkuDetailsResponseListener() {
|
||||
@Override
|
||||
public void onSkuDetailsResponse(BillingResult billingResult, final List<SkuDetails> skuDetailsListInApps) {
|
||||
// Is it a failure?
|
||||
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
|
||||
logError("Failed to query inapps sku details: " + billingResult.getResponseCode());
|
||||
notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage());
|
||||
stop(true);
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> skuSubscriptions = new ArrayList<>();
|
||||
for (InAppSubscription subscription : getInAppPurchases().getAllInAppSubscriptions()) {
|
||||
skuSubscriptions.add(subscription.getSku());
|
||||
}
|
||||
|
||||
// Have we been disposed of in the meantime? If so, quit.
|
||||
if (billingManager == null) {
|
||||
stop(true);
|
||||
return;
|
||||
}
|
||||
|
||||
billingManager.querySkuDetailsAsync(SkuType.SUBS, skuSubscriptions, new SkuDetailsResponseListener() {
|
||||
@Override
|
||||
public void onSkuDetailsResponse(BillingResult billingResult, final List<SkuDetails> skuDetailsListSubscriptions) {
|
||||
// Is it a failure?
|
||||
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
|
||||
logError("Failed to query subscriptipons sku details: " + billingResult.getResponseCode());
|
||||
notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage());
|
||||
stop(true);
|
||||
return;
|
||||
}
|
||||
|
||||
List<SkuDetails> skuDetailsList = new ArrayList<>(skuDetailsListInApps);
|
||||
skuDetailsList.addAll(skuDetailsListSubscriptions);
|
||||
InAppPurchaseHelper.this.skuDetailsList = skuDetailsList;
|
||||
|
||||
mSkuDetailsResponseListener.onSkuDetailsResponse(billingResult, skuDetailsList);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
for (Purchase purchase : purchases) {
|
||||
if (!purchase.isAcknowledged()) {
|
||||
onPurchaseFinished(purchase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPurchaseCanceled() {
|
||||
stop(true);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
logError("exec Error", e);
|
||||
|
@ -255,8 +328,11 @@ public class InAppPurchaseHelper {
|
|||
@Override
|
||||
public boolean run(InAppPurchaseHelper helper) {
|
||||
try {
|
||||
mHelper.launchPurchaseFlow(activity,
|
||||
getFullVersion().getSku(), RC_REQUEST, mPurchaseFinishedListener);
|
||||
SkuDetails skuDetails = getSkuDetails(getFullVersion().getSku());
|
||||
if (skuDetails == null) {
|
||||
throw new IllegalArgumentException("Cannot find sku details");
|
||||
}
|
||||
billingManager.initiatePurchaseFlow(activity, skuDetails);
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
complain("Cannot launch full version purchase!");
|
||||
|
@ -281,8 +357,11 @@ public class InAppPurchaseHelper {
|
|||
@Override
|
||||
public boolean run(InAppPurchaseHelper helper) {
|
||||
try {
|
||||
mHelper.launchPurchaseFlow(activity,
|
||||
getDepthContours().getSku(), RC_REQUEST, mPurchaseFinishedListener);
|
||||
SkuDetails skuDetails = getSkuDetails(getDepthContours().getSku());
|
||||
if (skuDetails == null) {
|
||||
throw new IllegalArgumentException("Cannot find sku details");
|
||||
}
|
||||
billingManager.initiatePurchaseFlow(activity, skuDetails);
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
complain("Cannot launch depth contours purchase!");
|
||||
|
@ -294,26 +373,74 @@ public class InAppPurchaseHelper {
|
|||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private SkuDetails getSkuDetails(@NonNull String sku) {
|
||||
List<SkuDetails> skuDetailsList = this.skuDetailsList;
|
||||
if (skuDetailsList != null) {
|
||||
for (SkuDetails details : skuDetailsList) {
|
||||
if (details.getSku().equals(sku)) {
|
||||
return details;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean hasDetails(@NonNull String sku) {
|
||||
return getSkuDetails(sku) != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Purchase getPurchase(@NonNull String sku) {
|
||||
BillingManager billingManager = this.billingManager;
|
||||
if (billingManager != null) {
|
||||
List<Purchase> purchases = billingManager.getPurchases();
|
||||
if (purchases != null) {
|
||||
for (Purchase p : purchases) {
|
||||
if (p.getSku().equals(sku)) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Listener that's called when we finish querying the items and subscriptions we own
|
||||
private QueryInventoryFinishedListener mGotInventoryListener = new QueryInventoryFinishedListener() {
|
||||
public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
|
||||
logDebug("Query inventory finished.");
|
||||
private SkuDetailsResponseListener mSkuDetailsResponseListener = new SkuDetailsResponseListener() {
|
||||
|
||||
@NonNull
|
||||
private List<String> getAllOwnedSubscriptionSkus() {
|
||||
List<String> result = new ArrayList<>();
|
||||
for (Purchase p : billingManager.getPurchases()) {
|
||||
InAppPurchase inAppPurchase = getInAppPurchases().getInAppPurchaseBySku(p.getSku());
|
||||
if (inAppPurchase instanceof InAppSubscription) {
|
||||
result.add(p.getSku());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
|
||||
|
||||
logDebug("Query sku details finished.");
|
||||
|
||||
// Have we been disposed of in the meantime? If so, quit.
|
||||
if (mHelper == null) {
|
||||
if (billingManager == null) {
|
||||
stop(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Is it a failure?
|
||||
if (result.isFailure()) {
|
||||
logError("Failed to query inventory: " + result);
|
||||
notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, result.getMessage());
|
||||
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
|
||||
logError("Failed to query inventory: " + billingResult.getResponseCode());
|
||||
notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage());
|
||||
stop(true);
|
||||
return;
|
||||
}
|
||||
|
||||
logDebug("Query inventory was successful.");
|
||||
logDebug("Query sku details was successful.");
|
||||
|
||||
/*
|
||||
* Check for items we own. Notice that for each purchase, we check
|
||||
|
@ -321,54 +448,64 @@ public class InAppPurchaseHelper {
|
|||
* verifyDeveloperPayload().
|
||||
*/
|
||||
|
||||
List<String> allOwnedSubscriptionSkus = inventory.getAllOwnedSkus(ITEM_TYPE_SUBS);
|
||||
List<String> allOwnedSubscriptionSkus = getAllOwnedSubscriptionSkus();
|
||||
for (InAppPurchase p : getLiveUpdates().getAllSubscriptions()) {
|
||||
if (inventory.hasDetails(p.getSku())) {
|
||||
Purchase purchase = inventory.getPurchase(p.getSku());
|
||||
SkuDetails liveUpdatesDetails = inventory.getSkuDetails(p.getSku());
|
||||
if (hasDetails(p.getSku())) {
|
||||
Purchase purchase = getPurchase(p.getSku());
|
||||
SkuDetails liveUpdatesDetails = getSkuDetails(p.getSku());
|
||||
if (liveUpdatesDetails != null) {
|
||||
fetchInAppPurchase(p, liveUpdatesDetails, purchase);
|
||||
}
|
||||
allOwnedSubscriptionSkus.remove(p.getSku());
|
||||
}
|
||||
}
|
||||
for (String sku : allOwnedSubscriptionSkus) {
|
||||
Purchase purchase = inventory.getPurchase(sku);
|
||||
SkuDetails liveUpdatesDetails = inventory.getSkuDetails(sku);
|
||||
Purchase purchase = getPurchase(sku);
|
||||
SkuDetails liveUpdatesDetails = getSkuDetails(sku);
|
||||
if (liveUpdatesDetails != null) {
|
||||
InAppSubscription s = getLiveUpdates().upgradeSubscription(sku);
|
||||
if (s == null) {
|
||||
s = new InAppPurchaseLiveUpdatesOldSubscription(liveUpdatesDetails);
|
||||
}
|
||||
fetchInAppPurchase(s, liveUpdatesDetails, purchase);
|
||||
}
|
||||
}
|
||||
|
||||
InAppPurchase fullVersion = getFullVersion();
|
||||
if (inventory.hasDetails(fullVersion.getSku())) {
|
||||
Purchase purchase = inventory.getPurchase(fullVersion.getSku());
|
||||
SkuDetails fullPriceDetails = inventory.getSkuDetails(fullVersion.getSku());
|
||||
if (hasDetails(fullVersion.getSku())) {
|
||||
Purchase purchase = getPurchase(fullVersion.getSku());
|
||||
SkuDetails fullPriceDetails = getSkuDetails(fullVersion.getSku());
|
||||
if (fullPriceDetails != null) {
|
||||
fetchInAppPurchase(fullVersion, fullPriceDetails, purchase);
|
||||
}
|
||||
}
|
||||
|
||||
InAppPurchase depthContours = getDepthContours();
|
||||
if (inventory.hasDetails(depthContours.getSku())) {
|
||||
Purchase purchase = inventory.getPurchase(depthContours.getSku());
|
||||
SkuDetails depthContoursDetails = inventory.getSkuDetails(depthContours.getSku());
|
||||
if (hasDetails(depthContours.getSku())) {
|
||||
Purchase purchase = getPurchase(depthContours.getSku());
|
||||
SkuDetails depthContoursDetails = getSkuDetails(depthContours.getSku());
|
||||
if (depthContoursDetails != null) {
|
||||
fetchInAppPurchase(depthContours, depthContoursDetails, purchase);
|
||||
}
|
||||
}
|
||||
|
||||
InAppPurchase contourLines = getContourLines();
|
||||
if (inventory.hasDetails(contourLines.getSku())) {
|
||||
Purchase purchase = inventory.getPurchase(contourLines.getSku());
|
||||
SkuDetails contourLinesDetails = inventory.getSkuDetails(contourLines.getSku());
|
||||
if (hasDetails(contourLines.getSku())) {
|
||||
Purchase purchase = getPurchase(contourLines.getSku());
|
||||
SkuDetails contourLinesDetails = getSkuDetails(contourLines.getSku());
|
||||
if (contourLinesDetails != null) {
|
||||
fetchInAppPurchase(contourLines, contourLinesDetails, purchase);
|
||||
}
|
||||
}
|
||||
|
||||
Purchase fullVersionPurchase = inventory.getPurchase(fullVersion.getSku());
|
||||
boolean fullVersionPurchased = (fullVersionPurchase != null && fullVersionPurchase.getPurchaseState() == 0);
|
||||
Purchase fullVersionPurchase = getPurchase(fullVersion.getSku());
|
||||
boolean fullVersionPurchased = fullVersionPurchase != null;
|
||||
if (fullVersionPurchased) {
|
||||
ctx.getSettings().FULL_VERSION_PURCHASED.set(true);
|
||||
}
|
||||
|
||||
Purchase depthContoursPurchase = inventory.getPurchase(depthContours.getSku());
|
||||
boolean depthContoursPurchased = (depthContoursPurchase != null && depthContoursPurchase.getPurchaseState() == 0);
|
||||
Purchase depthContoursPurchase = getPurchase(depthContours.getSku());
|
||||
boolean depthContoursPurchased = depthContoursPurchase != null;
|
||||
if (depthContoursPurchased) {
|
||||
ctx.getSettings().DEPTH_CONTOURS_PURCHASED.set(true);
|
||||
}
|
||||
|
@ -377,10 +514,10 @@ public class InAppPurchaseHelper {
|
|||
boolean subscribedToLiveUpdates = false;
|
||||
List<Purchase> liveUpdatesPurchases = new ArrayList<>();
|
||||
for (InAppPurchase p : getLiveUpdates().getAllSubscriptions()) {
|
||||
Purchase purchase = inventory.getPurchase(p.getSku());
|
||||
Purchase purchase = getPurchase(p.getSku());
|
||||
if (purchase != null) {
|
||||
liveUpdatesPurchases.add(purchase);
|
||||
if (!subscribedToLiveUpdates && purchase.getPurchaseState() == 0) {
|
||||
if (!subscribedToLiveUpdates) {
|
||||
subscribedToLiveUpdates = true;
|
||||
}
|
||||
}
|
||||
|
@ -453,8 +590,7 @@ public class InAppPurchaseHelper {
|
|||
|
||||
private void fetchInAppPurchase(@NonNull InAppPurchase inAppPurchase, @NonNull SkuDetails skuDetails, @Nullable Purchase purchase) {
|
||||
if (purchase != null) {
|
||||
inAppPurchase.setPurchaseState(purchase.getPurchaseState() == 0
|
||||
? PurchaseState.PURCHASED : PurchaseState.NOT_PURCHASED);
|
||||
inAppPurchase.setPurchaseState(PurchaseState.PURCHASED);
|
||||
inAppPurchase.setPurchaseTime(purchase.getPurchaseTime());
|
||||
} else {
|
||||
inAppPurchase.setPurchaseState(PurchaseState.NOT_PURCHASED);
|
||||
|
@ -563,10 +699,10 @@ public class InAppPurchaseHelper {
|
|||
public boolean run(InAppPurchaseHelper helper) {
|
||||
try {
|
||||
Activity a = activity.get();
|
||||
if (a != null) {
|
||||
mHelper.launchPurchaseFlow(a,
|
||||
sku, ITEM_TYPE_SUBS,
|
||||
RC_REQUEST, mPurchaseFinishedListener, payload);
|
||||
SkuDetails skuDetails = getSkuDetails(sku);
|
||||
if (a != null && skuDetails != null) {
|
||||
billingManager.setPayload(payload);
|
||||
billingManager.initiatePurchaseFlow(a, skuDetails);
|
||||
return false;
|
||||
} else {
|
||||
stop(true);
|
||||
|
@ -585,28 +721,6 @@ public class InAppPurchaseHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean onActivityResultHandled(int requestCode, int resultCode, Intent data) {
|
||||
logDebug("onActivityResult(" + requestCode + "," + resultCode + "," + data);
|
||||
if (mHelper == null) return false;
|
||||
|
||||
try {
|
||||
// Pass on the activity result to the helper for handling
|
||||
if (!mHelper.handleActivityResult(requestCode, resultCode, data)) {
|
||||
// not handled, so handle it ourselves (here's where you'd
|
||||
// perform any handling of activity results not related to in-app
|
||||
// billing...
|
||||
//super.onActivityResult(requestCode, resultCode, data);
|
||||
return false;
|
||||
} else {
|
||||
logDebug("onActivityResult handled by IABUtil.");
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logError("onActivityResultHandled", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private class RequestInventoryTask extends AsyncTask<Void, Void, String> {
|
||||
|
||||
|
@ -652,12 +766,8 @@ public class InAppPurchaseHelper {
|
|||
@Override
|
||||
public boolean run(InAppPurchaseHelper helper) {
|
||||
logDebug("Setup successful. Querying inventory.");
|
||||
Set<String> skus = new HashSet<>();
|
||||
for (InAppPurchase purchase : purchases.getAllInAppPurchases()) {
|
||||
skus.add(purchase.getSku());
|
||||
}
|
||||
try {
|
||||
mHelper.queryInventoryAsync(true, new ArrayList<>(skus), mGotInventoryListener);
|
||||
billingManager.queryPurchases();
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
logError("queryInventoryAsync Error", e);
|
||||
|
@ -679,23 +789,12 @@ public class InAppPurchaseHelper {
|
|||
parameters.put("aid", ctx.getUserAndroidId());
|
||||
}
|
||||
|
||||
// Callback for when a purchase is finished
|
||||
private OnIabPurchaseFinishedListener mPurchaseFinishedListener = new OnIabPurchaseFinishedListener() {
|
||||
public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
|
||||
logDebug("Purchase finished: " + result + ", purchase: " + purchase);
|
||||
// Call when a purchase is finished
|
||||
private void onPurchaseFinished(Purchase purchase) {
|
||||
logDebug("Purchase finished: " + purchase);
|
||||
|
||||
// if we were disposed of in the meantime, quit.
|
||||
if (mHelper == null) {
|
||||
stop(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.isFailure()) {
|
||||
if (result.getResponse() != IABHELPER_USER_CANCELLED) {
|
||||
complain("Error purchasing: " + result);
|
||||
}
|
||||
notifyDismissProgress(activeTask);
|
||||
notifyError(activeTask, "Error purchasing: " + result);
|
||||
if (billingManager == null) {
|
||||
stop(true);
|
||||
return;
|
||||
}
|
||||
|
@ -707,7 +806,7 @@ public class InAppPurchaseHelper {
|
|||
// bought live updates
|
||||
logDebug("Live updates subscription purchased.");
|
||||
final String sku = liveUpdatesPurchase.getSku();
|
||||
liveUpdatesPurchase.setPurchaseState(purchase.getPurchaseState() == 0 ? PurchaseState.PURCHASED : PurchaseState.NOT_PURCHASED);
|
||||
liveUpdatesPurchase.setPurchaseState(PurchaseState.PURCHASED);
|
||||
sendTokens(Collections.singletonList(purchase), new OnRequestResultListener() {
|
||||
@Override
|
||||
public void onResult(String result) {
|
||||
|
@ -727,7 +826,7 @@ public class InAppPurchaseHelper {
|
|||
|
||||
} else if (purchase.getSku().equals(getFullVersion().getSku())) {
|
||||
// bought full version
|
||||
getFullVersion().setPurchaseState(purchase.getPurchaseState() == 0 ? PurchaseState.PURCHASED : PurchaseState.NOT_PURCHASED);
|
||||
getFullVersion().setPurchaseState(PurchaseState.PURCHASED);
|
||||
logDebug("Full version purchased.");
|
||||
showToast(ctx.getString(R.string.full_version_thanks));
|
||||
ctx.getSettings().FULL_VERSION_PURCHASED.set(true);
|
||||
|
@ -738,7 +837,7 @@ public class InAppPurchaseHelper {
|
|||
|
||||
} else if (purchase.getSku().equals(getDepthContours().getSku())) {
|
||||
// bought sea depth contours
|
||||
getDepthContours().setPurchaseState(purchase.getPurchaseState() == 0 ? PurchaseState.PURCHASED : PurchaseState.NOT_PURCHASED);
|
||||
getDepthContours().setPurchaseState(PurchaseState.PURCHASED);
|
||||
logDebug("Sea depth contours purchased.");
|
||||
showToast(ctx.getString(R.string.sea_depth_thanks));
|
||||
ctx.getSettings().DEPTH_CONTOURS_PURCHASED.set(true);
|
||||
|
@ -753,7 +852,6 @@ public class InAppPurchaseHelper {
|
|||
stop(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Do not forget call stop() when helper is not needed anymore
|
||||
public void stop() {
|
||||
|
@ -762,14 +860,14 @@ public class InAppPurchaseHelper {
|
|||
|
||||
private void stop(boolean taskDone) {
|
||||
logDebug("Destroying helper.");
|
||||
if (mHelper != null) {
|
||||
if (billingManager != null) {
|
||||
if (taskDone) {
|
||||
processingTask = false;
|
||||
}
|
||||
if (!processingTask) {
|
||||
activeTask = null;
|
||||
mHelper.dispose();
|
||||
mHelper = null;
|
||||
billingManager.destroy();
|
||||
billingManager = null;
|
||||
}
|
||||
} else {
|
||||
processingTask = false;
|
||||
|
@ -793,7 +891,7 @@ public class InAppPurchaseHelper {
|
|||
Map<String, String> parameters = new HashMap<>();
|
||||
parameters.put("userid", userId);
|
||||
parameters.put("sku", purchase.getSku());
|
||||
parameters.put("purchaseToken", purchase.getToken());
|
||||
parameters.put("purchaseToken", purchase.getPurchaseToken());
|
||||
parameters.put("email", email);
|
||||
parameters.put("token", token);
|
||||
addUserInfo(parameters);
|
||||
|
|
|
@ -7,10 +7,11 @@ import android.text.Spannable;
|
|||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.StyleSpan;
|
||||
|
||||
import com.android.billingclient.api.SkuDetails;
|
||||
|
||||
import net.osmand.plus.OsmandApplication;
|
||||
import net.osmand.plus.R;
|
||||
import net.osmand.plus.Version;
|
||||
import net.osmand.plus.inapp.util.SkuDetails;
|
||||
import net.osmand.util.Algorithms;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
|
@ -122,15 +123,21 @@ public class InAppPurchases {
|
|||
return liveUpdates;
|
||||
}
|
||||
|
||||
public List<InAppPurchase> getAllInAppPurchases() {
|
||||
public List<InAppPurchase> getAllInAppPurchases(boolean includeSubscriptions) {
|
||||
List<InAppPurchase> purchases = new ArrayList<>();
|
||||
purchases.add(fullVersion);
|
||||
purchases.add(depthContours);
|
||||
purchases.add(contourLines);
|
||||
if (includeSubscriptions) {
|
||||
purchases.addAll(liveUpdates.getAllSubscriptions());
|
||||
}
|
||||
return purchases;
|
||||
}
|
||||
|
||||
public List<InAppSubscription> getAllInAppSubscriptions() {
|
||||
return liveUpdates.getAllSubscriptions();
|
||||
}
|
||||
|
||||
public boolean isFullVersion(String sku) {
|
||||
return FULL_VERSION.getSku().equals(sku);
|
||||
}
|
||||
|
|
420
OsmAnd/src/net/osmand/plus/inapp/util/BillingManager.java
Normal file
420
OsmAnd/src/net/osmand/plus/inapp/util/BillingManager.java
Normal file
|
@ -0,0 +1,420 @@
|
|||
package net.osmand.plus.inapp.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.android.billingclient.api.AcknowledgePurchaseParams;
|
||||
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
|
||||
import com.android.billingclient.api.BillingClient;
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode;
|
||||
import com.android.billingclient.api.BillingClient.FeatureType;
|
||||
import com.android.billingclient.api.BillingClient.SkuType;
|
||||
import com.android.billingclient.api.BillingClientStateListener;
|
||||
import com.android.billingclient.api.BillingFlowParams;
|
||||
import com.android.billingclient.api.BillingResult;
|
||||
import com.android.billingclient.api.ConsumeParams;
|
||||
import com.android.billingclient.api.ConsumeResponseListener;
|
||||
import com.android.billingclient.api.Purchase;
|
||||
import com.android.billingclient.api.Purchase.PurchasesResult;
|
||||
import com.android.billingclient.api.PurchasesUpdatedListener;
|
||||
import com.android.billingclient.api.SkuDetails;
|
||||
import com.android.billingclient.api.SkuDetailsParams;
|
||||
import com.android.billingclient.api.SkuDetailsResponseListener;
|
||||
|
||||
import net.osmand.PlatformUtil;
|
||||
import net.osmand.util.Algorithms;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Handles all the interactions with Play Store (via Billing library), maintains connection to
|
||||
* it through BillingClient and caches temporary states/data if needed
|
||||
*/
|
||||
public class BillingManager implements PurchasesUpdatedListener {
|
||||
// Default value of mBillingClientResponseCode until BillingManager was not yeat initialized
|
||||
public static final int BILLING_MANAGER_NOT_INITIALIZED = -1;
|
||||
|
||||
private static final org.apache.commons.logging.Log LOG = PlatformUtil.getLog(BillingManager.class);
|
||||
private static final String TAG = "BillingManager";
|
||||
|
||||
/**
|
||||
* A reference to BillingClient
|
||||
**/
|
||||
private BillingClient mBillingClient;
|
||||
|
||||
/**
|
||||
* True if billing service is connected now.
|
||||
*/
|
||||
private boolean mIsServiceConnected;
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
// Public key for verifying signature, in base64 encoding
|
||||
private String mSignatureBase64;
|
||||
private String mPayload;
|
||||
|
||||
private final BillingUpdatesListener mBillingUpdatesListener;
|
||||
private final List<Purchase> mPurchases = new ArrayList<>();
|
||||
private Set<String> mTokensToBeConsumed;
|
||||
|
||||
private int mBillingClientResponseCode = BILLING_MANAGER_NOT_INITIALIZED;
|
||||
private String mBillingClientResponseMessage;
|
||||
|
||||
|
||||
/**
|
||||
* Listener to the updates that happen when purchases list was updated or consumption of the
|
||||
* item was finished
|
||||
*/
|
||||
public interface BillingUpdatesListener {
|
||||
void onBillingClientSetupFinished();
|
||||
|
||||
void onConsumeFinished(String token, BillingResult billingResult);
|
||||
|
||||
void onPurchasesUpdated(List<Purchase> purchases);
|
||||
|
||||
void onPurchaseCanceled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for the Billing client state to become connected
|
||||
*/
|
||||
public interface ServiceConnectedListener {
|
||||
void onServiceConnected(BillingResult billingResult);
|
||||
}
|
||||
|
||||
public BillingManager(@NonNull final Context context, @NonNull final String base64PublicKey,
|
||||
@NonNull final BillingUpdatesListener updatesListener) {
|
||||
LOG.debug("Creating Billing client.");
|
||||
mContext = context;
|
||||
mSignatureBase64 = base64PublicKey;
|
||||
mBillingUpdatesListener = updatesListener;
|
||||
mBillingClient = BillingClient.newBuilder(mContext)
|
||||
.enablePendingPurchases()
|
||||
.setListener(this)
|
||||
.build();
|
||||
|
||||
LOG.debug("Starting setup.");
|
||||
|
||||
// Start setup. This is asynchronous and the specified listener will be called
|
||||
// once setup completes.
|
||||
// It also starts to report all the new purchases through onPurchasesUpdated() callback.
|
||||
startServiceConnection(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a callback that purchases were updated from the Billing library
|
||||
*/
|
||||
@Override
|
||||
public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchases) {
|
||||
int responseCode = billingResult.getResponseCode();
|
||||
if (responseCode == BillingResponseCode.OK) {
|
||||
if (purchases != null) {
|
||||
for (Purchase purchase : purchases) {
|
||||
handlePurchase(purchase);
|
||||
}
|
||||
} else {
|
||||
LOG.info("onPurchasesUpdated() - no purchases");
|
||||
}
|
||||
mBillingUpdatesListener.onPurchasesUpdated(mPurchases);
|
||||
} else if (responseCode == BillingResponseCode.USER_CANCELED) {
|
||||
LOG.info("onPurchasesUpdated() - user cancelled the purchase flow - skipping");
|
||||
mBillingUpdatesListener.onPurchaseCanceled();
|
||||
} else {
|
||||
LOG.warn("onPurchasesUpdated() got unknown responseCode: " + responseCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a purchase flow
|
||||
*/
|
||||
public void initiatePurchaseFlow(final Activity activity, final SkuDetails skuDetails) {
|
||||
initiatePurchaseFlow(activity, skuDetails, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a purchase or subscription replace flow
|
||||
*/
|
||||
public void initiatePurchaseFlow(final Activity activity, final SkuDetails skuDetails, final String oldSku) {
|
||||
Runnable purchaseFlowRequest = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
LOG.debug("Launching in-app purchase flow. Replace old SKU? " + (oldSku != null));
|
||||
BillingFlowParams purchaseParams = BillingFlowParams.newBuilder().setSkuDetails(skuDetails).setOldSku(oldSku).build();
|
||||
mBillingClient.launchBillingFlow(activity, purchaseParams);
|
||||
}
|
||||
};
|
||||
|
||||
executeServiceRequest(purchaseFlowRequest);
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the resources
|
||||
*/
|
||||
public void destroy() {
|
||||
LOG.debug("Destroying the manager.");
|
||||
|
||||
if (mBillingClient != null && mBillingClient.isReady()) {
|
||||
mBillingClient.endConnection();
|
||||
mBillingClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void querySkuDetailsAsync(@SkuType final String itemType, final List<String> skuList,
|
||||
final SkuDetailsResponseListener listener) {
|
||||
// Creating a runnable from the request to use it inside our connection retry policy below
|
||||
Runnable queryRequest = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Query the purchase async
|
||||
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
|
||||
params.setSkusList(skuList).setType(itemType);
|
||||
mBillingClient.querySkuDetailsAsync(params.build(),
|
||||
new SkuDetailsResponseListener() {
|
||||
@Override
|
||||
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> skuDetailsList) {
|
||||
listener.onSkuDetailsResponse(billingResult, skuDetailsList);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
executeServiceRequest(queryRequest);
|
||||
}
|
||||
|
||||
public void consumeAsync(final ConsumeParams consumeParams) {
|
||||
// If we've already scheduled to consume this token - no action is needed (this could happen
|
||||
// if you received the token when querying purchases inside onReceive() and later from
|
||||
// onActivityResult()
|
||||
final String purchaseToken = consumeParams.getPurchaseToken();
|
||||
if (mTokensToBeConsumed == null) {
|
||||
mTokensToBeConsumed = new HashSet<>();
|
||||
} else if (mTokensToBeConsumed.contains(purchaseToken)) {
|
||||
LOG.info("Token was already scheduled to be consumed - skipping...");
|
||||
return;
|
||||
}
|
||||
mTokensToBeConsumed.add(purchaseToken);
|
||||
|
||||
// Generating Consume Response listener
|
||||
final ConsumeResponseListener onConsumeListener = new ConsumeResponseListener() {
|
||||
@Override
|
||||
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
|
||||
// If billing service was disconnected, we try to reconnect 1 time
|
||||
// (feel free to introduce your retry policy here).
|
||||
mBillingUpdatesListener.onConsumeFinished(purchaseToken, billingResult);
|
||||
}
|
||||
};
|
||||
|
||||
// Creating a runnable from the request to use it inside our connection retry policy below
|
||||
Runnable consumeRequest = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Consume the purchase async
|
||||
mBillingClient.consumeAsync(consumeParams, onConsumeListener);
|
||||
}
|
||||
};
|
||||
|
||||
executeServiceRequest(consumeRequest);
|
||||
}
|
||||
|
||||
public boolean isIsServiceConnected() {
|
||||
return mIsServiceConnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value Billing client response code or BILLING_MANAGER_NOT_INITIALIZED if the
|
||||
* client connection response was not received yet.
|
||||
*/
|
||||
public int getBillingClientResponseCode() {
|
||||
return mBillingClientResponseCode;
|
||||
}
|
||||
|
||||
public String getBillingClientResponseMessage() {
|
||||
return mBillingClientResponseMessage;
|
||||
}
|
||||
|
||||
public List<Purchase> getPurchases() {
|
||||
return Collections.unmodifiableList(mPurchases);
|
||||
}
|
||||
|
||||
|
||||
public String getPayload() {
|
||||
return mPayload;
|
||||
}
|
||||
|
||||
public void setPayload(String payload) {
|
||||
this.mPayload = payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the purchase
|
||||
* <p>Note: Notice that for each purchase, we check if signature is valid on the client.
|
||||
* It's recommended to move this check into your backend.
|
||||
* See {@link Security#verifyPurchase(String, String, String)}
|
||||
* </p>
|
||||
*
|
||||
* @param purchase Purchase to be handled
|
||||
*/
|
||||
private void handlePurchase(final Purchase purchase) {
|
||||
if (!verifyValidSignature(purchase.getOriginalJson(), purchase.getSignature())) {
|
||||
LOG.info("Got a purchase: " + purchase + ", but signature is bad. Skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
|
||||
// Acknowledge the purchase if it hasn't already been acknowledged.
|
||||
if (!purchase.isAcknowledged()) {
|
||||
AcknowledgePurchaseParams.Builder builder =
|
||||
AcknowledgePurchaseParams.newBuilder()
|
||||
.setPurchaseToken(purchase.getPurchaseToken());
|
||||
if (!Algorithms.isEmpty(mPayload)) {
|
||||
builder.setDeveloperPayload(mPayload);
|
||||
}
|
||||
AcknowledgePurchaseParams acknowledgePurchaseParams = builder.build();
|
||||
mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() {
|
||||
@Override
|
||||
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
|
||||
if (billingResult.getResponseCode() != BillingResponseCode.OK) {
|
||||
LOG.info("Acknowledge a purchase: " + purchase + " failed (" + billingResult.getResponseCode() + "). " + billingResult.getDebugMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) {
|
||||
LOG.info("Got a purchase: " + purchase + ", but purchase state is pending. Skipping...");
|
||||
return;
|
||||
} else {
|
||||
LOG.info("Got a purchase: " + purchase + ", but purchase state is " + purchase.getPurchaseState() + ". Skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Got a verified purchase: " + purchase);
|
||||
|
||||
mPurchases.add(purchase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a result from querying of purchases and report an updated list to the listener
|
||||
*/
|
||||
private void onQueryPurchasesFinished(PurchasesResult result) {
|
||||
// Have we been disposed of in the meantime? If so, or bad result code, then quit
|
||||
if (mBillingClient == null || result.getResponseCode() != BillingResponseCode.OK) {
|
||||
LOG.warn("Billing client was null or result code (" + result.getResponseCode()
|
||||
+ ") was bad - quitting");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Query inventory was successful.");
|
||||
|
||||
// Update the UI and purchases inventory with new list of purchases
|
||||
mPurchases.clear();
|
||||
onPurchasesUpdated(result.getBillingResult(), result.getPurchasesList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if subscriptions are supported for current client
|
||||
* <p>Note: This method does not automatically retry for RESULT_SERVICE_DISCONNECTED.
|
||||
* It is only used in unit tests and after queryPurchases execution, which already has
|
||||
* a retry-mechanism implemented.
|
||||
* </p>
|
||||
*/
|
||||
public boolean areSubscriptionsSupported() {
|
||||
int responseCode = mBillingClient.isFeatureSupported(FeatureType.SUBSCRIPTIONS).getResponseCode();
|
||||
if (responseCode != BillingResponseCode.OK) {
|
||||
LOG.warn("areSubscriptionsSupported() got an error response: " + responseCode);
|
||||
}
|
||||
return responseCode == BillingResponseCode.OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query purchases across various use cases and deliver the result in a formalized way through
|
||||
* a listener
|
||||
*/
|
||||
public void queryPurchases() {
|
||||
Runnable queryToExecute = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
long time = System.currentTimeMillis();
|
||||
PurchasesResult purchasesResult = mBillingClient.queryPurchases(SkuType.INAPP);
|
||||
LOG.info("Querying purchases elapsed time: " + (System.currentTimeMillis() - time)
|
||||
+ "ms");
|
||||
// If there are subscriptions supported, we add subscription rows as well
|
||||
if (areSubscriptionsSupported()) {
|
||||
PurchasesResult subscriptionResult = mBillingClient.queryPurchases(SkuType.SUBS);
|
||||
LOG.info("Querying purchases and subscriptions elapsed time: "
|
||||
+ (System.currentTimeMillis() - time) + "ms");
|
||||
LOG.info("Querying subscriptions result code: "
|
||||
+ subscriptionResult.getResponseCode()
|
||||
+ " res: " + subscriptionResult.getPurchasesList().size());
|
||||
|
||||
if (subscriptionResult.getResponseCode() == BillingResponseCode.OK) {
|
||||
purchasesResult.getPurchasesList().addAll(
|
||||
subscriptionResult.getPurchasesList());
|
||||
} else {
|
||||
LOG.error("Got an error response trying to query subscription purchases");
|
||||
}
|
||||
} else if (purchasesResult.getResponseCode() == BillingResponseCode.OK) {
|
||||
LOG.info("Skipped subscription purchases query since they are not supported");
|
||||
} else {
|
||||
LOG.warn("queryPurchases() got an error response code: "
|
||||
+ purchasesResult.getResponseCode());
|
||||
}
|
||||
onQueryPurchasesFinished(purchasesResult);
|
||||
}
|
||||
};
|
||||
|
||||
executeServiceRequest(queryToExecute);
|
||||
}
|
||||
|
||||
public void startServiceConnection(final Runnable executeOnSuccess) {
|
||||
mBillingClient.startConnection(new BillingClientStateListener() {
|
||||
@Override
|
||||
public void onBillingSetupFinished(BillingResult billingResult) {
|
||||
|
||||
int billingResponseCode = billingResult.getResponseCode();
|
||||
LOG.debug("Setup finished. Response code: " + billingResponseCode);
|
||||
|
||||
mIsServiceConnected = billingResponseCode == BillingResponseCode.OK;
|
||||
mBillingClientResponseCode = billingResponseCode;
|
||||
mBillingClientResponseMessage = billingResult.getDebugMessage();
|
||||
mBillingUpdatesListener.onBillingClientSetupFinished();
|
||||
|
||||
if (mIsServiceConnected) {
|
||||
if (executeOnSuccess != null) {
|
||||
executeOnSuccess.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBillingServiceDisconnected() {
|
||||
mIsServiceConnected = false;
|
||||
mBillingUpdatesListener.onBillingClientSetupFinished();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void executeServiceRequest(Runnable runnable) {
|
||||
if (mIsServiceConnected) {
|
||||
runnable.run();
|
||||
} else {
|
||||
// If billing service was disconnected, we try to reconnect 1 time.
|
||||
startServiceConnection(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verifyValidSignature(String signedData, String signature) {
|
||||
return Security.verifyPurchase(mSignatureBase64, signedData, signature);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
/* Copyright (c) 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.osmand.plus.inapp.util;
|
||||
|
||||
/**
|
||||
* Exception thrown when something went wrong with in-app billing.
|
||||
* An IabException has an associated IabResult (an error).
|
||||
* To get the IAB result that caused this exception to be thrown,
|
||||
* call {@link #getResult()}.
|
||||
*/
|
||||
public class IabException extends Exception {
|
||||
IabResult mResult;
|
||||
|
||||
public IabException(IabResult r) {
|
||||
this(r, null);
|
||||
}
|
||||
public IabException(int response, String message) {
|
||||
this(new IabResult(response, message));
|
||||
}
|
||||
public IabException(IabResult r, Exception cause) {
|
||||
super(r.getMessage(), cause);
|
||||
mResult = r;
|
||||
}
|
||||
public IabException(int response, String message, Exception cause) {
|
||||
this(new IabResult(response, message), cause);
|
||||
}
|
||||
|
||||
/** Returns the IAB result (error) that this exception signals. */
|
||||
public IabResult getResult() { return mResult; }
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,45 +0,0 @@
|
|||
/* Copyright (c) 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.osmand.plus.inapp.util;
|
||||
|
||||
/**
|
||||
* Represents the result of an in-app billing operation.
|
||||
* A result is composed of a response code (an integer) and possibly a
|
||||
* message (String). You can get those by calling
|
||||
* {@link #getResponse} and {@link #getMessage()}, respectively. You
|
||||
* can also inquire whether a result is a success or a failure by
|
||||
* calling {@link #isSuccess()} and {@link #isFailure()}.
|
||||
*/
|
||||
public class IabResult {
|
||||
int mResponse;
|
||||
String mMessage;
|
||||
|
||||
public IabResult(int response, String message) {
|
||||
mResponse = response;
|
||||
if (message == null || message.trim().length() == 0) {
|
||||
mMessage = IabHelper.getResponseDesc(response);
|
||||
}
|
||||
else {
|
||||
mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")";
|
||||
}
|
||||
}
|
||||
public int getResponse() { return mResponse; }
|
||||
public String getMessage() { return mMessage; }
|
||||
public boolean isSuccess() { return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK; }
|
||||
public boolean isFailure() { return !isSuccess(); }
|
||||
public String toString() { return "IabResult: " + getMessage(); }
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
/* Copyright (c) 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.osmand.plus.inapp.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Represents a block of information about in-app items.
|
||||
* An Inventory is returned by such methods as {@link IabHelper#queryInventory}.
|
||||
*/
|
||||
public class Inventory {
|
||||
Map<String,SkuDetails> mSkuMap = new HashMap<String,SkuDetails>();
|
||||
Map<String,Purchase> mPurchaseMap = new HashMap<String,Purchase>();
|
||||
|
||||
Inventory() { }
|
||||
|
||||
/** Returns the listing details for an in-app product. */
|
||||
public SkuDetails getSkuDetails(String sku) {
|
||||
return mSkuMap.get(sku);
|
||||
}
|
||||
|
||||
/** Returns purchase information for a given product, or null if there is no purchase. */
|
||||
public Purchase getPurchase(String sku) {
|
||||
return mPurchaseMap.get(sku);
|
||||
}
|
||||
|
||||
/** Returns whether or not there exists a purchase of the given product. */
|
||||
public boolean hasPurchase(String sku) {
|
||||
return mPurchaseMap.containsKey(sku);
|
||||
}
|
||||
|
||||
/** Return whether or not details about the given product are available. */
|
||||
public boolean hasDetails(String sku) {
|
||||
return mSkuMap.containsKey(sku);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase a purchase (locally) from the inventory, given its product ID. This just
|
||||
* modifies the Inventory object locally and has no effect on the server! This is
|
||||
* useful when you have an existing Inventory object which you know to be up to date,
|
||||
* and you have just consumed an item successfully, which means that erasing its
|
||||
* purchase data from the Inventory you already have is quicker than querying for
|
||||
* a new Inventory.
|
||||
*/
|
||||
public void erasePurchase(String sku) {
|
||||
if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku);
|
||||
}
|
||||
|
||||
/** Returns a list of all owned product IDs. */
|
||||
public List<String> getAllOwnedSkus() {
|
||||
return new ArrayList<String>(mPurchaseMap.keySet());
|
||||
}
|
||||
|
||||
/** Returns a list of all owned product IDs of a given type */
|
||||
public List<String> getAllOwnedSkus(String itemType) {
|
||||
List<String> result = new ArrayList<String>();
|
||||
for (Purchase p : mPurchaseMap.values()) {
|
||||
if (p.getItemType().equals(itemType)) result.add(p.getSku());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Returns a list of all purchases. */
|
||||
List<Purchase> getAllPurchases() {
|
||||
return new ArrayList<Purchase>(mPurchaseMap.values());
|
||||
}
|
||||
|
||||
void addSkuDetails(SkuDetails d) {
|
||||
mSkuMap.put(d.getSku(), d);
|
||||
}
|
||||
|
||||
void addPurchase(Purchase p) {
|
||||
mPurchaseMap.put(p.getSku(), p);
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
/* Copyright (c) 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.osmand.plus.inapp.util;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Represents an in-app billing purchase.
|
||||
*/
|
||||
public class Purchase {
|
||||
String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS
|
||||
String mOrderId;
|
||||
String mPackageName;
|
||||
String mSku;
|
||||
long mPurchaseTime;
|
||||
int mPurchaseState;
|
||||
String mDeveloperPayload;
|
||||
String mToken;
|
||||
String mOriginalJson;
|
||||
String mSignature;
|
||||
|
||||
public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
|
||||
mItemType = itemType;
|
||||
mOriginalJson = jsonPurchaseInfo;
|
||||
JSONObject o = new JSONObject(mOriginalJson);
|
||||
mOrderId = o.optString("orderId");
|
||||
mPackageName = o.optString("packageName");
|
||||
mSku = o.optString("productId");
|
||||
mPurchaseTime = o.optLong("purchaseTime");
|
||||
mPurchaseState = o.optInt("purchaseState");
|
||||
mDeveloperPayload = o.optString("developerPayload");
|
||||
mToken = o.optString("token", o.optString("purchaseToken"));
|
||||
mSignature = signature;
|
||||
}
|
||||
|
||||
public String getItemType() { return mItemType; }
|
||||
public String getOrderId() { return mOrderId; }
|
||||
public String getPackageName() { return mPackageName; }
|
||||
public String getSku() { return mSku; }
|
||||
public long getPurchaseTime() { return mPurchaseTime; }
|
||||
public int getPurchaseState() { return mPurchaseState; }
|
||||
public String getDeveloperPayload() { return mDeveloperPayload; }
|
||||
public String getToken() { return mToken; }
|
||||
public String getOriginalJson() { return mOriginalJson; }
|
||||
public String getSignature() { return mSignature; }
|
||||
|
||||
@Override
|
||||
public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; }
|
||||
}
|
|
@ -18,10 +18,6 @@ package net.osmand.plus.inapp.util;
|
|||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
/* Copyright (c) 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.osmand.plus.inapp.util;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Represents an in-app product's listing details.
|
||||
*/
|
||||
public class SkuDetails {
|
||||
String mItemType;
|
||||
String mSku;
|
||||
String mType;
|
||||
String mPrice;
|
||||
long mPriceAmountMicros;
|
||||
String mPriceCurrencyCode;
|
||||
String mSubscriptionPeriod;
|
||||
String mTitle;
|
||||
String mDescription;
|
||||
String mJson;
|
||||
|
||||
public SkuDetails(String jsonSkuDetails) throws JSONException {
|
||||
this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails);
|
||||
}
|
||||
|
||||
public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException {
|
||||
mItemType = itemType;
|
||||
mJson = jsonSkuDetails;
|
||||
JSONObject o = new JSONObject(mJson);
|
||||
mSku = o.optString("productId");
|
||||
mType = o.optString("type");
|
||||
mPrice = o.optString("price");
|
||||
mPriceAmountMicros = o.optLong("price_amount_micros");
|
||||
mPriceCurrencyCode = o.optString("price_currency_code");
|
||||
mSubscriptionPeriod = o.optString("subscriptionPeriod");
|
||||
mTitle = o.optString("title");
|
||||
mDescription = o.optString("description");
|
||||
}
|
||||
|
||||
public String getSku() { return mSku; }
|
||||
public String getType() { return mType; }
|
||||
public String getPrice() { return mPrice; }
|
||||
public long getPriceAmountMicros() {
|
||||
return mPriceAmountMicros;
|
||||
}
|
||||
public String getPriceCurrencyCode() {
|
||||
return mPriceCurrencyCode;
|
||||
}
|
||||
public String getSubscriptionPeriod() {
|
||||
return mSubscriptionPeriod;
|
||||
}
|
||||
public String getTitle() { return mTitle; }
|
||||
public String getDescription() { return mDescription; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SkuDetails:" + mJson;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue