Android Open Source - gdx-pay Purchase Manager Android O U Y A






From Project

Back to project page gdx-pay.

License

The source code is released under:

Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUC...

If you think the Android project gdx-pay listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*******************************************************************************
 * Copyright 2011 See AUTHORS file.//from   w  w w . ja v a2 s  . c om
 *
 * 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 com.badlogic.gdx.pay.android.ouya;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.json.JSONException;
import org.json.JSONObject;

import tv.ouya.console.api.CancelIgnoringOuyaResponseListener;
import tv.ouya.console.api.OuyaEncryptionHelper;
import tv.ouya.console.api.OuyaErrorCodes;
import tv.ouya.console.api.OuyaFacade;
import tv.ouya.console.api.OuyaResponseListener;
import tv.ouya.console.api.Product;
import tv.ouya.console.api.Purchasable;
import tv.ouya.console.api.Receipt;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.ParseException;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Base64;
import android.util.Log;
import android.widget.Toast;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.pay.Information;
import com.badlogic.gdx.pay.PurchaseManager;
import com.badlogic.gdx.pay.PurchaseManagerConfig;
import com.badlogic.gdx.pay.PurchaseObserver;
import com.badlogic.gdx.pay.Transaction;

/** The purchase manager implementation for OUYA.
 * <p>
 * Include the gdx-pay-android-ouya.jar for this to work (plus gdx-pay-android.jar). Also update the "uses-permission" settings in
 * AndroidManifest.xml and your proguard settings.
 * 
 * @author just4phil */
public class PurchaseManagerAndroidOUYA implements PurchaseManager {

  /** Debug tag for logging. */
  private static final String TAG = "GdxPay/OUYA";
  private static final boolean LOGDEBUG = true;
  private static final boolean SHOWTOASTS = false;
  private static final int LOGTYPELOG = 0;
  private static final int LOGTYPEERROR = 1;

  /** Our Android activity. */
  Activity activity;

  /** The registered observer. */
  PurchaseObserver observer;
  /** The configuration. */
  PurchaseManagerConfig config;

  /** the ouya helper */
  OuyaFacade ouyaFacade;

  /** The OUYA cryptographic key for the application */
  PublicKey ouyaPublicKey;
  String applicationKeyPath;
  List<Purchasable> productIDList;   // --- This is the set of OUYA product IDs which our app knows about
  final Map<String, Product> ouyaOutstandingPurchaseRequests = new HashMap<String, Product>();
  ReceiptListener myOUYAreceiptListener = new ReceiptListener();
  List<Receipt> mReceiptList;     // the list of purchased items, sorted
  ArrayList<Product> productList = new ArrayList<Product>();
  Purchasable purchasable;       // for a concrete purchase
  Product OUYApurchaseProduct;
//  com.badlogic.gdx.pay.PurchaseListener appPurchaseListener; // this is the listener from the app that will be informed after a purchase

  // ------- for Toasts (debugging) -----
  String toastText;
  int duration;

  // --------------------------------------------------

  public PurchaseManagerAndroidOUYA (Activity activity, int requestCode) {
    this.activity = activity;
  }

  /** Used by IAP.java to determine if we are running on OUYA hardware :). */
  public static final boolean isRunningOnOUYAHardware () {
    // NOTE: - this would be nice but doesn't work yet (i.e. crashes before ouyaFacade.init(...) is called)
    // - promised to be fixed in the next SDK release...
    // - see for details: http://forums.ouya.tv/discussion/3772/bug-limit-in-isrunningonouyasupportedhardware
    // return OuyaFacade.getInstance().isRunningOnOUYAHardware();

    // let's determine if we are on OUYA-hardware by this hack...
    String name = android.os.Build.MODEL.toLowerCase();
    return (name.contains("ouya") || name.contains("mojo") || name.contains("m.o.j.o"));
  }

  @Override
  public String storeName () {
    return PurchaseManagerConfig.STORE_NAME_ANDROID_OUYA;
  }

