Started in-app purchase
This commit is contained in:
parent
23b801c09b
commit
3d24d20194
13 changed files with 768 additions and 6 deletions
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="org.linphone"
|
package="org.linphone"
|
||||||
android:versionCode="2302" android:installLocation="auto">
|
android:versionCode="2400" android:installLocation="auto">
|
||||||
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="22"/>
|
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="22"/>
|
||||||
|
|
||||||
<!-- Permissions for Push Notification -->
|
<!-- Permissions for Push Notification -->
|
||||||
|
@ -40,6 +40,8 @@
|
||||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
|
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
|
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
|
||||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
|
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
|
||||||
|
<!-- Needed for in-app purchase -->
|
||||||
|
<uses-permission android:name="com.android.vending.BILLING" />
|
||||||
|
|
||||||
<supports-screens android:smallScreens="true" android:normalScreens="true" android:largeScreens="true" android:xlargeScreens="true" android:anyDensity="true"/>
|
<supports-screens android:smallScreens="true" android:normalScreens="true" android:largeScreens="true" android:xlargeScreens="true" android:anyDensity="true"/>
|
||||||
|
|
||||||
|
@ -132,6 +134,14 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name="org.linphone.purchase.InAppPurchaseActivity"
|
||||||
|
android:theme="@style/NoTitle"
|
||||||
|
android:screenOrientation="nosensor">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity android:name="org.linphone.setup.RemoteProvisioningLoginActivity"
|
<activity android:name="org.linphone.setup.RemoteProvisioningLoginActivity"
|
||||||
android:theme="@style/NoTitle"
|
android:theme="@style/NoTitle"
|
||||||
android:screenOrientation="nosensor">
|
android:screenOrientation="nosensor">
|
||||||
|
|
31
res/layout/in_app_purchasable.xml
Normal file
31
res/layout/in_app_purchasable.xml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="vertical" >
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingLeft="10dp"
|
||||||
|
android:paddingRight="10dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/button"
|
||||||
|
android:layout_centerInParent="true" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@android:color/black"/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
26
res/layout/in_app_store.xml
Normal file
26
res/layout/in_app_store.xml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/topLayout"
|
||||||
|
android:background="@drawable/background"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:paddingTop="40dp"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@android:color/black"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="In-app store"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/purchasable_items"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingTop="40dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
|
@ -13,6 +13,10 @@
|
||||||
android:title="@string/setup_title"
|
android:title="@string/setup_title"
|
||||||
android:key="@string/setup_key"/>
|
android:key="@string/setup_key"/>
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:title="In-app Store"
|
||||||
|
android:key="in_app_store"/>
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
android:title="@string/pref_add_account"
|
android:title="@string/pref_add_account"
|
||||||
android:key="@string/pref_add_account_key"/>
|
android:key="@string/pref_add_account_key"/>
|
||||||
|
|
144
src/com/android/vending/billing/IInAppBillingService.aidl
Normal file
144
src/com/android/vending/billing/IInAppBillingService.aidl
Normal file
|
@ -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);
|
||||||
|
}
|
|
@ -1151,4 +1151,8 @@ public class LinphonePreferences {
|
||||||
public boolean isContactsMigrationDone(){
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,6 @@ import java.util.List;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
import org.linphone.core.LinphoneAddress;
|
|
||||||
import org.linphone.core.LinphoneCall;
|
import org.linphone.core.LinphoneCall;
|
||||||
import org.linphone.core.LinphoneCall.State;
|
import org.linphone.core.LinphoneCall.State;
|
||||||
import org.linphone.core.LinphoneCore;
|
import org.linphone.core.LinphoneCore;
|
||||||
|
@ -48,7 +47,6 @@ import org.linphone.mediastream.Version;
|
||||||
import org.linphone.mediastream.video.capture.hwconf.Hacks;
|
import org.linphone.mediastream.video.capture.hwconf.Hacks;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
|
|
|
@ -35,6 +35,7 @@ import org.linphone.core.PayloadType;
|
||||||
import org.linphone.mediastream.Log;
|
import org.linphone.mediastream.Log;
|
||||||
import org.linphone.mediastream.Version;
|
import org.linphone.mediastream.Version;
|
||||||
import org.linphone.mediastream.video.capture.hwconf.AndroidCameraConfiguration;
|
import org.linphone.mediastream.video.capture.hwconf.AndroidCameraConfiguration;
|
||||||
|
import org.linphone.purchase.InAppPurchaseActivity;
|
||||||
import org.linphone.setup.SetupActivity;
|
import org.linphone.setup.SetupActivity;
|
||||||
import org.linphone.ui.LedPreference;
|
import org.linphone.ui.LedPreference;
|
||||||
import org.linphone.ui.PreferencesListFragment;
|
import org.linphone.ui.PreferencesListFragment;
|
||||||
|
@ -56,6 +57,7 @@ import android.preference.PreferenceScreen;
|
||||||
*/
|
*/
|
||||||
public class SettingsFragment extends PreferencesListFragment {
|
public class SettingsFragment extends PreferencesListFragment {
|
||||||
private static final int WIZARD_INTENT = 1;
|
private static final int WIZARD_INTENT = 1;
|
||||||
|
private static final int STORE_INTENT = 2;
|
||||||
private LinphonePreferences mPrefs;
|
private LinphonePreferences mPrefs;
|
||||||
private Handler mHandler = new Handler();
|
private Handler mHandler = new Handler();
|
||||||
private LinphoneCoreListenerBase mListener;
|
private LinphoneCoreListenerBase mListener;
|
||||||
|
@ -137,6 +139,14 @@ public class SettingsFragment extends PreferencesListFragment {
|
||||||
return true;
|
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
|
// Sets listener for each preference to update the matching value in linphonecore
|
||||||
|
|
97
src/org/linphone/purchase/InAppPurchaseActivity.java
Normal file
97
src/org/linphone/purchase/InAppPurchaseActivity.java
Normal file
|
@ -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<Purchasable> 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<Purchasable> 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);
|
||||||
|
}
|
||||||
|
}
|
331
src/org/linphone/purchase/InAppPurchaseHelper.java
Normal file
331
src/org/linphone/purchase/InAppPurchaseHelper.java
Normal file
|
@ -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<Purchasable> getAvailableItemsForPurchase() {
|
||||||
|
ArrayList<Purchasable> products = new ArrayList<Purchasable>();
|
||||||
|
ArrayList<String> skuList = new ArrayList<String> ();
|
||||||
|
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<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);
|
||||||
|
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<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) {
|
||||||
|
Log.d("[In-app purchase] response is 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] 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);
|
||||||
|
}
|
||||||
|
}
|
46
src/org/linphone/purchase/InAppPurchaseListener.java
Normal file
46
src/org/linphone/purchase/InAppPurchaseListener.java
Normal file
|
@ -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<Purchasable> items);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param items
|
||||||
|
*/
|
||||||
|
void onPurchasedItemsQueryFinished(ArrayList<Purchasable> items);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
void onPurchasedItemConfirmationQueryFinished(Purchasable item);
|
||||||
|
}
|
61
src/org/linphone/purchase/Purchasable.java
Normal file
61
src/org/linphone/purchase/Purchasable.java
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue