package net.osmand.plus.inapp; import android.app.Activity; import android.content.Intent; import android.os.AsyncTask; import android.support.annotation.NonNull; import android.util.Log; import net.osmand.AndroidNetworkUtils; import net.osmand.AndroidNetworkUtils.OnRequestResultListener; import net.osmand.plus.OsmandApplication; import net.osmand.plus.OsmandSettings; import net.osmand.plus.R; import net.osmand.plus.Version; 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.liveupdates.CountrySelectionFragment; import net.osmand.plus.liveupdates.CountrySelectionFragment.CountryItem; import net.osmand.util.Algorithms; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class InAppHelper { // Debug tag, for logging static final String TAG = "InAppHelper"; boolean mDebugLog = false; private static boolean mSubscribedToLiveUpdates = false; private static boolean mFullVersionPurchased = false; private static boolean mDepthContoursPurchased = false; private static String mLiveUpdatesPrice; private static long lastValidationCheckTime; private static String mFullVersionPrice; private static String mDepthContoursPrice; public static final String SKU_FULL_VERSION_PRICE = "osmand_full_version_price"; private static final String SKU_LIVE_UPDATES_FULL = "osm_live_subscription_2"; private static final String SKU_LIVE_UPDATES_FREE = "osm_free_live_subscription_2"; private static final String SKU_DEPTH_CONTOURS_FULL = "net.osmand.seadepth_plus"; private static final String SKU_DEPTH_CONTOURS_FREE = "net.osmand.seadepth"; public static String SKU_LIVE_UPDATES; public static String SKU_DEPTH_CONTOURS; private static final long PURCHASE_VALIDATION_PERIOD_MSEC = 1000 * 60 * 60 * 24; // daily // (arbitrary) request code for the purchase flow private static final int RC_REQUEST = 10001; // The helper object private IabHelper mHelper; private boolean stopAfterResult = false; private boolean isDeveloperVersion = false; private boolean forceRequestInventory = false; private String token = ""; private OsmandApplication ctx; private List<InAppListener> listeners = new ArrayList<>(); /* base64EncodedPublicKey should be YOUR APPLICATION'S PUBLIC KEY * (that you got from the Google Play developer console). This is not your * developer public key, it's the *app-specific* public key. * * Instead of just storing the entire literal string here embedded in the * program, construct the key at runtime from pieces or * use bit manipulation (for example, XOR with some other string) to hide * the actual key. The key itself is not secret information, but we don't * want to make it easy for an attacker to replace the public key with one * of their own and then fake messages from the server. */ private static final String BASE64_ENCODED_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgk8cEx" + "UO4mfEwWFLkQnX1Tkzehr4SnXLXcm2Osxs5FTJPEgyTckTh0POKVMrxeGLn0KoTY2NTgp1U/inp" + "wccWisPhVPEmw9bAVvWsOkzlyg1kv03fJdnAXRBSqDDPV6X8Z3MtkPVqZkupBsxyIllEILKHK06" + "OCw49JLTsMR3oTRifGzma79I71X0spw0fM+cIRlkS2tsXN8GPbdkJwHofZKPOXS51pgC1zU8uWX" + "I+ftJO46a1XkNh1dO2anUiQ8P/H4yOTqnMsXF7biyYuiwjXPOcy0OMhEHi54Dq6Mr3u5ZALOAkc" + "YTjh1H/ZgqIHy5ZluahINuDE76qdLYMXrDMQIDAQAB"; public interface InAppListener { void onError(String error); void onGetItems(); void onItemPurchased(String sku); void showProgress(); void dismissProgress(); } public interface InAppRunnable { void run(InAppHelper helper); } public String getToken() { return token; } public static boolean isSubscribedToLiveUpdates() { return mSubscribedToLiveUpdates; } public static boolean isFullVersionPurchased() { return mFullVersionPurchased; } public static boolean isDepthContoursPurchased() { return mDepthContoursPurchased; } public static String getLiveUpdatesPrice() { return mLiveUpdatesPrice; } public static String getDepthContoursPrice() { return mDepthContoursPrice; } public static String getFullVersionPrice() { return mFullVersionPrice; } public static String getSkuLiveUpdates() { return SKU_LIVE_UPDATES; } public static boolean hasPrices(OsmandApplication app) { return !Algorithms.isEmpty(mLiveUpdatesPrice) && (!Version.isFreeVersion(app) || !Algorithms.isEmpty(mFullVersionPrice)); } public static void initialize(OsmandApplication ctx) { if (SKU_LIVE_UPDATES == null) { if (Version.isFreeVersion(ctx)) { SKU_LIVE_UPDATES = SKU_LIVE_UPDATES_FREE; } else { SKU_LIVE_UPDATES = SKU_LIVE_UPDATES_FULL; } } if (SKU_DEPTH_CONTOURS == null) { if (Version.isFreeVersion(ctx)) { SKU_DEPTH_CONTOURS = SKU_DEPTH_CONTOURS_FREE; } else { SKU_DEPTH_CONTOURS = SKU_DEPTH_CONTOURS_FULL; } } } public InAppHelper(OsmandApplication ctx, boolean forceRequestInventory) { this.ctx = ctx; this.forceRequestInventory = forceRequestInventory; isDeveloperVersion = Version.isDeveloperVersion(ctx); if (isDeveloperVersion) { mSubscribedToLiveUpdates = true; mFullVersionPurchased = true; mDepthContoursPurchased = true; ctx.getSettings().LIVE_UPDATES_PURCHASED.set(true); ctx.getSettings().FULL_VERSION_PURCHASED.set(true); ctx.getSettings().DEPTH_CONTOURS_PURCHASED.set(true); } } public static boolean isInAppIntentoryRead() { return lastValidationCheckTime != 0; } public static boolean isPurchased(OsmandApplication ctx, String inAppSku) { OsmandSettings settings = ctx.getSettings(); if (inAppSku.equals(SKU_FULL_VERSION_PRICE)) { return settings.FULL_VERSION_PURCHASED.get(); } else if (inAppSku.equals(SKU_LIVE_UPDATES_FULL) || inAppSku.equals(SKU_LIVE_UPDATES_FREE)) { return settings.LIVE_UPDATES_PURCHASED.get(); } else if (inAppSku.equals(SKU_DEPTH_CONTOURS_FULL) || inAppSku.equals(SKU_DEPTH_CONTOURS_FREE)) { return settings.DEPTH_CONTOURS_PURCHASED.get(); } return false; } public void exec(final @NonNull InAppRunnable runnable) { this.stopAfterResult = true; // Create the helper, passing it our context and the public key to verify signatures with logDebug("Creating InAppHelper."); 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. logDebug("Starting setup."); try { mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() { public void onIabSetupFinished(IabResult result) { logDebug("Setup finished."); if (!result.isSuccess()) { // Oh noes, there was a problem. //complain("Problem setting up in-app billing: " + result); notifyError(result.getMessage()); if (stopAfterResult) { stop(); } return; } // Have we been disposed of in the meantime? If so, quit. if (mHelper == null) return; runnable.run(InAppHelper.this); } }); } catch (Exception e) { logError("exec Error", e); if (stopAfterResult) { stop(); } } } public void start(final boolean stopAfterResult) { this.stopAfterResult = stopAfterResult; // Create the helper, passing it our context and the public key to verify signatures with logDebug("Creating InAppHelper."); 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. logDebug("Starting setup."); try { mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() { public void onIabSetupFinished(IabResult result) { logDebug("Setup finished."); if (!result.isSuccess()) { // Oh noes, there was a problem. //complain("Problem setting up in-app billing: " + result); notifyError(result.getMessage()); if (stopAfterResult) { stop(); } return; } // Have we been disposed of in the meantime? If so, quit. if (mHelper == null) return; // IAB is fully set up. Now, let's get an inventory of stuff we own if needed. if (forceRequestInventory || (!isDeveloperVersion && (!mSubscribedToLiveUpdates || !ctx.getSettings().BILLING_PURCHASE_TOKEN_SENT.get() || System.currentTimeMillis() - lastValidationCheckTime > PURCHASE_VALIDATION_PERIOD_MSEC))) { logDebug("Setup successful. Querying inventory."); List<String> skus = new ArrayList<>(); skus.add(SKU_LIVE_UPDATES); skus.add(SKU_DEPTH_CONTOURS); skus.add(SKU_FULL_VERSION_PRICE); try { mHelper.queryInventoryAsync(true, skus, mGotInventoryListener); } catch (Exception e) { logError("queryInventoryAsync Error", e); notifyDismissProgress(); if (stopAfterResult) { stop(); } } } else { notifyDismissProgress(); if (stopAfterResult) { stop(); } } } }); } catch (Exception e) { logError("start Error", e); if (stopAfterResult) { stop(); } } } // 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."); // Have we been disposed of in the meantime? If so, quit. if (mHelper == null) return; // Is it a failure? if (result.isFailure()) { logError("Failed to query inventory: " + result); notifyError(result.getMessage()); if (stopAfterResult) { stop(); } return; } logDebug("Query inventory was successful."); /* * Check for items we own. Notice that for each purchase, we check * the developer payload to see if it's correct! See * verifyDeveloperPayload(). */ // Do we have the live updates? Purchase liveUpdatesPurchase = inventory.getPurchase(SKU_LIVE_UPDATES); mSubscribedToLiveUpdates = isDeveloperVersion || (liveUpdatesPurchase != null && liveUpdatesPurchase.getPurchaseState() == 0); if (mSubscribedToLiveUpdates) { ctx.getSettings().LIVE_UPDATES_PURCHASED.set(true); } Purchase fullVersionPurchase = inventory.getPurchase(SKU_FULL_VERSION_PRICE); mFullVersionPurchased = isDeveloperVersion || (fullVersionPurchase != null && fullVersionPurchase.getPurchaseState() == 0); if (mFullVersionPurchased) { ctx.getSettings().FULL_VERSION_PURCHASED.set(true); } Purchase depthContoursPurchase = inventory.getPurchase(SKU_DEPTH_CONTOURS); mDepthContoursPurchased = isDeveloperVersion || (depthContoursPurchase != null && depthContoursPurchase.getPurchaseState() == 0); if (mDepthContoursPurchased) { ctx.getSettings().DEPTH_CONTOURS_PURCHASED.set(true); } lastValidationCheckTime = System.currentTimeMillis(); logDebug("User " + (mSubscribedToLiveUpdates ? "HAS" : "DOES NOT HAVE") + " live updates purchased."); if (inventory.hasDetails(SKU_LIVE_UPDATES)) { SkuDetails liveUpdatesDetails = inventory.getSkuDetails(SKU_LIVE_UPDATES); mLiveUpdatesPrice = liveUpdatesDetails.getPrice(); } if (inventory.hasDetails(SKU_DEPTH_CONTOURS)) { SkuDetails depthContoursDetails = inventory.getSkuDetails(SKU_DEPTH_CONTOURS); mDepthContoursPrice = depthContoursDetails.getPrice(); } if (inventory.hasDetails(SKU_FULL_VERSION_PRICE)) { SkuDetails fullPriceDetails = inventory.getSkuDetails(SKU_FULL_VERSION_PRICE); mFullVersionPrice = fullPriceDetails.getPrice(); } boolean needSendToken = false; if (!isDeveloperVersion && liveUpdatesPurchase != null) { OsmandSettings settings = ctx.getSettings(); if ((Algorithms.isEmpty(settings.BILLING_USER_ID.get()) || Algorithms.isEmpty(settings.BILLING_USER_TOKEN.get())) && !Algorithms.isEmpty(liveUpdatesPurchase.getDeveloperPayload())) { String payload = liveUpdatesPurchase.getDeveloperPayload(); if (!Algorithms.isEmpty(payload)) { String[] arr = payload.split(" "); if (arr.length > 0) { settings.BILLING_USER_ID.set(arr[0]); } if (arr.length > 1) { token = arr[1]; settings.BILLING_USER_TOKEN.set(token); } } } if (!settings.BILLING_PURCHASE_TOKEN_SENT.get()) { needSendToken = true; } } final OnRequestResultListener listener = new OnRequestResultListener() { @Override public void onResult(String result) { notifyDismissProgress(); notifyGetItems(); if (stopAfterResult) { stop(); } logDebug("Initial inapp query finished"); } }; if (needSendToken) { sendToken(liveUpdatesPurchase.getToken(), listener); } else { listener.onResult("OK"); } } }; public void purchaseFullVersion(final Activity activity) { if (mHelper == null) { //complain("In-app hepler is not initialized!"); notifyError("In-app hepler is not initialized!"); if (stopAfterResult) { stop(); } return; } logDebug("Launching purchase flow for full version"); if (mHelper != null) { try { mHelper.launchPurchaseFlow(activity, SKU_FULL_VERSION_PRICE, RC_REQUEST, mPurchaseFinishedListener); } catch (Exception e) { complain("Cannot launch full version purchase!"); logError("purchaseFullVersion Error", e); if (stopAfterResult) { stop(); } } } } public void purchaseDepthContours(final Activity activity) { if (mHelper == null) { //complain("In-app hepler is not initialized!"); notifyError("In-app hepler is not initialized!"); if (stopAfterResult) { stop(); } return; } logDebug("Launching purchase flow for sea depth contours"); if (mHelper != null) { try { mHelper.launchPurchaseFlow(activity, SKU_DEPTH_CONTOURS, RC_REQUEST, mPurchaseFinishedListener); } catch (Exception e) { complain("Cannot launch depth contours purchase!"); logError("purchaseDepthContours Error", e); if (stopAfterResult) { stop(); } } } } public void purchaseLiveUpdates(final Activity activity, final String email, final String userName, final String countryDownloadName, final boolean hideUserName) { try { if (mHelper == null || !mHelper.subscriptionsSupported()) { complain("Subscriptions not supported on your device yet. Sorry!"); notifyError("Subscriptions not supported on your device yet. Sorry!"); if (stopAfterResult) { stop(); } return; } } catch (Exception e) { logError("purchaseLiveUpdates Error", e); if (stopAfterResult) { stop(); } return; } notifyShowProgress(); new AsyncTask<Void, Void, String>() { private String userId; @Override protected String doInBackground(Void... params) { userId = ctx.getSettings().BILLING_USER_ID.get(); try { Map<String, String> parameters = new HashMap<>(); parameters.put("visibleName", hideUserName ? "" : userName); parameters.put("preferredCountry", countryDownloadName); parameters.put("email", email); if (Algorithms.isEmpty(userId)) { parameters.put("status", "new"); } return AndroidNetworkUtils.sendRequest(ctx, "http://download.osmand.net/subscription/register.php", parameters, "Requesting userId...", true, true); } catch (Exception e) { logError("sendRequest Error", e); return null; } } @Override protected void onPostExecute(String response) { logDebug("Response=" + response); if (response == null) { complain("Cannot retrieve userId from server."); notifyDismissProgress(); notifyError("Cannot retrieve userId from server."); if (stopAfterResult) { stop(); } return; } else { try { JSONObject obj = new JSONObject(response); userId = obj.getString("userid"); ctx.getSettings().BILLING_USER_ID.set(userId); token = obj.getString("token"); ctx.getSettings().BILLING_USER_TOKEN.set(token); logDebug("UserId=" + userId); } catch (JSONException e) { String message = "JSON parsing error: " + (e.getMessage() == null ? "unknown" : e.getMessage()); complain(message); notifyDismissProgress(); notifyError(message); if (stopAfterResult) { stop(); } } } notifyDismissProgress(); if (!Algorithms.isEmpty(userId)) { logDebug("Launching purchase flow for live updates subscription for userId=" + userId); String payload = userId + " " + token; if (mHelper != null) { try { mHelper.launchPurchaseFlow(activity, SKU_LIVE_UPDATES, IabHelper.ITEM_TYPE_SUBS, RC_REQUEST, mPurchaseFinishedListener, payload); } catch (Exception e) { logError("launchPurchaseFlow Error", e); if (stopAfterResult) { stop(); } } } } else { notifyError("Empty userId"); if (stopAfterResult) { stop(); } } } }.execute((Void) null); } 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; } } // 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); // if we were disposed of in the meantime, quit. if (mHelper == null) return; if (result.isFailure()) { complain("Error purchasing: " + result); notifyDismissProgress(); notifyError("Error purchasing: " + result); if (stopAfterResult) { stop(); } return; } logDebug("Purchase successful."); if (purchase.getSku().equals(SKU_LIVE_UPDATES)) { // bought live updates logDebug("Live updates subscription purchased."); sendToken(purchase.getToken(), new OnRequestResultListener() { @Override public void onResult(String result) { showToast(ctx.getString(R.string.osm_live_thanks)); mSubscribedToLiveUpdates = true; ctx.getSettings().LIVE_UPDATES_PURCHASED.set(true); notifyDismissProgress(); notifyItemPurchased(SKU_LIVE_UPDATES); if (stopAfterResult) { stop(); } } }); } if (purchase.getSku().equals(SKU_FULL_VERSION_PRICE)) { // bought full version logDebug("Full version purchased."); showToast(ctx.getString(R.string.full_version_thanks)); mFullVersionPurchased = true; ctx.getSettings().FULL_VERSION_PURCHASED.set(true); notifyDismissProgress(); notifyItemPurchased(SKU_FULL_VERSION_PRICE); if (stopAfterResult) { stop(); } } if (purchase.getSku().equals(SKU_DEPTH_CONTOURS)) { // bought sea depth contours logDebug("Sea depth contours purchased."); showToast(ctx.getString(R.string.sea_depth_thanks)); mDepthContoursPurchased = true; ctx.getSettings().DEPTH_CONTOURS_PURCHASED.set(true); ctx.getSettings().getCustomRenderBooleanProperty("depthContours").set(true); notifyDismissProgress(); notifyItemPurchased(SKU_DEPTH_CONTOURS); if (stopAfterResult) { stop(); } } } }; // Do not forget call stop() when helper is not needed anymore public void stop() { logDebug("Destroying helper."); if (mHelper != null) { mHelper.dispose(); mHelper = null; } } private void sendToken(String purchaseToken, final OnRequestResultListener listener) { final String userId = ctx.getSettings().BILLING_USER_ID.get(); final String token = ctx.getSettings().BILLING_USER_TOKEN.get(); final String email = ctx.getSettings().BILLING_USER_EMAIL.get(); try { Map<String, String> parameters = new HashMap<>(); parameters.put("userid", userId); parameters.put("sku", SKU_LIVE_UPDATES); parameters.put("purchaseToken", purchaseToken); parameters.put("email", email); parameters.put("token", token); AndroidNetworkUtils.sendRequestAsync(ctx, "http://download.osmand.net/subscription/purchased.php", parameters, "Sending purchase info...", true, true, new OnRequestResultListener() { @Override public void onResult(String result) { if (result != null) { try { JSONObject obj = new JSONObject(result); if (!obj.has("error")) { ctx.getSettings().BILLING_PURCHASE_TOKEN_SENT.set(true); if (obj.has("visibleName") && !Algorithms.isEmpty(obj.getString("visibleName"))) { ctx.getSettings().BILLING_USER_NAME.set(obj.getString("visibleName")); ctx.getSettings().BILLING_HIDE_USER_NAME.set(false); } else { ctx.getSettings().BILLING_HIDE_USER_NAME.set(true); } if (obj.has("preferredCountry")) { String prefferedCountry = obj.getString("preferredCountry"); if (!ctx.getSettings().BILLING_USER_COUNTRY_DOWNLOAD_NAME.get().equals(prefferedCountry)) { ctx.getSettings().BILLING_USER_COUNTRY_DOWNLOAD_NAME.set(prefferedCountry); CountrySelectionFragment countrySelectionFragment = new CountrySelectionFragment(); countrySelectionFragment.initCountries(ctx); CountryItem countryItem = null; if (Algorithms.isEmpty(prefferedCountry)) { countryItem = countrySelectionFragment.getCountryItems().get(0); } else if (!prefferedCountry.equals(OsmandSettings.BILLING_USER_DONATION_NONE_PARAMETER)) { countryItem = countrySelectionFragment.getCountryItem(prefferedCountry); } if (countryItem != null) { ctx.getSettings().BILLING_USER_COUNTRY.set(countryItem.getLocalName()); } } } if (obj.has("email")) { ctx.getSettings().BILLING_USER_EMAIL.set(obj.getString("email")); } } else { complain("SendToken Error: " + obj.getString("error") + " (userId=" + userId + " token=" + token + " response=" + result + ")"); } } catch (JSONException e) { logError("SendToken", e); complain("SendToken Error: " + (e.getMessage() != null ? e.getMessage() : "JSONException") + " (userId=" + userId + " token=" + token + " response=" + result + ")"); } } if (listener != null) { listener.onResult("OK"); } } }); } catch (Exception e) { logError("SendToken Error", e); if (listener != null) { listener.onResult("Error"); } } } private void notifyError(String message) { for (InAppListener l : listeners) { l.onError(message); } } private void notifyGetItems() { for (InAppListener l : listeners) { l.onGetItems(); } } private void notifyItemPurchased(String sku) { for (InAppListener l : listeners) { l.onItemPurchased(sku); } } private void notifyShowProgress() { for (InAppListener l : listeners) { l.showProgress(); } } private void notifyDismissProgress() { for (InAppListener l : listeners) { l.dismissProgress(); } } public void addListener(InAppListener listener) { this.listeners.add(listener); } public void removeListener(InAppListener listener) { this.listeners.remove(listener); } private void complain(String message) { logError("**** InAppHelper Error: " + message); showToast(message); } private void showToast(final String message) { ctx.showToastMessage(message); } private void logDebug(String msg) { if (mDebugLog) Log.d(TAG, msg); } private void logError(String msg) { Log.e(TAG, msg); } private void logError(String msg, Throwable e) { Log.e(TAG, "Error: " + msg, e); } }