Commit 97f3d354d8dc8fde74e7b5e7d30a19e98283f621

Authored by transpine
1 parent 0d4ed6b4

- add in-app purchase code

- add proguard for release
- add keystore
- add ActivityAbout
app/build.gradle 0 → 100644
  1 +apply plugin: 'com.android.application'
  2 +
  3 +android {
  4 + compileSdkVersion 22
  5 + buildToolsVersion '23.0.1'
  6 + defaultConfig {
  7 + applicationId "net.devfac.userstory"
  8 + minSdkVersion 21
  9 + versionCode 1
  10 + versionName "1.0"
  11 + targetSdkVersion 22
  12 + }
  13 + buildTypes {
  14 + release {
  15 + minifyEnabled true
  16 + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
  17 + proguardFile '/Users/Onether/_Project/_AndroidStudio_workspace/UserStory/app/proguard-rules.pro'
  18 + }
  19 + }
  20 + productFlavors {
  21 + }
  22 +}
  23 +
  24 +dependencies {
  25 + compile fileTree(dir: 'libs', include: ['*.jar'])
  26 + testCompile 'junit:junit:4.12'
  27 + compile 'net.htmlparser.jericho:jericho-html:3.4'
  28 + compile 'com.android.support:design:22.2.1'
  29 + compile 'com.android.support:support-v4:22.2.1'
  30 + compile 'com.android.support:appcompat-v7:22.2.1'
  31 + compile 'com.android.support:cardview-v7:22.2.1'
  32 + compile 'com.android.support:recyclerview-v7:22.2.1'
  33 +}
... ...
app/keystore.jks 0 → 100644
No preview for this file type
app/proguard-rules.pro 0 → 100644
  1 +# Add project specific ProGuard rules here.
  2 +# By default, the flags in this file are appended to flags specified
  3 +# in /Users/Onether/android-sdks/tools/proguard/proguard-android.txt
  4 +# You can edit the include path and order by changing the proguardFiles
  5 +# directive in build.gradle.
  6 +#
  7 +# For more details, see
  8 +# http://developer.android.com/guide/developing/tools/proguard.html
  9 +
  10 +# Add any project specific keep options here:
  11 +
  12 +# If your project uses WebView with JS, uncomment the following
  13 +# and specify the fully qualified class name to the JavaScript interface
  14 +# class:
  15 +#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
  16 +# public *;
  17 +#}
  18 +
  19 +-dontwarn android.support.v7.**
  20 +-dontwarn net.htmlparser.jericho.**
  21 +#-keep class android.support.v7.** { *; }
  22 +#-keep interface android.support.v7.** { *; }
... ...
app/src/main/AndroidManifest.xml
... ... @@ -6,6 +6,8 @@
6 6 <uses-permission android:name="android.permission.INTERNET"/>
7 7 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
8 8 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
  9 +
  10 + <uses-permission android:name="com.android.vending.BILLING"/>
9 11  
10 12 <application
11 13 android:allowBackup="true"
... ... @@ -31,6 +33,8 @@
31 33 <activity android:name=".ActivityLogin"
32 34 android:label="LOGIN"
33 35 android:theme="@style/AppTheme.UserstoryNoActionBar" />
  36 +
  37 + <activity android:name=".ActivityAbout"/>
34 38 </application>
35 39  
36 40 </manifest>
... ...
app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl 0 → 100644
  1 +/*
  2 + * Copyright (C) 2012 The Android Open Source Project
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.android.vending.billing;
  18 +
  19 +import android.os.Bundle;
  20 +
  21 +/**
  22 + * InAppBillingService is the service that provides in-app billing version 3 and beyond.
  23 + * This service provides the following features:
  24 + * 1. Provides a new API to get details of in-app items published for the app including
  25 + * price, type, title and description.
  26 + * 2. The purchase flow is synchronous and purchase information is available immediately
  27 + * after it completes.
  28 + * 3. Purchase information of in-app purchases is maintained within the Google Play system
  29 + * till the purchase is consumed.
  30 + * 4. An API to consume a purchase of an inapp item. All purchases of one-time
  31 + * in-app items are consumable and thereafter can be purchased again.
  32 + * 5. An API to get current purchases of the user immediately. This will not contain any
  33 + * consumed purchases.
  34 + *
  35 + * All calls will give a response code with the following possible values
  36 + * RESULT_OK = 0 - success
  37 + * RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
  38 + * RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
  39 + * RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
  40 + * RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
  41 + * RESULT_ERROR = 6 - Fatal error during the API action
  42 + * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
  43 + * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
  44 + */
  45 +interface IInAppBillingService {
  46 + /**
  47 + * Checks support for the requested billing API version, package and in-app type.
  48 + * Minimum API version supported by this interface is 3.
  49 + * @param apiVersion the billing version which the app is using
  50 + * @param packageName the package name of the calling app
  51 + * @param type type of the in-app item being purchased "inapp" for one-time purchases
  52 + * and "subs" for subscription.
  53 + * @return RESULT_OK(0) on success, corresponding result code on failures
  54 + */
  55 + int isBillingSupported(int apiVersion, String packageName, String type);
  56 +
  57 + /**
  58 + * Provides details of a list of SKUs
  59 + * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
  60 + * with a list JSON strings containing the productId, price, title and description.
  61 + * This API can be called with a maximum of 20 SKUs.
  62 + * @param apiVersion billing API version that the Third-party is using
  63 + * @param packageName the package name of the calling app
  64 + * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
  65 + * @return Bundle containing the following key-value pairs
  66 + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
  67 + * failure as listed above.
  68 + * "DETAILS_LIST" with a StringArrayList containing purchase information
  69 + * in JSON format similar to:
  70 + * '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
  71 + * "title : "Example Title", "description" : "This is an example description" }'
  72 + */
  73 + Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
  74 +
  75 + /**
  76 + * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
  77 + * the type, a unique purchase token and an optional developer payload.
  78 + * @param apiVersion billing API version that the app is using
  79 + * @param packageName package name of the calling app
  80 + * @param sku the SKU of the in-app item as published in the developer console
  81 + * @param type the type of the in-app item ("inapp" for one-time purchases
  82 + * and "subs" for subscription).
  83 + * @param developerPayload optional argument to be sent back with the purchase information
  84 + * @return Bundle containing the following key-value pairs
  85 + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
  86 + * failure as listed above.
  87 + * "BUY_INTENT" - PendingIntent to start the purchase flow
  88 + *
  89 + * The Pending intent should be launched with startIntentSenderForResult. When purchase flow
  90 + * has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
  91 + * If the purchase is successful, the result data will contain the following key-value pairs
  92 + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
  93 + * failure as listed above.
  94 + * "INAPP_PURCHASE_DATA" - String in JSON format similar to
  95 + * '{"orderId":"12999763169054705758.1371079406387615",
  96 + * "packageName":"com.example.app",
  97 + * "productId":"exampleSku",
  98 + * "purchaseTime":1345678900000,
  99 + * "purchaseToken" : "122333444455555",
  100 + * "developerPayload":"example developer payload" }'
  101 + * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
  102 + * was signed with the private key of the developer
  103 + * TODO: change this to app-specific keys.
  104 + */
  105 + Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
  106 + String developerPayload);
  107 +
  108 + /**
  109 + * Returns the current SKUs owned by the user of the type and package name specified along with
  110 + * purchase information and a signature of the data to be validated.
  111 + * This will return all SKUs that have been purchased in V3 and managed items purchased using
  112 + * V1 and V2 that have not been consumed.
  113 + * @param apiVersion billing API version that the app is using
  114 + * @param packageName package name of the calling app
  115 + * @param type the type of the in-app items being requested
  116 + * ("inapp" for one-time purchases and "subs" for subscription).
  117 + * @param continuationToken to be set as null for the first call, if the number of owned
  118 + * skus are too many, a continuationToken is returned in the response bundle.
  119 + * This method can be called again with the continuation token to get the next set of
  120 + * owned skus.
  121 + * @return Bundle containing the following key-value pairs
  122 + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
  123 + * failure as listed above.
  124 + * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
  125 + * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
  126 + * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
  127 + * of the purchase information
  128 + * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
  129 + * next set of in-app purchases. Only set if the
  130 + * user has more owned skus than the current list.
  131 + */
  132 + Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
  133 +
  134 + /**
  135 + * Consume the last purchase of the given SKU. This will result in this item being removed
  136 + * from all subsequent responses to getPurchases() and allow re-purchase of this item.
  137 + * @param apiVersion billing API version that the app is using
  138 + * @param packageName package name of the calling app
  139 + * @param purchaseToken token in the purchase information JSON that identifies the purchase
  140 + * to be consumed
  141 + * @return 0 if consumption succeeded. Appropriate error values for failures.
  142 + */
  143 + int consumePurchase(int apiVersion, String packageName, String purchaseToken);
  144 +}
