diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 6776daa4e..038571cd8 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,7 +1,7 @@ + android:versionCode="2400" android:installLocation="auto"> @@ -40,6 +40,8 @@ + + @@ -132,6 +134,14 @@ + + + + + + diff --git a/res/layout/in_app_purchasable.xml b/res/layout/in_app_purchasable.xml new file mode 100644 index 000000000..45a33e7b8 --- /dev/null +++ b/res/layout/in_app_purchasable.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/in_app_store.xml b/res/layout/in_app_store.xml new file mode 100644 index 000000000..d6ec049d1 --- /dev/null +++ b/res/layout/in_app_store.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/res/values/non_localizable_custom.xml b/res/values/non_localizable_custom.xml index de0d63ee6..16fe84ca0 100644 --- a/res/values/non_localizable_custom.xml +++ b/res/values/non_localizable_custom.xml @@ -2,7 +2,7 @@ false - + true 622464153529 diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 197455775..7e7af2c14 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -13,6 +13,10 @@ android:title="@string/setup_title" android:key="@string/setup_key"/> + + diff --git a/src/com/android/vending/billing/IInAppBillingService.aidl b/src/com/android/vending/billing/IInAppBillingService.aidl new file mode 100644 index 000000000..2a492f784 --- /dev/null +++ b/src/com/android/vending/billing/IInAppBillingService.aidl @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * 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.android.vending.billing; + +import android.os.Bundle; + +/** + * InAppBillingService is the service that provides in-app billing version 3 and beyond. + * This service provides the following features: + * 1. Provides a new API to get details of in-app items published for the app including + * price, type, title and description. + * 2. The purchase flow is synchronous and purchase information is available immediately + * after it completes. + * 3. Purchase information of in-app purchases is maintained within the Google Play system + * till the purchase is consumed. + * 4. An API to consume a purchase of an inapp item. All purchases of one-time + * in-app items are consumable and thereafter can be purchased again. + * 5. An API to get current purchases of the user immediately. This will not contain any + * consumed purchases. + * + * All calls will give a response code with the following possible values + * RESULT_OK = 0 - success + * RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog + * RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested + * RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase + * RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API + * RESULT_ERROR = 6 - Fatal error during the API action + * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned + * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned + */ +interface IInAppBillingService { + /** + * Checks support for the requested billing API version, package and in-app type. + * Minimum API version supported by this interface is 3. + * @param apiVersion the billing version which the app is using + * @param packageName the package name of the calling app + * @param type type of the in-app item being purchased "inapp" for one-time purchases + * and "subs" for subscription. + * @return RESULT_OK(0) on success, corresponding result code on failures + */ + int isBillingSupported(int apiVersion, String packageName, String type); + + /** + * Provides details of a list of SKUs + * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle + * with a list JSON strings containing the productId, price, title and description. + * This API can be called with a maximum of 20 SKUs. + * @param apiVersion billing API version that the Third-party is using + * @param packageName the package name of the calling app + * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST" + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on + * failure as listed above. + * "DETAILS_LIST" with a StringArrayList containing purchase information + * in JSON format similar to: + * '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00", + * "title : "Example Title", "description" : "This is an example description" }' + */ + Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle); + + /** + * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU, + * the type, a unique purchase token and an optional developer payload. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param sku the SKU of the in-app item as published in the developer console + * @param type the type of the in-app item ("inapp" for one-time purchases + * and "subs" for subscription). + * @param developerPayload optional argument to be sent back with the purchase information + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on + * failure as listed above. + * "BUY_INTENT" - PendingIntent to start the purchase flow + * + * The Pending intent should be launched with startIntentSenderForResult. When purchase flow + * has completed, the onActivityResult() will give a resultCode of OK or CANCELED. + * If the purchase is successful, the result data will contain the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on + * failure as listed above. + * "INAPP_PURCHASE_DATA" - String in JSON format similar to + * '{"orderId":"12999763169054705758.1371079406387615", + * "packageName":"com.example.app", + * "productId":"exampleSku", + * "purchaseTime":1345678900000, + * "purchaseToken" : "122333444455555", + * "developerPayload":"example developer payload" }' + * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that + * was signed with the private key of the developer + * TODO: change this to app-specific keys. + */ + Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type, + String developerPayload); + + /** + * Returns the current SKUs owned by the user of the type and package name specified along with + * purchase information and a signature of the data to be validated. + * This will return all SKUs that have been purchased in V3 and managed items purchased using + * V1 and V2 that have not been consumed. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param type the type of the in-app items being requested + * ("inapp" for one-time purchases and "subs" for subscription). + * @param continuationToken to be set as null for the first call, if the number of owned + * skus are too many, a continuationToken is returned in the response bundle. + * This method can be called again with the continuation token to get the next set of + * owned skus. + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on + * failure as listed above. + * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs + * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information + * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures + * of the purchase information + * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the + * next set of in-app purchases. Only set if the + * user has more owned skus than the current list. + */ + Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken); + + /** + * Consume the last purchase of the given SKU. This will result in this item being removed + * from all subsequent responses to getPurchases() and allow re-purchase of this item. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param purchaseToken token in the purchase information JSON that identifies the purchase + * to be consumed + * @return 0 if consumption succeeded. Appropriate error values for failures. + */ + int consumePurchase(int apiVersion, String packageName, String purchaseToken); +} diff --git a/src/org/linphone/LinphonePreferences.java b/src/org/linphone/LinphonePreferences.java index bdb89344a..1e2bc7216 100644 --- a/src/org/linphone/LinphonePreferences.java +++ b/src/org/linphone/LinphonePreferences.java @@ -1145,10 +1145,14 @@ public class LinphonePreferences { } public void contactsMigrationDone(){ - getConfig().setBool("app", "contacts_migration_done",true); + getConfig().setBool("app", "contacts_migration_done", true); } public boolean isContactsMigrationDone(){ - return getConfig().getBool("app", "contacts_migration_done",false); + return getConfig().getBool("app", "contacts_migration_done", false); + } + + public String getInAppPurchaseValidatingServerUrl() { + return getConfig().getString("in-app-purchase", "server_url", null); } } diff --git a/src/org/linphone/LinphoneUtils.java b/src/org/linphone/LinphoneUtils.java index 177d11534..be4e956dd 100644 --- a/src/org/linphone/LinphoneUtils.java +++ b/src/org/linphone/LinphoneUtils.java @@ -36,7 +36,6 @@ import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import org.linphone.core.LinphoneAddress; import org.linphone.core.LinphoneCall; import org.linphone.core.LinphoneCall.State; import org.linphone.core.LinphoneCore; @@ -48,7 +47,6 @@ import org.linphone.mediastream.Version; import org.linphone.mediastream.video.capture.hwconf.Hacks; import android.app.Activity; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.res.Resources; diff --git a/src/org/linphone/SettingsFragment.java b/src/org/linphone/SettingsFragment.java index 8c0fc659d..f8949173e 100644 --- a/src/org/linphone/SettingsFragment.java +++ b/src/org/linphone/SettingsFragment.java @@ -35,6 +35,7 @@ import org.linphone.core.PayloadType; import org.linphone.mediastream.Log; import org.linphone.mediastream.Version; import org.linphone.mediastream.video.capture.hwconf.AndroidCameraConfiguration; +import org.linphone.purchase.InAppPurchaseActivity; import org.linphone.setup.SetupActivity; import org.linphone.ui.LedPreference; import org.linphone.ui.PreferencesListFragment; @@ -56,6 +57,7 @@ import android.preference.PreferenceScreen; */ public class SettingsFragment extends PreferencesListFragment { private static final int WIZARD_INTENT = 1; + private static final int STORE_INTENT = 2; private LinphonePreferences mPrefs; private Handler mHandler = new Handler(); private LinphoneCoreListenerBase mListener; @@ -137,6 +139,14 @@ public class SettingsFragment extends PreferencesListFragment { return true; } }); + findPreference("in_app_store").setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Intent intent = new Intent(LinphoneService.instance(), InAppPurchaseActivity.class); + startActivityForResult(intent, STORE_INTENT); + return true; + } + }); } // Sets listener for each preference to update the matching value in linphonecore diff --git a/src/org/linphone/purchase/InAppPurchaseActivity.java b/src/org/linphone/purchase/InAppPurchaseActivity.java new file mode 100644 index 000000000..78292e2e2 --- /dev/null +++ b/src/org/linphone/purchase/InAppPurchaseActivity.java @@ -0,0 +1,97 @@ +package org.linphone.purchase; +/* +InAppPurchaseListener.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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +import java.util.ArrayList; + +import org.linphone.R; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * @author Sylvain Berfini + */ +public class InAppPurchaseActivity extends Activity implements InAppPurchaseListener, OnClickListener { + private InAppPurchaseHelper inAppPurchaseHelper; + private LinearLayout purchasableItems; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + inAppPurchaseHelper = new InAppPurchaseHelper(this, this); + + setContentView(R.layout.in_app_store); + purchasableItems = (LinearLayout) findViewById(R.id.purchasable_items); + } + + @Override + protected void onDestroy() { + inAppPurchaseHelper.destroy(); + super.onDestroy(); + } + + @Override + public void onServiceAvailableForQueries() { + inAppPurchaseHelper.getAvailableItemsForPurchaseAsync(); + } + + @Override + public void onAvailableItemsForPurchaseQueryFinished(ArrayList items) { + purchasableItems.removeAllViews(); + + for (Purchasable item : items) { + View layout = LayoutInflater.from(this).inflate(R.layout.in_app_purchasable, purchasableItems); + TextView text = (TextView) layout.findViewById(R.id.text); + text.setText(item.getTitle() + " (" + item.getPrice() + ")"); + ImageView image = (ImageView) layout.findViewById(R.id.image); + image.setTag(item); + image.setOnClickListener(this); + } + } + + @Override + public void onPurchasedItemsQueryFinished(ArrayList items) { + + } + + @Override + public void onPurchasedItemConfirmationQueryFinished(Purchasable item) { + + } + + @Override + public void onClick(View v) { + Purchasable item = (Purchasable) v.getTag(); + inAppPurchaseHelper.purchaseItemAsync(item.getId(), "sylvain@sip.linphone.org"); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + inAppPurchaseHelper.parseAndVerifyPurchaseItemResultAsync(requestCode, resultCode, data); + } +} diff --git a/src/org/linphone/purchase/InAppPurchaseHelper.java b/src/org/linphone/purchase/InAppPurchaseHelper.java new file mode 100644 index 000000000..52b9d1663 --- /dev/null +++ b/src/org/linphone/purchase/InAppPurchaseHelper.java @@ -0,0 +1,331 @@ +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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; + +import org.json.JSONException; +import org.json.JSONObject; +import org.linphone.LinphonePreferences; +import org.linphone.mediastream.Log; + +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 com.android.vending.billing.IInAppBillingService; + +import de.timroes.axmlrpc.XMLRPCCallback; +import de.timroes.axmlrpc.XMLRPCClient; +import de.timroes.axmlrpc.XMLRPCException; +import de.timroes.axmlrpc.XMLRPCServerException; + +/** + * @author Sylvain Berfini + */ +public class InAppPurchaseHelper { + public static final int API_VERSION = 3; + public static final String TEST_ITEM = "android.test.purchased"; + 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 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"; + + private Context mContext; + private InAppPurchaseListener mListener; + private IInAppBillingService mService; + private ServiceConnection mServiceConn; + private Handler mHandler = new Handler(); + + public InAppPurchaseHelper(Activity context, InAppPurchaseListener listener) { + mContext = context; + mListener = listener; + mServiceConn = new ServiceConnection() { + @Override + public void onServiceDisconnected(ComponentName name) { + mService = null; + Log.d("[In-app purchase] service disconnected"); + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + 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) { + Log.e("[In-app purchase] Error: Subscriptions aren't supported!"); + } else { + Log.d("[In-app purchase] service connected and subsciptions are available"); + 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"); + } else { + Log.d("[In-app purchase] service bound"); + } + } else { + Log.e("[In-app purchase] Error: Billing service unavailable on device."); + } + } + + private ArrayList getAvailableItemsForPurchase() { + ArrayList products = new ArrayList(); + ArrayList skuList = new ArrayList (); + skuList.add(TEST_ITEM); + 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) { + Log.d("[In-app purchase] response is OK"); + ArrayList 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); + Log.d("[In-app purchase] found purchasable " + title + " (" + desc + ") for " + price + " with id " + id); + + Purchasable purchasable = new Purchasable(id).setTitle(title).setDescription(desc).setPrice(price); + products.add(purchasable); + } catch (JSONException e) { + Log.e(e); + } + } + } else { + Log.e("[In-app purchase] Error: responde code is not ok: " + response); + } + } + + return products; + } + + public void getAvailableItemsForPurchaseAsync() { + new Thread(new Runnable() { + public void run() { + final ArrayList 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 items = new ArrayList(); + 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) { + Log.d("[In-app purchase] response is OK"); + ArrayList purchaseDataList = purchasedItems.getStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST); + ArrayList 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] Found purchase data: " + purchaseData); + + verifySignatureAsync(new VerifiedSignatureListener() { + @Override + public void onParsedAndVerifiedSignatureQueryFinished(Purchasable item) { + items.add(item); + } + }, purchaseData, signature); + } + } else { + Log.e("[In-app purchase] Error: responde code is not ok: " + response); + } + } + } while (continuationToken != null); + + if (mHandler != null && mListener != null) { + mHandler.post(new Runnable() { + public void run() { + mListener.onPurchasedItemsQueryFinished(items); + } + }); + } + } + }).start(); + } + + 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); + 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 parseAndVerifyPurchaseItemResultAsync(int requestCode, int resultCode, Intent data) { + if (requestCode == ACTIVITY_RESULT_CODE_PURCHASE_ITEM) { + int responseCode = data.getIntExtra(RESPONSE_CODE, 0); + String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA); + String signature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE); + + if (resultCode == Activity.RESULT_OK && responseCode == RESPONSE_RESULT_OK) { + Log.d("[In-app purchase] response is OK"); + verifySignatureAsync(new VerifiedSignatureListener() { + @Override + public void onParsedAndVerifiedSignatureQueryFinished(Purchasable item) { + mListener.onPurchasedItemConfirmationQueryFinished(item); + } + }, purchaseData, signature); + } else { + Log.e("[In-app purchase] Error: resultCode is " + resultCode + " and responseCode is " + responseCode); + } + } + } + + public void destroy() { + mContext.unbindService(mServiceConn); + } + + private void verifySignatureAsync(final VerifiedSignatureListener listener, String purchasedData, String signature) { + XMLRPCClient client = null; + try { + client = new XMLRPCClient(new URL(LinphonePreferences.instance().getInAppPurchaseValidatingServerUrl())); + } catch (MalformedURLException e) { + Log.e(e); + } + + if (client != null) { + client.callAsync(new XMLRPCCallback() { + @Override + public void onServerError(long id, XMLRPCServerException error) { + Log.e(error); + } + + @Override + public void onResponse(long id, Object result) { + try { + JSONObject object = new JSONObject((String)result); + String productId = object.getString(PURCHASE_DETAILS_PRODUCT_ID); + Log.d("[In-app purchase] Purchasable verified by server: " + productId); + Purchasable item = new Purchasable(productId); + //TODO parse JSON result to get the purchasable in it + listener.onParsedAndVerifiedSignatureQueryFinished(item); + } catch (JSONException e) { + Log.e(e); + } + } + + @Override + public void onError(long id, XMLRPCException error) { + Log.e(error); + } + }, "create_account_from_in_app_purchase", "android", purchasedData, signature); + } + } + + interface VerifiedSignatureListener { + void onParsedAndVerifiedSignatureQueryFinished(Purchasable item); + } +} diff --git a/src/org/linphone/purchase/InAppPurchaseListener.java b/src/org/linphone/purchase/InAppPurchaseListener.java new file mode 100644 index 000000000..c076a5905 --- /dev/null +++ b/src/org/linphone/purchase/InAppPurchaseListener.java @@ -0,0 +1,46 @@ +package org.linphone.purchase; +/* +InAppPurchaseListener.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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +import java.util.ArrayList; + +/** + * @author Sylvain Berfini + */ +public interface InAppPurchaseListener { + void onServiceAvailableForQueries(); + + /** + * + * @param items + */ + void onAvailableItemsForPurchaseQueryFinished(ArrayList items); + + /** + * + * @param items + */ + void onPurchasedItemsQueryFinished(ArrayList items); + + /** + * + * @param item + */ + void onPurchasedItemConfirmationQueryFinished(Purchasable item); +} diff --git a/src/org/linphone/purchase/Purchasable.java b/src/org/linphone/purchase/Purchasable.java new file mode 100644 index 000000000..9d0b9c253 --- /dev/null +++ b/src/org/linphone/purchase/Purchasable.java @@ -0,0 +1,61 @@ +package org.linphone.purchase; +/* +Purchasable.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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +/** + * @author Sylvain Berfini + */ +public class Purchasable { + private String id, title, description, price; + + public Purchasable(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public Purchasable setTitle(String title) { + this.title = title; + return this; + } + + public String getDescription() { + return description; + } + + public Purchasable setDescription(String description) { + this.description = description; + return this; + } + + public String getPrice() { + return price; + } + + public Purchasable setPrice(String price) { + this.price = price; + return this; + } +}