  @Override
  public void install (final PurchaseObserver observer, PurchaseManagerConfig config) {
    this.observer = observer;
    this.config = config;

    // Obtain applicationKey and developer ID. Pass in as follows:
    // -------------------------------------------------------------------------
    //    config.addStoreParam(
    //      PurchaseManagerConfig.STORE_NAME_ANDROID_OUYA, 
    //      new Object[] { OUYA_DEVELOPERID_STRING, applicationKeyPathSTRING });
    // -------------------------------------------------------------------------

    Object[] configuration = (Object[])config.getStoreParam(storeName());
    String developerID = (String)configuration[0];
    applicationKeyPath = (String)configuration[1]; // store our OUYA applicationKey-Path!
    ouyaFacade = OuyaFacade.getInstance();
    ouyaFacade.init((Context)activity, developerID);

    // --- copy all available products to the list of purchasables
    productIDList = new ArrayList<Purchasable>(config.getOfferCount());
    for (int i = 0; i < config.getOfferCount(); i++) {
      productIDList.add(new Purchasable(config.getOffer(i).getIdentifierForStore(storeName())));
    }

    // Create a PublicKey object from the key data downloaded from the developer portal.
    try {
      // Read in the key.der file (downloaded from the developer portal)
      FileHandle fHandle = Gdx.files.internal(applicationKeyPath);
      byte[] applicationKey = fHandle.readBytes();

      X509EncodedKeySpec keySpec = new X509EncodedKeySpec(applicationKey);
      KeyFactory keyFactory = KeyFactory.getInstance("RSA");
      ouyaPublicKey = keyFactory.generatePublic(keySpec);
      showMessage(LOGTYPELOG, "succesfully created publicKey");

      // ---- request the productlist ---------
      requestProductList();

      // notify of successful initialization
      observer.handleInstall();

    } catch (Exception e) {
      // notify about the problem
      showMessage(LOGTYPEERROR, "Problem setting up in-app billing: Unable to create encryption key");
      observer.handleInstallError(new RuntimeException(
        "Problem setting up in-app billing: Unable to create encryption key: " + e));
    }
  }

  // ----- Handler --------------------

  Handler handler = new HandlerExtension(Looper.getMainLooper());

  final static int showToast = 0;
  final static int requestOUYAproducts = 1;
  final static int requestOUYApurchase = 2;
  final static int requestPurchaseRestore = 3;

  final class HandlerExtension extends Handler {

    public HandlerExtension(Looper mainLooper) {
      super(mainLooper);
    }

    @Override
    public void handleMessage (Message msg) {

      switch (msg.what) {

      case requestOUYAproducts:
        ouyaFacade.requestProductList(productIDList, productListListener);
        break;

      case requestOUYApurchase:
        ouyaFacade.requestPurchase(purchasable, new PurchaseListener(OUYApurchaseProduct));
        break;

      case requestPurchaseRestore:
        ouyaFacade.requestReceipts(myOUYAreceiptListener);
        break;

      case showToast:
        Toast toast = Toast.makeText(activity, toastText, duration);
        toast.show();
        break;
      }
    }
  }

  // ------------------------------------------------
  /** Request the receipts from the users previous purchases from the server. */
  public void requestPurchaseRestore () {
    handler.sendEmptyMessage(requestPurchaseRestore);
  }

  /** Request the available products from the server. */
  public void requestProductList () {
    handler.sendEmptyMessage(requestOUYAproducts);
  }

  /** make a purchase */
  @Override
  public void purchase (String identifier) {
    // String payload = null;

    OUYApurchaseProduct = getProductByStoreIdentifier(config.getOffer(identifier).getIdentifierForStore(storeName()));

    if (OUYApurchaseProduct != null) {
      try {
        requestPurchase(OUYApurchaseProduct);
        handler.sendEmptyMessage(requestOUYApurchase);

      } catch (UnsupportedEncodingException e) {
        observer.handlePurchaseError(e);
        e.printStackTrace();
      } catch (GeneralSecurityException e) {
        observer.handlePurchaseError(e);
        e.printStackTrace();
      } catch (JSONException e) {
        observer.handlePurchaseError(e);
        e.printStackTrace();
      }
    } else {
      showMessage(LOGTYPEERROR, "There has been a Problem with your Internet connection. Please try again later");
      observer.handlePurchaseError(new RuntimeException(
        "There has been a Problem with your Internet connection. Please try again later"));
    }
  }

