Merge remote-tracking branch 'origin/dev_in_app_purchase'

This commit is contained in:
Margaux Clerc 2016-06-29 14:04:30 +02:00
commit 930b059a84
19 changed files with 1183 additions and 38 deletions

View file

@ -41,6 +41,8 @@
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
<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"/>
@ -133,6 +135,14 @@
</intent-filter>
</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.assistant.RemoteProvisioningLoginActivity"
android:theme="@style/NoTitle"
android:screenOrientation="nosensor">

View file

@ -0,0 +1,19 @@
<?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" >
<Button
android:id="@+id/inapp_button"
android:text="@string/assistant_create_account"
android:background="@drawable/assistant_button"
style="@style/font8"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:layout_width="match_parent"
android:layout_height="40dp"/>
</LinearLayout>

110
res/layout/in_app_store.xml Normal file
View file

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:background="@color/colorH"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<LinearLayout
android:id="@+id/topbar"
android:background="@color/colorF"
android:orientation="horizontal"
android:layout_gravity="center_horizontal"
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:text="@string/assistant"
style="@style/font1"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="30dp"
android:gravity="center"
android:orientation="vertical">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/username_label"
android:text="@string/username"
style="@style/font13"
android:textAllCaps="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<EditText
android:id="@+id/username"
android:background="@drawable/resizable_textfield"
android:textColor="@color/colorB"
android:contentDescription="@string/content_description_username_field"
android:inputType="text|textEmailAddress"
android:layout_width="match_parent"
android:layout_height="40dp"
android:singleLine="true"/>
<TextView
android:id="@+id/username_error"
android:text="@string/error"
style="@style/font20"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="invisible"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/email_label"
android:text="@string/email"
style="@style/font13"
android:textAllCaps="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<EditText
android:id="@+id/email"
android:background="@drawable/resizable_textfield"
android:textColor="@color/colorB"
android:inputType="text|textEmailAddress"
android:contentDescription="@string/content_description_email_field"
android:textCursorDrawable="@null"
android:layout_width="match_parent"
android:layout_height="40dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:singleLine="true"/>
</LinearLayout>
<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"/>
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="10dp"
android:paddingRight="10dp" />
</LinearLayout>
</LinearLayout>

View file

@ -31,4 +31,8 @@ dtmf_player_amp=0.1
ec_calibrator_cool_tones=1
[misc]
max_calls=10
max_calls=10
[in-app-purchase]
server_url=https://www.linphone.org/xmlrpc.php
purchasable_items_ids=test_account_subscription

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- New settings -->
<string name="default_domain">sip.linphone.org</string>
<string name="default_stun">stun.linphone.org</string>
@ -13,6 +14,7 @@
<bool name="show_statusbar_only_on_dialer">false</bool>
<bool name="replace_assistant_with_old_interface">false</bool>
<bool name="force_use_of_linphone_friends">false</bool>
<bool name="in_app_purchase_in_settings">false</bool>
<string name="wizard_url">https://www.linphone.org/wizard.php</string>

View file

@ -34,6 +34,7 @@
<string name="pref_sipaccounts_key">pref_sipaccounts_key</string>
<string name="setup_key">setup_key</string>
<string name="pref_add_account_key">pref_add_account_key</string>
<string name="pref_in_app_store_key">pref_in_app_store_key</string>
<string name="pref_video_key">pref_video_key</string>
<string name="pref_video_codecs_key">pref_video_codecs_key</string>

View file

@ -221,6 +221,7 @@
<string name="pref_sipaccounts">SIP Accounts</string>
<string name="default_account_flag">Default account</string>
<string name="pref_add_account">Add account</string>
<string name="pref_in_app_store">In-app Store</string>
<string name="pref_tunnel">Tunnel</string>
<string name="pref_tunnel_host">Hostname</string>
<string name="pref_tunnel_port">Port</string>

View file

@ -4,11 +4,15 @@
<PreferenceCategory
android:title="@string/pref_sipaccounts"
android:key="@string/pref_sipaccounts_key"/>
<Preference
android:title="@string/pref_add_account"
android:key="@string/pref_add_account_key"/>
<Preference
android:title="@string/pref_in_app_store"
android:key="@string/pref_in_app_store_key"/>
<PreferenceCategory
android:title="@string/pref_tunnel"
android:key="@string/pref_tunnel_key">

View 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);
}

View file

