Move purchases to google billing library

This commit is contained in:
max-klaus 2019-08-17 12:31:55 +03:00
parent cc382f5f74
commit 94fdabe624
12 changed files with 701 additions and 1509 deletions

View file

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

View file

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

View file

@ -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);

View file

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

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

View file

@ -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

View file

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

View file

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

View file

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

View file

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

View file

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