  // -------------------------------------------------------------

  /** The callback for when the list of user receipts has been requested. */
  public class ReceiptListener implements OuyaResponseListener<String> {
    /** Handle the successful fetching of the data for the receipts from the server.
     * 
     * @param receiptResponse The response from the server. */
    @Override
    public void onSuccess (String receiptResponse) {
      OuyaEncryptionHelper helper = new OuyaEncryptionHelper();
      List<Receipt> receipts = null;
      try {
        JSONObject response = new JSONObject(receiptResponse);
        if (response.has("key") && response.has("iv")) {
          receipts = helper.decryptReceiptResponse(response, ouyaPublicKey);
        } else
          receipts = helper.parseJSONReceiptResponse(receiptResponse);

      } catch (ParseException e) {
        observer.handleRestoreError(e);
        throw new RuntimeException(e);
      } catch (JSONException e) {
        observer.handleRestoreError(e);
        throw new RuntimeException(e);
      } catch (GeneralSecurityException e) {
        observer.handleRestoreError(e);
        throw new RuntimeException(e);
      } catch (IOException e) {
        observer.handleRestoreError(e);
        throw new RuntimeException(e);
      } catch (java.text.ParseException e) {
        observer.handleRestoreError(e);
        e.printStackTrace();
      }
      Collections.sort(receipts, new Comparator<Receipt>() {
        @Override
        public int compare (Receipt lhs, Receipt rhs) {
          return rhs.getPurchaseDate().compareTo(lhs.getPurchaseDate());
        }
      });

      mReceiptList = receipts;
      List<Transaction> transactions = new ArrayList<Transaction>(mReceiptList.size());

      for (int i = 0; i < mReceiptList.size(); i++) {
        transactions.add(convertToTransaction(mReceiptList.get(i)));    // TODO: how can i get the uniqueID out of the JSON response!?
      }
      // send inventory to observer
      observer.handleRestore(transactions.toArray(new Transaction[transactions.size()]));

      // ========= not sure if this is needed?? ---- shouldnt this be the task of the app ??

      // if the observer above didn't throw an error, we consume all consumeables as needed
      // for (int i = 0; i < mReceiptList.size(); i++) {
        // Receipt receipt = mReceiptList.get(i);
        // Offer offer = config.getOffer(receipt.getIdentifier());
          // if (offer == null) {
            // Gdx.app.debug(TAG, "Offer not found for: " + receipt.getIdentifier());
          // }
          // else if (offer.getType() == OfferType.CONSUMABLE) {
            //
            // Gdx.app.log("TODO", "we have to consume incoming receipts?!");
            //
            // // it's a consumable, so we consume right away!
            // helper.consumeAsync(purchase, new IabHelper.OnConsumeFinishedListener() {
              // @Override
              // public void onConsumeFinished (Purchase purchase, IabResult result) {
              // if (!result.isSuccess()) {
                // // NOTE: we should only rarely have an exception due to e.g. network outages etc.
                // Gdx.app.error(TAG, "Error while consuming: " + result);
              // }
            // }
          // });
        // }
      // }
    }

    @Override
    public void onCancel () {
//      observer.handlePurchaseCanceled();   // this is minor relevant
      showMessage(LOGTYPELOG, "receiptlistener: user canceled");
    }

    @Override
    public void onFailure (int arg0, String arg1, Bundle arg2) {
      // observer.handleRestoreError(e);
      showMessage(LOGTYPEERROR, "receiptlistener: onFailure!");
    }
  }
  // ----------------------------

  OuyaResponseListener<ArrayList<Product>> productListListener = new CancelIgnoringOuyaResponseListener<ArrayList<Product>>() {
    @Override
    public void onSuccess (ArrayList<Product> products) {
      productList = products;
      showMessage(LOGTYPELOG, "successfully loaded productlist. " + productList.size() + " products found");
    }
    @Override
    public void onFailure (int errorCode, String errorMessage, Bundle errorBundle) {
      productList = null;
      showMessage(LOGTYPEERROR, "failed to load productlist!");
    }
  };
  // ---------------------------