@ -19,6 +19,7 @@ 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 java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
@ -39,6 +40,7 @@ import org.linphone.core.LinphoneProxyConfig;
import org.linphone.core.LpConfig;
import org.linphone.core.TunnelConfig;
import org.linphone.mediastream.Log;
import org.linphone.purchase.Purchasable;
import android.content.Context;
@ -1237,7 +1239,44 @@ public class LinphonePreferences {
}
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);
}
public Purchasable getInAppPurchasedItem() {
String id = getConfig().getString("in-app-purchase", "purchase_item_id", null);
String payload = getConfig().getString("in-app-purchase", "purchase_item_payload", null);
String signature = getConfig().getString("in-app-purchase", "purchase_item_signature", null);
String username = getConfig().getString("in-app-purchase", "purchase_item_username", null);
Purchasable item = new Purchasable(id).setPayloadAndSignature(payload, signature).setUserData(username);
return item;
}
public void setInAppPurchasedItem(Purchasable item) {
if (item == null)
return;
getConfig().setString("in-app-purchase", "purchase_item_id", item.getId());
getConfig().setString("in-app-purchase", "purchase_item_payload", item.getPayload());
getConfig().setString("in-app-purchase", "purchase_item_signature", item.getPayloadSignature());
getConfig().setString("in-app-purchase", "purchase_item_username", item.getUserData());
}
public ArrayList<String> getInAppPurchasables() {
ArrayList<String> purchasables = new ArrayList<String>();
String list = getConfig().getString("in-app-purchase", "purchasable_items_ids", null);
if (list != null) {
for(String purchasable : list.split(";")) {
if (purchasable.length() > 0) {
purchasables.add(purchasable);
}
}
}
return purchasables;
}
public String getXmlRpcServerUrl() {

View file

@ -34,8 +34,10 @@ 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.ui.LedPreference;
import org.linphone.ui.PreferencesListFragment;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
@ -52,6 +54,8 @@ import android.preference.PreferenceScreen;
* @author Sylvain Berfini
*/
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;
@ -112,6 +116,14 @@ public class SettingsFragment extends PreferencesListFragment {
return true;
}
});
findPreference(getString(R.string.pref_in_app_store_key)).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
@ -134,6 +146,11 @@ public class SettingsFragment extends PreferencesListFragment {
hidePreference(R.string.pref_add_account_key);
}
if(!getResources().getBoolean(R.bool.in_app_purchase_in_settings)){
hidePreference(R.string.pref_in_app_store_key);
}
if (getResources().getBoolean(R.bool.disable_animations)) {
uncheckAndHidePreference(R.string.pref_animation_enable_key);
}

View file

