diff --git a/OsmAnd/build.gradle b/OsmAnd/build.gradle index c661bd9e07..42fd0a34af 100644 --- a/OsmAnd/build.gradle +++ b/OsmAnd/build.gradle @@ -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' diff --git a/OsmAnd/src/net/osmand/plus/activities/OsmandInAppPurchaseActivity.java b/OsmAnd/src/net/osmand/plus/activities/OsmandInAppPurchaseActivity.java index fc3f99b953..04aba266fd 100644 --- a/OsmAnd/src/net/osmand/plus/activities/OsmandInAppPurchaseActivity.java +++ b/OsmAnd/src/net/osmand/plus/activities/OsmandInAppPurchaseActivity.java @@ -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(); diff --git a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java index 6d0427fc1a..a1c04b63bc 100644 --- a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java +++ b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java @@ -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 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 purchases) { + + // Have we been disposed of in the meantime? If so, quit. + if (billingManager == null) { + stop(true); + return; + } + + if (activeTask == InAppPurchaseTaskType.REQUEST_INVENTORY) { + List 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 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 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 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 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 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 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 getAllOwnedSubscriptionSkus() { + List 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 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 allOwnedSubscriptionSkus = inventory.getAllOwnedSkus(ITEM_TYPE_SUBS); + List allOwnedSubscriptionSkus = getAllOwnedSubscriptionSkus(); for (InAppPurchase p : getLiveUpdates().getAllSubscriptions()) { - if (inventory.hasDetails(p.getSku())) { - Purchase purchase = inventory.getPurchase(p.getSku()); - SkuDetails liveUpdatesDetails = inventory.getSkuDetails(p.getSku()); - fetchInAppPurchase(p, liveUpdatesDetails, purchase); + 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); - InAppSubscription s = getLiveUpdates().upgradeSubscription(sku); - if (s == null) { - s = new InAppPurchaseLiveUpdatesOldSubscription(liveUpdatesDetails); + 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); } - fetchInAppPurchase(s, liveUpdatesDetails, purchase); } InAppPurchase fullVersion = getFullVersion(); - if (inventory.hasDetails(fullVersion.getSku())) { - Purchase purchase = inventory.getPurchase(fullVersion.getSku()); - SkuDetails fullPriceDetails = inventory.getSkuDetails(fullVersion.getSku()); - fetchInAppPurchase(fullVersion, fullPriceDetails, purchase); + 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()); - fetchInAppPurchase(depthContours, depthContoursDetails, purchase); + 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()); - fetchInAppPurchase(contourLines, contourLinesDetails, purchase); + 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 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 { @@ -652,12 +766,8 @@ public class InAppPurchaseHelper { @Override public boolean run(InAppPurchaseHelper helper) { logDebug("Setup successful. Querying inventory."); - Set 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,81 +789,69 @@ 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); - stop(true); - return; - } - - logDebug("Purchase successful."); - - InAppPurchase liveUpdatesPurchase = getLiveUpdates().getSubscriptionBySku(purchase.getSku()); - if (liveUpdatesPurchase != null) { - // bought live updates - logDebug("Live updates subscription purchased."); - final String sku = liveUpdatesPurchase.getSku(); - liveUpdatesPurchase.setPurchaseState(purchase.getPurchaseState() == 0 ? PurchaseState.PURCHASED : PurchaseState.NOT_PURCHASED); - sendTokens(Collections.singletonList(purchase), new OnRequestResultListener() { - @Override - public void onResult(String result) { - boolean active = ctx.getSettings().LIVE_UPDATES_PURCHASED.get(); - ctx.getSettings().LIVE_UPDATES_PURCHASED.set(true); - ctx.getSettings().getCustomRenderBooleanProperty("depthContours").set(true); - - ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_TIME.set(0L); - ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_FIRST_DLG_SHOWN.set(false); - ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_SECOND_DLG_SHOWN.set(false); - - notifyDismissProgress(InAppPurchaseTaskType.PURCHASE_LIVE_UPDATES); - notifyItemPurchased(sku, active); - stop(true); - } - }); - - } else if (purchase.getSku().equals(getFullVersion().getSku())) { - // bought full version - getFullVersion().setPurchaseState(purchase.getPurchaseState() == 0 ? PurchaseState.PURCHASED : PurchaseState.NOT_PURCHASED); - logDebug("Full version purchased."); - showToast(ctx.getString(R.string.full_version_thanks)); - ctx.getSettings().FULL_VERSION_PURCHASED.set(true); - - notifyDismissProgress(InAppPurchaseTaskType.PURCHASE_FULL_VERSION); - notifyItemPurchased(getFullVersion().getSku(), false); - stop(true); - - } else if (purchase.getSku().equals(getDepthContours().getSku())) { - // bought sea depth contours - getDepthContours().setPurchaseState(purchase.getPurchaseState() == 0 ? PurchaseState.PURCHASED : PurchaseState.NOT_PURCHASED); - logDebug("Sea depth contours purchased."); - showToast(ctx.getString(R.string.sea_depth_thanks)); - ctx.getSettings().DEPTH_CONTOURS_PURCHASED.set(true); - ctx.getSettings().getCustomRenderBooleanProperty("depthContours").set(true); - - notifyDismissProgress(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS); - notifyItemPurchased(getDepthContours().getSku(), false); - stop(true); - - } else { - notifyDismissProgress(activeTask); - stop(true); - } + // if we were disposed of in the meantime, quit. + if (billingManager == null) { + stop(true); + return; } - }; + + logDebug("Purchase successful."); + + InAppPurchase liveUpdatesPurchase = getLiveUpdates().getSubscriptionBySku(purchase.getSku()); + if (liveUpdatesPurchase != null) { + // bought live updates + logDebug("Live updates subscription purchased."); + final String sku = liveUpdatesPurchase.getSku(); + liveUpdatesPurchase.setPurchaseState(PurchaseState.PURCHASED); + sendTokens(Collections.singletonList(purchase), new OnRequestResultListener() { + @Override + public void onResult(String result) { + boolean active = ctx.getSettings().LIVE_UPDATES_PURCHASED.get(); + ctx.getSettings().LIVE_UPDATES_PURCHASED.set(true); + ctx.getSettings().getCustomRenderBooleanProperty("depthContours").set(true); + + ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_TIME.set(0L); + ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_FIRST_DLG_SHOWN.set(false); + ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_SECOND_DLG_SHOWN.set(false); + + notifyDismissProgress(InAppPurchaseTaskType.PURCHASE_LIVE_UPDATES); + notifyItemPurchased(sku, active); + stop(true); + } + }); + + } else if (purchase.getSku().equals(getFullVersion().getSku())) { + // bought full version + getFullVersion().setPurchaseState(PurchaseState.PURCHASED); + logDebug("Full version purchased."); + showToast(ctx.getString(R.string.full_version_thanks)); + ctx.getSettings().FULL_VERSION_PURCHASED.set(true); + + notifyDismissProgress(InAppPurchaseTaskType.PURCHASE_FULL_VERSION); + notifyItemPurchased(getFullVersion().getSku(), false); + stop(true); + + } else if (purchase.getSku().equals(getDepthContours().getSku())) { + // bought sea depth contours + getDepthContours().setPurchaseState(PurchaseState.PURCHASED); + logDebug("Sea depth contours purchased."); + showToast(ctx.getString(R.string.sea_depth_thanks)); + ctx.getSettings().DEPTH_CONTOURS_PURCHASED.set(true); + ctx.getSettings().getCustomRenderBooleanProperty("depthContours").set(true); + + notifyDismissProgress(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS); + notifyItemPurchased(getDepthContours().getSku(), false); + stop(true); + + } else { + notifyDismissProgress(activeTask); + 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 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); diff --git a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java index 24c25edfa1..a321d56eea 100644 --- a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java +++ b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java @@ -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 getAllInAppPurchases() { + public List getAllInAppPurchases(boolean includeSubscriptions) { List purchases = new ArrayList<>(); purchases.add(fullVersion); purchases.add(depthContours); purchases.add(contourLines); - purchases.addAll(liveUpdates.getAllSubscriptions()); + if (includeSubscriptions) { + purchases.addAll(liveUpdates.getAllSubscriptions()); + } return purchases; } + public List getAllInAppSubscriptions() { + return liveUpdates.getAllSubscriptions(); + } + public boolean isFullVersion(String sku) { return FULL_VERSION.getSku().equals(sku); } diff --git a/OsmAnd/src/net/osmand/plus/inapp/util/BillingManager.java b/OsmAnd/src/net/osmand/plus/inapp/util/BillingManager.java new file mode 100644 index 0000000000..f7fd8fe1ee --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/inapp/util/BillingManager.java @@ -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 mPurchases = new ArrayList<>(); + private Set 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 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 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 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 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 getPurchases() { + return Collections.unmodifiableList(mPurchases); + } + + + public String getPayload() { + return mPayload; + } + + public void setPayload(String payload) { + this.mPayload = payload; + } + + /** + * Handles the purchase + *

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)} + *

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

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. + *