  /** search for a specific product by identifier */
  public Product getProductByStoreIdentifier (String identifierForStore) {
    Product returnProduct = null;
    for (int i = 0; i < productList.size(); i++) {
      if (productList.get(i).getIdentifier().equals(identifierForStore)) {
        returnProduct = productList.get(i);
        break;
      }
    }
    return returnProduct;
  }
  // ------------------------------------------------

  public void requestPurchase (final Product product) throws GeneralSecurityException, UnsupportedEncodingException,
    JSONException {
    SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");

    // This is an ID that allows you to associate a successful purchase with
    // it's original request. The server does nothing with this string except
    // pass it back to you, so it only needs to be unique within this instance
    // of your app to allow you to pair responses with requests.
    String uniqueId = Long.toHexString(sr.nextLong());

    JSONObject purchaseRequest = new JSONObject();
    purchaseRequest.put("uuid", uniqueId);
    purchaseRequest.put("identifier", product.getIdentifier());
    // purchaseRequest.put("testing", "true"); // !!!! This value is only needed for testing, not setting it results in a live purchase
    String purchaseRequestJson = purchaseRequest.toString();

    byte[] keyBytes = new byte[16];
    sr.nextBytes(keyBytes);
    SecretKey key = new SecretKeySpec(keyBytes, "AES");

    byte[] ivBytes = new byte[16];
    sr.nextBytes(ivBytes);
    IvParameterSpec iv = new IvParameterSpec(ivBytes);

    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding", "BC");
    cipher.init(Cipher.ENCRYPT_MODE, key, iv);
    byte[] payload = cipher.doFinal(purchaseRequestJson.getBytes("UTF-8"));

    cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC");
    cipher.init(Cipher.ENCRYPT_MODE, ouyaPublicKey);
    byte[] encryptedKey = cipher.doFinal(keyBytes);

    purchasable = new Purchasable(product.getIdentifier(), Base64.encodeToString(encryptedKey, Base64.NO_WRAP),
      Base64.encodeToString(ivBytes, Base64.NO_WRAP), Base64.encodeToString(payload, Base64.NO_WRAP));

    synchronized (ouyaOutstandingPurchaseRequests) {
      ouyaOutstandingPurchaseRequests.put(uniqueId, product);
    }
  }
  // -----------------------------------------------------------

  /** The callback for when the user attempts to purchase something. We're not worried about the user cancelling the purchase so
   * we extend CancelIgnoringOuyaResponseListener, if you want to handle cancelations differently you should extend
   * OuyaResponseListener and implement an onCancel method.
   * 
   * @see tv.ouya.console.api.CancelIgnoringOuyaResponseListener
   * @see tv.ouya.console.api.OuyaResponseListener#onCancel() */
  private class PurchaseListener implements OuyaResponseListener<String> {
    /** The ID of the product the user is trying to purchase. This is used in the onFailure method to start a re-purchase if the
     * user wishes to do so. */
    private Product mProduct;

    PurchaseListener (final Product product) {
      mProduct = product;
    }