... ...
app/src/main/java/net/devfac/userstory/ActivityAbout.java 0 → 100644
  1 +package net.devfac.userstory;
  2 +
  3 +import android.content.ComponentName;
  4 +import android.content.Context;
  5 +import android.content.Intent;
  6 +import android.content.ServiceConnection;
  7 +import android.os.Bundle;
  8 +import android.os.IBinder;
  9 +import android.os.PersistableBundle;
  10 +import android.os.RemoteException;
  11 +import android.support.v7.app.AppCompatActivity;
  12 +import android.view.View;
  13 +import android.widget.Button;
  14 +import android.widget.Toast;
  15 +
  16 +import com.android.vending.billing.IInAppBillingService;
  17 +
  18 +import net.devfac.userstory.Utils.Logger;
  19 +import net.devfac.userstory.Utils.iab.IabHelper;
  20 +import net.devfac.userstory.Utils.iab.IabResult;
  21 +import net.devfac.userstory.Utils.iab.Inventory;
  22 +
  23 +import org.json.JSONException;
  24 +import org.json.JSONObject;
  25 +
  26 +import java.util.ArrayList;
  27 +
  28 +/**
  29 + * Created by Onether on 15. 11. 18..
  30 + */
  31 +public class ActivityAbout extends AppCompatActivity {
  32 + private IInAppBillingService mIabService;
  33 + private IabHelper mIabHelper;
  34 +
  35 +
  36 + private final ServiceConnection mIabServiceConnection = new ServiceConnection(){
  37 +
  38 + @Override
  39 + public void onServiceConnected(ComponentName name, IBinder service) {
  40 + mIabService = IInAppBillingService.Stub.asInterface(service);
  41 + Logger.i("IInAppBillingService connected");
  42 + }
  43 +
  44 + @Override
  45 + public void onServiceDisconnected(ComponentName name) {
  46 + mIabService = null;
  47 + Logger.e("IInAppBillingService disconnected");
  48 + }
  49 + };
  50 +
  51 +
  52 + @Override
  53 + protected void onCreate(Bundle savedInstanceState) {
  54 + super.onCreate(savedInstanceState);
  55 +
  56 + setContentView(R.layout.activity_about);
  57 +
  58 +
  59 + //explicit intent
  60 + Intent billingServiceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
  61 + billingServiceIntent.setPackage("com.android.vending");
  62 + bindService(billingServiceIntent, mIabServiceConnection,
  63 + Context.BIND_AUTO_CREATE);
  64 +
  65 +
  66 + String base64EncodePublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzvF/+kU40VVRtZOC/EvQ8Xz1qblRFEUmwnzH3570L1P6/pvbYnxrJc2TPi7iR3OMpmMQbpHgJbGEYBpNd5p5othiwO0JzDfX6SoUsdLJZWMkVW7ToBTHCoMtDURj+8wXv77Da/41GX8N0/83JLW5fYyl9dgExuUkXlt6j3IyjFJIcznKbyulPIg7BtvusKwM7TKcRUFLGe5CDWkbK3lraH0XIUs29ZTuVIYMmbs9u+aWwcWKrb9drkQTYwwrb0yQo/YQ0pAGGqA0UHJQWMllrpHb6spo+HXnVHBco9tq6p9zA6MKALcMiPS7LKBFKw2y1yVQ4OIa5oAITvNnCFkjTwIDAQAB";
  67 +
  68 + mIabHelper = new IabHelper(this, base64EncodePublicKey);
  69 + mIabHelper.enableDebugLogging(true);
  70 +
  71 + Button test = (Button)findViewById(R.id.btn_about_iab_test);
  72 + test.setOnClickListener(new View.OnClickListener() {
  73 + @Override
  74 + public void onClick(View v) {
  75 +
  76 + mIabHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
  77 + @Override
  78 + public void onIabSetupFinished(IabResult result) {
  79 + if( !result.isSuccess() ){
  80 + Toast.makeText(ActivityAbout.this, "Transjaction error", Toast.LENGTH_LONG).show();
  81 + }
  82 +
  83 + else{
  84 + ArrayList<String> skuList = new ArrayList<String>();
  85 + skuList.add("coffee_1");
  86 + skuList.add("coffee_2");
  87 + skuList.add("coffee_3");
  88 +
  89 + mIabHelper.queryInventoryAsync(true, skuList, new IabHelper.QueryInventoryFinishedListener() {
  90 + @Override
  91 + public void onQueryInventoryFinished(IabResult result, Inventory inv) {
  92 + if( result.isFailure()){
  93 + return;
  94 +
  95 + }
  96 +
  97 + if( inv.getSkuDetails("coffee_1") != null ) {
  98 + Logger.i("coffee_1 price : " + inv.getSkuDetails("coffee_1").getPrice());
  99 + }
  100 + }
  101 + });
  102 +
  103 + Bundle querySkus = new Bundle();
  104 + querySkus.putStringArrayList("ITEM_ID_LIST", skuList);
  105 +
  106 + if( mIabService == null) return;
  107 +
  108 + try {
  109 + Bundle skuDetails = mIabService.getSkuDetails(3, getPackageName(), "inapp", querySkus);
  110 +
  111 + int response = skuDetails.getInt("RESPONSE_CODE");
  112 +
  113 + if( response == IabHelper.BILLING_RESPONSE_RESULT_OK ){
  114 + ArrayList<String> responseList = skuDetails.getStringArrayList("DETAILS_LIST");
  115 +
  116 + for(String thisResponse:responseList){
  117 + JSONObject object = new JSONObject(thisResponse);
  118 + String sku = object.getString("productId");
  119 + String price = object.getString("price");
  120 + Logger.i("SKU : " + sku + " | PRICE : " + price);
  121 + }
  122 +
  123 + }
  124 + else{
  125 + Logger.e("Response code : " + String.valueOf(response));
  126 + }
  127 + } catch (RemoteException e) {
  128 + e.printStackTrace();
  129 + } catch (JSONException e) {
  130 + e.printStackTrace();
  131 + }
  132 + }
  133 + }
  134 + });
  135 + }
  136 + });
  137 + }
  138 +
  139 + @Override
  140 + protected void onDestroy() {
  141 + super.onDestroy();
  142 +
  143 + if( mIabService != null ){
  144 + unbindService(mIabServiceConnection);
  145 + }
  146 + }
  147 +}
... ...
app/src/main/java/net/devfac/userstory/MainActivity.java
... ... @@ -39,6 +39,7 @@ import net.devfac.userstory.Utils.PreferenceUtil;
39 39 import java.util.ArrayList;
40 40 import java.util.List;
41 41  
  42 +//TODO: Add Search bar - http://stackoverflow.com/questions/30398247/how-to-filter-a-recyclerview-with-a-searchview
