package me.barrasso.android.volume.utils;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Base64DataException;
import com.android.vending.billing.IInAppBillingService;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import me.barrasso.android.volume.LogUtils;
// Because not one enjoys Accounting.
public class Accountant {
private static String r(String s) {
return new StringBuilder(s).reverse().toString();
}
// TODO: this should NEVER be set to true for production!
public static final boolean FREE_FOR_ALL = true;
private IInAppBillingService mService;
private String mSignatureBase64;
private Context context;
protected boolean mConnected;
protected boolean mSetupDone;
protected boolean mSubscriptionsSupported;
private static Accountant mAccountant;
public static synchronized Accountant getInstance(Context context) {
LogUtils.LOGI("Accountant", "getInstance(" + context + "), mAccountant = " + mAccountant);
if (null == mAccountant)
mAccountant = new Accountant(context);
return mAccountant;
}
public static void destroyInstance() {
LogUtils.LOGI("Accountant", "destroyInstance()");
if (null != mAccountant) {
mAccountant.destroy();
mAccountant = null;
}
}
public Accountant(Context context) {
LogUtils.LOGI("Accountant", "new Accountant(" + context + ')');
this.context = context.getApplicationContext();
synchronized (this) {
// Build our Base64 Public Key... but in an annoying way!
// This makes static reverse engineering a pain in the butt.
StringBuilder b = new StringBuilder();
b.append('M');
b.append((char) 73);
b.append((char) 73);
b.append('B');
b.append((char) 73);
b.append(r("MkJnsAEQACKgCBIIMA8QACOAAFEQAB0w9GikhqkgBNAj"));
b.append('/');
b.append(r("IB9PgyKqXQ5RdjDO"));
b.append('/');
b.append(new String(new char[] { 76, 100, 97, 71, 70, 51 })); // LdaGF3
b.append("rvHk4tSabdTfA9PJr7FnlhO2H7rJaMzTSLriY");
b.append('/');
b.append("EXTvTI9xOhyKTHJyV1cmgCS6RA5VUsc3P3gPCIme");
b.append(r("bJBkSeWhLLis38ZBoiCl9BPZaLg34iTdiv3t1nf8m5WAm94kuWMhFE"));
b.append((char) 43);
b.append("FOiQxkVXd6ZHIrntBViIrry");
b.append('/');
b.append("0r6xg6qY8KLaworK" + 'O');
b.append('/');
b.append("zn85viDBGJWqKkbUOQCbhg1dmOj1xcPuCyJDSjH0jWx2gT3Vojwn9djcVOcoq+4puFGhOyFhLYNLPAX/");
b.append("j5ZG39FzcWq3xtn5POLUewvN581Zdl");
b.append(r("AQADIwnkAmAmlYN3RF3QHe60lAV84xL3Q"));
b.append('B');
mSignatureBase64 = b.toString();
}
connect();
}
public void destroy() {
LogUtils.LOGI("Accountant", "destroy()");
mSetupDone = false;
if (mService != null) {
if (context != null) {
try {
context.unbindService(mServiceConn);
} catch (Exception iae) {
LogUtils.LOGE("Accountant", "Error unbinding from the Google Play service.", iae);
}
}
}
mService = null;
mAccountant = null;
}
public IInAppBillingService getService() {
return mService;
}
/** @return All purchased SKUs */
public List<String> getPurchases() {
if (FREE_FOR_ALL) {
// TODO: include ALL in-app purchase SKUs here.
List<String> list = new ArrayList<String>(1);
list.add("theme_unlock");
return list;
}
if (null == mService) return new ArrayList<String>(0);
LogUtils.LOGI("Accountant", "getPurchases()");
try {
Bundle bundle = mService.getPurchases(3, context.getPackageName(), ITEM_TYPE_INAPP, null);
if (bundle.getInt(RESPONSE_CODE) == BILLING_RESPONSE_RESULT_OK) {
return bundle.getStringArrayList(RESPONSE_INAPP_ITEM_LIST);
}
} catch (RemoteException re) {
LogUtils.LOGE("Accountant", "Error obtaining in-app purchases.", re);
}
return new ArrayList<String>(0);
}
/** @return True if the user has purchased the given SKU. */
public Boolean has(String sku) {
if (FREE_FOR_ALL) return true;
if (null == mService || null == sku) return null;
LogUtils.LOGI("Accountant", "has(" + sku + ')');
List<String> skus = getPurchases();
return (null != skus && skus.contains(sku));
}
/** Launches the purchase flow (Google Play UI to complete in-app purchase. */
public boolean buy(Activity activity, String sku) {
if (null == mService || activity == null) return false;
LogUtils.LOGI("Accountant", "buy(" + sku + ')');
try {
Bundle bundle = mService.getBuyIntent(3, activity.getPackageName(),
sku, ITEM_TYPE_INAPP, null);
LogUtils.LOGD("Accountant", Utils.bundle2string(bundle));
PendingIntent pendingIntent = bundle.getParcelable(RESPONSE_BUY_INTENT);
if (bundle.getInt(RESPONSE_CODE) == BILLING_RESPONSE_RESULT_OK) {
// Start purchase flow (this brings up the Google Play UI).
// Result will be delivered through onActivityResult().
activity.startIntentSenderForResult(pendingIntent.getIntentSender(),
RESULT_CODE_BUY, new Intent(), 0, 0, 0);
return true;
}
} catch (Throwable t) {
LogUtils.LOGE("Accountant", "Error launching in-app purchase Intent.", t);
}
return false;
}
public boolean connect() {
LogUtils.LOGI("Accountant", "connect()");
if (null != mService) return true;
Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
serviceIntent.setPackage("com.android.vending");
if (!context.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {
Intent nServiceIntent = Utils.createExplicitFromImplicitIntent(context, serviceIntent);
return context.bindService(((null != nServiceIntent) ? nServiceIntent : serviceIntent),
mServiceConn, Context.BIND_AUTO_CREATE);
} else {
// LISTENER: no service available to handle that Intent
return false;
}
}
public String getSignatureBase64() { return mSignatureBase64; }
public boolean inAppPurchasesSupported() {
return mSetupDone;
}
ServiceConnection mServiceConn = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
LogUtils.LOGI("Accountant", "onServiceDisconnected(" + name.toString() + ')');
mService = null;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
LogUtils.LOGI("Accountant", "onServiceConnected(" + name.toString() + ')');
mService = IInAppBillingService.Stub.asInterface(service);
String packageName = context.getPackageName();
try {
// check for in-app billing v3 support
int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
if (response != BILLING_RESPONSE_RESULT_OK) {
// LISTENER: No in-app billing
// if in-app purchases aren't supported, neither are subscriptions.
mSubscriptionsSupported = false;
return;
}
// check for v3 subscriptions support
response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
if (response == BILLING_RESPONSE_RESULT_OK) {
mSubscriptionsSupported = true;
} else {
// LISTENER: Doesn't support version 3.
}
mSetupDone = true;
} catch (RemoteException e) {
// LISTENER: RemoteException
e.printStackTrace();
}
// LISTENER: SUCCESS!
}
};
// 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;
public static final int RESULT_CODE_BUY = 1984;
// IAB Helper error codes
public static final int RESPONSE_ERROR_BASE = -1000;
public static final int RESPONSE_REMOTE_EXCEPTION = -1001;
public static final int RESPONSE_BAD_RESPONSE = -1002;
public static final int RESPONSE_VERIFICATION_FAILED = -1003;
public static final int RESPONSE_SEND_INTENT_FAILED = -1004;
public static final int RESPONSE_USER_CANCELLED = -1005;
public static final int RESPONSE_UNKNOWN_PURCHASE_RESPONSE = -1006;
public static final int RESPONSE_MISSING_TOKEN = -1007;
public static final int RESPONSE_UNKNOWN_ERROR = -1008;
public static final int RESPONSE_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
public static final int RESPONSE_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";
private static final String KEY_FACTORY_ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
/**
* Verifies that the data was signed with the given signature, and returns
* the verified purchase. The data is in JSON format and signed
* with a private key.
* @param base64PublicKey the base64-encoded public key to use for verifying.
* @param signedData the signed JSON string (signed, not encrypted)
* @param signature the signature for the data, signed with the private key
*/
public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
if (signedData == null) {
return false;
}
boolean verified;
if (!TextUtils.isEmpty(signature)) {
PublicKey key = generatePublicKey(base64PublicKey);
verified = verify(key, signedData, signature);
if (!verified) {
return false;
}
}
return true;
}
/**
* Generates a PublicKey instance from a string containing the
* Base64-encoded public key.
*
* @param encodedPublicKey Base64-encoded public key
* @throws IllegalArgumentException if encodedPublicKey is invalid
*/
public static PublicKey generatePublicKey(String encodedPublicKey) {
try {
byte[] decodedKey = Base64.decode(encodedPublicKey, 0);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (InvalidKeySpecException e) {
LogUtils.LOGE("Accountant", "Invalid key specification.", e);
throw new IllegalArgumentException(e);
}
}
/**
* Verifies that the signature from the server matches the computed
* signature on the data. Returns true if the data is correctly signed.
*
* @param publicKey public key associated with the developer account
* @param signedData signed data from server
* @param signature server signature
* @return true if the data and signature match
*/
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
Signature sig;
try {
sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
return sig.verify(Base64.decode(signature, 0));
} catch (NoSuchAlgorithmException e) {
LogUtils.LOGE("Accountant", "NoSuchAlgorithmException.", e);
} catch (InvalidKeyException e) {
LogUtils.LOGE("Accountant", "Invalid key specification.", e);
} catch (SignatureException e) {
LogUtils.LOGE("Accountant", "Signature exception.", e);
}
return false;
}
}