    /** Handle a successful purchase.
     * 
     * @param result The response from the server. */
    @Override
    public void onSuccess (String result) {
      Product product = null;
      Product storedProduct = null;
      String id = "";
      try {
        OuyaEncryptionHelper helper = new OuyaEncryptionHelper();
        JSONObject response = new JSONObject(result);
        
        if (response.has("key") && response.has("iv")) {
          id = helper.decryptPurchaseResponse(response, ouyaPublicKey);  // this seems to be the unique purchase ID // TODO: check what is the result here
//          String uniquePurchaseRequestID = "";
//          uniquePurchaseRequestID = response.getString("uuid");      // TODO: check what is the result here.... encrypted?
          synchronized (ouyaOutstandingPurchaseRequests) {
            storedProduct = ouyaOutstandingPurchaseRequests.remove(id);
            // showMessage("PurchaseListener: looks good ....");
          }
          if (storedProduct == null || !storedProduct.getIdentifier().equals(mProduct.getIdentifier())) {
            showMessage(LOGTYPEERROR, "Purchased product is not the same as purchase request product");
            onFailure(OuyaErrorCodes.THROW_DURING_ON_SUCCESS,
              "Purchased product is not the same as purchase request product", Bundle.EMPTY);
            return;
          }
        } else {
          product = new Product(new JSONObject(result));
          if (!mProduct.getIdentifier().equals(product.getIdentifier())) {
            showMessage(LOGTYPEERROR, "Purchased product is not the same as purchase request product");
            onFailure(OuyaErrorCodes.THROW_DURING_ON_SUCCESS,
              "Purchased product is not the same as purchase request product", Bundle.EMPTY);
            return;
          }
        }
      } catch (ParseException e) {
        observer.handlePurchaseError(e);
        onFailure(OuyaErrorCodes.THROW_DURING_ON_SUCCESS, e.getMessage(), Bundle.EMPTY);
      } catch (JSONException e) {
        observer.handlePurchaseError(e);
        onFailure(OuyaErrorCodes.THROW_DURING_ON_SUCCESS, e.getMessage(), Bundle.EMPTY);
        return;
      } catch (IOException e) {
        observer.handlePurchaseError(e);
        onFailure(OuyaErrorCodes.THROW_DURING_ON_SUCCESS, e.getMessage(), Bundle.EMPTY);
        return;
      } catch (GeneralSecurityException e) {
        observer.handlePurchaseError(e);
        onFailure(OuyaErrorCodes.THROW_DURING_ON_SUCCESS, e.getMessage(), Bundle.EMPTY);
        return;
      } catch (java.text.ParseException e) {
        observer.handlePurchaseError(e);
        e.printStackTrace();
        return;
      }

      // evrything is ok ...
      // purchaseRestore(); // check for purchases ..... would work but is not intended here

      if (storedProduct != null) {
        // convert product to transaction
        Transaction trans = convertPurchasedProductToTransaction(storedProduct, id);
        
        // inform the listener
        observer.handlePurchase(trans);
      } else {
        // appPurchaseListener.handlePurchaseError(e);
        showMessage(LOGTYPEERROR, "PurchaseListener: storedProduct == null!");
      }
    }

    /** Handle a failure. Because displaying the receipts is not critical to the application we just show an error message rather
     * than asking the user to authenticate themselves just to start the application up.
     * 
     * @param errorCode An HTTP error code between 0 and 999, if there was one. Otherwise, an internal error code from the Ouya
     *           server, documented in the {@link OuyaErrorCodes} class.
     * 
     * @param errorMessage Empty for HTTP error codes. Otherwise, a brief, non-localized, explanation of the error.
     * 
     * @param optionalData A Map of optional key/value pairs which provide additional information. */
    @Override
    public void onFailure (int errorCode, String errorMessage, Bundle optionalData) {
      // TODO: inform observer 
      showMessage(LOGTYPEERROR, "PurchaseListener: onFailure :(");
    }

    @Override
    public void onCancel () {
      observer.handlePurchaseCanceled();
      showMessage(LOGTYPELOG, "PurchaseListener: onCancel ...");
    }
  }
  // ---------------------------------------------

  @Override
  public void purchaseRestore () {
    handler.sendEmptyMessage(requestPurchaseRestore);
  }
  // --------------------------------------------

  /** Converts a product to our transaction object. */
  Transaction convertPurchasedProductToTransaction (Product product, String uniquePurchaseRequestID) {
    // build the transaction from the purchase object
    Transaction transaction = new Transaction();
    transaction.setIdentifier(config.getOfferForStore(storeName(), product.getIdentifier()).getIdentifier());
    transaction.setStoreName(storeName());
    transaction.setOrderId(uniquePurchaseRequestID);   //---- should work ---- TODO: Testing -------------------------
    
    transaction.setPurchaseTime(new Date());   // FIXME: we need the purchase date (from receipt!) ..... maybe this is correct.... this is the "receipt" for the purchased product
     transaction.setPurchaseText("Purchased for " + product.getFormattedPrice() + ".");
     transaction.setPurchaseCost((int)(product.getLocalPrice() * 100));  //--------------- TODO: not sure if this works in every case! 
     transaction.setPurchaseCostCurrency(product.getCurrencyCode());

      transaction.setReversalTime(null);
      transaction.setReversalText(null);
      transaction.setTransactionData(null);
      transaction.setTransactionDataSignature(null);

    showMessage(LOGTYPELOG, "converted purchased product to transaction.");
    return transaction;
  }