42 43 public class MainActivity extends AppCompatActivity
43 44 implements NavigationView.OnNavigationItemSelectedListener, View.OnClickListener, StateEventListener {
44 45  
... ... @@ -204,12 +205,16 @@ public class MainActivity extends AppCompatActivity
204 205 // as you specify a parent activity in AndroidManifest.xml.
205 206 int id = item.getItemId();
206 207  
207   - //noinspection SimplifiableIfStatement
208   - if (id == R.id.action_main_settings) {
209   - return true;
  208 + switch( id ){
  209 + case R.id.action_main_settings:
  210 + break;
  211 + case R.id.action_main_about:
  212 + startActivity(new Intent(this, ActivityAbout.class));
  213 + break;
210 214 }
211 215  
212   - return super.onOptionsItemSelected(item);
  216 +// return super.onOptionsItemSelected(item);
  217 + return true;
213 218 }
214 219  
215 220 @SuppressWarnings("StatementWithEmptyBody")
... ...
app/src/main/java/net/devfac/userstory/Utils/iab/Base64.java 0 → 100644
  1 +// Portions copyright 2002, Google, Inc.
  2 +//
  3 +// Licensed under the Apache License, Version 2.0 (the "License");
  4 +// you may not use this file except in compliance with the License.
  5 +// You may obtain a copy of the License at
  6 +//
  7 +// http://www.apache.org/licenses/LICENSE-2.0
  8 +//
  9 +// Unless required by applicable law or agreed to in writing, software
  10 +// distributed under the License is distributed on an "AS IS" BASIS,
  11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12 +// See the License for the specific language governing permissions and
  13 +// limitations under the License.
  14 +
  15 +package net.devfac.userstory.Utils.iab;
  16 +
  17 +// This code was converted from code at http://iharder.sourceforge.net/base64/
  18 +// Lots of extraneous features were removed.
  19 +/* The original code said:
  20 + * <p>
  21 + * I am placing this code in the Public Domain. Do with it as you will.
  22 + * This software comes with no guarantees or warranties but with
  23 + * plenty of well-wishing instead!
  24 + * Please visit
  25 + * <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a>
  26 + * periodically to check for updates or to contribute improvements.
  27 + * </p>
  28 + *
  29 + * @author Robert Harder
  30 + * @author rharder@usa.net
  31 + * @version 1.3
  32 + */
  33 +
  34 +/**
  35 + * Base64 converter class. This code is not a complete MIME encoder;
  36 + * it simply converts binary data to base64 data and back.
  37 + *
  38 + * <p>Note {@link CharBase64} is a GWT-compatible implementation of this
  39 + * class.
  40 + */
  41 +public class Base64 {
  42 + /** Specify encoding (value is {@code true}). */
  43 + public final static boolean ENCODE = true;
  44 +
  45 + /** Specify decoding (value is {@code false}). */
  46 + public final static boolean DECODE = false;
  47 +
  48 + /** The equals sign (=) as a byte. */
  49 + private final static byte EQUALS_SIGN = (byte) '=';
  50 +
  51 + /** The new line character (\n) as a byte. */
  52 + private final static byte NEW_LINE = (byte) '\n';
  53 +
  54 + /**
  55 + * The 64 valid Base64 values.
  56 + */
  57 + private final static byte[] ALPHABET =
  58 + {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
  59 + (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
  60 + (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
  61 + (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
  62 + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
  63 + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
  64 + (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
  65 + (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
  66 + (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
  67 + (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
  68 + (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
  69 + (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
  70 + (byte) '9', (byte) '+', (byte) '/'};
  71 +
  72 + /**
  73 + * The 64 valid web safe Base64 values.
  74 + */
  75 + private final static byte[] WEBSAFE_ALPHABET =
  76 + {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
  77 + (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
  78 + (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
  79 + (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
  80 + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
  81 + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
  82 + (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
  83 + (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
  84 + (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
  85 + (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
  86 + (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
  87 + (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
  88 + (byte) '9', (byte) '-', (byte) '_'};
  89 +
  90 + /**
  91 + * Translates a Base64 value to either its 6-bit reconstruction value
  92 + * or a negative number indicating some other meaning.
  93 + **/
  94 + private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
  95 + -5, -5, // Whitespace: Tab and Linefeed
  96 + -9, -9, // Decimal 11 - 12
  97 + -5, // Whitespace: Carriage Return
  98 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
  99 + -9, -9, -9, -9, -9, // Decimal 27 - 31
  100 + -5, // Whitespace: Space
  101 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
  102 + 62, // Plus sign at decimal 43
  103 + -9, -9, -9, // Decimal 44 - 46
  104 + 63, // Slash at decimal 47
  105 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
  106 + -9, -9, -9, // Decimal 58 - 60
  107 + -1, // Equals sign at decimal 61
  108 + -9, -9, -9, // Decimal 62 - 64
  109 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
  110 + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
  111 + -9, -9, -9, -9, -9, -9, // Decimal 91 - 96
  112 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
  113 + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
  114 + -9, -9, -9, -9, -9 // Decimal 123 - 127
  115 + /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
  116 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
  117 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
  118 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
  119 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
  120 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
  121 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
  122 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
  123 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
  124 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
  125 + };
  126 +
  127 + /** The web safe decodabet */
  128 + private final static byte[] WEBSAFE_DECODABET =
  129 + {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
  130 + -5, -5, // Whitespace: Tab and Linefeed
  131 + -9, -9, // Decimal 11 - 12
  132 + -5, // Whitespace: Carriage Return
  133 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
  134 + -9, -9, -9, -9, -9, // Decimal 27 - 31
  135 + -5, // Whitespace: Space
  136 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
  137 + 62, // Dash '-' sign at decimal 45
  138 + -9, -9, // Decimal 46-47
  139 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
  140 + -9, -9, -9, // Decimal 58 - 60
  141 + -1, // Equals sign at decimal 61
  142 + -9, -9, -9, // Decimal 62 - 64
  143 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
  144 + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
  145 + -9, -9, -9, -9, // Decimal 91-94
  146 + 63, // Underscore '_' at decimal 95
  147 + -9, // Decimal 96
  148 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
  149 + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
  150 + -9, -9, -9, -9, -9 // Decimal 123 - 127
  151 + /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
  152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
  153 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
  154 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
  155 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
  156 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
  157 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
  158 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
  159 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
  160 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
  161 + };
  162 +
  163 + // Indicates white space in encoding
  164 + private final static byte WHITE_SPACE_ENC = -5;
  165 + // Indicates equals sign in encoding
  166 + private final static byte EQUALS_SIGN_ENC = -1;
  167 +
  168 + /** Defeats instantiation. */
  169 + private Base64() {
  170 + }
  171 +
  172 + /* ******** E N C O D I N G M E T H O D S ******** */
  173 +
  174 + /**
  175 + * Encodes up to three bytes of the array <var>source</var>
  176 + * and writes the resulting four Base64 bytes to <var>destination</var>.
  177 + * The source and destination arrays can be manipulated
  178 + * anywhere along their length by specifying
  179 + * <var>srcOffset</var> and <var>destOffset</var>.
  180 + * This method does not check to make sure your arrays
  181 + * are large enough to accommodate <var>srcOffset</var> + 3 for
  182 + * the <var>source</var> array or <var>destOffset</var> + 4 for
  183 + * the <var>destination</var> array.
  184 + * The actual number of significant bytes in your array is
  185 + * given by <var>numSigBytes</var>.
  186 + *
  187 + * @param source the array to convert
  188 + * @param srcOffset the index where conversion begins
  189 + * @param numSigBytes the number of significant bytes in your array
  190 + * @param destination the array to hold the conversion
  191 + * @param destOffset the index where output will be put
  192 + * @param alphabet is the encoding alphabet
  193 + * @return the <var>destination</var> array
  194 + * @since 1.3
  195 + */
  196 + private static byte[] encode3to4(byte[] source, int srcOffset,
  197 + int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
  198 + // 1 2 3
  199 + // 01234567890123456789012345678901 Bit position
  200 + // --------000000001111111122222222 Array position from threeBytes
  201 + // --------| || || || | Six bit groups to index alphabet
  202 + // >>18 >>12 >> 6 >> 0 Right shift necessary
  203 + // 0x3f 0x3f 0x3f Additional AND
  204 +
  205 + // Create buffer with zero-padding if there are only one or two
  206 + // significant bytes passed in the array.
  207 + // We have to shift left 24 in order to flush out the 1's that appear
  208 + // when Java treats a value as negative that is cast from a byte to an int.
  209 + int inBuff =
  210 + (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
  211 + | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
  212 + | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
  213 +
  214 + switch (numSigBytes) {
  215 + case 3:
  216 + destination[destOffset] = alphabet[(inBuff >>> 18)];
  217 + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
  218 + destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
  219 + destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
  220 + return destination;
  221 + case 2:
  222 + destination[destOffset] = alphabet[(inBuff >>> 18)];
  223 + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
  224 + destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
  225 + destination[destOffset + 3] = EQUALS_SIGN;
  226 + return destination;
  227 + case 1:
  228 + destination[destOffset] = alphabet[(inBuff >>> 18)];
  229 + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
  230 + destination[destOffset + 2] = EQUALS_SIGN;
  231 + destination[destOffset + 3] = EQUALS_SIGN;
  232 + return destination;
  233 + default:
  234 + return destination;
  235 + } // end switch
  236 + } // end encode3to4
  237 +
  238 + /**
  239 + * Encodes a byte array into Base64 notation.
  240 + * Equivalent to calling
  241 + * {@code encodeBytes(source, 0, source.length)}
  242 + *
  243 + * @param source The data to convert
  244 + * @since 1.4
  245 + */
  246 + public static String encode(byte[] source) {
  247 + return encode(source, 0, source.length, ALPHABET, true);
  248 + }
  249 +
  250 + /**
  251 + * Encodes a byte array into web safe Base64 notation.
  252 + *
  253 + * @param source The data to convert
  254 + * @param doPadding is {@code true} to pad result with '=' chars
  255 + * if it does not fall on 3 byte boundaries
  256 + */
  257 + public static String encodeWebSafe(byte[] source, boolean doPadding) {
  258 + return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
  259 + }
  260 +
  261 + /**
  262 + * Encodes a byte array into Base64 notation.
  263 + *
  264 + * @param source the data to convert
  265 + * @param off offset in array where conversion should begin
  266 + * @param len length of data to convert
  267 + * @param alphabet the encoding alphabet
  268 + * @param doPadding is {@code true} to pad result with '=' chars
  269 + * if it does not fall on 3 byte boundaries
  270 + * @since 1.4
  271 + */
  272 + public static String encode(byte[] source, int off, int len, byte[] alphabet,
  273 + boolean doPadding) {
  274 + byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
  275 + int outLen = outBuff.length;
  276 +
  277 + // If doPadding is false, set length to truncate '='
  278 + // padding characters
  279 + while (doPadding == false && outLen > 0) {
  280 + if (outBuff[outLen - 1] != '=') {
  281 + break;
  282 + }
  283 + outLen -= 1;
  284 + }
  285 +
  286 + return new String(outBuff, 0, outLen);
  287 + }
  288 +
  289 + /**
  290 + * Encodes a byte array into Base64 notation.
  291 + *
  292 + * @param source the data to convert
  293 + * @param off offset in array where conversion should begin
  294 + * @param len length of data to convert
  295 + * @param alphabet is the encoding alphabet
  296 + * @param maxLineLength maximum length of one line.
  297 + * @return the BASE64-encoded byte array
  298 + */
  299 + public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
  300 + int maxLineLength) {
  301 + int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
  302 + int len43 = lenDiv3 * 4;
  303 + byte[] outBuff = new byte[len43 // Main 4:3
  304 + + (len43 / maxLineLength)]; // New lines
  305 +
  306 + int d = 0;
  307 + int e = 0;
  308 + int len2 = len - 2;
  309 + int lineLength = 0;
  310 + for (; d < len2; d += 3, e += 4) {
  311 +
  312 + // The following block of code is the same as
  313 + // encode3to4( source, d + off, 3, outBuff, e, alphabet );
  314 + // but inlined for faster encoding (~20% improvement)
  315 + int inBuff =
  316 + ((source[d + off] << 24) >>> 8)
  317 + | ((source[d + 1 + off] << 24) >>> 16)
  318 + | ((source[d + 2 + off] << 24) >>> 24);
  319 + outBuff[e] = alphabet[(inBuff >>> 18)];
  320 + outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
  321 + outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
  322 + outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
  323 +
  324 + lineLength += 4;
  325 + if (lineLength == maxLineLength) {
  326 + outBuff[e + 4] = NEW_LINE;
  327 + e++;
  328 + lineLength = 0;
  329 + } // end if: end of line
  330 + } // end for: each piece of array
  331 +
  332 + if (d < len) {
  333 + encode3to4(source, d + off, len - d, outBuff, e, alphabet);
  334 +
  335 + lineLength += 4;
  336 + if (lineLength == maxLineLength) {
  337 + // Add a last newline
  338 + outBuff[e + 4] = NEW_LINE;
  339 + e++;
  340 + }
  341 + e += 4;
  342 + }
  343 +
  344 + assert (e == outBuff.length);
  345 + return outBuff;
  346 + }
  347 +
  348 +
  349 + /* ******** D E C O D I N G M E T H O D S ******** */
  350 +
  351 +
  352 + /**
  353 + * Decodes four bytes from array <var>source</var>
  354 + * and writes the resulting bytes (up to three of them)
  355 + * to <var>destination</var>.
  356 + * The source and destination arrays can be manipulated
  357 + * anywhere along their length by specifying
  358 + * <var>srcOffset</var> and <var>destOffset</var>.
  359 + * This method does not check to make sure your arrays
  360 + * are large enough to accommodate <var>srcOffset</var> + 4 for
  361 + * the <var>source</var> array or <var>destOffset</var> + 3 for
  362 + * the <var>destination</var> array.
  363 + * This method returns the actual number of bytes that
  364 + * were converted from the Base64 encoding.
  365 + *
  366 + *
  367 + * @param source the array to convert
  368 + * @param srcOffset the index where conversion begins
  369 + * @param destination the array to hold the conversion
  370 + * @param destOffset the index where output will be put
  371 + * @param decodabet the decodabet for decoding Base64 content
  372 + * @return the number of decoded bytes converted
  373 + * @since 1.3
  374 + */
  375 + private static int decode4to3(byte[] source, int srcOffset,
  376 + byte[] destination, int destOffset, byte[] decodabet) {
  377 + // Example: Dk==
  378 + if (source[srcOffset + 2] == EQUALS_SIGN) {
  379 + int outBuff =
  380 + ((decodabet[source[srcOffset]] << 24) >>> 6)
  381 + | ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
  382 +
  383 + destination[destOffset] = (byte) (outBuff >>> 16);
  384 + return 1;
  385 + } else if (source[srcOffset + 3] == EQUALS_SIGN) {
  386 + // Example: DkL=
  387 + int outBuff =
  388 + ((decodabet[source[srcOffset]] << 24) >>> 6)
  389 + | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
  390 + | ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
  391 +
  392 + destination[destOffset] = (byte) (outBuff >>> 16);
  393 + destination[destOffset + 1] = (byte) (outBuff >>> 8);
  394 + return 2;
  395 + } else {
  396 + // Example: DkLE
  397 + int outBuff =
  398 + ((decodabet[source[srcOffset]] << 24) >>> 6)
  399 + | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
  400 + | ((decodabet[source[srcOffset + 2]] << 24) >>> 18)
  401 + | ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
  402 +
  403 + destination[destOffset] = (byte) (outBuff >> 16);
  404 + destination[destOffset + 1] = (byte) (outBuff >> 8);
  405 + destination[destOffset + 2] = (byte) (outBuff);
  406 + return 3;
  407 + }
  408 + } // end decodeToBytes
  409 +
  410 +
  411 + /**
  412 + * Decodes data from Base64 notation.
  413 + *
  414 + * @param s the string to decode (decoded in default encoding)
  415 + * @return the decoded data
  416 + * @since 1.4
  417 + */
  418 + public static byte[] decode(String s) throws Base64DecoderException {
  419 + byte[] bytes = s.getBytes();
  420 + return decode(bytes, 0, bytes.length);
  421 + }
  422 +
  423 + /**
  424 + * Decodes data from web safe Base64 notation.
  425 + * Web safe encoding uses '-' instead of '+', '_' instead of '/'
  426 + *
  427 + * @param s the string to decode (decoded in default encoding)
  428 + * @return the decoded data
  429 + */
  430 + public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
  431 + byte[] bytes = s.getBytes();
  432 + return decodeWebSafe(bytes, 0, bytes.length);
  433 + }
  434 +
  435 + /**
  436 + * Decodes Base64 content in byte array format and returns
  437 + * the decoded byte array.
  438 + *
  439 + * @param source The Base64 encoded data
  440 + * @return decoded data
  441 + * @since 1.3
  442 + * @throws Base64DecoderException
  443 + */
  444 + public static byte[] decode(byte[] source) throws Base64DecoderException {
  445 + return decode(source, 0, source.length);
  446 + }
  447 +
  448 + /**
  449 + * Decodes web safe Base64 content in byte array format and returns
  450 + * the decoded data.
  451 + * Web safe encoding uses '-' instead of '+', '_' instead of '/'
  452 + *
  453 + * @param source the string to decode (decoded in default encoding)
  454 + * @return the decoded data
  455 + */
  456 + public static byte[] decodeWebSafe(byte[] source)
  457 + throws Base64DecoderException {
  458 + return decodeWebSafe(source, 0, source.length);
  459 + }
  460 +
  461 + /**
  462 + * Decodes Base64 content in byte array format and returns
  463 + * the decoded byte array.
  464 + *
  465 + * @param source the Base64 encoded data
  466 + * @param off the offset of where to begin decoding
  467 + * @param len the length of characters to decode
  468 + * @return decoded data
  469 + * @since 1.3
  470 + * @throws Base64DecoderException
  471 + */
  472 + public static byte[] decode(byte[] source, int off, int len)
  473 + throws Base64DecoderException {
  474 + return decode(source, off, len, DECODABET);
  475 + }
  476 +
  477 + /**
  478 + * Decodes web safe Base64 content in byte array format and returns
  479 + * the decoded byte array.
  480 + * Web safe encoding uses '-' instead of '+', '_' instead of '/'
  481 + *
  482 + * @param source the Base64 encoded data
  483 + * @param off the offset of where to begin decoding
  484 + * @param len the length of characters to decode
  485 + * @return decoded data
  486 + */
  487 + public static byte[] decodeWebSafe(byte[] source, int off, int len)
  488 + throws Base64DecoderException {
  489 + return decode(source, off, len, WEBSAFE_DECODABET);
  490 + }
  491 +
  492 + /**
  493 + * Decodes Base64 content using the supplied decodabet and returns
  494 + * the decoded byte array.
  495 + *
  496 + * @param source the Base64 encoded data
  497 + * @param off the offset of where to begin decoding
  498 + * @param len the length of characters to decode
  499 + * @param decodabet the decodabet for decoding Base64 content
  500 + * @return decoded data
  501 + */
  502 + public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
  503 + throws Base64DecoderException {
  504 + int len34 = len * 3 / 4;
  505 + byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
  506 + int outBuffPosn = 0;
  507 +
  508 + byte[] b4 = new byte[4];
  509 + int b4Posn = 0;
  510 + int i = 0;
  511 + byte sbiCrop = 0;
  512 + byte sbiDecode = 0;
  513 + for (i = 0; i < len; i++) {
  514 + sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits
  515 + sbiDecode = decodabet[sbiCrop];
  516 +
  517 + if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
  518 + if (sbiDecode >= EQUALS_SIGN_ENC) {
  519 + // An equals sign (for padding) must not occur at position 0 or 1
  520 + // and must be the last byte[s] in the encoded value
  521 + if (sbiCrop == EQUALS_SIGN) {
  522 + int bytesLeft = len - i;
  523 + byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
  524 + if (b4Posn == 0 || b4Posn == 1) {
  525 + throw new Base64DecoderException(
  526 + "invalid padding byte '=' at byte offset " + i);
  527 + } else if ((b4Posn == 3 && bytesLeft > 2)
  528 + || (b4Posn == 4 && bytesLeft > 1)) {
  529 + throw new Base64DecoderException(
  530 + "padding byte '=' falsely signals end of encoded value "
  531 + + "at offset " + i);
  532 + } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
  533 + throw new Base64DecoderException(
  534 + "encoded value has invalid trailing byte");
  535 + }
  536 + break;
  537 + }
  538 +
  539 + b4[b4Posn++] = sbiCrop;
  540 + if (b4Posn == 4) {
  541 + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
  542 + b4Posn = 0;
  543 + }
  544 + }
  545 + } else {
  546 + throw new Base64DecoderException("Bad Base64 input character at " + i
  547 + + ": " + source[i + off] + "(decimal)");
  548 + }
  549 + }
  550 +
  551 + // Because web safe encoding allows non padding base64 encodes, we
  552 + // need to pad the rest of the b4 buffer with equal signs when
  553 + // b4Posn != 0. There can be at most 2 equal signs at the end of
  554 + // four characters, so the b4 buffer must have two or three
  555 + // characters. This also catches the case where the input is
  556 + // padded with EQUALS_SIGN
  557 + if (b4Posn != 0) {
  558 + if (b4Posn == 1) {
  559 + throw new Base64DecoderException("single trailing character at offset "
  560 + + (len - 1));
  561 + }
  562 + b4[b4Posn++] = EQUALS_SIGN;
  563 + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
  564 + }
  565 +
  566 + byte[] out = new byte[outBuffPosn];
  567 + System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
  568 + return out;
  569 + }
  570 +}
... ...
app/src/main/java/net/devfac/userstory/Utils/iab/Base64DecoderException.java 0 → 100644
  1 +// Copyright 2002, Google, Inc.
  2 +//
  3 +// Licensed under the Apache License, Version 2.0 (the "License");
  4 +// you may not use this file except in compliance with the License.
  5 +// You may obtain a copy of the License at
  6 +//
  7 +// http://www.apache.org/licenses/LICENSE-2.0
  8 +//
  9 +// Unless required by applicable law or agreed to in writing, software
  10 +// distributed under the License is distributed on an "AS IS" BASIS,
  11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12 +// See the License for the specific language governing permissions and
  13 +// limitations under the License.
  14 +
  15 +package net.devfac.userstory.Utils.iab;
  16 +
  17 +/**
  18 + * Exception thrown when encountering an invalid Base64 input character.
  19 + *
  20 + * @author nelson
  21 + */
  22 +public class Base64DecoderException extends Exception {
  23 + public Base64DecoderException() {
  24 + super();
  25 + }
  26 +
  27 + public Base64DecoderException(String s) {
  28 + super(s);
  29 + }
  30 +
  31 + private static final long serialVersionUID = 1L;
  32 +}
... ...
app/src/main/java/net/devfac/userstory/Utils/iab/IabException.java 0 → 100644
  1 +/* Copyright (c) 2012 Google Inc.
  2 + *
  3 + * Licensed under the Apache License, Version 2.0 (the "License");
  4 + * you may not use this file except in compliance with the License.
  5 + * You may obtain a copy of the License at
  6 + *
  7 + * http://www.apache.org/licenses/LICENSE-2.0
  8 + *
  9 + * Unless required by applicable law or agreed to in writing, software
  10 + * distributed under the License is distributed on an "AS IS" BASIS,
  11 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12 + * See the License for the specific language governing permissions and
  13 + * limitations under the License.
  14 + */
  15 +
  16 +package net.devfac.userstory.Utils.iab;
  17 +
  18 +/**
  19 + * Exception thrown when something went wrong with in-app billing.
  20 + * An IabException has an associated IabResult (an error).
  21 + * To get the IAB result that caused this exception to be thrown,
  22 + * call {@link #getResult()}.
  23 + */
  24 +public class IabException extends Exception {
  25 + IabResult mResult;
  26 +
  27 + public IabException(IabResult r) {
  28 + this(r, null);
  29 + }
  30 + public IabException(int response, String message) {
  31 + this(new IabResult(response, message));
  32 + }
  33 + public IabException(IabResult r, Exception cause) {
  34 + super(r.getMessage(), cause);
  35 + mResult = r;
  36 + }
  37 + public IabException(int response, String message, Exception cause) {
  38 + this(new IabResult(response, message), cause);
  39 + }
  40 +
  41 + /** Returns the IAB result (error) that this exception signals. */
  42 + public IabResult getResult() { return mResult; }
  43 +}
0 44 \ No newline at end of file
... ...
app/src/main/java/net/devfac/userstory/Utils/iab/IabHelper.java 0 → 100644
  1 +/* Copyright (c) 2012 Google Inc.
  2 + *
  3 + * Licensed under the Apache License, Version 2.0 (the "License");
  4 + * you may not use this file except in compliance with the License.
  5 + * You may obtain a copy of the License at
  6 + *
  7 + * http://www.apache.org/licenses/LICENSE-2.0
  8 + *
  9 + * Unless required by applicable law or agreed to in writing, software
  10 + * distributed under the License is distributed on an "AS IS" BASIS,
  11 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12 + * See the License for the specific language governing permissions and
  13 + * limitations under the License.
  14 + */
  15 +
  16 +package net.devfac.userstory.Utils.iab;
  17 +
  18 +import android.app.Activity;
  19 +import android.app.PendingIntent;
  20 +import android.content.ComponentName;
  21 +import android.content.Context;
  22 +import android.content.Intent;
  23 +import android.content.IntentSender.SendIntentException;
  24 +import android.content.ServiceConnection;
  25 +import android.os.Bundle;
  26 +import android.os.Handler;
  27 +import android.os.IBinder;
  28 +import android.os.RemoteException;
  29 +import android.text.TextUtils;
  30 +import android.util.Log;
  31 +
  32 +import com.android.vending.billing.IInAppBillingService;
  33 +
  34 +import org.json.JSONException;
  35 +
  36 +import java.util.ArrayList;
  37 +import java.util.List;
  38 +
  39 +
  40 +/**
  41 + * Provides convenience methods for in-app billing. You can create one instance of this
  42 + * class for your application and use it to process in-app billing operations.
  43 + * It provides synchronous (blocking) and asynchronous (non-blocking) methods for
  44 + * many common in-app billing operations, as well as automatic signature
  45 + * verification.
  46 + *
  47 + * After instantiating, you must perform setup in order to start using the object.
  48 + * To perform setup, call the {@link #startSetup} method and provide a listener;
  49 + * that listener will be notified when setup is complete, after which (and not before)
  50 + * you may call other methods.
  51 + *
  52 + * After setup is complete, you will typically want to request an inventory of owned
  53 + * items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync}
  54 + * and related methods.
  55 + *
  56 + * When you are done with this object, don't forget to call {@link #dispose}
  57 + * to ensure proper cleanup. This object holds a binding to the in-app billing
  58 + * service, which will leak unless you dispose of it correctly. If you created
  59 + * the object on an Activity's onCreate method, then the recommended
  60 + * place to dispose of it is the Activity's onDestroy method.
  61 + *
  62 + * A note about threading: When using this object from a background thread, you may
  63 + * call the blocking versions of methods; when using from a UI thread, call
  64 + * only the asynchronous versions and handle the results via callbacks.
  65 + * Also, notice that you can only call one asynchronous operation at a time;
  66 + * attempting to start a second asynchronous operation while the first one
  67 + * has not yet completed will result in an exception being thrown.
  68 + *
  69 + * @author Bruno Oliveira (Google)
  70 + *
  71 + */
  72 +public class IabHelper {
  73 + // Is debug logging enabled?
  74 + boolean mDebugLog = false;
  75 + String mDebugTag = "IabHelper";
  76 +
  77 + // Is setup done?
  78 + boolean mSetupDone = false;
  79 +
  80 + // Has this object been disposed of? (If so, we should ignore callbacks, etc)
  81 + boolean mDisposed = false;
  82 +
  83 + // Are subscriptions supported?
  84 + boolean mSubscriptionsSupported = false;
  85 +
  86 + // Is an asynchronous operation in progress?
  87 + // (only one at a time can be in progress)
  88 + boolean mAsyncInProgress = false;
  89 +
  90 + // (for logging/debugging)
  91 + // if mAsyncInProgress == true, what asynchronous operation is in progress?
  92 + String mAsyncOperation = "";
  93 +
  94 + // Context we were passed during initialization
  95 + Context mContext;
  96 +
  97 + // Connection to the service
  98 + IInAppBillingService mService;
  99 + ServiceConnection mServiceConn;
  100 +
  101 + // The request code used to launch purchase flow
  102 + int mRequestCode;
  103 +
  104 + // The item type of the current purchase flow
  105 + String mPurchasingItemType;
  106 +
  107 + // Public key for verifying signature, in base64 encoding
  108 + String mSignatureBase64 = null;
  109 +
  110 + // Billing response codes
  111 + public static final int BILLING_RESPONSE_RESULT_OK = 0;
  112 + public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
  113 + public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
  114 + public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
  115 + public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
  116 + public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
  117 + public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
  118 + public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
  119 +
  120 + // IAB Helper error codes
  121 + public static final int IABHELPER_ERROR_BASE = -1000;
  122 + public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
  123 + public static final int IABHELPER_BAD_RESPONSE = -1002;
  124 + public static final int IABHELPER_VERIFICATION_FAILED = -1003;
  125 + public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
  126 + public static final int IABHELPER_USER_CANCELLED = -1005;
  127 + public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
  128 + public static final int IABHELPER_MISSING_TOKEN = -1007;
  129 + public static final int IABHELPER_UNKNOWN_ERROR = -1008;
  130 + public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
  131 + public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
  132 +
  133 + // Keys for the responses from InAppBillingService
  134 + public static final String RESPONSE_CODE = "RESPONSE_CODE";
  135 + public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
  136 + public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
  137 + public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
  138 + public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
  139 + public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
  140 + public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
  141 + public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
  142 + public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
  143 +
  144 + // Item types
  145 + public static final String ITEM_TYPE_INAPP = "inapp";
  146 + public static final String ITEM_TYPE_SUBS = "subs";
  147 +
  148 + // some fields on the getSkuDetails response bundle
  149 + public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
  150 + public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";
  151 +
  152 + /**
  153 + * Creates an instance. After creation, it will not yet be ready to use. You must perform
  154 + * setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not
  155 + * block and is safe to call from a UI thread.
  156 + *
  157 + * @param ctx Your application or Activity context. Needed to bind to the in-app billing service.
  158 + * @param base64PublicKey Your application's public key, encoded in base64.
  159 + * This is used for verification of purchase signatures. You can find your app's base64-encoded
  160 + * public key in your application's page on Google Play Developer Console. Note that this
  161 + * is NOT your "developer public key".
  162 + */
  163 + public IabHelper(Context ctx, String base64PublicKey) {
  164 + mContext = ctx.getApplicationContext();
  165 + mSignatureBase64 = base64PublicKey;
  166 + logDebug("IAB helper created.");
  167 + }
  168 +
  169 + /**
  170 + * Enables or disable debug logging through LogCat.
  171 + */
  172 + public void enableDebugLogging(boolean enable, String tag) {
  173 + checkNotDisposed();
  174 + mDebugLog = enable;
  175 + mDebugTag = tag;
  176 + }
  177 +
  178 + public void enableDebugLogging(boolean enable) {
  179 + checkNotDisposed();
  180 + mDebugLog = enable;
  181 + }
  182 +
  183 + /**
  184 + * Callback for setup process. This listener's {@link #onIabSetupFinished} method is called
  185 + * when the setup process is complete.
  186 + */
  187 + public interface OnIabSetupFinishedListener {
  188 + /**
  189 + * Called to notify that setup is complete.
  190 + *
  191 + * @param result The result of the setup process.
  192 + */
  193 + public void onIabSetupFinished(IabResult result);
  194 + }
  195 +
  196 + /**
  197 + * Starts the setup process. This will start up the setup process asynchronously.
  198 + * You will be notified through the listener when the setup process is complete.
  199 + * This method is safe to call from a UI thread.
  200 + *
  201 + * @param listener The listener to notify when the setup process is complete.
  202 + */
  203 + public void startSetup(final OnIabSetupFinishedListener listener) {
  204 + // If already set up, can't do it again.
  205 + checkNotDisposed();
  206 + if (mSetupDone) throw new IllegalStateException("IAB helper is already set up.");
  207 +
  208 + // Connection to IAB service
  209 + logDebug("Starting in-app billing setup.");
  210 + mServiceConn = new ServiceConnection() {
  211 + @Override
  212 + public void onServiceDisconnected(ComponentName name) {
  213 + logDebug("Billing service disconnected.");
  214 + mService = null;
  215 + }
  216 +
  217 + @Override
  218 + public void onServiceConnected(ComponentName name, IBinder service) {
  219 + if (mDisposed) return;
  220 + logDebug("Billing service connected.");
  221 + mService = IInAppBillingService.Stub.asInterface(service);
  222 + String packageName = mContext.getPackageName();
  223 + try {
  224 + logDebug("Checking for in-app billing 3 support.");
  225 +
  226 + // check for in-app billing v3 support
  227 + int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
  228 + if (response != BILLING_RESPONSE_RESULT_OK) {
  229 + if (listener != null) listener.onIabSetupFinished(new IabResult(response,
  230 + "Error checking for billing v3 support."));
  231 +
  232 + // if in-app purchases aren't supported, neither are subscriptions.
  233 + mSubscriptionsSupported = false;
  234 + return;
  235 + }
  236 + logDebug("In-app billing version 3 supported for " + packageName);
  237 +
  238 + // check for v3 subscriptions support
  239 + response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
  240 + if (response == BILLING_RESPONSE_RESULT_OK) {
  241 + logDebug("Subscriptions AVAILABLE.");
  242 + mSubscriptionsSupported = true;
  243 + }
  244 + else {
  245 + logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
  246 + }
  247 +
  248 + mSetupDone = true;
  249 + }
  250 + catch (RemoteException e) {
  251 + if (listener != null) {
  252 + listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
  253 + "RemoteException while setting up in-app billing."));
  254 + }
  255 + e.printStackTrace();
  256 + return;
  257 + }
  258 +
  259 + if (listener != null) {
  260 + listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
  261 + }
  262 + }
  263 + };
  264 +
  265 + Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
  266 + serviceIntent.setPackage("com.android.vending");
  267 + if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {
  268 + // service available to handle that Intent
  269 + mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
  270 + }
  271 + else {
  272 + // no service available to handle that Intent
  273 + if (listener != null) {
  274 + listener.onIabSetupFinished(
  275 + new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
  276 + "Billing service unavailable on device."));
  277 + }
  278 + }
  279 + }
  280 +
  281 + /**
  282 + * Dispose of object, releasing resources. It's very important to call this
  283 + * method when you are done with this object. It will release any resources
  284 + * used by it such as service connections. Naturally, once the object is
  285 + * disposed of, it can't be used again.
  286 + */
  287 + public void dispose() {
  288 + logDebug("Disposing.");
  289 + mSetupDone = false;
  290 + if (mServiceConn != null) {
  291 + logDebug("Unbinding from service.");
  292 + if (mContext != null) mContext.unbindService(mServiceConn);
  293 + }
  294 + mDisposed = true;
  295 + mContext = null;
  296 + mServiceConn = null;
  297 + mService = null;
  298 + mPurchaseListener = null;
  299 + }
  300 +
  301 + private void checkNotDisposed() {
  302 + if (mDisposed) throw new IllegalStateException("IabHelper was disposed of, so it cannot be used.");
  303 + }
  304 +
  305 + /** Returns whether subscriptions are supported. */
  306 + public boolean subscriptionsSupported() {
  307 + checkNotDisposed();
  308 + return mSubscriptionsSupported;
  309 + }
  310 +
  311 +
  312 + /**
  313 + * Callback that notifies when a purchase is finished.
  314 + */
  315 + public interface OnIabPurchaseFinishedListener {
  316 + /**
  317 + * Called to notify that an in-app purchase finished. If the purchase was successful,
  318 + * then the sku parameter specifies which item was purchased. If the purchase failed,
  319 + * the sku and extraData parameters may or may not be null, depending on how far the purchase
  320 + * process went.
  321 + *
  322 + * @param result The result of the purchase.
  323 + * @param info The purchase information (null if purchase failed)
  324 + */
  325 + public void onIabPurchaseFinished(IabResult result, Purchase info);
  326 + }
  327 +
  328 + // The listener registered on launchPurchaseFlow, which we have to call back when
  329 + // the purchase finishes
  330 + OnIabPurchaseFinishedListener mPurchaseListener;
  331 +
  332 + public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) {
  333 + launchPurchaseFlow(act, sku, requestCode, listener, "");
  334 + }
  335 +
  336 + public void launchPurchaseFlow(Activity act, String sku, int requestCode,
  337 + OnIabPurchaseFinishedListener listener, String extraData) {
  338 + launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData);
  339 + }
  340 +
  341 + public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
  342 + OnIabPurchaseFinishedListener listener) {
  343 + launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, "");
  344 + }
  345 +
  346 + public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
  347 + OnIabPurchaseFinishedListener listener, String extraData) {
  348 + launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData);
  349 + }
  350 +
  351 + /**
  352 + * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
  353 + * which will involve bringing up the Google Play screen. The calling activity will be paused while
  354 + * the user interacts with Google Play, and the result will be delivered via the activity's
  355 + * {@link Activity#onActivityResult} method, at which point you must call
  356 + * this object's {@link #handleActivityResult} method to continue the purchase flow. This method
  357 + * MUST be called from the UI thread of the Activity.
  358 + *
  359 + * @param act The calling activity.
  360 + * @param sku The sku of the item to purchase.
  361 + * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS)
  362 + * @param requestCode A request code (to differentiate from other responses --
  363 + * as in {@link Activity#startActivityForResult}).
  364 + * @param listener The listener to notify when the purchase process finishes
  365 + * @param extraData Extra data (developer payload), which will be returned with the purchase data
  366 + * when the purchase completes. This extra data will be permanently bound to that purchase
  367 + * and will always be returned when the purchase is queried.
  368 + */
  369 + public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode,
  370 + OnIabPurchaseFinishedListener listener, String extraData) {
  371 + checkNotDisposed();
  372 + checkSetupDone("launchPurchaseFlow");
  373 + flagStartAsync("launchPurchaseFlow");
  374 + IabResult result;
  375 +
  376 + if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
  377 + IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE,
  378 + "Subscriptions are not available.");
  379 + flagEndAsync();
  380 + if (listener != null) listener.onIabPurchaseFinished(r, null);
  381 + return;
  382 + }
  383 +
  384 + try {
  385 + logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
  386 + Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);
  387 + int response = getResponseCodeFromBundle(buyIntentBundle);
  388 + if (response != BILLING_RESPONSE_RESULT_OK) {
  389 + logError("Unable to buy item, Error response: " + getResponseDesc(response));
  390 + flagEndAsync();
  391 + result = new IabResult(response, "Unable to buy item");
  392 + if (listener != null) listener.onIabPurchaseFinished(result, null);
  393 + return;
  394 + }
  395 +
  396 + PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
  397 + logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
  398 + mRequestCode = requestCode;
  399 + mPurchaseListener = listener;
  400 + mPurchasingItemType = itemType;
  401 + act.startIntentSenderForResult(pendingIntent.getIntentSender(),
  402 + requestCode, new Intent(),
  403 + Integer.valueOf(0), Integer.valueOf(0),
  404 + Integer.valueOf(0));
  405 + }
  406 + catch (SendIntentException e) {
  407 + logError("SendIntentException while launching purchase flow for sku " + sku);
  408 + e.printStackTrace();
  409 + flagEndAsync();
  410 +
  411 + result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
  412 + if (listener != null) listener.onIabPurchaseFinished(result, null);
  413 + }
  414 + catch (RemoteException e) {
  415 + logError("RemoteException while launching purchase flow for sku " + sku);
  416 + e.printStackTrace();
  417 + flagEndAsync();
  418 +
  419 + result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
  420 + if (listener != null) listener.onIabPurchaseFinished(result, null);
  421 + }
  422 + }
  423 +
  424 + /**
  425 + * Handles an activity result that's part of the purchase flow in in-app billing. If you
  426 + * are calling {@link #launchPurchaseFlow}, then you must call this method from your
  427 + * Activity's {@link Activity@onActivityResult} method. This method
  428 + * MUST be called from the UI thread of the Activity.
  429 + *
  430 + * @param requestCode The requestCode as you received it.
  431 + * @param resultCode The resultCode as you received it.
  432 + * @param data The data (Intent) as you received it.
  433 + * @return Returns true if the result was related to a purchase flow and was handled;
  434 + * false if the result was not related to a purchase, in which case you should
  435 + * handle it normally.
  436 + */
  437 + public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
  438 + IabResult result;
  439 + if (requestCode != mRequestCode) return false;
  440 +
  441 + checkNotDisposed();
  442 + checkSetupDone("handleActivityResult");
  443 +
  444 + // end of async purchase operation that started on launchPurchaseFlow
  445 + flagEndAsync();
  446 +
  447 + if (data == null) {
  448 + logError("Null data in IAB activity result.");
  449 + result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
  450 + if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
  451 + return true;
  452 + }
  453 +
  454 + int responseCode = getResponseCodeFromIntent(data);
  455 + String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
  456 + String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
  457 +
  458 + if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
  459 + logDebug("Successful resultcode from purchase activity.");
  460 + logDebug("Purchase data: " + purchaseData);
  461 + logDebug("Data signature: " + dataSignature);
  462 + logDebug("Extras: " + data.getExtras());
  463 + logDebug("Expected item type: " + mPurchasingItemType);
  464 +
  465 + if (purchaseData == null || dataSignature == null) {
  466 + logError("BUG: either purchaseData or dataSignature is null.");
  467 + logDebug("Extras: " + data.getExtras().toString());
  468 + result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
  469 + if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
  470 + return true;
  471 + }
  472 +
  473 + Purchase purchase = null;
  474 + try {
  475 + purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
  476 + String sku = purchase.getSku();
  477 +
  478 + // Verify signature
  479 + if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
  480 + logError("Purchase signature verification FAILED for sku " + sku);
  481 + result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
  482 + if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase);
  483 + return true;
  484 + }
  485 + logDebug("Purchase signature successfully verified.");
  486 + }
  487 + catch (JSONException e) {
  488 + logError("Failed to parse purchase data.");
  489 + e.printStackTrace();
  490 + result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
  491 + if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
  492 + return true;
  493 + }
  494 +
  495 + if (mPurchaseListener != null) {
  496 + mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
  497 + }
  498 + }
  499 + else if (resultCode == Activity.RESULT_OK) {
  500 + // result code was OK, but in-app billing response was not OK.
  501 + logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));
  502 + if (mPurchaseListener != null) {
  503 + result = new IabResult(responseCode, "Problem purchashing item.");
  504 + mPurchaseListener.onIabPurchaseFinished(result, null);
  505 + }
  506 + }
  507 + else if (resultCode == Activity.RESULT_CANCELED) {
  508 + logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
  509 + result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
  510 + if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
  511 + }
  512 + else {
  513 + logError("Purchase failed. Result code: " + Integer.toString(resultCode)
  514 + + ". Response: " + getResponseDesc(responseCode));
  515 + result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
  516 + if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
  517 + }
  518 + return true;
  519 + }
  520 +
  521 + public Inventory queryInventory(boolean querySkuDetails, List<String> moreSkus) throws IabException {
  522 + return queryInventory(querySkuDetails, moreSkus, null);
  523 + }
  524 +
  525 + /**
  526 + * Queries the inventory. This will query all owned items from the server, as well as
  527 + * information on additional skus, if specified. This method may block or take long to execute.
  528 + * Do not call from a UI thread. For that, use the non-blocking version {@link #refreshInventoryAsync}.
  529 + *
  530 + * @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well
  531 + * as purchase information.
  532 + * @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership.
  533 + * Ignored if null or if querySkuDetails is false.
  534 + * @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership.
  535 + * Ignored if null or if querySkuDetails is false.
  536 + * @throws IabException if a problem occurs while refreshing the inventory.
  537 + */
  538 + public Inventory queryInventory(boolean querySkuDetails, List<String> moreItemSkus,
  539 + List<String> moreSubsSkus) throws IabException {
  540 + checkNotDisposed();
  541 + checkSetupDone("queryInventory");
  542 + try {
  543 + Inventory inv = new Inventory();
  544 + int r = queryPurchases(inv, ITEM_TYPE_INAPP);
  545 + if (r != BILLING_RESPONSE_RESULT_OK) {
  546 + throw new IabException(r, "Error refreshing inventory (querying owned items).");
  547 + }
  548 +
  549 + if (querySkuDetails) {
  550 + r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
  551 + if (r != BILLING_RESPONSE_RESULT_OK) {
  552 + throw new IabException(r, "Error refreshing inventory (querying prices of items).");
  553 + }
  554 + }
  555 +
  556 + // if subscriptions are supported, then also query for subscriptions
  557 + if (mSubscriptionsSupported) {
  558 + r = queryPurchases(inv, ITEM_TYPE_SUBS);
  559 + if (r != BILLING_RESPONSE_RESULT_OK) {
  560 + throw new IabException(r, "Error refreshing inventory (querying owned subscriptions).");
  561 + }
  562 +
  563 + if (querySkuDetails) {
  564 + r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus);
  565 + if (r != BILLING_RESPONSE_RESULT_OK) {
  566 + throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
  567 + }
  568 + }
  569 + }
  570 +
  571 + return inv;
  572 + }
  573 + catch (RemoteException e) {
  574 + throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e);
  575 + }
  576 + catch (JSONException e) {
  577 + throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e);
  578 + }
  579 + }
  580 +
  581 + /**
  582 + * Listener that notifies when an inventory query operation completes.
  583 + */
  584 + public interface QueryInventoryFinishedListener {
  585 + /**
  586 + * Called to notify that an inventory query operation completed.
  587 + *
  588 + * @param result The result of the operation.
  589 + * @param inv The inventory.
  590 + */
  591 + public void onQueryInventoryFinished(IabResult result, Inventory inv);
  592 + }
  593 +
  594 +
  595 + /**
  596 + * Asynchronous wrapper for inventory query. This will perform an inventory
  597 + * query as described in {@link #queryInventory}, but will do so asynchronously
  598 + * and call back the specified listener upon completion. This method is safe to
  599 + * call from a UI thread.
  600 + *
  601 + * @param querySkuDetails as in {@link #queryInventory}
  602 + * @param moreSkus as in {@link #queryInventory}
  603 + * @param listener The listener to notify when the refresh operation completes.
  604 + */
  605 + public void queryInventoryAsync(final boolean querySkuDetails,
  606 + final List<String> moreSkus,
  607 + final QueryInventoryFinishedListener listener) {
  608 + final Handler handler = new Handler();
  609 + checkNotDisposed();
  610 + checkSetupDone("queryInventory");
  611 + flagStartAsync("refresh inventory");
  612 + (new Thread(new Runnable() {
  613 + public void run() {
  614 + IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
  615 + Inventory inv = null;
  616 + try {
  617 + inv = queryInventory(querySkuDetails, moreSkus);
  618 + }
  619 + catch (IabException ex) {
  620 + result = ex.getResult();
  621 + }
  622 +
  623 + flagEndAsync();
  624 +
  625 + final IabResult result_f = result;
  626 + final Inventory inv_f = inv;
  627 + if (!mDisposed && listener != null) {
  628 + handler.post(new Runnable() {
  629 + public void run() {
  630 + listener.onQueryInventoryFinished(result_f, inv_f);
  631 + }
  632 + });
  633 + }
  634 + }
  635 + })).start();
  636 + }
  637 +
  638 + public void queryInventoryAsync(QueryInventoryFinishedListener listener) {
  639 + queryInventoryAsync(true, null, listener);
  640 + }
  641 +
  642 + public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) {
  643 + queryInventoryAsync(querySkuDetails, null, listener);
  644 + }
  645 +
  646 +
  647 + /**
  648 + * Consumes a given in-app product. Consuming can only be done on an item
  649 + * that's owned, and as a result of consumption, the user will no longer own it.
  650 + * This method may block or take long to return. Do not call from the UI thread.
  651 + * For that, see {@link #consumeAsync}.
  652 + *
  653 + * @param itemInfo The PurchaseInfo that represents the item to consume.
  654 + * @throws IabException if there is a problem during consumption.
  655 + */
  656 + void consume(Purchase itemInfo) throws IabException {
  657 + checkNotDisposed();
  658 + checkSetupDone("consume");
  659 +
  660 + if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) {
  661 + throw new IabException(IABHELPER_INVALID_CONSUMPTION,
  662 + "Items of type '" + itemInfo.mItemType + "' can't be consumed.");
  663 + }
  664 +
  665 + try {
  666 + String token = itemInfo.getToken();
  667 + String sku = itemInfo.getSku();
  668 + if (token == null || token.equals("")) {
  669 + logError("Can't consume "+ sku + ". No token.");
  670 + throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: "
  671 + + sku + " " + itemInfo);
  672 + }
  673 +
  674 + logDebug("Consuming sku: " + sku + ", token: " + token);
  675 + int response = mService.consumePurchase(3, mContext.getPackageName(), token);
  676 + if (response == BILLING_RESPONSE_RESULT_OK) {
  677 + logDebug("Successfully consumed sku: " + sku);
  678 + }
  679 + else {
  680 + logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
  681 + throw new IabException(response, "Error consuming sku " + sku);
  682 + }
  683 + }
  684 + catch (RemoteException e) {
  685 + throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e);
  686 + }
  687 + }
  688 +
  689 + /**
  690 + * Callback that notifies when a consumption operation finishes.
  691 + */
  692 + public interface OnConsumeFinishedListener {
  693 + /**
  694 + * Called to notify that a consumption has finished.
  695 + *
  696 + * @param purchase The purchase that was (or was to be) consumed.
  697 + * @param result The result of the consumption operation.
  698 + */
  699 + public void onConsumeFinished(Purchase purchase, IabResult result);
  700 + }
  701 +
  702 + /**
  703 + * Callback that notifies when a multi-item consumption operation finishes.
  704 + */
  705 + public interface OnConsumeMultiFinishedListener {
  706 + /**
  707 + * Called to notify that a consumption of multiple items has finished.
  708 + *
  709 + * @param purchases The purchases that were (or were to be) consumed.
  710 + * @param results The results of each consumption operation, corresponding to each
  711 + * sku.
  712 + */
  713 + public void onConsumeMultiFinished(List<Purchase> purchases, List<IabResult> results);
  714 + }
  715 +
  716 + /**
  717 + * Asynchronous wrapper to item consumption. Works like {@link #consume}, but
  718 + * performs the consumption in the background and notifies completion through
  719 + * the provided listener. This method is safe to call from a UI thread.
  720 + *
  721 + * @param purchase The purchase to be consumed.
  722 + * @param listener The listener to notify when the consumption operation finishes.
  723 + */
  724 + public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) {
  725 + checkNotDisposed();
  726 + checkSetupDone("consume");
  727 + List<Purchase> purchases = new ArrayList<Purchase>();
  728 + purchases.add(purchase);
  729 + consumeAsyncInternal(purchases, listener, null);
  730 + }
  731 +
  732 + /**
  733 + * Same as {@link consumeAsync}, but for multiple items at once.
  734 + * @param purchases The list of PurchaseInfo objects representing the purchases to consume.
  735 + * @param listener The listener to notify when the consumption operation finishes.
  736 + */
  737 + public void consumeAsync(List<Purchase> purchases, OnConsumeMultiFinishedListener listener) {
  738 + checkNotDisposed();
  739 + checkSetupDone("consume");
  740 + consumeAsyncInternal(purchases, null, listener);
  741 + }
  742 +
  743 + /**
  744 + * Returns a human-readable description for the given response code.
  745 + *
  746 + * @param code The response code
  747 + * @return A human-readable string explaining the result code.
  748 + * It also includes the result code numerically.
  749 + */
  750 + public static String getResponseDesc(int code) {
  751 + String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" +
  752 + "3:Billing Unavailable/4:Item unavailable/" +
  753 + "5:Developer Error/6:Error/7:Item Already Owned/" +
  754 + "8:Item not owned").split("/");
  755 + String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" +
  756 + "-1002:Bad response received/" +
  757 + "-1003:Purchase signature verification failed/" +
  758 + "-1004:Send intent failed/" +
  759 + "-1005:User cancelled/" +
  760 + "-1006:Unknown purchase response/" +
  761 + "-1007:Missing token/" +
  762 + "-1008:Unknown error/" +
  763 + "-1009:Subscriptions not available/" +
  764 + "-1010:Invalid consumption attempt").split("/");
  765 +
  766 + if (code <= IABHELPER_ERROR_BASE) {
  767 + int index = IABHELPER_ERROR_BASE - code;
  768 + if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index];
  769 + else return String.valueOf(code) + ":Unknown IAB Helper Error";
  770 + }
  771 + else if (code < 0 || code >= iab_msgs.length)
  772 + return String.valueOf(code) + ":Unknown";
  773 + else
  774 + return iab_msgs[code];
  775 + }
  776 +
  777 +
  778 + // Checks that setup was done; if not, throws an exception.
  779 + void checkSetupDone(String operation) {
  780 + if (!mSetupDone) {
  781 + logError("Illegal state for operation (" + operation + "): IAB helper is not set up.");
  782 + throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation);
  783 + }
  784 + }
  785 +
  786 + // Workaround to bug where sometimes response codes come as Long instead of Integer
  787 + int getResponseCodeFromBundle(Bundle b) {
  788 + Object o = b.get(RESPONSE_CODE);
  789 + if (o == null) {
  790 + logDebug("Bundle with null response code, assuming OK (known issue)");
  791 + return BILLING_RESPONSE_RESULT_OK;
  792 + }
  793 + else if (o instanceof Integer) return ((Integer)o).intValue();
  794 + else if (o instanceof Long) return (int)((Long)o).longValue();
  795 + else {
  796 + logError("Unexpected type for bundle response code.");
  797 + logError(o.getClass().getName());
  798 + throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
  799 + }
  800 + }
  801 +
  802 + // Workaround to bug where sometimes response codes come as Long instead of Integer
  803 + int getResponseCodeFromIntent(Intent i) {
  804 + Object o = i.getExtras().get(RESPONSE_CODE);
  805 + if (o == null) {
  806 + logError("Intent with no response code, assuming OK (known issue)");
  807 + return BILLING_RESPONSE_RESULT_OK;
  808 + }
  809 + else if (o instanceof Integer) return ((Integer)o).intValue();
  810 + else if (o instanceof Long) return (int)((Long)o).longValue();
  811 + else {
  812 + logError("Unexpected type for intent response code.");
  813 + logError(o.getClass().getName());
  814 + throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
  815 + }
  816 + }
  817 +
  818 + void flagStartAsync(String operation) {
  819 + if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" +
  820 + operation + ") because another async operation(" + mAsyncOperation + ") is in progress.");
  821 + mAsyncOperation = operation;
  822 + mAsyncInProgress = true;
  823 + logDebug("Starting async operation: " + operation);
  824 + }
  825 +
  826 + void flagEndAsync() {
  827 + logDebug("Ending async operation: " + mAsyncOperation);
  828 + mAsyncOperation = "";
  829 + mAsyncInProgress = false;
  830 + }
  831 +
  832 +
  833 + int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
  834 + // Query purchases
  835 + logDebug("Querying owned items, item type: " + itemType);
  836 + logDebug("Package name: " + mContext.getPackageName());
  837 + boolean verificationFailed = false;
  838 + String continueToken = null;
  839 +
  840 + do {
  841 + logDebug("Calling getPurchases with continuation token: " + continueToken);
  842 + Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
  843 + itemType, continueToken);
  844 +
  845 + int response = getResponseCodeFromBundle(ownedItems);
  846 + logDebug("Owned items response: " + String.valueOf(response));
  847 + if (response != BILLING_RESPONSE_RESULT_OK) {
  848 + logDebug("getPurchases() failed: " + getResponseDesc(response));
  849 + return response;
  850 + }
  851 + if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
  852 + || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
  853 + || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
  854 + logError("Bundle returned from getPurchases() doesn't contain required fields.");
  855 + return IABHELPER_BAD_RESPONSE;
  856 + }
  857 +
  858 + ArrayList<String> ownedSkus = ownedItems.getStringArrayList(
  859 + RESPONSE_INAPP_ITEM_LIST);
  860 + ArrayList<String> purchaseDataList = ownedItems.getStringArrayList(
  861 + RESPONSE_INAPP_PURCHASE_DATA_LIST);
  862 + ArrayList<String> signatureList = ownedItems.getStringArrayList(
  863 + RESPONSE_INAPP_SIGNATURE_LIST);
  864 +
  865 + for (int i = 0; i < purchaseDataList.size(); ++i) {
  866 + String purchaseData = purchaseDataList.get(i);
  867 + String signature = signatureList.get(i);
  868 + String sku = ownedSkus.get(i);
  869 + if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
  870 + logDebug("Sku is owned: " + sku);
  871 + Purchase purchase = new Purchase(itemType, purchaseData, signature);
  872 +
  873 + if (TextUtils.isEmpty(purchase.getToken())) {
  874 + logWarn("BUG: empty/null token!");
  875 + logDebug("Purchase data: " + purchaseData);
  876 + }
  877 +
  878 + // Record ownership and token
  879 + inv.addPurchase(purchase);
  880 + }
  881 + else {
  882 + logWarn("Purchase signature verification **FAILED**. Not adding item.");
  883 + logDebug(" Purchase data: " + purchaseData);
  884 + logDebug(" Signature: " + signature);
  885 + verificationFailed = true;
  886 + }
  887 + }
  888 +
  889 + continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
  890 + logDebug("Continuation token: " + continueToken);
  891 + } while (!TextUtils.isEmpty(continueToken));
  892 +
  893 + return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
  894 + }
  895 +
  896 + int querySkuDetails(String itemType, Inventory inv, List<String> moreSkus)
  897 + throws RemoteException, JSONException {
  898 + logDebug("Querying SKU details.");
  899 + ArrayList<String> skuList = new ArrayList<String>();
  900 + skuList.addAll(inv.getAllOwnedSkus(itemType));
  901 + if (moreSkus != null) {
  902 + for (String sku : moreSkus) {
  903 + if (!skuList.contains(sku)) {
  904 + skuList.add(sku);
  905 + }
  906 + }
  907 + }
  908 +
  909 + if (skuList.size() == 0) {
  910 + logDebug("queryPrices: nothing to do because there are no SKUs.");
  911 + return BILLING_RESPONSE_RESULT_OK;
  912 + }
  913 +
  914 + Bundle querySkus = new Bundle();
  915 + querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList);
  916 + Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(),
  917 + itemType, querySkus);
  918 +
  919 + if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
  920 + int response = getResponseCodeFromBundle(skuDetails);
  921 + if (response != BILLING_RESPONSE_RESULT_OK) {
  922 + logDebug("getSkuDetails() failed: " + getResponseDesc(response));
  923 + return response;
  924 + }
  925 + else {
  926 + logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
  927 + return IABHELPER_BAD_RESPONSE;
  928 + }
  929 + }
  930 +
  931 + ArrayList<String> responseList = skuDetails.getStringArrayList(
  932 + RESPONSE_GET_SKU_DETAILS_LIST);
  933 +
  934 + for (String thisResponse : responseList) {
  935 + SkuDetails d = new SkuDetails(itemType, thisResponse);
  936 + logDebug("Got sku details: " + d);
  937 + inv.addSkuDetails(d);
  938 + }
  939 + return BILLING_RESPONSE_RESULT_OK;
  940 + }
  941 +
  942 +
  943 + void consumeAsyncInternal(final List<Purchase> purchases,
  944 + final OnConsumeFinishedListener singleListener,
  945 + final OnConsumeMultiFinishedListener multiListener) {
  946 + final Handler handler = new Handler();
  947 + flagStartAsync("consume");
  948 + (new Thread(new Runnable() {
  949 + public void run() {
  950 + final List<IabResult> results = new ArrayList<IabResult>();
  951 + for (Purchase purchase : purchases) {
  952 + try {
  953 + consume(purchase);
  954 + results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
  955 + }
  956 + catch (IabException ex) {
  957 + results.add(ex.getResult());
  958 + }
  959 + }
  960 +
  961 + flagEndAsync();
  962 + if (!mDisposed && singleListener != null) {
  963 + handler.post(new Runnable() {
  964 + public void run() {
  965 + singleListener.onConsumeFinished(purchases.get(0), results.get(0));
  966 + }
  967 + });
  968 + }
  969 + if (!mDisposed && multiListener != null) {
  970 + handler.post(new Runnable() {
  971 + public void run() {
  972 + multiListener.onConsumeMultiFinished(purchases, results);
  973 + }
  974 + });
  975 + }
  976 + }
  977 + })).start();
  978 + }
  979 +
  980 + void logDebug(String msg) {
  981 + if (mDebugLog) Log.d(mDebugTag, msg);
  982 + }
  983 +
  984 + void logError(String msg) {
  985 + Log.e(mDebugTag, "In-app billing error: " + msg);
  986 + }
  987 +
  988 + void logWarn(String msg) {
  989 + Log.w(mDebugTag, "In-app billing warning: " + msg);
  990 + }
  991 +}
