0
0
mirror of https://github.com/schwabe/ics-openvpn.git synced 2024-09-20 03:52:27 +02:00

Add Google in App purchasing as an Alternative for Paypal

Also make sure it works on non Gplay devices too!
This commit is contained in:
Arne Schwabe 2013-09-10 22:45:16 +02:00
parent 7b2fe6b5ca
commit 6181f4f56e
8 changed files with 423 additions and 57 deletions

View File

@ -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"

View File

@ -24,8 +24,8 @@ android {
defaultConfig {
minSdkVersion 14
targetSdkVersion 18
versionCode = 78
versionName = "0.5.45"
versionCode = 79
versionName = "0.5.46"
}
sourceSets {

View File

@ -46,8 +46,20 @@
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" />

View File

@ -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>

View File

@ -1 +1 @@
include ':vpndialogxposed'
//include ':vpndialogxposed'

View File

@ -0,0 +1,144 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.vending.billing;
import android.os.Bundle;
/**
* InAppBillingService is the service that provides in-app billing version 3 and beyond.
* This service provides the following features:
* 1. Provides a new API to get details of in-app items published for the app including
* price, type, title and description.
* 2. The purchase flow is synchronous and purchase information is available immediately
* after it completes.
* 3. Purchase information of in-app purchases is maintained within the Google Play system
* till the purchase is consumed.
* 4. An API to consume a purchase of an inapp item. All purchases of one-time
* in-app items are consumable and thereafter can be purchased again.
* 5. An API to get current purchases of the user immediately. This will not contain any
* consumed purchases.
*
* All calls will give a response code with the following possible values
* RESULT_OK = 0 - success
* RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
* RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
* RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
* RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
* RESULT_ERROR = 6 - Fatal error during the API action
* RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
* RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
*/
interface IInAppBillingService {
/**
* Checks support for the requested billing API version, package and in-app type.
* Minimum API version supported by this interface is 3.
* @param apiVersion the billing version which the app is using
* @param packageName the package name of the calling app
* @param type type of the in-app item being purchased "inapp" for one-time purchases
* and "subs" for subscription.
* @return RESULT_OK(0) on success, corresponding result code on failures
*/
int isBillingSupported(int apiVersion, String packageName, String type);
/**
* Provides details of a list of SKUs
* Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
* with a list JSON strings containing the productId, price, title and description.
* This API can be called with a maximum of 20 SKUs.
* @param apiVersion billing API version that the Third-party is using
* @param packageName the package name of the calling app
* @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "DETAILS_LIST" with a StringArrayList containing purchase information
* in JSON format similar to:
* '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
* "title : "Example Title", "description" : "This is an example description" }'
*/
Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
/**
* Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
* the type, a unique purchase token and an optional developer payload.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param sku the SKU of the in-app item as published in the developer console
* @param type the type of the in-app item ("inapp" for one-time purchases
* and "subs" for subscription).
* @param developerPayload optional argument to be sent back with the purchase information
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "BUY_INTENT" - PendingIntent to start the purchase flow
*
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
* If the purchase is successful, the result data will contain the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
* '{"orderId":"12999763169054705758.1371079406387615",
* "packageName":"com.example.app",
* "productId":"exampleSku",
* "purchaseTime":1345678900000,
* "purchaseToken" : "122333444455555",
* "developerPayload":"example developer payload" }'
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
* was signed with the private key of the developer
* TODO: change this to app-specific keys.
*/
Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
String developerPayload);
/**
* Returns the current SKUs owned by the user of the type and package name specified along with
* purchase information and a signature of the data to be validated.
* This will return all SKUs that have been purchased in V3 and managed items purchased using
* V1 and V2 that have not been consumed.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param type the type of the in-app items being requested
* ("inapp" for one-time purchases and "subs" for subscription).
* @param continuationToken to be set as null for the first call, if the number of owned
* skus are too many, a continuationToken is returned in the response bundle.
* This method can be called again with the continuation token to get the next set of
* owned skus.
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
* "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
* of the purchase information
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
* next set of in-app purchases. Only set if the
* user has more owned skus than the current list.
*/
Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
/**
* Consume the last purchase of the given SKU. This will result in this item being removed
* from all subsequent responses to getPurchases() and allow re-purchase of this item.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param purchaseToken token in the purchase information JSON that identifies the purchase
* to be consumed
* @return 0 if consumption succeeded. Appropriate error values for failures.
*/
int consumePurchase(int apiVersion, String packageName, String purchaseToken);
}

View File

@ -1,28 +1,245 @@
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);
View v = inflater.inflate(R.layout.about, container, false);
TextView ver = (TextView) v.findViewById(R.id.version);
String version;
String name="Openvpn";
String name = "Openvpn";
try {
PackageInfo packageinfo = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0);
version = packageinfo.versionName;
@ -32,7 +249,7 @@ public class AboutFragment extends Fragment {
}
ver.setText(getString(R.string.version_info,name,version));
ver.setText(getString(R.string.version_info, name, version));
TextView paypal = (TextView) v.findViewById(R.id.donatestring);
@ -40,15 +257,25 @@ public class AboutFragment extends Fragment {
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"))
if (getString(R.string.translationby).contains("Arne Schwabe"))
translation.setText("");
else
translation.setText(R.string.translationby);
return v;
}
@Override
public void onClick(View v) {
}
}

View File

@ -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 />