@ -88,7 +88,7 @@ public class RemoteProvisioningLoginActivity extends Activity implements OnClick
private boolean storeAccount(String username, String password, String domain) {
LinphoneCore lc = LinphoneManager.getLc();
XmlRpcHelper xmlRpcHelper = new XmlRpcHelper(null);
XmlRpcHelper xmlRpcHelper = new XmlRpcHelper();
xmlRpcHelper.getRemoteProvisioningFilenameAsync(new XmlRpcListenerBase() {
@Override
public void onRemoteProvisioningFilenameSent(String result) {

View file

@ -0,0 +1,240 @@
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 java.util.Locale;
import org.linphone.LinphoneManager;
import org.linphone.R;
import org.linphone.core.LinphoneProxyConfig;
import org.linphone.mediastream.Log;
import org.linphone.xmlrpc.XmlRpcHelper;
import org.linphone.xmlrpc.XmlRpcListenerBase;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
/**
* @author Sylvain Berfini
*/
public class InAppPurchaseActivity extends Activity implements InAppPurchaseListener, OnClickListener {
private InAppPurchaseHelper inAppPurchaseHelper;
private LinearLayout purchasableItemsLayout;
private ArrayList<Purchasable> purchasedItems;
private Button buyItemButton, recoverAccountButton;
private Handler mHandler = new Handler();
private EditText username, email;
private TextView errorMessage;
private boolean usernameOk = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
inAppPurchaseHelper = new InAppPurchaseHelper(this, this);
setContentView(R.layout.in_app_store);
purchasableItemsLayout = (LinearLayout) findViewById(R.id.purchasable_items);
username = (EditText) findViewById(R.id.username);
email = (EditText) findViewById(R.id.email);
errorMessage = (TextView) findViewById(R.id.username_error);
addUsernameHandler(username, errorMessage);
}
@Override
protected void onDestroy() {
inAppPurchaseHelper.destroy();
super.onDestroy();
}
@Override
public void onServiceAvailableForQueries() {
email.setText(inAppPurchaseHelper.getGmailAccount());
email.setEnabled(false);
inAppPurchaseHelper.getPurchasedItemsAsync();
}
@Override
public void onAvailableItemsForPurchaseQueryFinished(ArrayList<Purchasable> items) {
purchasableItemsLayout.removeAllViews();
for (Purchasable item : items) {
displayBuySubscriptionButton(item);
}
}
@Override
public void onPurchasedItemsQueryFinished(ArrayList<Purchasable> items) {
purchasedItems = items;
if (items == null || items.size() == 0) {
inAppPurchaseHelper.getAvailableItemsForPurchaseAsync();
} else {
for (Purchasable purchasedItem : purchasedItems) {
Log.d("[In-app purchase] Found already bought item, expires " + purchasedItem.getExpireDate());
displayRecoverAccountButton(purchasedItem);
}
}
}
@Override
public void onPurchasedItemConfirmationQueryFinished(boolean success) {
if (success) {
XmlRpcHelper xmlRpcHelper = new XmlRpcHelper();
xmlRpcHelper.createAccountAsync(new XmlRpcListenerBase() {
@Override
public void onAccountCreated(String result) {
//TODO
}
}, getUsername(), email.getText().toString(), null);
}
}
@Override
public void onClick(View v) {
Purchasable item = (Purchasable) v.getTag();
if (v.equals(recoverAccountButton)) {
XmlRpcHelper xmlRpcHelper = new XmlRpcHelper();
xmlRpcHelper.createAccountAsync(new XmlRpcListenerBase() {
@Override
public void onAccountCreated(String result) {
//TODO
}
}, getUsername(), email.getText().toString(), null);
} else {
inAppPurchaseHelper.purchaseItemAsync(item.getId(), getUsername());
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
inAppPurchaseHelper.parseAndVerifyPurchaseItemResultAsync(requestCode, resultCode, data);
}
@Override
public void onRecoverAccountSuccessful(boolean success) {
mHandler.post(new Runnable() {
@Override
public void run() {
recoverAccountButton.setEnabled(false);
}
});
}
@Override
public void onError(final String error) {
Log.e(error);
mHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(InAppPurchaseActivity.this, error, Toast.LENGTH_LONG).show();
}
});
}
@Override
public void onActivateAccountSuccessful(boolean success) {
if (success) {
Log.d("[In-app purchase] Account activated");
}
}
private void displayBuySubscriptionButton(Purchasable item) {
View layout = LayoutInflater.from(this).inflate(R.layout.in_app_purchasable, purchasableItemsLayout);
Button button = (Button) layout.findViewById(R.id.inapp_button);
button.setText("Buy account (" + item.getPrice() + ")");
button.setTag(item);
button.setOnClickListener(this);
XmlRpcHelper xmlRpcHelper = new XmlRpcHelper();
xmlRpcHelper.createAccountAsync(new XmlRpcListenerBase() {
@Override
public void onAccountCreated(String result) {
//TODO
}
}, getUsername(), email.getText().toString(), null);
buyItemButton = button;
buyItemButton.setEnabled(usernameOk);
}
private void displayRecoverAccountButton(Purchasable item) {
View layout = LayoutInflater.from(this).inflate(R.layout.in_app_purchasable, purchasableItemsLayout);
Button button = (Button) layout.findViewById(R.id.inapp_button);
button.setText("Recover account");
button.setTag(item);
button.setOnClickListener(this);
recoverAccountButton = button;
recoverAccountButton.setEnabled(usernameOk);
}
private String getUsername() {
String username = this.username.getText().toString();
LinphoneProxyConfig lpc = LinphoneManager.getLc().createProxyConfig();
username = lpc.normalizePhoneNumber(username);
return username.toLowerCase(Locale.getDefault());
}
private boolean isUsernameCorrect(String username) {
LinphoneProxyConfig lpc = LinphoneManager.getLc().createProxyConfig();
return lpc.isPhoneNumber(username);
}
private void addUsernameHandler(final EditText field, final TextView errorMessage) {
field.addTextChangedListener(new TextWatcher() {
public void afterTextChanged(Editable s) {
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int count, int after) {
usernameOk = false;
String username = s.toString();
if (isUsernameCorrect(username)) {
usernameOk = true;
errorMessage.setText("");
} else {
errorMessage.setText(R.string.wizard_username_incorrect);
}
if (buyItemButton != null) buyItemButton.setEnabled(usernameOk);
if (recoverAccountButton != null) recoverAccountButton.setEnabled(usernameOk);
}
});
}
}

View file

@ -0,0 +1,360 @@
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.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();
mServiceConn = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
mService = null;
}
@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 || 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);
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);
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) {
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);
}
}

View file