... ...
app/src/main/java/net/devfac/userstory/Utils/iab/IabResult.java 0 → 100644
  1 +/* Copyright (c) 2012 Google Inc.
  2 + *
  3 + * Licensed under the Apache License, Version 2.0 (the "License");
  4 + * you may not use this file except in compliance with the License.
  5 + * You may obtain a copy of the License at
  6 + *
  7 + * http://www.apache.org/licenses/LICENSE-2.0
  8 + *
  9 + * Unless required by applicable law or agreed to in writing, software
  10 + * distributed under the License is distributed on an "AS IS" BASIS,
  11 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12 + * See the License for the specific language governing permissions and
  13 + * limitations under the License.
  14 + */
  15 +
  16 +package net.devfac.userstory.Utils.iab;
  17 +
  18 +/**
  19 + * Represents the result of an in-app billing operation.
  20 + * A result is composed of a response code (an integer) and possibly a
  21 + * message (String). You can get those by calling
  22 + * {@link #getResponse} and {@link #getMessage()}, respectively. You
  23 + * can also inquire whether a result is a success or a failure by
  24 + * calling {@link #isSuccess()} and {@link #isFailure()}.
  25 + */
  26 +public class IabResult {
  27 + int mResponse;
  28 + String mMessage;
  29 +
  30 + public IabResult(int response, String message) {
  31 + mResponse = response;
  32 + if (message == null || message.trim().length() == 0) {
  33 + mMessage = IabHelper.getResponseDesc(response);
  34 + }
  35 + else {
  36 + mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")";
  37 + }
  38 + }
  39 + public int getResponse() { return mResponse; }
  40 + public String getMessage() { return mMessage; }
  41 + public boolean isSuccess() { return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK; }
  42 + public boolean isFailure() { return !isSuccess(); }
  43 + public String toString() { return "IabResult: " + getMessage(); }
  44 +}
  45 +