+ */ + 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); + } +} + + diff --git a/OsmAnd/src/net/osmand/plus/inapp/util/IabException.java b/OsmAnd/src/net/osmand/plus/inapp/util/IabException.java deleted file mode 100644 index b52fef5bca..0000000000 --- a/OsmAnd/src/net/osmand/plus/inapp/util/IabException.java +++ /dev/null @@ -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; } -} \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/inapp/util/IabHelper.java b/OsmAnd/src/net/osmand/plus/inapp/util/IabHelper.java deleted file mode 100644 index 74171d67ca..0000000000 --- a/OsmAnd/src/net/osmand/plus/inapp/util/IabHelper.java +++ /dev/null @@ -1,1004 +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 android.app.Activity; -import android.app.PendingIntent; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentSender.SendIntentException; -import android.content.ServiceConnection; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.RemoteException; -import android.text.TextUtils; -import android.util.Log; - -import com.android.vending.billing.IInAppBillingService; - -import org.json.JSONException; - -import java.util.ArrayList; -import java.util.List; - - -/** - * Provides convenience methods for in-app billing. You can create one instance of this - * class for your application and use it to process in-app billing operations. - * It provides synchronous (blocking) and asynchronous (non-blocking) methods for - * many common in-app billing operations, as well as automatic signature - * verification. - * - * After instantiating, you must perform setup in order to start using the object. - * To perform setup, call the {@link #startSetup} method and provide a listener; - * that listener will be notified when setup is complete, after which (and not before) - * you may call other methods. - * - * After setup is complete, you will typically want to request an inventory of owned - * items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync} - * and related methods. - * - * When you are done with this object, don't forget to call {@link #dispose} - * to ensure proper cleanup. This object holds a binding to the in-app billing - * service, which will leak unless you dispose of it correctly. If you created - * the object on an Activity's onCreate method, then the recommended - * place to dispose of it is the Activity's onDestroy method. - * - * A note about threading: When using this object from a background thread, you may - * call the blocking versions of methods; when using from a UI thread, call - * only the asynchronous versions and handle the results via callbacks. - * Also, notice that you can only call one asynchronous operation at a time; - * attempting to start a second asynchronous operation while the first one - * has not yet completed will result in an exception being thrown. - * - * @author Bruno Oliveira (Google) - * - */ -public class IabHelper { - // Is debug logging enabled? - boolean mDebugLog = false; - String mDebugTag = "IabHelper"; - - // Is setup done? - boolean mSetupDone = false; - - // Has this object been disposed of? (If so, we should ignore callbacks, etc) - boolean mDisposed = false; - - // Are subscriptions supported? - boolean mSubscriptionsSupported = false; - - // Is an asynchronous operation in progress? - // (only one at a time can be in progress) - boolean mAsyncInProgress = false; - - // (for logging/debugging) - // if mAsyncInProgress == true, what asynchronous operation is in progress? - String mAsyncOperation = ""; - - // Context we were passed during initialization - Context mContext; - - // Connection to the service - IInAppBillingService mService; - ServiceConnection mServiceConn; - - // The request code used to launch purchase flow - int mRequestCode; - - // The item type of the current purchase flow - String mPurchasingItemType; - - // Public key for verifying signature, in base64 encoding - String mSignatureBase64 = null; - - // Billing response codes - public static final int BILLING_RESPONSE_RESULT_OK = 0; - public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1; - public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3; - public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4; - public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5; - public static final int BILLING_RESPONSE_RESULT_ERROR = 6; - public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7; - public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8; - - // IAB Helper error codes - public static final int IABHELPER_ERROR_BASE = -1000; - public static final int IABHELPER_REMOTE_EXCEPTION = -1001; - public static final int IABHELPER_BAD_RESPONSE = -1002; - public static final int IABHELPER_VERIFICATION_FAILED = -1003; - public static final int IABHELPER_SEND_INTENT_FAILED = -1004; - public static final int IABHELPER_USER_CANCELLED = -1005; - public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006; - public static final int IABHELPER_MISSING_TOKEN = -1007; - public static final int IABHELPER_UNKNOWN_ERROR = -1008; - public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009; - public static final int IABHELPER_INVALID_CONSUMPTION = -1010; - - // Keys for the responses from InAppBillingService - public static final String RESPONSE_CODE = "RESPONSE_CODE"; - public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST"; - public static final String RESPONSE_BUY_INTENT = "BUY_INTENT"; - public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA"; - public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE"; - public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST"; - public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST"; - public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST"; - public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN"; - - // Item types - public static final String ITEM_TYPE_INAPP = "inapp"; - public static final String ITEM_TYPE_SUBS = "subs"; - - // some fields on the getSkuDetails response bundle - public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST"; - public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST"; - - /** - * Creates an instance. After creation, it will not yet be ready to use. You must perform - * setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not - * block and is safe to call from a UI thread. - * - * @param ctx Your application or Activity context. Needed to bind to the in-app billing service. - * @param base64PublicKey Your application's public key, encoded in base64. - * This is used for verification of purchase signatures. You can find your app's base64-encoded - * public key in your application's page on Google Play Developer Console. Note that this - * is NOT your "developer public key". - */ - public IabHelper(Context ctx, String base64PublicKey) { - mContext = ctx.getApplicationContext(); - mSignatureBase64 = base64PublicKey; - logDebug("IAB helper created."); - } - - /** - * Enables or disable debug logging through LogCat. - */ - public void enableDebugLogging(boolean enable, String tag) { - checkNotDisposed(); - mDebugLog = enable; - mDebugTag = tag; - } - - public void enableDebugLogging(boolean enable) { - checkNotDisposed(); - mDebugLog = enable; - } - - /** - * Callback for setup process. This listener's {@link #onIabSetupFinished} method is called - * when the setup process is complete. - */ - public interface OnIabSetupFinishedListener { - /** - * Called to notify that setup is complete. - * - * @param result The result of the setup process. - */ - public void onIabSetupFinished(IabResult result); - } - - /** - * Starts the setup process. This will start up the setup process asynchronously. - * You will be notified through the listener when the setup process is complete. - * This method is safe to call from a UI thread. - * - * @param listener The listener to notify when the setup process is complete. - */ - public void startSetup(final OnIabSetupFinishedListener listener) { - // If already set up, can't do it again. - checkNotDisposed(); - if (mSetupDone) throw new IllegalStateException("IAB helper is already set up."); - - // Connection to IAB service - logDebug("Starting in-app billing setup."); - mServiceConn = new ServiceConnection() { - @Override - public void onServiceDisconnected(ComponentName name) { - logDebug("Billing service disconnected."); - mService = null; - } - - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - if (mDisposed) return; - logDebug("Billing service connected."); - mService = IInAppBillingService.Stub.asInterface(service); - String packageName = mContext.getPackageName(); - try { - logDebug("Checking for in-app billing 3 support."); - - // check for in-app billing v3 support - int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP); - if (response != BILLING_RESPONSE_RESULT_OK) { - if (listener != null) listener.onIabSetupFinished(new IabResult(response, - "Error checking for billing v3 support.")); - - // if in-app purchases aren't supported, neither are subscriptions. - mSubscriptionsSupported = false; - return; - } - logDebug("In-app billing version 3 supported for " + packageName); - - // check for v3 subscriptions support - response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS); - if (response == BILLING_RESPONSE_RESULT_OK) { - logDebug("Subscriptions AVAILABLE."); - mSubscriptionsSupported = true; - } - else { - logDebug("Subscriptions NOT AVAILABLE. Response: " + response); - } - - mSetupDone = true; - } - catch (RemoteException e) { - if (listener != null) { - listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION, - "RemoteException while setting up in-app billing.")); - } - e.printStackTrace(); - return; - } - - if (listener != null) { - listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful.")); - } - } - }; - - try { - Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND"); - serviceIntent.setPackage("com.android.vending"); - if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) { - // service available to handle that Intent - mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE); - } else { - // no service available to handle that Intent - mServiceConn = null; - if (listener != null) { - listener.onIabSetupFinished( - new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE, - "Billing service unavailable on device.")); - } - } - } catch (Exception e) { - if (listener != null) { - listener.onIabSetupFinished(new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE, - "InAppBillingService not available")); - } - } - } - - /** - * Dispose of object, releasing resources. It's very important to call this - * method when you are done with this object. It will release any resources - * used by it such as service connections. Naturally, once the object is - * disposed of, it can't be used again. - */ - public void dispose() { - logDebug("Disposing."); - mSetupDone = false; - if (mServiceConn != null) { - logDebug("Unbinding from service."); - try { - if (mContext != null) { - mContext.unbindService(mServiceConn); - } - } catch (Exception e) { - logError("Unbinding failed."); - } - } - mDisposed = true; - mContext = null; - mServiceConn = null; - mService = null; - mPurchaseListener = null; - } - - private void checkNotDisposed() { - if (mDisposed) throw new IllegalStateException("IabHelper was disposed of, so it cannot be used."); - } - - /** Returns whether subscriptions are supported. */ - public boolean subscriptionsSupported() { - checkNotDisposed(); - return mSubscriptionsSupported; - } - - - /** - * Callback that notifies when a purchase is finished. - */ - public interface OnIabPurchaseFinishedListener { - /** - * Called to notify that an in-app purchase finished. If the purchase was successful, - * then the sku parameter specifies which item was purchased. If the purchase failed, - * the sku and extraData parameters may or may not be null, depending on how far the purchase - * process went. - * - * @param result The result of the purchase. - * @param info The purchase information (null if purchase failed) - */ - public void onIabPurchaseFinished(IabResult result, Purchase info); - } - - // The listener registered on launchPurchaseFlow, which we have to call back when - // the purchase finishes - OnIabPurchaseFinishedListener mPurchaseListener; - - public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) { - launchPurchaseFlow(act, sku, requestCode, listener, ""); - } - - public void launchPurchaseFlow(Activity act, String sku, int requestCode, - OnIabPurchaseFinishedListener listener, String extraData) { - launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData); - } - - public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, - OnIabPurchaseFinishedListener listener) { - launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, ""); - } - - public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, - OnIabPurchaseFinishedListener listener, String extraData) { - launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData); - } - - /** - * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase, - * which will involve bringing up the Google Play screen. The calling activity will be paused while - * the user interacts with Google Play, and the result will be delivered via the activity's - * {@link android.app.Activity#onActivityResult} method, at which point you must call - * this object's {@link #handleActivityResult} method to continue the purchase flow. This method - * MUST be called from the UI thread of the Activity. - * - * @param act The calling activity. - * @param sku The sku of the item to purchase. - * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS) - * @param requestCode A request code (to differentiate from other responses -- - * as in {@link android.app.Activity#startActivityForResult}). - * @param listener The listener to notify when the purchase process finishes - * @param extraData Extra data (developer payload), which will be returned with the purchase data - * when the purchase completes. This extra data will be permanently bound to that purchase - * and will always be returned when the purchase is queried. - */ - public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode, - OnIabPurchaseFinishedListener listener, String extraData) { - checkNotDisposed(); - checkSetupDone("launchPurchaseFlow"); - flagStartAsync("launchPurchaseFlow"); - IabResult result; - - if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) { - IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE, - "Subscriptions are not available."); - flagEndAsync(); - if (listener != null) listener.onIabPurchaseFinished(r, null); - return; - } - - try { - logDebug("Constructing buy intent for " + sku + ", item type: " + itemType); - Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData); - int response = getResponseCodeFromBundle(buyIntentBundle); - if (response != BILLING_RESPONSE_RESULT_OK) { - logError("Unable to buy item, Error response: " + getResponseDesc(response)); - flagEndAsync(); - result = new IabResult(response, "Unable to buy item"); - if (listener != null) listener.onIabPurchaseFinished(result, null); - return; - } - - PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT); - logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode); - mRequestCode = requestCode; - mPurchaseListener = listener; - mPurchasingItemType = itemType; - act.startIntentSenderForResult(pendingIntent.getIntentSender(), - requestCode, new Intent(), - Integer.valueOf(0), Integer.valueOf(0), - Integer.valueOf(0)); - } - catch (SendIntentException e) { - logError("SendIntentException while launching purchase flow for sku " + sku); - e.printStackTrace(); - flagEndAsync(); - - result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent."); - if (listener != null) listener.onIabPurchaseFinished(result, null); - } - catch (RemoteException e) { - logError("RemoteException while launching purchase flow for sku " + sku); - e.printStackTrace(); - flagEndAsync(); - - result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow"); - if (listener != null) listener.onIabPurchaseFinished(result, null); - } - } - - /** - * Handles an activity result that's part of the purchase flow in in-app billing. If you - * are calling {@link #launchPurchaseFlow}, then you must call this method from your - * Activity's {@link android.app.Activity@onActivityResult} method. This method - * MUST be called from the UI thread of the Activity. - * - * @param requestCode The requestCode as you received it. - * @param resultCode The resultCode as you received it. - * @param data The data (Intent) as you received it. - * @return Returns true if the result was related to a purchase flow and was handled; - * false if the result was not related to a purchase, in which case you should - * handle it normally. - */ - public boolean handleActivityResult(int requestCode, int resultCode, Intent data) { - IabResult result; - if (requestCode != mRequestCode) return false; - - checkNotDisposed(); - checkSetupDone("handleActivityResult"); - - // end of async purchase operation that started on launchPurchaseFlow - flagEndAsync(); - - if (data == null) { - logError("Null data in IAB activity result."); - result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result"); - if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); - return true; - } - - int responseCode = getResponseCodeFromIntent(data); - String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA); - String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE); - - if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) { - logDebug("Successful resultcode from purchase activity."); - logDebug("Purchase data: " + purchaseData); - logDebug("Data signature: " + dataSignature); - logDebug("Extras: " + data.getExtras()); - logDebug("Expected item type: " + mPurchasingItemType); - - if (purchaseData == null || dataSignature == null) { - logError("BUG: either purchaseData or dataSignature is null."); - logDebug("Extras: " + data.getExtras().toString()); - result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature"); - if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); - return true; - } - - Purchase purchase = null; - try { - purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature); - String sku = purchase.getSku(); - - // Verify signature - if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) { - logError("Purchase signature verification FAILED for sku " + sku); - result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku); - if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase); - return true; - } - logDebug("Purchase signature successfully verified."); - } - catch (JSONException e) { - logError("Failed to parse purchase data."); - e.printStackTrace(); - result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data."); - if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); - return true; - } - - if (mPurchaseListener != null) { - mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase); - } - } - else if (resultCode == Activity.RESULT_OK) { - // result code was OK, but in-app billing response was not OK. - logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode)); - if (mPurchaseListener != null) { - result = new IabResult(responseCode, "Problem purchashing item."); - mPurchaseListener.onIabPurchaseFinished(result, null); - } - } - else if (resultCode == Activity.RESULT_CANCELED) { - logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode)); - result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled."); - if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); - } - else { - logError("Purchase failed. Result code: " + Integer.toString(resultCode) - + ". Response: " + getResponseDesc(responseCode)); - result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response."); - if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); - } - return true; - } - - public Inventory queryInventory(boolean querySkuDetails, List moreSkus) throws IabException { - return queryInventory(querySkuDetails, moreSkus, null); - } - - /** - * Queries the inventory. This will query all owned items from the server, as well as - * information on additional skus, if specified. This method may block or take long to execute. - * Do not call from a UI thread. For that, use the non-blocking version {@link #refreshInventoryAsync}. - * - * @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well - * as purchase information. - * @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership. - * Ignored if null or if querySkuDetails is false. - * @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership. - * Ignored if null or if querySkuDetails is false. - * @throws IabException if a problem occurs while refreshing the inventory. - */ - public Inventory queryInventory(boolean querySkuDetails, List moreItemSkus, - List moreSubsSkus) throws IabException { - checkNotDisposed(); - checkSetupDone("queryInventory"); - try { - Inventory inv = new Inventory(); - int r = queryPurchases(inv, ITEM_TYPE_INAPP); - if (r != BILLING_RESPONSE_RESULT_OK) { - throw new IabException(r, "Error refreshing inventory (querying owned items)."); - } - - if (querySkuDetails) { - r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus); - if (r != BILLING_RESPONSE_RESULT_OK) { - throw new IabException(r, "Error refreshing inventory (querying prices of items)."); - } - } - - // if subscriptions are supported, then also query for subscriptions - if (mSubscriptionsSupported) { - r = queryPurchases(inv, ITEM_TYPE_SUBS); - if (r != BILLING_RESPONSE_RESULT_OK) { - throw new IabException(r, "Error refreshing inventory (querying owned subscriptions)."); - } - - if (querySkuDetails) { - r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus); - if (r != BILLING_RESPONSE_RESULT_OK) { - throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions)."); - } - } - } - - return inv; - } - catch (RemoteException e) { - throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e); - } - catch (JSONException e) { - throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e); - } - } - - /** - * Listener that notifies when an inventory query operation completes. - */ - public interface QueryInventoryFinishedListener { - /** - * Called to notify that an inventory query operation completed. - * - * @param result The result of the operation. - * @param inv The inventory. - */ - public void onQueryInventoryFinished(IabResult result, Inventory inv); - } - - - /** - * Asynchronous wrapper for inventory query. This will perform an inventory - * query as described in {@link #queryInventory}, but will do so asynchronously - * and call back the specified listener upon completion. This method is safe to - * call from a UI thread. - * - * @param querySkuDetails as in {@link #queryInventory} - * @param moreSkus as in {@link #queryInventory} - * @param listener The listener to notify when the refresh operation completes. - */ - public void queryInventoryAsync(final boolean querySkuDetails, - final List moreSkus, - final QueryInventoryFinishedListener listener) { - final Handler handler = new Handler(); - checkNotDisposed(); - checkSetupDone("queryInventory"); - flagStartAsync("refresh inventory"); - (new Thread(new Runnable() { - public void run() { - IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful."); - Inventory inv = null; - try { - inv = queryInventory(querySkuDetails, moreSkus); - } - catch (IabException ex) { - result = ex.getResult(); - } - - flagEndAsync(); - - final IabResult result_f = result; - final Inventory inv_f = inv; - if (!mDisposed && listener != null) { - handler.post(new Runnable() { - public void run() { - listener.onQueryInventoryFinished(result_f, inv_f); - } - }); - } - } - })).start(); - } - - public void queryInventoryAsync(QueryInventoryFinishedListener listener) { - queryInventoryAsync(true, null, listener); - } - - public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) { - queryInventoryAsync(querySkuDetails, null, listener); - } - - - /** - * Consumes a given in-app product. Consuming can only be done on an item - * that's owned, and as a result of consumption, the user will no longer own it. - * This method may block or take long to return. Do not call from the UI thread. - * For that, see {@link #consumeAsync}. - * - * @param itemInfo The PurchaseInfo that represents the item to consume. - * @throws IabException if there is a problem during consumption. - */ - void consume(Purchase itemInfo) throws IabException { - checkNotDisposed(); - checkSetupDone("consume"); - - if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) { - throw new IabException(IABHELPER_INVALID_CONSUMPTION, - "Items of type '" + itemInfo.mItemType + "' can't be consumed."); - } - - try { - String token = itemInfo.getToken(); - String sku = itemInfo.getSku(); - if (token == null || token.equals("")) { - logError("Can't consume "+ sku + ". No token."); - throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: " - + sku + " " + itemInfo); - } - - logDebug("Consuming sku: " + sku + ", token: " + token); - int response = mService.consumePurchase(3, mContext.getPackageName(), token); - if (response == BILLING_RESPONSE_RESULT_OK) { - logDebug("Successfully consumed sku: " + sku); - } - else { - logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response)); - throw new IabException(response, "Error consuming sku " + sku); - } - } - catch (RemoteException e) { - throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e); - } - } - - /** - * Callback that notifies when a consumption operation finishes. - */ - public interface OnConsumeFinishedListener { - /** - * Called to notify that a consumption has finished. - * - * @param purchase The purchase that was (or was to be) consumed. - * @param result The result of the consumption operation. - */ - public void onConsumeFinished(Purchase purchase, IabResult result); - } - - /** - * Callback that notifies when a multi-item consumption operation finishes. - */ - public interface OnConsumeMultiFinishedListener { - /** - * Called to notify that a consumption of multiple items has finished. - * - * @param purchases The purchases that were (or were to be) consumed. - * @param results The results of each consumption operation, corresponding to each - * sku. - */ - public void onConsumeMultiFinished(List purchases, List results); - } - - /** - * Asynchronous wrapper to item consumption. Works like {@link #consume}, but - * performs the consumption in the background and notifies completion through - * the provided listener. This method is safe to call from a UI thread. - * - * @param purchase The purchase to be consumed. - * @param listener The listener to notify when the consumption operation finishes. - */ - public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) { - checkNotDisposed(); - checkSetupDone("consume"); - List purchases = new ArrayList(); - purchases.add(purchase); - consumeAsyncInternal(purchases, listener, null); - } - - /** - * Same as {@link consumeAsync}, but for multiple items at once. - * @param purchases The list of PurchaseInfo objects representing the purchases to consume. - * @param listener The listener to notify when the consumption operation finishes. - */ - public void consumeAsync(List purchases, OnConsumeMultiFinishedListener listener) { - checkNotDisposed(); - checkSetupDone("consume"); - consumeAsyncInternal(purchases, null, listener); - } - - /** - * Returns a human-readable description for the given response code. - * - * @param code The response code - * @return A human-readable string explaining the result code. - * It also includes the result code numerically. - */ - public static String getResponseDesc(int code) { - String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" + - "3:Billing Unavailable/4:Item unavailable/" + - "5:Developer Error/6:Error/7:Item Already Owned/" + - "8:Item not owned").split("/"); - String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" + - "-1002:Bad response received/" + - "-1003:Purchase signature verification failed/" + - "-1004:Send intent failed/" + - "-1005:User cancelled/" + - "-1006:Unknown purchase response/" + - "-1007:Missing token/" + - "-1008:Unknown error/" + - "-1009:Subscriptions not available/" + - "-1010:Invalid consumption attempt").split("/"); - - if (code <= IABHELPER_ERROR_BASE) { - int index = IABHELPER_ERROR_BASE - code; - if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index]; - else return String.valueOf(code) + ":Unknown IAB Helper Error"; - } - else if (code < 0 || code >= iab_msgs.length) - return String.valueOf(code) + ":Unknown"; - else - return iab_msgs[code]; - } - - - // Checks that setup was done; if not, throws an exception. - void checkSetupDone(String operation) { - if (!mSetupDone) { - logError("Illegal state for operation (" + operation + "): IAB helper is not set up."); - throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation); - } - } - - // Workaround to bug where sometimes response codes come as Long instead of Integer - int getResponseCodeFromBundle(Bundle b) { - Object o = b.get(RESPONSE_CODE); - if (o == null) { - logDebug("Bundle with null response code, assuming OK (known issue)"); - return BILLING_RESPONSE_RESULT_OK; - } - else if (o instanceof Integer) return ((Integer)o).intValue(); - else if (o instanceof Long) return (int)((Long)o).longValue(); - else { - logError("Unexpected type for bundle response code."); - logError(o.getClass().getName()); - throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName()); - } - } - - // Workaround to bug where sometimes response codes come as Long instead of Integer - int getResponseCodeFromIntent(Intent i) { - Object o = i.getExtras().get(RESPONSE_CODE); - if (o == null) { - logError("Intent with no response code, assuming OK (known issue)"); - return BILLING_RESPONSE_RESULT_OK; - } - else if (o instanceof Integer) return ((Integer)o).intValue(); - else if (o instanceof Long) return (int)((Long)o).longValue(); - else { - logError("Unexpected type for intent response code."); - logError(o.getClass().getName()); - throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName()); - } - } - - void flagStartAsync(String operation) { - if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" + - operation + ") because another async operation(" + mAsyncOperation + ") is in progress."); - mAsyncOperation = operation; - mAsyncInProgress = true; - logDebug("Starting async operation: " + operation); - } - - void flagEndAsync() { - logDebug("Ending async operation: " + mAsyncOperation); - mAsyncOperation = ""; - mAsyncInProgress = false; - } - - - int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException { - // Query purchases - logDebug("Querying owned items, item type: " + itemType); - logDebug("Package name: " + mContext.getPackageName()); - boolean verificationFailed = false; - String continueToken = null; - - do { - logDebug("Calling getPurchases with continuation token: " + continueToken); - Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(), - itemType, continueToken); - - int response = getResponseCodeFromBundle(ownedItems); - logDebug("Owned items response: " + String.valueOf(response)); - if (response != BILLING_RESPONSE_RESULT_OK) { - logDebug("getPurchases() failed: " + getResponseDesc(response)); - return response; - } - if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST) - || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST) - || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) { - logError("Bundle returned from getPurchases() doesn't contain required fields."); - return IABHELPER_BAD_RESPONSE; - } - - ArrayList ownedSkus = ownedItems.getStringArrayList( - RESPONSE_INAPP_ITEM_LIST); - ArrayList purchaseDataList = ownedItems.getStringArrayList( - RESPONSE_INAPP_PURCHASE_DATA_LIST); - ArrayList signatureList = ownedItems.getStringArrayList( - RESPONSE_INAPP_SIGNATURE_LIST); - - for (int i = 0; i < purchaseDataList.size(); ++i) { - String purchaseData = purchaseDataList.get(i); - String signature = signatureList.get(i); - String sku = ownedSkus.get(i); - if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) { - logDebug("Sku is owned: " + sku); - Purchase purchase = new Purchase(itemType, purchaseData, signature); - - if (TextUtils.isEmpty(purchase.getToken())) { - logWarn("BUG: empty/null token!"); - logDebug("Purchase data: " + purchaseData); - } - - // Record ownership and token - inv.addPurchase(purchase); - } - else { - logWarn("Purchase signature verification **FAILED**. Not adding item."); - logDebug(" Purchase data: " + purchaseData); - logDebug(" Signature: " + signature); - verificationFailed = true; - } - } - - continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN); - logDebug("Continuation token: " + continueToken); - } while (!TextUtils.isEmpty(continueToken)); - - return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK; - } - - int querySkuDetails(String itemType, Inventory inv, List moreSkus) - throws RemoteException, JSONException { - logDebug("Querying SKU details."); - ArrayList skuList = new ArrayList(); - skuList.addAll(inv.getAllOwnedSkus(itemType)); - if (moreSkus != null) { - for (String sku : moreSkus) { - if (!skuList.contains(sku)) { - skuList.add(sku); - } - } - } - - if (skuList.size() == 0) { - logDebug("queryPrices: nothing to do because there are no SKUs."); - return BILLING_RESPONSE_RESULT_OK; - } - - Bundle querySkus = new Bundle(); - querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList); - Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(), - itemType, querySkus); - - if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) { - int response = getResponseCodeFromBundle(skuDetails); - if (response != BILLING_RESPONSE_RESULT_OK) { - logDebug("getSkuDetails() failed: " + getResponseDesc(response)); - return response; - } - else { - logError("getSkuDetails() returned a bundle with neither an error nor a detail list."); - return IABHELPER_BAD_RESPONSE; - } - } - - ArrayList responseList = skuDetails.getStringArrayList( - RESPONSE_GET_SKU_DETAILS_LIST); - - for (String thisResponse : responseList) { - SkuDetails d = new SkuDetails(itemType, thisResponse); - logDebug("Got sku details: " + d); - inv.addSkuDetails(d); - } - return BILLING_RESPONSE_RESULT_OK; - } - - - void consumeAsyncInternal(final List purchases, - final OnConsumeFinishedListener singleListener, - final OnConsumeMultiFinishedListener multiListener) { - final Handler handler = new Handler(); - flagStartAsync("consume"); - (new Thread(new Runnable() { - public void run() { - final List results = new ArrayList(); - for (Purchase purchase : purchases) { - try { - consume(purchase); - results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku())); - } - catch (IabException ex) { - results.add(ex.getResult()); - } - } - - flagEndAsync(); - if (!mDisposed && singleListener != null) { - handler.post(new Runnable() { - public void run() { - singleListener.onConsumeFinished(purchases.get(0), results.get(0)); - } - }); - } - if (!mDisposed && multiListener != null) { - handler.post(new Runnable() { - public void run() { - multiListener.onConsumeMultiFinished(purchases, results); - } - }); - } - } - })).start(); - } - - void logDebug(String msg) { - if (mDebugLog) Log.d(mDebugTag, msg); - } - - void logError(String msg) { - Log.e(mDebugTag, "In-app billing error: " + msg); - } - - void logWarn(String msg) { - Log.w(mDebugTag, "In-app billing warning: " + msg); - } -} diff --git a/OsmAnd/src/net/osmand/plus/inapp/util/IabResult.java b/OsmAnd/src/net/osmand/plus/inapp/util/IabResult.java deleted file mode 100644 index a548ed3886..0000000000 --- a/OsmAnd/src/net/osmand/plus/inapp/util/IabResult.java +++ /dev/null @@ -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(); } -} - diff --git a/OsmAnd/src/net/osmand/plus/inapp/util/Inventory.java b/OsmAnd/src/net/osmand/plus/inapp/util/Inventory.java deleted file mode 100644 index 2f9d1eb05a..0000000000 --- a/OsmAnd/src/net/osmand/plus/inapp/util/Inventory.java +++ /dev/null @@ -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 mSkuMap = new HashMap(); - Map mPurchaseMap = new HashMap(); - - 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 getAllOwnedSkus() { - return new ArrayList(mPurchaseMap.keySet()); - } - - /** Returns a list of all owned product IDs of a given type */ - public List getAllOwnedSkus(String itemType) { - List result = new ArrayList(); - for (Purchase p : mPurchaseMap.values()) { - if (p.getItemType().equals(itemType)) result.add(p.getSku()); - } - return result; - } - - /** Returns a list of all purchases. */ - List getAllPurchases() { - return new ArrayList(mPurchaseMap.values()); - } - - void addSkuDetails(SkuDetails d) { - mSkuMap.put(d.getSku(), d); - } - - void addPurchase(Purchase p) { - mPurchaseMap.put(p.getSku(), p); - } -} diff --git a/OsmAnd/src/net/osmand/plus/inapp/util/Purchase.java b/OsmAnd/src/net/osmand/plus/inapp/util/Purchase.java deleted file mode 100644 index ce2d1e828e..0000000000 --- a/OsmAnd/src/net/osmand/plus/inapp/util/Purchase.java +++ /dev/null @@ -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; } -} diff --git a/OsmAnd/src/net/osmand/plus/inapp/util/Security.java b/OsmAnd/src/net/osmand/plus/inapp/util/Security.java index 721f54dfd5..68f62038ff 100644 --- a/OsmAnd/src/net/osmand/plus/inapp/util/Security.java +++ b/OsmAnd/src/net/osmand/plus/inapp/util/Security.java @@ -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; diff --git a/OsmAnd/src/net/osmand/plus/inapp/util/SkuDetails.java b/OsmAnd/src/net/osmand/plus/inapp/util/SkuDetails.java deleted file mode 100644 index 5b77a01564..0000000000 --- a/OsmAnd/src/net/osmand/plus/inapp/util/SkuDetails.java +++ /dev/null @@ -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; - } -}