@ -0,0 +1,67 @@
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 {
/**
* Callback called when the in-app purchase listener is connected and available for queries
*/
void onServiceAvailableForQueries();
/**
* Callback called when the query for items available for purchase is done
* @param items the list of items that can be purchased (also contains the ones already bought)
*/
void onAvailableItemsForPurchaseQueryFinished(ArrayList<Purchasable> items);
/**
* Callback called when the query for items bought by the user is done
* @param items the list of items already purchased by the user
*/
void onPurchasedItemsQueryFinished(ArrayList<Purchasable> items);
/**
* Callback called when the purchase has been validated by our external server
* @param success true if ok, false otherwise
*/
void onPurchasedItemConfirmationQueryFinished(boolean success);
/**
* Callback called when the account has been recovered (or not)
* @param success true if the recover has been successful, false otherwise
*/
void onRecoverAccountSuccessful(boolean success);
/**
* Callback called when the account has been activated (or not)
* @param success true if the activation has been successful, false otherwise
*/
void onActivateAccountSuccessful(boolean success);
/**
* Callback called when an error occurred.
* @param error the error that occurred
*/
void onError(String error);
}

View file

@ -0,0 +1,47 @@
package org.linphone.purchase;
import java.util.ArrayList;
public class InAppPurchaseListenerBase implements InAppPurchaseListener {
@Override
public void onServiceAvailableForQueries() {
// TODO Auto-generated method stub
}
@Override
public void onAvailableItemsForPurchaseQueryFinished(ArrayList<Purchasable> items) {
// TODO Auto-generated method stub
}
@Override
public void onPurchasedItemsQueryFinished(ArrayList<Purchasable> items) {
// TODO Auto-generated method stub
}
@Override
public void onPurchasedItemConfirmationQueryFinished(boolean success) {
// TODO Auto-generated method stub
}
@Override
public void onRecoverAccountSuccessful(boolean success) {
// TODO Auto-generated method stub
}
@Override
public void onActivateAccountSuccessful(boolean success) {
// TODO Auto-generated method stub
}
@Override
public void onError(String error) {
// TODO Auto-generated method stub
}
}

View file

@ -0,0 +1,108 @@
package org.linphone.purchase;
import java.sql.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Locale;
/*
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;
private long expire;
private String purchasePayload, purchasePayloadSignature;
private String userData;
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;
}
public long getExpire() {
return expire;
}
public String getExpireDate() {
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault());
Date date = new Date(expire);
return dateFormat.format(date);
}
public Purchasable setExpire(long expire) {
this.expire = expire;
return this;
}
public Purchasable setPayloadAndSignature(String payload, String signature) {
this.purchasePayload = payload;
this.purchasePayloadSignature = signature;
return this;
}
public String getPayload() {
return this.purchasePayload;
}
public String getPayloadSignature() {
return this.purchasePayloadSignature;
}
public Purchasable setUserData(String data) {
this.userData = data;
return this;
}
public String getUserData() {
return this.userData;
}
}

View file

@ -24,14 +24,10 @@ public class XmlRpcHelper {
public static final String CLIENT_ERROR_SERVER_NOT_REACHABLE = "SERVER_NOT_REACHABLE";
private XMLRPCClient mXmlRpcClient;
public XmlRpcHelper(String serverUrl) {
public XmlRpcHelper() {
try {
if(serverUrl != null) {
mXmlRpcClient = new XMLRPCClient(new URL(serverUrl));
} else {
mXmlRpcClient = new XMLRPCClient(new URL(LinphonePreferences.instance().getXmlRpcServerUrl()));
}
mXmlRpcClient = new XMLRPCClient(new URL(LinphonePreferences.instance().getInAppPurchaseValidatingServerUrl()));
} catch (MalformedURLException e) {
Log.e(e);
}
@ -274,12 +270,11 @@ public class XmlRpcHelper {
Log.e(error);
listener.onError(error.toString());
}
@Override
public void onResponse(long id, Object object) {
String result = (String) object;
String result = (String)object;
Log.d("isAccountActivatedAsync: " + result);
if ("OK".equals(result)) {
listener.onAccountActivatedFetched(true);
return;
@ -289,7 +284,6 @@ public class XmlRpcHelper {
}
listener.onAccountActivatedFetched(false);
}
@Override
public void onError(long id, XMLRPCException error) {
Log.e(error);
@ -859,26 +853,4 @@ public class XmlRpcHelper {
listener.onError(CLIENT_ERROR_INVALID_SERVER_URL);
}
}
public String getRemoteProvisioningFilename(String username, String domain, String password){
if (mXmlRpcClient != null) {
try {
Object object = mXmlRpcClient.call("get_remote_provisioning_filename", username, domain, password);
String result = (String)object;
Log.d("getRemoteProvisioningFilename:: " + result);
if (result.startsWith("ERROR_")) {
Log.e(result);
return result;
}
return result;
} catch (XMLRPCException e) {
Log.e(e);
}
} else {
Log.e(CLIENT_ERROR_INVALID_SERVER_URL);
}
return null;
}
}

View file

@ -84,7 +84,7 @@ public class XmlRpcListenerBase implements XmlRpcListener {
// TODO Auto-generated method stub
}
@Override
public void onRemoteProvisioningFilenameSent(String result) {
// TODO Auto-generated method stub