... ...
app/src/main/java/net/devfac/userstory/Utils/iab/Inventory.java 0 → 100644
  1 +/* Copyright (c) 2012 Google Inc.
  2 + *
  3 + * Licensed under the Apache License, Version 2.0 (the "License");
  4 + * you may not use this file except in compliance with the License.
  5 + * You may obtain a copy of the License at
  6 + *
  7 + * http://www.apache.org/licenses/LICENSE-2.0
  8 + *
  9 + * Unless required by applicable law or agreed to in writing, software
  10 + * distributed under the License is distributed on an "AS IS" BASIS,
  11 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12 + * See the License for the specific language governing permissions and
  13 + * limitations under the License.
  14 + */
  15 +
  16 +package net.devfac.userstory.Utils.iab;
  17 +
  18 +import java.util.ArrayList;
  19 +import java.util.HashMap;
  20 +import java.util.List;
  21 +import java.util.Map;
  22 +
  23 +/**
  24 + * Represents a block of information about in-app items.
  25 + * An Inventory is returned by such methods as {@link IabHelper#queryInventory}.
  26 + */
  27 +public class Inventory {
  28 + Map<String,SkuDetails> mSkuMap = new HashMap<String,SkuDetails>();
  29 + Map<String,Purchase> mPurchaseMap = new HashMap<String,Purchase>();
  30 +
  31 + Inventory() { }
  32 +
  33 + /** Returns the listing details for an in-app product. */
  34 + public SkuDetails getSkuDetails(String sku) {
  35 + return mSkuMap.get(sku);
  36 + }
  37 +
  38 + /** Returns purchase information for a given product, or null if there is no purchase. */
  39 + public Purchase getPurchase(String sku) {
  40 + return mPurchaseMap.get(sku);
  41 + }
  42 +
  43 + /** Returns whether or not there exists a purchase of the given product. */
  44 + public boolean hasPurchase(String sku) {
  45 + return mPurchaseMap.containsKey(sku);
  46 + }
  47 +
  48 + /** Return whether or not details about the given product are available. */
  49 + public boolean hasDetails(String sku) {
  50 + return mSkuMap.containsKey(sku);
  51 + }
  52 +
  53 + /**
  54 + * Erase a purchase (locally) from the inventory, given its product ID. This just
  55 + * modifies the Inventory object locally and has no effect on the server! This is
  56 + * useful when you have an existing Inventory object which you know to be up to date,
  57 + * and you have just consumed an item successfully, which means that erasing its
  58 + * purchase data from the Inventory you already have is quicker than querying for
  59 + * a new Inventory.
  60 + */
  61 + public void erasePurchase(String sku) {
  62 + if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku);
  63 + }
  64 +
  65 + /** Returns a list of all owned product IDs. */
  66 + List<String> getAllOwnedSkus() {
  67 + return new ArrayList<String>(mPurchaseMap.keySet());
  68 + }
  69 +
  70 + /** Returns a list of all owned product IDs of a given type */
  71 + List<String> getAllOwnedSkus(String itemType) {
  72 + List<String> result = new ArrayList<String>();
  73 + for (Purchase p : mPurchaseMap.values()) {
  74 + if (p.getItemType().equals(itemType)) result.add(p.getSku());
  75 + }
  76 + return result;
  77 + }
  78 +
  79 + /** Returns a list of all purchases. */
  80 + List<Purchase> getAllPurchases() {
  81 + return new ArrayList<Purchase>(mPurchaseMap.values());
  82 + }
  83 +
  84 + void addSkuDetails(SkuDetails d) {
  85 + mSkuMap.put(d.getSku(), d);
  86 + }
  87 +
  88 + void addPurchase(Purchase p) {
  89 + mPurchaseMap.put(p.getSku(), p);
  90 + }
  91 +}
