diff --git a/.gitmodules b/.gitmodules
index 263d8ce9b..fa0235b31 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -58,9 +58,6 @@
[submodule "submodules/externals/libupnp"]
path = submodules/externals/libupnp
url = git://git.linphone.org/libupnp.git
-[submodule "submodules/externals/cunit"]
- path = submodules/externals/cunit
- url = git://git.linphone.org/cunit.git
[submodule "submodules/externals/axmlrpc"]
path = submodules/externals/axmlrpc
url = git://git.linphone.org/axmlrpc.git
@@ -104,3 +101,6 @@
[submodule "submodules/belcard"]
path = submodules/belcard
url = git://git.linphone.org/belcard.git
+[submodule "submodules/bcunit"]
+ path = submodules/bcunit
+ url = git://git.linphone.org/bcunit.git
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 309e28424..7d99c6672 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -41,6 +41,8 @@
+
+
@@ -133,6 +135,14 @@
+
+
+
+
+
+
diff --git a/prepare.py b/prepare.py
index f90dc1416..1e2316165 100755
--- a/prepare.py
+++ b/prepare.py
@@ -84,7 +84,7 @@ class AndroidPreparator(prepare.Preparator):
def __init__(self, targets=android_targets):
prepare.Preparator.__init__(self, targets)
self.min_supported_ndk = 10
- self.max_supported_ndk = 11
+ self.max_supported_ndk = 12
self.unsupported_ndk_version = None
self.release_with_debug_info = True
self.veryclean = True
@@ -138,8 +138,8 @@ class AndroidPreparator(prepare.Preparator):
python_config_files.append(os.path.join(root, filename))
if len(python_config_files) > 0:
version = open(python_config_files[0]).readlines()[0]
- res = re.match('^.*/aosp-ndk-r(\d+).*$', version)
- version = int(res.group(1))
+ res = re.match('^.*/(aosp-)?ndk-r(\d+).*$', version)
+ version = int(res.group(2))
retval = False
else:
error("Could not get Android NDK version!")
diff --git a/res/layout-land/call.xml b/res/layout-land/call.xml
index 9c556801b..f88ab92ac 100644
--- a/res/layout-land/call.xml
+++ b/res/layout-land/call.xml
@@ -241,15 +241,32 @@
android:layout_weight="0.5"
android:padding="12dp"/>
-
+ android:layout_weight="1">
+
+
+
+
+
diff --git a/res/layout-sw533dp-land/call.xml b/res/layout-sw533dp-land/call.xml
index 12862af3b..5de923dec 100644
--- a/res/layout-sw533dp-land/call.xml
+++ b/res/layout-sw533dp-land/call.xml
@@ -241,15 +241,32 @@
android:padding="12dp"
android:contentDescription="@string/content_description_hang_up"/>
-
+ android:layout_weight="1">
+
+
+
+
+
diff --git a/res/layout/assistant_linphone_login.xml b/res/layout/assistant_linphone_login.xml
index 692b89a08..a1a0dc3aa 100644
--- a/res/layout/assistant_linphone_login.xml
+++ b/res/layout/assistant_linphone_login.xml
@@ -68,11 +68,18 @@
android:layout_height="40dp"
android:singleLine="true"/>
+
+
diff --git a/res/layout/call.xml b/res/layout/call.xml
index 280bdb653..06c66b396 100644
--- a/res/layout/call.xml
+++ b/res/layout/call.xml
@@ -294,16 +294,33 @@
android:layout_height="wrap_content"
android:layout_weight="0.4"
android:padding="12dp"/>
-
-
+ android:layout_weight="0.8">
+
+
+
+
+
diff --git a/res/layout/contact_cell.xml b/res/layout/contact_cell.xml
index cf233a2ce..0d5615ff8 100644
--- a/res/layout/contact_cell.xml
+++ b/res/layout/contact_cell.xml
@@ -95,6 +95,19 @@
android:scaleType="centerInside"
android:src="@drawable/led_connected" />
+
+
+
\ No newline at end of file
diff --git a/res/layout/in_app_purchasable.xml b/res/layout/in_app_purchasable.xml
new file mode 100644
index 000000000..768c58572
--- /dev/null
+++ b/res/layout/in_app_purchasable.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ 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..ba4341aed
--- /dev/null
+++ b/res/layout/in_app_store.xml
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/raw/linphonerc_default b/res/raw/linphonerc_default
index 61e812811..6ced4c1b6 100644
--- a/res/raw/linphonerc_default
+++ b/res/raw/linphonerc_default
@@ -7,6 +7,8 @@ contact="Linphone Android"
use_info=0
use_ipv6=0
keepalive_period=30000
+rls_uri=sip:rls@sip1.linphone.org
+use_rls_presence=1
[video]
size=qvga
diff --git a/res/raw/linphonerc_factory b/res/raw/linphonerc_factory
index e13273368..648a0ab83 100644
--- a/res/raw/linphonerc_factory
+++ b/res/raw/linphonerc_factory
@@ -31,4 +31,8 @@ dtmf_player_amp=0.1
ec_calibrator_cool_tones=1
[misc]
-max_calls=10
\ No newline at end of file
+max_calls=10
+
+[in-app-purchase]
+server_url=https://www.linphone.org/xmlrpc.php
+purchasable_items_ids=test_account_subscription
diff --git a/res/values/non_localizable_custom.xml b/res/values/non_localizable_custom.xml
index 9557622ca..4266988c7 100644
--- a/res/values/non_localizable_custom.xml
+++ b/res/values/non_localizable_custom.xml
@@ -1,5 +1,6 @@
+
sip.linphone.org
stun.linphone.org
@@ -13,6 +14,7 @@
false
false
false
+ false
https://www.linphone.org/wizard.php
diff --git a/res/values/non_localizable_strings.xml b/res/values/non_localizable_strings.xml
index 2a3a3b75b..f204815ee 100644
--- a/res/values/non_localizable_strings.xml
+++ b/res/values/non_localizable_strings.xml
@@ -34,6 +34,7 @@
pref_sipaccounts_key
setup_key
pref_add_account_key
+ pref_in_app_store_key
pref_video_key
pref_video_codecs_key
@@ -71,6 +72,7 @@
pref_bandwidth_limit_key
pref_animation_enable_key
pref_escape_plus_key
+ pref_friendlist_subscribe_key
pref_echo_cancellation_key
pref_autostart_key
Outbound proxy
diff --git a/res/values/strings.xml b/res/values/strings.xml
index cd3ac8d34..f673d0845 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -90,6 +90,7 @@
Passwords do not match.
Your username will be %s.\r\n\r\nIt may differ from your input to match requirements.\r\nDo you accept?
Please enter your login and password
+ Forgot password ?
Enter a number or an address
@@ -206,6 +207,7 @@
AVPF
AVPF regular RTCP interval in seconds (between 1 and 5)
Replace + by 00
+ Friendlist subscribe
Auth userid
Enter authentication userid (optional)
Display name
@@ -222,6 +224,7 @@
SIP Accounts
Default account
Add account
+ In-app Store
Tunnel
Hostname
Port
diff --git a/res/xml/account_preferences.xml b/res/xml/account_preferences.xml
index cd70b14fd..2e46704d7 100644
--- a/res/xml/account_preferences.xml
+++ b/res/xml/account_preferences.xml
@@ -72,6 +72,10 @@
+
+
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml
index 544eb39af..8eab25ca6 100644
--- a/res/xml/preferences.xml
+++ b/res/xml/preferences.xml
@@ -4,11 +4,15 @@
-
+
+
+
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/AboutFragment.java b/src/org/linphone/AboutFragment.java
index 77a5f516f..86502d948 100644
--- a/src/org/linphone/AboutFragment.java
+++ b/src/org/linphone/AboutFragment.java
@@ -19,13 +19,22 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
import org.linphone.core.LinphoneCore;
+import org.linphone.core.LinphoneCore.LogCollectionUploadState;
+import org.linphone.core.LinphoneCoreListenerBase;
import org.linphone.mediastream.Log;
+import android.app.Dialog;
import android.app.Fragment;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.WindowManager;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
@@ -38,6 +47,9 @@ public class AboutFragment extends Fragment implements OnClickListener {
View sendLogButton = null;
View resetLogButton = null;
ImageView cancel;
+ LinphoneCoreListenerBase mListener;
+ private ProgressDialog progress;
+ private boolean uploadInProgress;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@@ -62,22 +74,82 @@ public class AboutFragment extends Fragment implements OnClickListener {
resetLogButton = view.findViewById(R.id.reset_log);
resetLogButton.setOnClickListener(this);
resetLogButton.setVisibility(org.linphone.LinphonePreferences.instance().isDebugEnabled() ? View.VISIBLE : View.GONE);
+
+ mListener = new LinphoneCoreListenerBase() {
+ @Override
+ public void uploadProgressIndication(LinphoneCore lc, int offset, int total) {
+ }
+
+ @Override
+ public void uploadStateChanged(LinphoneCore lc, LogCollectionUploadState state, String info) {
+ if (state == LogCollectionUploadState.LogCollectionUploadStateInProgress) {
+ displayUploadLogsInProgress();
+ } else if (state == LogCollectionUploadState.LogCollectionUploadStateDelivered || state == LogCollectionUploadState.LogCollectionUploadStateNotDelivered) {
+ uploadInProgress = false;
+ if (progress != null) progress.dismiss();
+ if (state == LogCollectionUploadState.LogCollectionUploadStateDelivered) {
+ sendLogs(LinphoneService.instance().getApplicationContext(), info);
+ }
+ }
+ }
+ };
return view;
}
+
+ private void displayUploadLogsInProgress() {
+ if (uploadInProgress) {
+ return;
+ }
+ uploadInProgress = true;
+
+ progress = ProgressDialog.show(LinphoneActivity.instance(), null, null);
+ Drawable d = new ColorDrawable(getResources().getColor(R.color.colorE));
+ d.setAlpha(200);
+ progress.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
+ progress.getWindow().setBackgroundDrawable(d);
+ progress.setContentView(R.layout.progress_dialog);
+ progress.show();
+ }
+
+ private void sendLogs(Context context, String info){
+ final String appName = context.getString(R.string.app_name);
+
+ Intent i = new Intent(Intent.ACTION_SEND);
+ i.putExtra(Intent.EXTRA_EMAIL, new String[]{ context.getString(R.string.about_bugreport_email) });
+ i.putExtra(Intent.EXTRA_SUBJECT, appName + " Logs");
+ i.putExtra(Intent.EXTRA_TEXT, info);
+ i.setType("application/zip");
+
+ try {
+ startActivity(Intent.createChooser(i, "Send mail..."));
+ } catch (android.content.ActivityNotFoundException ex) {
+ Log.e(ex);
+ }
+ }
@Override
public void onPause() {
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc != null) {
+ lc.removeListener(mListener);
+ }
+
super.onPause();
}
@Override
public void onResume() {
- super.onResume();
-
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc != null) {
+ lc.addListener(mListener);
+ }
+
if (org.linphone.LinphoneActivity.isInstanciated()) {
LinphoneActivity.instance().selectMenu(FragmentsAvailable.ABOUT);
}
+
+ super.onResume();
}
@Override
diff --git a/src/org/linphone/AccountPreferencesFragment.java b/src/org/linphone/AccountPreferencesFragment.java
index a1b217840..2b8b78538 100644
--- a/src/org/linphone/AccountPreferencesFragment.java
+++ b/src/org/linphone/AccountPreferencesFragment.java
@@ -176,7 +176,7 @@ public class AccountPreferencesFragment extends PreferencesListFragment {
}
preference.setSummary(newValue.toString());
return true;
- }
+ }
};
OnPreferenceChangeListener prefixChangedListener = new OnPreferenceChangeListener() {
@Override
@@ -234,6 +234,14 @@ public class AccountPreferencesFragment extends PreferencesListFragment {
return true;
}
};
+ OnPreferenceChangeListener friendlistSubscribeListener = new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ boolean value = (Boolean) newValue;
+ LinphoneManager.getInstance().subscribeFriendList(value);
+ return true;
+ }
+ };
OnPreferenceChangeListener disableChangedListener = new OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
@@ -365,6 +373,12 @@ public class AccountPreferencesFragment extends PreferencesListFragment {
if(!isNewAccount){
escape.setChecked(mPrefs.getReplacePlusByZeroZero(n));
}
+
+ CheckBoxPreference friendlistSubscribe = (CheckBoxPreference) advanced.getPreference(8);
+ friendlistSubscribe.setOnPreferenceChangeListener(friendlistSubscribeListener);
+ if(!isNewAccount){
+ escape.setChecked(mPrefs.getReplacePlusByZeroZero(n));
+ }
PreferenceCategory manage = (PreferenceCategory) getPreferenceScreen().findPreference(getString(R.string.pref_manage_key));
final CheckBoxPreference disable = (CheckBoxPreference) manage.getPreference(0);
diff --git a/src/org/linphone/CallActivity.java b/src/org/linphone/CallActivity.java
index 635adba9f..afdbd8b60 100644
--- a/src/org/linphone/CallActivity.java
+++ b/src/org/linphone/CallActivity.java
@@ -24,6 +24,8 @@ import org.linphone.core.LinphoneAddress;
import org.linphone.core.LinphoneCall;
import org.linphone.core.LinphoneCall.State;
import org.linphone.core.LinphoneCallParams;
+import org.linphone.core.LinphoneChatMessage;
+import org.linphone.core.LinphoneChatRoom;
import org.linphone.core.LinphoneCore;
import org.linphone.core.LinphoneCoreException;
import org.linphone.core.LinphoneCoreListenerBase;
@@ -91,6 +93,7 @@ public class CallActivity extends Activity implements OnClickListener, SensorEve
private Handler mControlsHandler = new Handler();
private Runnable mControls;
private ImageView switchCamera;
+ private TextView missedChats;
private RelativeLayout mActiveCallHeader, sideMenuContent, avatar_layout;
private ImageView pause, hangUp, dialer, video, micro, speaker, options, addCall, transfer, conference, conferenceStatus, contactPicture;
private ImageView audioRoute, routeSpeaker, routeEarpiece, routeBluetooth, menu, chat;
@@ -161,7 +164,12 @@ public class CallActivity extends Activity implements OnClickListener, SensorEve
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
mProximity = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
- mListener = new LinphoneCoreListenerBase(){
+ mListener = new LinphoneCoreListenerBase() {
+ @Override
+ public void messageReceived(LinphoneCore lc, LinphoneChatRoom cr, LinphoneChatMessage message) {
+ displayMissedChats();
+ }
+
@Override
public void callState(LinphoneCore lc, final LinphoneCall call, LinphoneCall.State state, String message) {
if (LinphoneManager.getLc().getCallsNb() == 0) {
@@ -362,6 +370,7 @@ public class CallActivity extends Activity implements OnClickListener, SensorEve
chat = (ImageView) findViewById(R.id.chat);
chat.setOnClickListener(this);
+ missedChats = (TextView) findViewById(R.id.missed_chats);
//Others
@@ -500,6 +509,7 @@ public class CallActivity extends Activity implements OnClickListener, SensorEve
refreshInCallActions();
refreshCallList(getResources());
enableAndRefreshInCallActions();
+ displayMissedChats();
}
private void refreshInCallActions() {
@@ -1806,4 +1816,26 @@ public class CallActivity extends Activity implements OnClickListener, SensorEve
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
+
+ private void displayMissedChats() {
+ int count = 0;
+ LinphoneChatRoom[] chats = LinphoneManager.getLc().getChatRooms();
+ for (LinphoneChatRoom chatroom : chats) {
+ count += chatroom.getUnreadMessagesCount();
+ }
+
+ if (count > 0) {
+ missedChats.setText(count + "");
+ missedChats.setVisibility(View.VISIBLE);
+ if (!isAnimationDisabled) {
+ missedChats.startAnimation(AnimationUtils.loadAnimation(this, R.anim.bounce));
+ }
+ if(count > 99){
+ //TODO
+ }
+ } else {
+ missedChats.clearAnimation();
+ missedChats.setVisibility(View.GONE);
+ }
+ }
}
diff --git a/src/org/linphone/ContactDetailsFragment.java b/src/org/linphone/ContactDetailsFragment.java
index 7909e0f1e..1c2c8d11c 100644
--- a/src/org/linphone/ContactDetailsFragment.java
+++ b/src/org/linphone/ContactDetailsFragment.java
@@ -213,13 +213,8 @@ public class ContactDetailsFragment extends Fragment implements OnClickListener
LinphoneActivity.instance().selectMenu(FragmentsAvailable.CONTACT_DETAIL);
LinphoneActivity.instance().hideTabBar(false);
}
- if (contact.getFullName() == null || contact.getFullName().equals("")) {
- //Contact has been deleted, return
- LinphoneActivity.instance().displayContacts(false);
- } else {
- contact.refresh();
- displayContact(inflater, view);
- }
+ contact.refresh();
+ displayContact(inflater, view);
}
@Override
diff --git a/src/org/linphone/ContactEditorFragment.java b/src/org/linphone/ContactEditorFragment.java
index 8544e3056..e94aefdc0 100644
--- a/src/org/linphone/ContactEditorFragment.java
+++ b/src/org/linphone/ContactEditorFragment.java
@@ -385,7 +385,7 @@ public class ContactEditorFragment extends Fragment {
if (contact != null) {
for (LinphoneNumberOrAddress numberOrAddress : contact.getNumbersOrAddresses()) {
if (!numberOrAddress.isSIPAddress()) {
- View view = displayNumberOrAddress(controls, numberOrAddress.getValue());
+ View view = displayNumberOrAddress(controls, numberOrAddress.getValue(), false);
if (view != null)
controls.addView(view);
}
@@ -395,7 +395,7 @@ public class ContactEditorFragment extends Fragment {
if (newSipOrNumberToAdd != null) {
boolean isSip = LinphoneUtils.isStrictSipAddress(newSipOrNumberToAdd) || !LinphoneUtils.isNumberAddress(newSipOrNumberToAdd);
if(!isSip) {
- View view = displayNumberOrAddress(controls, newSipOrNumberToAdd);
+ View view = displayNumberOrAddress(controls, newSipOrNumberToAdd, false);
if (view != null)
controls.addView(view);
}
@@ -415,7 +415,7 @@ public class ContactEditorFragment extends Fragment {
if (contact != null) {
for (LinphoneNumberOrAddress numberOrAddress : contact.getNumbersOrAddresses()) {
if (numberOrAddress.isSIPAddress()) {
- View view = displayNumberOrAddress(controls, numberOrAddress.getValue());
+ View view = displayNumberOrAddress(controls, numberOrAddress.getValue(), true);
if (view != null)
controls.addView(view);
}
@@ -425,7 +425,7 @@ public class ContactEditorFragment extends Fragment {
if (newSipOrNumberToAdd != null) {
boolean isSip = LinphoneUtils.isStrictSipAddress(newSipOrNumberToAdd) || !LinphoneUtils.isNumberAddress(newSipOrNumberToAdd);
if (isSip) {
- View view = displayNumberOrAddress(controls, newSipOrNumberToAdd);
+ View view = displayNumberOrAddress(controls, newSipOrNumberToAdd, true);
if (view != null)
controls.addView(view);
}
@@ -438,35 +438,33 @@ public class ContactEditorFragment extends Fragment {
return controls;
}
- private View displayNumberOrAddress(final LinearLayout controls, String numberOrAddress) {
- return displayNumberOrAddress(controls, numberOrAddress, false);
+ private View displayNumberOrAddress(final LinearLayout controls, String numberOrAddress, boolean isSIP) {
+ return displayNumberOrAddress(controls, numberOrAddress, isSIP, false);
}
@SuppressLint("InflateParams")
- private View displayNumberOrAddress(final LinearLayout controls, String numberOrAddress, boolean forceAddNumber) {
- boolean isSip = LinphoneUtils.isStrictSipAddress(numberOrAddress) || !LinphoneUtils.isNumberAddress(numberOrAddress);
-
- if (isSip) {
+ private View displayNumberOrAddress(final LinearLayout controls, String numberOrAddress, boolean isSIP, boolean forceAddNumber) {
+ if (isSIP) {
if (firstSipAddressIndex == -1) {
firstSipAddressIndex = controls.getChildCount();
}
numberOrAddress = numberOrAddress.replace("sip:", "");
}
- if ((getResources().getBoolean(R.bool.hide_phone_numbers_in_editor) && !isSip) || (getResources().getBoolean(R.bool.hide_sip_addresses_in_editor) && isSip)) {
+ if ((getResources().getBoolean(R.bool.hide_phone_numbers_in_editor) && !isSIP) || (getResources().getBoolean(R.bool.hide_sip_addresses_in_editor) && isSIP)) {
if (forceAddNumber)
- isSip = !isSip; // If number can't be displayed because we hide a sort of number, change that category
+ isSIP = !isSIP; // If number can't be displayed because we hide a sort of number, change that category
else
return null;
}
LinphoneNumberOrAddress tempNounoa;
if (forceAddNumber) {
- tempNounoa = new LinphoneNumberOrAddress(null, isSip);
+ tempNounoa = new LinphoneNumberOrAddress(null, isSIP);
} else {
if(isNewContact || newSipOrNumberToAdd != null) {
- tempNounoa = new LinphoneNumberOrAddress(numberOrAddress, isSip);
+ tempNounoa = new LinphoneNumberOrAddress(numberOrAddress, isSIP);
} else {
- tempNounoa = new LinphoneNumberOrAddress(null, isSip, numberOrAddress);
+ tempNounoa = new LinphoneNumberOrAddress(null, isSIP, numberOrAddress);
}
}
final LinphoneNumberOrAddress nounoa = tempNounoa;
@@ -475,7 +473,7 @@ public class ContactEditorFragment extends Fragment {
final View view = inflater.inflate(R.layout.contact_edit_row, null);
final EditText noa = (EditText) view.findViewById(R.id.numoraddr);
- noa.setInputType(isSip ? InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS : InputType.TYPE_CLASS_PHONE);
+ noa.setInputType(isSIP ? InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS : InputType.TYPE_CLASS_PHONE);
noa.setText(numberOrAddress);
noa.addTextChangedListener(new TextWatcher() {
@Override
@@ -496,7 +494,7 @@ public class ContactEditorFragment extends Fragment {
}
ImageView delete = (ImageView) view.findViewById(R.id.delete_field);
- if ((getResources().getBoolean(R.bool.allow_only_one_phone_number) && !isSip) || (getResources().getBoolean(R.bool.allow_only_one_sip_address) && isSip)) {
+ if ((getResources().getBoolean(R.bool.allow_only_one_phone_number) && !isSIP) || (getResources().getBoolean(R.bool.allow_only_one_sip_address) && isSIP)) {
delete.setVisibility(View.GONE);
}
delete.setOnClickListener(new OnClickListener() {
diff --git a/src/org/linphone/ContactsListFragment.java b/src/org/linphone/ContactsListFragment.java
index f81e59b00..6c2ffa021 100644
--- a/src/org/linphone/ContactsListFragment.java
+++ b/src/org/linphone/ContactsListFragment.java
@@ -459,6 +459,7 @@ public class ContactsListFragment extends Fragment implements OnClickListener, O
}
CheckBox delete = (CheckBox) view.findViewById(R.id.delete);
+ ImageView linphoneFriend = (ImageView) view.findViewById(R.id.friendLinphone);
TextView name = (TextView) view.findViewById(R.id.name);
name.setText(contact.getFullName());
@@ -474,7 +475,13 @@ public class ContactsListFragment extends Fragment implements OnClickListener, O
separatorText.setText(String.valueOf(fullName.charAt(0)));
}
}
-
+
+ if(contact.isInLinphoneFriendList()){
+ linphoneFriend.setVisibility(View.VISIBLE);
+ } else {
+ linphoneFriend.setVisibility(View.GONE);
+ }
+
ImageView icon = (ImageView) view.findViewById(R.id.contact_picture);
if (contact.hasPhoto()) {
LinphoneUtils.setImagePictureFromUri(getActivity(), icon, contact.getPhotoUri(), contact.getThumbnailUri());
diff --git a/src/org/linphone/ContactsManager.java b/src/org/linphone/ContactsManager.java
index 59611a8f3..78b7ef8b5 100644
--- a/src/org/linphone/ContactsManager.java
+++ b/src/org/linphone/ContactsManager.java
@@ -126,8 +126,10 @@ public class ContactsManager extends ContentObserver {
search = search.toLowerCase(Locale.getDefault());
List searchContacts = new ArrayList();
for (LinphoneContact contact : contacts) {
- if (contact.getFullName().toLowerCase(Locale.getDefault()).contains(search)) {
- searchContacts.add(contact);
+ if (contact.getFullName() != null) {
+ if (contact.getFullName().toLowerCase(Locale.getDefault()).contains(search)) {
+ searchContacts.add(contact);
+ }
}
}
return searchContacts;
@@ -137,8 +139,10 @@ public class ContactsManager extends ContentObserver {
search = search.toLowerCase(Locale.getDefault());
List searchContacts = new ArrayList();
for (LinphoneContact contact : sipContacts) {
- if (contact.getFullName().toLowerCase(Locale.getDefault()).contains(search)) {
- searchContacts.add(contact);
+ if (contact.getFullName() != null) {
+ if (contact.getFullName().toLowerCase(Locale.getDefault()).contains(search)) {
+ searchContacts.add(contact);
+ }
}
}
return searchContacts;
diff --git a/src/org/linphone/LinphoneActivity.java b/src/org/linphone/LinphoneActivity.java
index 0fcfc05a8..4af290067 100644
--- a/src/org/linphone/LinphoneActivity.java
+++ b/src/org/linphone/LinphoneActivity.java
@@ -50,13 +50,16 @@ import org.linphone.ui.AddressText;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
+import android.app.ActivityManager;
import android.app.Dialog;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.drawable.ColorDrawable;
@@ -126,6 +129,7 @@ public class LinphoneActivity extends Activity implements OnClickListener, Conta
private ListView accountsList, sideMenuItemList;
private ImageView menu;
private boolean fetchedContactsOnce = false;
+ private boolean doNotGoToCallActivity = false;
static final boolean isInstanciated() {
return instance != null;
@@ -970,22 +974,6 @@ public class LinphoneActivity extends Activity implements OnClickListener, Conta
startActivityForResult(intent, CALL_ACTIVITY);
}
- public void sendLogs(Context context, String info){
- final String appName = context.getString(R.string.app_name);
-
- Intent i = new Intent(Intent.ACTION_SEND);
- i.putExtra(Intent.EXTRA_EMAIL, new String[]{ context.getString(R.string.about_bugreport_email) });
- i.putExtra(Intent.EXTRA_SUBJECT, appName + " Logs");
- i.putExtra(Intent.EXTRA_TEXT, info);
- i.setType("application/zip");
-
- try {
- startActivity(Intent.createChooser(i, "Send mail..."));
- } catch (android.content.ActivityNotFoundException ex) {
- Log.e(ex);
- }
- }
-
/**
* Register a sensor to track phoneOrientation changes
*/
@@ -1137,6 +1125,9 @@ public class LinphoneActivity extends Activity implements OnClickListener, Conta
protected void onPause() {
getIntent().putExtra("PreviousActivity", 0);
+ if(LinphonePreferences.instance().isFriendlistsubscriptionEnabled()){
+ LinphoneManager.getInstance().subscribeFriendList(!isApplicationBroughtToBackground(this));
+ }
LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
if (lc != null) {
lc.removeListener(mListener);
@@ -1144,7 +1135,28 @@ public class LinphoneActivity extends Activity implements OnClickListener, Conta
super.onPause();
}
-
+
+ public static boolean isApplicationBroughtToBackground(final Activity activity) {
+ ActivityManager activityManager = (ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE);
+ List tasks = activityManager.getRunningTasks(1);
+
+ // Check the top Activity against the list of Activities contained in the Application's package.
+ if (!tasks.isEmpty()) {
+ ComponentName topActivity = tasks.get(0).topActivity;
+ try {
+ PackageInfo pi = activity.getPackageManager().getPackageInfo(activity.getPackageName(), PackageManager.GET_ACTIVITIES);
+ for (ActivityInfo activityInfo : pi.activities) {
+ if(topActivity.getClassName().equals(activityInfo.name)) {
+ return false;
+ }
+ }
+ } catch( PackageManager.NameNotFoundException e) {
+ return false; // Never happens.
+ }
+ }
+ return true;
+ }
+
public void checkAndRequestExternalStoragePermission() {
if (LinphonePreferences.instance().writeExternalStoragePermAsked()) {
return;
@@ -1186,7 +1198,7 @@ public class LinphoneActivity extends Activity implements OnClickListener, Conta
public void checkAndRequestPermission(String permission, int result) {
if (getPackageManager().checkPermission(permission, getPackageName()) != PackageManager.PERMISSION_GRANTED) {
- ActivityCompat.requestPermissions(this, new String[]{ permission }, result);
+ ActivityCompat.requestPermissions(this, new String[]{permission}, result);
}
}
@@ -1259,12 +1271,15 @@ public class LinphoneActivity extends Activity implements OnClickListener, Conta
refreshAccounts();
updateMissedChatCount();
+ if(LinphonePreferences.instance().isFriendlistsubscriptionEnabled()){
+ LinphoneManager.getInstance().subscribeFriendList(true);
+ }
displayMissedCalls(LinphoneManager.getLc().getMissedCallsCount());
LinphoneManager.getInstance().changeStatusToOnline();
- if (getIntent().getIntExtra("PreviousActivity", 0) != CALL_ACTIVITY){
+ if (getIntent().getIntExtra("PreviousActivity", 0) != CALL_ACTIVITY && !doNotGoToCallActivity) {
if (LinphoneManager.getLc().getCalls().length > 0) {
LinphoneCall call = LinphoneManager.getLc().getCalls()[0];
LinphoneCall.State callState = call.getState();
@@ -1289,6 +1304,7 @@ public class LinphoneActivity extends Activity implements OnClickListener, Conta
}
}
}
+ doNotGoToCallActivity = false;
}
@Override
@@ -1325,6 +1341,7 @@ public class LinphoneActivity extends Activity implements OnClickListener, Conta
if (extras != null && extras.getBoolean("GoToChat", false)) {
LinphoneService.instance().removeMessageNotification();
String sipUri = extras.getString("ChatContactSipUri");
+ doNotGoToCallActivity = true;
displayChat(sipUri);
} else if (extras != null && extras.getBoolean("Notification", false)) {
if (LinphoneManager.getLc().getCallsNb() > 0) {
@@ -1349,19 +1366,6 @@ public class LinphoneActivity extends Activity implements OnClickListener, Conta
}
}
if (LinphoneManager.getLc().getCalls().length > 0) {
- LinphoneCall calls[] = LinphoneManager.getLc().getCalls();
- if (calls.length > 0) {
- LinphoneCall call = calls[0];
-
- if (call != null && call.getState() != LinphoneCall.State.IncomingReceived) {
- if (call.getCurrentParamsCopy().getVideoEnabled()) {
- //startVideoActivity(call);
- } else {
- //startIncallActivity(call);
- }
- }
- }
-
// If a call is ringing, start incomingcallactivity
Collection incoming = new ArrayList();
incoming.add(LinphoneCall.State.IncomingReceived);
diff --git a/src/org/linphone/LinphoneContact.java b/src/org/linphone/LinphoneContact.java
index 66a2920f5..ba76ea319 100644
--- a/src/org/linphone/LinphoneContact.java
+++ b/src/org/linphone/LinphoneContact.java
@@ -29,6 +29,7 @@ import org.linphone.core.LinphoneCoreException;
import org.linphone.core.LinphoneCoreFactory;
import org.linphone.core.LinphoneFriend;
import org.linphone.core.LinphoneFriend.SubscribePolicy;
+import org.linphone.core.PresenceBasicStatus;
import org.linphone.mediastream.Log;
import android.content.ContentProviderOperation;
@@ -203,12 +204,14 @@ public class LinphoneContact implements Serializable, Comparable= 0 && accountIndex < prxCfgs.length)
@@ -1228,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 getInAppPurchasables() {
+ ArrayList purchasables = new ArrayList();
+ 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() {
diff --git a/src/org/linphone/SettingsFragment.java b/src/org/linphone/SettingsFragment.java
index 73b0064c2..14aa5661a 100644
--- a/src/org/linphone/SettingsFragment.java
+++ b/src/org/linphone/SettingsFragment.java
@@ -34,8 +34,10 @@ import org.linphone.mediastream.Log;
import org.linphone.mediastream.Version;
import org.linphone.mediastream.video.capture.hwconf.AndroidCameraConfiguration;
import org.linphone.tools.OpenH264DownloadHelper;
+import org.linphone.purchase.InAppPurchaseActivity;
import org.linphone.ui.LedPreference;
import org.linphone.ui.PreferencesListFragment;
+import android.content.Intent;
import android.app.AlertDialog;
import android.content.DialogInterface;
@@ -54,6 +56,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;
@@ -114,6 +118,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
@@ -136,6 +148,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);
}
diff --git a/src/org/linphone/assistant/LinphoneLoginFragment.java b/src/org/linphone/assistant/LinphoneLoginFragment.java
index be0d610d3..bf4dcbd8d 100644
--- a/src/org/linphone/assistant/LinphoneLoginFragment.java
+++ b/src/org/linphone/assistant/LinphoneLoginFragment.java
@@ -22,13 +22,16 @@ import org.linphone.R;
import android.app.Fragment;
import android.os.Bundle;
import android.text.Editable;
+import android.text.Html;
import android.text.TextWatcher;
+import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
+import android.widget.TextView;
import android.widget.Toast;
/**
* @author Sylvain Berfini
@@ -36,16 +39,22 @@ import android.widget.Toast;
public class LinphoneLoginFragment extends Fragment implements OnClickListener, TextWatcher {
private EditText login, password, displayName;
private Button apply;
+ private TextView forgotPassword;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.assistant_linphone_login, container, false);
+
+ String url = "http://linphone.org/free-sip-service.html&action=recover";
login = (EditText) view.findViewById(R.id.assistant_username);
login.addTextChangedListener(this);
password = (EditText) view.findViewById(R.id.assistant_password);
password.addTextChangedListener(this);
+ forgotPassword = (TextView) view.findViewById(R.id.forgot_password);
+ forgotPassword.setText(Html.fromHtml(""+ getString(R.string.forgot_password) + ""));
+ forgotPassword.setMovementMethod(LinkMovementMethod.getInstance());
displayName = (EditText) view.findViewById(R.id.assistant_display_name);
apply = (Button) view.findViewById(R.id.assistant_apply);
apply.setEnabled(false);
diff --git a/src/org/linphone/assistant/RemoteProvisioningLoginActivity.java b/src/org/linphone/assistant/RemoteProvisioningLoginActivity.java
index 4987aeaf9..1d5a90afe 100644
--- a/src/org/linphone/assistant/RemoteProvisioningLoginActivity.java
+++ b/src/org/linphone/assistant/RemoteProvisioningLoginActivity.java
@@ -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) {
diff --git a/src/org/linphone/purchase/InAppPurchaseActivity.java b/src/org/linphone/purchase/InAppPurchaseActivity.java
new file mode 100644
index 000000000..ecc993a0b
--- /dev/null
+++ b/src/org/linphone/purchase/InAppPurchaseActivity.java
@@ -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 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 items) {
+ purchasableItemsLayout.removeAllViews();
+
+ for (Purchasable item : items) {
+ displayBuySubscriptionButton(item);
+ }
+ }
+
+ @Override
+ public void onPurchasedItemsQueryFinished(ArrayList 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);
+ }
+ });
+ }
+}
diff --git a/src/org/linphone/purchase/InAppPurchaseHelper.java b/src/org/linphone/purchase/InAppPurchaseHelper.java
new file mode 100644
index 000000000..729aabb25
--- /dev/null
+++ b/src/org/linphone/purchase/InAppPurchaseHelper.java
@@ -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 getAvailableItemsForPurchase() {
+ ArrayList products = new ArrayList();
+ ArrayList 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 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 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) {
+ 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] " + 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);
+ }
+}
diff --git a/src/org/linphone/purchase/InAppPurchaseListener.java b/src/org/linphone/purchase/InAppPurchaseListener.java
new file mode 100644
index 000000000..a0f17878a
--- /dev/null
+++ b/src/org/linphone/purchase/InAppPurchaseListener.java
@@ -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 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 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);
+}
diff --git a/src/org/linphone/purchase/InAppPurchaseListenerBase.java b/src/org/linphone/purchase/InAppPurchaseListenerBase.java
new file mode 100644
index 000000000..b99a97ddc
--- /dev/null
+++ b/src/org/linphone/purchase/InAppPurchaseListenerBase.java
@@ -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 items) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void onPurchasedItemsQueryFinished(ArrayList 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
+
+ }
+}
diff --git a/src/org/linphone/purchase/Purchasable.java b/src/org/linphone/purchase/Purchasable.java
new file mode 100644
index 000000000..22b8a84a8
--- /dev/null
+++ b/src/org/linphone/purchase/Purchasable.java
@@ -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;
+ }
+}
diff --git a/src/org/linphone/xmlrpc/XmlRpcHelper.java b/src/org/linphone/xmlrpc/XmlRpcHelper.java
index ed66dfabc..25d543b8b 100644
--- a/src/org/linphone/xmlrpc/XmlRpcHelper.java
+++ b/src/org/linphone/xmlrpc/XmlRpcHelper.java
@@ -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;
- }
}
diff --git a/src/org/linphone/xmlrpc/XmlRpcListenerBase.java b/src/org/linphone/xmlrpc/XmlRpcListenerBase.java
index 9f096c021..d849f40b5 100644
--- a/src/org/linphone/xmlrpc/XmlRpcListenerBase.java
+++ b/src/org/linphone/xmlrpc/XmlRpcListenerBase.java
@@ -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
diff --git a/submodules/bcunit b/submodules/bcunit
new file mode 160000
index 000000000..53b8f5fed
--- /dev/null
+++ b/submodules/bcunit
@@ -0,0 +1 @@
+Subproject commit 53b8f5fed434857af9934b377f21adfc6c334bd6
diff --git a/submodules/belle-sip b/submodules/belle-sip
index 00dd3c003..374a1ae8b 160000
--- a/submodules/belle-sip
+++ b/submodules/belle-sip
@@ -1 +1 @@
-Subproject commit 00dd3c003ed62cdd85f5f46261d5c050a943171e
+Subproject commit 374a1ae8be6eea497df54e90041b11d81bb32cdf
diff --git a/submodules/cmake-builder b/submodules/cmake-builder
index 0f209d9d5..6c04a199e 160000
--- a/submodules/cmake-builder
+++ b/submodules/cmake-builder
@@ -1 +1 @@
-Subproject commit 0f209d9d5dff24a439aebf2ba3d3a06db97410dd
+Subproject commit 6c04a199ea95ce4caabac33ec7bbb4a7c8a39deb
diff --git a/submodules/linphone b/submodules/linphone
index 1bfb20e56..171ada8a3 160000
--- a/submodules/linphone
+++ b/submodules/linphone
@@ -1 +1 @@
-Subproject commit 1bfb20e56f99ae77578d90850750c740a065c7fe
+Subproject commit 171ada8a3215767b94f419a5445e02a6144d079a