Java tutorial
///////////////////////////////////////////////////////////////////// // // TestInAppBilling // The TestInAppBilling activity is a demo application to // test the InAppBilling class. // // InAppBilling // The InAppBilling class handles the in-app billing flow for // purchasing of one item. The purchased item can be "inapp" for // one-time purchases or "subs" for subscription. The program // logic steps are: // 1. Instantiate InAppBilling object. // 2. Call startServiceConnection method. This method will create // a serviceConnection and bind it to Google Play Store service // on the device. The binding process is asynchronous. When // the binding process is done the system will call serviceConnected // method. // 3. The serviceConnected method will test if in-app billing // service is available on this device. // 4. If in-app billing is supported, the method will check if // the item to be purchased is available for sale. // 5. If the item is available for sale, the method will check if // it is already owned by the customer. // 6. If the item is not owned by the customer, the program will // send an asynchronous request to purchase the item. // 7. When the request was processed by Google Play Store // The system calls onActivityResult method in that parent // activity class This call is transferred to onActivityResult // method in this class. // 8. The InAppBilling class checks the result. There are four // possible outcomes: (1) User canceled (2) Error (3) Result is // ok but verifying returned data failed (4) Purchase is successful. // 9. In all of these cases, the class will un-bind from the service // and call either inAppBillingCancel, inAppBillingError or // inAppBillingSuccess. // 10. After step 5 if the item is owned and the program is running // in production mode, the class will unbind the service // connection to Google Play Store and call the // inAppBillingItemAlreadyOwned call back method. // 11. If the item is owned by the customer and the program is // running in debug mode, the class will consume the item. Un-bind // the service connection to Google Play Store and call the // inAppBillingItemAlreadyOwned call back method. // // Granotech Limited // Author: Uzi Granot // Version: 1.0 // Date: January 16, 2014 // Copyright (C) 2014 Granotech Limited. All Rights Reserved // // TestInAppBilling is an open source software. You can // redistribute it and/or modify as part of your software under // the terms of the Eclipse Public License Version 1.0 (EPL-1.0) // as published by the Open Source Initiative organization. // TestInAppBilling is distributed in the hope that it will // be useful, but WITHOUT ANY WARRANTY; without even the implied // warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // Go to Open Source Initiative web site for more details. // http://opensource.org/licenses/EPL-1.0 // // Version History: // // Version 1.0 2014/01/16 // Original revision // ///////////////////////////////////////////////////////////////////// // NOTE: You must replace this package name with your own package name package com.yourkey.billing.util; import java.security.KeyFactory; import java.security.PublicKey; import java.security.Signature; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import org.json.JSONObject; import android.app.Activity; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; import android.content.Context; import android.os.Bundle; import android.os.IBinder; import android.text.TextUtils; import com.android.vending.billing.IInAppBillingService; ///////////////////////////////////////////////////////////////////// // InAppBilling class ///////////////////////////////////////////////////////////////////// public class InAppBilling { // InAppBillingService is the service that provides in-app billing version 3 and beyond. // All api calls will give a response code with the following possible values private static final int RESULT_INVALID_RESPONSE = -1; // invalid response code private static final int RESULT_OK = 0; // success private static final int RESULT_USER_CANCELED = 1; // user pressed back or canceled a dialog private static final int RESULT_UNKNOWN_RESULT_CODE = 2; // unknown result code private static final int RESULT_BILLING_UNAVAILABLE = 3; // this billing API version is not supported for the type requested private static final int RESULT_ITEM_UNAVAILABLE = 4; // requested SKU is not available for purchase private static final int RESULT_DEVELOPER_ERROR = 5; // invalid arguments provided to the API private static final int RESULT_ERROR = 6; // fatal error during the API action private static final int RESULT_ITEM_ALREADY_OWNED = 7; // failure to purchase since item is already owned private static final int RESULT_ITEM_NOT_OWNED = 8; // failure to consume since item is not owned private static final String RESULT_TEXT_ITEM_ALREADY_OWNED = "ITEM_ALREADY_OWNED"; private static final String RESULT_TEXT_ITEM_NOT_OWNED = "ITEM_NOT_OWNED"; // response code text private static final String[] resultText = new String[] { "Invalid response from Google Play", "Success", "User canceled the purchase", "Unknown result code", "This device does not support In App Billing service", "The requested item is not available for purchase", "Program error", "Communicating with Google Play failed", "You have already purchased this item", "Consume failed since item is not owned", }; // translate response code to text message private static String resultMessage(int result) { if (result >= RESULT_INVALID_RESPONSE && result <= RESULT_ITEM_NOT_OWNED && result != RESULT_UNKNOWN_RESULT_CODE) return (resultText[result + 1]); return ("Unknown response code from Google Play Store. Code: " + Integer.toString(result)); } // user error messages private static final String PLAY_STORE_UNAVAILABLE = "Google Play Store service unavailable on this device."; private static final String PLAY_STORE_SERVICE_DISCONNECTED = "Google Play Store service was disconnected."; private static final String PLAY_STORE_PURCHASE_NOT_SUPPORTED = "Google Play Store purchases not supported on this device."; private static final String PLAY_STORE_PURCHASE_FAILED = "Google Play Store purchase failed."; private static final String PLAY_STORE_INVALID_PURCHASE_RESPONSE = "Google Play Store invalid purchase response."; private static final String PLAY_STORE_INVALID_PRODUCT_ID = "Google Play Store invalid product ID."; private static final String PLAY_STORE_LOAD_OWNED_PRODUCT_FAILED = "Google Play Store loading owned products failed."; private static final String PLAY_STORE_CONSUME_PRODUCT_FAILED = "Consume product failed."; // Keys for the responses from InAppBillingService private static final String RESPONSE_CODE = "RESPONSE_CODE"; private static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST"; private static final String RESPONSE_BUY_INTENT = "BUY_INTENT"; private static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA"; private static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE"; private static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST"; private static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST"; private static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST"; private static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN"; // some fields on the getSkuDetails response bundle private static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST"; // service intent strings private static final String BIND_SERVICE_INTENT = "com.android.vending.billing.InAppBillingService.BIND"; private static final String SERVICE_INTENT_PACKAGE = "com.android.vending"; private static final String KEY_PRODUCT_ID = "productId"; private static final String KEY_PURCHASE_TOKEN = "purchaseToken"; private static final String PURCHASE_EXTRA_DATA = "ColorSelector"; // InAppBilling version 3 private static final int IN_APP_BILLING_API_VERSION = 3; // signature security private static final String KEY_FACTORY_ALGORITHM = "RSA"; private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; // Translates encoded base64 characters to binary value 0 to 63 is valid value private static final int PD = 64; // padding private static final int WS = 65; // white space private static final int ER = 66; // error private final static byte[] DecodeArray = { // 0 1 2 3 4 5 6 7 8 9 A B C D E F ER, ER, ER, ER, ER, ER, ER, ER, ER, WS, WS, ER, ER, WS, ER, ER, // 0x00 (0x09=\t, 0x0A=\n, 0x0D=\r) ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, // 0x10 WS, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, 62, ER, ER, ER, 63, // 0x20 (0x20=space, 0x2B=+, 0x2F=/) 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, ER, ER, ER, PD, ER, ER, // 0x30 (0x30-0x39 for 0-9, 0x3D for =) ER, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 0x40 (0x41-0x4F for A-O) 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, ER, ER, ER, ER, ER, // 0x50 (0x50-0x5A for P-Z) ER, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 0x60 (0x61-0x6F for a-o) 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, ER, ER, ER, ER, ER, // 0x70 (0x70-0x7A for p-z) ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, // 0x80 ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, // 0x90 ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, // 0xA0 ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, // 0xB0 ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, // 0xC0 ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, // 0xD0 ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, // 0xE0 ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, ER, // 0xF0 }; //itemType values public static final String ITEM_TYPE_ONE_TIME_PURCHASE = "inapp"; public static final String ITEM_TYPE_SUBSCRIPTION = "subs"; // activity type values public static final boolean ACTIVITY_TYPE_PURCHASE = false; public static final boolean ACTIVITY_TYPE_CONSUME = true; // item types and SKU and intent request code private String itemType; private String itemSku; private boolean consumeActivity; // application context private Activity parentActivity; private Context applicationContext; private InAppBillingListener inAppBillingListener; private String packageName; private int purchaseRequestCode; // application public key for verifying signature, in base64 encoding private String appPublicKeyStr; private PublicKey appPublicKey; // Connection to the service private IInAppBillingService inAppBillingService; private ServiceConnection serviceConnection; // class is active private boolean active; public boolean isActive() { return (active); } ///////////////////////////////////////////////////////////////////// // Callback that notifies when a purchase is done ///////////////////////////////////////////////////////////////////// public interface InAppBillingListener { public void inAppBillingBuySuccsess(); public void inAppBillingItemAlreadyOwned(); public void inAppBillingCanceled(); public void inAppBillingConsumeSuccsess(); public void inAppBillingItemNotOwned(); public void inAppBillingFailure(String errorMessage); } ///////////////////////////////////////////////////////////////////// // constructor // parentActivity: the activity that instantiate this class // appPublicKeyStr: the public key assigned by Google Play Store to // this application in base64 format // inAppBillingListener: a class implementing the four call back // methods to deliver results // purchaseRequestCode: request code for onActivityResult ///////////////////////////////////////////////////////////////////// public InAppBilling(Activity parentActivity, final InAppBillingListener inAppBillingListener, String appPublicKeyStr, int purchaseRequestCode) { // context initialization this.parentActivity = parentActivity; this.applicationContext = parentActivity.getApplicationContext(); this.packageName = applicationContext.getPackageName(); // listening class this.inAppBillingListener = inAppBillingListener; // google provided public key for this application this.appPublicKeyStr = appPublicKeyStr; // request code this.purchaseRequestCode = purchaseRequestCode; return; } ///////////////////////////////////////////////////////////////////// // Dispose InAppBilling service ///////////////////////////////////////////////////////////////////// public void dispose() { // un-bind service connection if (serviceConnection != null) { applicationContext.unbindService(serviceConnection); serviceConnection = null; } active = false; return; } ///////////////////////////////////////////////////////////////////// // Start in-app billing service connection // itemType: "inapp" or "subs" // itemSku: product ID // consumeActivity: Purchase = false, Consume = true ///////////////////////////////////////////////////////////////////// public boolean startServiceConnection(String itemType, String itemSku, boolean consumeActivity) { // already active if (active) return (false); // set active active = true; // save item type inapp or Subs this.itemType = itemType; // save requested item SKU this.itemSku = itemSku; // buy or consume the item this.consumeActivity = consumeActivity; // create bind service intent object Intent serviceIntent = new Intent(BIND_SERVICE_INTENT); serviceIntent.setPackage(SERVICE_INTENT_PACKAGE); // make sure there is at least one service matching the intent if (applicationContext.getPackageManager().resolveService(serviceIntent, 0) == null) { // no service available to handle that Intent // call purchase listener with error message active = false; inAppBillingListener.inAppBillingFailure(errorMessage(PLAY_STORE_UNAVAILABLE)); return (true); } // define connect and disconnect methods to handle call back from InAppBillingService serviceConnection = new ServiceConnection() { // service was disconnected call back @Override public void onServiceDisconnected(ComponentName name) { // reset serviceConnection to make sure that dispose method will not un-bind the service serviceConnection = null; active = false; // report service is disconnected to listener inAppBillingListener.inAppBillingFailure(errorMessage(PLAY_STORE_SERVICE_DISCONNECTED)); return; } // service was connected call back @Override public void onServiceConnected(ComponentName name, IBinder service) { // process service connected serviceConnected(service); return; } }; try { // bind the service to our context and // pass the service connection object defining the connect and disconnect call back methods if (!applicationContext.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE)) { // reset serviceConnection to make sure that dispose method will not un-bind the service serviceConnection = null; active = false; inAppBillingListener.inAppBillingFailure(errorMessage(PLAY_STORE_UNAVAILABLE)); return (true); } } // binding to in app billing service failed catch (Exception e) { // reset serviceConnection to make sure that dispose method will not un-bind the service serviceConnection = null; active = false; inAppBillingListener.inAppBillingFailure(exceptionMessage(PLAY_STORE_UNAVAILABLE, e)); return (true); } // exit while waiting for service connection return (true); } ///////////////////////////////////////////////////////////////////// // InAppBilling service is now connected to our application ///////////////////////////////////////////////////////////////////// private void serviceConnected(IBinder service) { // get service pointer inAppBillingService = IInAppBillingService.Stub.asInterface(service); // make sure billing is supported for itemType (one-time or subscription) try { // check for in-app billing v3 support int result = inAppBillingService.isBillingSupported(IN_APP_BILLING_API_VERSION, packageName, itemType); if (result != RESULT_OK) { // unbind service dispose(); // call purchase listener with error message inAppBillingListener.inAppBillingFailure(errorMessage(PLAY_STORE_PURCHASE_NOT_SUPPORTED, result)); return; } } // system exception executing isBillingSupported method catch (Exception e) { // unbind service dispose(); // call purchase listener with error message inAppBillingListener.inAppBillingFailure(exceptionMessage(PLAY_STORE_PURCHASE_NOT_SUPPORTED, e)); return; } // test to make sure item sku is valid String resultMsg = testItemSku(); if (resultMsg != null) { // unbind service dispose(); // call purchase listener with error message inAppBillingListener.inAppBillingFailure(resultMsg); return; } // Get all items owned by this user for this application // and test if user already owns this item. // If we are in consume mode and item is owned, consume the item /* resultMsg = testItemOwned(); if(!resultMsg.equals(RESULT_TEXT_ITEM_ALREADY_OWNED) && !resultMsg.equals(RESULT_TEXT_ITEM_NOT_OWNED)) { // unbind service dispose(); // call purchase listener with error message inAppBillingListener.inAppBillingFailure(resultMsg); return; } // user owns this item if(resultMsg.equals(RESULT_TEXT_ITEM_ALREADY_OWNED)) { // unbind service dispose(); // for consume mode if(consumeActivity) inAppBillingListener.inAppBillingConsumeSuccsess(); // for buy mode else inAppBillingListener.inAppBillingItemAlreadyOwned(); // exit return; }*/ // consume and user does not own this item if (consumeActivity) { // unbind service dispose(); // call listener inAppBillingListener.inAppBillingItemNotOwned(); return; } try { // build intent bundle for the purpose of purchase itemSku Bundle buyIntentBundle = inAppBillingService.getBuyIntent(IN_APP_BILLING_API_VERSION, packageName, itemSku, itemType, PURCHASE_EXTRA_DATA); // buyIntentBundle has two key value pairs // RESPONSE_CODE with standard response code // BUY_INTENT with PendingIntent to start the purchase flow int buyResult = getResponseCodeFromBundle(buyIntentBundle); if (buyResult != RESULT_OK) { // unbind service dispose(); // error message inAppBillingListener .inAppBillingFailure(errorMessage(PLAY_STORE_PURCHASE_FAILED + resultMessage(buyResult))); return; } // send Google Play Store request to purchase itemSku // when the purchase process is completed, the system will call onActivityResult method in parent activity PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT); parentActivity.startIntentSenderForResult(pendingIntent.getIntentSender(), purchaseRequestCode, new Intent(), Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0)); } // trap exceptions catch (Exception e) { // unbind service dispose(); // error message inAppBillingListener.inAppBillingFailure(exceptionMessage(PLAY_STORE_PURCHASE_FAILED, e)); return; } // exit return; } ///////////////////////////////////////////////////////////////////// // onActivityResult method is called when in-app billing purchase // process is done. The billing process calla onActivityResult // method in the parentActivity class. The parent activity calls // directly or indirectly this method. ///////////////////////////////////////////////////////////////////// public void onActivityResult(int activityResultCode, Intent data) { // any result but OK we will assume user cancelled the activity if (activityResultCode != Activity.RESULT_OK) { // unbind service dispose(); // user canceled inAppBillingListener.inAppBillingCanceled(); return; } int result = getResponseCodeFromBundle(data.getExtras()); if (result != RESULT_OK) { // unbind service dispose(); // user canceled the purchase if (result == RESULT_USER_CANCELED) { inAppBillingListener.inAppBillingCanceled(); } // purchase was canceled for other reason else { inAppBillingListener.inAppBillingFailure(errorMessage(PLAY_STORE_PURCHASE_FAILED, result)); } return; } // extract purchased data and signature String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA); String signature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE); // invalid response if (purchaseData == null || signature == null || !verifySignature(purchaseData, signature)) { // unbind service dispose(); // invalid response inAppBillingListener.inAppBillingFailure(errorMessage(PLAY_STORE_INVALID_PURCHASE_RESPONSE)); return; } // final test make sure we received the same sku we wanted try { // create json object JSONObject jsonObject = new JSONObject(purchaseData); // verify product id if (!jsonObject.optString(KEY_PRODUCT_ID).equals(itemSku)) { // unbind service dispose(); // invalid response inAppBillingListener.inAppBillingFailure(errorMessage(PLAY_STORE_INVALID_PURCHASE_RESPONSE)); return; } // NOTE TO PROGRAMMERS // the jsonObject contains the following information // "orderId":"12999763169054705758.1371079406387615" // "packageName":"com.example.app", // "productId":"exampleSku", // "purchaseTime":1345678900000, (msec since 1970/01/01) // "purchaseState":0, // 0-purchased, 1 canceled, 2 refunded // "developerPayload":"example developer payload" // "purchaseToken" : "122333444455555", } catch (Exception e) { // unbind service dispose(); // invalid response inAppBillingListener.inAppBillingFailure(exceptionMessage(PLAY_STORE_INVALID_PURCHASE_RESPONSE, e)); return; } // unbind service dispose(); // success inAppBillingListener.inAppBillingBuySuccsess(); return; } ///////////////////////////////////////////////////////////////////// // Test if item is available for sale ///////////////////////////////////////////////////////////////////// private String testItemSku() { // create empty array of requested item SKU ArrayList<String> itemSkuArray = new ArrayList<String>(); // add our one item to the list itemSkuArray.add(itemSku); // convert the string array to input bundle Bundle itemSkuBundle = new Bundle(); itemSkuBundle.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, itemSkuArray); // query response bundle Bundle itemDataBundle = null; try { // get info for the one item sku itemDataBundle = inAppBillingService.getSkuDetails(IN_APP_BILLING_API_VERSION, packageName, itemType, itemSkuBundle); } // catch exceptions catch (Exception e) { return (exceptionMessage(PLAY_STORE_INVALID_PRODUCT_ID, e)); } // extract the response code from the response bundle int result = getResponseCodeFromBundle(itemDataBundle); if (result != RESULT_OK) { return (errorMessage(PLAY_STORE_INVALID_PRODUCT_ID, result)); } // response must have the following key value if (!itemDataBundle.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) return (errorMessage(PLAY_STORE_INVALID_PRODUCT_ID)); // extract response array ArrayList<String> itemDataArray = itemDataBundle.getStringArrayList(RESPONSE_GET_SKU_DETAILS_LIST); // this array must have one item if (itemDataArray.size() != 1) return (errorMessage(PLAY_STORE_INVALID_PRODUCT_ID)); // get the returned product ID and compare it to our itemSku try { // get JSON object JSONObject jsonObject = new JSONObject(itemDataArray.get(0)); // test product id if (!(jsonObject.optString(KEY_PRODUCT_ID)).equals(itemSku)) return (errorMessage(PLAY_STORE_INVALID_PRODUCT_ID)); // NOTE TO PROGRAMMERS // the jsonObject contains the following information // "productId" : "exampleSku" // "type" : "inapp" // "price" : "$5.00" // "title : "Example Title" // "description" : "This is an example description" } // JSON extraction failed catch (Exception e) { return (exceptionMessage(PLAY_STORE_INVALID_PRODUCT_ID, e)); } // Google play has our item return (null); } ///////////////////////////////////////////////////////////////////// //Test if item is owned by user ///////////////////////////////////////////////////////////////////// private String testItemOwned() { // assume item is not owned boolean itemOwned = false; String jsonItemData = null; // Continuation token // This will be used if user owns many items and the system will // break it into multiple blocks. String continueToken = null; // loop in case of large numbers of owned items for (;;) { // define owned items bundle Bundle ownedItems = null; // load next block of owned items try { // get all items owned by the user ownedItems = inAppBillingService.getPurchases(IN_APP_BILLING_API_VERSION, packageName, itemType, continueToken); } // system error catch (Exception e) { return (exceptionMessage(PLAY_STORE_LOAD_OWNED_PRODUCT_FAILED, e)); } // extract the response code from the bundle int result = getResponseCodeFromBundle(ownedItems); if (result != RESULT_OK) { return (errorMessage(PLAY_STORE_LOAD_OWNED_PRODUCT_FAILED, result)); } // response must have the following three key value pairs if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST) || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST) || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) return (errorMessage(PLAY_STORE_LOAD_OWNED_PRODUCT_FAILED)); // get all items ArrayList<String> itemSkuArray = ownedItems.getStringArrayList(RESPONSE_INAPP_ITEM_LIST); ArrayList<String> itemDataArray = ownedItems.getStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST); ArrayList<String> signatureArray = ownedItems.getStringArrayList(RESPONSE_INAPP_SIGNATURE_LIST); // make sure all three arrays have the same size int arraySize = itemSkuArray.size(); if (itemDataArray.size() != arraySize || signatureArray.size() != arraySize) return (errorMessage(PLAY_STORE_LOAD_OWNED_PRODUCT_FAILED)); // verify signatures of all items for (int Index = 0; Index < arraySize; Index++) { // item information String ownedItemSku = itemSkuArray.get(Index); String ownedItemData = itemDataArray.get(Index); String signature = signatureArray.get(Index); // verify signature if (!verifySignature(ownedItemData, signature)) return (errorMessage(PLAY_STORE_LOAD_OWNED_PRODUCT_FAILED)); // test for our item sku if (ownedItemSku.equals(itemSku)) { // our item is owned by user itemOwned = true; jsonItemData = ownedItemData; } // NOTE TO PROGRAMMERS // the ownedItemData contains the following information // it must be converted to JSON object // JSONOBJECT jsonObject = new JSONOBJECT(ownedItemData); // "orderId":"12999763169054705758.1371079406387615" // "packageName":"com.example.app", // "productId":"exampleSku", // "purchaseTime":1345678900000, (msec since 1970/01/01) // "purchaseState":0, // 0-purchased, 1 canceled, 2 refunded // "developerPayload":"example developer payload" // "purchaseToken" : "122333444455555", } // get continuation token continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN); // if continuation token is blank or empty, break out of the loop if (TextUtils.isEmpty(continueToken)) break; } // item is not owned if (!itemOwned) return (RESULT_TEXT_ITEM_NOT_OWNED); // purchase item if (!consumeActivity) return (RESULT_TEXT_ITEM_ALREADY_OWNED); // consume item // get the purchase token try { // create json object for item data JSONObject json = new JSONObject(jsonItemData); // get purchase token String itemPurchaseToken = json.optString(KEY_PURCHASE_TOKEN); // consume this item int result = inAppBillingService.consumePurchase(IN_APP_BILLING_API_VERSION, packageName, itemPurchaseToken); if (result != RESULT_OK) { return (errorMessage(PLAY_STORE_CONSUME_PRODUCT_FAILED, result)); } } catch (Exception e) { return (exceptionMessage(PLAY_STORE_CONSUME_PRODUCT_FAILED, e)); } // the item was consumed but return with already owned return (RESULT_TEXT_ITEM_ALREADY_OWNED); } ///////////////////////////////////////////////////////////////////// // get response code from a bundle // for intent response, call it with intent.getExtras() ///////////////////////////////////////////////////////////////////// private int getResponseCodeFromBundle(Bundle bundle) { Object responseCode = bundle.get(RESPONSE_CODE); if (responseCode == null) return (RESULT_OK); if (responseCode instanceof Integer) return (((Integer) responseCode).intValue()); if (responseCode instanceof Long) return ((int) ((Long) responseCode).longValue()); return (RESULT_INVALID_RESPONSE); } ///////////////////////////////////////////////////////////////////// // Verifies that the data was signed with the given signature. ///////////////////////////////////////////////////////////////////// private boolean verifySignature(String signedData, String signature) { try { // do it only once if (appPublicKey == null) { // decode application public key from base64 to binary byte[] decodedKey = decodeBase64(appPublicKeyStr); if (decodedKey == null) return (false); // convert public key from binary to PublicKey object appPublicKey = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM) .generatePublic(new X509EncodedKeySpec(decodedKey)); } // decode signature byte[] decodedSig = decodeBase64(signature); if (decodedSig == null) return (false); // verify signature Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM); sig.initVerify(appPublicKey); sig.update(signedData.getBytes()); return (sig.verify(decodedSig)); } catch (Exception e) { return (false); } } ///////////////////////////////////////////////////////////////////// // Decodes Base64 string into decoded byte array ///////////////////////////////////////////////////////////////////// private byte[] decodeBase64(String sourceStr) { // source string length int len = sourceStr.length(); // output buffer with worst case length // every 4 input makes 3 output plus 2 extra for last partial buffer byte[] outBuf = new byte[2 + len * 3 / 4]; // loop initialization int outBufPos = 0; int bytesBuf = 0; int bytesBufLen = 0; int index; for (index = 0; index < len; index++) { // decode next character int binChar = DecodeArray[(byte) sourceStr.charAt(index)]; // valid character if (binChar < PD) { // add to bytes buffer bytesBuf = (bytesBuf << 6) | binChar; bytesBufLen++; // we have 4 valid input characters if (bytesBufLen == 4) { // save three binary bytes outBuf[outBufPos++] = (byte) (bytesBuf >>> 16); outBuf[outBufPos++] = (byte) (bytesBuf >>> 8); outBuf[outBufPos++] = (byte) bytesBuf; // reset bytes buffer bytesBuf = 0; bytesBufLen = 0; } continue; } // padding character (must be at end of buffer) if (binChar == PD) break; // white space is ignored if (binChar == WS) continue; // error return (null); } // we have a final partial buffer if (index < len || bytesBufLen != 0) { // last partial buffer must be at least two characters if (bytesBufLen < 2) return (null); // make sure we have no more than two equal signs, white space is ok, all other error int equalCount = 0; for (; index < len; index++) { int binChar = DecodeArray[(byte) sourceStr.charAt(index)]; if (binChar == PD) { equalCount++; if (equalCount > 2) return (null); } else if (binChar != WS) return (null); } // last partial buffer has 2 input bytes translated to 12 output bits if (bytesBufLen == 2) { // take the most significant 8 bits outBuf[outBufPos++] = (byte) (bytesBuf >>> 4); } // last buffer has 3 input bytes translated to 18 output bits else { // take the most significant 16 bits outBuf[outBufPos++] = (byte) (bytesBuf >>> 10); outBuf[outBufPos++] = (byte) (bytesBuf >>> 2); } } // create final output array byte[] out = new byte[outBufPos]; System.arraycopy(outBuf, 0, out, 0, outBufPos); return (out); } ///////////////////////////////////////////////////////////////////// // compose error message with error location ///////////////////////////////////////////////////////////////////// private String errorMessage(String message) { return (message + "\n" + getErrorLocation(1)); } ///////////////////////////////////////////////////////////////////// // compose error message with error location ///////////////////////////////////////////////////////////////////// private String errorMessage(String message, int result) { return (message + "\n" + resultMessage(result) + "\n" + getErrorLocation(1)); } ///////////////////////////////////////////////////////////////////// // compose exception error message with error location ///////////////////////////////////////////////////////////////////// private String exceptionMessage(String message, Exception e) { return (message + "\n" + e.getMessage() + "\n" + getErrorLocation(1)); } ///////////////////////////////////////////////////////////////////// // error location based on stack frame ///////////////////////////////////////////////////////////////////// private String getErrorLocation(int stackFrame) { StackTraceElement element = new Throwable().getStackTrace()[stackFrame + 1]; String className = element.getClassName(); if (className.startsWith(packageName + ".")) className = className.substring(packageName.length() + 1); return ("Error location: " + element.getFileName() + " (" + Integer.toString(element.getLineNumber()) + ")\n" + className + "." + element.getMethodName()); } }