... ...
app/src/main/java/net/devfac/userstory/Utils/iab/Purchase.java 0 → 100644
  1 +/* Copyright (c) 2012 Google Inc.
  2 + *
  3 + * Licensed under the Apache License, Version 2.0 (the "License");
  4 + * you may not use this file except in compliance with the License.
  5 + * You may obtain a copy of the License at
  6 + *
  7 + * http://www.apache.org/licenses/LICENSE-2.0
  8 + *
  9 + * Unless required by applicable law or agreed to in writing, software
  10 + * distributed under the License is distributed on an "AS IS" BASIS,
  11 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12 + * See the License for the specific language governing permissions and
  13 + * limitations under the License.
  14 + */
  15 +
  16 +package net.devfac.userstory.Utils.iab;
  17 +
  18 +import org.json.JSONException;
  19 +import org.json.JSONObject;
  20 +
  21 +/**
  22 + * Represents an in-app billing purchase.
  23 + */
  24 +public class Purchase {
  25 + String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS
  26 + String mOrderId;
  27 + String mPackageName;
  28 + String mSku;
  29 + long mPurchaseTime;
  30 + int mPurchaseState;
  31 + String mDeveloperPayload;
  32 + String mToken;
  33 + String mOriginalJson;
  34 + String mSignature;
  35 +
  36 + public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
  37 + mItemType = itemType;
  38 + mOriginalJson = jsonPurchaseInfo;
  39 + JSONObject o = new JSONObject(mOriginalJson);
  40 + mOrderId = o.optString("orderId");
  41 + mPackageName = o.optString("packageName");
  42 + mSku = o.optString("productId");
  43 + mPurchaseTime = o.optLong("purchaseTime");
  44 + mPurchaseState = o.optInt("purchaseState");
  45 + mDeveloperPayload = o.optString("developerPayload");
  46 + mToken = o.optString("token", o.optString("purchaseToken"));
  47 + mSignature = signature;
  48 + }
  49 +
  50 + public String getItemType() { return mItemType; }
  51 + public String getOrderId() { return mOrderId; }
  52 + public String getPackageName() { return mPackageName; }
  53 + public String getSku() { return mSku; }
  54 + public long getPurchaseTime() { return mPurchaseTime; }
  55 + public int getPurchaseState() { return mPurchaseState; }
  56 + public String getDeveloperPayload() { return mDeveloperPayload; }
  57 + public String getToken() { return mToken; }
  58 + public String getOriginalJson() { return mOriginalJson; }
  59 + public String getSignature() { return mSignature; }
  60 +
  61 + @Override
  62 + public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; }
  63 +}
