linphone-android/src/org/linphone/purchase/InAppPurchaseHelper.java
Margaux Clerc 5b264a6685 Add inapp purchase
Add account creator
2016-08-09 13:22:07 +02:00

371 lines
14 KiB
Java

package org.linphone.purchase;
/*
InAppPurchaseHelper.java
Copyright (C) 2015 Belledonne Communications, Grenoble, France
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program 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. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import java.util.ArrayList;
import java.util.regex.Pattern;
import org.json.JSONException;
import org.json.JSONObject;
import org.linphone.LinphonePreferences;
import org.linphone.mediastream.Log;
import org.linphone.xmlrpc.XmlRpcHelper;
import org.linphone.xmlrpc.XmlRpcListenerBase;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender.SendIntentException;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Patterns;
import com.android.vending.billing.IInAppBillingService;
/**
* @author Sylvain Berfini
*/
public class InAppPurchaseHelper {
public static final int API_VERSION = 3;
public static final int ACTIVITY_RESULT_CODE_PURCHASE_ITEM = 11089;
public static final String SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
public static final String SKU_DETAILS_LIST = "DETAILS_LIST";
public static final String SKU_DETAILS_PRODUCT_ID = "productId";
public static final String SKU_DETAILS_PRICE = "price";
public static final String SKU_DETAILS_TITLE = "title";
public static final String SKU_DETAILS_DESC = "description";
public static final String ITEM_TYPE_INAPP = "inapp";
public static final String ITEM_TYPE_SUBS = "subs";
public static final int RESPONSE_RESULT_OK = 0;
public static final int RESULT_USER_CANCELED = 1;
public static final int RESULT_SERVICE_UNAVAILABLE = 2;
public static final int RESULT_BILLING_UNAVAILABLE = 3;
public static final int RESULT_ITEM_UNAVAILABLE = 4;
public static final int RESULT_DEVELOPER_ERROR = 5;
public static final int RESULT_ERROR = 6;
public static final int RESULT_ITEM_ALREADY_OWNED = 7;
public static final int RESULT_ITEM_NOT_OWNED = 8;
public static final String RESPONSE_CODE = "RESPONSE_CODE";
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 RESPONSE_INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
public static final String PURCHASE_DETAILS_PRODUCT_ID = "productId";
public static final String PURCHASE_DETAILS_ORDER_ID = "orderId";
public static final String PURCHASE_DETAILS_AUTO_RENEWING = "autoRenewing";
public static final String PURCHASE_DETAILS_PURCHASE_TIME = "purchaseTime";
public static final String PURCHASE_DETAILS_PURCHASE_STATE = "purchaseState";
public static final String PURCHASE_DETAILS_PAYLOAD = "developerPayload";
public static final String PURCHASE_DETAILS_PURCHASE_TOKEN = "purchaseToken";
public static final String CLIENT_ERROR_SUBSCRIPTION_PURCHASE_NOT_AVAILABLE = "SUBSCRIPTION_PURCHASE_NOT_AVAILABLE";
public static final String CLIENT_ERROR_BIND_TO_BILLING_SERVICE_FAILED = "BIND_TO_BILLING_SERVICE_FAILED";
public static final String CLIENT_ERROR_BILLING_SERVICE_UNAVAILABLE = "BILLING_SERVICE_UNAVAILABLE";
private Context mContext;
private InAppPurchaseListener mListener;
private IInAppBillingService mService;
private ServiceConnection mServiceConn;
private Handler mHandler = new Handler();
private String mGmailAccount;
private String responseCodeToErrorMessage(int responseCode) {
switch (responseCode) {
case RESULT_USER_CANCELED:
return "BILLING_RESPONSE_RESULT_USER_CANCELED";
case RESULT_SERVICE_UNAVAILABLE:
return "BILLING_RESPONSE_RESULT_SERVICE_UNAVAILABLE";
case RESULT_BILLING_UNAVAILABLE:
return "BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE";
case RESULT_ITEM_UNAVAILABLE:
return "BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE";
case RESULT_DEVELOPER_ERROR:
return "BILLING_RESPONSE_RESULT_DEVELOPER_ERROR";
case RESULT_ERROR:
return "BILLING_RESPONSE_RESULT_ERROR";
case RESULT_ITEM_ALREADY_OWNED:
return "BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED";
case RESULT_ITEM_NOT_OWNED:
return "BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED";
}
return "UNKNOWN_RESPONSE_CODE";
}
public InAppPurchaseHelper(Activity context, InAppPurchaseListener listener) {
mContext = context;
mListener = listener;
mGmailAccount = getGmailAccount();
Log.d("[In-app purchase] creating InAppPurchaseHelper for context "+context.getLocalClassName());
mServiceConn = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d("[In-app purchase] onServiceDisconnected!");
mService = null;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d("[In-app purchase] onServiceConnected!");
mService = IInAppBillingService.Stub.asInterface(service);
String packageName = mContext.getPackageName();
try {
int response = mService.isBillingSupported(API_VERSION, packageName, ITEM_TYPE_SUBS);
if (response != RESPONSE_RESULT_OK || mGmailAccount == null) {
Log.e("[In-app purchase] Error: Subscriptions aren't supported!");
mListener.onError(CLIENT_ERROR_SUBSCRIPTION_PURCHASE_NOT_AVAILABLE);
} else {
mListener.onServiceAvailableForQueries();
}
} catch (RemoteException e) {
Log.e(e);
}
}
};
Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
serviceIntent.setPackage("com.android.vending");
if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {
boolean ok = mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
if (!ok) {
Log.e("[In-app purchase] Error: Bind service failed");
mListener.onError(CLIENT_ERROR_BIND_TO_BILLING_SERVICE_FAILED);
}
} else {
Log.e("[In-app purchase] Error: Billing service unavailable on device.");
mListener.onError(CLIENT_ERROR_BILLING_SERVICE_UNAVAILABLE);
}
}
private ArrayList<Purchasable> getAvailableItemsForPurchase() {
ArrayList<Purchasable> products = new ArrayList<Purchasable>();
ArrayList<String> skuList = LinphonePreferences.instance().getInAppPurchasables();
Bundle querySkus = new Bundle();
querySkus.putStringArrayList(SKU_DETAILS_ITEM_LIST, skuList);
Bundle skuDetails = null;
try {
skuDetails = mService.getSkuDetails(API_VERSION, mContext.getPackageName(), ITEM_TYPE_SUBS, querySkus);
} catch (RemoteException e) {
Log.e(e);
}
if (skuDetails != null) {
int response = skuDetails.getInt(RESPONSE_CODE);
if (response == RESPONSE_RESULT_OK) {
ArrayList<String> responseList = skuDetails.getStringArrayList(SKU_DETAILS_LIST);
for (String thisResponse : responseList) {
try {
JSONObject object = new JSONObject(thisResponse);
String id = object.getString(SKU_DETAILS_PRODUCT_ID);
String price = object.getString(SKU_DETAILS_PRICE);
String title = object.getString(SKU_DETAILS_TITLE);
String desc = object.getString(SKU_DETAILS_DESC);
Purchasable purchasable = new Purchasable(id).setTitle(title).setDescription(desc).setPrice(price);
Log.w("Purchasable item " + purchasable.getDescription());
products.add(purchasable);
} catch (JSONException e) {
Log.e(e);
}
}
} else {
Log.e("[In-app purchase] Error: responde code is not ok: " + responseCodeToErrorMessage(response));
mListener.onError(responseCodeToErrorMessage(response));
}
}
return products;
}
public void getAvailableItemsForPurchaseAsync() {
new Thread(new Runnable() {
public void run() {
final ArrayList<Purchasable> items = getAvailableItemsForPurchase();
if (mHandler != null && mListener != null) {
mHandler.post(new Runnable() {
public void run() {
mListener.onAvailableItemsForPurchaseQueryFinished(items);
}
});
}
}
}).start();
}
public void getPurchasedItemsAsync() {
new Thread(new Runnable() {
public void run() {
final ArrayList<Purchasable> items = new ArrayList<Purchasable>();
String continuationToken = null;
do {
Bundle purchasedItems = null;
try {
purchasedItems = mService.getPurchases(API_VERSION, mContext.getPackageName(), ITEM_TYPE_SUBS, continuationToken);
} catch (RemoteException e) {
Log.e(e);
}
if (purchasedItems != null) {
int response = purchasedItems.getInt(RESPONSE_CODE);
if (response == RESPONSE_RESULT_OK) {
ArrayList<String> purchaseDataList = purchasedItems.getStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST);
ArrayList<String> signatureList = purchasedItems.getStringArrayList(RESPONSE_INAPP_SIGNATURE_LIST);
continuationToken = purchasedItems.getString(RESPONSE_INAPP_CONTINUATION_TOKEN);
for (int i = 0; i < purchaseDataList.size(); ++i) {
String purchaseData = purchaseDataList.get(i);
String signature = signatureList.get(i);
Log.d("[In-app purchase] " + purchaseData);
Purchasable item = verifySignature(purchaseData, signature);
if (item != null) {
items.add(item);
}
}
} else {
Log.e("[In-app purchase] Error: responde code is not ok: " + responseCodeToErrorMessage(response));
mListener.onError(responseCodeToErrorMessage(response));
}
}
} while (continuationToken != null);
if (mHandler != null && mListener != null) {
mHandler.post(new Runnable() {
public void run() {
mListener.onPurchasedItemsQueryFinished(items);
}
});
}
}
}).start();
}
public void parseAndVerifyPurchaseItemResultAsync(int requestCode, int resultCode, Intent data) {
if (requestCode == ACTIVITY_RESULT_CODE_PURCHASE_ITEM) {
int responseCode = data.getIntExtra(RESPONSE_CODE, 0);
if (resultCode == Activity.RESULT_OK && responseCode == RESPONSE_RESULT_OK) {
String payload = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
String signature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
Purchasable item = LinphonePreferences.instance().getInAppPurchasedItem();
item.setPayloadAndSignature(payload, signature);
LinphonePreferences.instance().setInAppPurchasedItem(item);
XmlRpcHelper xmlRpcHelper = new XmlRpcHelper();
xmlRpcHelper.verifySignatureAsync(new XmlRpcListenerBase() {
@Override
public void onSignatureVerified(boolean success) {
mListener.onPurchasedItemConfirmationQueryFinished(success);
}
}, payload, signature);
}
}
}
private void purchaseItem(String productId, String sipIdentity) {
Bundle buyIntentBundle = null;
try {
buyIntentBundle = mService.getBuyIntent(API_VERSION, mContext.getPackageName(), productId, ITEM_TYPE_SUBS, sipIdentity);
} catch (RemoteException e) {
Log.e(e);
}
if (buyIntentBundle != null) {
PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
if (pendingIntent != null) {
try {
((Activity) mContext).startIntentSenderForResult(pendingIntent.getIntentSender(), ACTIVITY_RESULT_CODE_PURCHASE_ITEM, new Intent(), 0, 0, 0);
} catch (SendIntentException e) {
Log.e(e);
}
}
}
}
public void purchaseItemAsync(final String productId, final String sipIdentity) {
new Thread(new Runnable() {
public void run() {
purchaseItem(productId, sipIdentity);
}
}).start();
}
public void destroy() {
mContext.unbindService(mServiceConn);
}
public String getGmailAccount() {
Account[] accounts = AccountManager.get(mContext).getAccountsByType("com.google");
for (Account account: accounts) {
if (isEmailCorrect(account.name)) {
String possibleEmail = account.name;
return possibleEmail;
}
}
return null;
}
private boolean isEmailCorrect(String email) {
Pattern emailPattern = Patterns.EMAIL_ADDRESS;
return emailPattern.matcher(email).matches();
}
private Purchasable verifySignature(String payload, String signature) {
// TODO FIXME rework to be async
/*XmlRpcHelper helper = new XmlRpcHelper();
if (helper.verifySignature(payload, signature)) {
try {
JSONObject json = new JSONObject(payload);
String productId = json.getString(PURCHASE_DETAILS_PRODUCT_ID);
Purchasable item = new Purchasable(productId);
item.setPayloadAndSignature(payload, signature);
return item;
} catch (JSONException e) {
Log.e(e);
}
}*/
return null;
}
interface VerifiedSignatureListener {
void onParsedAndVerifiedSignatureQueryFinished(Purchasable item);
}
}