summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArne Schwabe <arne@rfc2549.org>2013-09-10 22:45:16 +0200
committerArne Schwabe <arne@rfc2549.org>2013-09-10 22:45:16 +0200
commit6181f4f56ee560ee0a94e12ce865b90a826ea645 (patch)
treec2879f4657bc806b809d3101c83c7708ac65a1c6
parent7b2fe6b5cabd255b2292804c951f4e5c9e9ddfa2 (diff)
Add Google in App purchasing as an Alternative for Paypal
Also make sure it works on non Gplay devices too!
-rw-r--r--AndroidManifest.xml2
-rw-r--r--build.gradle4
-rw-r--r--res/layout/about.xml16
-rwxr-xr-xres/values/strings.xml2
-rw-r--r--settings.gradle2
-rw-r--r--src/com/android/vending/billing/IInAppBillingService.aidl144
-rw-r--r--src/de/blinkt/openvpn/fragments/AboutFragment.java293
-rw-r--r--vpndialogxposed/vpndialogxposed.iml19
8 files changed, 424 insertions, 58 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 4533aa32..1472f001 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -10,7 +10,7 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
- <!-- <uses-permission android:name="com.android.vending.BILLING" /> -->
+ <uses-permission android:name="com.android.vending.BILLING" />
<uses-sdk
android:minSdkVersion="14"
diff --git a/build.gradle b/build.gradle
index 16207056..943adc9d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -24,8 +24,8 @@ android {
defaultConfig {
minSdkVersion 14
targetSdkVersion 18
- versionCode = 78
- versionName = "0.5.45"
+ versionCode = 79
+ versionName = "0.5.46"
}
sourceSets {
diff --git a/res/layout/about.xml b/res/layout/about.xml
index 3afc165c..50accad2 100644
--- a/res/layout/about.xml
+++ b/res/layout/about.xml
@@ -46,11 +46,23 @@
android:id="@+id/donatestring"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:text="@string/donatewithpaypal"
tools:ignore="SelectableText" />
+
+ <TextView
+ android:paddingTop="12sp"
+ android:id="@+id/donategms"
+ android:text="@string/donatePlayStore"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ tools:ignore="SelectableText"/>
+
+
<Space
- android:layout_width="match_parent"
- android:layout_height="12sp" />
+ android:layout_width="match_parent"
+ android:layout_height="12sp" />
<TextView
android:id="@+id/translation"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index dae5cbf1..d0dd1e45 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -294,4 +294,6 @@
<string name="permission_icon_app">Icon of app trying to use OpenVPN for Android</string>
<string name="faq_vpndialog43">"Starting with Android 4.3 the VPN confirmation is guarded against \"overlaying apps\". This results in the dialog not reacting to touch input. If you have an app that uses overlays it may cause this behaviour. If you find an offending app contact the author of the app. This problem affect all VPN applications on Android 4.3 and later. See also &lt;a href=\"http://code.google.com/p/ics-openvpn/issues/detail?id=185\">Issue 185&lt;a> for additional details"</string>
<string name="faq_vpndialog43_title">Vpn Confirm Dialog on Android 4.3 and later</string>
+ <string name="donatePlayStore">Alternatively you can send me a donation with the Play Store:</string>
+ <string name="thanks_for_donation">Thanks for donating %s!</string>
</resources>
diff --git a/settings.gradle b/settings.gradle
index a81923f4..f4d094c9 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':vpndialogxposed'
+//include ':vpndialogxposed'
diff --git a/src/com/android/vending/billing/IInAppBillingService.aidl b/src/com/android/vending/billing/IInAppBillingService.aidl
new file mode 100644
index 00000000..2a492f78
--- /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/de/blinkt/openvpn/fragments/AboutFragment.java b/src/de/blinkt/openvpn/fragments/AboutFragment.java
index 54bd3667..156493c1 100644
--- a/src/de/blinkt/openvpn/fragments/AboutFragment.java
+++ b/src/de/blinkt/openvpn/fragments/AboutFragment.java
@@ -1,54 +1,281 @@
package de.blinkt.openvpn.fragments;
import android.app.Fragment;
+import android.app.PendingIntent;
+import android.content.*;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
import android.text.Html;
+import android.text.SpannableString;
import android.text.Spanned;
+import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
+import android.text.style.ClickableSpan;
+import android.util.Log;
+import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
+import com.android.vending.billing.IInAppBillingService;
import de.blinkt.openvpn.R;
+import org.json.JSONException;
+import org.json.JSONObject;
-public class AboutFragment extends Fragment {
+import java.util.*;
+
+public class AboutFragment extends Fragment implements View.OnClickListener {
+
+ public static final String INAPPITEM_TYPE_INAPP = "inapp";
+ public static final String RESPONSE_CODE = "RESPONSE_CODE";
+ private static final int DONATION_CODE = 12;
+ private static final int BILLING_RESPONSE_RESULT_OK = 0;
+ private static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
+ private static final String[] donationSkus = { "donation1eur", "donation2eur", "donation5eur", "donation10eur"};
+ IInAppBillingService mService;
+ Hashtable<View, String> viewToProduct = new Hashtable<View, String>();
+ ServiceConnection mServiceConn = new ServiceConnection() {
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mService = null;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name,
+ IBinder service) {
+ mService = IInAppBillingService.Stub.asInterface(service);
+ initGooglePlayDonation();
+
+ }
+ };
+
+ private void initGooglePlayDonation() {
+ new Thread("queryGMSInApp") {
+ @Override
+ public void run() {
+ initGMSDonateOptions();
+ }
+ }.start();
+ }
+
+ private TextView gmsTextView;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getActivity().bindService(new
+ Intent("com.android.vending.billing.InAppBillingService.BIND"),
+ mServiceConn, Context.BIND_AUTO_CREATE);
+
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (mServiceConn != null) {
+ getActivity().unbindService(mServiceConn);
+ }
+
+ }
+
+ private void initGMSDonateOptions() {
+ try {
+ int billingSupported = mService.isBillingSupported(3, getActivity().getPackageName(), INAPPITEM_TYPE_INAPP);
+ if (billingSupported != BILLING_RESPONSE_RESULT_OK) {
+ Log.i("OpenVPN", "Play store billing not supported");
+ return;
+ }
+
+ ArrayList skuList = new ArrayList();
+ Collections.addAll(skuList, donationSkus);
+ Bundle querySkus = new Bundle();
+ querySkus.putStringArrayList("ITEM_ID_LIST", skuList);
+
+ Bundle ownedItems = mService.getPurchases(3, getActivity().getPackageName(), INAPPITEM_TYPE_INAPP, null);
+
+
+ if (ownedItems.getInt(RESPONSE_CODE) != BILLING_RESPONSE_RESULT_OK)
+ return;
+
+ final ArrayList<String> ownedSkus = ownedItems.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
+
+ Bundle skuDetails = mService.getSkuDetails(3, getActivity().getPackageName(), INAPPITEM_TYPE_INAPP, querySkus);
+
+
+ if (skuDetails.getInt(RESPONSE_CODE) != BILLING_RESPONSE_RESULT_OK)
+ return;
+
+ final ArrayList<String> responseList = skuDetails.getStringArrayList("DETAILS_LIST");
+
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ createPlayBuyOptions(ownedSkus, responseList);
+
+ }
+ });
+
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private static class SkuResponse {
+ String title;
+ String price;
+
+ SkuResponse(String p, String t)
+ {
+ title=t;
+ price=p;
+ }
+ }
+
+
+
+ private void createPlayBuyOptions(ArrayList<String> ownedSkus, ArrayList<String> responseList) {
+ try {
+ Vector<Pair<String,String>> gdonation = new Vector<Pair<String, String>>();
+
+ gdonation.add(new Pair<String, String>(getString(R.string.donatePlayStore),null));
+ HashMap<String, SkuResponse> responseMap = new HashMap<String, SkuResponse>();
+ for (String thisResponse : responseList) {
+ JSONObject object = new JSONObject(thisResponse);
+ responseMap.put(
+ object.getString("productId"),
+ new SkuResponse(
+ object.getString("price"),
+ object.getString("title")));
+
+ }
+ for (String sku: donationSkus)
+ if (responseMap.containsKey(sku))
+ gdonation.add(getSkuTitle(sku,
+ responseMap.get(sku).title, responseMap.get(sku).price, ownedSkus));
+
+ String gmsTextString="";
+ for(int i=0;i<gdonation.size();i++) {
+ if(i==1)
+ gmsTextString+= " ";
+ else if(i>1)
+ gmsTextString+= ", ";
+ gmsTextString+=gdonation.elementAt(i).first;
+ }
+ SpannableString gmsText = new SpannableString(gmsTextString);
+
+
+ int lStart = 0;
+ int lEnd=0;
+ for(Pair<String, String> item:gdonation){
+ lEnd = lStart + item.first.length();
+ if (item.second!=null) {
+ final String mSku = item.second;
+ ClickableSpan cspan = new ClickableSpan()
+ {
+ @Override
+ public void onClick(View widget) {
+ triggerBuy(mSku);
+ }
+ };
+ gmsText.setSpan(cspan,lStart,lEnd,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ lStart = lEnd+2; // Account for ", " between items
+ }
+
+ if(gmsTextView !=null) {
+ gmsTextView.setText(gmsText);
+ gmsTextView.setMovementMethod(LinkMovementMethod.getInstance());
+ gmsTextView.setVisibility(View.VISIBLE);
+ }
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ private Pair<String,String> getSkuTitle(final String sku, String title, String price, ArrayList<String> ownedSkus) {
+ String text;
+ if (ownedSkus.contains(sku))
+ return new Pair<String,String>(getString(R.string.thanks_for_donation, price),null);
+
+ if (price.contains("€")|| price.contains("\u20ac")) {
+ text= title;
+ } else {
+ text = String.format(Locale.getDefault(), "%s (%s)", title, price);
+ }
+ //return text;
+ return new Pair<String,String>(price, sku);
+
+ }
+
+ private void triggerBuy(String sku) {
+ try {
+ Bundle buyBundle
+ = mService.getBuyIntent(3, getActivity().getPackageName(),
+ sku, INAPPITEM_TYPE_INAPP, "Thanks for the donation! :)");
+
+
+ if (buyBundle.getInt(RESPONSE_CODE) == BILLING_RESPONSE_RESULT_OK) {
+ PendingIntent buyIntent = (PendingIntent) buyBundle.getParcelable(RESPONSE_BUY_INTENT);
+ getActivity().startIntentSenderForResult(buyIntent.getIntentSender(), DONATION_CODE, new Intent(),
+ 0, 0, 0);
+ }
+
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ } catch (IntentSender.SendIntentException e) {
+ e.printStackTrace();
+ }
+ }
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View v= inflater.inflate(R.layout.about, container, false);
- TextView ver = (TextView) v.findViewById(R.id.version);
-
- String version;
- String name="Openvpn";
- try {
- PackageInfo packageinfo = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0);
- version = packageinfo.versionName;
- name = getString(R.string.app);
- } catch (NameNotFoundException e) {
- version = "error fetching version";
- }
-
-
- ver.setText(getString(R.string.version_info,name,version));
-
- TextView paypal = (TextView) v.findViewById(R.id.donatestring);
-
- String donatetext = getActivity().getString(R.string.donatewithpaypal);
- Spanned htmltext = Html.fromHtml(donatetext);
- paypal.setText(htmltext);
- paypal.setMovementMethod(LinkMovementMethod.getInstance());
-
- TextView translation = (TextView) v.findViewById(R.id.translation);
-
- // Don't print a text for myself
- if ( getString(R.string.translationby).contains("Arne Schwabe"))
- translation.setText("");
- else
- translation.setText(R.string.translationby);
- return v;
+ Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.about, container, false);
+ TextView ver = (TextView) v.findViewById(R.id.version);
+
+ String version;
+ String name = "Openvpn";
+ try {
+ PackageInfo packageinfo = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0);
+ version = packageinfo.versionName;
+ name = getString(R.string.app);
+ } catch (NameNotFoundException e) {
+ version = "error fetching version";
+ }
+
+
+ ver.setText(getString(R.string.version_info, name, version));
+
+ TextView paypal = (TextView) v.findViewById(R.id.donatestring);
+
+ String donatetext = getActivity().getString(R.string.donatewithpaypal);
+ Spanned htmltext = Html.fromHtml(donatetext);
+ paypal.setText(htmltext);
+ paypal.setMovementMethod(LinkMovementMethod.getInstance());
+ gmsTextView = (TextView) v.findViewById(R.id.donategms);
+ /* recreating view without onCreate/onDestroy cycle */
+ if (mService!=null)
+ initGooglePlayDonation();
+
+ TextView translation = (TextView) v.findViewById(R.id.translation);
+
+ // Don't print a text for myself
+ if (getString(R.string.translationby).contains("Arne Schwabe"))
+ translation.setText("");
+ else
+ translation.setText(R.string.translationby);
+ return v;
}
+
+
+ @Override
+ public void onClick(View v) {
+
+ }
}
diff --git a/vpndialogxposed/vpndialogxposed.iml b/vpndialogxposed/vpndialogxposed.iml
index a1c6e847..b9f945d6 100644
--- a/vpndialogxposed/vpndialogxposed.iml
+++ b/vpndialogxposed/vpndialogxposed.iml
@@ -1,24 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" type="JAVA_MODULE" version="4">
- <component name="FacetManager">
- <facet type="android" name="Android">
- <configuration>
- <option name="SELECTED_BUILD_VARIANT" value="Debug" />
- <option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
- <option name="ASSEMBLE_TEST_TASK_NAME" value="assembleTest" />
- <option name="SOURCE_GEN_TASK_NAME" value="TODO" />
- <option name="ALLOW_USER_CONFIGURATION" value="false" />
- <option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
- <option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
- <option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
- </configuration>
- </facet>
- <facet type="android-gradle" name="Android-Gradle">
- <configuration>
- <option name="GRADLE_PROJECT_PATH" value=":vpndialogxposed" />
- </configuration>
- </facet>
- </component>
<component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/classes/debug" />
<exclude-output />