... ...
app/src/main/java/net/devfac/userstory/Utils/iab/Security.java 0 → 100644
  1 +/* Copyright (c) 2012 Google Inc.
  2 + *
  3 + * Licensed under the Apache License, Version 2.0 (the "License");
  4 + * you may not use this file except in compliance with the License.
  5 + * You may obtain a copy of the License at
  6 + *
  7 + * http://www.apache.org/licenses/LICENSE-2.0
  8 + *
  9 + * Unless required by applicable law or agreed to in writing, software
  10 + * distributed under the License is distributed on an "AS IS" BASIS,
  11 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12 + * See the License for the specific language governing permissions and
  13 + * limitations under the License.
  14 + */
  15 +
  16 +package net.devfac.userstory.Utils.iab;
  17 +
  18 +import android.text.TextUtils;
  19 +import android.util.Log;
  20 +
  21 +import org.json.JSONException;
  22 +import org.json.JSONObject;
  23 +
  24 +
  25 +import java.security.InvalidKeyException;
  26 +import java.security.KeyFactory;
  27 +import java.security.NoSuchAlgorithmException;
  28 +import java.security.PublicKey;
  29 +import java.security.Signature;
  30 +import java.security.SignatureException;
  31 +import java.security.spec.InvalidKeySpecException;
  32 +import java.security.spec.X509EncodedKeySpec;
  33 +
  34 +/**
  35 + * Security-related methods. For a secure implementation, all of this code
  36 + * should be implemented on a server that communicates with the
  37 + * application on the device. For the sake of simplicity and clarity of this
  38 + * example, this code is included here and is executed on the device. If you
  39 + * must verify the purchases on the phone, you should obfuscate this code to
  40 + * make it harder for an attacker to replace the code with stubs that treat all
  41 + * purchases as verified.
  42 + */
  43 +public class Security {
  44 + private static final String TAG = "IABUtil/Security";
  45 +
  46 + private static final String KEY_FACTORY_ALGORITHM = "RSA";
  47 + private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
  48 +
  49 + /**
  50 + * Verifies that the data was signed with the given signature, and returns
  51 + * the verified purchase. The data is in JSON format and signed
  52 + * with a private key. The data also contains the {@link PurchaseState}
  53 + * and product ID of the purchase.
  54 + * @param base64PublicKey the base64-encoded public key to use for verifying.
  55 + * @param signedData the signed JSON string (signed, not encrypted)
  56 + * @param signature the signature for the data, signed with the private key
  57 + */
  58 + public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
  59 + if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
  60 + TextUtils.isEmpty(signature)) {
  61 + Log.e(TAG, "Purchase verification failed: missing data.");
  62 + return false;
  63 + }
  64 +
  65 + PublicKey key = Security.generatePublicKey(base64PublicKey);
  66 + return Security.verify(key, signedData, signature);
  67 + }
  68 +
  69 + /**
  70 + * Generates a PublicKey instance from a string containing the
  71 + * Base64-encoded public key.
  72 + *
  73 + * @param encodedPublicKey Base64-encoded public key
  74 + * @throws IllegalArgumentException if encodedPublicKey is invalid
  75 + */
  76 + public static PublicKey generatePublicKey(String encodedPublicKey) {
  77 + try {
  78 + byte[] decodedKey = Base64.decode(encodedPublicKey);
  79 + KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
  80 + return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
  81 + } catch (NoSuchAlgorithmException e) {
  82 + throw new RuntimeException(e);
  83 + } catch (InvalidKeySpecException e) {
  84 + Log.e(TAG, "Invalid key specification.");
  85 + throw new IllegalArgumentException(e);
  86 + } catch (Base64DecoderException e) {
  87 + Log.e(TAG, "Base64 decoding failed.");
  88 + throw new IllegalArgumentException(e);
  89 + }
  90 + }
  91 +
  92 + /**
  93 + * Verifies that the signature from the server matches the computed
  94 + * signature on the data. Returns true if the data is correctly signed.
  95 + *
  96 + * @param publicKey public key associated with the developer account
  97 + * @param signedData signed data from server
  98 + * @param signature server signature
  99 + * @return true if the data and signature match
  100 + */
  101 + public static boolean verify(PublicKey publicKey, String signedData, String signature) {
  102 + Signature sig;
  103 + try {
  104 + sig = Signature.getInstance(SIGNATURE_ALGORITHM);
  105 + sig.initVerify(publicKey);
  106 + sig.update(signedData.getBytes());
  107 + if (!sig.verify(Base64.decode(signature))) {
  108 + Log.e(TAG, "Signature verification failed.");
  109 + return false;
  110 + }
  111 + return true;
  112 + } catch (NoSuchAlgorithmException e) {
  113 + Log.e(TAG, "NoSuchAlgorithmException.");
  114 + } catch (InvalidKeyException e) {
  115 + Log.e(TAG, "Invalid key specification.");
  116 + } catch (SignatureException e) {
  117 + Log.e(TAG, "Signature exception.");
  118 + } catch (Base64DecoderException e) {
  119 + Log.e(TAG, "Base64 decoding failed.");
  120 + }
  121 + return false;
  122 + }
  123 +}
