summaryrefslogtreecommitdiff
path: root/src
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 /src
parent7b2fe6b5cabd255b2292804c951f4e5c9e9ddfa2 (diff)
Add Google in App purchasing as an Alternative for Paypal
Also make sure it works on non Gplay devices too!
Diffstat (limited to 'src')
-rw-r--r--src/com/android/vending/billing/IInAppBillingService.aidl144
-rw-r--r--src/de/blinkt/openvpn/fragments/AboutFragment.java293
2 files changed, 404 insertions, 33 deletions
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) {
+
+ }
}