  /** Converts a purchase to our transaction object. */
  Transaction convertToTransaction (Receipt receipt) {
    // build the transaction from the purchase object
    Transaction transaction = new Transaction();
    transaction.setIdentifier(config.getOfferForStore(storeName(), receipt.getIdentifier()).getIdentifier());
    transaction.setStoreName(storeName());
    
    transaction.setOrderId("not available");    //----- TODO: how can i get the uniqueID out of the JSON response!?
    transaction.setPurchaseTime(receipt.getPurchaseDate());
    transaction.setPurchaseText("Purchased by \"" + receipt.getGamer() + "\" for " + receipt.getFormattedPrice() + ".");
    transaction.setPurchaseCost((int)(receipt.getLocalPrice() * 100));  //--------------- TODO: not sure if this works in every case! 
    transaction.setPurchaseCostCurrency(receipt.getCurrency());
    transaction.setReversalTime(null);
    transaction.setReversalText(null);
    transaction.setTransactionData(null);
    transaction.setTransactionDataSignature(null);

    showMessage(LOGTYPELOG, "converted receipt to transaction.");
    return transaction;
  }

  public void onActivityResult (int requestCode, int resultCode, Intent data) {
    // forwards activities to OpenIAB for processing
    // this is only relevant for android
  }

  @Override
  public String toString () {
    return storeName();
  }

  void showMessage (final int type, final String message) {
    if (LOGDEBUG) {
      if (type == LOGTYPELOG) Log.d(TAG, message);
      if (type == LOGTYPEERROR) Log.e(TAG, message);
    }
    if (SHOWTOASTS) {
      if (type == LOGTYPELOG) showToast(message);
      if (type == LOGTYPEERROR) showToast("error: " + message);
    }
  }

  // ---- saves the toast text and displays it
  void showToast (String toastText) {
    this.duration = Toast.LENGTH_SHORT;
    this.toastText = toastText;
    handler.sendEmptyMessage(showToast);
  }

  @Override
  public boolean installed () {
    return ouyaFacade != null;
  }

  @Override
  public void dispose () {
    if (ouyaFacade != null) {
      ouyaFacade.shutdown();
      ouyaFacade = null;

      productIDList = null;
      productList = null;
      
      // remove observer and config as well
      observer = null;
      config = null;

      showMessage(LOGTYPELOG, "disposed all the OUYA IAP stuff.");
    }
  }

    @Override
    public Information getInformation(String identifier) {
        // not implemented yet for this purchase manager
        return Information.UNAVAILABLE;
    }
}




Java Source Code List

com.badlogic.gdx.pay.Information.java
com.badlogic.gdx.pay.OfferType.java
com.badlogic.gdx.pay.Offer.java
com.badlogic.gdx.pay.PurchaseManagerConfig.java
com.badlogic.gdx.pay.PurchaseManager.java
com.badlogic.gdx.pay.PurchaseObserver.java
com.badlogic.gdx.pay.PurchaseSystem.java
com.badlogic.gdx.pay.Transaction.java
com.badlogic.gdx.pay.android.IAP.java
com.badlogic.gdx.pay.android.amazon.PurchaseManagerAndroidAmazon.java
com.badlogic.gdx.pay.android.openiab.PurchaseManagerAndroidOpenIAB.java
com.badlogic.gdx.pay.android.ouya.PurchaseManagerAndroidOUYA.java
com.badlogic.gdx.pay.desktop.apple.PurchaseManagerDesktopApple.java
com.badlogic.gdx.pay.gwt.googlewallet.PurchaseManagerGwtGoogleWallet.java
com.badlogic.gdx.pay.ios.apple.PurchaseManageriOSApple.java
com.badlogic.gdx.pay.server.PurchaseVerifierManager.java
com.badlogic.gdx.pay.server.PurchaseVerifier.java
com.badlogic.gdx.pay.server.impl.PurchaseVerifierAndroidAmazon.java
com.badlogic.gdx.pay.server.impl.PurchaseVerifierAndroidGoogle.java
com.badlogic.gdx.pay.server.impl.PurchaseVerifieriOSApple.java
com.badlogic.gdx.pay.server.util.Base64Util.java