... ...
app/src/main/java/net/devfac/userstory/Utils/iab/SkuDetails.java 0 → 100644
  1 +/* Copyright (c) 2012 Google Inc.
  2 + *
  3 + * Licensed under the Apache License, Version 2.0 (the "License");
  4 + * you may not use this file except in compliance with the License.
  5 + * You may obtain a copy of the License at
  6 + *
  7 + * http://www.apache.org/licenses/LICENSE-2.0
  8 + *
  9 + * Unless required by applicable law or agreed to in writing, software
  10 + * distributed under the License is distributed on an "AS IS" BASIS,
  11 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12 + * See the License for the specific language governing permissions and
  13 + * limitations under the License.
  14 + */
  15 +
  16 +package net.devfac.userstory.Utils.iab;
  17 +
  18 +import org.json.JSONException;
  19 +import org.json.JSONObject;
  20 +
  21 +/**
  22 + * Represents an in-app product's listing details.
  23 + */
  24 +public class SkuDetails {
  25 + String mItemType;
  26 + String mSku;
  27 + String mType;
  28 + String mPrice;
  29 + String mTitle;
  30 + String mDescription;
  31 + String mJson;
  32 +
  33 + public SkuDetails(String jsonSkuDetails) throws JSONException {
  34 + this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails);
  35 + }
  36 +
  37 + public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException {
  38 + mItemType = itemType;
  39 + mJson = jsonSkuDetails;
  40 + JSONObject o = new JSONObject(mJson);
  41 + mSku = o.optString("productId");
  42 + mType = o.optString("type");
  43 + mPrice = o.optString("price");
  44 + mTitle = o.optString("title");
  45 + mDescription = o.optString("description");
  46 + }
  47 +
  48 + public String getSku() { return mSku; }
  49 + public String getType() { return mType; }
  50 + public String getPrice() { return mPrice; }
  51 + public String getTitle() { return mTitle; }
  52 + public String getDescription() { return mDescription; }
  53 +
  54 + @Override
  55 + public String toString() {
  56 + return "SkuDetails:" + mJson;
  57 + }
  58 +}
... ...
app/src/main/res/layout/activity_about.xml 0 → 100644
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3 + android:orientation="vertical" android:layout_width="match_parent"
  4 + android:layout_height="match_parent">
  5 +
  6 + <Button
  7 + android:layout_width="wrap_content"
  8 + android:layout_height="wrap_content"
  9 + android:text="New Button"
  10 + android:id="@+id/btn_about_iab_test"
  11 + android:layout_gravity="center_horizontal" />
  12 +</LinearLayout>
0 13 \ No newline at end of file
... ...